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  }