Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add span attribute limit to auto/sdk #1315

Merged
merged 7 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading