diff --git a/sdk/doc.go b/sdk/doc.go new file mode 100644 index 000000000..ad73d8cb9 --- /dev/null +++ b/sdk/doc.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +/* +Package sdk provides an auto-instrumentable OpenTelemetry SDK. + +An [go.opentelemetry.io/auto.Instrumentation] can be configured to target the +process running this SDK. In that case, all telemetry the SDK produces will be +processed and handled by that [go.opentelemetry.io/auto.Instrumentation]. + +By default, if there is no [go.opentelemetry.io/auto.Instrumentation] set to +auto-instrument the SDK, the SDK will not generate any telemetry. +*/ +package sdk diff --git a/sdk/go.mod b/sdk/go.mod new file mode 100644 index 000000000..9e9d86284 --- /dev/null +++ b/sdk/go.mod @@ -0,0 +1,28 @@ +module go.opentelemetry.io/auto/sdk + +go 1.23.0 + +require ( + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/pdata v1.14.1 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sdk/go.sum b/sdk/go.sum new file mode 100644 index 000000000..39c259dac --- /dev/null +++ b/sdk/go.sum @@ -0,0 +1,84 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector/pdata v1.14.1 h1:wXZjtQA7Vy5HFqco+yA95ENyMQU5heBB1IxMHQf6mUk= +go.opentelemetry.io/collector/pdata v1.14.1/go.mod h1:z1dTjwwtcoXxZx2/nkHysjxMeaxe9pEmYTEr4SMNIx8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/trace.go b/sdk/trace.go new file mode 100644 index 000000000..843f969c6 --- /dev/null +++ b/sdk/trace.go @@ -0,0 +1,350 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package sdk + +import ( + "context" + "fmt" + "reflect" + "runtime" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" +) + +// GetTracerProvider returns an auto-instrumentable [trace.TracerProvider]. +// +// If an [go.opentelemetry.io/auto.Instrumentation] is configured to instrument +// the process using the returned TracerProvider, all of the telemetry it +// produces will be processed and handled by that Instrumentation. By default, +// if no Instrumentation instruments the TracerProvider it will not generate +// any trace telemetry. +func GetTracerProvider() trace.TracerProvider { return tracerProviderInstance } + +var tracerProviderInstance = new(tracerProvider) + +type tracerProvider struct { + embedded.TracerProvider +} + +var _ trace.TracerProvider = (*tracerProvider)(nil) + +func (p *tracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer { + cfg := trace.NewTracerConfig(opts...) + return tracer{ + name: name, + version: cfg.InstrumentationVersion(), + schemaURL: cfg.SchemaURL(), + } +} + +type tracer struct { + embedded.Tracer + + name, schemaURL, version string +} + +var _ trace.Tracer = tracer{} + +func (t tracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + var psc trace.SpanContext + span := &span{sampled: true} + + // Ask eBPF for sampling decision and span context info. + t.start(ctx, span, &psc, &span.sampled, &span.spanContext) + + ctx = trace.ContextWithSpan(ctx, span) + + if span.sampled { + // Only build traces if sampled. + cfg := trace.NewSpanStartConfig(opts...) + span.traces, span.span = t.traces(ctx, name, cfg, span.spanContext, psc) + } + + return ctx, span +} + +// Expected to be implemented in eBPF. +// +//go:noinline +func (t *tracer) start( + ctx context.Context, + spanPtr *span, + psc *trace.SpanContext, + sampled *bool, + sc *trace.SpanContext, +) { + start(ctx, spanPtr, psc, sampled, sc) +} + +// start is used for testing. +var start = func(context.Context, *span, *trace.SpanContext, *bool, *trace.SpanContext) { +} + +func (t tracer) traces(ctx context.Context, name string, cfg trace.SpanConfig, sc, psc trace.SpanContext) (ptrace.Traces, ptrace.Span) { + // TODO: pool this. It can be returned on end. + traces := ptrace.NewTraces() + traces.ResourceSpans().EnsureCapacity(1) + + rs := traces.ResourceSpans().AppendEmpty() + rs.ScopeSpans().EnsureCapacity(1) + + ss := rs.ScopeSpans().AppendEmpty() + ss.Scope().SetName(t.name) + ss.Scope().SetVersion(t.version) + ss.SetSchemaUrl(t.schemaURL) + ss.Spans().EnsureCapacity(1) + + span := ss.Spans().AppendEmpty() + span.SetTraceID(pcommon.TraceID(sc.TraceID())) + span.SetSpanID(pcommon.SpanID(sc.SpanID())) + span.SetFlags(uint32(sc.TraceFlags())) + span.TraceState().FromRaw(sc.TraceState().String()) + span.SetParentSpanID(pcommon.SpanID(psc.SpanID())) + span.SetName(name) + span.SetKind(spanKind(cfg.SpanKind())) + + var start pcommon.Timestamp + if t := cfg.Timestamp(); !t.IsZero() { + start = pcommon.NewTimestampFromTime(cfg.Timestamp()) + } else { + start = pcommon.NewTimestampFromTime(time.Now()) + } + span.SetStartTimestamp(start) + + setAttributes(span.Attributes(), cfg.Attributes()) + addLinks(span.Links(), cfg.Links()...) + + return traces, span +} + +func spanKind(kind trace.SpanKind) ptrace.SpanKind { + switch kind { + case trace.SpanKindInternal: + return ptrace.SpanKindInternal + case trace.SpanKindServer: + return ptrace.SpanKindServer + case trace.SpanKindClient: + return ptrace.SpanKindClient + case trace.SpanKindProducer: + return ptrace.SpanKindProducer + case trace.SpanKindConsumer: + return ptrace.SpanKindConsumer + } + return ptrace.SpanKindUnspecified +} + +type span struct { + embedded.Span + + sampled bool + spanContext trace.SpanContext + + traces ptrace.Traces + span ptrace.Span +} + +func (s *span) SpanContext() trace.SpanContext { + if s == nil { + return trace.SpanContext{} + } + + return s.spanContext +} + +func (s *span) IsRecording() bool { + if s == nil { + return false + } + return s.sampled +} + +func (s *span) SetStatus(c codes.Code, msg string) { + if s == nil || !s.sampled { + return + } + + stat := s.span.Status() + stat.SetMessage(msg) + + switch c { + case codes.Unset: + stat.SetCode(ptrace.StatusCodeUnset) + case codes.Error: + stat.SetCode(ptrace.StatusCodeError) + case codes.Ok: + stat.SetCode(ptrace.StatusCodeOk) + } +} + +func (s *span) SetAttributes(attrs ...attribute.KeyValue) { + if s == nil || !s.sampled { + return + } + + // TODO: handle attribute limits. + + setAttributes(s.span.Attributes(), attrs) +} + +func setAttributes(dest pcommon.Map, attrs []attribute.KeyValue) { + dest.EnsureCapacity(len(attrs)) + for _, attr := range attrs { + key := string(attr.Key) + switch attr.Value.Type() { + case attribute.BOOL: + dest.PutBool(key, attr.Value.AsBool()) + case attribute.INT64: + dest.PutInt(key, attr.Value.AsInt64()) + case attribute.FLOAT64: + dest.PutDouble(key, attr.Value.AsFloat64()) + case attribute.STRING: + dest.PutStr(key, attr.Value.AsString()) + case attribute.BOOLSLICE: + val := attr.Value.AsBoolSlice() + s := dest.PutEmptySlice(key) + s.EnsureCapacity(len(val)) + for _, v := range val { + s.AppendEmpty().SetBool(v) + } + case attribute.INT64SLICE: + val := attr.Value.AsInt64Slice() + s := dest.PutEmptySlice(key) + s.EnsureCapacity(len(val)) + for _, v := range val { + s.AppendEmpty().SetInt(v) + } + case attribute.FLOAT64SLICE: + val := attr.Value.AsFloat64Slice() + s := dest.PutEmptySlice(key) + s.EnsureCapacity(len(val)) + for _, v := range val { + s.AppendEmpty().SetDouble(v) + } + case attribute.STRINGSLICE: + val := attr.Value.AsStringSlice() + s := dest.PutEmptySlice(key) + s.EnsureCapacity(len(val)) + for _, v := range val { + s.AppendEmpty().SetStr(v) + } + } + } +} + +func (s *span) End(opts ...trace.SpanEndOption) { + if s == nil || !s.sampled { + return + } + + cfg := trace.NewSpanEndConfig(opts...) + var end time.Time + if t := cfg.Timestamp(); !t.IsZero() { + end = t + } else { + end = time.Now() + } + s.span.SetEndTimestamp(pcommon.NewTimestampFromTime(end)) + + var m ptrace.ProtoMarshaler + b, _ := m.MarshalTraces(s.traces) // TODO: do not ignore this error. + + s.sampled = false + + s.ended(b) +} + +// Expected to be implemented in eBPF. +// +//go:noinline +func (*span) ended(buf []byte) { ended(buf) } + +// ended is used for testing. +var ended = func([]byte) {} + +func (s *span) RecordError(err error, opts ...trace.EventOption) { + if s == nil || err == nil || !s.sampled { + return + } + + cfg := trace.NewEventConfig(opts...) + + attrs := cfg.Attributes() + attrs = append(attrs, + semconv.ExceptionType(typeStr(err)), + semconv.ExceptionMessage(err.Error()), + ) + if cfg.StackTrace() { + buf := make([]byte, 2048) + n := runtime.Stack(buf, false) + attrs = append(attrs, semconv.ExceptionStacktrace(string(buf[0:n]))) + } + + s.addEvent(semconv.ExceptionEventName, cfg.Timestamp(), attrs) +} + +func typeStr(i any) string { + t := reflect.TypeOf(i) + if t.PkgPath() == "" && t.Name() == "" { + // Likely a builtin type. + return t.String() + } + return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) +} + +func (s *span) AddEvent(name string, opts ...trace.EventOption) { + if s == nil || !s.sampled { + return + } + + cfg := trace.NewEventConfig(opts...) + s.addEvent(name, cfg.Timestamp(), cfg.Attributes()) +} + +func (s *span) addEvent(name string, tStamp time.Time, attrs []attribute.KeyValue) { + // TODO: handle link limits. + + event := s.span.Events().AppendEmpty() + event.SetName(name) + ts := pcommon.NewTimestampFromTime(tStamp) + event.SetTimestamp(ts) + setAttributes(event.Attributes(), attrs) +} + +func (s *span) AddLink(link trace.Link) { + if s == nil || !s.sampled { + return + } + + // 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) { + if s == nil || !s.sampled { + return + } + s.span.SetName(name) +} + +func (*span) TracerProvider() trace.TracerProvider { return GetTracerProvider() } diff --git a/sdk/trace_test.go b/sdk/trace_test.go new file mode 100644 index 000000000..957a82b42 --- /dev/null +++ b/sdk/trace_test.go @@ -0,0 +1,556 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package sdk + +import ( + "context" + "errors" + "math" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" +) + +var ( + attrs = []attribute.KeyValue{ + attribute.Bool("bool", true), + attribute.Int("int", -1), + attribute.Int64("int64", 43), + attribute.Float64("float64", 0.3), + attribute.String("string", "value"), + attribute.BoolSlice("bool slice", []bool{true, false, true}), + attribute.IntSlice("int slice", []int{-1, -30, 328}), + attribute.Int64Slice("int64 slice", []int64{1030, 0, 0}), + attribute.Float64Slice("float64 slice", []float64{1e9}), + attribute.StringSlice("string slice", []string{"one", "two"}), + } + + pAttrs = func() pcommon.Map { + m := pcommon.NewMap() + m.PutBool("bool", true) + m.PutInt("int", -1) + m.PutInt("int64", 43) + m.PutDouble("float64", 0.3) + m.PutStr("string", "value") + + s := m.PutEmptySlice("bool slice") + s.AppendEmpty().SetBool(true) + s.AppendEmpty().SetBool(false) + s.AppendEmpty().SetBool(true) + + s = m.PutEmptySlice("int slice") + s.AppendEmpty().SetInt(-1) + s.AppendEmpty().SetInt(-30) + s.AppendEmpty().SetInt(328) + + s = m.PutEmptySlice("int64 slice") + s.AppendEmpty().SetInt(1030) + s.AppendEmpty().SetInt(0) + s.AppendEmpty().SetInt(0) + + s = m.PutEmptySlice("float64 slice") + s.AppendEmpty().SetDouble(1e9) + + s = m.PutEmptySlice("string slice") + s.AppendEmpty().SetStr("one") + s.AppendEmpty().SetStr("two") + + return m + }() + + spanContext0 = trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x1}, + 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) { + const ( + spanName = "span name" + tracerName = "go.opentelemetry.io/otel/sdk/test" + tracerVer = "v0.1.0" + ) + + ts := time.Now() + + tracer := GetTracerProvider().Tracer( + tracerName, + trace.WithInstrumentationVersion(tracerVer), + trace.WithSchemaURL(semconv.SchemaURL), + ) + + assertTracer := func(traces ptrace.Traces) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + rs := traces.ResourceSpans() + require.Equal(t, 1, rs.Len()) + sss := rs.At(0).ScopeSpans() + require.Equal(t, 1, sss.Len()) + ss := sss.At(0) + assert.Equal(t, tracerName, ss.Scope().Name(), "tracer name") + assert.Equal(t, tracerVer, ss.Scope().Version(), "tracer version") + assert.Equal(t, semconv.SchemaURL, ss.SchemaUrl(), "tracer schema URL") + } + } + + testcases := []struct { + TestName string + SpanName string + Options []trace.SpanStartOption + Setup func(*testing.T) + Eval func(*testing.T, context.Context, *span) + }{ + { + TestName: "SampledByDefault", + Eval: func(t *testing.T, _ context.Context, s *span) { + assertTracer(s.traces) + + assert.True(t, s.sampled, "not sampled by default.") + }, + }, + { + TestName: "ParentSpanContext", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *span, psc *trace.SpanContext, _ *bool, _ *trace.SpanContext) { + *psc = spanContext0 + } + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assertTracer(s.traces) + + want := spanContext0.SpanID().String() + got := s.span.ParentSpanID().String() + assert.Equal(t, want, got) + }, + }, + { + TestName: "SpanContext", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *span, _ *trace.SpanContext, _ *bool, sc *trace.SpanContext) { + *sc = spanContext0 + } + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assertTracer(s.traces) + + str := func(i interface{ String() string }) string { + return i.String() + } + assert.Equal(t, str(spanContext0.TraceID()), str(s.span.TraceID()), "trace ID") + assert.Equal(t, str(spanContext0.SpanID()), str(s.span.SpanID()), "span ID") + assert.Equal(t, uint32(spanContext0.TraceFlags()), s.span.Flags(), "flags") + assert.Equal(t, str(spanContext0.TraceState()), s.span.TraceState().AsRaw(), "tracestate") + }, + }, + { + TestName: "NotSampled", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *span, _ *trace.SpanContext, s *bool, _ *trace.SpanContext) { + *s = false + } + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assert.False(t, s.sampled, "sampled") + }, + }, + { + TestName: "WithName", + SpanName: spanName, + Eval: func(t *testing.T, _ context.Context, s *span) { + assert.Equal(t, spanName, s.span.Name()) + }, + }, + { + TestName: "WithSpanKind", + Options: []trace.SpanStartOption{ + trace.WithSpanKind(trace.SpanKindClient), + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + assert.Equal(t, ptrace.SpanKindClient, s.span.Kind()) + }, + }, + { + TestName: "WithTimestamp", + Options: []trace.SpanStartOption{ + trace.WithTimestamp(ts), + }, + Eval: func(t *testing.T, _ context.Context, s *span) { + 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) { + 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) { + want := ptrace.NewSpanLinkSlice() + pLink0.CopyTo(want.AppendEmpty()) + pLink1.CopyTo(want.AppendEmpty()) + assert.Equal(t, want, s.span.Links()) + }, + }, + } + + ctx := context.Background() + for _, tc := range testcases { + t.Run(tc.TestName, func(t *testing.T) { + if tc.Setup != nil { + tc.Setup(t) + } + + c, sIface := tracer.Start(ctx, tc.SpanName, tc.Options...) + require.IsType(t, &span{}, sIface) + s := sIface.(*span) + + tc.Eval(t, c, s) + }) + } +} + +func TestSpanKindTransform(t *testing.T) { + tests := map[trace.SpanKind]ptrace.SpanKind{ + trace.SpanKind(-1): ptrace.SpanKindUnspecified, + trace.SpanKindUnspecified: ptrace.SpanKindUnspecified, + trace.SpanKind(math.MaxInt): ptrace.SpanKindUnspecified, + + trace.SpanKindInternal: ptrace.SpanKindInternal, + trace.SpanKindServer: ptrace.SpanKindServer, + trace.SpanKindClient: ptrace.SpanKindClient, + trace.SpanKindProducer: ptrace.SpanKindProducer, + trace.SpanKindConsumer: ptrace.SpanKindConsumer, + } + + for in, want := range tests { + assert.Equal(t, want, spanKind(in), in.String()) + } +} + +func TestSpanNilUnsampledGuards(t *testing.T) { + run := func(f func(s *span) func()) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + var s *span + assert.NotPanics(t, f(s), "nil span") + + s = new(span) + assert.NotPanics(t, f(s), "unsampled span") + } + } + + t.Run("End", run(func(s *span) func() { + return func() { s.End() } + })) + + t.Run("AddEvent", run(func(s *span) func() { + return func() { s.AddEvent("event name") } + })) + + t.Run("AddLink", run(func(s *span) func() { + return func() { s.AddLink(trace.Link{}) } + })) + + t.Run("IsRecording", run(func(s *span) func() { + return func() { _ = s.IsRecording() } + })) + + t.Run("RecordError", run(func(s *span) func() { + return func() { s.RecordError(nil) } + })) + + t.Run("SpanContext", run(func(s *span) func() { + return func() { _ = s.SpanContext() } + })) + + t.Run("SetStatus", run(func(s *span) func() { + return func() { s.SetStatus(codes.Error, "test") } + })) + + t.Run("SetName", run(func(s *span) func() { + return func() { s.SetName("span name") } + })) + + t.Run("SetAttributes", run(func(s *span) func() { + return func() { s.SetAttributes(attribute.Bool("key", true)) } + })) + + t.Run("TracerProvider", run(func(s *span) func() { + return func() { _ = s.TracerProvider() } + })) +} + +func TestSpanEnd(t *testing.T) { + orig := ended + t.Cleanup(func() { ended = orig }) + + var buf []byte + ended = func(b []byte) { buf = b } + + timeNow := time.Now() + + tests := []struct { + Name string + Options []trace.SpanEndOption + Eval func(*testing.T, pcommon.Timestamp) + }{ + { + Name: "Now", + Eval: func(t *testing.T, ts pcommon.Timestamp) { + assert.False(t, ts.AsTime().IsZero(), "zero end time") + }, + }, + { + Name: "WithTimestamp", + Options: []trace.SpanEndOption{ + trace.WithTimestamp(timeNow), + }, + Eval: func(t *testing.T, ts pcommon.Timestamp) { + assert.True(t, ts.AsTime().Equal(timeNow), "end time not set") + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + _, s := spanBuilder{}.Build(context.Background()) + s.End(test.Options...) + + assert.False(t, s.sampled, "ended span should not be sampled") + require.NotNil(t, buf, "no span data emitted") + + var m ptrace.ProtoUnmarshaler + traces, err := m.UnmarshalTraces(buf) + require.NoError(t, err) + + rs := traces.ResourceSpans() + require.Equal(t, 1, rs.Len()) + ss := rs.At(0).ScopeSpans() + require.Equal(t, 1, ss.Len()) + spans := ss.At(0).Spans() + require.Equal(t, 1, spans.Len()) + + test.Eval(t, spans.At(0).EndTimestamp()) + }) + } +} + +func TestSpanAddEvent(t *testing.T) { + _, s := spanBuilder{}.Build(context.Background()) + + const name = "event name" + ts := time.Now() + s.AddEvent(name, trace.WithTimestamp(ts)) + + want := ptrace.NewSpanEventSlice() + e := want.AppendEmpty() + e.SetName(name) + e.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + require.Equal(t, want, s.span.Events()) +} + +func TestSpanAddLink(t *testing.T) { + _, s := spanBuilder{ + Options: []trace.SpanStartOption{trace.WithLinks(link0)}, + }.Build(context.Background()) + 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(context.Background()) + assert.True(t, s.IsRecording(), "sampled span should be recorded") + + builder.NotSampled = true + _, s = builder.Build(context.Background()) + assert.False(t, s.IsRecording(), "unsampled span should not be recorded") +} + +func TestSpanRecordError(t *testing.T) { + _, s := spanBuilder{}.Build(context.Background()) + + want := ptrace.NewSpanEventSlice() + s.RecordError(nil) + require.Equal(t, want, s.span.Events(), "nil error recorded") + + ts := time.Now() + err := errors.New("test") + s.RecordError( + err, + trace.WithTimestamp(ts), + trace.WithAttributes(attribute.Bool("testing", true)), + ) + e := want.AppendEmpty() + e.SetName(semconv.ExceptionEventName) + e.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + e.Attributes().PutBool("testing", true) + e.Attributes().PutStr(string(semconv.ExceptionTypeKey), "*errors.errorString") + e.Attributes().PutStr(string(semconv.ExceptionMessageKey), err.Error()) + assert.Equal(t, want, s.span.Events(), "nil error recorded") + + s.RecordError(err, trace.WithStackTrace(true)) + require.Equal(t, 2, s.span.Events().Len(), "missing event") + e = s.span.Events().At(1) + _, ok := e.Attributes().Get(string(semconv.ExceptionStacktraceKey)) + assert.True(t, ok, "missing stacktrace attribute") +} + +func TestSpanSpanContext(t *testing.T) { + _, s := spanBuilder{SpanContext: spanContext0}.Build(context.Background()) + assert.Equal(t, spanContext0, s.SpanContext()) +} + +func TestSpanSetStatus(t *testing.T) { + _, s := spanBuilder{}.Build(context.Background()) + + want := ptrace.NewStatus() + assert.Equal(t, want, s.span.Status()) + + c, msg := codes.Error, "test" + want.SetMessage(msg) + want.SetCode(ptrace.StatusCodeError) + s.SetStatus(c, msg) + assert.Equalf(t, want, s.span.Status(), "code: %s, msg: %s", c, msg) + + c = codes.Ok + want.SetCode(ptrace.StatusCodeOk) + s.SetStatus(c, msg) + assert.Equalf(t, want, s.span.Status(), "code: %s, msg: %s", c, msg) + + c = codes.Unset + want.SetCode(ptrace.StatusCodeUnset) + s.SetStatus(c, msg) + assert.Equalf(t, want, s.span.Status(), "code: %s, msg: %s", c, msg) +} + +func TestSpanSetName(t *testing.T) { + const name = "span name" + builder := spanBuilder{} + + _, s := builder.Build(context.Background()) + s.SetName(name) + assert.Equal(t, name, s.span.Name(), "span name not set") + + builder.Name = "alt" + _, s = builder.Build(context.Background()) + s.SetName(name) + assert.Equal(t, name, s.span.Name(), "SetName overrides default") +} + +func TestSpanSetAttributes(t *testing.T) { + builder := spanBuilder{} + + _, s := builder.Build(context.Background()) + s.SetAttributes(attrs...) + assert.Equal(t, pAttrs, s.span.Attributes(), "span attributes not set") + + builder.Options = []trace.SpanStartOption{ + trace.WithAttributes(attrs[0].Key.Bool(!attrs[0].Value.AsBool())), + } + + _, s = builder.Build(context.Background()) + s.SetAttributes(attrs...) + assert.Equal(t, pAttrs, s.span.Attributes(), "SpanAttributes did not override") +} + +func TestSpanTracerProvider(t *testing.T) { + var s span + + got := s.TracerProvider() + require.IsType(t, &tracerProvider{}, got) + assert.Same(t, got.(*tracerProvider), tracerProviderInstance) +} + +type spanBuilder struct { + Name string + NotSampled bool + SpanContext trace.SpanContext + ParentSpanContext trace.SpanContext + Options []trace.SpanStartOption + + Tracer *tracer +} + +func (b spanBuilder) Build(ctx context.Context) (context.Context, *span) { + if b.Tracer == nil { + b.Tracer = new(tracer) + } + + s := &span{sampled: !b.NotSampled, spanContext: b.SpanContext} + s.traces, s.span = b.Tracer.traces( + ctx, + b.Name, + trace.NewSpanStartConfig(b.Options...), + s.spanContext, + b.ParentSpanContext, + ) + + ctx = trace.ContextWithSpan(ctx, s) + return ctx, s +}