diff --git a/internal/pkg/instrumentation/bpf/go.opentelemetry.io/auto/sdk/probe.go b/internal/pkg/instrumentation/bpf/go.opentelemetry.io/auto/sdk/probe.go index 41cdcc985..77d9d9223 100644 --- a/internal/pkg/instrumentation/bpf/go.opentelemetry.io/auto/sdk/probe.go +++ b/internal/pkg/instrumentation/bpf/go.opentelemetry.io/auto/sdk/probe.go @@ -156,9 +156,9 @@ func (c *converter) convertEvent(e *event) []*probe.SpanEvent { TracerSchema: ss.SchemaUrl(), Kind: spanKind(span.Kind()), Attributes: attributes(span.Attributes()), + Links: c.links(span.Links()), Status: status(span.Status()), // TODO: Events. - // TODO: Links. }} } @@ -179,6 +179,35 @@ func spanKind(kind ptrace.SpanKind) trace.SpanKind { } } +func (c *converter) links(links ptrace.SpanLinkSlice) []trace.Link { + n := links.Len() + if n == 0 { + return nil + } + + out := make([]trace.Link, n) + for i := range out { + l := links.At(i) + + raw := l.TraceState().AsRaw() + ts, err := trace.ParseTraceState(raw) + if err != nil { + c.logger.Error("failed to parse link tracestate", "error", err, "tracestate", raw) + } + + out[i] = trace.Link{ + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID(l.TraceID()), + SpanID: trace.SpanID(l.SpanID()), + TraceFlags: trace.TraceFlags(l.Flags()), + TraceState: ts, + }), + Attributes: attributes(l.Attributes()), + } + } + return out +} + func attributes(m pcommon.Map) []attribute.KeyValue { out := make([]attribute.KeyValue, 0, m.Len()) m.Range(func(key string, val pcommon.Value) bool { diff --git a/internal/pkg/instrumentation/probe/event.go b/internal/pkg/instrumentation/probe/event.go index ecbd15291..f8e3b26bb 100644 --- a/internal/pkg/instrumentation/probe/event.go +++ b/internal/pkg/instrumentation/probe/event.go @@ -35,4 +35,5 @@ type SpanEvent struct { TracerName string TracerVersion string TracerSchema string + Links []trace.Link } diff --git a/internal/pkg/opentelemetry/controller.go b/internal/pkg/opentelemetry/controller.go index 183092448..2a251a932 100644 --- a/internal/pkg/opentelemetry/controller.go +++ b/internal/pkg/opentelemetry/controller.go @@ -77,7 +77,9 @@ func (c *Controller) Trace(event *probe.Event) { Start(ctx, se.SpanName, trace.WithAttributes(se.Attributes...), trace.WithSpanKind(kind), - trace.WithTimestamp(se.StartTime)) + trace.WithTimestamp(se.StartTime), + trace.WithLinks(se.Links...), + ) span.SetStatus(se.Status.Code, se.Status.Description) span.End(trace.WithTimestamp(se.EndTime)) } diff --git a/internal/test/e2e/autosdk/main.go b/internal/test/e2e/autosdk/main.go index ed9e297c0..5ab23643e 100644 --- a/internal/test/e2e/autosdk/main.go +++ b/internal/test/e2e/autosdk/main.go @@ -17,7 +17,11 @@ import ( "go.opentelemetry.io/auto/sdk" ) -const pkgName = "go.opentelemetry.io/auto/internal/test/e2e/autosdk" +const ( + pkgName = "go.opentelemetry.io/auto/internal/test/e2e/autosdk" + pkgVer = "v1.23.42" + schemaURL = "https://some_schema" +) // Y2K (January 1, 2000). var y2k = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) @@ -26,7 +30,7 @@ type app struct { tracer trace.Tracer } -func (a *app) Run(ctx context.Context, user string, admin bool) error { +func (a *app) Run(ctx context.Context, user string, admin bool, in <-chan msg) error { opts := []trace.SpanStartOption{ trace.WithAttributes( attribute.String("user", user), @@ -38,9 +42,39 @@ func (a *app) Run(ctx context.Context, user string, admin bool) error { _, span := a.tracer.Start(ctx, "Run", opts...) defer span.End(trace.WithTimestamp(y2k.Add(1 * time.Second))) + for m := range in { + span.AddLink(trace.Link{ + SpanContext: m.SpanContext, + Attributes: []attribute.KeyValue{attribute.String("data", m.Data)}, + }) + } + return errors.New("broken") } +type msg struct { + SpanContext trace.SpanContext + Data string +} + +func sig(ctx context.Context) <-chan msg { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer( + pkgName, + trace.WithInstrumentationVersion(pkgVer), + trace.WithSchemaURL(schemaURL), + ) + + ts := y2k.Add(10 * time.Microsecond) + _, span := tracer.Start(ctx, "sig", trace.WithTimestamp(ts)) + defer span.End(trace.WithTimestamp(ts.Add(100 * time.Microsecond))) + + out := make(chan msg, 1) + out <- msg{SpanContext: span.SpanContext(), Data: "Hello World"} + close(out) + + return out +} + func main() { // give time for auto-instrumentation to start up time.Sleep(5 * time.Second) @@ -48,8 +82,8 @@ func main() { provider := sdk.GetTracerProvider() tracer := provider.Tracer( pkgName, - trace.WithInstrumentationVersion("v1.23.42"), - trace.WithSchemaURL("https://some_schema"), + trace.WithInstrumentationVersion(pkgVer), + trace.WithSchemaURL(schemaURL), ) app := app{tracer: tracer} @@ -59,7 +93,7 @@ func main() { ctx, span := tracer.Start(ctx, "main", trace.WithTimestamp(y2k)) defer span.End(trace.WithTimestamp(y2k.Add(5 * time.Second))) - err := app.Run(ctx, "Alice", true) + err := app.Run(ctx, "Alice", true, sig(ctx)) if err != nil { span.SetStatus(codes.Error, "application error") span.RecordError( diff --git a/internal/test/e2e/autosdk/traces.json b/internal/test/e2e/autosdk/traces.json index ca9091bd3..60892ae00 100644 --- a/internal/test/e2e/autosdk/traces.json +++ b/internal/test/e2e/autosdk/traces.json @@ -54,6 +54,17 @@ "version": "v1.23.42" }, "spans": [ + { + "traceId": "xxxxx", + "spanId": "xxxxx", + "parentSpanId": "xxxxx", + "flags": 256, + "name": "sig", + "kind": 3, + "startTimeUnixNano": "946684800000010000", + "endTimeUnixNano": "946684800000110000", + "status": {} + } { "traceId": "xxxxx", "spanId": "xxxxx", @@ -63,6 +74,21 @@ "kind": 1, "startTimeUnixNano": "946684800500000000", "endTimeUnixNano": "946684801000000000", + "links": [ + { + "traceId": "xxxxx", + "spanId": "xxxxx", + "attributes": [ + { + "key": "data", + "value": { + "stringValue": "Hello World" + } + } + ], + "flags": 256 + } + ] "attributes": [ { "key": "user", diff --git a/internal/test/e2e/autosdk/verify.bats b/internal/test/e2e/autosdk/verify.bats index 1422fd715..9fabfab03 100644 --- a/internal/test/e2e/autosdk/verify.bats +++ b/internal/test/e2e/autosdk/verify.bats @@ -55,6 +55,31 @@ SCOPE="go.opentelemetry.io/auto/internal/test/e2e/autosdk" assert_equal "$(echo $status | jq ".message")" '"application error"' } +@test "autosdk :: sig span :: trace ID" { + trace_id=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".traceId") + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} +} + +@test "autosdk :: sig span :: span ID" { + trace_id=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".spanId") + assert_regex "$trace_id" ${MATCH_A_SPAN_ID} +} + +@test "autosdk :: sig span :: parent span ID" { + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".parentSpanId") + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} +} + +@test "autosdk :: sig span :: start time" { + timestamp=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".startTimeUnixNano") + assert_regex "$timestamp" "946684800000010000" +} + +@test "autosdk :: sig span :: end time" { + timestamp=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".endTimeUnixNano") + assert_regex "$timestamp" "946684800000110000" +} + @test "autosdk :: Run span :: trace ID" { trace_id=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"Run\")" | jq ".traceId") assert_regex "$trace_id" ${MATCH_A_TRACE_ID} @@ -94,3 +119,20 @@ SCOPE="go.opentelemetry.io/auto/internal/test/e2e/autosdk" result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"admin\").value.boolValue") assert_equal "$result" 'true' } + +@test "autosdk :: Run span :: link :: traceID" { + want=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".traceId") + got=$(span_links ${SCOPE} "Run" | jq ".traceId") + assert_equal "$got" "$want" +} + +@test "autosdk :: Run span :: link :: spanID" { + want=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"sig\")" | jq ".spanId") + got=$(span_links ${SCOPE} "Run" | jq ".spanId") + assert_equal "$got" "$want" +} + +@test "autosdk :: Run span :: link :: attributes" { + got=$(span_links ${SCOPE} "Run" | jq ".attributes[] | select(.key == \"data\").value.stringValue") + assert_equal "$got" '"Hello World"' +} diff --git a/internal/test/test_helpers/utilities.sh b/internal/test/test_helpers/utilities.sh index 062701df6..a88efdb21 100644 --- a/internal/test/test_helpers/utilities.sh +++ b/internal/test/test_helpers/utilities.sh @@ -68,6 +68,13 @@ resource_attributes_received() { spans_received | jq ".resource.attributes[]?" } +# Returns an array of all span links emitted by a given library/scope and span. +# $1 - library/scope name +# $2 - span name +span_links() { + spans_from_scope_named $1 | jq "select(.name == \"$2\").links[]" +} + # Returns an array of all spans emitted by a given library/scope # $1 - library/scope name spans_from_scope_named() { diff --git a/sdk/trace.go b/sdk/trace.go index 0ceb3c1fb..e63c55e5e 100644 --- a/sdk/trace.go +++ b/sdk/trace.go @@ -111,9 +111,8 @@ func (t tracer) traces(ctx context.Context, name string, cfg trace.SpanConfig, s start = pcommon.NewTimestampFromTime(time.Now()) } span.SetStartTimestamp(start) - + addLinks(span.Links(), cfg.Links()...) setAttributes(span.Attributes(), cfg.Attributes()) - // TODO: Add Links. return traces, span } @@ -279,7 +278,22 @@ func (s *span) AddLink(link trace.Link) { if s == nil || !s.sampled { return } - /* TODO: implement */ + + // TODO: handle link limits. + + addLinks(s.span.Links(), link) +} + +func addLinks(dest ptrace.SpanLinkSlice, links ...trace.Link) { + dest.EnsureCapacity(len(links)) + for _, link := range links { + l := dest.AppendEmpty() + l.SetTraceID(pcommon.TraceID(link.SpanContext.TraceID())) + l.SetSpanID(pcommon.SpanID(link.SpanContext.SpanID())) + l.SetFlags(uint32(link.SpanContext.TraceFlags())) + l.TraceState().FromRaw(link.SpanContext.TraceState().String()) + setAttributes(l.Attributes(), link.Attributes) + } } func (s *span) SetName(name string) { diff --git a/sdk/trace_test.go b/sdk/trace_test.go index 766a4f107..c4aacd39f 100644 --- a/sdk/trace_test.go +++ b/sdk/trace_test.go @@ -71,6 +71,41 @@ var ( SpanID: trace.SpanID{0x1}, TraceFlags: trace.FlagsSampled, }) + spanContext1 = trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x2}, + SpanID: trace.SpanID{0x2}, + TraceFlags: trace.FlagsSampled, + }) + + link0 = trace.Link{ + SpanContext: spanContext0, + Attributes: []attribute.KeyValue{ + attribute.Int("n", 0), + }, + } + link1 = trace.Link{ + SpanContext: spanContext1, + Attributes: []attribute.KeyValue{ + attribute.Int("n", 1), + }, + } + + pLink0 = func() ptrace.SpanLink { + l := ptrace.NewSpanLink() + l.SetTraceID(pcommon.TraceID(spanContext0.TraceID())) + l.SetSpanID(pcommon.SpanID(spanContext0.SpanID())) + l.SetFlags(uint32(spanContext0.TraceFlags())) + l.Attributes().PutInt("n", 0) + return l + }() + pLink1 = func() ptrace.SpanLink { + l := ptrace.NewSpanLink() + l.SetTraceID(pcommon.TraceID(spanContext1.TraceID())) + l.SetSpanID(pcommon.SpanID(spanContext1.SpanID())) + l.SetFlags(uint32(spanContext1.TraceFlags())) + l.Attributes().PutInt("n", 1) + return l + }() ) func TestSpanCreation(t *testing.T) { @@ -207,6 +242,19 @@ func TestSpanCreation(t *testing.T) { assert.Equal(t, pAttrs, s.span.Attributes()) }, }, + { + TestName: "WithLinks", + Options: []trace.SpanStartOption{ + trace.WithLinks(link0, link1), + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assertTracer(s.traces) + want := ptrace.NewSpanLinkSlice() + pLink0.CopyTo(want.AppendEmpty()) + pLink1.CopyTo(want.AppendEmpty()) + assert.Equal(t, want, s.span.Links()) + }, + }, } ctx := context.Background() @@ -321,6 +369,18 @@ func TestSpanNilUnsampledGuards(t *testing.T) { t.Run("TracerProvider", run(func(s *span) { _ = s.TracerProvider() })) } +func TestSpanAddLink(t *testing.T) { + s := spanBuilder{ + Options: []trace.SpanStartOption{trace.WithLinks(link0)}, + }.Build() + s.AddLink(link1) + + want := ptrace.NewSpanLinkSlice() + pLink0.CopyTo(want.AppendEmpty()) + pLink1.CopyTo(want.AppendEmpty()) + assert.Equal(t, want, s.span.Links()) +} + func TestSpanIsRecording(t *testing.T) { builder := spanBuilder{} s := builder.Build()