Observe Typed Config Sections¶
Use config.ObserveSection when a long-lived component needs settings from GTB
config but the reusable package should not depend on pkg/config.
The pattern is:
- Define a package-owned settings struct.
- Let the package accept that struct, or a tiny settings-source interface.
- In GTB adapter code, bind the config section with
ObserveSection. - Reconfigure from
SectionChange.Currentonly when the typed struct changes.
Define Package Settings¶
type ServerSettings struct {
Port int `mapstructure:"port"`
MaxHeaderBytes int `mapstructure:"max_header_bytes"`
}
type SettingsSource interface {
Current() *ServerSettings
Version() uint64
}
The package can accept SettingsSource without importing pkg/config.
*config.ObservedSection[ServerSettings] satisfies that shape from the GTB
adapter layer.
Bind the Config Section¶
settings, err := config.ObserveSection[ServerSettings](
props.Config,
"server.http",
config.WithSectionDefaults(defaultServerSettings(), mergeServerSettings),
config.WithSectionValidator(func(next ServerSettings) error {
return next.Validate()
}),
config.WithSectionApply(func(change config.SectionChange[ServerSettings]) error {
return server.Reconfigure(&change.Current.Value)
}),
)
if err != nil {
return err
}
server.SetSettingsSource(settings)
ObserveSection performs the initial unmarshal, registers an observer, validates
fresh snapshots on successful reloads, and publishes immutable snapshots through
Current.
React Only to Real Changes¶
The binding compares the previous and current typed section as whole structs.
Unrelated config reloads do not increment Version and do not call
WithSectionApply.
When the section changes, the callback receives:
| Field | Meaning |
|---|---|
Previous |
Last valid typed section snapshot. |
Current |
New validated typed section snapshot. |
Changed |
True when the binding published a new version. |
Version |
Monotonic version after the change was published. |
Packages should reconfigure from change.Current.Value instead of watching
individual config keys.
Custom Equality¶
Use WithSectionEqual when a package needs to ignore derived or operationally
irrelevant fields:
config.WithSectionEqual(func(previous, current config.Section[ServerSettings]) bool {
return previous.Exists == current.Exists &&
previous.Value.Port == current.Value.Port &&
previous.Value.MaxHeaderBytes == current.Value.MaxHeaderBytes
})
Keep the equality function package-owned and based on complete settings semantics, not scattered call-site field checks.
Dynamic Defaults¶
Use WithSectionDefaultFunc when defaults come from another config location and
must rehydrate on reload too. For example, a transport-specific port may fall
back to a shared server.port key:
config.WithSectionDefaultFunc(func(cfg config.Observed) ServerSettings {
return ServerSettings{Port: cfg.GetInt("server.port")}
}, mergeServerSettings)
The default function runs during the initial bind and on every successful reload before equality is checked.
HTTP Server Settings¶
pkg/http provides a package-specific helper for its server settings:
settings, err := gtbhttp.ObserveServerSettingsFromConfig(
cfg,
"server.http",
config.WithSectionApply(func(change config.SectionChange[gtbhttp.ServerSettings]) error {
log.Info("http settings changed", "version", change.Version)
return nil
}),
)
if err != nil {
return err
}
var source gtbhttp.ServerSettingsSource = settings
_ = source.Current()
The helper keeps the existing GTB config shape intact. It reads
server.http.*, keeps server.port as the shared port fallback, and rehydrates
that fallback on reload. The returned source exposes Version() so code can
quickly tell whether the whole typed settings snapshot has changed.
gRPC Server Settings¶
pkg/grpc provides the same observer pattern for its server settings:
settings, err := gtbgrpc.ObserveServerSettingsFromConfig(
cfg,
"server.grpc",
config.WithSectionApply(func(change config.SectionChange[gtbgrpc.ServerSettings]) error {
log.Info("grpc settings changed", "version", change.Version)
return nil
}),
)
if err != nil {
return err
}
var source gtbgrpc.ServerSettingsSource = settings
_ = source.Current()
The helper reads server.grpc.*, keeps server.port as the shared port
fallback, and rehydrates that fallback on reload. A version change tells the
owning package that the full typed snapshot changed; existing listeners still
need explicit restart or reconfigure logic before a port change takes effect.
Gateway Settings¶
pkg/gateway observes a composed settings shape because it owns an HTTP server
and dials the local gRPC server:
settings, err := gateway.ObserveSettingsFromConfig(
cfg,
config.WithSectionApply(func(change config.SectionChange[gateway.Settings]) error {
log.Info("gateway settings changed", "version", change.Version)
return nil
}),
)
if err != nil {
return err
}
var source gateway.SettingsSource = settings
_ = source.Current()
The helper composes server.gateway.* and server.gateway.tls.* with
server.grpc.* and server.grpc.tls.*. Existing gateway servers and client
connections still need explicit restart or redial logic before changed ports or
TLS paths affect live traffic.
OTel Signal Settings¶
pkg/telemetry observes resolved settings for one OTel signal at a time, over the
typed Settings from the standalone
go/observability/otelcore module:
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/telemetry"
"gitlab.com/phpboyscout/go/observability/otelcore"
)
settings, err := telemetry.ObserveSettingsFromConfig(
cfg,
otelcore.SignalTracing,
config.WithSectionApply(func(change config.SectionChange[otelcore.Settings]) error {
log.Info("otel tracing settings changed", "version", change.Version)
return nil
}),
)
if err != nil {
return err
}
var source otelcore.SettingsSource = settings
_ = source.Current()
The helper preserves the existing telemetry.* shape. Shared keys such as
telemetry.endpoint, telemetry.headers and telemetry.insecure are resolved
first, then telemetry.<signal>.* overrides are applied. On reload, a version
change means the full resolved signal snapshot changed. Existing exporters do
not rebuild automatically; the owning package must decide whether to rebuild,
restart, or keep using the current provider.
Failure Behaviour¶
If a reload cannot be decoded or validation fails, the binding keeps the last valid snapshot:
Currentcontinues to return the previous settings.Versiondoes not increment.WithSectionApplyis not called.- The observer returns the decode or validation error so the container logs it.
This mirrors GTB's fail-closed config reload behaviour while keeping package constructors independent of the framework config container.