Skip to content

Commit

Permalink
Add support for Prometheus exporter (#4479)
Browse files Browse the repository at this point in the history
  • Loading branch information
punya committed Oct 31, 2023
1 parent 724a94b commit bead7e4
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion exporters/autoexport/go.mod
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions 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=
Expand All @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
77 changes: 77 additions & 0 deletions exporters/autoexport/metrics.go
Expand Up @@ -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"
)

Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
55 changes: 55 additions & 0 deletions exporters/autoexport/metrics_test.go
Expand Up @@ -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))
}

Expand All @@ -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.
Expand All @@ -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")
}
6 changes: 6 additions & 0 deletions exporters/autoexport/spans_test.go
Expand Up @@ -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))
}

Expand All @@ -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.
Expand Down

0 comments on commit bead7e4

Please sign in to comment.