Skip to content

Commit

Permalink
add tests for prom exporter
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Boten <aboten@lightstep.com>
  • Loading branch information
Alex Boten committed Jan 22, 2024
1 parent f5e672b commit 792c424
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 11 deletions.
11 changes: 11 additions & 0 deletions config/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ module go.opentelemetry.io/contrib/config
go 1.20

require (
github.com/prometheus/client_golang v1.18.0
github.com/stretchr/testify v1.8.4
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0
go.opentelemetry.io/otel/exporters/prometheus v0.45.0
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.45.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0
Expand All @@ -15,13 +20,19 @@ require (
)

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.4.1 // 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/v2 v2.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.20.0 // indirect
Expand Down
24 changes: 23 additions & 1 deletion config/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 @@ -17,19 +21,37 @@ 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/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
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/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0 h1:tfil6di0PoNV7FZdsCS7A5izZoVVQ7AuXtyekbOpG/I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0/go.mod h1:AKFZIEPOnqB00P63bTjOiah4ZTaRzl1TKwUWpZdYUHI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0 h1:+RbSCde0ERway5FwKvXR3aRJIFeDu9rtwC6E7BC6uoM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0/go.mod h1:zcI8u2EJxbLPyoZ3SkVAAcQPgYb1TDRzW93xLFnsggU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/exporters/prometheus v0.45.0 h1:BeIK2KGho0oCWa7LxEGSqfDZbs7Fpv/Viz+FS4P8CXE=
go.opentelemetry.io/otel/exporters/prometheus v0.45.0/go.mod h1:UVJZPLnfDSvHj+eJuZE+E1GjIBD267mEMfAAHJdghWg=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.45.0 h1:NjN6zc7Mwy9torqa3mo+pMJ3mHoPI0uzVSYcqB2t72A=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.45.0/go.mod h1:U+T5v2bk4fCC8XdSEWZja3Pm/ZhvV/zE7JwX/ELJKts=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0 h1:zr8ymM5OWWjjiWRzwTfZ67c905+2TMHYp2lMJ52QTyM=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0/go.mod h1:sQs7FT2iLVJ+67vYngGJkPe1qr39IzaBzaj9IDNNY8k=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
Expand Down
210 changes: 200 additions & 10 deletions config/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,23 @@ 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"
Expand Down Expand Up @@ -44,27 +59,202 @@ func metricReader(ctx context.Context, r MetricReader) (sdkmetric.Reader, error)
}

if r.Periodic != nil {
exp, err := metricExporter(ctx, r.Periodic.Exporter)
if err != nil {
return nil, err
}
return periodMetricReader(r.Periodic, exp)
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 periodMetricReader(pmr *PeriodicMetricReader, exp sdkmetric.Exporter) (sdkmetric.Reader, error) {
var opts []sdkmetric.PeriodicReaderOption
return sdkmetric.NewPeriodicReader(exp, opts...), nil
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 metricExporter(ctx context.Context, exporter MetricExporter) (sdkmetric.Exporter, error) {
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 %s", 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)))
}
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),
)
}

0 comments on commit 792c424

Please sign in to comment.