Skip to content

Commit d9c5596

Browse files
authored
Merge pull request #38 from vincentfree/feature/add-slog
add slog support
2 parents ee585b7 + 5be8f36 commit d9c5596

File tree

4 files changed

+693
-0
lines changed

4 files changed

+693
-0
lines changed

otelslog/example_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2023 Vincent Free
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelslog_test
16+
17+
import (
18+
"context"
19+
"errors"
20+
"github.com/vincentfree/opentelemetry/otelslog"
21+
"go.opentelemetry.io/otel"
22+
"go.opentelemetry.io/otel/attribute"
23+
"golang.org/x/exp/slog"
24+
"os"
25+
)
26+
27+
func ExampleAddTracingContext() {
28+
tracer := otel.Tracer("otelslog/example")
29+
_, span := tracer.Start(context.Background(), "example-span")
30+
31+
// pass span to AddTracingContext
32+
slog.LogAttrs(nil, slog.LevelInfo, "in case of a success", otelslog.AddTracingContext(span)...)
33+
34+
// or in the case of an error
35+
err := errors.New("example error")
36+
slog.LogAttrs(nil, slog.LevelError, "in case of a failure", otelslog.AddTracingContext(span, err)...)
37+
}
38+
39+
func ExampleAddTracingContextWithAttributes() {
40+
tracer := otel.Tracer("otelslog/example")
41+
_, span := tracer.Start(context.Background(), "example-span")
42+
43+
attributes := []attribute.KeyValue{
44+
attribute.String("exampleKey", "exampleValue"),
45+
attribute.Bool("isValid", true),
46+
}
47+
48+
// pass span to AddTracingContext
49+
slog.LogAttrs(nil, slog.LevelInfo, "in case of a success", otelslog.AddTracingContextWithAttributes(span, attributes)...)
50+
51+
// or in the case of an error
52+
err := errors.New("example error")
53+
slog.LogAttrs(nil, slog.LevelError, "in case of a failure", otelslog.AddTracingContextWithAttributes(span, attributes, err)...)
54+
}
55+
56+
func ExampleWithAttributes() {
57+
option := otelslog.WithAttributes(attribute.String("test", "value"), attribute.Bool("isValid", true))
58+
otelslog.SetLogOptions(option)
59+
60+
tracer := otel.Tracer("otelslog/example")
61+
_, span := tracer.Start(context.Background(), "example-span")
62+
63+
// pass span to AddTracingContext
64+
slog.LogAttrs(nil, slog.LevelInfo, "in case of a success", otelslog.AddTracingContext(span)...)
65+
66+
// or in the case of an error
67+
err := errors.New("example error")
68+
slog.LogAttrs(nil, slog.LevelError, "in case of a failure", otelslog.AddTracingContext(span, err)...)
69+
}
70+
71+
func ExampleWithAttributePrefix() {
72+
otelslog.SetLogOptions(otelslog.WithAttributePrefix("prefix"))
73+
// use AddTracingContext or AddTracingContextWithAttributes
74+
}
75+
76+
func ExampleWithServiceName() {
77+
otelslog.SetLogOptions(otelslog.WithServiceName("example-service"))
78+
// use AddTracingContext or AddTracingContextWithAttributes
79+
}
80+
81+
func ExampleWithSpanID() {
82+
otelslog.SetLogOptions(otelslog.WithSpanID("span-id"))
83+
// use AddTracingContext or AddTracingContextWithAttributes
84+
}
85+
86+
func ExampleWithTraceID() {
87+
otelslog.SetLogOptions(otelslog.WithTraceID("trace-id"))
88+
// use AddTracingContext or AddTracingContextWithAttributes
89+
}
90+
91+
func ExampleSetLogOptions() {
92+
option := otelslog.WithAttributes(attribute.String("test", "value"), attribute.Bool("isValid", true))
93+
// use of SetLogOptions
94+
otelslog.SetLogOptions(option)
95+
96+
// set up tracer
97+
tracer := otel.Tracer("otelslog/example")
98+
_, span := tracer.Start(context.Background(), "example-span")
99+
100+
// pass span to AddTracingContext
101+
slog.LogAttrs(nil, slog.LevelInfo, "in case of a success", otelslog.AddTracingContext(span)...)
102+
103+
// or in the case of an error
104+
err := errors.New("example error")
105+
slog.LogAttrs(nil, slog.LevelError, "in case of a failure", otelslog.AddTracingContext(span, err)...)
106+
}
107+
108+
func ExampleLogger_WithTracingContext() {
109+
tracer := otel.Tracer("otelslog/example")
110+
_, span := tracer.Start(context.Background(), "example-span")
111+
112+
logger := otelslog.New()
113+
// pass span to AddTracingContext
114+
logger.WithTracingContext(nil, slog.LevelInfo, "in case of a success", span, nil)
115+
slog.LogAttrs(nil, slog.LevelInfo, "in case of a success", otelslog.AddTracingContext(span)...)
116+
117+
// or in the case of an error
118+
err := errors.New("example error")
119+
logger.WithTracingContext(nil, slog.LevelError, "in case of a failure", span, err)
120+
}
121+
122+
func ExampleLogger_WithTracingContextAndAttributes() {
123+
attributes := []attribute.KeyValue{
124+
attribute.String("exampleKey", "exampleValue"),
125+
attribute.Bool("isValid", true),
126+
}
127+
128+
tracer := otel.Tracer("otelslog/example")
129+
_, span := tracer.Start(context.Background(), "example-span")
130+
131+
logger := otelslog.New()
132+
// pass span to AddTracingContext
133+
logger.WithTracingContextAndAttributes(nil, slog.LevelInfo, "in case of a success", span, nil, attributes)
134+
135+
// or in the case of an error
136+
err := errors.New("example error")
137+
logger.WithTracingContextAndAttributes(nil, slog.LevelError, "in case of a failure", span, err, attributes)
138+
}
139+
140+
func ExampleNew() {
141+
tracer := otel.Tracer("otelslog/example")
142+
_, span := tracer.Start(context.Background(), "example-span")
143+
144+
logger := otelslog.New()
145+
// pass span to AddTracingContext
146+
logger.WithTracingContext(nil, slog.LevelInfo, "in case of a success", span, nil)
147+
}
148+
149+
func ExampleNewWithHandler() {
150+
tracer := otel.Tracer("otelslog/example")
151+
_, span := tracer.Start(context.Background(), "example-span")
152+
153+
logger := otelslog.NewWithHandler(slog.NewTextHandler(os.Stdout, nil))
154+
// pass span to AddTracingContext
155+
logger.WithTracingContext(nil, slog.LevelInfo, "in case of a success", span, nil)
156+
}

otelslog/go.mod

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/vincentfree/opentelemetry/otelslog
2+
3+
go 1.20
4+
5+
require (
6+
github.com/go-logr/logr v1.2.4 // indirect
7+
github.com/go-logr/stdr v1.2.2 // indirect
8+
go.opentelemetry.io/otel v1.16.0 // indirect
9+
go.opentelemetry.io/otel/metric v1.16.0 // indirect
10+
go.opentelemetry.io/otel/trace v1.16.0 // indirect
11+
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
12+
)

otelslog/slog.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright 2023 Vincent Free
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelslog
16+
17+
import (
18+
"context"
19+
"go.opentelemetry.io/otel/attribute"
20+
"go.opentelemetry.io/otel/codes"
21+
"go.opentelemetry.io/otel/trace"
22+
"golang.org/x/exp/slog"
23+
"os"
24+
"strconv"
25+
"strings"
26+
)
27+
28+
// LogOption takes a logConfig struct and applies changes.
29+
// It can be passed to the SetLogOptions function to configure a logConfig struct.
30+
type LogOption func(*logConfig)
31+
32+
type logConfig struct {
33+
attributes []attribute.KeyValue
34+
serviceName string
35+
traceId string
36+
spanId string
37+
attributePrefix string
38+
}
39+
40+
type Logger struct {
41+
*slog.Logger
42+
}
43+
44+
var (
45+
// _traceId has a default trace ID key in the logs
46+
_traceId = "traceID"
47+
// _spanId has a default span ID key in the logs
48+
_spanId = "spanID"
49+
// _serviceName is empty by default, when no value is set the service name won't be used with a default in the logs
50+
_serviceName string
51+
// _attributes contains a global set of attribute.KeyValue's that will be added to very structured log. when the slice is empty they won't be added
52+
_attributes = []attribute.KeyValue(nil)
53+
54+
// _attrPrefix x
55+
_attrPrefix = "trace.attribute"
56+
)
57+
58+
// SetLogOptions takes LogOption's and overwrites library defaults
59+
func SetLogOptions(options ...LogOption) {
60+
// initialize an empty logConfig
61+
config := &logConfig{}
62+
63+
for _, option := range options {
64+
option(config)
65+
}
66+
67+
if config.traceId != "" {
68+
_traceId = config.traceId
69+
}
70+
71+
if config.spanId != "" {
72+
_traceId = config.spanId
73+
}
74+
75+
if config.serviceName != "" {
76+
_serviceName = config.serviceName
77+
}
78+
79+
if config.attributePrefix != "" {
80+
ap, _ := strings.CutSuffix(config.attributePrefix, ".")
81+
_attrPrefix = ap
82+
}
83+
84+
if len(config.attributes) > 0 {
85+
_attributes = append(_attributes, config.attributes...)
86+
}
87+
}
88+
89+
// WithTraceID overwrites the default 'traceID' field in the structured logs with your own key
90+
func WithTraceID(traceID string) LogOption {
91+
return func(c *logConfig) {
92+
c.traceId = traceID
93+
}
94+
}
95+
96+
// WithSpanID overwrites the default 'spanID' field in the structured logs with your own key
97+
func WithSpanID(spanID string) LogOption {
98+
return func(c *logConfig) {
99+
c.spanId = spanID
100+
}
101+
}
102+
103+
// WithServiceName adds a service name to the field 'service.name' in your structured logs
104+
func WithServiceName(serviceName string) LogOption {
105+
return func(c *logConfig) {
106+
c.serviceName = serviceName
107+
}
108+
}
109+
110+
// WithAttributePrefix updates the default 'trace.attribute' attribute prefix
111+
func WithAttributePrefix(prefix string) LogOption {
112+
return func(c *logConfig) {
113+
c.attributePrefix = prefix
114+
}
115+
}
116+
117+
// WithAttributes adds global attributes that will be added to all structured logs. attributes have a prefix followed by the key of the attribute.
118+
//
119+
// Example: if the attribute is of type string and the key is: 'http.method' then in the log it uses the default(but over-writable) 'trace.attribute' followed by 'http.method' so the end result is: 'trace.attribute.http.method'
120+
func WithAttributes(attributes ...attribute.KeyValue) LogOption {
121+
return func(c *logConfig) {
122+
c.attributes = append(c.attributes, attributes...)
123+
}
124+
}
125+
126+
// AddTracingContext lets you add the trace context to a structured log
127+
func AddTracingContext(span trace.Span, err ...error) []slog.Attr {
128+
a := []attribute.KeyValue(nil)
129+
return AddTracingContextWithAttributes(span, a, err...)
130+
}
131+
132+
// AddTracingContextWithAttributes lets you add the trace context to a structured log, including attribute.KeyValue's to extend the log
133+
func AddTracingContextWithAttributes(span trace.Span, attributes []attribute.KeyValue, err ...error) []slog.Attr {
134+
var result []slog.Attr
135+
if len(err) > 0 && err[0] != nil {
136+
span.RecordError(err[0])
137+
span.SetStatus(codes.Error, err[0].Error())
138+
result = append(result, slog.String("error", err[0].Error()))
139+
}
140+
141+
result = append(result,
142+
slog.String(_traceId, span.SpanContext().TraceID().String()),
143+
slog.String(_spanId, span.SpanContext().SpanID().String()),
144+
)
145+
// set service.name if the value isn't empty
146+
if _serviceName != "" {
147+
result = append(result, slog.String("service.name", _serviceName))
148+
}
149+
150+
attrs := attributes
151+
attrs = append(attrs, _attributes...)
152+
153+
// add attributes when global or passed attributes are > 0
154+
if len(attrs) > 0 {
155+
for _, attr := range attrs {
156+
switch attr.Value.Type() {
157+
case attribute.STRING:
158+
result = append(result, slog.String(_attrPrefix+"."+string(attr.Key), attr.Value.AsString()))
159+
case attribute.FLOAT64:
160+
result = append(result, slog.Float64(_attrPrefix+"."+string(attr.Key), attr.Value.AsFloat64()))
161+
case attribute.BOOL:
162+
result = append(result, slog.Bool(_attrPrefix+"."+string(attr.Key), attr.Value.AsBool()))
163+
case attribute.INT64:
164+
result = append(result, slog.Int64(_attrPrefix+"."+string(attr.Key), attr.Value.AsInt64()))
165+
case attribute.BOOLSLICE:
166+
s := attr.Value.AsBoolSlice()
167+
vals := []slog.Attr(nil)
168+
for i, b := range s {
169+
key := _attrPrefix + "." + string(attr.Key) + "." + strconv.FormatInt(int64(i), 10)
170+
vals = append(vals, slog.Bool(key, b))
171+
}
172+
result = append(result, vals...)
173+
174+
case attribute.INT64SLICE:
175+
s := attr.Value.AsInt64Slice()
176+
vals := []slog.Attr(nil)
177+
for i, b := range s {
178+
key := _attrPrefix + "." + string(attr.Key) + "." + strconv.FormatInt(int64(i), 10)
179+
vals = append(vals, slog.Int64(key, b))
180+
}
181+
result = append(result, vals...)
182+
case attribute.FLOAT64SLICE:
183+
s := attr.Value.AsFloat64Slice()
184+
vals := []slog.Attr(nil)
185+
for i, b := range s {
186+
key := _attrPrefix + "." + string(attr.Key) + "." + strconv.FormatInt(int64(i), 10)
187+
vals = append(vals, slog.Float64(key, b))
188+
}
189+
result = append(result, vals...)
190+
case attribute.STRINGSLICE:
191+
s := attr.Value.AsStringSlice()
192+
vals := []slog.Attr(nil)
193+
for i, b := range s {
194+
key := _attrPrefix + "." + string(attr.Key) + "." + strconv.FormatInt(int64(i), 10)
195+
vals = append(vals, slog.String(key, b))
196+
}
197+
result = append(result, vals...)
198+
}
199+
}
200+
}
201+
202+
return result
203+
}
204+
205+
func New() *Logger {
206+
return &Logger{slog.New(slog.NewJSONHandler(os.Stdout, nil))}
207+
}
208+
209+
func NewWithHandler(handler slog.Handler) *Logger {
210+
if handler == nil {
211+
return New()
212+
}
213+
return &Logger{slog.New(handler)}
214+
}
215+
216+
func (l Logger) WithTracingContext(ctx context.Context, level slog.Level, msg string, span trace.Span, err error, attrs ...slog.Attr) {
217+
attrs = append(attrs, AddTracingContext(span, err)...)
218+
l.LogAttrs(ctx, level, msg, attrs...)
219+
}
220+
221+
func (l Logger) WithTracingContextAndAttributes(ctx context.Context, level slog.Level, msg string, span trace.Span, err error, attributes []attribute.KeyValue, attrs ...slog.Attr) {
222+
attrs = append(attrs, AddTracingContextWithAttributes(span, attributes, err)...)
223+
l.LogAttrs(ctx, level, msg, attrs...)
224+
}

0 commit comments

Comments
 (0)