Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f7c180e
implementation of `otel.sdk.processor.span.processed` metric for simp…
mahendrabishnoi2 Aug 9, 2025
f15ae4e
add correct PR number
mahendrabishnoi2 Aug 9, 2025
d743f6a
fix component type
mahendrabishnoi2 Aug 9, 2025
b686526
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Aug 16, 2025
17eb6c7
processorIDCounter -> simpleProcessorIDCounter as processorIDCounter …
mahendrabishnoi2 Aug 16, 2025
f8aa01e
test cases for simple span processor
mahendrabishnoi2 Aug 16, 2025
5c0fafb
fix lint
mahendrabishnoi2 Aug 16, 2025
6f72bbb
review comments
mahendrabishnoi2 Aug 16, 2025
ec0b88a
review comments
mahendrabishnoi2 Aug 16, 2025
966d7d1
review comments
mahendrabishnoi2 Aug 16, 2025
df565df
run `make precommit`
mahendrabishnoi2 Aug 16, 2025
053ca17
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Aug 28, 2025
97c11ff
fix issue caused by merge with main due to function name collision
mahendrabishnoi2 Aug 28, 2025
89e258e
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Sep 17, 2025
9753d0f
fix changelog -> move to unreleased section
mahendrabishnoi2 Sep 17, 2025
754b9bb
move sdk observability out of simpleSpanProcessor struct
mahendrabishnoi2 Sep 17, 2025
5545ac6
run make precommit
mahendrabishnoi2 Sep 17, 2025
a4ab409
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Sep 27, 2025
e855db1
Record -> SpanProcessed
mahendrabishnoi2 Sep 27, 2025
558eb43
add tests for simple_span_processor in observ package
mahendrabishnoi2 Sep 27, 2025
323bc44
fix formatting
mahendrabishnoi2 Sep 27, 2025
8e9b90b
add benchmark test
mahendrabishnoi2 Sep 27, 2025
1cca2be
fix formatting
mahendrabishnoi2 Sep 27, 2025
870ed15
fix formatting
mahendrabishnoi2 Sep 27, 2025
927d13b
review comment - rename sspComponentId to sspComponentID
mahendrabishnoi2 Sep 28, 2025
0c5fa21
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Sep 28, 2025
9c29553
capture span processed metric irrespective of whether its sampled or …
mahendrabishnoi2 Oct 8, 2025
9bcc692
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 8, 2025
36be6aa
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 8, 2025
83f6d2a
review comments, optimize memory allocation by precomputing in succes…
mahendrabishnoi2 Oct 11, 2025
1028666
rename self observability -> observability
mahendrabishnoi2 Oct 11, 2025
bd07d99
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 11, 2025
7f1ab1b
fix CHANGELOG.md
mahendrabishnoi2 Oct 11, 2025
7ae87cb
retrigger build
mahendrabishnoi2 Oct 11, 2025
c44abb6
reduce allocations in happy path to 0 (from 1)
mahendrabishnoi2 Oct 11, 2025
747e794
address review comments
mahendrabishnoi2 Oct 14, 2025
88f0c5f
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 14, 2025
01e65b1
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 15, 2025
d833159
Merge branch 'main' into sdk-trace-simple-processor-metrics
mahendrabishnoi2 Oct 17, 2025
c08bb45
remove file
mahendrabishnoi2 Oct 17, 2025
69b3a14
fix import
mahendrabishnoi2 Oct 17, 2025
3a89a59
Merge branch 'main' into sdk-trace-simple-processor-metrics
MrAlias Oct 17, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#7353)
- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#7459)
- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7486)
- Add experimental observability metrics for simple span processor in `go.opentelemetry.io/otel/sdk/trace`. (#7374)

### Fixed

Expand Down
97 changes: 97 additions & 0 deletions sdk/trace/internal/observ/simple_span_processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package observ // import "go.opentelemetry.io/otel/sdk/trace/internal/observ"

import (
"context"
"fmt"
"sync"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk"
"go.opentelemetry.io/otel/sdk/trace/internal/x"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv"
)

var measureAttrsPool = sync.Pool{
New: func() any {
// "component.name" + "component.type" + "error.type"
const n = 1 + 1 + 1
s := make([]attribute.KeyValue, 0, n)
// Return a pointer to a slice instead of a slice itself
// to avoid allocations on every call.
return &s
},
}

// SSP is the instrumentation for an OTel SDK SimpleSpanProcessor.
type SSP struct {
spansProcessedCounter metric.Int64Counter
addOpts []metric.AddOption
attrs []attribute.KeyValue
}

// SSPComponentName returns the component name attribute for a
// SimpleSpanProcessor with the given ID.
func SSPComponentName(id int64) attribute.KeyValue {
t := otelconv.ComponentTypeSimpleSpanProcessor
name := fmt.Sprintf("%s/%d", t, id)
return semconv.OTelComponentName(name)
}

// NewSSP returns instrumentation for an OTel SDK SimpleSpanProcessor with the
// provided ID.
//
// If the experimental observability is disabled, nil is returned.
func NewSSP(id int64) (*SSP, error) {
if !x.Observability.Enabled() {
return nil, nil
}

meter := otel.GetMeterProvider().Meter(
ScopeName,
metric.WithInstrumentationVersion(sdk.Version()),
metric.WithSchemaURL(SchemaURL),
)
spansProcessedCounter, err := otelconv.NewSDKProcessorSpanProcessed(meter)
if err != nil {
err = fmt.Errorf("failed to create SSP processed spans metric: %w", err)
}

componentName := SSPComponentName(id)
componentType := spansProcessedCounter.AttrComponentType(otelconv.ComponentTypeSimpleSpanProcessor)
attrs := []attribute.KeyValue{componentName, componentType}
addOpts := []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(attrs...))}

