Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions adapters/gelf/gelf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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),
Expand Down
43 changes: 43 additions & 0 deletions adapters/gelf/gelf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}