Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Prometheus exporter #4479

Merged
merged 17 commits into from
Oct 31, 2023
Merged
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.2.4 // 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
Original file line number Diff line number Diff line change
@@ -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.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
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
73 changes: 73 additions & 0 deletions exporters/autoexport/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@

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 Down Expand Up @@ -94,4 +104,67 @@
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
}

Check warning on line 115 in exporters/autoexport/metrics.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/metrics.go#L114-L115

Added lines #L114 - L115 were not covered by tests

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 {
//lint:ignore ST1005 Prometheus is capitalized because it's a proper noun
otel.Handle(fmt.Errorf("Prometheus HTTP server exited unexpectedly: %w", err))
}

Check warning on line 144 in exporters/autoexport/metrics.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/metrics.go#L142-L144

Added lines #L142 - L144 were not covered by tests
}()

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
}
65 changes: 65 additions & 0 deletions exporters/autoexport/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,27 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"
import (
"context"
"fmt"
"io"
"net/http"
"os"
"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 +56,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 +75,56 @@ 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")
punya marked this conversation as resolved.
Show resolved Hide resolved
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")

switch os.Geteuid() {
case -1:
t.Skip("skipping invalid port check on Windows")
case 0:
t.Skip("skipping invalid port check because the test is running as root (!!)")
}

// an unprivileged user on Unix should fail to bind a port < 1024
t.Setenv("OTEL_EXPORTER_PROMETHEUS_PORT", "10")
punya marked this conversation as resolved.
Show resolved Hide resolved

_, err := NewMetricReader(context.Background())
assert.ErrorContains(t, err, "binding")
}
6 changes: 6 additions & 0 deletions exporters/autoexport/spans_test.go
Original file line number Diff line number Diff line change
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
Loading