Skip to content

Commit

Permalink
Add OTLP exporter constructors for convenience (goadesign#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphael authored Jan 10, 2024
1 parent 033644b commit 605ad53
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 37 deletions.
30 changes: 13 additions & 17 deletions clue/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@ import (
)

type (
// Config is used to initialize metrics and tracing.
// Config is used to configure OpenTelemetry.
Config struct {
// MeterProvider is the OpenTelemetry meter provider used by the clue
// metrics package.
// MeterProvider is the OpenTelemetry meter provider used by clue
MeterProvider metric.MeterProvider
// TracerProvider is the OpenTelemetry tracer provider used by the clue
// trace package.
// TracerProvider is the OpenTelemetry tracer provider used clue
TracerProvider trace.TracerProvider
// Propagators is the OpenTelemetry propagator used by the clue trace
// package.
// Propagators is the OpenTelemetry propagator used by clue
Propagators propagation.TextMapPropagator
// ErrorHandler is the error handler used by the OpenTelemetry
// package.
// ErrorHandler is the error handler used by OpenTelemetry
ErrorHandler otel.ErrorHandler
}
)
Expand All @@ -47,9 +43,9 @@ func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) {
}

// NewConfig creates a new Config object adequate for use by
// ConfigureOpenTelemetry. The metricsExporter and spanExporter are used to
// ConfigureOpenTelemetry. The metricExporter and spanExporter are used to
// record telemetry. If either is nil then the corresponding package will not
// record any telemetry. The OpenTelemetry metrics provider is configured with a
// record any telemetry. The OpenTelemetry metric provider is configured with a
// periodic reader. The OpenTelemetry tracer provider is configured to use a
// batch span processor and an adaptive sampler that aims at a maximum sampling
// rate of requests per second. The resulting configuration can be modified
Expand All @@ -58,20 +54,20 @@ func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) {
//
// Example:
//
// metricsExporter, err := stdoutmetric.New()
// metricExporter, err := stdoutmetric.New()
// if err != nil {
// return err
// }
// spanExporter, err := stdouttrace.New()
// if err != nil {
// return err
// }
// cfg := clue.NewConfig("mysvc", "1.0.0", metricsExporter, spanExporter)
// cfg := clue.NewConfig("mysvc", "1.0.0", metricExporter, spanExporter)
func NewConfig(
ctx context.Context,
svcName string,
svcVersion string,
metricsExporter sdkmetric.Exporter,
metricExporter sdkmetric.Exporter,
spanExporter sdktrace.SpanExporter,
opts ...Option,
) (*Config, error) {
Expand All @@ -90,15 +86,15 @@ func NewConfig(
return nil, err
}
var meterProvider metric.MeterProvider
if metricsExporter == nil {
if metricExporter == nil {
meterProvider = metricnoop.NewMeterProvider()
} else {
var reader sdkmetric.Reader
if options.readerInterval == 0 {
reader = sdkmetric.NewPeriodicReader(metricsExporter)
reader = sdkmetric.NewPeriodicReader(metricExporter)
} else {
reader = sdkmetric.NewPeriodicReader(
metricsExporter,
metricExporter,
sdkmetric.WithInterval(options.readerInterval),
)
}
Expand Down
90 changes: 90 additions & 0 deletions clue/exporters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package clue

import (
"context"

"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"

"goa.design/clue/log"
)

// Allow mocking
var (
otlpmetricgrpcNew = otlpmetricgrpc.New
otlpmetrichttpNew = otlpmetrichttp.New
otlptracegrpcNew = otlptracegrpc.New
otlptracehttpNew = otlptracehttp.New
)

// NewGRPCMetricExporter returns an OpenTelementry Protocol metric exporter that
// report metrics to a gRPC collector.
func NewGRPCMetricExporter(ctx context.Context, options ...otlpmetricgrpc.Option) (exporter metric.Exporter, shutdown func(), err error) {
exporter, err = otlpmetricgrpcNew(ctx, options...)
if err != nil {
return
}
shutdown = func() {
// Create new context in case the parent context has been canceled.
ctx := log.WithContext(context.Background(), ctx)
if err := exporter.Shutdown(ctx); err != nil {
log.Errorf(ctx, err, "failed to shutdown metric exporter")
}
}
return
}

// NewGRPCSpanExporter returns an OpenTelementry Protocol span exporter that
// report spans to a gRPC collector.
func NewGRPCSpanExporter(ctx context.Context, options ...otlptracegrpc.Option) (exporter trace.SpanExporter, shutdown func(), err error) {
exporter, err = otlptracegrpcNew(ctx, options...)
if err != nil {
return
}
shutdown = func() {
// Create new context in case the parent context has been canceled.
ctx := log.WithContext(context.Background(), ctx)
if err := exporter.Shutdown(ctx); err != nil {
log.Errorf(ctx, err, "failed to shutdown span exporter")
}
}
return
}

// NewHTTPMetricExporter returns an OpenTelementry Protocol metric exporter that
// report metrics to a HTTP collector.
func NewHTTPMetricExporter(ctx context.Context, options ...otlpmetrichttp.Option) (exporter metric.Exporter, shutdown func(), err error) {
exporter, err = otlpmetrichttpNew(ctx, options...)
if err != nil {
return
}
shutdown = func() {
// Create new context in case the parent context has been canceled.
ctx := log.WithContext(context.Background(), ctx)
if err := exporter.Shutdown(ctx); err != nil {
log.Errorf(ctx, err, "failed to shutdown metric exporter")
}
}
return
}

// NewHTTPSpanExporter returns an OpenTelementry Protocol span exporter that
// report spans to a HTTP collector.
func NewHTTPSpanExporter(ctx context.Context, options ...otlptracehttp.Option) (exporter trace.SpanExporter, shutdown func(), err error) {
exporter, err = otlptracehttpNew(ctx, options...)
if err != nil {
return
}
shutdown = func() {
// Create new context in case the parent context has been canceled.
ctx := log.WithContext(context.Background(), ctx)
if err := exporter.Shutdown(ctx); err != nil {
log.Errorf(ctx, err, "failed to shutdown span exporter")
}
}
return
}
224 changes: 224 additions & 0 deletions clue/exporters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package clue

import (
"bytes"
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"goa.design/clue/log"
)

func TestNewGRPCMetricExporter(t *testing.T) {
testErr := errors.New("test error")
// Define test cases
tests := []struct {
name string
options []otlpmetricgrpc.Option
newErr error
wantLog string
wantErr bool
}{
{
name: "Success",
},
{
name: "Options",
options: []otlpmetricgrpc.Option{otlpmetricgrpc.WithInsecure(), otlpmetricgrpc.WithEndpoint("test")},
},
{
name: "New Error",
newErr: testErr,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
otlpmetricgrpcNew = func(ctx context.Context, options ...otlpmetricgrpc.Option) (*otlpmetricgrpc.Exporter, error) {
if tt.newErr != nil {
return nil, tt.newErr
}
return otlpmetricgrpc.New(ctx)
}
var buf bytes.Buffer
ctx := log.Context(context.Background(), log.WithOutput(&buf))

exporter, shutdown, err := NewGRPCMetricExporter(ctx, tt.options...)

if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, exporter)
assert.Nil(t, shutdown)
return
}
assert.NoError(t, err)
assert.NotNil(t, exporter)
assert.NotNil(t, shutdown)
shutdown()
assert.Empty(t, buf.String())
})
}
}

func TestNewHTTPMetricExporter(t *testing.T) {
testErr := errors.New("test error")
// Define test cases
tests := []struct {
name string
options []otlpmetrichttp.Option
newErr error
wantLog string
wantErr bool
}{
{
name: "Success",
},
{
name: "Options",
options: []otlpmetrichttp.Option{otlpmetrichttp.WithEndpoint("test")},
},
{
name: "New Error",
newErr: testErr,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
otlpmetrichttpNew = func(ctx context.Context, options ...otlpmetrichttp.Option) (*otlpmetrichttp.Exporter, error) {
if tt.newErr != nil {
return nil, tt.newErr
}
return otlpmetrichttp.New(ctx)
}
var buf bytes.Buffer
ctx := log.Context(context.Background(), log.WithOutput(&buf))

exporter, shutdown, err := NewHTTPMetricExporter(ctx, tt.options...)

if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, exporter)
assert.Nil(t, shutdown)
return
}
assert.NoError(t, err)
assert.NotNil(t, exporter)
assert.NotNil(t, shutdown)
shutdown()
assert.Empty(t, buf.String())
})
}
}

