Skip to content

Commit 8e92da2

Browse files
Alex Botencodeboten
authored andcommitted
config: NewSDK can return valid MeterProvider
Follow up to #4741, does the same but for metric signal. Fixes #4371 Signed-off-by: Alex Boten <[email protected]>
1 parent a111e3e commit 8e92da2

File tree

7 files changed

+757
-11
lines changed

7 files changed

+757
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1111
### Added
1212

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

1516
### Removed
1617

config/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ func NewSDK(opts ...ConfigurationOption) (SDK, error) {
6767
return SDK{}, err
6868
}
6969

70-
mp, mpShutdown := initMeterProvider(o)
70+
mp, mpShutdown, err := meterProvider(o, r)
71+
if err != nil {
72+
return SDK{}, err
73+
}
74+
7175
tp, tpShutdown, err := tracerProvider(o, r)
7276
if err != nil {
7377
return SDK{}, err

config/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ module go.opentelemetry.io/contrib/config
33
go 1.21
44

55
require (
6+
github.com/prometheus/client_golang v1.19.0
67
github.com/stretchr/testify v1.9.0
78
go.opentelemetry.io/otel v1.24.0
9+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0
10+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0
811
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
912
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
13+
go.opentelemetry.io/otel/exporters/prometheus v0.46.0
14+
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0
1015
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0
1116
go.opentelemetry.io/otel/metric v1.24.0
1217
go.opentelemetry.io/otel/sdk v1.24.0
@@ -15,13 +20,18 @@ require (
1520
)
1621

1722
require (
23+
github.com/beorn7/perks v1.0.1 // indirect
1824
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
25+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
1926
github.com/davecgh/go-spew v1.1.1 // indirect
2027
github.com/go-logr/logr v1.4.1 // indirect
2128
github.com/go-logr/stdr v1.2.2 // indirect
2229
github.com/golang/protobuf v1.5.3 // indirect
2330
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
2431
github.com/pmezard/go-difflib v1.0.0 // indirect
32+
github.com/prometheus/client_model v0.6.0 // indirect
33+
github.com/prometheus/common v0.48.0 // indirect
34+
github.com/prometheus/procfs v0.12.0 // indirect
2535
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
2636
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
2737
golang.org/x/net v0.21.0 // indirect

config/go.sum

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
13
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
24
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
5+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
6+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
37
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
48
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
59
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -21,18 +25,34 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2125
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2226
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2327
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24-
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
25-
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
28+
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
29+
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
30+
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
31+
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
32+
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
33+
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
34+
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
35+
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
36+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
37+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
2638
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2739
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2840
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
2941
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
42+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 h1:f2jriWfOdldanBwS9jNBdeOKAQN7b4ugAMaNu1/1k9g=
43+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0/go.mod h1:B+bcQI1yTY+N0vqMpoZbEN7+XU4tNM0DmUiOwebFJWI=
44+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0 h1:mM8nKi6/iFQ0iqst80wDHU2ge198Ye/TfN0WBS5U24Y=
45+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0/go.mod h1:0PrIIzDteLSmNyxqcGYRL4mDIo8OTuBAOI/Bn1URxac=
3046
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
3147
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
3248
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
3349
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
3450
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
3551
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
52+
go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ=
53+
go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs=
54+
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc=
55+
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY=
3656
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
3757
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
3858
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=

config/metric.go

Lines changed: 246 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,257 @@
44
package config // import "go.opentelemetry.io/contrib/config"
55

66
import (
7+
"context"
8+
"encoding/json"
9+
"errors"
10+
"fmt"
11+
"net"
12+
"net/http"
13+
"net/url"
14+
"os"
15+
"time"
16+
17+
"github.com/prometheus/client_golang/prometheus"
18+
"github.com/prometheus/client_golang/prometheus/promhttp"
19+
20+
"go.opentelemetry.io/otel"
21+
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
22+
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
23+
otelprom "go.opentelemetry.io/otel/exporters/prometheus"
24+
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
725
"go.opentelemetry.io/otel/metric"
826
"go.opentelemetry.io/otel/metric/noop"
927
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
28+
"go.opentelemetry.io/otel/sdk/resource"
1029
)
1130

12-
func initMeterProvider(cfg configOptions) (metric.MeterProvider, shutdownFunc) {
31+
func meterProvider(cfg configOptions, res *resource.Resource) (metric.MeterProvider, shutdownFunc, error) {
1332
if cfg.opentelemetryConfig.MeterProvider == nil {
14-
return noop.NewMeterProvider(), noopShutdown
33+
return noop.NewMeterProvider(), noopShutdown, nil
34+
}
35+
opts := []sdkmetric.Option{
36+
sdkmetric.WithResource(res),
37+
}
38+
39+
var errs []error
40+
for _, reader := range cfg.opentelemetryConfig.MeterProvider.Readers {
41+
r, err := metricReader(cfg.ctx, reader)
42+
if err == nil {
43+
opts = append(opts, sdkmetric.WithReader(r))
44+
} else {
45+
errs = append(errs, err)
46+
}
47+
}
48+
if len(errs) > 0 {
49+
return noop.NewMeterProvider(), noopShutdown, errors.Join(errs...)
50+
}
51+
52+
mp := sdkmetric.NewMeterProvider(opts...)
53+
return mp, mp.Shutdown, nil
54+
}
55+
56+
func metricReader(ctx context.Context, r MetricReader) (sdkmetric.Reader, error) {
57+
if r.Periodic != nil && r.Pull != nil {
58+
return nil, errors.New("must not specify multiple metric reader type")
59+
}
60+
61+
if r.Periodic != nil {
62+
return periodicExporter(ctx, r.Periodic.Exporter)
63+
}
64+
65+
if r.Pull != nil {
66+
return pullReader(ctx, r.Pull.Exporter)
67+
}
68+
return nil, errors.New("no valid metric reader")
69+
}
70+
71+
func pullReader(ctx context.Context, exporter MetricExporter) (sdkmetric.Reader, error) {
72+
if exporter.Prometheus != nil {
73+
return prometheusReader(ctx, exporter.Prometheus)
74+
}
75+
return nil, errors.New("no valid metric exporter")
76+
}
77+
78+
func periodicExporter(ctx context.Context, exporter MetricExporter, opts ...sdkmetric.PeriodicReaderOption) (sdkmetric.Reader, error) {
79+
if exporter.Console != nil && exporter.OTLP != nil {
80+
return nil, errors.New("must not specify multiple exporters")
81+
}
82+
if exporter.Console != nil {
83+
enc := json.NewEncoder(os.Stdout)
84+
enc.SetIndent("", " ")
85+
86+
exp, err := stdoutmetric.New(
87+
stdoutmetric.WithEncoder(enc),
88+
)
89+
if err != nil {
90+
return nil, err
91+
}
92+
return sdkmetric.NewPeriodicReader(exp, opts...), nil
93+
}
94+
if exporter.OTLP != nil {
95+
var err error
96+
var exp sdkmetric.Exporter
97+
switch exporter.OTLP.Protocol {
98+
case protocolProtobufHTTP:
99+
exp, err = otlpHTTPMetricExporter(ctx, exporter.OTLP)
100+
case protocolProtobufGRPC:
101+
exp, err = otlpGRPCMetricExporter(ctx, exporter.OTLP)
102+
default:
103+
return nil, fmt.Errorf("unsupported protocol %q", exporter.OTLP.Protocol)
104+
}
105+
if err != nil {
106+
return nil, err
107+
}
108+
return sdkmetric.NewPeriodicReader(exp, opts...), nil
109+
}
110+
return nil, errors.New("no valid metric exporter")
111+
}
112+
113+
func otlpHTTPMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmetric.Exporter, error) {
114+
opts := []otlpmetrichttp.Option{}
115+
116+
if len(otlpConfig.Endpoint) > 0 {
117+
u, err := url.ParseRequestURI(otlpConfig.Endpoint)
118+
if err != nil {
119+
return nil, err
120+
}
121+
opts = append(opts, otlpmetrichttp.WithEndpoint(u.Host))
122+
123+
if u.Scheme == "http" {
124+
opts = append(opts, otlpmetrichttp.WithInsecure())
125+
}
126+
if len(u.Path) > 0 {
127+
opts = append(opts, otlpmetrichttp.WithURLPath(u.Path))
128+
}
129+
}
130+
if otlpConfig.Compression != nil {
131+
switch *otlpConfig.Compression {
132+
case compressionGzip:
133+
opts = append(opts, otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression))
134+
case compressionNone:
135+
opts = append(opts, otlpmetrichttp.WithCompression(otlpmetrichttp.NoCompression))
136+
default:
137+
return nil, fmt.Errorf("unsupported compression %q", *otlpConfig.Compression)
138+
}
139+
}
140+
if otlpConfig.Timeout != nil {
141+
opts = append(opts, otlpmetrichttp.WithTimeout(time.Millisecond*time.Duration(*otlpConfig.Timeout)))
15142
}
16-
mp := sdkmetric.NewMeterProvider()
17-
return mp, mp.Shutdown
143+
if len(otlpConfig.Headers) > 0 {
144+
opts = append(opts, otlpmetrichttp.WithHeaders(otlpConfig.Headers))
145+
}
146+
147+
return otlpmetrichttp.New(ctx, opts...)
148+
}
149+
150+
func otlpGRPCMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmetric.Exporter, error) {
151+
opts := []otlpmetricgrpc.Option{}
152+
153+
if len(otlpConfig.Endpoint) > 0 {
154+
u, err := url.ParseRequestURI(otlpConfig.Endpoint)
155+
if err != nil {
156+
return nil, err
157+
}
158+
// ParseRequestURI leaves the Host field empty when no
159+
// scheme is specified (i.e. localhost:4317). This check is
160+
// here to support the case where a user may not specify a
161+
// scheme. The code does its best effort here by using
162+
// otlpConfig.Endpoint as-is in that case
163+
if u.Host != "" {
164+
opts = append(opts, otlpmetricgrpc.WithEndpoint(u.Host))
165+
} else {
166+
opts = append(opts, otlpmetricgrpc.WithEndpoint(otlpConfig.Endpoint))
167+
}
168+
if u.Scheme == "http" {
169+
opts = append(opts, otlpmetricgrpc.WithInsecure())
170+
}
171+
}
172+
173+
if otlpConfig.Compression != nil {
174+
switch *otlpConfig.Compression {
175+
case compressionGzip:
176+
opts = append(opts, otlpmetricgrpc.WithCompressor(*otlpConfig.Compression))
177+
case compressionNone:
178+
// none requires no options
179+
default:
180+
return nil, fmt.Errorf("unsupported compression %q", *otlpConfig.Compression)
181+
}
182+
}
183+
if otlpConfig.Timeout != nil && *otlpConfig.Timeout > 0 {
184+
opts = append(opts, otlpmetricgrpc.WithTimeout(time.Millisecond*time.Duration(*otlpConfig.Timeout)))
185+
}
186+
if len(otlpConfig.Headers) > 0 {
187+
opts = append(opts, otlpmetricgrpc.WithHeaders(otlpConfig.Headers))
188+
}
189+
190+
return otlpmetricgrpc.New(ctx, opts...)
191+
}
192+
193+
func prometheusReader(ctx context.Context, prometheusConfig *Prometheus) (sdkmetric.Reader, error) {
194+
var opts []otelprom.Option
195+
if prometheusConfig.Host == nil {
196+
return nil, fmt.Errorf("host must be specified")
197+
}
198+
if prometheusConfig.Port == nil {
199+
return nil, fmt.Errorf("port must be specified")
200+
}
201+
if prometheusConfig.WithoutScopeInfo != nil && *prometheusConfig.WithoutScopeInfo {
202+
opts = append(opts, otelprom.WithoutScopeInfo())
203+
}
204+
if prometheusConfig.WithoutTypeSuffix != nil && *prometheusConfig.WithoutTypeSuffix {
205+
opts = append(opts, otelprom.WithoutCounterSuffixes())
206+
}
207+
if prometheusConfig.WithoutUnits != nil && *prometheusConfig.WithoutUnits {
208+
opts = append(opts, otelprom.WithoutUnits())
209+
}
210+
211+
reg := prometheus.NewRegistry()
212+
opts = append(opts, otelprom.WithRegisterer(reg))
213+
214+
mux := http.NewServeMux()
215+
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
216+
server := http.Server{
217+
// Timeouts are necessary to make a server resilent to attacks, but ListenAndServe doesn't set any.
218+
// We use values from this example: https://blog.cloudflare.com/exposing-go-on-the-internet/#:~:text=There%20are%20three%20main%20timeouts
219+
ReadTimeout: 5 * time.Second,
220+
WriteTimeout: 10 * time.Second,
221+
IdleTimeout: 120 * time.Second,
222+
Handler: mux,
223+
}
224+
addr := fmt.Sprintf("%s:%d", *prometheusConfig.Host, *prometheusConfig.Port)
225+
226+
// TODO: add support for constant label filter
227+
// otelprom.WithResourceAsConstantLabels(attribute.NewDenyKeysFilter()),
228+
// )
229+
reader, err := otelprom.New(opts...)
230+
if err != nil {
231+
return nil, fmt.Errorf("error creating otel prometheus exporter: %w", err)
232+
}
233+
lis, err := net.Listen("tcp", addr)
234+
if err != nil {
235+
return nil, errors.Join(
236+
fmt.Errorf("binding address %s for Prometheus exporter: %w", addr, err),
237+
reader.Shutdown(ctx),
238+
)
239+
}
240+
241+
go func() {
242+
if err := server.Serve(lis); err != nil && err != http.ErrServerClosed {
243+
otel.Handle(fmt.Errorf("the Prometheus HTTP server exited unexpectedly: %w", err))
244+
}
245+
}()
246+
247+
return readerWithServer{reader, &server}, nil
248+
}
249+
250+
type readerWithServer struct {
251+
sdkmetric.Reader
252+
server *http.Server
253+
}
254+
255+
func (rws readerWithServer) Shutdown(ctx context.Context) error {
256+
return errors.Join(
257+
rws.Reader.Shutdown(ctx),
258+
rws.server.Shutdown(ctx),
259+
)
18260
}

0 commit comments

Comments
 (0)