Skip to content
Open
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
60 changes: 60 additions & 0 deletions ddtrace/opentelemetry/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ package opentelemetry
import (
"encoding/binary"
"errors"
"fmt"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
Expand All @@ -19,6 +22,7 @@ import (

"go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
oteltrace "go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
Expand Down Expand Up @@ -206,6 +210,62 @@ func (s *span) AddEvent(name string, opts ...oteltrace.EventOption) {
s.events = append(s.events, e)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side note, it looks as if s.events is missing mutex protection when being written on this line. Let me know and I am happy to add lock/unlock to the AddEvent method.

}

// RecordError will record err as an exception span event for this span. An
// additional call to SetStatus is required if the Status of the Span should
// be set to Error, as this method does not change the Span status. If this
// span is not being recorded or err is nil then this method does nothing.
func (s *span) RecordError(err error, opts ...oteltrace.EventOption) {
if !s.IsRecording() || err == nil {
return
}

opts = append(opts, oteltrace.WithAttributes(
semconv.ExceptionType(typeStr(err)),
semconv.ExceptionMessage(err.Error()),
))

cfg := oteltrace.NewEventConfig(opts...)
if cfg.StackTrace() {
buf := make([]byte, 2048)
n := runtime.Stack(buf, false)
stacktrace := string(buf[0:n])
opts = append(opts, oteltrace.WithAttributes(
semconv.ExceptionStacktrace(stacktrace),
))
}

s.AddEvent(semconv.ExceptionEventName, opts...)
}

func typeStr(i any) string {
t := reflect.TypeOf(i)
if t.PkgPath() == "" && t.Name() == "" {
// Likely a builtin type.
return t.String()
}
return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
}

// AddLink adds OTel Span Links to the underlying Datadog span.
func (s *span) AddLink(link oteltrace.Link) {
if !s.IsRecording() || !link.SpanContext.IsValid() {
return
}
ctx := otelCtxToDDCtx{link.SpanContext}
attrs := make(map[string]string, len(link.Attributes))
for _, a := range link.Attributes {
attrs[string(a.Key)] = a.Value.Emit()
}
s.DD.AddLink(tracer.SpanLink{
TraceID: ctx.TraceIDLower(),
TraceIDHigh: ctx.TraceIDUpper(),
SpanID: ctx.SpanID(),
Tracestate: link.SpanContext.TraceState().String(),
Attributes: attrs,
Flags: uint32(link.SpanContext.TraceFlags()) | (1 << 31),
})
}

// SetAttributes sets the key-value pairs as tags on the span.
// Every value is propagated as an interface.
// Some attribute keys are reserved and will be remapped to Datadog reserved tags.
Expand Down
90 changes: 90 additions & 0 deletions ddtrace/opentelemetry/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,62 @@ func TestSpanLink(t *testing.T) {
assert.Equal(uint32(0x80000001), spanLinks[0].Flags) // sampled and set
}

func TestSpanAddLink(t *testing.T) {
assert := assert.New(t)
_, payloads, cleanup := mockTracerProvider(t)
tr := otel.Tracer("")
defer cleanup()

// Create a span
_, span := tr.Start(context.Background(), "span_add_link")

// Create a link to a remote span
traceID, _ := oteltrace.TraceIDFromHex("00000000000001c8000000000000007b")
spanID, _ := oteltrace.SpanIDFromHex("000000000000000f")
traceStateVal := "dd_origin=ci"
traceState, _ := oteltrace.ParseTraceState(traceStateVal)
remoteSpanContext := oteltrace.NewSpanContext(
oteltrace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: oteltrace.FlagsSampled,
TraceState: traceState,
Remote: true,
},
)
link := oteltrace.Link{
SpanContext: remoteSpanContext,
Attributes: []attribute.KeyValue{attribute.String("link.name", "alpha_transaction")},
}

// Add the link to the span and end the span
span.AddLink(link)
span.End()

tracer.Flush()
payload, err := waitForPayload(payloads)
if err != nil {
t.Fatal(err.Error())
}
assert.NotNil(payload)
assert.Len(payload, 1) // only one trace
assert.Len(payload[0], 1) // only one span

// Convert the span_links field from type []map[string]interface{} to a struct
var spanLinks []tracer.SpanLink
spanLinkBytes, _ := json.Marshal(payload[0][0]["span_links"])
json.Unmarshal(spanLinkBytes, &spanLinks)
assert.Len(spanLinks, 1) // only one span link

// Ensure the span link has the correct values
assert.Equal(uint64(123), spanLinks[0].TraceID)
assert.Equal(uint64(456), spanLinks[0].TraceIDHigh)
assert.Equal(uint64(15), spanLinks[0].SpanID)
assert.Equal(map[string]string{"link.name": "alpha_transaction"}, spanLinks[0].Attributes)
assert.Equal(traceStateVal, spanLinks[0].Tracestate)
assert.Equal(uint32(0x80000001), spanLinks[0].Flags) // sampled and set
}

func TestSpanEnd(t *testing.T) {
assert := assert.New(t)
_, payloads, cleanup := mockTracerProvider(t)
Expand Down Expand Up @@ -402,6 +458,40 @@ func attributesContains(attrs map[string]interface{}, key string, val interface{
return false
}

func TestRecordError(t *testing.T) {
assert := assert.New(t)
_, _, cleanup := mockTracerProvider(t)
tr := otel.Tracer("")
defer cleanup()

errMsg := "something went wrong"
_, sp := tr.Start(context.Background(), "span_record_error")

// Record the error as an event
sp.RecordError(errors.New(errMsg), oteltrace.WithStackTrace(true))
sp.End()

dd := sp.(*span)
assert.Len(dd.events, 1)
e := dd.events[0]
assert.Equal("exception", e.name)

cfg := tracer.SpanEventConfig{}
for _, opt := range e.options {
opt(&cfg)
}

// Assert expected attributes exist on the event
assert.Len(cfg.Attributes, 3)

assert.True(attributesContains(cfg.Attributes, "exception.message", errMsg))
assert.True(attributesContains(cfg.Attributes, "exception.type", "*errors.errorString"))
assert.Contains(cfg.Attributes, "exception.stacktrace")

// verify status did not change to error since RecordError should not change span status
assert.Equal(codes.Unset, dd.statusInfo.code)
}

func TestSpanContextWithStartOptions(t *testing.T) {
assert := assert.New(t)
_, payloads, cleanup := mockTracerProvider(t)
Expand Down