From f75dc055476a5f5fed34be04bdd002ccf2892cb8 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 27 Nov 2024 09:16:17 -0800 Subject: [PATCH] Add span attribute limit to `auto/sdk` (#1315) * Configure span limits Add functionality to `auto/sdk` so it can interpret configuration from OTel defined environment variables for tracing limits. * Add attribute limits * Add changelog entry * Change convCappedAttrs return drop count to uint32 * Replace tests with table test * Log failure to parse env var --- CHANGELOG.md | 4 ++ sdk/limit.go | 94 ++++++++++++++++++++++++++++++++++++++++ sdk/limit_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++ sdk/span.go | 33 +++++++++++++- sdk/span_test.go | 38 ++++++++++++++++ sdk/tracer.go | 2 +- 6 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 sdk/limit.go create mode 100644 sdk/limit_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c93f3179e..4b216387d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http ## [Unreleased] +### Added + +- Add support for span attribute limits to `go.opentelemtry.io/auto/sdk`. ([#1315](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1315)) + ## [v0.18.0-alpha] - 2024-11-20 ### Changed diff --git a/sdk/limit.go b/sdk/limit.go new file mode 100644 index 000000000..86babf1a8 --- /dev/null +++ b/sdk/limit.go @@ -0,0 +1,94 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package sdk + +import ( + "log/slog" + "os" + "strconv" +) + +// maxSpan are the span limits resolved during startup. +var maxSpan = newSpanLimits() + +type spanLimits struct { + // Attrs is the number of allowed attributes for a span. + // + // This is resolved from the environment variable value for the + // OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT key if it exists. Otherwise, the + // environment variable value for OTEL_ATTRIBUTE_COUNT_LIMIT, or 128 if + // that is not set, is used. + Attrs int + // AttrValueLen is the maximum attribute value length allowed for a span. + // + // This is resolved from the environment variable value for the + // OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT key if it exists. Otherwise, the + // environment variable value for OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, or -1 + // if that is not set, is used. + AttrValueLen int + // Events is the number of allowed events for a span. + // + // This is resolved from the environment variable value for the + // OTEL_SPAN_EVENT_COUNT_LIMIT key, or 128 is used if that is not set. + Events int + // EventAttrs is the number of allowed attributes for a span event. + // + // The is resolved from the environment variable value for the + // OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT key, or 128 is used if that is not set. + EventAttrs int + // Links is the number of allowed Links for a span. + // + // This is resolved from the environment variable value for the + // OTEL_SPAN_LINK_COUNT_LIMIT, or 128 is used if that is not set. + Links int + // LinkAttrs is the number of allowed attributes for a span link. + // + // This is resolved from the environment variable value for the + // OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, or 128 is used if that is not set. + LinkAttrs int +} + +func newSpanLimits() spanLimits { + return spanLimits{ + Attrs: firstEnv( + 128, + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", + "OTEL_ATTRIBUTE_COUNT_LIMIT", + ), + AttrValueLen: firstEnv( + -1, // Unlimited. + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", + "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", + ), + Events: firstEnv(128, "OTEL_SPAN_EVENT_COUNT_LIMIT"), + EventAttrs: firstEnv(128, "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"), + Links: firstEnv(128, "OTEL_SPAN_LINK_COUNT_LIMIT"), + LinkAttrs: firstEnv(128, "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"), + } +} + +// firstEnv returns the parsed integer value of the first matching environment +// variable from keys. The defaultVal is returned if the value is not an +// integer or no match is found. +func firstEnv(defaultVal int, keys ...string) int { + for _, key := range keys { + strV := os.Getenv(key) + if strV == "" { + continue + } + + v, err := strconv.Atoi(strV) + if err == nil { + return v + } + slog.Warn( + "invalid limit environment variable", + "error", err, + "key", key, + "value", strV, + ) + } + + return defaultVal +} diff --git a/sdk/limit_test.go b/sdk/limit_test.go new file mode 100644 index 000000000..448651f2a --- /dev/null +++ b/sdk/limit_test.go @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package sdk + +import ( + "bytes" + "encoding/json" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpanLimit(t *testing.T) { + tests := []struct { + name string + get func(spanLimits) int + zero int + keys []string + }{ + { + name: "AttributeValueLengthLimit", + get: func(sl spanLimits) int { return sl.AttrValueLen }, + zero: -1, + keys: []string{ + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", + "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", + }, + }, + { + name: "AttributeCountLimit", + get: func(sl spanLimits) int { return sl.Attrs }, + zero: 128, + keys: []string{ + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", + "OTEL_ATTRIBUTE_COUNT_LIMIT", + }, + }, + { + name: "EventCountLimit", + get: func(sl spanLimits) int { return sl.Events }, + zero: 128, + keys: []string{"OTEL_SPAN_EVENT_COUNT_LIMIT"}, + }, + { + name: "EventAttributeCountLimit", + get: func(sl spanLimits) int { return sl.EventAttrs }, + zero: 128, + keys: []string{"OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"}, + }, + { + name: "LinkCountLimit", + get: func(sl spanLimits) int { return sl.Links }, + zero: 128, + keys: []string{"OTEL_SPAN_LINK_COUNT_LIMIT"}, + }, + { + name: "LinkAttributeCountLimit", + get: func(sl spanLimits) int { return sl.LinkAttrs }, + zero: 128, + keys: []string{"OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("Default", func(t *testing.T) { + assert.Equal(t, test.zero, test.get(newSpanLimits())) + }) + + t.Run("ValidValue", func(t *testing.T) { + for _, key := range test.keys { + t.Run(key, func(t *testing.T) { + t.Setenv(key, "43") + assert.Equal(t, 43, test.get(newSpanLimits())) + }) + } + }) + + t.Run("InvalidValue", func(t *testing.T) { + for _, key := range test.keys { + t.Run(key, func(t *testing.T) { + orig := slog.Default() + t.Cleanup(func() { slog.SetDefault(orig) }) + + var buf bytes.Buffer + h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{}) + slog.SetDefault(slog.New(h)) + + const value = "invalid int value." + t.Setenv(key, value) + assert.Equal(t, test.zero, test.get(newSpanLimits())) + + var data map[string]any + require.NoError(t, json.NewDecoder(&buf).Decode(&data)) + assert.Equal(t, "invalid limit environment variable", data["msg"], "log message") + assert.Equal(t, "WARN", data["level"], "logged level") + assert.Equal(t, key, data["key"], "logged key") + assert.Equal(t, value, data["value"], "logged value") + assert.NotEmpty(t, data["error"], "logged error") + }) + } + }) + }) + } +} diff --git a/sdk/span.go b/sdk/span.go index d8f5786e0..e81a13014 100644 --- a/sdk/span.go +++ b/sdk/span.go @@ -80,7 +80,12 @@ func (s *span) SetAttributes(attrs ...attribute.KeyValue) { s.mu.Lock() defer s.mu.Unlock() - // TODO: handle attribute limits. + 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 { @@ -90,6 +95,7 @@ func (s *span) SetAttributes(attrs ...attribute.KeyValue) { for _, a := range attrs { val := convAttrValue(a.Value) if val.Empty() { + s.span.DroppedAttrs++ continue } @@ -98,17 +104,40 @@ func (s *span) SetAttributes(attrs ...attribute.KeyValue) { Key: string(a.Key), Value: val, } - } else { + } 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) diff --git a/sdk/span_test.go b/sdk/span_test.go index 00d3a272d..e855aa4b4 100644 --- a/sdk/span_test.go +++ b/sdk/span_test.go @@ -456,6 +456,44 @@ func TestSpanSetAttributes(t *testing.T) { assert.Equal(t, tAttrs, s.span.Attrs, "SpanAttributes did not override") } +func TestSpanAttributeLimits(t *testing.T) { + tests := []struct { + limit int + want []telemetry.Attr + dropped uint32 + }{ + {0, nil, uint32(len(tAttrs))}, + {2, tAttrs[:2], uint32(len(tAttrs) - 2)}, + {len(tAttrs), tAttrs, 0}, + {-1, tAttrs, 0}, + } + + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.Attrs + maxSpan.Attrs = test.limit + t.Cleanup(func() { maxSpan.Attrs = orig }) + + builder := spanBuilder{} + + s := builder.Build() + s.SetAttributes(attrs...) + assert.Equal(t, test.want, s.span.Attrs, "set span attributes") + assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs") + + s.SetAttributes(attrs...) + assert.Equal(t, test.want, s.span.Attrs, "set span attributes twice") + assert.Equal(t, 2*test.dropped, s.span.DroppedAttrs, "2x dropped attrs") + + builder.Options = []trace.SpanStartOption{trace.WithAttributes(attrs...)} + + s = builder.Build() + assert.Equal(t, test.want, s.span.Attrs, "new span attributes") + assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs") + }) + } +} + func TestSpanTracerProvider(t *testing.T) { var s span diff --git a/sdk/tracer.go b/sdk/tracer.go index 478141fff..b2c6ea62e 100644 --- a/sdk/tracer.go +++ b/sdk/tracer.go @@ -67,9 +67,9 @@ func (t tracer) traces(name string, cfg trace.SpanConfig, sc, psc trace.SpanCont ParentSpanID: telemetry.SpanID(psc.SpanID()), Name: name, Kind: spanKind(cfg.SpanKind()), - Attrs: convAttrs(cfg.Attributes()), Links: convLinks(cfg.Links()), } + span.Attrs, span.DroppedAttrs = convCappedAttrs(maxSpan.Attrs, cfg.Attributes()) if t := cfg.Timestamp(); !t.IsZero() { span.StartTime = cfg.Timestamp()