Skip to content

GELF source is the literal {{.Container.Config.Hostname}} since add-on 1.12.0 (hostname resolved at package init, before the launcher sets SYSLOG_HOSTNAME) #104

Description

@john120283

Summary

Since the Logspout Home Assistant add-on 1.12.0 (the version that "replaced the old shell-based startup wrapper with a native Home Assistant launcher"), every log forwarded to Graylog arrives with its source set to the literal, unrendered string:

{{.Container.Config.Hostname}}

instead of the configured hostname (e.g. homeassistant). The previous add-on version 1.11.0 works correctly.

  • Symptom is observed in: bertbaron/hassio-addons, add-on logspout, version 1.12.0.
  • Root cause / code fix belongs in: bertbaron/logspout (the add-on Dockerfile builds this repo via LOGSPOUT_SOURCE=https://github.com/bertbaron/logspout.git, building ./cmd/launcher).

User-visible symptom

In Graylog, the source field (which comes from the GELF host field) is the literal template string {{.Container.Config.Hostname}} for all messages, rather than the host name set via the add-on's hostname: homeassistant option. This makes it impossible to filter/group logs by source.

Root cause: init-order vs os.Setenv

In adapters/gelf/gelf.go, the GELF host was resolved at package init() time:

var hostname string

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 hostname
}

func init() {
    hostname = getHostname()                       // runs at package init, before main()
    router.AdapterFactories.Register(NewGelfAdapter, "gelf")
}

The native launcher (launcher/launcher.go, RunWithRunner) reads the add-on hostname option and applies it to the environment inside main():

env := BuildEnvironment(config, ...) // includes "SYSLOG_HOSTNAME": config.Hostname
...
for key, value := range env {
    os.Setenv(key, value)   // happens in main(), AFTER gelf's init() already read SYSLOG_HOSTNAME
}

Go runs all package init() functions before main(). So the ordering is:

  1. The gelf package init() runs getHostname().
    • SYSLOG_HOSTNAME is not yet set (the launcher sets it later in main()).
    • /etc/host_hostname no longer exists — the old shell wrapper that used to create/export this was removed in the 1.12.0 rewrite.
    • So it falls back to the literal default {{.Container.Config.Hostname}}.
  2. main() runs the launcher, which calls os.Setenv("SYSLOG_HOSTNAME", "homeassistant") — but it is too late, the package-level hostname variable was already frozen to the literal default.

In 1.11.0 the bash wrapper exported SYSLOG_HOSTNAME (and/or wrote /etc/host_hostname) before the binary started, so the gelf init() saw the correct value. The Go-launcher rewrite moved that work from "before process start" to "inside main()", which is after package init — introducing this regression.

Why config / env workarounds don't help

You cannot work around this from the add-on configuration:

  • Setting hostname: homeassistant is exactly what's broken — the launcher does set SYSLOG_HOSTNAME from it, but only in main(), after init() already ran.
  • Adding SYSLOG_HOSTNAME via the add-on's custom env list does not help either: the launcher applies those with the same os.Setenv loop in main(), again after init(). (And SYSLOG_HOSTNAME is a reserved managed key in the launcher anyway.)

The only correct fix is to stop resolving the hostname at package init() time.

Suggested fix

Resolve the hostname lazily — after the launcher's os.Setenv has run — rather than at package init(). For example, resolve it inside NewGelfAdapter (called when routes are set up, after main()), keeping the existing precedence /etc/host_hostname -> SYSLOG_HOSTNAME -> literal default, and keeping the init() adapter-factory registration.

Environment

  • Logspout HA add-on (bertbaron/hassio-addons, logspout/): 1.12.0 (broken), 1.11.0 (works).
  • Forwarder: Graylog GELF (UDP/TCP/TLS).

Proposed fix (implemented & tested)

I have a minimal, behavior-preserving patch that makes getHostname() a pure function and resolves the hostname in NewGelfAdapter (per-adapter, after the launcher's os.Setenv) instead of at package init(). Lookup precedence (/etc/host_hostnameSYSLOG_HOSTNAME → literal default) is unchanged.

--- a/adapters/gelf/gelf.go
+++ b/adapters/gelf/gelf.go
@@
-var hostname string
-
+// getHostname resolves the GELF host field, preferring the /etc/host_hostname
+// file, then the SYSLOG_HOSTNAME environment variable, then a literal default.
+// It must be called lazily (e.g. when an adapter is created) rather than at
+// package init() time, so that any launcher that sets SYSLOG_HOSTNAME via
+// os.Setenv during main() has already run.
 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.
@@ func NewGelfAdapter(route *router.Route) (router.LogAdapter, error) {
 	return &Adapter{
-		route:  route,
-		writer: gelfWriter,
+		route:    route,
+		writer:   gelfWriter,
+		hostname: getHostname(),
 	}, nil
 }
@@ func (a *Adapter) Stream(logstream chan *router.Message) {
 		msg := gelf.Message{
 			Version:  "1.1",
-			Host:     hostname,
+			Host:     a.hostname,

Plus two regression tests in adapters/gelf/gelf_test.go that set SYSLOG_HOSTNAME after import and assert it is picked up (these fail against the old init()-based code). go test ./... passes; gofmt and go vet are clean.

Happy to open a PR for this if you're open to it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions