| // Copyright The OpenTelemetry Authors |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| package sdk |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "runtime" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "time" |
| "unicode/utf8" |
| |
| "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/noop" |
| |
| "go.opentelemetry.io/auto/sdk/internal/telemetry" |
| ) |
| |
| type span struct { |
| noop.Span |
| |
| spanContext trace.SpanContext |
| sampled atomic.Bool |
| |
| mu sync.Mutex |
| traces *telemetry.Traces |
| span *telemetry.Span |
| } |
| |
| func (s *span) SpanContext() trace.SpanContext { |
| if s == nil { |
| return trace.SpanContext{} |
| } |
| // s.spanContext is immutable, do not acquire lock s.mu. |
| return s.spanContext |
| } |
| |
| func (s *span) IsRecording() bool { |
| if s == nil { |
| return false |
| } |
| |
| return s.sampled.Load() |
| } |
| |
| func (s *span) SetStatus(c codes.Code, msg string) { |
| if s == nil || !s.sampled.Load() { |
| return |
| } |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| if s.span.Status == nil { |
| s.span.Status = new(telemetry.Status) |
| } |
| |
| s.span.Status.Message = msg |
| |
| switch c { |
| case codes.Unset: |
| s.span.Status.Code = telemetry.StatusCodeUnset |
| case codes.Error: |
| s.span.Status.Code = telemetry.StatusCodeError |
| case codes.Ok: |
| s.span.Status.Code = telemetry.StatusCodeOK |
| } |
| } |
| |
| func (s *span) SetAttributes(attrs ...attribute.KeyValue) { |
| if s == nil || !s.sampled.Load() { |
| return |
| } |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| limit := maxSpan.Attrs |
| if limit == 0 { |
| // No attributes allowed. |
| s.span.DroppedAttrs += uint32(len(attrs)) |
| return |
| } |
| |
| m := make(map[string]int) |
| for i, a := range s.span.Attrs { |
| m[a.Key] = i |
| } |
| |
| for _, a := range attrs { |
| val := convAttrValue(a.Value) |
| if val.Empty() { |
| s.span.DroppedAttrs++ |
| continue |
| } |
| |
| if idx, ok := m[string(a.Key)]; ok { |
| s.span.Attrs[idx] = telemetry.Attr{ |
| Key: string(a.Key), |
| Value: val, |
| } |
| } else if limit < 0 || len(s.span.Attrs) < limit { |
| s.span.Attrs = append(s.span.Attrs, telemetry.Attr{ |
| Key: string(a.Key), |
| Value: val, |
| }) |
| m[string(a.Key)] = len(s.span.Attrs) - 1 |
| } else { |
| s.span.DroppedAttrs++ |
| } |
| } |
| } |
| |
| // convCappedAttrs converts up to limit attrs into a []telemetry.Attr. The |
| // number of dropped attributes is also returned. |
| func convCappedAttrs(limit int, attrs []attribute.KeyValue) ([]telemetry.Attr, uint32) { |
| if limit == 0 { |
| return nil, uint32(len(attrs)) |
| } |
| |
| if limit < 0 { |
| // Unlimited. |
| return convAttrs(attrs), 0 |
| } |
| |
| limit = min(len(attrs), limit) |
| return convAttrs(attrs[:limit]), uint32(len(attrs) - limit) |
| } |
| |
| func convAttrs(attrs []attribute.KeyValue) []telemetry.Attr { |
| if len(attrs) == 0 { |
| // Avoid allocations if not necessary. |
| return nil |
| } |
| |
| out := make([]telemetry.Attr, 0, len(attrs)) |
| for _, attr := range attrs { |
| key := string(attr.Key) |
| val := convAttrValue(attr.Value) |
| if val.Empty() { |
| continue |
| } |
| out = append(out, telemetry.Attr{Key: key, Value: val}) |
| } |
| return out |
| } |
| |
| func convAttrValue(value attribute.Value) telemetry.Value { |
| switch value.Type() { |
| case attribute.BOOL: |
| return telemetry.BoolValue(value.AsBool()) |
| case attribute.INT64: |
| return telemetry.Int64Value(value.AsInt64()) |
| case attribute.FLOAT64: |
| return telemetry.Float64Value(value.AsFloat64()) |
| case attribute.STRING: |
| v := truncate(maxSpan.AttrValueLen, value.AsString()) |
| return telemetry.StringValue(v) |
| case attribute.BOOLSLICE: |
| slice := value.AsBoolSlice() |
| out := make([]telemetry.Value, 0, len(slice)) |
| for _, v := range slice { |
| out = append(out, telemetry.BoolValue(v)) |
| } |
| return telemetry.SliceValue(out...) |
| case attribute.INT64SLICE: |
| slice := value.AsInt64Slice() |
| out := make([]telemetry.Value, 0, len(slice)) |
| for _, v := range slice { |
| out = append(out, telemetry.Int64Value(v)) |
| } |
| return telemetry.SliceValue(out...) |
| case attribute.FLOAT64SLICE: |
| slice := value.AsFloat64Slice() |
| out := make([]telemetry.Value, 0, len(slice)) |
| for _, v := range slice { |
| out = append(out, telemetry.Float64Value(v)) |
| } |
| return telemetry.SliceValue(out...) |
| case attribute.STRINGSLICE: |
| slice := value.AsStringSlice() |
| out := make([]telemetry.Value, 0, len(slice)) |
| for _, v := range slice { |
| v = truncate(maxSpan.AttrValueLen, v) |
| out = append(out, telemetry.StringValue(v)) |
| } |
| return telemetry.SliceValue(out...) |
| } |
| return telemetry.Value{} |
| } |
| |
| // truncate returns a truncated version of s such that it contains less than |
| // the limit number of characters. Truncation is applied by returning the limit |
| // number of valid characters contained in s. |
| // |
| // If limit is negative, it returns the original string. |
| // |
| // UTF-8 is supported. When truncating, all invalid characters are dropped |
| // before applying truncation. |
| // |
| // If s already contains less than the limit number of bytes, it is returned |
| // unchanged. No invalid characters are removed. |
| func truncate(limit int, s string) string { |
| // This prioritize performance in the following order based on the most |
| // common expected use-cases. |
| // |
| // - Short values less than the default limit (128). |
| // - Strings with valid encodings that exceed the limit. |
| // - No limit. |
| // - Strings with invalid encodings that exceed the limit. |
| if limit < 0 || len(s) <= limit { |
| return s |
| } |
| |
| // Optimistically, assume all valid UTF-8. |
| var b strings.Builder |
| count := 0 |
| for i, c := range s { |
| if c != utf8.RuneError { |
| count++ |
| if count > limit { |
| return s[:i] |
| } |
| continue |
| } |
| |
| _, size := utf8.DecodeRuneInString(s[i:]) |
| if size == 1 { |
| // Invalid encoding. |
| b.Grow(len(s) - 1) |
| _, _ = b.WriteString(s[:i]) |
| s = s[i:] |
| break |
| } |
| } |
| |
| // Fast-path, no invalid input. |
| if b.Cap() == 0 { |
| return s |
| } |
| |
| // Truncate while validating UTF-8. |
| for i := 0; i < len(s) && count < limit; { |
| c := s[i] |
| if c < utf8.RuneSelf { |
| // Optimization for single byte runes (common case). |
| _ = b.WriteByte(c) |
| i++ |
| count++ |
| continue |
| } |
| |
| _, size := utf8.DecodeRuneInString(s[i:]) |
| if size == 1 { |
| // We checked for all 1-byte runes above, this is a RuneError. |
| i++ |
| continue |
| } |
| |
| _, _ = b.WriteString(s[i : i+size]) |
| i += size |
| count++ |
| } |
| |
| return b.String() |
| } |
| |
| func (s *span) End(opts ...trace.SpanEndOption) { |
| if s == nil || !s.sampled.Swap(false) { |
| return |
| } |
| |
| // s.end exists so the lock (s.mu) is not held while s.ended is called. |
| s.ended(s.end(opts)) |
| } |
| |
| func (s *span) end(opts []trace.SpanEndOption) []byte { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| cfg := trace.NewSpanEndConfig(opts...) |
| if t := cfg.Timestamp(); !t.IsZero() { |
| s.span.EndTime = cfg.Timestamp() |
| } else { |
| s.span.EndTime = time.Now() |
| } |
| |
| b, _ := json.Marshal(s.traces) // TODO: do not ignore this error. |
| return 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.Load() { |
| 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.mu.Lock() |
| defer s.mu.Unlock() |
| |
| 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.Load() { |
| return |
| } |
| |
| cfg := trace.NewEventConfig(opts...) |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| s.addEvent(name, cfg.Timestamp(), cfg.Attributes()) |
| } |
| |
| // addEvent adds an event with name and attrs at tStamp to the span. The span |
| // lock (s.mu) needs to be held by the caller. |
| func (s *span) addEvent(name string, tStamp time.Time, attrs []attribute.KeyValue) { |
| limit := maxSpan.Events |
| |
| if limit == 0 { |
| s.span.DroppedEvents++ |
| return |
| } |
| |
| if limit > 0 && len(s.span.Events) == limit { |
| // Drop head while avoiding allocation of more capacity. |
| copy(s.span.Events[:limit-1], s.span.Events[1:]) |
| s.span.Events = s.span.Events[:limit-1] |
| s.span.DroppedEvents++ |
| } |
| |
| e := &telemetry.SpanEvent{Time: tStamp, Name: name} |
| e.Attrs, e.DroppedAttrs = convCappedAttrs(maxSpan.EventAttrs, attrs) |
| |
| s.span.Events = append(s.span.Events, e) |
| } |
| |
| func (s *span) AddLink(link trace.Link) { |
| if s == nil || !s.sampled.Load() { |
| return |
| } |
| |
| l := maxSpan.Links |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| if l == 0 { |
| s.span.DroppedLinks++ |
| return |
| } |
| |
| if l > 0 && len(s.span.Links) == l { |
| // Drop head while avoiding allocation of more capacity. |
| copy(s.span.Links[:l-1], s.span.Links[1:]) |
| s.span.Links = s.span.Links[:l-1] |
| s.span.DroppedLinks++ |
| } |
| |
| s.span.Links = append(s.span.Links, convLink(link)) |
| } |
| |
| func convLinks(links []trace.Link) []*telemetry.SpanLink { |
| out := make([]*telemetry.SpanLink, 0, len(links)) |
| for _, link := range links { |
| out = append(out, convLink(link)) |
| } |
| return out |
| } |
| |
| func convLink(link trace.Link) *telemetry.SpanLink { |
| l := &telemetry.SpanLink{ |
| TraceID: telemetry.TraceID(link.SpanContext.TraceID()), |
| SpanID: telemetry.SpanID(link.SpanContext.SpanID()), |
| TraceState: link.SpanContext.TraceState().String(), |
| Flags: uint32(link.SpanContext.TraceFlags()), |
| } |
| l.Attrs, l.DroppedAttrs = convCappedAttrs(maxSpan.LinkAttrs, link.Attributes) |
| |
| return l |
| } |
| |
| func (s *span) SetName(name string) { |
| if s == nil || !s.sampled.Load() { |
| return |
| } |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| s.span.Name = name |
| } |
| |
| func (*span) TracerProvider() trace.TracerProvider { return TracerProvider() } |