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) + } +}