Wire tracing into the composition root
Setup after SetLogger, wrapped rest configs (manager + kubernetes provider), tracing-outermost provider decorators, and an explicit trace flush after mgr.Start returns (os.Exit skips defers). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
40
cmd/main.go
40
cmd/main.go
@@ -58,6 +58,7 @@ import (
|
|||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/gcp"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/gcp"
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/kubernetes"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/kubernetes"
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/registry"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/registry"
|
||||||
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/tracing"
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version"
|
||||||
// +kubebuilder:scaffold:imports
|
// +kubebuilder:scaffold:imports
|
||||||
)
|
)
|
||||||
@@ -148,6 +149,17 @@ func main() {
|
|||||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||||
setupLog.Info("Starting egress-proxies-operator",
|
setupLog.Info("Starting egress-proxies-operator",
|
||||||
"commit", version.Resolve(), "goVersion", goruntime.Version())
|
"commit", version.Resolve(), "goVersion", goruntime.Version())
|
||||||
|
|
||||||
|
// Off (no-op spans, unchanged logs) unless OTEL_* env opts in; see
|
||||||
|
// internal/tracing. Shutdown is called explicitly after mgr.Start
|
||||||
|
// returns — the os.Exit paths below skip defers.
|
||||||
|
tracingShutdown, err := tracing.Setup(context.Background(), setupLog,
|
||||||
|
"egress-proxies-operator", version.Resolve())
|
||||||
|
if err != nil {
|
||||||
|
setupLog.Error(err, "Failed to set up tracing")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := ctrl.SetupSignalHandler()
|
ctx := ctrl.SetupSignalHandler()
|
||||||
|
|
||||||
// Providers load first and fail fast: a manager that comes up without
|
// Providers load first and fail fast: a manager that comes up without
|
||||||
@@ -162,7 +174,11 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
providers, err := registry.Build(ctx, cfg, map[string]registry.Constructor{
|
providers, err := registry.Build(ctx, cfg, map[string]registry.Constructor{
|
||||||
"kubernetes": kubernetes.New,
|
"kubernetes": func(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
|
||||||
|
// The kubernetes provider builds its own uncached client;
|
||||||
|
// wrap its transport so its API calls join the caller's trace.
|
||||||
|
return kubernetes.NewWithTransportWrapper(ctx, pc, tracing.RestConfigWrapper())
|
||||||
|
},
|
||||||
"gcp": func(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
|
"gcp": func(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
|
||||||
return gcp.NewWithWireOptions(ctx, pc, gcp.WireLogOptions{FullPayloads: gcpWireFullPayloads})
|
return gcp.NewWithWireOptions(ctx, pc, gcp.WireLogOptions{FullPayloads: gcpWireFullPayloads})
|
||||||
},
|
},
|
||||||
@@ -173,7 +189,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
m := metrics.New()
|
m := metrics.New()
|
||||||
for name, p := range providers {
|
for name, p := range providers {
|
||||||
providers[name] = provider.WithMetrics(name, p, m)
|
// Tracing outermost: the span covers the metrics recording too.
|
||||||
|
providers[name] = provider.WithTracing(name, provider.WithMetrics(name, p, m))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||||
@@ -236,7 +253,9 @@ func main() {
|
|||||||
cacheOpts.DefaultNamespaces = map[string]cache.Config{proxyNamespace: {}}
|
cacheOpts.DefaultNamespaces = map[string]cache.Config{proxyNamespace: {}}
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
restCfg := ctrl.GetConfigOrDie()
|
||||||
|
restCfg.Wrap(tracing.RestConfigWrapper())
|
||||||
|
mgr, err := ctrl.NewManager(restCfg, ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Metrics: metricsServerOptions,
|
Metrics: metricsServerOptions,
|
||||||
HealthProbeBindAddress: probeAddr,
|
HealthProbeBindAddress: probeAddr,
|
||||||
@@ -323,8 +342,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupLog.Info("Starting manager")
|
setupLog.Info("Starting manager")
|
||||||
if err := mgr.Start(ctx); err != nil {
|
startErr := mgr.Start(ctx)
|
||||||
setupLog.Error(err, "Failed to run manager")
|
|
||||||
|
// Flush pending spans on the way out, error path included. Fresh
|
||||||
|
// context: the signal ctx is already cancelled by the time Start
|
||||||
|
// returns.
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
if err := tracingShutdown(shutdownCtx); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to flush traces on shutdown")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if startErr != nil {
|
||||||
|
setupLog.Error(startErr, "Failed to run manager")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Plan: `docs/plans/2026-08-24-1025-otel-tracing.md`
|
|||||||
- [x] Step 1 — Dependencies
|
- [x] Step 1 — Dependencies
|
||||||
- [x] Step 2 — New package `internal/tracing`
|
- [x] Step 2 — New package `internal/tracing`
|
||||||
- [x] Step 3 — `provider.WithTracing` decorator
|
- [x] Step 3 — `provider.WithTracing` decorator
|
||||||
- [ ] Step 4 — `cmd/main.go` wiring
|
- [x] Step 4 — `cmd/main.go` wiring
|
||||||
- [ ] Step 5 — Reconciler spans
|
- [ ] Step 5 — Reconciler spans
|
||||||
- [ ] Step 6 — Discovery server
|
- [ ] Step 6 — Discovery server
|
||||||
- [ ] Step 7 — GC + health
|
- [ ] Step 7 — GC + health
|
||||||
@@ -81,3 +81,22 @@ One small API addition to `internal/tracing` for this: exported
|
|||||||
and couldn't reach the unexported option resolver. The `cmd/main.go` wiring
|
and couldn't reach the unexported option resolver. The `cmd/main.go` wiring
|
||||||
(`WithTracing` outermost around `WithMetrics`) lands with Step 4's commit —
|
(`WithTracing` outermost around `WithMetrics`) lands with Step 4's commit —
|
||||||
same file, one commit.
|
same file, one commit.
|
||||||
|
|
||||||
|
## Step 4 — `cmd/main.go` wiring
|
||||||
|
|
||||||
|
As planned: `tracing.Setup` right after `ctrl.SetLogger`; manager rest.Config
|
||||||
|
wrapped via `restCfg := ctrl.GetConfigOrDie(); restCfg.Wrap(...)` (the
|
||||||
|
previous inline call left nowhere to wrap); providers decorated tracing-
|
||||||
|
outermost; explicit trace flush after `mgr.Start` returns on both the error
|
||||||
|
and clean paths, with a fresh 10s context since the signal ctx is already
|
||||||
|
cancelled by then.
|
||||||
|
|
||||||
|
The kubernetes provider grew `NewWithTransportWrapper(ctx, cfg, wrap)` (its
|
||||||
|
client is built from its own `ctrl.GetConfig()`, invisible to the manager's
|
||||||
|
wrapped config); `New` now delegates with a nil wrapper, so `newWithClient`
|
||||||
|
tests stayed untouched, and main registers a closure — the same pattern the
|
||||||
|
gcp constructor already used for wire-log options.
|
||||||
|
|
||||||
|
Deviation: the plan put `--trace-health-probes` here, but the flag needs the
|
||||||
|
`health.Engine.TraceProbes` field that Step 7 introduces — moved there to
|
||||||
|
keep every commit compiling.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
@@ -39,11 +40,22 @@ type Provider struct {
|
|||||||
|
|
||||||
// New builds a kubernetes Provider from its config block. Satisfies
|
// New builds a kubernetes Provider from its config block. Satisfies
|
||||||
// registry.Constructor.
|
// registry.Constructor.
|
||||||
func New(_ context.Context, cfg provider.ProviderConfig) (provider.Provider, error) {
|
func New(ctx context.Context, cfg provider.ProviderConfig) (provider.Provider, error) {
|
||||||
|
return NewWithTransportWrapper(ctx, cfg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithTransportWrapper is New with an optional transport wrapper applied
|
||||||
|
// to the provider's own rest.Config (this client is built independently of
|
||||||
|
// the manager's, so the composition root must wrap it separately for
|
||||||
|
// tracing). A nil wrapper means a plain client.
|
||||||
|
func NewWithTransportWrapper(_ context.Context, cfg provider.ProviderConfig, wrap transport.WrapperFunc) (provider.Provider, error) {
|
||||||
restCfg, err := ctrl.GetConfig()
|
restCfg, err := ctrl.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("kubernetes provider %q: loading kubeconfig: %w", cfg.Name, err)
|
return nil, fmt.Errorf("kubernetes provider %q: loading kubeconfig: %w", cfg.Name, err)
|
||||||
}
|
}
|
||||||
|
if wrap != nil {
|
||||||
|
restCfg.Wrap(wrap)
|
||||||
|
}
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
if err := corev1.AddToScheme(scheme); err != nil {
|
if err := corev1.AddToScheme(scheme); err != nil {
|
||||||
return nil, fmt.Errorf("kubernetes provider %q: %w", cfg.Name, err)
|
return nil, fmt.Errorf("kubernetes provider %q: %w", cfg.Name, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user