Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add spanlogger package #42

Merged
merged 13 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
* [ENHANCEMENT] Add limiter package. #41
* [ENHANCEMENT] Add grpcclient, grpcencoding and grpcutil packages. #39
* [ENHANCEMENT] Replace go-kit/kit/log with go-kit/log. #52
* [ENHANCEMENT] Add spanlogger package. #42
52 changes: 52 additions & 0 deletions spanlogger/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package spanlogger

import (
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
)

type noopTracer struct{}

type noopSpan struct{}
type noopSpanContext struct{}

var (
defaultNoopSpanContext = noopSpanContext{}
defaultNoopSpan = noopSpan{}
defaultNoopTracer = noopTracer{}
)

const (
emptyString = ""
)

func (n noopSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {}

func (n noopSpan) Context() opentracing.SpanContext { return defaultNoopSpanContext }
func (n noopSpan) SetBaggageItem(key, val string) opentracing.Span { return defaultNoopSpan }
func (n noopSpan) BaggageItem(key string) string { return emptyString }
func (n noopSpan) SetTag(key string, value interface{}) opentracing.Span { return n }
func (n noopSpan) LogFields(fields ...log.Field) {}
func (n noopSpan) LogKV(keyVals ...interface{}) {}
func (n noopSpan) Finish() {}
func (n noopSpan) FinishWithOptions(opts opentracing.FinishOptions) {}
func (n noopSpan) SetOperationName(operationName string) opentracing.Span { return n }
func (n noopSpan) Tracer() opentracing.Tracer { return defaultNoopTracer }
func (n noopSpan) LogEvent(event string) {}
func (n noopSpan) LogEventWithPayload(event string, payload interface{}) {}
func (n noopSpan) Log(data opentracing.LogData) {}

// StartSpan belongs to the Tracer interface.
func (n noopTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span {
return defaultNoopSpan
}

// Inject belongs to the Tracer interface.
func (n noopTracer) Inject(sp opentracing.SpanContext, format interface{}, carrier interface{}) error {
return nil
}

// Extract belongs to the Tracer interface.
func (n noopTracer) Extract(format interface{}, carrier interface{}) (opentracing.SpanContext, error) {
return nil, opentracing.ErrSpanContextNotFound
}
123 changes: 123 additions & 0 deletions spanlogger/spanlogger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package spanlogger

import (
"context"
"strings"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/weaveworks/common/tracing"
"github.com/weaveworks/common/user"
)

type loggerCtxMarker struct{}

const (
// TenantIDsTagName is the tenant IDs tag name.
TenantIDsTagName = "tenant_ids"
)

var (
loggerCtxKey = &loggerCtxMarker{}
)

// SpanLogger unifies tracing and logging, to reduce repetition.
type SpanLogger struct {
log.Logger
opentracing.Span
}

// New makes a new SpanLogger with a log.Logger to send logs to. The provided context will have the logger attached
// to it and can be retrieved with FromContext.
func New(ctx context.Context, logger log.Logger, method string, kvps ...interface{}) (*SpanLogger, context.Context) {
span, ctx := opentracing.StartSpanFromContext(ctx, method)
if id := tenantID(ctx); id != "" {
span.SetTag(TenantIDsTagName, []string{id})
}
l := &SpanLogger{
Logger: log.With(withContext(ctx, logger), "method", method),
Span: span,
}
if len(kvps) > 0 {
level.Debug(l).Log(kvps...)
}

ctx = context.WithValue(ctx, loggerCtxKey, logger)
return l, ctx
}

// FromContext returns a span logger using the current parent span.
// If there is no parent span, the SpanLogger will only log to the logger
// within the context. If the context doesn't have a logger, the fallback
// logger is used.
func FromContext(ctx context.Context, fallback log.Logger) *SpanLogger {
logger, ok := ctx.Value(loggerCtxKey).(log.Logger)
if !ok {
logger = fallback
}
sp := opentracing.SpanFromContext(ctx)
if sp == nil {
sp = defaultNoopSpan
}
return &SpanLogger{
Logger: withContext(ctx, logger),
Span: sp,
}
}

// Log implements gokit's Logger interface; sends logs to underlying logger and
// also puts the on the spans.
func (s *SpanLogger) Log(kvps ...interface{}) error {
s.Logger.Log(kvps...)
fields, err := otlog.InterleavedKVToFields(kvps...)
if err != nil {
return err
}
s.Span.LogFields(fields...)
return nil
}

// Error sets error flag and logs the error on the span, if non-nil. Returns the err passed in.
func (s *SpanLogger) Error(err error) error {
if err == nil {
return nil
}
ext.Error.Set(s.Span, true)
s.Span.LogFields(otlog.Error(err))
return err
}

// tenantID tries to extract the tenant ID from ctx.
func tenantID(ctx context.Context) string {
//lint:ignore faillint wrapper around upstream method
id, err := user.ExtractOrgID(ctx)
if err != nil {
return ""
}

// handle the relative reference to current and parent path.
if id == "." || id == ".." || strings.ContainsAny(id, `\/`) {
return ""
}

return id
}

func withContext(ctx context.Context, l log.Logger) log.Logger {
// Weaveworks uses "orgs" and "orgID" to represent Cortex users,
// even though the code-base generally uses `userID` to refer to the same thing.
userID := tenantID(ctx)
if userID != "" {
l = log.With(l, "org_id", userID)
}

traceID, ok := tracing.ExtractSampledTraceID(ctx)
if !ok {
return l
}

return log.With(l, "traceID", traceID)
}
76 changes: 76 additions & 0 deletions spanlogger/spanlogger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package spanlogger

import (
"context"
"testing"

"github.com/go-kit/log"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/weaveworks/common/user"
)

func TestSpanLogger_Log(t *testing.T) {
logger := log.NewNopLogger()
span, ctx := New(context.Background(), logger, "test", "bar")
_ = span.Log("foo")
newSpan := FromContext(ctx, logger)
require.Equal(t, span.Span, newSpan.Span)
_ = newSpan.Log("bar")
noSpan := FromContext(context.Background(), logger)
_ = noSpan.Log("foo")
require.Error(t, noSpan.Error(errors.New("err")))
require.NoError(t, noSpan.Error(nil))
}

func TestSpanLogger_CustomLogger(t *testing.T) {
var logged [][]interface{}
var logger funcLogger = func(keyvals ...interface{}) error {
logged = append(logged, keyvals)
return nil
}
span, ctx := New(context.Background(), logger, "test")
_ = span.Log("msg", "original spanlogger")

span = FromContext(ctx, log.NewNopLogger())
_ = span.Log("msg", "restored spanlogger")

span = FromContext(context.Background(), logger)
_ = span.Log("msg", "fallback spanlogger")

expect := [][]interface{}{
{"method", "test", "msg", "original spanlogger"},
{"msg", "restored spanlogger"},
{"msg", "fallback spanlogger"},
}
require.Equal(t, expect, logged)
}

func TestSpanCreatedWithTenantTag(t *testing.T) {
mockSpan := createSpan(user.InjectOrgID(context.Background(), "team-a"))

require.Equal(t, []string{"team-a"}, mockSpan.Tag(TenantIDsTagName))
}

func TestSpanCreatedWithoutTenantTag(t *testing.T) {
mockSpan := createSpan(context.Background())

_, exist := mockSpan.Tags()[TenantIDsTagName]
require.False(t, exist)
}

func createSpan(ctx context.Context) *mocktracer.MockSpan {
mockTracer := mocktracer.New()
opentracing.SetGlobalTracer(mockTracer)

logger, _ := New(ctx, log.NewNopLogger(), "name")
return logger.Span.(*mocktracer.MockSpan)
}

type funcLogger func(keyvals ...interface{}) error

func (f funcLogger) Log(keyvals ...interface{}) error {
return f(keyvals...)
}