Skip to content

Commit 8b5d653

Browse files
authored
Merge pull request #25 from thaJeztah/integrate_tracing_improve
otel: add options to configure log level and span error status (WithLevel, WithErrorStatusLevel)
2 parents 6fcf9c1 + 65e60ca commit 8b5d653

2 files changed

Lines changed: 190 additions & 1 deletion

File tree

otel/log.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
package otel
2222

2323
import (
24+
"slices"
25+
2426
"github.com/containerd/log"
2527
"go.opentelemetry.io/otel/attribute"
28+
"go.opentelemetry.io/otel/codes"
2629
"go.opentelemetry.io/otel/trace"
2730
)
2831

@@ -47,6 +50,9 @@ func NewLogrusHook(opts ...HookOpt) *LogrusHook {
4750
for _, opt := range opts {
4851
opt(hook)
4952
}
53+
if hook.levels == nil {
54+
hook.levels = slices.Clone(allLevels)
55+
}
5056
return hook
5157
}
5258

@@ -56,18 +62,44 @@ func WithTraceIDField(enabled bool) HookOpt {
5662
}
5763
}
5864

65+
// WithLevel configures the minimum log level handled by the hook.
66+
// Entries below this level are ignored.
67+
func WithLevel(level log.Level) HookOpt {
68+
return func(h *LogrusHook) {
69+
for i, l := range allLevels {
70+
if l == level {
71+
h.levels = slices.Clone(allLevels[:i+1])
72+
return
73+
}
74+
}
75+
}
76+
}
77+
78+
// WithErrorStatusLevel configures the minimum log level that marks the
79+
// active span with an error status.
80+
func WithErrorStatusLevel(level log.Level) HookOpt {
81+
return func(h *LogrusHook) {
82+
h.errorStatusLevel = &level
83+
}
84+
}
85+
5986
// LogrusHook is a [logrus.Hook] which adds logrus events to active spans.
6087
// If the span is not recording or the span context is invalid, the hook
6188
// is a no-op.
6289
//
6390
// [logrus.Hook]: https://github.com/sirupsen/logrus/blob/v1.9.3/hooks.go#L3-L11
6491
type LogrusHook struct {
6592
enableTraceIDField bool
93+
errorStatusLevel *log.Level
94+
levels []log.Level
6695
}
6796

6897
// Levels returns the logrus levels that this hook is interested in.
6998
func (h *LogrusHook) Levels() []log.Level {
70-
return allLevels
99+
if h.levels == nil {
100+
return allLevels
101+
}
102+
return h.levels
71103
}
72104

73105
// Fire is called when a log event occurs.
@@ -97,6 +129,13 @@ func (h *LogrusHook) Fire(entry *log.Entry) error {
97129
trace.WithTimestamp(entry.Time),
98130
)
99131

132+
// Set the span status based on the log level, rather than the presence of
133+
// an error field. Error values may be attached to lower-severity log entries
134+
// without indicating that the operation represented by the span failed.
135+
if h.errorStatusLevel != nil && entry.Level <= *h.errorStatusLevel {
136+
span.SetStatus(codes.Error, entry.Message)
137+
}
138+
100139
return nil
101140
}
102141

otel/log_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ package otel_test
1818

1919
import (
2020
"context"
21+
"errors"
2122
"io"
23+
"slices"
2224
"testing"
25+
"time"
2326

27+
"github.com/containerd/log"
2428
"github.com/containerd/log/otel"
2529
"github.com/sirupsen/logrus"
2630
"github.com/sirupsen/logrus/hooks/test"
31+
"go.opentelemetry.io/otel/codes"
2732
"go.opentelemetry.io/otel/trace"
2833
)
2934

@@ -34,6 +39,21 @@ var (
3439
testSpanID = trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}
3540
)
3641

