From f72d2e755f9342acf5476e3a383fa9ad83c492ed Mon Sep 17 00:00:00 2001 From: Aaron Ten Clay Date: Wed, 3 Jun 2026 08:05:15 -0700 Subject: [PATCH] fix: resolve GELF hostname at adapter creation, not package init The HA launcher sets SYSLOG_HOSTNAME inside main(), which runs after all package init() functions. Reading the hostname in gelf.init() froze it to the "{{.Container.Config.Hostname}}" fallback before the launcher could apply the configured value, so that literal template string was shipped as the GELF source for every message. Resolve the hostname in NewGelfAdapter (called by runner.Run, after the launcher populates the env) and store it per-adapter. Worked before the in-process launcher because the old shell wrapper exported SYSLOG_HOSTNAME and exec'd logspout as a separate process. --- adapters/gelf/gelf.go | 26 ++++++++++++----------- adapters/gelf/gelf_test.go | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/adapters/gelf/gelf.go b/adapters/gelf/gelf.go index 3aa37191..623987c3 100644 --- a/adapters/gelf/gelf.go +++ b/adapters/gelf/gelf.go @@ -15,27 +15,28 @@ import ( "github.com/gliderlabs/logspout/router" ) -var hostname string - +// getHostname resolves the GELF source hostname. It must be called when an +// adapter is created (NewGelfAdapter) rather than at package init: the Home +// Assistant launcher sets SYSLOG_HOSTNAME inside main(), which runs after all +// package init() functions, so reading it at init time would always miss the +// configured value and fall back to the default below. func getHostname() string { content, err := os.ReadFile("/etc/host_hostname") if err == nil && len(content) > 0 { - hostname = strings.TrimRight(string(content), "\r\n") - } else { - hostname = cfg.GetEnvDefault("SYSLOG_HOSTNAME", "{{.Container.Config.Hostname}}") + return strings.TrimRight(string(content), "\r\n") } - return hostname + return cfg.GetEnvDefault("SYSLOG_HOSTNAME", "{{.Container.Config.Hostname}}") } func init() { - hostname = getHostname() router.AdapterFactories.Register(NewGelfAdapter, "gelf") } // Adapter is an adapter that streams UDP JSON to Graylog type Adapter struct { - writer gelf.Writer - route *router.Route + writer gelf.Writer + route *router.Route + hostname string } // NewGelfAdapter creates an Adapter with UDP as the default transport. @@ -46,8 +47,9 @@ func NewGelfAdapter(route *router.Route) (router.LogAdapter, error) { } return &Adapter{ - route: route, - writer: gelfWriter, + route: route, + writer: gelfWriter, + hostname: getHostname(), }, nil } @@ -84,7 +86,7 @@ func (a *Adapter) Stream(logstream chan *router.Message) { msg := gelf.Message{ Version: "1.1", - Host: hostname, + Host: a.hostname, Short: m.Data, TimeUnix: float64(m.Time.UnixNano()/int64(time.Millisecond)) / 1000.0, Level: int32(level), diff --git a/adapters/gelf/gelf_test.go b/adapters/gelf/gelf_test.go index 3f7711c3..1ddc9c5e 100644 --- a/adapters/gelf/gelf_test.go +++ b/adapters/gelf/gelf_test.go @@ -188,3 +188,46 @@ func TestGelfNewAdapterUnknownTransport(t *testing.T) { t.Error("expected error for unknown transport, got nil") } } + +// TestGelfAdapterResolvesHostnameAtCreation is the regression test for the +// init-order bug. The Home Assistant launcher sets SYSLOG_HOSTNAME inside +// main(), which Go runs AFTER every package init(). Resolving the hostname in +// init() (the old behavior) froze it to the "{{.Container.Config.Hostname}}" +// default before the launcher could set the configured value, so that literal +// template string was shipped as the GELF source. The adapter must read +// SYSLOG_HOSTNAME when it is created instead. +func TestGelfAdapterResolvesHostnameAtCreation(t *testing.T) { + t.Setenv("SYSLOG_HOSTNAME", "myhost.example.com") + + route := &router.Route{Address: "127.0.0.1:12201"} // default udp transport + adapter, err := NewGelfAdapter(route) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + got := adapter.(*Adapter).hostname + if got != "myhost.example.com" { + t.Fatalf("expected hostname resolved from SYSLOG_HOSTNAME at creation, got %q", got) + } +} + +// TestGelfStreamUsesAdapterHostname verifies the per-adapter hostname is stamped +// on every emitted message's Host (the GELF source) field. +func TestGelfStreamUsesAdapterHostname(t *testing.T) { + mock := &mockGelfWriter{} + adapter := &Adapter{writer: mock, hostname: "myhost.example.com"} + + streamAndWait(adapter, &router.Message{ + Container: newTestContainer(), + Data: "hello world", + Source: "stdout", + Time: time.Now(), + }) + + if len(mock.messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(mock.messages)) + } + if mock.messages[0].Host != "myhost.example.com" { + t.Errorf("expected Host='myhost.example.com', got '%s'", mock.messages[0].Host) + } +}