diff --git a/contrib/log/slog/example_test.go b/contrib/log/slog/example_test.go new file mode 100644 index 0000000000..0e5683b897 --- /dev/null +++ b/contrib/log/slog/example_test.go @@ -0,0 +1,48 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package slog_test + +import ( + "context" + "log/slog" + "os" + + slogtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/log/slog" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +func ExampleNewJSONHandler() { + // start the DataDog tracer + tracer.Start() + defer tracer.Stop() + + // create the application logger + logger := slog.New(slogtrace.NewJSONHandler(os.Stdout, nil)) + + // start a new span + span, ctx := tracer.StartSpanFromContext(context.Background(), "ExampleNewJSONHandler") + defer span.Finish() + + // log a message using the context containing span information + logger.Log(ctx, slog.LevelInfo, "this is a log with tracing information") +} + +func ExampleWrapHandler() { + // start the DataDog tracer + tracer.Start() + defer tracer.Stop() + + // create the application logger + myHandler := slog.NewJSONHandler(os.Stdout, nil) + logger := slog.New(slogtrace.WrapHandler(myHandler)) + + // start a new span + span, ctx := tracer.StartSpanFromContext(context.Background(), "ExampleWrapHandler") + defer span.Finish() + + // log a message using the context containing span information + logger.Log(ctx, slog.LevelInfo, "this is a log with tracing information") +} diff --git a/contrib/log/slog/slog.go b/contrib/log/slog/slog.go new file mode 100644 index 0000000000..1a27186a27 --- /dev/null +++ b/contrib/log/slog/slog.go @@ -0,0 +1,51 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Package slog provides functions to correlate logs and traces using log/slog package (https://pkg.go.dev/log/slog). +package slog // import "gopkg.in/DataDog/dd-trace-go.v1/contrib/log/slog" + +import ( + "context" + "io" + "log/slog" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" +) + +const componentName = "log/slog" + +func init() { + telemetry.LoadIntegration(componentName) + tracer.MarkIntegrationImported("log/slog") +} + +// NewJSONHandler is a convenience function that returns a *slog.JSONHandler logger enhanced with +// tracing information. +func NewJSONHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler { + return WrapHandler(slog.NewJSONHandler(w, opts)) +} + +// WrapHandler enhances the given logger handler attaching tracing information to logs. +func WrapHandler(h slog.Handler) slog.Handler { + return &handler{h} +} + +type handler struct { + slog.Handler +} + +// Handle handles the given Record, attaching tracing information if found. +func (h *handler) Handle(ctx context.Context, rec slog.Record) error { + span, ok := tracer.SpanFromContext(ctx) + if ok { + rec.Add( + slog.Uint64(ext.LogKeyTraceID, span.Context().TraceID()), + slog.Uint64(ext.LogKeySpanID, span.Context().SpanID()), + ) + } + return h.Handler.Handle(ctx, rec) +} diff --git a/contrib/log/slog/slog_test.go b/contrib/log/slog/slog_test.go new file mode 100644 index 0000000000..5b74691469 --- /dev/null +++ b/contrib/log/slog/slog_test.go @@ -0,0 +1,76 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package slog + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + internallog "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +func assertLogEntry(t *testing.T, rawEntry, wantMsg, wantLevel string) { + t.Helper() + + var data map[string]interface{} + err := json.Unmarshal([]byte(rawEntry), &data) + require.NoError(t, err) + require.NotEmpty(t, data) + + assert.Equal(t, wantMsg, data["msg"]) + assert.Equal(t, wantLevel, data["level"]) + assert.NotEmpty(t, data["time"]) + assert.NotEmpty(t, data[ext.LogKeyTraceID]) + assert.NotEmpty(t, data[ext.LogKeySpanID]) +} + +func testLogger(t *testing.T, createHandler func(b *bytes.Buffer) slog.Handler) { + tracer.Start(tracer.WithLogger(internallog.DiscardLogger{})) + defer tracer.Stop() + + // create the application logger + var b bytes.Buffer + h := createHandler(&b) + logger := slog.New(h) + + // start a new span + span, ctx := tracer.StartSpanFromContext(context.Background(), "test") + defer span.Finish() + + // log a message using the context containing span information + logger.Log(ctx, slog.LevelInfo, "this is an info log with tracing information") + logger.Log(ctx, slog.LevelError, "this is an error log with tracing information") + + logs := strings.Split( + strings.TrimRight(b.String(), "\n"), + "\n", + ) + // assert log entries contain trace information + require.Len(t, logs, 2) + assertLogEntry(t, logs[0], "this is an info log with tracing information", "INFO") + assertLogEntry(t, logs[1], "this is an error log with tracing information", "ERROR") +} + +func TestNewJSONHandler(t *testing.T) { + testLogger(t, func(b *bytes.Buffer) slog.Handler { + return NewJSONHandler(b, nil) + }) +} + +func TestWrapHandler(t *testing.T) { + testLogger(t, func(b *bytes.Buffer) slog.Handler { + return WrapHandler(slog.NewJSONHandler(b, nil)) + }) +} diff --git a/contrib/sirupsen/logrus/logrus.go b/contrib/sirupsen/logrus/logrus.go index cce5dd43f4..d0e379d796 100644 --- a/contrib/sirupsen/logrus/logrus.go +++ b/contrib/sirupsen/logrus/logrus.go @@ -7,6 +7,7 @@ package logrus import ( + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" @@ -34,7 +35,7 @@ func (d *DDContextLogHook) Fire(e *logrus.Entry) error { if !found { return nil } - e.Data["dd.trace_id"] = span.Context().TraceID() - e.Data["dd.span_id"] = span.Context().SpanID() + e.Data[ext.LogKeyTraceID] = span.Context().TraceID() + e.Data[ext.LogKeySpanID] = span.Context().SpanID() return nil } diff --git a/ddtrace/ext/log_key.go b/ddtrace/ext/log_key.go new file mode 100644 index 0000000000..b17e098ffa --- /dev/null +++ b/ddtrace/ext/log_key.go @@ -0,0 +1,13 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package ext + +const ( + // LogKeyTraceID is used by log integrations to correlate logs with a given trace. + LogKeyTraceID = "dd.trace_id" + // LogKeySpanID is used by log integrations to correlate logs with a given span. + LogKeySpanID = "dd.span_id" +) diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 9d7bdfcbed..d8ce6843a2 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -95,6 +95,7 @@ var contribIntegrations = map[string]struct { "github.com/urfave/negroni": {"Negroni", false}, "github.com/valyala/fasthttp": {"FastHTTP", false}, "github.com/zenazn/goji": {"Goji", false}, + "log/slog": {"log/slog", false}, } var ( diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 51efde3363..7ef25b0acd 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -255,7 +255,7 @@ func TestAgentIntegration(t *testing.T) { defer clearIntegrationsForTests() cfg.loadContribIntegrations(nil) - assert.Equal(t, len(cfg.integrations), 55) + assert.Equal(t, 56, len(cfg.integrations)) for integrationName, v := range cfg.integrations { assert.False(t, v.Instrumented, "integrationName=%s", integrationName) }