From 87e4232c5b747eb13025f46b3a10a754309b6241 Mon Sep 17 00:00:00 2001 From: Florian Lehner Date: Tue, 17 Mar 2026 10:44:26 +0100 Subject: [PATCH 1/2] reporter: report Links Extend reporter to also report Span/Trace IDs in the respective message and format. Signed-off-by: Florian Lehner --- libpf/apm.go | 1 + reporter/base_reporter.go | 2 ++ reporter/internal/pdata/generate.go | 19 ++++++++++++++++++- reporter/internal/pdata/helper.go | 7 +++++++ reporter/samples/samples.go | 5 +++++ 5 files changed, 33 insertions(+), 1 deletion(-) 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/base_reporter.go b/reporter/base_reporter.go index ddd21627f..75585bcb4 100644 --- a/reporter/base_reporter.go +++ b/reporter/base_reporter.go @@ -71,6 +71,8 @@ func (b *baseReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceE Pid: int64(meta.PID), Tid: int64(meta.TID), CPU: int64(meta.CPU), + SpanID: meta.SpanID, + TraceID: meta.TraceID, ExtraMeta: extraMeta, } diff --git a/reporter/internal/pdata/generate.go b/reporter/internal/pdata/generate.go index af7bf04ed..2668234ed 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("") @@ -104,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, + attrMgr, stringSet, funcSet, mappingSet, stackSet, locationSet, linkSet, origin, originToEvents[origin], prof, collectionStartTime, collectionEndTime); err != nil { return profiles, err @@ -143,6 +144,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 map[samples.TraceAndMetaKey]*samples.TraceEvents, profile pprofile.Profile, @@ -177,6 +179,21 @@ func (p *Pdata) setProfile( sample.Values().Append(traceInfo.OffTimes...) } + if traceKey.SpanID != libpf.InvalidAPMSpanID && + traceKey.TraceID != libpf.InvalidAPMTraceID { + link, ok := linkSet.AddWithCheck(linkInfo{ + traceID: traceKey.TraceID, + spanID: traceKey.SpanID, + }) + if !ok { + l := dic.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID(traceKey.SpanID)) + l.SetTraceID(pcommon.TraceID(traceKey.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/helper.go b/reporter/internal/pdata/helper.go index 39a0e8c06..1b773dde7 100644 --- a/reporter/internal/pdata/helper.go +++ b/reporter/internal/pdata/helper.go @@ -3,6 +3,7 @@ package pdata // import "go.opentelemetry.io/ebpf-profiler/reporter/internal/pda import ( "hash/fnv" + "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe" ) @@ -34,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 4e1559a87..50e940e90 100644 --- a/reporter/samples/samples.go +++ b/reporter/samples/samples.go @@ -19,6 +19,8 @@ type TraceEventMeta struct { Origin libpf.Origin OffTime int64 EnvVars map[libpf.String]libpf.String + SpanID libpf.APMSpanID + TraceID libpf.APMTraceID } // TraceEvents holds known information about a trace. @@ -48,6 +50,9 @@ type TraceAndMetaKey struct { // Executable path is retrieved from /proc/PID/exe ExecutablePath libpf.String + SpanID libpf.APMSpanID + TraceID libpf.APMTraceID + // ExtraMeta stores extra meta info that may have been produced by a // `SampleAttrProducer` instance. May be nil. ExtraMeta any From 2935fefa66a80e28636010d25451aee461c05187 Mon Sep 17 00:00:00 2001 From: Florian Lehner Date: Mon, 30 Mar 2026 10:19:35 +0200 Subject: [PATCH 2/2] resolve conflicts and add test Signed-off-by: Florian Lehner --- reporter/internal/pdata/generate.go | 13 ++--- reporter/internal/pdata/generate_test.go | 64 +++++++++++++++++++++++- reporter/samples/samples.go | 3 ++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/reporter/internal/pdata/generate.go b/reporter/internal/pdata/generate.go index ae4deb7ab..9aeb4a3aa 100644 --- a/reporter/internal/pdata/generate.go +++ b/reporter/internal/pdata/generate.go @@ -69,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() @@ -179,16 +180,16 @@ func (p *Pdata) setProfile( sample.Values().Append(traceInfo.OffTimes...) } - if traceKey.SpanID != libpf.InvalidAPMSpanID && - traceKey.TraceID != libpf.InvalidAPMTraceID { + if sampleKey.SpanID != libpf.InvalidAPMSpanID && + sampleKey.TraceID != libpf.InvalidAPMTraceID { link, ok := linkSet.AddWithCheck(linkInfo{ - traceID: traceKey.TraceID, - spanID: traceKey.SpanID, + traceID: sampleKey.TraceID, + spanID: sampleKey.SpanID, }) if !ok { l := dic.LinkTable().AppendEmpty() - l.SetSpanID(pcommon.SpanID(traceKey.SpanID)) - l.SetTraceID(pcommon.TraceID(traceKey.TraceID)) + l.SetSpanID(pcommon.SpanID(sampleKey.SpanID)) + l.SetTraceID(pcommon.TraceID(sampleKey.TraceID)) } sample.SetLinkIndex(link) 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/samples/samples.go b/reporter/samples/samples.go index f38cf7f6b..cbed57ebf 100644 --- a/reporter/samples/samples.go +++ b/reporter/samples/samples.go @@ -80,4 +80,7 @@ type SampleKey struct { TID int64 CPU int64 + + SpanID libpf.APMSpanID + TraceID libpf.APMTraceID }