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