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 100cd2aed..229a82630 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 @@ -6,6 +6,7 @@ package sdk import ( "bytes" "encoding/binary" + "fmt" "log/slog" "github.com/cilium/ebpf/perf" @@ -15,6 +16,7 @@ import ( "go.opentelemetry.io/auto/internal/pkg/instrumentation/probe" "go.opentelemetry.io/auto/internal/pkg/structfield" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -151,10 +153,84 @@ func (c *converter) convertEvent(e *event) []*probe.SpanEvent { TracerName: ss.Scope().Name(), TracerVersion: ss.Scope().Version(), TracerSchema: ss.SchemaUrl(), + Attributes: attributes(span.Attributes()), // TODO: Status. - // TODO: Attributes. // TODO: Events. // TODO: Links. // TODO: Span Kind. }} } + +func attributes(m pcommon.Map) []attribute.KeyValue { + out := make([]attribute.KeyValue, 0, m.Len()) + m.Range(func(key string, val pcommon.Value) bool { + out = append(out, attribute.KeyValue{ + Key: attribute.Key(key), + Value: attributeValue(val), + }) + return true + }) + return out +} + +func attributeValue(val pcommon.Value) (out attribute.Value) { + switch val.Type() { + case pcommon.ValueTypeEmpty: + case pcommon.ValueTypeStr: + out = attribute.StringValue(val.AsString()) + case pcommon.ValueTypeInt: + out = attribute.Int64Value(val.Int()) + case pcommon.ValueTypeDouble: + out = attribute.Float64Value(val.Double()) + case pcommon.ValueTypeBool: + out = attribute.BoolValue(val.Bool()) + case pcommon.ValueTypeSlice: + s := val.Slice() + if s.Len() == 0 { + // Undetectable slice type. + out = attribute.StringValue("") + return out + } + + // Validate homogeneity before allocating. + t := s.At(0).Type() + for i := 1; i < s.Len(); i++ { + if s.At(i).Type() != t { + out = attribute.StringValue("") + return out + } + } + + switch t { + case pcommon.ValueTypeBool: + v := make([]bool, s.Len()) + for i := 0; i < s.Len(); i++ { + v[i] = s.At(i).Bool() + } + out = attribute.BoolSliceValue(v) + case pcommon.ValueTypeStr: + v := make([]string, s.Len()) + for i := 0; i < s.Len(); i++ { + v[i] = s.At(i).Str() + } + out = attribute.StringSliceValue(v) + case pcommon.ValueTypeInt: + v := make([]int64, s.Len()) + for i := 0; i < s.Len(); i++ { + v[i] = s.At(i).Int() + } + out = attribute.Int64SliceValue(v) + case pcommon.ValueTypeDouble: + v := make([]float64, s.Len()) + for i := 0; i < s.Len(); i++ { + v[i] = s.At(i).Double() + } + out = attribute.Float64SliceValue(v) + default: + out = attribute.StringValue(fmt.Sprintf("", t.String())) + } + default: + out = attribute.StringValue(fmt.Sprintf("", val.AsRaw())) + } + return out +} diff --git a/internal/test/e2e/autosdk/build.sh b/internal/test/e2e/autosdk/build.sh index d4cfa55b4..c9cf03243 100755 --- a/internal/test/e2e/autosdk/build.sh +++ b/internal/test/e2e/autosdk/build.sh @@ -15,11 +15,11 @@ usage() { OPTIONS: -t --tag Docker tag to use ["sample-app"] -h --help Show this help message - EOF +EOF } parse_opts() { - # Make sure getopts starts at the begining + # Make sure getopts starts at the beginning OPTIND=1 local deliminator option @@ -100,9 +100,9 @@ main() { # Check dependencies hash git 2>/dev/null\ - || { echo >&2 "Requrired git client not found"; exit 1; } + || { echo >&2 "Required git client not found"; exit 1; } hash docker 2>/dev/null\ - || { echo >&2 "Requrired docker client not found"; exit 1; } + || { echo >&2 "Required docker client not found"; exit 1; } parse_opts "$@" diff --git a/internal/test/e2e/autosdk/traces.json b/internal/test/e2e/autosdk/traces.json index 53b97d71f..7823c0380 100644 --- a/internal/test/e2e/autosdk/traces.json +++ b/internal/test/e2e/autosdk/traces.json @@ -63,7 +63,21 @@ "kind": 3, "startTimeUnixNano": "946684800500000000", "endTimeUnixNano": "946684801000000000", - "status": {} + "attributes": [ + { + "key": "user", + "value": { + "stringValue": "Alice" + } + }, + { + "key": "admin", + "value": { + "boolValue": true + } + } + ], + "status": {}, }, { "traceId": "xxxxx", diff --git a/internal/test/e2e/autosdk/verify.bats b/internal/test/e2e/autosdk/verify.bats index 855c0906c..b316f65e9 100644 --- a/internal/test/e2e/autosdk/verify.bats +++ b/internal/test/e2e/autosdk/verify.bats @@ -68,3 +68,13 @@ SCOPE="go.opentelemetry.io/auto/internal/test/e2e/autosdk" timestamp=$(spans_from_scope_named ${SCOPE} | jq "select(.name == \"Run\")" | jq ".endTimeUnixNano") assert_regex "$timestamp" "946684801000000000" } + +@test "autosdk :: Run span :: attribute :: user" { + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"user\").value.stringValue") + assert_equal "$result" '"Alice"' +} + +@test "autosdk :: Run span :: attribute :: admin" { + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"admin\").value.boolValue") + assert_equal "$result" 'true' +} diff --git a/sdk/trace.go b/sdk/trace.go index 23a68b75e..601a40e23 100644 --- a/sdk/trace.go +++ b/sdk/trace.go @@ -112,7 +112,7 @@ func (t tracer) traces(ctx context.Context, name string, cfg trace.SpanConfig, s } span.SetStartTimestamp(start) - // TODO: Set Attributes. + setAttributes(span.Attributes(), cfg.Attributes()) // TODO: Add Links. return traces, span diff --git a/sdk/trace_test.go b/sdk/trace_test.go index cf2083765..88d554bd7 100644 --- a/sdk/trace_test.go +++ b/sdk/trace_test.go @@ -186,6 +186,16 @@ func TestSpanCreation(t *testing.T) { assert.Equal(t, pcommon.NewTimestampFromTime(ts), s.span.StartTimestamp()) }, }, + { + TestName: "WithAttributes", + Options: []trace.SpanStartOption{ + trace.WithAttributes(attrs...), + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assertTracer(s.traces) + assert.Equal(t, pAttrs, s.span.Attributes()) + }, + }, } ctx := context.Background()