Skip to content

fix: resolve GELF hostname at adapter creation, not package init - #5

Open
aarontc wants to merge 1 commit into
bertbaron:masterfrom
aarontc:fix/gelf-hostname-init-order
Open

fix: resolve GELF hostname at adapter creation, not package init#5
aarontc wants to merge 1 commit into
bertbaron:masterfrom
aarontc:fix/gelf-hostname-init-order

Conversation

@aarontc

@aarontc aarontc commented Jun 3, 2026

Copy link
Copy Markdown

fix: resolve GELF hostname at adapter creation, not package init

Summary

Since the native Home Assistant launcher was introduced (the change that
"directly launch[es] logspout and parse[s] the config from there instead of
using a shell wrapper", HA app v1.12.0), the GELF adapter ships the literal,
unrendered string {{.Container.Config.Hostname}} as the message host
(Graylog source) field, ignoring the configured hostname option.

This breaks any downstream tooling that filters by source (e.g. Graylog
searches/streams scoped to a host), because every container from the host now
shares the bogus source {{.Container.Config.Hostname}} instead of the
configured value.

Root cause: Go package-init ordering

adapters/gelf/gelf.go resolved the hostname in package init() and cached
it in a package global:

var hostname string

func init() {
    hostname = getHostname()                 // reads SYSLOG_HOSTNAME / /etc/host_hostname
    router.AdapterFactories.Register(NewGelfAdapter, "gelf")
}
// ...
msg := gelf.Message{ /* ... */ Host: hostname }

The native launcher sets SYSLOG_HOSTNAME from the addon config inside
main()
(launcher.BuildEnvironmentos.Setenv). Go runs every package
init() before main()
, and cmd/launcher imports the adapters
transitively (modulesadapters/gelf). So the ordering is:

  1. gelf.init() runs → SYSLOG_HOSTNAME is still unset and /etc/host_hostname
    is absent → the global is frozen to the fallback default
    "{{.Container.Config.Hostname}}".
  2. main()launcher.Run()os.Setenv("SYSLOG_HOSTNAME", <configured>)
    too late; the global was already set.
  3. Stream() stamps the frozen literal on every message.

It worked before the native launcher because the old shell wrapper exported
SYSLOG_HOSTNAME and then exec'd logspout as a separate process, so the
environment was already in place when init() ran. Moving the launcher
in-process exposed the latent init-time read.

(The default value "{{.Container.Config.Hostname}}" is a Go text/template
that the upstream adapter this is "based on"
micahhausler/logspout-gelf
renders per message; this fork uses it as a raw string. That's why the literal
template text appears rather than a rendered hostname.)

Fix

Resolve the hostname when the adapter is created (NewGelfAdapter, invoked
by runner.Run after the launcher has populated the environment) and store it
on the Adapter, instead of reading it in init():

func getHostname() string {
    if content, err := os.ReadFile("/etc/host_hostname"); err == nil && len(content) > 0 {
        return strings.TrimRight(string(content), "\r\n")
    }
    return cfg.GetEnvDefault("SYSLOG_HOSTNAME", "{{.Container.Config.Hostname}}")
}

func init() {
    router.AdapterFactories.Register(NewGelfAdapter, "gelf")
}

type Adapter struct {
    writer   gelf.Writer
    route    *router.Route
    hostname string
}

func NewGelfAdapter(route *router.Route) (router.LogAdapter, error) {
    w, err := gelfWriter(route)
    if err != nil {
        return nil, err
    }
    return &Adapter{route: route, writer: w, hostname: getHostname()}, nil
}
// Stream(): Host: a.hostname

The launcher always sets SYSLOG_HOSTNAME (defaulting to homeassistant when
the option is empty), so the template-default branch is no longer reached in the
addon; the configured hostname now reaches Graylog as the source again.

Tests

Added two tests in adapters/gelf/gelf_test.go:

  • TestGelfAdapterResolvesHostnameAtCreation — sets SYSLOG_HOSTNAME
    after package init (simulating the launcher) and asserts NewGelfAdapter
    resolves it. This fails against the old init-time read and is the direct
    regression guard.
  • TestGelfStreamUsesAdapterHostname — asserts the per-adapter hostname is
    stamped on each emitted message's Host.

go test ./adapters/gelf/ ./launcher/ passes; go build ./cmd/launcher
succeeds; gofmt clean.

Optional follow-up (not in this PR)

To make the {{.Container.Config.Hostname}} default behave as intended for
non-addon users, render hostname as a text/template against each
router.Message (restoring the upstream micahhausler behavior). Happy to add
this if desired.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant