Skip to content

Commit 63e6de6

Browse files
committed
Add spanlogger package
Signed-off-by: Arve Knudsen <[email protected]>
1 parent 1ae630c commit 63e6de6

File tree

4 files changed

+253
-1
lines changed

4 files changed

+253
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
* [CHANGE] Rename `kv/kvtls` to `crypto/tls`. #39
1010
* [ENHANCEMENT] Add middleware package. #38
1111
* [ENHANCEMENT] Add limiter package. #41
12-
* [ENHANCEMENT] Add grpcclient, grpcencoding and grpcutil packages. #39
12+
* [ENHANCEMENT] Add grpcclient, grpcencoding and grpcutil packages. #39
13+
* [ENHANCEMENT] Add spanlogger package. #42

spanlogger/noop.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package spanlogger
2+
3+
import (
4+
opentracing "github.com/opentracing/opentracing-go"
5+
"github.com/opentracing/opentracing-go/log"
6+
)
7+
8+
type noopTracer struct{}
9+
10+
type noopSpan struct{}
11+
type noopSpanContext struct{}
12+
13+
var (
14+
defaultNoopSpanContext = noopSpanContext{}
15+
defaultNoopSpan = noopSpan{}
16+
defaultNoopTracer = noopTracer{}
17+
)
18+
19+
const (
20+
emptyString = ""
21+
)
22+
23+
func (n noopSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {}
24+
25+
func (n noopSpan) Context() opentracing.SpanContext { return defaultNoopSpanContext }
26+
func (n noopSpan) SetBaggageItem(key, val string) opentracing.Span { return defaultNoopSpan }
27+
func (n noopSpan) BaggageItem(key string) string { return emptyString }
28+
func (n noopSpan) SetTag(key string, value interface{}) opentracing.Span { return n }
29+
func (n noopSpan) LogFields(fields ...log.Field) {}
30+
func (n noopSpan) LogKV(keyVals ...interface{}) {}
31+
func (n noopSpan) Finish() {}
32+
func (n noopSpan) FinishWithOptions(opts opentracing.FinishOptions) {}
33+
func (n noopSpan) SetOperationName(operationName string) opentracing.Span { return n }
34+
func (n noopSpan) Tracer() opentracing.Tracer { return defaultNoopTracer }
35+
func (n noopSpan) LogEvent(event string) {}
36+
func (n noopSpan) LogEventWithPayload(event string, payload interface{}) {}
37+
func (n noopSpan) Log(data opentracing.LogData) {}
38+
39+
// StartSpan belongs to the Tracer interface.
40+
func (n noopTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span {
41+
return defaultNoopSpan
42+
}
43+
44+
// Inject belongs to the Tracer interface.
45+
func (n noopTracer) Inject(sp opentracing.SpanContext, format interface{}, carrier interface{}) error {
46+
return nil
47+
}
48+
49+
// Extract belongs to the Tracer interface.
50+
func (n noopTracer) Extract(format interface{}, carrier interface{}) (opentracing.SpanContext, error) {
51+
return nil, opentracing.ErrSpanContextNotFound
52+
}

spanlogger/spanlogger.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package spanlogger
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/go-kit/kit/log"
8+
"github.com/go-kit/kit/log/level"
9+
opentracing "github.com/opentracing/opentracing-go"
10+
"github.com/opentracing/opentracing-go/ext"
11+
otlog "github.com/opentracing/opentracing-go/log"
12+
"github.com/weaveworks/common/tracing"
13+
"github.com/weaveworks/common/user"
14+
)
15+
16+
type loggerCtxMarker struct{}
17+
18+
const (
19+
// TenantIDsTagName is the tenant IDs tag name.
20+
TenantIDsTagName = "tenant_ids"
21+
)
22+
23+
var (
24+
loggerCtxKey = &loggerCtxMarker{}
25+
)
26+
27+
// SpanLogger unifies tracing and logging, to reduce repetition.
28+
type SpanLogger struct {
29+
log.Logger
30+
opentracing.Span
31+
}
32+
33+
// tenantID tries to extract the tenant ID from ctx.
34+
func tenantID(ctx context.Context) string {
35+
//lint:ignore faillint wrapper around upstream method
36+
id, err := user.ExtractOrgID(ctx)
37+
if err != nil {
38+
return ""
39+
}
40+
41+
// handle the relative reference to current and parent path.
42+
if id == "." || id == ".." || strings.ContainsAny(id, `\/`) {
43+
return ""
44+
}
45+
46+
return id
47+
}
48+
49+
func withContext(ctx context.Context, l log.Logger) log.Logger {
50+
// Weaveworks uses "orgs" and "orgID" to represent Cortex users,
51+
// even though the code-base generally uses `userID` to refer to the same thing.
52+
userID := tenantID(ctx)
53+
if userID != "" {
54+
l = log.With(l, "org_id", userID)
55+
}
56+
57+
traceID, ok := tracing.ExtractSampledTraceID(ctx)
58+
if !ok {
59+
return l
60+
}
61+
62+
return log.With(l, "traceID", traceID)
63+
}
64+
65+
// New makes a new SpanLogger with a log.Logger to send logs to. The provided context will have the logger attached
66+
// to it and can be retrieved with FromContext.
67+
func New(ctx context.Context, logger log.Logger, method string, kvps ...interface{}) (*SpanLogger, context.Context) {
68+
span, ctx := opentracing.StartSpanFromContext(ctx, method)
69+
if id := tenantID(ctx); id != "" {
70+
span.SetTag(TenantIDsTagName, []string{id})
71+
}
72+
l := &SpanLogger{
73+
Logger: log.With(withContext(ctx, logger), "method", method),
74+
Span: span,
75+
}
76+
if len(kvps) > 0 {
77+
level.Debug(l).Log(kvps...)
78+
}
79+
80+
ctx = context.WithValue(ctx, loggerCtxKey, logger)
81+
return l, ctx
82+
}
83+
84+
// FromContext returns a span logger using the current parent span.
85+
// If there is no parent span, the SpanLogger will only log to the logger
86+
// within the context. If the context doesn't have a logger, the fallback
87+
// logger is used.
88+
func FromContext(ctx context.Context, fallback log.Logger) *SpanLogger {
89+
logger, ok := ctx.Value(loggerCtxKey).(log.Logger)
90+
if !ok {
91+
logger = fallback
92+
}
93+
sp := opentracing.SpanFromContext(ctx)
94+
if sp == nil {
95+
sp = defaultNoopSpan
96+
}
97+
return &SpanLogger{
98+
Logger: withContext(ctx, logger),
99+
Span: sp,
100+
}
101+
}
102+
103+
// Log implements gokit's Logger interface; sends logs to underlying logger and
104+
// also puts the on the spans.
105+
func (s *SpanLogger) Log(kvps ...interface{}) error {
106+
s.Logger.Log(kvps...)
107+
fields, err := otlog.InterleavedKVToFields(kvps...)
108+
if err != nil {
109+
return err
110+
}
111+
s.Span.LogFields(fields...)
112+
return nil
113+
}
114+
115+
// Error sets error flag and logs the error on the span, if non-nil. Returns the err passed in.
116+
func (s *SpanLogger) Error(err error) error {
117+
if err == nil {
118+
return nil
119+
}
120+
ext.Error.Set(s.Span, true)
121+
s.Span.LogFields(otlog.Error(err))
122+
return err
123+
}

spanlogger/spanlogger_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package spanlogger
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/go-kit/kit/log"
8+
"github.com/opentracing/opentracing-go"
9+
"github.com/opentracing/opentracing-go/mocktracer"
10+
"github.com/pkg/errors"
11+
"github.com/stretchr/testify/require"
12+
"github.com/weaveworks/common/user"
13+
)
14+
15+
func TestSpanLogger_Log(t *testing.T) {
16+
logger := log.NewNopLogger()
17+
span, ctx := New(context.Background(), logger, "test", "bar")
18+
_ = span.Log("foo")
19+
newSpan := FromContext(ctx, logger)
20+
require.Equal(t, span.Span, newSpan.Span)
21+
_ = newSpan.Log("bar")
22+
noSpan := FromContext(context.Background(), logger)
23+
_ = noSpan.Log("foo")
24+
require.Error(t, noSpan.Error(errors.New("err")))
25+
require.NoError(t, noSpan.Error(nil))
26+
}
27+
28+
func TestSpanLogger_CustomLogger(t *testing.T) {
29+
var logged [][]interface{}
30+
var logger funcLogger = func(keyvals ...interface{}) error {
31+
logged = append(logged, keyvals)
32+
return nil
33+
}
34+
span, ctx := New(context.Background(), logger, "test")
35+
_ = span.Log("msg", "original spanlogger")
36+
37+
span = FromContext(ctx, log.NewNopLogger())
38+
_ = span.Log("msg", "restored spanlogger")
39+
40+
span = FromContext(context.Background(), logger)
41+
_ = span.Log("msg", "fallback spanlogger")
42+
43+
expect := [][]interface{}{
44+
{"method", "test", "msg", "original spanlogger"},
45+
{"msg", "restored spanlogger"},
46+
{"msg", "fallback spanlogger"},
47+
}
48+
require.Equal(t, expect, logged)
49+
}
50+
51+
func TestSpanCreatedWithTenantTag(t *testing.T) {
52+
mockSpan := createSpan(user.InjectOrgID(context.Background(), "team-a"))
53+
54+
require.Equal(t, []string{"team-a"}, mockSpan.Tag(TenantIDsTagName))
55+
}
56+
57+
func TestSpanCreatedWithoutTenantTag(t *testing.T) {
58+
mockSpan := createSpan(context.Background())
59+
60+
_, exist := mockSpan.Tags()[TenantIDsTagName]
61+
require.False(t, exist)
62+
}
63+
64+
func createSpan(ctx context.Context) *mocktracer.MockSpan {
65+
mockTracer := mocktracer.New()
66+
opentracing.SetGlobalTracer(mockTracer)
67+
68+
logger, _ := New(ctx, log.NewNopLogger(), "name")
69+
return logger.Span.(*mocktracer.MockSpan)
70+
}
71+
72+
type funcLogger func(keyvals ...interface{}) error
73+
74+
func (f funcLogger) Log(keyvals ...interface{}) error {
75+
return f(keyvals...)
76+
}

0 commit comments

Comments
 (0)