diff --git a/contrib/gocql/gocql/gocql.go b/contrib/gocql/gocql/gocql.go index 7881392ba4..fd39fe454e 100644 --- a/contrib/gocql/gocql/gocql.go +++ b/contrib/gocql/gocql/gocql.go @@ -205,6 +205,14 @@ func (tq *Query) MapScan(m map[string]interface{}) error { return err } +// MapScanCAS wraps in a span query.MapScanCAS call. +func (tq *Query) MapScanCAS(m map[string]interface{}) (applied bool, err error) { + span := tq.newChildSpan(tq.ctx) + applied, err = tq.Query.MapScanCAS(m) + tq.finishSpan(span, err) + return applied, err +} + // Scan wraps in a span query.Scan call. func (tq *Query) Scan(dest ...interface{}) error { span := tq.newChildSpan(tq.ctx) 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/exec_tracer_test.go b/ddtrace/tracer/exec_tracer_test.go deleted file mode 100644 index 0ff8e239f9..0000000000 --- a/ddtrace/tracer/exec_tracer_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// 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. - -// Tests in this file rely on parsing execution tracer data, which can change -// formats across Go releases. This guard should be updated as the Go trace -// parser library is upgraded to support new versions. -//go:build !go1.21 - -package tracer - -import ( - "bytes" - "context" - "encoding/binary" - rt "runtime/trace" - "testing" - - "github.com/stretchr/testify/assert" - gotraceui "honnef.co/go/gotraceui/trace" -) - -func TestExecutionTraceSpans(t *testing.T) { - if rt.IsEnabled() { - t.Skip("runtime execution tracing is already enabled") - } - - buf := new(bytes.Buffer) - if err := rt.Start(buf); err != nil { - t.Fatal(err) - } - // Ensure we unconditionally stop tracing. It's safe to call this - // multiple times. - defer rt.Stop() - - _, _, _, stop := startTestTracer(t) - defer stop() - - root, ctx := StartSpanFromContext(context.Background(), "root") - child, _ := StartSpanFromContext(ctx, "child") - root.Finish() - child.Finish() - - rt.Stop() - - execTrace, err := gotraceui.Parse(buf, nil) - if err != nil { - t.Fatalf("parsing trace: %s", err) - } - - type traceSpan struct { - name string - parent string - spanID uint64 - } - - spans := make(map[int]*traceSpan) - for _, ev := range execTrace.Events { - switch ev.Type { - case gotraceui.EvUserTaskCreate: - id := int(ev.Args[0]) - name := execTrace.Strings[ev.Args[2]] - var parent string - if p, ok := spans[int(ev.Args[1])]; ok { - parent = p.name - } - spans[id] = &traceSpan{ - name: name, - parent: parent, - } - case gotraceui.EvUserLog: - id := int(ev.Args[0]) - span, ok := spans[id] - if !ok { - continue - } - key := execTrace.Strings[ev.Args[1]] - if key == "datadog.uint64_span_id" { - span.spanID = binary.LittleEndian.Uint64([]byte(execTrace.Strings[ev.Args[3]])) - } - } - } - - want := []traceSpan{ - {name: "root", spanID: root.Context().SpanID()}, - {name: "child", parent: "root", spanID: child.Context().SpanID()}, - } - var got []traceSpan - for _, v := range spans { - got = append(got, *v) - } - - assert.ElementsMatch(t, want, got) -} 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) } diff --git a/internal/apps/go.sum b/internal/apps/go.sum index 2ed8e429d9..7776e817cb 100644 --- a/internal/apps/go.sum +++ b/internal/apps/go.sum @@ -201,8 +201,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ= -honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= diff --git a/internal/exectracetest/exectrace_test.go b/internal/exectracetest/exectrace_test.go index ffeb280d66..9a8799217f 100644 --- a/internal/exectracetest/exectrace_test.go +++ b/internal/exectracetest/exectrace_test.go @@ -16,7 +16,9 @@ package exectracetest import ( "bytes" "context" + "encoding/binary" "io" + "reflect" "regexp" "runtime/pprof" "runtime/trace" @@ -139,3 +141,59 @@ func TestSpanDoubleFinish(t *testing.T) { } // TODO: move database/sql tests here? likely requires copying over contrib/sql/internal.MockDriver + +func TestExecutionTraceSpans(t *testing.T) { + var root, child tracer.Span + _, execTrace := collectTestData(t, func() { + tracer.Start(tracer.WithLogger(discardLogger{})) + defer tracer.Stop() + var ctx context.Context + root, ctx = tracer.StartSpanFromContext(context.Background(), "root") + child, _ = tracer.StartSpanFromContext(ctx, "child") + root.Finish() + child.Finish() + }) + + type traceSpan struct { + name string + parent string + spanID uint64 + } + + spans := make(map[exptrace.TaskID]*traceSpan) + for _, ev := range execTrace { + switch ev.Kind() { + case exptrace.EventTaskBegin: + task := ev.Task() + var parent string + if p, ok := spans[task.Parent]; ok { + parent = p.name + } + spans[task.ID] = &traceSpan{ + name: task.Type, + parent: parent, + } + case exptrace.EventLog: + span, ok := spans[ev.Log().Task] + if !ok { + continue + } + if key := ev.Log().Category; key == "datadog.uint64_span_id" { + span.spanID = binary.LittleEndian.Uint64([]byte(ev.Log().Message)) + } + } + } + + want := []traceSpan{ + {name: "root", spanID: root.Context().SpanID()}, + {name: "child", parent: "root", spanID: child.Context().SpanID()}, + } + var got []traceSpan + for _, v := range spans { + got = append(got, *v) + } + + if !reflect.DeepEqual(want, got) { + t.Errorf("wanted spans %+v, got %+v", want, got) + } +} diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index bd8c1c5f19..b0b547e961 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -170,8 +170,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ= -honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=