blob: 815d271ffb2694d112ea970eda937600e6ea618e [file] [log] [blame]
Abhay Kumar40252eb2025-10-13 13:25:53 +00001// Copyright The OpenTelemetry Authors
2// SPDX-License-Identifier: Apache-2.0
3
4package sdk
5
6import (
7 "encoding/json"
8 "fmt"
Abhay Kumar062cda52025-12-23 06:49:37 +00009 "math"
Abhay Kumar40252eb2025-10-13 13:25:53 +000010 "reflect"
11 "runtime"
12 "strings"
13 "sync"
14 "sync/atomic"
15 "time"
16 "unicode/utf8"
17
18 "go.opentelemetry.io/otel/attribute"
19 "go.opentelemetry.io/otel/codes"
Abhay Kumar062cda52025-12-23 06:49:37 +000020 semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
Abhay Kumar40252eb2025-10-13 13:25:53 +000021 "go.opentelemetry.io/otel/trace"
22 "go.opentelemetry.io/otel/trace/noop"
23
24 "go.opentelemetry.io/auto/sdk/internal/telemetry"
25)
26
27type span struct {
28 noop.Span
29
30 spanContext trace.SpanContext
31 sampled atomic.Bool
32
33 mu sync.Mutex
34 traces *telemetry.Traces
35 span *telemetry.Span
36}
37
38func (s *span) SpanContext() trace.SpanContext {
39 if s == nil {
40 return trace.SpanContext{}
41 }
42 // s.spanContext is immutable, do not acquire lock s.mu.
43 return s.spanContext
44}
45
46func (s *span) IsRecording() bool {
47 if s == nil {
48 return false
49 }
50
51 return s.sampled.Load()
52}
53
54func (s *span) SetStatus(c codes.Code, msg string) {
55 if s == nil || !s.sampled.Load() {
56 return
57 }
58
59 s.mu.Lock()
60 defer s.mu.Unlock()
61
62 if s.span.Status == nil {
63 s.span.Status = new(telemetry.Status)
64 }
65
66 s.span.Status.Message = msg
67
68 switch c {
69 case codes.Unset:
70 s.span.Status.Code = telemetry.StatusCodeUnset
71 case codes.Error:
72 s.span.Status.Code = telemetry.StatusCodeError
73 case codes.Ok:
74 s.span.Status.Code = telemetry.StatusCodeOK
75 }
76}
77
78func (s *span) SetAttributes(attrs ...attribute.KeyValue) {
79 if s == nil || !s.sampled.Load() {
80 return
81 }
82
83 s.mu.Lock()
84 defer s.mu.Unlock()
85
86 limit := maxSpan.Attrs
87 if limit == 0 {
88 // No attributes allowed.
Abhay Kumar062cda52025-12-23 06:49:37 +000089 n := int64(len(attrs))
90 if n > 0 {
91 s.span.DroppedAttrs += uint32( //nolint:gosec // Bounds checked.
92 min(n, math.MaxUint32),
93 )
94 }
Abhay Kumar40252eb2025-10-13 13:25:53 +000095 return
96 }
97
98 m := make(map[string]int)
99 for i, a := range s.span.Attrs {
100 m[a.Key] = i
101 }
102
103 for _, a := range attrs {
104 val := convAttrValue(a.Value)
105 if val.Empty() {
106 s.span.DroppedAttrs++
107 continue
108 }
109
110 if idx, ok := m[string(a.Key)]; ok {
111 s.span.Attrs[idx] = telemetry.Attr{
112 Key: string(a.Key),
113 Value: val,
114 }
115 } else if limit < 0 || len(s.span.Attrs) < limit {
116 s.span.Attrs = append(s.span.Attrs, telemetry.Attr{
117 Key: string(a.Key),
118 Value: val,
119 })
120 m[string(a.Key)] = len(s.span.Attrs) - 1
121 } else {
122 s.span.DroppedAttrs++
123 }
124 }
125}
126
127// convCappedAttrs converts up to limit attrs into a []telemetry.Attr. The
128// number of dropped attributes is also returned.
129func convCappedAttrs(limit int, attrs []attribute.KeyValue) ([]telemetry.Attr, uint32) {
Abhay Kumar062cda52025-12-23 06:49:37 +0000130 n := len(attrs)
Abhay Kumar40252eb2025-10-13 13:25:53 +0000131 if limit == 0 {
Abhay Kumar062cda52025-12-23 06:49:37 +0000132 var out uint32
133 if n > 0 {
134 out = uint32(min(int64(n), math.MaxUint32)) //nolint:gosec // Bounds checked.
135 }
136 return nil, out
Abhay Kumar40252eb2025-10-13 13:25:53 +0000137 }
138
139 if limit < 0 {
140 // Unlimited.
141 return convAttrs(attrs), 0
142 }
143
Abhay Kumar062cda52025-12-23 06:49:37 +0000144 if n < 0 {
145 n = 0
146 }
147
148 limit = min(n, limit)
149 return convAttrs(attrs[:limit]), uint32(n - limit) //nolint:gosec // Bounds checked.
Abhay Kumar40252eb2025-10-13 13:25:53 +0000150}
151
152func convAttrs(attrs []attribute.KeyValue) []telemetry.Attr {
153 if len(attrs) == 0 {
154 // Avoid allocations if not necessary.
155 return nil
156 }
157
158 out := make([]telemetry.Attr, 0, len(attrs))
159 for _, attr := range attrs {
160 key := string(attr.Key)
161 val := convAttrValue(attr.Value)
162 if val.Empty() {
163 continue
164 }
165 out = append(out, telemetry.Attr{Key: key, Value: val})
166 }
167 return out
168}
169
170func convAttrValue(value attribute.Value) telemetry.Value {
171 switch value.Type() {
172 case attribute.BOOL:
173 return telemetry.BoolValue(value.AsBool())
174 case attribute.INT64:
175 return telemetry.Int64Value(value.AsInt64())
176 case attribute.FLOAT64:
177 return telemetry.Float64Value(value.AsFloat64())
178 case attribute.STRING:
179 v := truncate(maxSpan.AttrValueLen, value.AsString())
180 return telemetry.StringValue(v)
181 case attribute.BOOLSLICE:
182 slice := value.AsBoolSlice()
183 out := make([]telemetry.Value, 0, len(slice))
184 for _, v := range slice {
185 out = append(out, telemetry.BoolValue(v))
186 }
187 return telemetry.SliceValue(out...)
188 case attribute.INT64SLICE:
189 slice := value.AsInt64Slice()
190 out := make([]telemetry.Value, 0, len(slice))
191 for _, v := range slice {
192 out = append(out, telemetry.Int64Value(v))
193 }
194 return telemetry.SliceValue(out...)
195 case attribute.FLOAT64SLICE:
196 slice := value.AsFloat64Slice()
197 out := make([]telemetry.Value, 0, len(slice))
198 for _, v := range slice {
199 out = append(out, telemetry.Float64Value(v))
200 }
201 return telemetry.SliceValue(out...)
202 case attribute.STRINGSLICE:
203 slice := value.AsStringSlice()
204 out := make([]telemetry.Value, 0, len(slice))
205 for _, v := range slice {
206 v = truncate(maxSpan.AttrValueLen, v)
207 out = append(out, telemetry.StringValue(v))
208 }
209 return telemetry.SliceValue(out...)
210 }
211 return telemetry.Value{}
212}
213
214// truncate returns a truncated version of s such that it contains less than
215// the limit number of characters. Truncation is applied by returning the limit
216// number of valid characters contained in s.
217//
218// If limit is negative, it returns the original string.
219//
220// UTF-8 is supported. When truncating, all invalid characters are dropped
221// before applying truncation.
222//
223// If s already contains less than the limit number of bytes, it is returned
224// unchanged. No invalid characters are removed.
225func truncate(limit int, s string) string {
226 // This prioritize performance in the following order based on the most
227 // common expected use-cases.
228 //
229 // - Short values less than the default limit (128).
230 // - Strings with valid encodings that exceed the limit.
231 // - No limit.
232 // - Strings with invalid encodings that exceed the limit.
233 if limit < 0 || len(s) <= limit {
234 return s
235 }
236
237 // Optimistically, assume all valid UTF-8.
238 var b strings.Builder
239 count := 0
240 for i, c := range s {
241 if c != utf8.RuneError {
242 count++
243 if count > limit {
244 return s[:i]
245 }
246 continue
247 }
248
249 _, size := utf8.DecodeRuneInString(s[i:])
250 if size == 1 {
251 // Invalid encoding.
252 b.Grow(len(s) - 1)
253 _, _ = b.WriteString(s[:i])
254 s = s[i:]
255 break
256 }
257 }
258
259 // Fast-path, no invalid input.
260 if b.Cap() == 0 {
261 return s
262 }
263
264 // Truncate while validating UTF-8.
265 for i := 0; i < len(s) && count < limit; {
266 c := s[i]
267 if c < utf8.RuneSelf {
268 // Optimization for single byte runes (common case).
269 _ = b.WriteByte(c)
270 i++
271 count++
272 continue
273 }
274
275 _, size := utf8.DecodeRuneInString(s[i:])
276 if size == 1 {
277 // We checked for all 1-byte runes above, this is a RuneError.
278 i++
279 continue
280 }
281
282 _, _ = b.WriteString(s[i : i+size])
283 i += size
284 count++
285 }
286
287 return b.String()
288}
289
290func (s *span) End(opts ...trace.SpanEndOption) {
291 if s == nil || !s.sampled.Swap(false) {
292 return
293 }
294
295 // s.end exists so the lock (s.mu) is not held while s.ended is called.
296 s.ended(s.end(opts))
297}
298
299func (s *span) end(opts []trace.SpanEndOption) []byte {
300 s.mu.Lock()
301 defer s.mu.Unlock()
302
303 cfg := trace.NewSpanEndConfig(opts...)
304 if t := cfg.Timestamp(); !t.IsZero() {
305 s.span.EndTime = cfg.Timestamp()
306 } else {
307 s.span.EndTime = time.Now()
308 }
309
310 b, _ := json.Marshal(s.traces) // TODO: do not ignore this error.
311 return b
312}
313
314// Expected to be implemented in eBPF.
315//
316//go:noinline
317func (*span) ended(buf []byte) { ended(buf) }
318
319// ended is used for testing.
320var ended = func([]byte) {}
321
322func (s *span) RecordError(err error, opts ...trace.EventOption) {
323 if s == nil || err == nil || !s.sampled.Load() {
324 return
325 }
326
327 cfg := trace.NewEventConfig(opts...)
328
329 attrs := cfg.Attributes()
330 attrs = append(attrs,
331 semconv.ExceptionType(typeStr(err)),
332 semconv.ExceptionMessage(err.Error()),
333 )
334 if cfg.StackTrace() {
335 buf := make([]byte, 2048)
336 n := runtime.Stack(buf, false)
337 attrs = append(attrs, semconv.ExceptionStacktrace(string(buf[0:n])))
338 }
339
340 s.mu.Lock()
341 defer s.mu.Unlock()
342
343 s.addEvent(semconv.ExceptionEventName, cfg.Timestamp(), attrs)
344}
345
346func typeStr(i any) string {
347 t := reflect.TypeOf(i)
348 if t.PkgPath() == "" && t.Name() == "" {
349 // Likely a builtin type.
350 return t.String()
351 }
352 return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
353}
354
355func (s *span) AddEvent(name string, opts ...trace.EventOption) {
356 if s == nil || !s.sampled.Load() {
357 return
358 }
359
360 cfg := trace.NewEventConfig(opts...)
361
362 s.mu.Lock()
363 defer s.mu.Unlock()
364
365 s.addEvent(name, cfg.Timestamp(), cfg.Attributes())
366}
367
368// addEvent adds an event with name and attrs at tStamp to the span. The span
369// lock (s.mu) needs to be held by the caller.
370func (s *span) addEvent(name string, tStamp time.Time, attrs []attribute.KeyValue) {
371 limit := maxSpan.Events
372
373 if limit == 0 {
374 s.span.DroppedEvents++
375 return
376 }
377
378 if limit > 0 && len(s.span.Events) == limit {
379 // Drop head while avoiding allocation of more capacity.
380 copy(s.span.Events[:limit-1], s.span.Events[1:])
381 s.span.Events = s.span.Events[:limit-1]
382 s.span.DroppedEvents++
383 }
384
385 e := &telemetry.SpanEvent{Time: tStamp, Name: name}
386 e.Attrs, e.DroppedAttrs = convCappedAttrs(maxSpan.EventAttrs, attrs)
387
388 s.span.Events = append(s.span.Events, e)
389}
390
391func (s *span) AddLink(link trace.Link) {
392 if s == nil || !s.sampled.Load() {
393 return
394 }
395
396 l := maxSpan.Links
397
398 s.mu.Lock()
399 defer s.mu.Unlock()
400
401 if l == 0 {
402 s.span.DroppedLinks++
403 return
404 }
405
406 if l > 0 && len(s.span.Links) == l {
407 // Drop head while avoiding allocation of more capacity.
408 copy(s.span.Links[:l-1], s.span.Links[1:])
409 s.span.Links = s.span.Links[:l-1]
410 s.span.DroppedLinks++
411 }
412
413 s.span.Links = append(s.span.Links, convLink(link))
414}
415
416func convLinks(links []trace.Link) []*telemetry.SpanLink {
417 out := make([]*telemetry.SpanLink, 0, len(links))
418 for _, link := range links {
419 out = append(out, convLink(link))
420 }
421 return out
422}
423
424func convLink(link trace.Link) *telemetry.SpanLink {
425 l := &telemetry.SpanLink{
426 TraceID: telemetry.TraceID(link.SpanContext.TraceID()),
427 SpanID: telemetry.SpanID(link.SpanContext.SpanID()),
428 TraceState: link.SpanContext.TraceState().String(),
429 Flags: uint32(link.SpanContext.TraceFlags()),
430 }
431 l.Attrs, l.DroppedAttrs = convCappedAttrs(maxSpan.LinkAttrs, link.Attributes)
432
433 return l
434}
435
436func (s *span) SetName(name string) {
437 if s == nil || !s.sampled.Load() {
438 return
439 }
440
441 s.mu.Lock()
442 defer s.mu.Unlock()
443
444 s.span.Name = name
445}
446
447func (*span) TracerProvider() trace.TracerProvider { return TracerProvider() }