-
Notifications
You must be signed in to change notification settings - Fork 7
/
slog_otel.go
222 lines (192 loc) · 6.52 KB
/
slog_otel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package slogotel
import (
"context"
"fmt"
"log/slog"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
const (
// TraceIDKey is the key used by the Otel handler
// to inject the trace ID in the log record.
TraceIDKey = "trace_id"
// SpanIDKey is the key used by the Otel handler
// to inject the span ID in the log record.
SpanIDKey = "span_id"
// SpanEventKey is the key used by the Otel handler
// to inject the log record in the recording span, as a span event.
SpanEventKey = "log_record"
)
// OtelHandler is an implementation of slog's Handler interface.
// Its role is to ensure correlation between logs and OTel spans
// by:
//
// 1. Adding otel span and trace IDs to the log record.
// 2. Adding otel context baggage members to the log record.
// 3. Setting slog record as otel span event.
// 4. Adding slog record attributes to the otel span event.
// 5. Setting span status based on slog record level (only if >= slog.LevelError).
type OtelHandler struct {
// Next represents the next handler in the chain.
Next slog.Handler
// NoBaggage determines whether to add context baggage members to the log record.
NoBaggage bool
// NoTraceEvents determines whether to record an event for every log on the active trace.
NoTraceEvents bool
}
type OtelHandlerOpt func(handler *OtelHandler)
// HandlerFn defines the handler used by slog.Handler as return value.
type HandlerFn func(slog.Handler) slog.Handler
// WithNoBaggage returns an OtelHandlerOpt, which sets the NoBaggage flag
func WithNoBaggage(noBaggage bool) OtelHandlerOpt {
return func(handler *OtelHandler) {
handler.NoBaggage = noBaggage
}
}
// WithNoTraceEvents returns an OtelHandlerOpt, which sets the NoTraceEvents flag
func WithNoTraceEvents(noTraceEvents bool) OtelHandlerOpt {
return func(handler *OtelHandler) {
handler.NoTraceEvents = noTraceEvents
}
}
// New creates a new OtelHandler to use with log/slog
func New(next slog.Handler, opts ...OtelHandlerOpt) *OtelHandler {
ret := &OtelHandler{
Next: next,
}
for _, opt := range opts {
opt(ret)
}
return ret
}
// NewOtelHandler creates and returns a new HandlerFn, which wraps a handler with OtelHandler to use with log/slog.
func NewOtelHandler(opts ...OtelHandlerOpt) HandlerFn {
return func(next slog.Handler) slog.Handler {
return New(next, opts...)
}
}
// Handle handles the provided log record and adds correlation between a slog record and an Open-Telemetry span.
func (h OtelHandler) Handle(ctx context.Context, record slog.Record) error {
if ctx == nil {
return h.Next.Handle(ctx, record)
}
if !h.NoBaggage {
// Adding context baggage members to log record.
b := baggage.FromContext(ctx)
for _, m := range b.Members() {
record.AddAttrs(slog.String(m.Key(), m.Value()))
}
}
span := trace.SpanFromContext(ctx)
if span == nil || !span.IsRecording() {
return h.Next.Handle(ctx, record)
}
if !h.NoTraceEvents {
// Adding log info to span event.
eventAttrs := make([]attribute.KeyValue, 0, record.NumAttrs())
eventAttrs = append(eventAttrs, attribute.String(slog.MessageKey, record.Message))
eventAttrs = append(eventAttrs, attribute.String(slog.LevelKey, record.Level.String()))
eventAttrs = append(eventAttrs, attribute.String(slog.TimeKey, record.Time.Format(time.RFC3339Nano)))
record.Attrs(func(attr slog.Attr) bool {
otelAttr := h.slogAttrToOtelAttr(attr)
if otelAttr.Valid() {
eventAttrs = append(eventAttrs, otelAttr)
}
return true
})
span.AddEvent(SpanEventKey, trace.WithAttributes(eventAttrs...))
}
// Adding span info to log record.
spanContext := span.SpanContext()
if spanContext.HasTraceID() {
traceID := spanContext.TraceID().String()
record.AddAttrs(slog.String(TraceIDKey, traceID))
}
if spanContext.HasSpanID() {
spanID := spanContext.SpanID().String()
record.AddAttrs(slog.String(SpanIDKey, spanID))
}
// Setting span status if the log is an error.
// Purposely leaving as codes.Unset (default) otherwise.
if record.Level >= slog.LevelError {
span.SetStatus(codes.Error, record.Message)
}
return h.Next.Handle(ctx, record)
}
// WithAttrs returns a new Otel whose attributes consists of handler's attributes followed by attrs.
func (h OtelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return OtelHandler{
Next: h.Next.WithAttrs(attrs),
NoBaggage: h.NoBaggage,
NoTraceEvents: h.NoTraceEvents,
}
}
// WithGroup returns a new Otel with a group, provided the group's name.
func (h OtelHandler) WithGroup(name string) slog.Handler {
return OtelHandler{
Next: h.Next.WithGroup(name),
NoBaggage: h.NoBaggage,
NoTraceEvents: h.NoTraceEvents,
}
}
// Enabled reports whether the logger emits log records at the given context and level.
// Note: We handover the decision down to the next handler.
func (h OtelHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.Next.Enabled(ctx, level)
}
// slogAttrToOtelAttr converts a slog attribute to an OTel one.
// Note: returns an empty attribute if the provided slog attribute is empty.
func (h OtelHandler) slogAttrToOtelAttr(attr slog.Attr, groupKeys ...string) attribute.KeyValue {
attr.Value = attr.Value.Resolve()
if attr.Equal(slog.Attr{}) {
return attribute.KeyValue{}
}
key := func(k string, prefixes ...string) string {
for _, prefix := range prefixes {
k = fmt.Sprintf("%s.%s", prefix, k)
}
return k
}(attr.Key, groupKeys...)
value := attr.Value.Resolve()
switch attr.Value.Kind() {
case slog.KindBool:
return attribute.Bool(key, value.Bool())
case slog.KindFloat64:
return attribute.Float64(key, value.Float64())
case slog.KindInt64:
return attribute.Int64(key, value.Int64())
case slog.KindString:
return attribute.String(key, value.String())
case slog.KindTime:
return attribute.String(key, value.Time().Format(time.RFC3339Nano))
case slog.KindGroup:
groupAttrs := value.Group()
if len(groupAttrs) == 0 {
return attribute.KeyValue{}
}
for _, groupAttr := range groupAttrs {
return h.slogAttrToOtelAttr(groupAttr, append(groupKeys, key)...)
}
case slog.KindAny:
switch v := attr.Value.Any().(type) {
case []string:
return attribute.StringSlice(key, v)
case []int:
return attribute.IntSlice(key, v)
case []int64:
return attribute.Int64Slice(key, v)
case []float64:
return attribute.Float64Slice(key, v)
case []bool:
return attribute.BoolSlice(key, v)
default:
return attribute.String(key, fmt.Sprintf("%+v", v))
}
default:
return attribute.KeyValue{}
}
return attribute.KeyValue{}
}