42+
// testSpan is a minimal recording span used to test hook behavior without
43+
// depending on the OpenTelemetry SDK.
44+
type testSpan struct {
45+
trace.Span
46+
status codes.Code
47+
}
48+
49+
func (s *testSpan) SpanContext() trace.SpanContext {
50+
return trace.NewSpanContext(trace.SpanContextConfig{TraceID: testTraceID, SpanID: testSpanID})
51+
}
52+
53+
func (s *testSpan) IsRecording() bool { return true }
54+
func (s *testSpan) AddEvent(string, ...trace.EventOption) {}
55+
func (s *testSpan) SetStatus(code codes.Code, _ string) { s.status = code }
56+
3757
func TestLogrusHookTraceID(t *testing.T) {
3858
tests := []struct {
3959
name string
@@ -106,3 +126,133 @@ func TestLogrusHookTraceID(t *testing.T) {
106126
})
107127
}
108128
}
129+
130+
// TestLogrusHookLevels verifies that [WithLevel] limits the levels handled by
131+
// the hook while preserving all levels by default.
132+
func TestLogrusHookLevels(t *testing.T) {
133+
tests := []struct {
134+
name string
135+
opts []otel.HookOpt
136+
want []log.Level
137+
}{
138+
{
139+
name: "default",
140+
want: []log.Level{
141+
log.PanicLevel,
142+
log.FatalLevel,
143+
log.ErrorLevel,
144+
log.WarnLevel,
145+
log.InfoLevel,
146+
log.DebugLevel,
147+
log.TraceLevel,
148+
},
149+
},
150+
{
151+
name: "warn",
152+
opts: []otel.HookOpt{
153+
otel.WithLevel(log.WarnLevel),
154+
},
155+
want: []log.Level{
156+
log.PanicLevel,
157+
log.FatalLevel,
158+
log.ErrorLevel,
159+
log.WarnLevel,
160+
},
161+
},
162+
{
163+
name: "error",
164+
opts: []otel.HookOpt{
165+
otel.WithLevel(log.ErrorLevel),
166+
},
167+
want: []log.Level{
168+
log.PanicLevel,
169+
log.FatalLevel,
170+
log.ErrorLevel,
171+
},
172+
},
173+
}
174+
175+
for _, tc := range tests {
176+
t.Run(tc.name, func(t *testing.T) {
177+
hook := otel.NewLogrusHook(tc.opts...)
178+
if got := hook.Levels(); !slices.Equal(got, tc.want) {
179+
t.Errorf("Levels() = %v; want %v", got, tc.want)
180+
}
181+
})
182+
}
183+
}
184+
185+
// TestLogrusHookErrorStatusLevel verifies that [WithErrorStatusLevel] marks
186+
// spans as errors based on log severity and leaves span status unchanged by
187+
// default.
188+
func TestLogrusHookErrorStatusLevel(t *testing.T) {
189+
tests := []struct {
190+
name string
191+
opts []otel.HookOpt
192+
level log.Level
193+
fields log.Fields
194+
wantError bool
195+
}{
196+
{
197+
name: "default",
198+
level: log.ErrorLevel,
199+
},
200+
{
201+
name: "below threshold",
202+
opts: []otel.HookOpt{
203+
otel.WithErrorStatusLevel(log.ErrorLevel),
204+
},
205+
level: log.WarnLevel,
206+
},
207+
{
208+
name: "at threshold",
209+
opts: []otel.HookOpt{
210+
otel.WithErrorStatusLevel(log.ErrorLevel),
211+
},
212+
level: log.ErrorLevel,
213+
wantError: true,
214+
},
215+
{
216+
name: "above threshold",
217+
opts: []otel.HookOpt{
218+
otel.WithErrorStatusLevel(log.ErrorLevel),
219+
},
220+
level: log.FatalLevel,
221+
wantError: true,
222+
},
223+
{
224+
name: "error field below threshold",
225+
opts: []otel.HookOpt{
226+
otel.WithErrorStatusLevel(log.ErrorLevel),
227+
},
228+
level: log.DebugLevel,
229+
fields: log.Fields{
230+
"error": errors.New("ignored"),
231+
},
232+
},
233+
}
234+
235+
for _, tc := range tests {
236+
t.Run(tc.name, func(t *testing.T) {
237+
span := &testSpan{}
238+
ctx := trace.ContextWithSpan(context.Background(), span)
239+
240+
hook := otel.NewLogrusHook(tc.opts...)
241+
err := hook.Fire(&log.Entry{
242+
Context: ctx,
243+
Data: tc.fields,
244+
Level: tc.level,
245+
Message: "message",
246+
Time: time.Now(),
247+
})
248+
if err != nil {
249+
t.Fatal(err)
250+
}
251+
252+
gotError := span.status == codes.Error
253+
if gotError != tc.wantError {
254+
t.Errorf("span error status = %v; want %v", gotError, tc.wantError)
255+
}
256+
})
257+
}
258+
}

0 commit comments

Comments
 (0)