Skip to content

Commit 1ed7e54

Browse files
authored
fix(llmobs): fix llmobs to apm link for spans (#4104)
Co-authored-by: rodrigo.arguello <[email protected]>
1 parent 61f9647 commit 1ed7e54

File tree

3 files changed

+135
-4
lines changed

3 files changed

+135
-4
lines changed

internal/llmobs/llmobs.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,15 @@ func (l *LLMObs) llmobsSpanEvent(span *Span) *transport.LLMObsSpanEvent {
578578
tagsSlice = append(tagsSlice, fmt.Sprintf("%s:%s", k, v))
579579
}
580580

581+
ddAttrs := transport.DDAttributes{
582+
SpanID: spanID,
583+
TraceID: span.llmTraceID,
584+
APMTraceID: span.apm.TraceID(),
585+
}
586+
if span.scope != "" {
587+
ddAttrs.Scope = span.scope
588+
}
589+
581590
ev := &transport.LLMObsSpanEvent{
582591
SpanID: spanID,
583592
TraceID: span.llmTraceID,
@@ -593,7 +602,7 @@ func (l *LLMObs) llmobsSpanEvent(span *Span) *transport.LLMObsSpanEvent {
593602
Metrics: span.llmCtx.metrics,
594603
CollectionErrors: nil,
595604
SpanLinks: span.spanLinks,
596-
Scope: span.scope,
605+
DDAttributes: ddAttrs,
597606
}
598607
if b, err := json.Marshal(ev); err == nil {
599608
rawSize := len(b)

internal/llmobs/llmobs_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"net/http/httptest"
1313
"os"
14+
"strconv"
1415
"strings"
1516
"testing"
1617
"time"
@@ -1844,3 +1845,117 @@ func (rt *tracedRT) RoundTrip(req *http.Request) (*http.Response, error) {
18441845
func ptrFromVal[T any](v T) *T {
18451846
return &v
18461847
}
1848+
1849+
func TestDDAttributes(t *testing.T) {
1850+
t.Run("regular-span", func(t *testing.T) {
1851+
tt, ll := testTracer(t)
1852+
ctx := context.Background()
1853+
1854+
span, _ := ll.StartSpan(ctx, llmobs.SpanKindLLM, "test-llm", llmobs.StartSpanConfig{})
1855+
span.Finish(llmobs.FinishSpanConfig{})
1856+
1857+
apmSpans := tt.WaitForSpans(t, 1)
1858+
llmSpans := tt.WaitForLLMObsSpans(t, 1)
1859+
1860+
apmSpan := apmSpans[0]
1861+
llmSpan := llmSpans[0]
1862+
1863+
assert.NotEmpty(t, llmSpan.DDAttributes.SpanID, "DDAttributes.SpanID should be populated")
1864+
assert.NotEmpty(t, llmSpan.DDAttributes.TraceID, "DDAttributes.TraceID should be populated")
1865+
assert.NotEmpty(t, llmSpan.DDAttributes.APMTraceID, "DDAttributes.APMTraceID should be populated")
1866+
1867+
assert.Equal(t, llmSpan.SpanID, llmSpan.DDAttributes.SpanID, "DDAttributes.SpanID should match SpanID")
1868+
assert.Equal(t, llmSpan.TraceID, llmSpan.DDAttributes.TraceID, "DDAttributes.TraceID should match TraceID")
1869+
assert.NotEqual(t, llmSpan.DDAttributes.TraceID, llmSpan.DDAttributes.APMTraceID, "LLMObs trace ID should differ from DDAttributes.APMTraceID")
1870+
1871+
// compare only the lower 64 bits of the trace ID
1872+
low64Hex := llmSpan.DDAttributes.APMTraceID[len(llmSpan.DDAttributes.APMTraceID)-16:]
1873+
low64HexUint, err := strconv.ParseUint(low64Hex, 16, 64)
1874+
require.NoError(t, err)
1875+
assert.Equal(t, apmSpan.TraceID, low64HexUint, "APM trace ID should match DDAttributes.APMTraceID")
1876+
1877+
// Verify Scope is empty for regular spans
1878+
assert.Empty(t, llmSpan.DDAttributes.Scope, "DDAttributes.Scope should be empty for regular spans")
1879+
})
1880+
t.Run("experiment-span", func(t *testing.T) {
1881+
tt, ll := testTracer(t)
1882+
ctx := context.Background()
1883+
1884+
experimentID := "test-experiment-123"
1885+
span, _ := ll.StartExperimentSpan(ctx, "test-experiment", experimentID, llmobs.StartSpanConfig{})
1886+
span.Finish(llmobs.FinishSpanConfig{})
1887+
1888+
apmSpans := tt.WaitForSpans(t, 1)
1889+
llmSpans := tt.WaitForLLMObsSpans(t, 1)
1890+
1891+
apmSpan := apmSpans[0]
1892+
llmSpan := llmSpans[0]
1893+
1894+
assert.NotEmpty(t, llmSpan.DDAttributes.SpanID, "DDAttributes.SpanID should be populated")
1895+
assert.NotEmpty(t, llmSpan.DDAttributes.TraceID, "DDAttributes.TraceID should be populated")
1896+
assert.NotEmpty(t, llmSpan.DDAttributes.APMTraceID, "DDAttributes.APMTraceID should be populated")
1897+
1898+
assert.Equal(t, llmSpan.SpanID, llmSpan.DDAttributes.SpanID, "DDAttributes.SpanID should match SpanID")
1899+
assert.Equal(t, llmSpan.TraceID, llmSpan.DDAttributes.TraceID, "DDAttributes.TraceID should match TraceID")
1900+
assert.NotEqual(t, llmSpan.DDAttributes.TraceID, llmSpan.DDAttributes.APMTraceID, "LLMObs trace ID should differ from DDAttributes.APMTraceID")
1901+
1902+
assertAPMTraceID(t, apmSpan, llmSpan)
1903+
1904+
// Verify Scope is set to "experiments"
1905+
assert.Equal(t, "experiments", llmSpan.DDAttributes.Scope, "DDAttributes.Scope should be 'experiments' for experiment spans")
1906+
})
1907+
t.Run("child-span-trace-ids", func(t *testing.T) {
1908+
tt, ll := testTracer(t)
1909+
ctx := context.Background()
1910+
1911+
parentSpan, ctx := ll.StartSpan(ctx, llmobs.SpanKindWorkflow, "parent-workflow", llmobs.StartSpanConfig{})
1912+
childSpan, _ := ll.StartSpan(ctx, llmobs.SpanKindLLM, "child-llm", llmobs.StartSpanConfig{})
1913+
1914+
childSpan.Finish(llmobs.FinishSpanConfig{})
1915+
parentSpan.Finish(llmobs.FinishSpanConfig{})
1916+
1917+
apmSpans := tt.WaitForSpans(t, 2)
1918+
llmSpans := tt.WaitForLLMObsSpans(t, 2)
1919+
1920+
var parentLLM, childLLM *llmobstransport.LLMObsSpanEvent
1921+
for i := range llmSpans {
1922+
if llmSpans[i].Name == "parent-workflow" {
1923+
parentLLM = &llmSpans[i]
1924+
} else if llmSpans[i].Name == "child-llm" {
1925+
childLLM = &llmSpans[i]
1926+
}
1927+
}
1928+
1929+
var parentAPM, childAPM *testtracer.Span
1930+
for i := range apmSpans {
1931+
if apmSpans[i].Name == "parent-workflow" {
1932+
parentAPM = &apmSpans[i]
1933+
} else if apmSpans[i].Name == "child-llm" {
1934+
childAPM = &apmSpans[i]
1935+
}
1936+
}
1937+
1938+
require.NotNil(t, parentLLM, "Parent LLM span should exist")
1939+
require.NotNil(t, childLLM, "Child LLM span should exist")
1940+
require.NotNil(t, parentAPM, "Parent APM span should exist")
1941+
require.NotNil(t, childAPM, "Child APM span should exist")
1942+
1943+
assert.Equal(t, parentLLM.DDAttributes.TraceID, childLLM.DDAttributes.TraceID,
1944+
"Parent and child should have the same LLMObs trace ID in DDAttributes")
1945+
assert.Equal(t, parentLLM.DDAttributes.APMTraceID, childLLM.DDAttributes.APMTraceID,
1946+
"Parent and child should have the same APM trace ID in DDAttributes")
1947+
assert.NotEqual(t, parentLLM.DDAttributes.TraceID, parentLLM.DDAttributes.APMTraceID,
1948+
"LLMObs trace ID should differ from APM trace ID")
1949+
1950+
assertAPMTraceID(t, *parentAPM, *parentLLM)
1951+
assertAPMTraceID(t, *childAPM, *childLLM)
1952+
})
1953+
}
1954+
1955+
func assertAPMTraceID(t *testing.T, apmSpan testtracer.Span, llmSpan llmobstransport.LLMObsSpanEvent) {
1956+
// compare only the lower 64 bits of the trace ID
1957+
low64Hex := llmSpan.DDAttributes.APMTraceID[len(llmSpan.DDAttributes.APMTraceID)-16:]
1958+
low64HexUint, err := strconv.ParseUint(low64Hex, 16, 64)
1959+
require.NoError(t, err)
1960+
assert.Equal(t, apmSpan.TraceID, low64HexUint, "APM trace ID should match DDAttributes.APMTraceID")
1961+
}

internal/llmobs/transport/span.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ type SpanLink struct {
2222
Flags uint32 `json:"flags,omitempty"`
2323
}
2424

25+
type DDAttributes struct {
26+
SpanID string `json:"span_id"`
27+
TraceID string `json:"trace_id"`
28+
APMTraceID string `json:"apm_trace_id"`
29+
Scope string `json:"scope,omitempty"`
30+
}
31+
2532
type LLMObsSpanEvent struct {
2633
SpanID string `json:"span_id,omitempty"`
2734
TraceID string `json:"trace_id,omitempty"`
@@ -37,7 +44,7 @@ type LLMObsSpanEvent struct {
3744
Metrics map[string]float64 `json:"metrics,omitempty"`
3845
CollectionErrors []string `json:"collection_errors,omitempty"`
3946
SpanLinks []SpanLink `json:"span_links,omitempty"`
40-
Scope string `json:"-"`
47+
DDAttributes DDAttributes `json:"_dd"`
4148
}
4249

4350
type PushSpanEventsRequest struct {
@@ -65,8 +72,8 @@ func (c *Transport) PushSpanEvents(
6572
EventType: "span",
6673
Spans: []*LLMObsSpanEvent{ev},
6774
}
68-
if ev.Scope != "" {
69-
req.Scope = ev.Scope
75+
if ev.DDAttributes.Scope != "" {
76+
req.Scope = ev.DDAttributes.Scope
7077
}
7178
body = append(body, req)
7279
}

0 commit comments

Comments
 (0)