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:
- 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}}.
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_hostname → SYSLOG_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.
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
sourceset to the literal, unrendered string:instead of the configured
hostname(e.g.homeassistant). The previous add-on version 1.11.0 works correctly.bertbaron/hassio-addons, add-onlogspout, version1.12.0.bertbaron/logspout(the add-onDockerfilebuilds this repo viaLOGSPOUT_SOURCE=https://github.com/bertbaron/logspout.git, building./cmd/launcher).User-visible symptom
In Graylog, the
sourcefield (which comes from the GELFhostfield) is the literal template string{{.Container.Config.Hostname}}for all messages, rather than the host name set via the add-on'shostname: homeassistantoption. This makes it impossible to filter/group logs by source.Root cause: init-order vs
os.SetenvIn
adapters/gelf/gelf.go, the GELF host was resolved at packageinit()time:The native launcher (
launcher/launcher.go,RunWithRunner) reads the add-onhostnameoption and applies it to the environment insidemain():Go runs all package
init()functions beforemain(). So the ordering is:gelfpackageinit()runsgetHostname().SYSLOG_HOSTNAMEis not yet set (the launcher sets it later inmain())./etc/host_hostnameno longer exists — the old shell wrapper that used to create/export this was removed in the 1.12.0 rewrite.{{.Container.Config.Hostname}}.main()runs the launcher, which callsos.Setenv("SYSLOG_HOSTNAME", "homeassistant")— but it is too late, the package-levelhostnamevariable 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 thegelfinit()saw the correct value. The Go-launcher rewrite moved that work from "before process start" to "insidemain()", which is after package init — introducing this regression.Why config /
envworkarounds don't helpYou cannot work around this from the add-on configuration:
hostname: homeassistantis exactly what's broken — the launcher does setSYSLOG_HOSTNAMEfrom it, but only inmain(), afterinit()already ran.SYSLOG_HOSTNAMEvia the add-on's customenvlist does not help either: the launcher applies those with the sameos.Setenvloop inmain(), again afterinit(). (AndSYSLOG_HOSTNAMEis 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.Setenvhas run — rather than at packageinit(). For example, resolve it insideNewGelfAdapter(called when routes are set up, aftermain()), keeping the existing precedence/etc/host_hostname->SYSLOG_HOSTNAME-> literal default, and keeping theinit()adapter-factory registration.Environment
bertbaron/hassio-addons,logspout/): 1.12.0 (broken), 1.11.0 (works).Proposed fix (implemented & tested)
I have a minimal, behavior-preserving patch that makes
getHostname()a pure function and resolves the hostname inNewGelfAdapter(per-adapter, after the launcher'sos.Setenv) instead of at packageinit(). Lookup precedence (/etc/host_hostname→SYSLOG_HOSTNAME→ literal default) is unchanged.Plus two regression tests in
adapters/gelf/gelf_test.gothat setSYSLOG_HOSTNAMEafter import and assert it is picked up (these fail against the oldinit()-based code).go test ./...passes;gofmtandgo vetare clean.Happy to open a PR for this if you're open to it.