knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/observability/metrics/provider.go (about) 1 /* 2 Copyright 2025 The Knative Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package metrics 18 19 import ( 20 "cmp" 21 "context" 22 "errors" 23 "fmt" 24 "net/url" 25 "os" 26 27 "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" 28 "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 29 "go.opentelemetry.io/otel/metric" 30 "go.opentelemetry.io/otel/metric/noop" 31 sdkmetric "go.opentelemetry.io/otel/sdk/metric" 32 ) 33 34 type shutdownFunc func(ctx context.Context) error 35 36 func noopFunc(context.Context) error { return nil } 37 38 type MeterProvider struct { 39 metric.MeterProvider 40 shutdown []shutdownFunc 41 } 42 43 func (m *MeterProvider) Shutdown(ctx context.Context) error { 44 var errs []error 45 for _, shutdown := range m.shutdown { 46 if err := shutdown(ctx); err != nil { 47 errs = append(errs, err) 48 } 49 } 50 return errors.Join(errs...) 51 } 52 53 func NewMeterProvider( 54 ctx context.Context, 55 cfg Config, 56 opts ...sdkmetric.Option, 57 ) (*MeterProvider, error) { 58 if cfg.Protocol == ProtocolNone { 59 return &MeterProvider{MeterProvider: noop.NewMeterProvider()}, nil 60 } 61 62 reader, rShutdown, err := readerFor(ctx, cfg) 63 if err != nil { 64 return nil, fmt.Errorf("error creating reader: %w", err) 65 } 66 67 opts = append(opts, sdkmetric.WithReader(reader)) 68 provider := sdkmetric.NewMeterProvider(opts...) 69 70 return &MeterProvider{ 71 MeterProvider: provider, 72 shutdown: []shutdownFunc{provider.Shutdown, rShutdown}, 73 }, nil 74 } 75 76 func readerFor(ctx context.Context, cfg Config) (sdkmetric.Reader, shutdownFunc, error) { 77 switch cfg.Protocol { 78 case ProtocolGRPC: 79 return buildGRPC(ctx, cfg) 80 case ProtocolHTTPProtobuf: 81 return buildHTTP(ctx, cfg) 82 case ProtocolPrometheus: 83 return buildPrometheus(ctx, cfg) 84 default: 85 return nil, noopFunc, fmt.Errorf("unsupported metric exporter: %q", cfg.Protocol) 86 } 87 } 88 89 func buildGRPC(ctx context.Context, cfg Config) (sdkmetric.Reader, shutdownFunc, error) { 90 var grpcOpts []otlpmetricgrpc.Option 91 92 opt, err := endpointFor(cfg, otlpmetricgrpc.WithEndpointURL) 93 if err != nil { 94 return nil, noopFunc, fmt.Errorf("unable to process metrics endpoint: %w", err) 95 } else if opt != nil { 96 grpcOpts = append(grpcOpts, opt) 97 } 98 99 exporter, err := otlpmetricgrpc.New(ctx, grpcOpts...) 100 if err != nil { 101 return nil, noopFunc, fmt.Errorf("failed to build exporter: %w", err) 102 } 103 104 var opts []sdkmetric.PeriodicReaderOption 105 106 if interval := intervalFor(cfg); interval != nil { 107 opts = append(opts, interval) 108 } 109 110 return sdkmetric.NewPeriodicReader(exporter, opts...), noopFunc, nil 111 } 112 113 func buildHTTP(ctx context.Context, cfg Config) (sdkmetric.Reader, shutdownFunc, error) { 114 var httpOpts []otlpmetrichttp.Option 115 116 opt, err := endpointFor(cfg, otlpmetrichttp.WithEndpointURL) 117 if err != nil { 118 return nil, noopFunc, fmt.Errorf("unable to process metrics endpoint: %w", err) 119 } else if opt != nil { 120 httpOpts = append(httpOpts, opt) 121 } 122 123 exporter, err := otlpmetrichttp.New(ctx, httpOpts...) 124 if err != nil { 125 return nil, noopFunc, fmt.Errorf("failed to build exporter: %w", err) 126 } 127 128 var opts []sdkmetric.PeriodicReaderOption 129 if opt := intervalFor(cfg); opt != nil { 130 opts = append(opts, opt) 131 } 132 133 return sdkmetric.NewPeriodicReader(exporter, opts...), noopFunc, nil 134 } 135 136 func intervalFor(cfg Config) sdkmetric.PeriodicReaderOption { 137 var interval sdkmetric.PeriodicReaderOption 138 139 // Use the configuration's export interval if it's non-zero and there isn't 140 // an environment override 141 if os.Getenv("OTEL_METRIC_EXPORT_INTERVAL") == "" && cfg.ExportInterval != 0 { 142 interval = sdkmetric.WithInterval(cfg.ExportInterval) 143 } 144 145 return interval 146 } 147 148 // If the OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_METRICS_ENDPOINT 149 func endpointFor[T any](cfg Config, opt func(string) T) (T, error) { 150 var epOption T 151 152 override := cmp.Or( 153 os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), 154 os.Getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"), 155 ) 156 if override != "" { 157 return epOption, nil 158 } 159 160 ep := cfg.Endpoint 161 162 u, err := url.Parse(cfg.Endpoint) 163 if err != nil { 164 return epOption, err 165 } 166 if u.Opaque != "" { 167 ep = "https://" + ep 168 } 169 170 epOption = opt(ep) 171 return epOption, nil 172 }