diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e6a83b813..47844f6b57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Changed + +- `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric` does not prettifies the output by default anymore. (#4507) + ### Added - Add the "Roll the dice" getting started application example in `go.opentelemetry.io/otel/example/dice`. (#4539) +- The `WithWriter` and `WithPrettyPrint` options to `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric` to set a custom `io.Writer`, and allow displaying the output in human-readable JSON (#4507). + +### Removed + +- Remove `"go.opentelemetry.io/otel/bridge/opencensus".NewMetricExporter`, which is replaced by `NewMetricProducer`. (#4566) ## [1.19.0-rc.1/0.42.0-rc.1] 2023-09-14 diff --git a/bridge/opencensus/metric.go b/bridge/opencensus/metric.go index c2e0be49052..1c2496d8c9b 100644 --- a/bridge/opencensus/metric.go +++ b/bridge/opencensus/metric.go @@ -18,15 +18,12 @@ import ( "context" ocmetricdata "go.opencensus.io/metric/metricdata" - "go.opencensus.io/metric/metricexport" "go.opencensus.io/metric/metricproducer" - "go.opentelemetry.io/otel" internal "go.opentelemetry.io/otel/bridge/opencensus/internal/ocmetric" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/resource" ) const scopeName = "go.opentelemetry.io/otel/bridge/opencensus" @@ -60,40 +57,3 @@ func (p *producer) Produce(context.Context) ([]metricdata.ScopeMetrics, error) { Metrics: otelmetrics, }}, err } - -// exporter implements the OpenCensus metric Exporter interface using an -// OpenTelemetry base exporter. -type exporter struct { - base metric.Exporter - res *resource.Resource -} - -// NewMetricExporter returns an OpenCensus exporter that exports to an -// OpenTelemetry (push) exporter. -// -// Deprecated: Use [NewMetricProducer] instead. -func NewMetricExporter(base metric.Exporter, res *resource.Resource) metricexport.Exporter { - return &exporter{base: base, res: res} -} - -// ExportMetrics implements the OpenCensus metric Exporter interface by sending -// to an OpenTelemetry exporter. -func (e *exporter) ExportMetrics(ctx context.Context, ocmetrics []*ocmetricdata.Metric) error { - otelmetrics, err := internal.ConvertMetrics(ocmetrics) - if err != nil { - otel.Handle(err) - } - if len(otelmetrics) == 0 { - return nil - } - return e.base.Export(ctx, &metricdata.ResourceMetrics{ - Resource: e.res, - ScopeMetrics: []metricdata.ScopeMetrics{ - { - Scope: instrumentation.Scope{ - Name: scopeName, - }, - Metrics: otelmetrics, - }, - }}) -} diff --git a/bridge/opencensus/metric_test.go b/bridge/opencensus/metric_test.go index 58c11aadc0a..29ec835c8ba 100644 --- a/bridge/opencensus/metric_test.go +++ b/bridge/opencensus/metric_test.go @@ -16,7 +16,6 @@ package opencensus // import "go.opentelemetry.io/otel/bridge/opencensus" import ( "context" - "fmt" "testing" "time" @@ -27,10 +26,8 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/instrumentation" - "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" - "go.opentelemetry.io/otel/sdk/resource" ) func TestMetricProducer(t *testing.T) { @@ -160,127 +157,3 @@ type fakeOCProducer struct { func (f *fakeOCProducer) Read() []*ocmetricdata.Metric { return f.metrics } - -func TestPushMetricsExporter(t *testing.T) { - now := time.Now() - for _, tc := range []struct { - desc string - input []*ocmetricdata.Metric - inputResource *resource.Resource - exportErr error - expected *metricdata.ResourceMetrics - expectErr bool - }{ - { - desc: "empty batch isn't sent", - }, - { - desc: "export error", - exportErr: fmt.Errorf("failed to export"), - input: []*ocmetricdata.Metric{ - { - Resource: &ocresource.Resource{ - Labels: map[string]string{ - "R1": "V1", - "R2": "V2", - }, - }, - TimeSeries: []*ocmetricdata.TimeSeries{ - { - StartTime: now, - Points: []ocmetricdata.Point{ - {Value: int64(123), Time: now}, - }, - }, - }, - }, - }, - expectErr: true, - }, - { - desc: "success", - input: []*ocmetricdata.Metric{ - { - Resource: &ocresource.Resource{ - Labels: map[string]string{ - "R1": "V1", - "R2": "V2", - }, - }, - TimeSeries: []*ocmetricdata.TimeSeries{ - { - StartTime: now, - Points: []ocmetricdata.Point{ - {Value: int64(123), Time: now}, - }, - }, - }, - }, - }, - inputResource: resource.NewSchemaless( - attribute.String("R1", "V1"), - attribute.String("R2", "V2"), - ), - expected: &metricdata.ResourceMetrics{ - Resource: resource.NewSchemaless( - attribute.String("R1", "V1"), - attribute.String("R2", "V2"), - ), - ScopeMetrics: []metricdata.ScopeMetrics{ - { - Scope: instrumentation.Scope{ - Name: scopeName, - }, - Metrics: []metricdata.Metrics{ - { - Name: "", - Description: "", - Unit: "", - Data: metricdata.Gauge[int64]{ - DataPoints: []metricdata.DataPoint[int64]{ - { - Attributes: attribute.NewSet(), - StartTime: now, - Time: now, - Value: 123, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - fake := &fakeExporter{err: tc.exportErr} - exporter := NewMetricExporter(fake, tc.inputResource) - err := exporter.ExportMetrics(context.Background(), tc.input) - if tc.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - if tc.expected != nil { - require.NotNil(t, fake.data) - metricdatatest.AssertEqual(t, *tc.expected, *fake.data) - } else { - require.Nil(t, fake.data) - } - }) - } -} - -type fakeExporter struct { - metric.Exporter - data *metricdata.ResourceMetrics - err error -} - -func (f *fakeExporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) error { - if f.err == nil { - f.data = data - } - return f.err -} diff --git a/exporters/stdout/stdoutmetric/config.go b/exporters/stdout/stdoutmetric/config.go index 6189c019f37..cac5afeeb67 100644 --- a/exporters/stdout/stdoutmetric/config.go +++ b/exporters/stdout/stdoutmetric/config.go @@ -15,6 +15,7 @@ package stdoutmetric // import "go.opentelemetry.io/otel/exporters/stdout/stdout import ( "encoding/json" + "io" "os" "go.opentelemetry.io/otel/sdk/metric" @@ -22,6 +23,7 @@ import ( // config contains options for the exporter. type config struct { + prettyPrint bool encoder *encoderHolder temporalitySelector metric.TemporalitySelector aggregationSelector metric.AggregationSelector @@ -37,10 +39,15 @@ func newConfig(options ...Option) config { if cfg.encoder == nil { enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", "\t") cfg.encoder = &encoderHolder{encoder: enc} } + if cfg.prettyPrint { + if e, ok := cfg.encoder.encoder.(*json.Encoder); ok { + e.SetIndent("", "\t") + } + } + if cfg.temporalitySelector == nil { cfg.temporalitySelector = metric.DefaultTemporalitySelector } @@ -74,6 +81,22 @@ func WithEncoder(encoder Encoder) Option { }) } +// WithWriter sets the export stream destination. +// Using this option overrides any previously set encoder. +func WithWriter(w io.Writer) Option { + return WithEncoder(json.NewEncoder(w)) +} + +// WithPrettyPrint prettifies the emitted output. +// This option only works if the encoder is a *json.Encoder, as is the case +// when using `WithWriter`. +func WithPrettyPrint() Option { + return optionFunc(func(c config) config { + c.prettyPrint = true + return c + }) +} + // WithTemporalitySelector sets the TemporalitySelector the exporter will use // to determine the Temporality of an instrument based on its kind. If this // option is not used, the exporter will use the DefaultTemporalitySelector diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 71679d623a1..2dbfe6357a2 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -15,6 +15,7 @@ package stdoutmetric_test // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" import ( + "bytes" "context" "encoding/json" "io" @@ -103,6 +104,43 @@ func deltaSelector(metric.InstrumentKind) metricdata.Temporality { return metricdata.DeltaTemporality } +func TestExportWithOptions(t *testing.T) { + var ( + data = new(metricdata.ResourceMetrics) + ctx = context.Background() + ) + + for _, tt := range []struct { + name string + opts []stdoutmetric.Option + + expectedData string + }{ + { + name: "with no options", + expectedData: "{\"Resource\":null,\"ScopeMetrics\":null}\n", + }, + { + name: "with pretty print", + opts: []stdoutmetric.Option{ + stdoutmetric.WithPrettyPrint(), + }, + expectedData: "{\n\t\"Resource\": null,\n\t\"ScopeMetrics\": null\n}\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + var b bytes.Buffer + opts := append(tt.opts, stdoutmetric.WithWriter(&b)) + + exp, err := stdoutmetric.New(opts...) + require.NoError(t, err) + require.NoError(t, exp.Export(ctx, data)) + + assert.Equal(t, tt.expectedData, b.String()) + }) + } +} + func TestTemporalitySelector(t *testing.T) { exp, err := stdoutmetric.New( testEncoderOption(),