diff --git a/CHANGELOG.md b/CHANGELOG.md index dfcc289de16..3f0a89237f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108) +- `NewSDK` in `go.opentelemetry.io/contrib/config` now returns a configured SDK with a valid `MeterProvider`. (#4804) - The `go.opentelemetry.io/contrib/bridges/otellogr` module. This module provides an OpenTelemetry logging bridge for "github.com/go-logr/logr". (#5357) diff --git a/config/config.go b/config/config.go index 9002809fbe3..15f471bc6ed 100644 --- a/config/config.go +++ b/config/config.go @@ -67,7 +67,11 @@ func NewSDK(opts ...ConfigurationOption) (SDK, error) { return SDK{}, err } - mp, mpShutdown := initMeterProvider(o) + mp, mpShutdown, err := meterProvider(o, r) + if err != nil { + return SDK{}, err + } + tp, tpShutdown, err := tracerProvider(o, r) if err != nil { return SDK{}, err diff --git a/config/go.mod b/config/go.mod index 38d3354f033..0b3fcd3837f 100644 --- a/config/go.mod +++ b/config/go.mod @@ -3,10 +3,15 @@ module go.opentelemetry.io/contrib/config go 1.21 require ( + github.com/prometheus/client_golang v1.19.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 + go.opentelemetry.io/otel/exporters/prometheus v0.47.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0 go.opentelemetry.io/otel/metric v1.25.0 go.opentelemetry.io/otel/sdk v1.25.0 @@ -15,12 +20,17 @@ require ( ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/net v0.24.0 // indirect diff --git a/config/go.sum b/config/go.sum index 66922d26c47..9c9181372e0 100644 --- a/config/go.sum +++ b/config/go.sum @@ -1,5 +1,9 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -17,18 +21,34 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0 h1:hDKnobznDpcdTlNzO0S/owRB8tyVr1OoeZZhDoqY+Cs= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0/go.mod h1:kUDQaUs1h8iTIHbQTk+iJRiUvSfJYMMKTtMCaiVu7B0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0 h1:Wc4hZuYXhVqq+TfRXLXlmNIL/awOanGx8ssq3ciDQxc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0/go.mod h1:BydOvapRqVEc0DVz27qWBX2jq45Ca5TI9mhZBDIdweY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 h1:vOL89uRfOCCNIjkisd0r7SEdJF3ZJFyCNY34fdZs8eU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0 h1:OL6yk1Z/pEGdDnrBbxSsH+t4FY1zXfBRGd7bjwhlMLU= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0/go.mod h1:xF3N4OSICZDVbbYZydz9MHFro1RjmkPUKEvar2utG+Q= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0 h1:d7nHbdzU84STOiszaOxQ3kw5IwkSmHsU5Muol5/vL4I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0/go.mod h1:yiPA1iZbb/EHYnODXOxvtKuB0I2hV8ehfLTEWpl7BJU= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0 h1:0vZZdECYzhTt9MKQZ5qQ0V+J3MFu4MQaQ3COfugF+FQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0/go.mod h1:e7iXx3HjaSSBXfy9ykVUlupS2Vp7LBIBuT21ousM2Hk= go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= diff --git a/config/metric.go b/config/metric.go index 870d8a8c5c9..0b7543d3dbc 100644 --- a/config/metric.go +++ b/config/metric.go @@ -4,15 +4,257 @@ package config // import "go.opentelemetry.io/contrib/config" import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + otelprom "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" ) -func initMeterProvider(cfg configOptions) (metric.MeterProvider, shutdownFunc) { +func meterProvider(cfg configOptions, res *resource.Resource) (metric.MeterProvider, shutdownFunc, error) { if cfg.opentelemetryConfig.MeterProvider == nil { - return noop.NewMeterProvider(), noopShutdown + return noop.NewMeterProvider(), noopShutdown, nil + } + opts := []sdkmetric.Option{ + sdkmetric.WithResource(res), + } + + var errs []error + for _, reader := range cfg.opentelemetryConfig.MeterProvider.Readers { + r, err := metricReader(cfg.ctx, reader) + if err == nil { + opts = append(opts, sdkmetric.WithReader(r)) + } else { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return noop.NewMeterProvider(), noopShutdown, errors.Join(errs...) + } + + mp := sdkmetric.NewMeterProvider(opts...) + return mp, mp.Shutdown, nil +} + +func metricReader(ctx context.Context, r MetricReader) (sdkmetric.Reader, error) { + if r.Periodic != nil && r.Pull != nil { + return nil, errors.New("must not specify multiple metric reader type") + } + + if r.Periodic != nil { + return periodicExporter(ctx, r.Periodic.Exporter) + } + + if r.Pull != nil { + return pullReader(ctx, r.Pull.Exporter) + } + return nil, errors.New("no valid metric reader") +} + +func pullReader(ctx context.Context, exporter MetricExporter) (sdkmetric.Reader, error) { + if exporter.Prometheus != nil { + return prometheusReader(ctx, exporter.Prometheus) + } + return nil, errors.New("no valid metric exporter") +} + +func periodicExporter(ctx context.Context, exporter MetricExporter, opts ...sdkmetric.PeriodicReaderOption) (sdkmetric.Reader, error) { + if exporter.Console != nil && exporter.OTLP != nil { + return nil, errors.New("must not specify multiple exporters") + } + if exporter.Console != nil { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + + exp, err := stdoutmetric.New( + stdoutmetric.WithEncoder(enc), + ) + if err != nil { + return nil, err + } + return sdkmetric.NewPeriodicReader(exp, opts...), nil + } + if exporter.OTLP != nil { + var err error + var exp sdkmetric.Exporter + switch exporter.OTLP.Protocol { + case protocolProtobufHTTP: + exp, err = otlpHTTPMetricExporter(ctx, exporter.OTLP) + case protocolProtobufGRPC: + exp, err = otlpGRPCMetricExporter(ctx, exporter.OTLP) + default: + return nil, fmt.Errorf("unsupported protocol %q", exporter.OTLP.Protocol) + } + if err != nil { + return nil, err + } + return sdkmetric.NewPeriodicReader(exp, opts...), nil + } + return nil, errors.New("no valid metric exporter") +} + +func otlpHTTPMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmetric.Exporter, error) { + opts := []otlpmetrichttp.Option{} + + if len(otlpConfig.Endpoint) > 0 { + u, err := url.ParseRequestURI(otlpConfig.Endpoint) + if err != nil { + return nil, err + } + opts = append(opts, otlpmetrichttp.WithEndpoint(u.Host)) + + if u.Scheme == "http" { + opts = append(opts, otlpmetrichttp.WithInsecure()) + } + if len(u.Path) > 0 { + opts = append(opts, otlpmetrichttp.WithURLPath(u.Path)) + } + } + if otlpConfig.Compression != nil { + switch *otlpConfig.Compression { + case compressionGzip: + opts = append(opts, otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression)) + case compressionNone: + opts = append(opts, otlpmetrichttp.WithCompression(otlpmetrichttp.NoCompression)) + default: + return nil, fmt.Errorf("unsupported compression %q", *otlpConfig.Compression) + } + } + if otlpConfig.Timeout != nil { + opts = append(opts, otlpmetrichttp.WithTimeout(time.Millisecond*time.Duration(*otlpConfig.Timeout))) } - mp := sdkmetric.NewMeterProvider() - return mp, mp.Shutdown + if len(otlpConfig.Headers) > 0 { + opts = append(opts, otlpmetrichttp.WithHeaders(otlpConfig.Headers)) + } + + return otlpmetrichttp.New(ctx, opts...) +} + +func otlpGRPCMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmetric.Exporter, error) { + opts := []otlpmetricgrpc.Option{} + + if len(otlpConfig.Endpoint) > 0 { + u, err := url.ParseRequestURI(otlpConfig.Endpoint) + if err != nil { + return nil, err + } + // ParseRequestURI leaves the Host field empty when no + // scheme is specified (i.e. localhost:4317). This check is + // here to support the case where a user may not specify a + // scheme. The code does its best effort here by using + // otlpConfig.Endpoint as-is in that case + if u.Host != "" { + opts = append(opts, otlpmetricgrpc.WithEndpoint(u.Host)) + } else { + opts = append(opts, otlpmetricgrpc.WithEndpoint(otlpConfig.Endpoint)) + } + if u.Scheme == "http" { + opts = append(opts, otlpmetricgrpc.WithInsecure()) + } + } + + if otlpConfig.Compression != nil { + switch *otlpConfig.Compression { + case compressionGzip: + opts = append(opts, otlpmetricgrpc.WithCompressor(*otlpConfig.Compression)) + case compressionNone: + // none requires no options + default: + return nil, fmt.Errorf("unsupported compression %q", *otlpConfig.Compression) + } + } + if otlpConfig.Timeout != nil && *otlpConfig.Timeout > 0 { + opts = append(opts, otlpmetricgrpc.WithTimeout(time.Millisecond*time.Duration(*otlpConfig.Timeout))) + } + if len(otlpConfig.Headers) > 0 { + opts = append(opts, otlpmetricgrpc.WithHeaders(otlpConfig.Headers)) + } + + return otlpmetricgrpc.New(ctx, opts...) +} + +func prometheusReader(ctx context.Context, prometheusConfig *Prometheus) (sdkmetric.Reader, error) { + var opts []otelprom.Option + if prometheusConfig.Host == nil { + return nil, fmt.Errorf("host must be specified") + } + if prometheusConfig.Port == nil { + return nil, fmt.Errorf("port must be specified") + } + if prometheusConfig.WithoutScopeInfo != nil && *prometheusConfig.WithoutScopeInfo { + opts = append(opts, otelprom.WithoutScopeInfo()) + } + if prometheusConfig.WithoutTypeSuffix != nil && *prometheusConfig.WithoutTypeSuffix { + opts = append(opts, otelprom.WithoutCounterSuffixes()) + } + if prometheusConfig.WithoutUnits != nil && *prometheusConfig.WithoutUnits { + opts = append(opts, otelprom.WithoutUnits()) + } + + reg := prometheus.NewRegistry() + opts = append(opts, otelprom.WithRegisterer(reg)) + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})) + server := http.Server{ + // Timeouts are necessary to make a server resilent to attacks, but ListenAndServe doesn't set any. + // We use values from this example: https://blog.cloudflare.com/exposing-go-on-the-internet/#:~:text=There%20are%20three%20main%20timeouts + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + Handler: mux, + } + addr := fmt.Sprintf("%s:%d", *prometheusConfig.Host, *prometheusConfig.Port) + + // TODO: add support for constant label filter + // otelprom.WithResourceAsConstantLabels(attribute.NewDenyKeysFilter()), + // ) + reader, err := otelprom.New(opts...) + if err != nil { + return nil, fmt.Errorf("error creating otel prometheus exporter: %w", err) + } + lis, err := net.Listen("tcp", addr) + if err != nil { + return nil, errors.Join( + fmt.Errorf("binding address %s for Prometheus exporter: %w", addr, err), + reader.Shutdown(ctx), + ) + } + + go func() { + if err := server.Serve(lis); err != nil && err != http.ErrServerClosed { + otel.Handle(fmt.Errorf("the Prometheus HTTP server exited unexpectedly: %w", err)) + } + }() + + return readerWithServer{reader, &server}, nil +} + +type readerWithServer struct { + sdkmetric.Reader + server *http.Server +} + +func (rws readerWithServer) Shutdown(ctx context.Context) error { + return errors.Join( + rws.Reader.Shutdown(ctx), + rws.server.Shutdown(ctx), + ) } diff --git a/config/metric_test.go b/config/metric_test.go index dd5dfdd74f6..f8208b080f0 100644 --- a/config/metric_test.go +++ b/config/metric_test.go @@ -4,15 +4,26 @@ package config import ( + "context" + "errors" + "net/url" + "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + otelprom "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" ) -func TestInitMeterProvider(t *testing.T) { +func TestMeterProvider(t *testing.T) { tests := []struct { name string cfg configOptions @@ -23,10 +34,445 @@ func TestInitMeterProvider(t *testing.T) { name: "no-meter-provider-configured", wantProvider: noop.NewMeterProvider(), }, + { + name: "error-in-config", + cfg: configOptions{ + opentelemetryConfig: OpenTelemetryConfiguration{ + MeterProvider: &MeterProvider{ + Readers: []MetricReader{ + { + Periodic: &PeriodicMetricReader{}, + Pull: &PullMetricReader{}, + }, + }, + }, + }, + }, + wantProvider: noop.NewMeterProvider(), + wantErr: errors.Join(errors.New("must not specify multiple metric reader type")), + }, + { + name: "multiple-errors-in-config", + cfg: configOptions{ + opentelemetryConfig: OpenTelemetryConfiguration{ + MeterProvider: &MeterProvider{ + Readers: []MetricReader{ + { + Periodic: &PeriodicMetricReader{}, + Pull: &PullMetricReader{}, + }, + { + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + Console: Console{}, + OTLP: &OTLPMetric{}, + }, + }, + }, + }, + }, + }, + }, + wantProvider: noop.NewMeterProvider(), + wantErr: errors.Join(errors.New("must not specify multiple metric reader type"), errors.New("must not specify multiple exporters")), + }, } for _, tt := range tests { - mp, err := initMeterProvider(tt.cfg) + mp, shutdown, err := meterProvider(tt.cfg, resource.Default()) require.Equal(t, tt.wantProvider, mp) - require.NoError(t, tt.wantErr, err) + assert.Equal(t, tt.wantErr, err) + require.NoError(t, shutdown(context.Background())) + } +} + +func TestReader(t *testing.T) { + consoleExporter, err := stdoutmetric.New( + stdoutmetric.WithPrettyPrint(), + ) + require.NoError(t, err) + ctx := context.Background() + otlpGRPCExporter, err := otlpmetricgrpc.New(ctx) + require.NoError(t, err) + otlpHTTPExporter, err := otlpmetrichttp.New(ctx) + require.NoError(t, err) + promExporter, err := otelprom.New() + require.NoError(t, err) + testCases := []struct { + name string + reader MetricReader + args any + wantErr error + wantReader sdkmetric.Reader + }{ + { + name: "no reader", + wantErr: errors.New("no valid metric reader"), + }, + { + name: "pull/no-exporter", + reader: MetricReader{ + Pull: &PullMetricReader{}, + }, + wantErr: errors.New("no valid metric exporter"), + }, + { + name: "pull/prometheus-no-host", + reader: MetricReader{ + Pull: &PullMetricReader{ + Exporter: MetricExporter{ + Prometheus: &Prometheus{}, + }, + }, + }, + wantErr: errors.New("host must be specified"), + }, + { + name: "pull/prometheus-no-port", + reader: MetricReader{ + Pull: &PullMetricReader{ + Exporter: MetricExporter{ + Prometheus: &Prometheus{ + Host: ptr("localhost"), + }, + }, + }, + }, + wantErr: errors.New("port must be specified"), + }, + { + name: "pull/prometheus", + reader: MetricReader{ + Pull: &PullMetricReader{ + Exporter: MetricExporter{ + Prometheus: &Prometheus{ + Host: ptr("localhost"), + Port: ptr(8888), + }, + }, + }, + }, + wantReader: readerWithServer{promExporter, nil}, + }, + { + name: "periodic/otlp-exporter-invalid-protocol", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/invalid", + }, + }, + }, + }, + wantErr: errors.New("unsupported protocol \"http/invalid\""), + }, + { + name: "periodic/otlp-grpc-exporter", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: "http://localhost:4318", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-exporter-with-path", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: "http://localhost:4318/path/123", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-exporter-no-endpoint", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-exporter-no-scheme", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-invalid-endpoint", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: " ", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantErr: &url.Error{Op: "parse", URL: " ", Err: errors.New("invalid URI for request")}, + }, + { + name: "periodic/grpc-http-none-compression", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("none"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-invalid-compression", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "grpc/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("invalid"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantErr: errors.New("unsupported compression \"invalid\""), + }, + { + name: "periodic/otlp-http-exporter", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: "http://localhost:4318", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-exporter-with-path", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: "http://localhost:4318/path/123", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-exporter-no-endpoint", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-exporter-no-scheme", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-invalid-endpoint", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: " ", + Compression: ptr("gzip"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantErr: &url.Error{Op: "parse", URL: " ", Err: errors.New("invalid URI for request")}, + }, + { + name: "periodic/otlp-http-none-compression", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("none"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-invalid-compression", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + OTLP: &OTLPMetric{ + Protocol: "http/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("invalid"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantErr: errors.New("unsupported compression \"invalid\""), + }, + { + name: "periodic/no-exporter", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{}, + }, + }, + wantErr: errors.New("no valid metric exporter"), + }, + { + name: "periodic/console-exporter", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: MetricExporter{ + Console: Console{}, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(consoleExporter), + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, err := metricReader(context.Background(), tt.reader) + require.Equal(t, tt.wantErr, err) + if tt.wantReader == nil { + require.Nil(t, got) + } else { + require.Equal(t, reflect.TypeOf(tt.wantReader), reflect.TypeOf(got)) + var fieldName string + switch reflect.TypeOf(tt.wantReader).String() { + case "*metric.PeriodicReader": + fieldName = "exporter" + case "config.readerWithServer": + fieldName = "Reader" + default: + fieldName = "e" + } + wantExporterType := reflect.Indirect(reflect.ValueOf(tt.wantReader)).FieldByName(fieldName).Elem().Type() + gotExporterType := reflect.Indirect(reflect.ValueOf(got)).FieldByName(fieldName).Elem().Type() + require.Equal(t, wantExporterType.String(), gotExporterType.String()) + require.NoError(t, got.Shutdown(context.Background())) + } + }) } } diff --git a/config/trace_test.go b/config/trace_test.go index 2f081cb704f..de1461342ae 100644 --- a/config/trace_test.go +++ b/config/trace_test.go @@ -209,7 +209,7 @@ func TestSpanProcessor(t *testing.T) { ScheduleDelay: ptr(0), Exporter: SpanExporter{ OTLP: &OTLP{ - Protocol: *ptr("http/invalid"), + Protocol: "http/invalid", }, }, }, @@ -444,6 +444,29 @@ func TestSpanProcessor(t *testing.T) { }, wantErr: &url.Error{Op: "parse", URL: " ", Err: errors.New("invalid URI for request")}, }, + { + name: "batch/otlp-http-none-compression", + processor: SpanProcessor{ + Batch: &BatchSpanProcessor{ + MaxExportBatchSize: ptr(0), + ExportTimeout: ptr(0), + MaxQueueSize: ptr(0), + ScheduleDelay: ptr(0), + Exporter: SpanExporter{ + OTLP: &OTLP{ + Protocol: "http/protobuf", + Endpoint: "localhost:4318", + Compression: ptr("none"), + Timeout: ptr(1000), + Headers: map[string]string{ + "test": "test1", + }, + }, + }, + }, + }, + wantProcessor: sdktrace.NewBatchSpanProcessor(otlpHTTPExporter), + }, { name: "batch/otlp-http-invalid-compression", processor: SpanProcessor{