Passed
Push — master ( 2b5f8d...e36456 )
by Tolga
01:14 queued 15s
created

telemetry.WithNoTraceEvents   A

Complexity

Conditions 2

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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