diff --git a/libpf/apm.go b/libpf/apm.go index 903316b05..530c6ab3a 100644 --- a/libpf/apm.go +++ b/libpf/apm.go @@ -8,3 +8,4 @@ type APMTraceID [16]byte type APMTransactionID = APMSpanID var InvalidAPMSpanID = APMSpanID{0, 0, 0, 0, 0, 0, 0, 0} +var InvalidAPMTraceID = APMTraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} diff --git a/reporter/internal/pdata/generate.go b/reporter/internal/pdata/generate.go index 830c77ca8..9aeb4a3aa 100644 --- a/reporter/internal/pdata/generate.go +++ b/reporter/internal/pdata/generate.go @@ -61,6 +61,7 @@ func (p *Pdata) Generate(tree samples.TraceEventsTree, mappingSet := make(orderedset.OrderedSet[libpf.FrameMapping], 64) stackSet := make(orderedset.OrderedSet[stackInfo], 64) locationSet := make(orderedset.OrderedSet[locationInfo], 64) + linkSet := make(orderedset.OrderedSet[linkInfo], 64) // By specification, the first element should be empty. stringSet.Add("") @@ -68,6 +69,7 @@ func (p *Pdata) Generate(tree samples.TraceEventsTree, mappingSet.Add(libpf.FrameMapping{}) stackSet.Add(stackInfo{}) locationSet.Add(locationInfo{}) + linkSet.Add(linkInfo{}) dic.LinkTable().AppendEmpty() dic.MappingTable().AppendEmpty() @@ -103,7 +105,7 @@ func (p *Pdata) Generate(tree samples.TraceEventsTree, prof := sp.Profiles().AppendEmpty() if err := p.setProfile(dic, attrMgr, - stringSet, funcSet, mappingSet, stackSet, locationSet, + stringSet, funcSet, mappingSet, stackSet, locationSet, linkSet, origin, toEvents.Events[origin], prof, collectionStartTime, collectionEndTime); err != nil { return profiles, err @@ -143,6 +145,7 @@ func (p *Pdata) setProfile( mappingSet orderedset.OrderedSet[libpf.FrameMapping], stackSet orderedset.OrderedSet[stackInfo], locationSet orderedset.OrderedSet[locationInfo], + linkSet orderedset.OrderedSet[linkInfo], origin libpf.Origin, events samples.SampleToEvents, profile pprofile.Profile, @@ -177,6 +180,21 @@ func (p *Pdata) setProfile( sample.Values().Append(traceInfo.OffTimes...) } + if sampleKey.SpanID != libpf.InvalidAPMSpanID && + sampleKey.TraceID != libpf.InvalidAPMTraceID { + link, ok := linkSet.AddWithCheck(linkInfo{ + traceID: sampleKey.TraceID, + spanID: sampleKey.SpanID, + }) + if !ok { + l := dic.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID(sampleKey.SpanID)) + l.SetTraceID(pcommon.TraceID(sampleKey.TraceID)) + + } + sample.SetLinkIndex(link) + } + locationIndices := make([]int32, 0, len(traceInfo.Frames)) // Walk every frame of the trace. for _, uniqueFrame := range traceInfo.Frames { diff --git a/reporter/internal/pdata/generate_test.go b/reporter/internal/pdata/generate_test.go index f9fc35293..ef86f701d 100644 --- a/reporter/internal/pdata/generate_test.go +++ b/reporter/internal/pdata/generate_test.go @@ -593,7 +593,15 @@ func TestGenerate_NativeFrame(t *testing.T) { } events := map[libpf.Origin]samples.SampleToEvents{ support.TraceOriginSampling: { - {}: &samples.TraceEvents{ + { + Hash: libpf.NewTraceHash(0, 1), + Comm: libpf.Intern("abc"), + TID: 42, + CPU: 73, + SpanID: libpf.APMSpanID{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}, + TraceID: libpf.APMTraceID{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}, + }: &samples.TraceEvents{ Frames: singleFrameNative(mappingFile, 0x1000, 0x1000, 0x2000, 0x100), Timestamps: []uint64{ uint64(time.Unix(1010, 0).UnixNano()), @@ -668,6 +676,50 @@ func TestGenerate_NativeFrame(t *testing.T) { // since it's resolved by the backend. The function table should be empty. assert.Equal(t, 1, dic.FunctionTable().Len(), "Function table should be empty for native frames") + + // Verify SpanID and TraceID are set via Link + linkIndex := sample.LinkIndex() + assert.Greater(t, linkIndex, int32(0), "Sample should have a link set (index > 0, since 0 is dummy)") + link := dic.LinkTable().At(int(linkIndex)) + expectedSpanID := pcommon.SpanID{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7} + expectedTraceID := pcommon.TraceID{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27} + assert.Equal(t, expectedSpanID, link.SpanID()) + assert.Equal(t, expectedTraceID, link.TraceID()) + + // Verify Comm, TID, and CPU are set in sample attributes + attributeIndices := sample.AttributeIndices().AsRaw() + assert.NotEmpty(t, attributeIndices, "Sample should have attributes") + + attributeTable := dic.AttributeTable() + stringTable := dic.StringTable() + + foundComm := false + foundTID := false + foundCPU := false + + for _, attrIdx := range attributeIndices { + attr := attributeTable.At(int(attrIdx)) + keyStrIdx := attr.KeyStrindex() + key := stringTable.At(int(keyStrIdx)) + + switch key { + case string(semconv.ThreadNameKey): + assert.Equal(t, "abc", attr.Value().Str()) + foundComm = true + case string(semconv.ThreadIDKey): + assert.Equal(t, int64(42), attr.Value().Int()) + foundTID = true + case string(semconv.CPULogicalNumberKey): + assert.Equal(t, int64(73), attr.Value().Int()) + foundCPU = true + } + } + + assert.True(t, foundComm, "Sample should have Comm attribute set") + assert.True(t, foundTID, "Sample should have TID attribute set") + assert.True(t, foundCPU, "Sample should have CPU attribute set") + } func TestStackTableOrder(t *testing.T) { @@ -761,7 +813,15 @@ func TestGenerate_Validate(t *testing.T) { } events := map[libpf.Origin]samples.SampleToEvents{ support.TraceOriginSampling: { - {}: &samples.TraceEvents{ + { + Hash: libpf.NewTraceHash(0, 1), + Comm: libpf.Intern("abc"), + TID: 42, + CPU: 73, + SpanID: libpf.APMSpanID{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}, + TraceID: libpf.APMTraceID{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}, + }: &samples.TraceEvents{ Frames: singleFrameTrace(libpf.PythonFrame, mapping, 0x30, funcName, filePath, 123), Timestamps: []uint64{42}, diff --git a/reporter/internal/pdata/helper.go b/reporter/internal/pdata/helper.go index 0a86418aa..7927f0f95 100644 --- a/reporter/internal/pdata/helper.go +++ b/reporter/internal/pdata/helper.go @@ -35,3 +35,9 @@ func hashLocationIndices(locationIndices []int32) uint64 { h.Write(pfunsafe.FromSlice(locationIndices)) return h.Sum64() } + +// linkInfo is a helper used to deduplicate Links. +type linkInfo struct { + spanID libpf.APMSpanID + traceID libpf.APMTraceID +} diff --git a/reporter/samples/samples.go b/reporter/samples/samples.go index a0f08b2c8..cbed57ebf 100644 --- a/reporter/samples/samples.go +++ b/reporter/samples/samples.go @@ -19,6 +19,8 @@ type TraceEventMeta struct { Origin libpf.Origin OffTime int64 PID, TID libpf.PID + SpanID libpf.APMSpanID + TraceID libpf.APMTraceID } // TraceEvents holds known information about a trace. @@ -78,4 +80,7 @@ type SampleKey struct { TID int64 CPU int64 + + SpanID libpf.APMSpanID + TraceID libpf.APMTraceID }