Skip to content
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
48b1568
copied code with minor modifications for docs and feature flag: OTEL_…
mahendrabishnoi2 Aug 4, 2025
c03b9f0
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 7, 2025
4ddd0b3
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 7, 2025
987b77e
added self-observability support to stdoutmetric exporter for below m…
mahendrabishnoi2 Aug 7, 2025
b4d2756
fixed broken link
mahendrabishnoi2 Aug 7, 2025
b7181a4
added changelog entry for self-observability support in stdoutmetric …
mahendrabishnoi2 Aug 7, 2025
c6f91ac
run `make precommit`
mahendrabishnoi2 Aug 7, 2025
a9ad0e2
fix a bug where attributes defined in ExporterMetrics are mutated
mahendrabishnoi2 Aug 9, 2025
7b1fb2d
test cases for ExporterMetrics
mahendrabishnoi2 Aug 9, 2025
ef4a629
test cases for stdoutmetric exporter
mahendrabishnoi2 Aug 9, 2025
51f2f2b
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 9, 2025
38eb316
remove unused receiver to make linter (unused-receiver) happy
mahendrabishnoi2 Aug 9, 2025
b67b593
fix version
mahendrabishnoi2 Aug 9, 2025
ecbb337
fix version
mahendrabishnoi2 Aug 9, 2025
d6d0dd8
make stdoutMetricExporterComponentType as constant
mahendrabishnoi2 Aug 11, 2025
ee64105
Use defer to call trackExportFunc, Thanks to @flc1125
mahendrabishnoi2 Aug 11, 2025
ddfb3c3
duration -> durationSeconds
mahendrabishnoi2 Aug 11, 2025
db64dc0
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 11, 2025
089252e
suppress linter as err is used in defer statement
mahendrabishnoi2 Aug 11, 2025
884ade7
instead of suppressing error, split if and err check on 2 lines
mahendrabishnoi2 Aug 11, 2025
32b6f61
Merge branch 'main' into stdoutmetric-auto-instrumentation
pellared Aug 12, 2025
460f303
addressed review comment: use named return to make code more readable
mahendrabishnoi2 Aug 16, 2025
9fea018
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 16, 2025
a6f0637
name component similar to https://github.com/open-telemetry/opentelem…
mahendrabishnoi2 Aug 16, 2025
938cbb0
flatten the self-observability initialization and return the error to…
mahendrabishnoi2 Aug 16, 2025
b247192
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 27, 2025
ee0328e
Merge branch 'main' into stdoutmetric-auto-instrumentation
mahendrabishnoi2 Aug 28, 2025
4c357a9
address review comments
mahendrabishnoi2 Aug 28, 2025
2edfed9
address review comments
mahendrabishnoi2 Aug 28, 2025
18e0530
generate internal counter package so that it can be tested by resetti…
mahendrabishnoi2 Aug 28, 2025
9976cfc
review comments: improve tests, merge tests
mahendrabishnoi2 Aug 28, 2025
2f84c09
Run `make precommit`
mahendrabishnoi2 Aug 28, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ The next release will require at least [Go 1.24].
- Add experimental self-observability trace exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`.
Check the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace/internal/x` package documentation for more information. (#7133)
- Support testing of [Go 1.25]. (#7187)
- Add experimental self-observability stdoutmetric exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`.
Check the `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x` package documentation for more information. (#7150)

### Changed

Expand Down
90 changes: 82 additions & 8 deletions exporters/stdout/stdoutmetric/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,27 @@ import (
"sync"
"sync/atomic"

"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x"
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
semconv "go.opentelemetry.io/otel/semconv/v1.36.0"
)

// otelComponentType is a name identifying the type of the OpenTelemetry
// component. It is not a standardized OTel component type, so it uses the
// Go package prefixed type name to ensure uniqueness and identity.
const otelComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"

var exporterIDCounter atomic.Int64

// nextExporterID returns an identifier for this stdoutmetric exporter,
// starting with 0 and incrementing by 1 each time it is called.
func nextExporterID() int64 {
return exporterIDCounter.Add(1) - 1
}

// exporter is an OpenTelemetry metric exporter.
type exporter struct {
encVal atomic.Value // encoderHolder
Expand All @@ -25,6 +41,9 @@ type exporter struct {
aggregationSelector metric.AggregationSelector

redactTimestamps bool

selfObservabilityEnabled bool
exporterMetric *selfobservability.ExporterMetrics
}

// New returns a configured metric exporter.
Expand All @@ -34,12 +53,22 @@ type exporter struct {
func New(options ...Option) (metric.Exporter, error) {
cfg := newConfig(options...)
exp := &exporter{
temporalitySelector: cfg.temporalitySelector,
aggregationSelector: cfg.aggregationSelector,
redactTimestamps: cfg.redactTimestamps,
temporalitySelector: cfg.temporalitySelector,
aggregationSelector: cfg.aggregationSelector,
redactTimestamps: cfg.redactTimestamps,
selfObservabilityEnabled: x.SelfObservability.Enabled(),
}
exp.encVal.Store(*cfg.encoder)
return exp, nil
var err error
if exp.selfObservabilityEnabled {
componentName := fmt.Sprintf("%s/%d", otelComponentType, nextExporterID())
exp.exporterMetric, err = selfobservability.NewExporterMetrics(
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric",
semconv.OTelComponentName(componentName),
semconv.OTelComponentTypeKey.String(otelComponentType),
)
}
return exp, err
}

func (e *exporter) Temporality(k metric.InstrumentKind) metricdata.Temporality {
Expand All @@ -50,17 +79,28 @@ func (e *exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation {
return e.aggregationSelector(k)
}

func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) error {
if err := ctx.Err(); err != nil {
return err
func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) (err error) {
trackExportFunc := e.trackExport(context.Background(), countDataPoints(data))
defer func() { trackExportFunc(err) }()
err = ctx.Err()
if err != nil {
return
}
if e.redactTimestamps {
redactTimestamps(data)
}

global.Debug("STDOUT exporter export", "Data", data)

return e.encVal.Load().(encoderHolder).Encode(data)
err = e.encVal.Load().(encoderHolder).Encode(data)
return
}

func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) {
if !e.selfObservabilityEnabled {
return func(error) {}
}
return e.exporterMetric.TrackExport(ctx, count)
}

func (*exporter) ForceFlush(context.Context) error {
Expand Down Expand Up @@ -159,3 +199,37 @@ func redactDataPointTimestamps[T int64 | float64](sdp []metricdata.DataPoint[T])
}
return out
}

// countDataPoints counts the total number of data points in a ResourceMetrics.
func countDataPoints(rm *metricdata.ResourceMetrics) int64 {
if rm == nil {
return 0
}

var total int64
for _, sm := range rm.ScopeMetrics {
for _, m := range sm.Metrics {
switch data := m.Data.(type) {
case metricdata.Gauge[int64]:
total += int64(len(data.DataPoints))
case metricdata.Gauge[float64]:
total += int64(len(data.DataPoints))
case metricdata.Sum[int64]:
total += int64(len(data.DataPoints))
case metricdata.Sum[float64]:
total += int64(len(data.DataPoints))
case metricdata.Histogram[int64]:
total += int64(len(data.DataPoints))
case metricdata.Histogram[float64]:
total += int64(len(data.DataPoints))
case metricdata.ExponentialHistogram[int64]:
total += int64(len(data.DataPoints))
case metricdata.ExponentialHistogram[float64]:
total += int64(len(data.DataPoints))
case metricdata.Summary:
total += int64(len(data.DataPoints))
}
}
}
return total
}
210 changes: 210 additions & 0 deletions exporters/stdout/stdoutmetric/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"strconv"
"testing"
"time"

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

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
semconv "go.opentelemetry.io/otel/semconv/v1.36.0"
"go.opentelemetry.io/otel/semconv/v1.36.0/otelconv"
)

func testEncoderOption() stdoutmetric.Option {
Expand All @@ -25,6 +31,13 @@ func testEncoderOption() stdoutmetric.Option {
return stdoutmetric.WithEncoder(enc)
}

// failingEncoder always returns an error when Encode is called.
type failingEncoder struct{}

func (failingEncoder) Encode(any) error {
return errors.New("encoding failed")
}

func testCtxErrHonored(factory func(*testing.T) func(context.Context) error) func(t *testing.T) {
return func(t *testing.T) {
t.Helper()
Expand Down Expand Up @@ -178,3 +191,200 @@ func TestAggregationSelector(t *testing.T) {
var unknownKind metric.InstrumentKind
assert.Equal(t, metric.AggregationDrop{}, exp.Aggregation(unknownKind))
}

func TestExporter_Export_SelfObservability(t *testing.T) {
tests := []struct {
name string
selfObservabilityEnabled bool
expectedExportedCount int64
}{
{
name: "Enabled",
selfObservabilityEnabled: true,
expectedExportedCount: 19,
},
{
name: "Disabled",
selfObservabilityEnabled: false,
expectedExportedCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled))
reader := metric.NewManualReader()
mp := metric.NewMeterProvider(metric.WithReader(reader))
origMp := otel.GetMeterProvider()
otel.SetMeterProvider(mp)
defer otel.SetMeterProvider(origMp)

exp, err := stdoutmetric.New(testEncoderOption())
require.NoError(t, err)

rm := &metricdata.ResourceMetrics{
ScopeMetrics: []metricdata.ScopeMetrics{
{
Metrics: []metricdata.Metrics{
{
Name: "gauge_int64",
Data: metricdata.Gauge[int64]{
DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}},
},
},
{
Name: "gauge_float64",
Data: metricdata.Gauge[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Value: 1.0},
{Value: 2.0},
{Value: 3.0},
},
},
},
{
Name: "sum_int64",
Data: metricdata.Sum[int64]{
DataPoints: []metricdata.DataPoint[int64]{{Value: 10}},
},
},
{
Name: "sum_float64",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{{Value: 10.5}, {Value: 20.5}},
},
},
{
Name: "histogram_int64",
Data: metricdata.Histogram[int64]{
DataPoints: []metricdata.HistogramDataPoint[int64]{
{Count: 1},
{Count: 2},
{Count: 3},
},
},
},
{
Name: "histogram_float64",
Data: metricdata.Histogram[float64]{
DataPoints: []metricdata.HistogramDataPoint[float64]{{Count: 1}},
},
},
{
Name: "exponential_histogram_int64",
Data: metricdata.ExponentialHistogram[int64]{
DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{
{Count: 1},
{Count: 2},
},
},
},
{
Name: "exponential_histogram_float64",
Data: metricdata.ExponentialHistogram[float64]{
DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{
{Count: 1},
{Count: 2},
{Count: 3},
{Count: 4},
},
},
},
{
Name: "summary",
Data: metricdata.Summary{
DataPoints: []metricdata.SummaryDataPoint{{Count: 1}},
},
},
},
},
},
}

ctx := context.Background()
err = exp.Export(ctx, rm)
require.NoError(t, err)

var metrics metricdata.ResourceMetrics
err = reader.Collect(ctx, &metrics)
require.NoError(t, err)

var foundExported, foundDuration, foundInflight bool
var exportedCount int64

for _, sm := range metrics.ScopeMetrics {
for _, m := range sm.Metrics {
switch m.Name {
case otelconv.SDKExporterMetricDataPointExported{}.Name():
foundExported = true
if sum, ok := m.Data.(metricdata.Sum[int64]); ok {
for _, dp := range sum.DataPoints {
exportedCount += dp.Value
}
}
case otelconv.SDKExporterOperationDuration{}.Name():
foundDuration = true
case otelconv.SDKExporterMetricDataPointInflight{}.Name():
foundInflight = true
}
}
}

assert.Equal(t, tt.selfObservabilityEnabled, foundExported)
assert.Equal(t, tt.selfObservabilityEnabled, foundDuration)
assert.Equal(t, tt.selfObservabilityEnabled, foundInflight)
assert.Equal(t, tt.expectedExportedCount, exportedCount)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it’s just my OCD, but could we reorganize the unit tests here?

Also, we can use

func AssertEqual[T Datatypes](t TestingT, expected, actual T, opts ...Option) bool {
for testing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. I have merged both tests cases and using metricdatatest.AssertEqual now.

Could you please check this again and see if anything else can be improved?

})
}
}

func TestExporter_Export_EncodingErrorTracking(t *testing.T) {
t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
reader := metric.NewManualReader()
mp := metric.NewMeterProvider(metric.WithReader(reader))
origMp := otel.GetMeterProvider()
otel.SetMeterProvider(mp)
defer otel.SetMeterProvider(origMp)

exp, err := stdoutmetric.New(stdoutmetric.WithEncoder(failingEncoder{}))
assert.NoError(t, err)

rm := &metricdata.ResourceMetrics{
ScopeMetrics: []metricdata.ScopeMetrics{
{
Metrics: []metricdata.Metrics{
{
Name: "test_gauge",
Data: metricdata.Gauge[int64]{
DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}},
},
},
},
},
},
}

ctx := context.Background()
err = exp.Export(ctx, rm)
assert.EqualError(t, err, "encoding failed")

var metrics metricdata.ResourceMetrics
err = reader.Collect(ctx, &metrics)
require.NoError(t, err)

var foundErrorType bool
for _, sm := range metrics.ScopeMetrics {
for _, m := range sm.Metrics {
x := otelconv.SDKExporterMetricDataPointExported{}.Name()
if m.Name == x {
if sum, ok := m.Data.(metricdata.Sum[int64]); ok {
for _, dp := range sum.DataPoints {
var attr attribute.Value
attr, foundErrorType = dp.Attributes.Value(semconv.ErrorTypeKey)
assert.Equal(t, "*errors.errorString", attr.AsString())
}
}
}
}
}
assert.True(t, foundErrorType)
}
2 changes: 1 addition & 1 deletion exporters/stdout/stdoutmetric/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23.0
require (
github.com/stretchr/testify v1.11.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/metric v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/sdk/metric v1.37.0
)
Expand All @@ -16,7 +17,6 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/sys v0.35.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
Loading
Loading