From b46d70062c6ac7c7fee4ddbc37f6796ab03471cc Mon Sep 17 00:00:00 2001 From: Goutham Veeramachaneni Date: Mon, 6 May 2024 17:36:52 +0200 Subject: [PATCH] autoexport: Add OTEL_METRICS_PRODUCERS environment variable support (#5281) --- CHANGELOG.md | 3 + exporters/autoexport/go.mod | 7 + exporters/autoexport/go.sum | 48 +++++++ exporters/autoexport/metrics.go | 129 +++++++++++++++++- exporters/autoexport/metrics_test.go | 182 ++++++++++++++++++++++++++ exporters/autoexport/noop.go | 11 ++ exporters/autoexport/registry.go | 10 +- exporters/autoexport/registry_test.go | 2 +- 8 files changed, 380 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e87e5e6fb..0789460f4bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - `NewSDK` in `go.opentelemetry.io/contrib/config` now returns a configured SDK with a valid `MeterProvider`. (#4804) +- Add an experimental `OTEL_METRICS_PRODUCERS` environment variable to `go.opentelemetry.io/contrib/autoexport` to be set metrics producers. (#5281) + - `prometheus` and `none` are supported values. You can specify multiple producers separated by a comma. + - Add `WithFallbackMetricProducer` option that adds a fallback if the `OTEL_METRICS_PRODUCERS` is not set or empty. ### Changed diff --git a/exporters/autoexport/go.mod b/exporters/autoexport/go.mod index a8aa8cb5d44..57abe501119 100644 --- a/exporters/autoexport/go.mod +++ b/exporters/autoexport/go.mod @@ -5,6 +5,8 @@ go 1.21 require ( github.com/prometheus/client_golang v1.19.0 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/pdata v1.5.0 + go.opentelemetry.io/contrib/bridges/prometheus v0.50.0 go.opentelemetry.io/otel v1.26.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0 @@ -26,7 +28,11 @@ require ( 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/gogo/protobuf v1.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // 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 @@ -34,6 +40,7 @@ require ( go.opentelemetry.io/otel/metric v1.26.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/exporters/autoexport/go.sum b/exporters/autoexport/go.sum index 72234e0694c..db90ea05020 100644 --- a/exporters/autoexport/go.sum +++ b/exporters/autoexport/go.sum @@ -4,6 +4,7 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -11,14 +12,26 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= @@ -31,8 +44,16 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector/pdata v1.5.0 h1:1fKTmUpr0xCOhP/B0VEvtz7bYPQ45luQ8XFyA07j8LE= +go.opentelemetry.io/collector/pdata v1.5.0/go.mod h1:TYj8aKRWZyT/KuKQXKyqSEvK/GV+slFaDMEI+Ke64Yw= +go.opentelemetry.io/contrib/bridges/prometheus v0.50.0 h1:akXN45Sg2oS2NOb2xBL0LKeq/oSyEIvc8CC/7XLaB+4= +go.opentelemetry.io/contrib/bridges/prometheus v0.50.0/go.mod h1:uoFuIBjQ9kWtUv4KbRNq0ExS9BQoWxHrr63JWX/EMb8= go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0 h1:+hm+I+KigBy3M24/h1p/NHkUx/evbLH0PNcjpMyCHc4= @@ -63,12 +84,39 @@ go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IO go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= diff --git a/exporters/autoexport/metrics.go b/exporters/autoexport/metrics.go index ddcea0cbc13..0dcb6c0cc53 100644 --- a/exporters/autoexport/metrics.go +++ b/exporters/autoexport/metrics.go @@ -10,11 +10,13 @@ import ( "net" "net/http" "os" + "strings" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + prometheusbridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" @@ -52,9 +54,13 @@ func WithFallbackMetricReader(metricReaderFactory func(ctx context.Context) (met // OTEL_EXPORTER_PROMETHEUS_PORT (defaulting to 9464) define the host and port for the // Prometheus exporter's HTTP server. // +// Experimental: OTEL_METRICS_PRODUCERS can be used to configure metric producers. +// supported values: prometheus, none. Multiple values can be specified separated by commas. +// // An error is returned if an environment value is set to an unhandled value. // // Use [RegisterMetricReader] to handle more values of OTEL_METRICS_EXPORTER. +// Use [RegisterMetricProducer] to handle more values of OTEL_METRICS_PRODUCERS. // // Use [WithFallbackMetricReader] option to change the returned exporter // when OTEL_METRICS_EXPORTER is unset or empty. @@ -71,10 +77,35 @@ func RegisterMetricReader(name string, factory func(context.Context) (metric.Rea must(metricsSignal.registry.store(name, factory)) } -var metricsSignal = newSignal[metric.Reader]("OTEL_METRICS_EXPORTER") +// RegisterMetricProducer sets the MetricReader factory to be used when the +// OTEL_METRICS_PRODUCERS environment variable contains the producer name. This +// will panic if name has already been registered. +func RegisterMetricProducer(name string, factory func(context.Context) (metric.Producer, error)) { + must(metricsProducers.registry.store(name, factory)) +} + +// WithFallbackMetricProducer sets the fallback producer to use when no producer +// is configured through the OTEL_METRICS_PRODUCERS environment variable. +func WithFallbackMetricProducer(producerFactory func(ctx context.Context) (metric.Producer, error)) { + metricsProducers.fallbackProducer = producerFactory +} + +var ( + metricsSignal = newSignal[metric.Reader]("OTEL_METRICS_EXPORTER") + metricsProducers = newProducerRegistry("OTEL_METRICS_PRODUCERS") +) func init() { RegisterMetricReader("otlp", func(ctx context.Context) (metric.Reader, error) { + producers, err := metricsProducers.create(ctx) + if err != nil { + return nil, err + } + readerOpts := []metric.PeriodicReaderOption{} + for _, producer := range producers { + readerOpts = append(readerOpts, metric.WithProducer(producer)) + } + proto := os.Getenv(otelExporterOTLPProtoEnvKey) if proto == "" { proto = "http/protobuf" @@ -86,33 +117,54 @@ func init() { if err != nil { return nil, err } - return metric.NewPeriodicReader(r), nil + return metric.NewPeriodicReader(r, readerOpts...), nil case "http/protobuf": r, err := otlpmetrichttp.New(ctx) if err != nil { return nil, err } - return metric.NewPeriodicReader(r), nil + return metric.NewPeriodicReader(r, readerOpts...), nil default: return nil, errInvalidOTLPProtocol } }) RegisterMetricReader("console", func(ctx context.Context) (metric.Reader, error) { + producers, err := metricsProducers.create(ctx) + if err != nil { + return nil, err + } + readerOpts := []metric.PeriodicReaderOption{} + for _, producer := range producers { + readerOpts = append(readerOpts, metric.WithProducer(producer)) + } + r, err := stdoutmetric.New() if err != nil { return nil, err } - return metric.NewPeriodicReader(r), nil + return metric.NewPeriodicReader(r, readerOpts...), nil }) RegisterMetricReader("none", func(ctx context.Context) (metric.Reader, error) { return newNoopMetricReader(), nil }) RegisterMetricReader("prometheus", func(ctx context.Context) (metric.Reader, error) { // create an isolated registry instead of using the global registry -- - // the user might not want to mix OTel with non-OTel metrics + // the user might not want to mix OTel with non-OTel metrics. + // Those that want to comingle metrics from global registry can use + // OTEL_METRICS_PRODUCERS=prometheus reg := prometheus.NewRegistry() - reader, err := promexporter.New(promexporter.WithRegisterer(reg)) + exporterOpts := []promexporter.Option{promexporter.WithRegisterer(reg)} + + producers, err := metricsProducers.create(ctx) + if err != nil { + return nil, err + } + for _, producer := range producers { + exporterOpts = append(exporterOpts, promexporter.WithProducer(producer)) + } + + reader, err := promexporter.New(exporterOpts...) if err != nil { return nil, err } @@ -148,6 +200,13 @@ func init() { return readerWithServer{lis.Addr(), reader, &server}, nil }) + + RegisterMetricProducer("prometheus", func(ctx context.Context) (metric.Producer, error) { + return prometheusbridge.NewMetricProducer(), nil + }) + RegisterMetricProducer("none", func(ctx context.Context) (metric.Producer, error) { + return newNoopMetricProducer(), nil + }) } type readerWithServer struct { @@ -170,3 +229,61 @@ func getenv(key, fallback string) string { } return result } + +type producerRegistry struct { + envKey string + fallbackProducer func(context.Context) (metric.Producer, error) + registry *registry[metric.Producer] +} + +func newProducerRegistry(envKey string) producerRegistry { + return producerRegistry{ + envKey: envKey, + registry: ®istry[metric.Producer]{ + names: make(map[string]func(context.Context) (metric.Producer, error)), + }, + } +} + +func (pr producerRegistry) create(ctx context.Context) ([]metric.Producer, error) { + expType := os.Getenv(pr.envKey) + if expType == "" { + if pr.fallbackProducer != nil { + producer, err := pr.fallbackProducer(ctx) + if err != nil { + return nil, err + } + + return []metric.Producer{producer}, nil + } + + return nil, nil + } + + producers := dedupedMetricProducers(expType) + metricProducers := make([]metric.Producer, 0, len(producers)) + for _, producer := range producers { + producer, err := pr.registry.load(ctx, producer) + if err != nil { + return nil, err + } + + metricProducers = append(metricProducers, producer) + } + + return metricProducers, nil +} + +func dedupedMetricProducers(envValue string) []string { + producers := make(map[string]struct{}) + for _, producer := range strings.Split(envValue, ",") { + producers[producer] = struct{}{} + } + + result := make([]string, 0, len(producers)) + for producer := range producers { + result = append(result, producer) + } + + return result +} diff --git a/exporters/autoexport/metrics_test.go b/exporters/autoexport/metrics_test.go index d54dc0994ce..3eb711b8690 100644 --- a/exporters/autoexport/metrics_test.go +++ b/exporters/autoexport/metrics_test.go @@ -8,13 +8,18 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "reflect" "runtime/debug" + "strings" "testing" + "go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp" + prometheusbridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/metric" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "go.uber.org/goleak" ) @@ -119,3 +124,180 @@ func TestMetricExporterPrometheusInvalidPort(t *testing.T) { _, err := NewMetricReader(context.Background()) assert.ErrorContains(t, err, "binding") } + +func TestMetricProducerPrometheusWithOTLPExporter(t *testing.T) { + assertNoOtelHandleErrors(t) + + requestWaitChan := make(chan struct{}) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.NoError(t, r.Body.Close()) + + // Now parse the otlp proto message from request body. + req := pmetricotlp.NewExportRequest() + assert.NoError(t, req.UnmarshalProto(body)) + + // This is 0 without the producer registered. + assert.NotZero(t, req.Metrics().MetricCount()) + close(requestWaitChan) + })) + + t.Setenv("OTEL_METRICS_EXPORTER", "otlp") + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", ts.URL) + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + t.Setenv("OTEL_METRICS_PRODUCERS", "prometheus") + + r, err := NewMetricReader(context.Background()) + assert.NoError(t, err) + assert.IsType(t, &metric.PeriodicReader{}, r) + + // Register it with a meter provider to ensure it is used. + // mp.Shutdown errors out because r.Shutdown closes the reader. + metric.NewMeterProvider(metric.WithReader(r)) + + // Shutdown actually makes an export call. + assert.NoError(t, r.Shutdown(context.Background())) + + <-requestWaitChan + ts.Close() + goleak.VerifyNone(t) +} + +func TestMetricProducerPrometheusWithPrometheusExporter(t *testing.T) { + assertNoOtelHandleErrors(t) + + t.Setenv("OTEL_METRICS_EXPORTER", "prometheus") + t.Setenv("OTEL_EXPORTER_PROMETHEUS_PORT", "0") + t.Setenv("OTEL_METRICS_PRODUCERS", "prometheus") + + r, err := NewMetricReader(context.Background()) + assert.NoError(t, err) + + // pull-based exporters like Prometheus need to be registered + mp := metric.NewMeterProvider(metric.WithReader(r)) + + rws, ok := r.(readerWithServer) + if !ok { + t.Errorf("expected readerWithServer but got %v", r) + } + + resp, err := http.Get(fmt.Sprintf("http://%s/metrics", rws.addr)) + assert.NoError(t, err) + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + // By default there are two metrics exporter. target_info and promhttp_metric_handler_errors_total. + // But by including the prometheus producer we should have more. + assert.Greater(t, strings.Count(string(body), "# HELP"), 2) + + assert.NoError(t, mp.Shutdown(context.Background())) + goleak.VerifyNone(t) +} + +func TestMetricProducerFallbackWithPrometheusExporter(t *testing.T) { + assertNoOtelHandleErrors(t) + + reg := prometheus.NewRegistry() + someDummyMetric := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "dummy_metric", + Help: "dummy metric", + }) + reg.MustRegister(someDummyMetric) + + WithFallbackMetricProducer(func(context.Context) (metric.Producer, error) { + return prometheusbridge.NewMetricProducer(prometheusbridge.WithGatherer(reg)), nil + }) + + t.Setenv("OTEL_METRICS_EXPORTER", "prometheus") + t.Setenv("OTEL_EXPORTER_PROMETHEUS_PORT", "0") + + r, err := NewMetricReader(context.Background()) + assert.NoError(t, err) + + // pull-based exporters like Prometheus need to be registered + mp := metric.NewMeterProvider(metric.WithReader(r)) + + rws, ok := r.(readerWithServer) + if !ok { + t.Errorf("expected readerWithServer but got %v", r) + } + + resp, err := http.Get(fmt.Sprintf("http://%s/metrics", rws.addr)) + assert.NoError(t, err) + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + assert.Contains(t, string(body), "HELP dummy_metric_total dummy metric") + + assert.NoError(t, mp.Shutdown(context.Background())) + goleak.VerifyNone(t) +} + +func TestMultipleMetricProducerWithOTLPExporter(t *testing.T) { + requestWaitChan := make(chan struct{}) + + reg1 := prometheus.NewRegistry() + someDummyMetric := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "dummy_metric_1", + Help: "dummy metric ONE", + }) + reg1.MustRegister(someDummyMetric) + reg2 := prometheus.NewRegistry() + someOtherDummyMetric := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "dummy_metric_2", + Help: "dummy metric TWO", + }) + reg2.MustRegister(someOtherDummyMetric) + + RegisterMetricProducer("first_producer", func(context.Context) (metric.Producer, error) { + return prometheusbridge.NewMetricProducer(prometheusbridge.WithGatherer(reg1)), nil + }) + RegisterMetricProducer("second_producer", func(context.Context) (metric.Producer, error) { + return prometheusbridge.NewMetricProducer(prometheusbridge.WithGatherer(reg2)), nil + }) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.NoError(t, r.Body.Close()) + + // Now parse the otlp proto message from request body. + req := pmetricotlp.NewExportRequest() + assert.NoError(t, req.UnmarshalProto(body)) + + metricNames := []string{} + sm := req.Metrics().ResourceMetrics().At(0).ScopeMetrics() + + for i := 0; i < sm.Len(); i++ { + m := sm.At(i).Metrics() + for i := 0; i < m.Len(); i++ { + metricNames = append(metricNames, m.At(i).Name()) + } + } + + assert.ElementsMatch(t, metricNames, []string{"dummy_metric_1", "dummy_metric_2"}) + + close(requestWaitChan) + })) + + t.Setenv("OTEL_METRICS_EXPORTER", "otlp") + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", ts.URL) + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + t.Setenv("OTEL_METRICS_PRODUCERS", "first_producer,second_producer,first_producer") + + r, err := NewMetricReader(context.Background()) + assert.NoError(t, err) + assert.IsType(t, &metric.PeriodicReader{}, r) + + // Register it with a meter provider to ensure it is used. + // mp.Shutdown errors out because r.Shutdown closes the reader. + metric.NewMeterProvider(metric.WithReader(r)) + + // Shutdown actually makes an export call. + assert.NoError(t, r.Shutdown(context.Background())) + + <-requestWaitChan + ts.Close() + goleak.VerifyNone(t) +} diff --git a/exporters/autoexport/noop.go b/exporters/autoexport/noop.go index c0921941d87..7ea4bd69754 100644 --- a/exporters/autoexport/noop.go +++ b/exporters/autoexport/noop.go @@ -7,6 +7,7 @@ import ( "context" "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/trace" ) @@ -46,3 +47,13 @@ func IsNoneMetricReader(e metric.Reader) bool { _, ok := e.(noopMetricReader) return ok } + +type noopMetricProducer struct{} + +func (e noopMetricProducer) Produce(ctx context.Context) ([]metricdata.ScopeMetrics, error) { + return nil, nil +} + +func newNoopMetricProducer() noopMetricProducer { + return noopMetricProducer{} +} diff --git a/exporters/autoexport/registry.go b/exporters/autoexport/registry.go index a7032e101be..3d9abcafdc0 100644 --- a/exporters/autoexport/registry.go +++ b/exporters/autoexport/registry.go @@ -21,9 +21,9 @@ type registry[T any] struct { } var ( - // errUnknownExporter is returned when an unknown exporter name is used in - // the OTEL_*_EXPORTER environment variables. - errUnknownExporter = errors.New("unknown exporter") + // errUnknownExporterProducer is returned when an unknown exporter name is used in + // the OTEL_*_EXPORTER or OTEL_METRICS_PRODUCERS environment variables. + errUnknownExporterProducer = errors.New("unknown exporter or metrics producer") // errInvalidOTLPProtocol is returned when an invalid protocol is used in // the OTEL_EXPORTER_OTLP_PROTOCOL environment variable. @@ -35,7 +35,7 @@ var ( // load returns tries to find the exporter factory with the key and // then execute the factory, returning the created SpanExporter. -// errUnknownExporter is returned if the registration is missing and the error from +// errUnknownExporterProducer is returned if the registration is missing and the error from // executing the factory if not nil. func (r *registry[T]) load(ctx context.Context, key string) (T, error) { r.mu.Lock() @@ -43,7 +43,7 @@ func (r *registry[T]) load(ctx context.Context, key string) (T, error) { factory, ok := r.names[key] if !ok { var zero T - return zero, errUnknownExporter + return zero, errUnknownExporterProducer } return factory(ctx) } diff --git a/exporters/autoexport/registry_test.go b/exporters/autoexport/registry_test.go index 85138e53db3..d33b7483c1c 100644 --- a/exporters/autoexport/registry_test.go +++ b/exporters/autoexport/registry_test.go @@ -34,7 +34,7 @@ func TestCanStoreExporterFactory(t *testing.T) { func TestLoadOfUnknownExporterReturnsError(t *testing.T) { r := newTestRegistry() exp, err := r.load(context.Background(), "non-existent") - assert.Equal(t, err, errUnknownExporter, "empty registry should hold nothing") + assert.Equal(t, err, errUnknownExporterProducer, "empty registry should hold nothing") assert.Nil(t, exp, "non-nil exporter returned") }