diff --git a/CHANGELOG.md b/CHANGELOG.md index 85047db8419..cff94fe55d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `"go.opentelemetry.io/contrib/samplers/jaegerremote".WithSamplingStrategyFetcher` which sets custom fetcher implementation. (#4045) - Add `"go.opentelemetry.io/contrib/config"` package that includes configuration models generated via go-jsonschema. (#4376) - Add `NewSDK` function to `"go.opentelemetry.io/contrib/config"`. The initial implementation only returns noop providers. (#4414) -- Add metrics support to `go.opentelemetry.io/contrib/exporters/autoexport`. (#4229) +- Add metrics support (No-op, OTLP and Prometheus) to `go.opentelemetry.io/contrib/exporters/autoexport`. (#4229, #4479) ### Changed diff --git a/exporters/autoexport/go.mod b/exporters/autoexport/go.mod index ceb3347805d..e2a8b562877 100644 --- a/exporters/autoexport/go.mod +++ b/exporters/autoexport/go.mod @@ -3,25 +3,34 @@ module go.opentelemetry.io/contrib/exporters/autoexport go 1.20 require ( + github.com/prometheus/client_golang v1.16.0 github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 + go.opentelemetry.io/otel/exporters/prometheus v0.42.0 go.opentelemetry.io/otel/sdk v1.19.0 go.opentelemetry.io/otel/sdk/metric v1.19.0 + go.uber.org/goleak v1.2.1 ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // 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.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect diff --git a/exporters/autoexport/go.sum b/exporters/autoexport/go.sum index 78c02b337e1..47b02887646 100644 --- a/exporters/autoexport/go.sum +++ b/exporters/autoexport/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.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/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= @@ -8,6 +12,7 @@ github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 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/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -17,8 +22,18 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 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.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -36,6 +51,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S2 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/prometheus v0.42.0 h1:jwV9iQdvp38fxXi8ZC+lNpxjK16MRcZlpDYvbuO1FiA= +go.opentelemetry.io/otel/exporters/prometheus v0.42.0/go.mod h1:f3bYiqNqhoPxkvI2LrXqQVC546K7BuRDL/kKuxkujhA= go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= @@ -47,8 +64,10 @@ go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmY go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= diff --git a/exporters/autoexport/metrics.go b/exporters/autoexport/metrics.go index d43e81fa325..d8f260cadd8 100644 --- a/exporters/autoexport/metrics.go +++ b/exporters/autoexport/metrics.go @@ -16,10 +16,20 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" + "errors" + "fmt" + "net" + "net/http" "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" + promexporter "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/sdk/metric" ) @@ -38,6 +48,7 @@ func WithFallbackMetricReader(exporter metric.Reader) MetricOption { // OTEL_METRICS_EXPORTER defines the metrics exporter; supported values: // - "none" - "no operation" exporter // - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlpmetric] +// - "prometheus" - Prometheus exporter + HTTP server; see [go.opentelemetry.io/otel/exporters/prometheus] // // OTEL_EXPORTER_OTLP_PROTOCOL defines OTLP exporter's transport protocol; // supported values: @@ -46,6 +57,10 @@ func WithFallbackMetricReader(exporter metric.Reader) MetricOption { // - "http/protobuf" (default) - protobuf-encoded data over HTTP connection; // see: [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp] // +// OTEL_EXPORTER_PROMETHEUS_HOST (defaulting to "localhost") and +// OTEL_EXPORTER_PROMETHEUS_PORT (defaulting to 9464) define the host and port for the +// Prometheus exporter's HTTP server. +// // An error is returned if an environment value is set to an unhandled value. // // Use [RegisterMetricReader] to handle more values of OTEL_METRICS_EXPORTER. @@ -94,4 +109,66 @@ func init() { 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 + reg := prometheus.NewRegistry() + + reader, err := promexporter.New(promexporter.WithRegisterer(reg)) + if err != nil { + return nil, err + } + + 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, + } + + // environment variable names and defaults specified at https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#prometheus-exporter + host := getenv("OTEL_EXPORTER_PROMETHEUS_HOST", "localhost") + port := getenv("OTEL_EXPORTER_PROMETHEUS_PORT", "9464") + addr := host + ":" + port + 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{lis.Addr(), reader, &server}, nil + }) +} + +type readerWithServer struct { + addr net.Addr + metric.Reader + server *http.Server +} + +func (rws readerWithServer) Shutdown(ctx context.Context) error { + return errors.Join( + rws.Reader.Shutdown(ctx), + rws.server.Shutdown(ctx), + ) +} + +func getenv(key, fallback string) string { + result, ok := os.LookupEnv(key) + if !ok { + return fallback + } + return result } diff --git a/exporters/autoexport/metrics_test.go b/exporters/autoexport/metrics_test.go index 4a3ecff2f68..6624db36ecd 100644 --- a/exporters/autoexport/metrics_test.go +++ b/exporters/autoexport/metrics_test.go @@ -17,18 +17,26 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" "fmt" + "io" + "net/http" "reflect" + "runtime/debug" "testing" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/metric" "github.com/stretchr/testify/assert" + "go.uber.org/goleak" ) func TestMetricExporterNone(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "none") got, err := NewMetricReader(context.Background()) assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.Shutdown(context.Background())) + }) assert.True(t, IsNoneMetricReader(got)) } @@ -47,6 +55,9 @@ func TestMetricExporterOTLP(t *testing.T) { got, err := NewMetricReader(context.Background()) assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.Shutdown(context.Background())) + }) assert.IsType(t, &metric.PeriodicReader{}, got) // Implementation detail hack. This may break when bumping OTLP exporter modules as it uses unexported API. @@ -63,3 +74,47 @@ func TestMetricExporterOTLPOverInvalidProtocol(t *testing.T) { _, err := NewMetricReader(context.Background()) assert.Error(t, err) } + +func assertNoOtelHandleErrors(t *testing.T) { + h := otel.GetErrorHandler() + t.Cleanup(func() { otel.SetErrorHandler(h) }) + + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) { + t.Errorf("expected to calls to otel.Handle but got %v from %s", cause, debug.Stack()) + })) +} + +func TestMetricExporterPrometheus(t *testing.T) { + assertNoOtelHandleErrors(t) + + 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") + + assert.NoError(t, mp.Shutdown(context.Background())) + goleak.VerifyNone(t) +} + +func TestMetricExporterPrometheusInvalidPort(t *testing.T) { + t.Setenv("OTEL_METRICS_EXPORTER", "prometheus") + t.Setenv("OTEL_EXPORTER_PROMETHEUS_PORT", "invalid-port") + + _, err := NewMetricReader(context.Background()) + assert.ErrorContains(t, err, "binding") +} diff --git a/exporters/autoexport/spans_test.go b/exporters/autoexport/spans_test.go index 35ec975b301..b2851549295 100644 --- a/exporters/autoexport/spans_test.go +++ b/exporters/autoexport/spans_test.go @@ -29,6 +29,9 @@ func TestSpanExporterNone(t *testing.T) { t.Setenv("OTEL_TRACES_EXPORTER", "none") got, err := NewSpanExporter(context.Background()) assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.Shutdown(context.Background())) + }) assert.True(t, IsNoneSpanExporter(got)) } @@ -47,6 +50,9 @@ func TestSpanExporterOTLP(t *testing.T) { got, err := NewSpanExporter(context.Background()) assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.Shutdown(context.Background())) + }) assert.IsType(t, &otlptrace.Exporter{}, got) // Implementation detail hack. This may break when bumping OTLP exporter modules as it uses unexported API.