return &SSP{
spansProcessedCounter: spansProcessedCounter.Inst(),
addOpts: addOpts,
attrs: attrs,
}, err
}

// SpanProcessed records that a span has been processed by the SimpleSpanProcessor.
// If err is non-nil, it records the processing error as an attribute.
func (ssp *SSP) SpanProcessed(ctx context.Context, err error) {
ssp.spansProcessedCounter.Add(ctx, 1, ssp.addOption(err)...)
}

func (ssp *SSP) addOption(err error) []metric.AddOption {
if err == nil {
return ssp.addOpts
}
attrs := measureAttrsPool.Get().(*[]attribute.KeyValue)
defer func() {
*attrs = (*attrs)[:0] // reset the slice for reuse
measureAttrsPool.Put(attrs)
}()
*attrs = append(*attrs, ssp.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
// Do not inefficiently make a copy of attrs by using
// WithAttributes instead of WithAttributeSet.
return []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(*attrs...))}
}
127 changes: 127 additions & 0 deletions sdk/trace/internal/observ/simple_span_processor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package observ_test

import (
"errors"
"testing"

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

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/noop"
"go.opentelemetry.io/otel/sdk/trace/internal/observ"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)

const sspComponentID = 0

func TestSSPComponentName(t *testing.T) {
got := observ.SSPComponentName(10)
want := semconv.OTelComponentName("simple_span_processor/10")
assert.Equal(t, want, got)
}

func TestNewSSPError(t *testing.T) {
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")

orig := otel.GetMeterProvider()
t.Cleanup(func() { otel.SetMeterProvider(orig) })

mp := &errMeterProvider{err: assert.AnError}
otel.SetMeterProvider(mp)

_, err := observ.NewSSP(sspComponentID)
require.ErrorIs(t, err, assert.AnError, "new instrument errors")
assert.ErrorContains(t, err, "create SSP processed spans metric")
}

func TestNewSSPDisabled(t *testing.T) {
ssp, err := observ.NewSSP(sspComponentID)
assert.NoError(t, err)
assert.Nil(t, ssp)
}

func TestSSPSpanProcessed(t *testing.T) {
ctx := t.Context()
collect := setup(t)
ssp, err := observ.NewSSP(sspComponentID)
assert.NoError(t, err)

ssp.SpanProcessed(ctx, nil)
check(t, collect(), processed(dPt(sspSet(), 1)))
ssp.SpanProcessed(ctx, nil)
ssp.SpanProcessed(ctx, nil)
check(t, collect(), processed(dPt(sspSet(), 3)))

processErr := errors.New("error processing span")
ssp.SpanProcessed(ctx, processErr)
check(t, collect(), processed(
dPt(sspSet(), 3),
dPt(sspSet(semconv.ErrorType(processErr)), 1),
))
}