func TestNewGRPCSpanExporter(t *testing.T) {
testErr := errors.New("test error")
// Define test cases
tests := []struct {
name string
options []otlptracegrpc.Option
newErr error
wantLog string
wantErr bool
}{
{
name: "Success",
},
{
name: "Options",
options: []otlptracegrpc.Option{otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint("test")},
},
{
name: "New Error",
newErr: testErr,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
otlptracegrpcNew = func(ctx context.Context, options ...otlptracegrpc.Option) (*otlptrace.Exporter, error) {
if tt.newErr != nil {
return nil, tt.newErr
}
return otlptracegrpc.New(ctx)
}
var buf bytes.Buffer
ctx := log.Context(context.Background(), log.WithOutput(&buf))

exporter, shutdown, err := NewGRPCSpanExporter(ctx, tt.options...)

if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, exporter)
assert.Nil(t, shutdown)
return
}
assert.NoError(t, err)
assert.NotNil(t, exporter)
assert.NotNil(t, shutdown)
shutdown()
assert.Empty(t, buf.String())
})
}
}

func TestNewHTTPSpanExporter(t *testing.T) {
testErr := errors.New("test error")
// Define test cases
tests := []struct {
name string
options []otlptracehttp.Option
newErr error
wantLog string
wantErr bool
}{
{
name: "Success",
},
{
name: "Options",
options: []otlptracehttp.Option{otlptracehttp.WithEndpoint("test")},
},
{
name: "New Error",
newErr: testErr,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
otlptracehttpNew = func(ctx context.Context, options ...otlptracehttp.Option) (*otlptrace.Exporter, error) {
if tt.newErr != nil {
return nil, tt.newErr
}
return otlptracehttp.New(ctx)
}
var buf bytes.Buffer
ctx := log.Context(context.Background(), log.WithOutput(&buf))

exporter, shutdown, err := NewHTTPSpanExporter(ctx, tt.options...)

if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, exporter)
assert.Nil(t, shutdown)
return
}
assert.NoError(t, err)
assert.NotNil(t, exporter)
assert.NotNil(t, shutdown)
shutdown()
assert.Empty(t, buf.String())
})
}
}
Loading

0 comments on commit 605ad53

Please sign in to comment.