feat(otel): logs are exported with otel now#1260
Conversation
|
For the logs/traces correlation follow-up: I dug into how Three ways to close that gap: 1. Bind the ctx at the span boundary (good starting point) ctx = logr.NewContext(ctx, klog.FromContext(ctx).WithValues(otelCtxKey, ctx))Every downstream
2. Pass ctx per call
3. Dedicated correlated-log emit path
What won't work, to save time: bumping For transparency, these three options were generated by Claude Opus 4.8. |
|
👋 Heads up — this pull request changes files owned by @aljesusg @josunect @ksimon1 @lyarwood @mjudeikis. You are listed as an owner of one or more of the changed areas in |
|
@manusa what I went for is switching |
manusa
left a comment
There was a problem hiding this comment.
Review summary
Thanks for this. The log export works and the trace/log correlation is real: otelCtx satisfies context.Context, otellogr extracts the active span, and the SDK stamps the trace/span IDs onto the record (exercised by TestOtelLogRecordCarriesTraceContext). The migration to klogutil.FromContext is complete, go build/go vet are clean, and the new pkg/logging/pkg/telemetry tests pass race-clean. The otelCtx placeholder is also a good safety net: I checked both branches and no bearer token or header can leak into either the text logs or the exported OTel attributes.
Two things I think are worth addressing before merge, both left inline:
- The context strip never runs, so every text log line now carries
ctx="<otel-ctx>", including the default telemetry-off config. There is a failing test and a small validated fix in the inline comment onpkg/klogutil/util.go. - There is no
OTEL_LOGS_EXPORTER=noneopt-out, so enabling telemetry now also ships the entire klog stream off-box. Inline onpkg/telemetry/logs.go.
I also have a few minor notes that are not blockers (the discarded NewLogProvider error, buffered records not flushed on stdio SIGTERM, a per-call allocation in FromContext on hot paths, and a couple of test-coverage gaps). Happy to add those if you want them, but I left them out to keep this focused.
| // placeholder instead of formatting the full context chain, which could | ||
| // contain sensitive data such as HTTP request headers. | ||
| func FromContext(ctx context.Context) klog.Logger { | ||
| return klog.FromContext(ctx).WithValues("ctx", otelCtx{ctx}) |
There was a problem hiding this comment.
The context strip this relies on never runs, so every text log line now carries ctx="<otel-ctx>" (including the default telemetry-off config).
logging.New wires klog with SetLoggerWithOptions(...) and no ContextualLogger(true), so klog.FromContext(ctx) returns klog's internal klogger, not the teeSink. The WithValues("ctx", otelCtx{ctx}) here lands on the klogger, which forwards "ctx" as a per-record key/value into teeSink.Info, and teeSink.Info passes it to the primary text sink unmodified. stripContextValues only runs inside teeSink.WithValues, which this path never calls, so stripContextValues/teeSink.WithValues are effectively dead code and the otelCtx placeholder is the only thing keeping the raw context out of the text output.
Observed (both telemetry off and on):
I0707 ... "hello world" ctx="<otel-ctx>" k="v"
It is not a data leak (the placeholder holds and otellogr drops the ctx from exported attributes), but it changes the default log format for everyone and will break exact-match log assertions and log parsers.
A test that showcases it (fails on the current branch, both subtests):
func (s *SinkSuite) TestKlogutilFromContextTextIsClean() {
s.Run("OTel disabled (default): text log carries no ctx field", func() {
path := filepath.Join(s.tempDir, "disabled.log")
sink, err := logging.New(&config.StaticConfig{LogLevel: 1, LogFile: path}, s.httpOut, s.errOut)
s.Require().NoError(err)
s.T().Cleanup(func() { _ = sink.Close() })
klogutil.FromContext(s.T().Context()).Info("default-line", "k", "v")
klog.Flush()
content, err := os.ReadFile(path)
s.Require().NoError(err)
s.NotContains(string(content), "otel-ctx", "text log must not carry the correlation context")
})
s.Run("OTel enabled: text log clean, OTel record still correlates", func() {
recorder := logtest.NewRecorder()
otelSink := telemetry.NewLogSink("svc", "1.0.0", recorder)
path := filepath.Join(s.tempDir, "enabled.log")
sink, err := logging.New(&config.StaticConfig{LogLevel: 1, LogFile: path}, s.httpOut, s.errOut, logging.WithOtelLogSink(otelSink, nil))
s.Require().NoError(err)
s.T().Cleanup(func() { _ = sink.Close() })
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
s.T().Cleanup(func() { _ = tp.Shutdown(s.T().Context()) })
ctx, span := tp.Tracer("test").Start(s.T().Context(), "span")
defer span.End()
klogutil.FromContext(ctx).Info("enabled-line", "k", "v")
klog.Flush()
content, err := os.ReadFile(path)
s.Require().NoError(err)
s.NotContains(string(content), "otel-ctx")
var records []logtest.Record
for _, recs := range recorder.Result() {
records = append(records, recs...)
}
s.Require().NotEmpty(records)
s.True(trace.SpanContextFromContext(records[0].Context).HasTraceID(), "OTel record must still carry the trace context")
})
}Heads up that the fix is not simply flipping ContextualLogger(true): that routes Background() through the teeSink whose Enabled() is always-true (textlogger Verbosity(9) + otellogr), which breaks -v gating. A small fix that keeps correlation, cleans the text branch, and also drops the per-call allocation in the default path:
// pkg/logging/tee_sink.go
func (t *teeSink) Info(level int, msg string, keysAndValues ...any) {
if t.primary.Enabled(level) {
- t.primary.Info(level, msg, keysAndValues...)
+ t.primary.Info(level, msg, stripContextValues(keysAndValues)...)
}
if t.secondary.Enabled(level) {
t.secondary.Info(level, msg, keysAndValues...)
}
}
func (t *teeSink) Error(err error, msg string, keysAndValues ...any) {
- t.primary.Error(err, msg, keysAndValues...)
+ t.primary.Error(err, msg, stripContextValues(keysAndValues)...)
t.secondary.Error(err, msg, keysAndValues...)
} // pkg/klogutil/util.go
+var otelLogSinkActive atomic.Bool
+
+// SetOtelLogSinkActive records whether the OTel log bridge is active.
+func SetOtelLogSinkActive(active bool) { otelLogSinkActive.Store(active) }
+
func FromContext(ctx context.Context) klog.Logger {
- return klog.FromContext(ctx).WithValues("ctx", otelCtx{ctx})
+ logger := klog.FromContext(ctx)
+ if !otelLogSinkActive.Load() {
+ return logger
+ }
+ return logger.WithValues("ctx", otelCtx{ctx})
} // pkg/logging/sink.go (in New, where the sink is wired)
if o.otelSink != nil {
klog.SetLoggerWithOptions(logr.New(&teeSink{primary: textLogger.GetSink(), secondary: o.otelSink}))
s.logProvider = o.otelProvider
+ klogutil.SetOtelLogSinkActive(true)
} else {
klog.SetLoggerWithOptions(textLogger)
+ klogutil.SetOtelLogSinkActive(false)
}With this, the showcase test passes and the existing pkg/logging/pkg/telemetry suites stay green (verified locally, race included).
|
|
||
| // createLogExporter creates an OTLP log exporter using the same endpoint and | ||
| // protocol configuration as traces and metrics. | ||
| func createLogExporter(ctx context.Context, cfg *config.TelemetryConfig) (sdklog.Exporter, error) { |
There was a problem hiding this comment.
No per-signal opt-out for logs.
createLogExporter does not check OTEL_LOGS_EXPORTER, unlike metrics which honors OTEL_METRICS_EXPORTER=none (pkg/metrics/otel_stats_collector.go). Because log export auto-enables whenever an endpoint is set (shared IsEnabled()), an operator who wants traces and metrics but not the log stream (the highest-volume and most sensitive signal) has no way to disable only logs short of turning telemetry off entirely.
Worth adding the standard OTEL_LOGS_EXPORTER=none escape hatch, or at least documenting that enabling telemetry now also ships all klog output off-box.
manusa
left a comment
There was a problem hiding this comment.
LGTM. thx!
Unfortunately there are some merge conflicts, I think they should be easy to deal with in a rebase.
Signed-off-by: Calum Murray <cmurray@redhat.com>
Signed-off-by: Calum Murray <cmurray@redhat.com>
Signed-off-by: Calum Murray <cmurray@redhat.com>
Signed-off-by: Calum Murray <cmurray@redhat.com>
|
@manusa I rebased on main and fixed the conflicts |
…text (#1278) pkg/kubernetes/provider_gvk_filter.go was added in #1196 (target-specific tool filtering), which merged after the OTel logging work in #1260, so it kept calling klog.FromContext directly and is now the only production file that does. Switch it to klogutil.FromContext so AnyTargetHasGVKs warning logs carry the active trace span for log-trace correlation, and to restore the "no klog.FromContext in production code" invariant documented in AGENTS.md. Drops the now-unused k8s.io/klog/v2 import. Signed-off-by: Marc Nuri <marc@marcnuri.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With this PR, our logs are exported with otel now.
As a follow up, once #1228 lands I will add a test to verify that logs and traces are actually correlated after tool calls in an otel backend