func BenchmarkSSP(b *testing.B) {
b.Setenv("OTEL_GO_X_OBSERVABILITY", "true")

newSSP := func(b *testing.B) *observ.SSP {
b.Helper()
ssp, err := observ.NewSSP(sspComponentID)
require.NoError(b, err)
require.NotNil(b, ssp)
return ssp
}

b.Run("SpanProcessed", func(b *testing.B) {
orig := otel.GetMeterProvider()
b.Cleanup(func() {
otel.SetMeterProvider(orig)
})

// Ensure deterministic benchmark by using noop meter.
otel.SetMeterProvider(noop.NewMeterProvider())

ssp := newSSP(b)
ctx := b.Context()

b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ssp.SpanProcessed(ctx, nil)
}
})
})

b.Run("SpanProcessedWithError", func(b *testing.B) {
orig := otel.GetMeterProvider()
b.Cleanup(func() {
otel.SetMeterProvider(orig)
})

// Ensure deterministic benchmark by using noop meter.
otel.SetMeterProvider(noop.NewMeterProvider())

ssp := newSSP(b)
ctx := b.Context()
processErr := errors.New("error processing span")

b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ssp.SpanProcessed(ctx, processErr)
}
})
})
}

func sspSet(attrs ...attribute.KeyValue) attribute.Set {
return attribute.NewSet(append([]attribute.KeyValue{
semconv.OTelComponentTypeSimpleSpanProcessor,
observ.SSPComponentName(sspComponentID),
}, attrs...)...)
}
1 change: 1 addition & 0 deletions sdk/trace/internal/x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ When enabled, the SDK will create the following metrics using the global `MeterP

- `otel.sdk.span.live`
- `otel.sdk.span.started`
- `otel.sdk.processor.span.processed` (only for simple span processor)

Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics.

Expand Down
31 changes: 30 additions & 1 deletion sdk/trace/simple_span_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package trace // import "go.opentelemetry.io/otel/sdk/trace"
import (
"context"
"sync"
"sync/atomic"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/sdk/trace/internal/observ"
"go.opentelemetry.io/otel/trace"
)

// simpleSpanProcessor is a SpanProcessor that synchronously sends all
Expand All @@ -17,6 +20,8 @@ type simpleSpanProcessor struct {
exporterMu sync.Mutex
exporter SpanExporter
stopOnce sync.Once

inst *observ.SSP
}

var _ SpanProcessor = (*simpleSpanProcessor)(nil)
Expand All @@ -33,11 +38,26 @@ func NewSimpleSpanProcessor(exporter SpanExporter) SpanProcessor {
ssp := &simpleSpanProcessor{
exporter: exporter,
}

var err error
ssp.inst, err = observ.NewSSP(nextSimpleProcessorID())
if err != nil {
otel.Handle(err)
}

global.Warn("SimpleSpanProcessor is not recommended for production use, consider using BatchSpanProcessor instead.")

return ssp
}

var simpleProcessorIDCounter atomic.Int64

// nextSimpleProcessorID returns an identifier for this simple span processor,
// starting with 0 and incrementing by 1 each time it is called.
func nextSimpleProcessorID() int64 {
return simpleProcessorIDCounter.Add(1) - 1
}

// OnStart does nothing.
func (*simpleSpanProcessor) OnStart(context.Context, ReadWriteSpan) {}

Expand All @@ -46,11 +66,20 @@ func (ssp *simpleSpanProcessor) OnEnd(s ReadOnlySpan) {
ssp.exporterMu.Lock()
defer ssp.exporterMu.Unlock()

var err error
if ssp.exporter != nil && s.SpanContext().TraceFlags().IsSampled() {
if err := ssp.exporter.ExportSpans(context.Background(), []ReadOnlySpan{s}); err != nil {
err = ssp.exporter.ExportSpans(context.Background(), []ReadOnlySpan{s})
if err != nil {
otel.Handle(err)
}
}

if ssp.inst != nil {
// Add the span to the context to ensure the metric is recorded
// with the correct span context.
ctx := trace.ContextWithSpanContext(context.Background(), s.SpanContext())
ssp.inst.SpanProcessed(ctx, err)
}
}

// Shutdown shuts down the exporter this SimpleSpanProcessor exports to.
Expand Down
Loading