Skip to content

Commit

Permalink
config: NewSDK can return valid MeterProvider (#4804)
Browse files Browse the repository at this point in the history
  • Loading branch information
codeboten authored Apr 12, 2024
1 parent 7a9e861 commit 07d8068
Show file tree
Hide file tree
Showing 7 changed files with 757 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108)
- `NewSDK` in `go.opentelemetry.io/contrib/config` now returns a configured SDK with a valid `MeterProvider`. (#4804)

## [1.25.0/0.50.0/0.19.0/0.5.0/0.0.1] - 2024-04-05

Expand Down
6 changes: 5 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ func NewSDK(opts ...ConfigurationOption) (SDK, error) {
return SDK{}, err
}

mp, mpShutdown := initMeterProvider(o)
mp, mpShutdown, err := meterProvider(o, r)
if err != nil {
return SDK{}, err
}

tp, tpShutdown, err := tracerProvider(o, r)
if err != nil {
return SDK{}, err
Expand Down
10 changes: 10 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.21

require (
github.com/prometheus/client_golang v1.19.0
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0
go.opentelemetry.io/otel/exporters/prometheus v0.47.0
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0
go.opentelemetry.io/otel/metric v1.25.0
go.opentelemetry.io/otel/sdk v1.25.0
Expand All @@ -15,12 +20,17 @@ require (
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // 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
github.com/prometheus/procfs v0.12.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/net v0.24.0 // indirect
Expand Down
24 changes: 22 additions & 2 deletions 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.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
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.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,18 +21,34 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
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/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
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=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0 h1:hDKnobznDpcdTlNzO0S/owRB8tyVr1OoeZZhDoqY+Cs=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0/go.mod h1:kUDQaUs1h8iTIHbQTk+iJRiUvSfJYMMKTtMCaiVu7B0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0 h1:Wc4hZuYXhVqq+TfRXLXlmNIL/awOanGx8ssq3ciDQxc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.25.0/go.mod h1:BydOvapRqVEc0DVz27qWBX2jq45Ca5TI9mhZBDIdweY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 h1:vOL89uRfOCCNIjkisd0r7SEdJF3ZJFyCNY34fdZs8eU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
go.opentelemetry.io/otel/exporters/prometheus v0.47.0 h1:OL6yk1Z/pEGdDnrBbxSsH+t4FY1zXfBRGd7bjwhlMLU=
go.opentelemetry.io/otel/exporters/prometheus v0.47.0/go.mod h1:xF3N4OSICZDVbbYZydz9MHFro1RjmkPUKEvar2utG+Q=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0 h1:d7nHbdzU84STOiszaOxQ3kw5IwkSmHsU5Muol5/vL4I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.25.0/go.mod h1:yiPA1iZbb/EHYnODXOxvtKuB0I2hV8ehfLTEWpl7BJU=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0 h1:0vZZdECYzhTt9MKQZ5qQ0V+J3MFu4MQaQ3COfugF+FQ=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0/go.mod h1:e7iXx3HjaSSBXfy9ykVUlupS2Vp7LBIBuT21ousM2Hk=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
Expand Down
250 changes: 246 additions & 4 deletions config/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,257 @@
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"
"go.opentelemetry.io/otel/sdk/resource"
)

func initMeterProvider(cfg configOptions) (metric.MeterProvider, shutdownFunc) {
func meterProvider(cfg configOptions, res *resource.Resource) (metric.MeterProvider, shutdownFunc, error) {
if cfg.opentelemetryConfig.MeterProvider == nil {
return noop.NewMeterProvider(), noopShutdown
return noop.NewMeterProvider(), noopShutdown, nil
}
opts := []sdkmetric.Option{
sdkmetric.WithResource(res),
}

var errs []error
for _, reader := range cfg.opentelemetryConfig.MeterProvider.Readers {
r, err := metricReader(cfg.ctx, reader)
if err == nil {
opts = append(opts, sdkmetric.WithReader(r))
} else {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return noop.NewMeterProvider(), noopShutdown, errors.Join(errs...)
}

mp := sdkmetric.NewMeterProvider(opts...)
return mp, mp.Shutdown, nil
}

func metricReader(ctx context.Context, r MetricReader) (sdkmetric.Reader, error) {
if r.Periodic != nil && r.Pull != nil {
return nil, errors.New("must not specify multiple metric reader type")
}

if r.Periodic != nil {
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 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 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 %q", 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)))
}
mp := sdkmetric.NewMeterProvider()
return mp, mp.Shutdown
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),
)
}
Loading

0 comments on commit 07d8068

Please sign in to comment.