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

[PoC] Configure span limits #1301

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
87 changes: 87 additions & 0 deletions sdk/limit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package sdk

import (
"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
}
}

return defaultVal
}
81 changes: 81 additions & 0 deletions sdk/limit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package sdk

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSpanAttrValLenLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.AttrValueLen },
-1,
"OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT",
"OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT",
)
}

func TestSpanAttrsLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.Attrs },
128,
"OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT",
"OTEL_ATTRIBUTE_COUNT_LIMIT",
)
}

func TestSpanEventsLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.Events },
128,
"OTEL_SPAN_EVENT_COUNT_LIMIT",
)
}

func TestSpanLinksLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.Links },
128,
"OTEL_SPAN_LINK_COUNT_LIMIT",
)
}

func TestSpanEventAttrsLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.EventAttrs },
128,
"OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT",
)
}

func TestSpanLinkAttrsLimit(t *testing.T) {
testLimit(
t,
func(sl spanLimits) int { return sl.LinkAttrs },
128,
"OTEL_LINK_ATTRIBUTE_COUNT_LIMIT",
)
}

func testLimit(t *testing.T, f func(spanLimits) int, zero int, keys ...string) {
t.Helper()

t.Run("Default", func(t *testing.T) {
assert.Equal(t, zero, f(newSpanLimits()))
})

for _, key := range keys {
t.Run(key, func(t *testing.T) {
t.Setenv(key, "43")
assert.Equal(t, 43, f(newSpanLimits()))
})
}
}
140 changes: 134 additions & 6 deletions sdk/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"fmt"
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"unicode/utf8"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
Expand Down Expand Up @@ -80,7 +82,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 +97,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 +106,40 @@ func (s *span) SetAttributes(attrs ...attribute.KeyValue) {
Key: string(a.Key),
Value: val,
}
} else {
} else if 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, int) {
if limit == 0 {
return nil, len(attrs)
}

if limit < 0 {
// Unlimited.
return convAttrs(attrs), 0
}

limit = min(len(attrs), limit)
return convAttrs(attrs[:limit]), 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 All @@ -130,7 +161,8 @@ func convAttrValue(value attribute.Value) telemetry.Value {
case attribute.FLOAT64:
return telemetry.Float64Value(value.AsFloat64())
case attribute.STRING:
return telemetry.StringValue(value.AsString())
v := truncate(maxSpan.AttrValueLen, value.AsString())
return telemetry.StringValue(v)
case attribute.BOOLSLICE:
slice := value.AsBoolSlice()
out := make([]telemetry.Value, 0, len(slice))
Expand All @@ -156,13 +188,90 @@ func convAttrValue(value attribute.Value) telemetry.Value {
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
Expand Down Expand Up @@ -258,10 +367,22 @@ func (s *span) AddLink(link trace.Link) {
return
}

l := maxSpan.Links

s.mu.Lock()
defer s.mu.Unlock()

// TODO: handle link limits.
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))
}
Expand All @@ -275,13 +396,20 @@ func convLinks(links []trace.Link) []*telemetry.SpanLink {
}

func convLink(link trace.Link) *telemetry.SpanLink {
return &telemetry.SpanLink{
l := &telemetry.SpanLink{
TraceID: telemetry.TraceID(link.SpanContext.TraceID()),
SpanID: telemetry.SpanID(link.SpanContext.SpanID()),
TraceState: link.SpanContext.TraceState().String(),
Attrs: convAttrs(link.Attributes),
Flags: uint32(link.SpanContext.TraceFlags()),
}

limit := maxSpan.LinkAttrs

var dropped int
l.Attrs, dropped = convCappedAttrs(limit, link.Attributes)
l.DroppedAttrs += uint32(dropped)

return l
}

func (s *span) SetName(name string) {
Expand Down
Loading
Loading