Skip to content

Commit

Permalink
Add span attribute limit to auto/sdk (#1315)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MrAlias authored Nov 27, 2024
1 parent 65ba45d commit f75dc05
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions sdk/limit.go
Original file line number Diff line number Diff line change
@@ -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
}
108 changes: 108 additions & 0 deletions sdk/limit_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
})
})
}
}
33 changes: 31 additions & 2 deletions sdk/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions sdk/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion sdk/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit f75dc05

Please sign in to comment.