From 437abf27fb45efbccf182b537cc9b9b2290e35e8 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 21 Sep 2026 17:12:55 +0200 Subject: [PATCH] otel: add WithSpanIDField option to Logrus hook Add WithSpanIDField to allow callers to include the active span ID in Logrus entries, independently of the existing trace ID option. Extend the span-context field tests to cover trace ID, span ID, both fields, disabled options, and entries without a span or context. Signed-off-by: Sebastiaan van Stijn --- otel/log.go | 12 ++++++++++ otel/log_test.go | 60 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/otel/log.go b/otel/log.go index 3233eff..25fca2b 100644 --- a/otel/log.go +++ b/otel/log.go @@ -62,6 +62,14 @@ func WithTraceIDField(enabled bool) HookOpt { } } +// WithSpanIDField configures the hook to add the span ID of the active span +// to log entries. +func WithSpanIDField(enabled bool) HookOpt { + return func(h *LogrusHook) { + h.enableSpanIDField = enabled + } +} + // WithLevel configures the minimum log level handled by the hook. // Entries below this level are ignored. func WithLevel(level log.Level) HookOpt { @@ -90,6 +98,7 @@ func WithErrorStatusLevel(level log.Level) HookOpt { // [logrus.Hook]: https://github.com/sirupsen/logrus/blob/v1.9.3/hooks.go#L3-L11 type LogrusHook struct { enableTraceIDField bool + enableSpanIDField bool errorStatusLevel *log.Level levels []log.Level } @@ -117,6 +126,9 @@ func (h *LogrusHook) Fire(entry *log.Entry) error { if h.enableTraceIDField { entry.Data["trace_id"] = spanCtx.TraceID().String() } + if h.enableSpanIDField { + entry.Data["span_id"] = spanCtx.SpanID().String() + } if !span.IsRecording() { return nil diff --git a/otel/log_test.go b/otel/log_test.go index d3755b5..c4f61cc 100644 --- a/otel/log_test.go +++ b/otel/log_test.go @@ -32,7 +32,10 @@ import ( "go.opentelemetry.io/otel/trace" ) -const expectedTraceIDStr = "0102030405060708090a0b0c0d0e0f10" +const ( + expectedTraceIDStr = "0102030405060708090a0b0c0d0e0f10" + expectedSpanIDStr = "0102030405060708" +) var ( testTraceID = trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} @@ -54,33 +57,49 @@ func (s *testSpan) IsRecording() bool { return true } func (s *testSpan) AddEvent(string, ...trace.EventOption) {} func (s *testSpan) SetStatus(code codes.Code, _ string) { s.status = code } -func TestLogrusHookTraceID(t *testing.T) { +func TestLogrusHookSpanContextFields(t *testing.T) { tests := []struct { name string - enableOpt bool + traceID bool + spanID bool nilContext bool withSpan bool expectedTID string + expectedSID string }{ { name: "TraceIDInjected", - enableOpt: true, + traceID: true, + withSpan: true, + expectedTID: expectedTraceIDStr, + }, + { + name: "SpanIDInjected", + spanID: true, + withSpan: true, + expectedSID: expectedSpanIDStr, + }, + { + name: "TraceAndSpanIDInjected", + traceID: true, + spanID: true, withSpan: true, expectedTID: expectedTraceIDStr, + expectedSID: expectedSpanIDStr, }, { - name: "TraceIDNotInjected_OptionDisabled", - enableOpt: false, - withSpan: true, + name: "FieldsNotInjected_OptionsDisabled", + withSpan: true, }, { - name: "TraceIDNotInjected_NoSpan", - enableOpt: true, - withSpan: false, + name: "FieldsNotInjected_NoSpan", + traceID: true, + spanID: true, }, { - name: "TraceIDNotInjected_NoContext", - enableOpt: true, + name: "FieldsNotInjected_NoContext", + traceID: true, + spanID: true, nilContext: true, }, } @@ -89,7 +108,10 @@ func TestLogrusHookTraceID(t *testing.T) { t.Run(tc.name, func(t *testing.T) { logger := logrus.New() logger.SetOutput(io.Discard) - logger.AddHook(otel.NewLogrusHook(otel.WithTraceIDField(tc.enableOpt))) + logger.AddHook(otel.NewLogrusHook( + otel.WithTraceIDField(tc.traceID), + otel.WithSpanIDField(tc.spanID), + )) testHook := test.NewLocal(logger) switch { @@ -123,6 +145,18 @@ func TestLogrusHookTraceID(t *testing.T) { } else if ok { t.Errorf(`unexpected "trace_id" field: %v`, traceID) } + + spanID, ok := entry.Data["span_id"] + if tc.expectedSID != "" { + if !ok { + t.Fatal(`expected "span_id" field`) + } + if spanID != tc.expectedSID { + t.Errorf(`"span_id" = %v; want %q`, spanID, tc.expectedSID) + } + } else if ok { + t.Errorf(`unexpected "span_id" field: %v`, spanID) + } }) } }