From bffeb6a966923e8f1a3c8dc289b8b2238e69d5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:14:58 +0100 Subject: [PATCH 1/7] feat: enhance metrics exporter configuration and add collector URI override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 2 +- flagd/cmd/start.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index 55baf5a51..6dbd3f418 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -167,7 +167,7 @@ func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { } // Handle metric reader override - if cfg.MetricsExporter != metricsExporterOtel { + if cfg.MetricsExporter != metricsExporterOtel && cfg.MetricsExporter != "otel-sdk" { return nil, fmt.Errorf("provided metrics operator %s is not supported. currently only support %s", cfg.MetricsExporter, metricsExporterOtel) } diff --git a/flagd/cmd/start.go b/flagd/cmd/start.go index 83745dd5a..1ac76cbc4 100644 --- a/flagd/cmd/start.go +++ b/flagd/cmd/start.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "log" + "os" "strings" "time" @@ -119,6 +120,32 @@ func bindFlags(flags *pflag.FlagSet) { _ = viper.BindPFlag(disableSyncMetadata, flags.Lookup(disableSyncMetadata)) } +func overrideMetricsExporter() string { + var metricsExporter = viper.GetString(metricsExporter) + if metricsExporter != "" { + return metricsExporter + } + + if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { + metricsExporter = "otel-sdk" + } + + return metricsExporter +} + +func overrideOtelUri() string { + var collectorUri = viper.GetString(otelCollectorURI) + if collectorUri != "" { + return collectorUri + } + + if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { + collectorUri = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + } + + return collectorUri +} + // startCmd represents the start command var startCmd = &cobra.Command{ Use: "start", @@ -171,13 +198,16 @@ var startCmd = &cobra.Command{ headerToContextKeyMappings[k] = v } + var metricsExporter = overrideMetricsExporter() + var collectorUri = overrideOtelUri() + // Build Runtime ----------------------------------------------------------- rt, err := runtime.FromConfig(logger, Version, runtime.Config{ CORS: viper.GetStringSlice(corsFlagName), - MetricExporter: viper.GetString(metricsExporter), + MetricExporter: metricsExporter, ManagementPort: viper.GetUint16(managementPortFlagName), OfrepServicePort: viper.GetUint16(ofrepPortFlagName), - OtelCollectorURI: viper.GetString(otelCollectorURI), + OtelCollectorURI: collectorUri, OtelCertPath: viper.GetString(otelCertPathFlagName), OtelKeyPath: viper.GetString(otelKeyPathFlagName), OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName), From 4a84cc74fd3e7bf793b12c87589b13b0b6333cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:21:54 +0100 Subject: [PATCH 2/7] feat: add support for OTLP headers and protocol in telemetry configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 2 ++ flagd/cmd/start.go | 12 ++++++++++++ flagd/pkg/runtime/from_config.go | 10 +++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index 6dbd3f418..cf7ece73c 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -38,6 +38,8 @@ type CollectorConfig struct { KeyPath string ReloadInterval time.Duration CAPath string + Headers string + Protocol string } // Config of the telemetry runtime. These are expected to be mapped to start-up arguments diff --git a/flagd/cmd/start.go b/flagd/cmd/start.go index 1ac76cbc4..faa12e3f5 100644 --- a/flagd/cmd/start.go +++ b/flagd/cmd/start.go @@ -146,6 +146,14 @@ func overrideOtelUri() string { return collectorUri } +func getOtelHeaders() string { + return os.Getenv("OTEL_EXPORTER_OTLP_HEADERS") +} + +func getOtelProtocol() string { + return os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") +} + // startCmd represents the start command var startCmd = &cobra.Command{ Use: "start", @@ -200,6 +208,8 @@ var startCmd = &cobra.Command{ var metricsExporter = overrideMetricsExporter() var collectorUri = overrideOtelUri() + var otelHeaders = getOtelHeaders() + var otelProtocol = getOtelProtocol() // Build Runtime ----------------------------------------------------------- rt, err := runtime.FromConfig(logger, Version, runtime.Config{ @@ -211,6 +221,8 @@ var startCmd = &cobra.Command{ OtelCertPath: viper.GetString(otelCertPathFlagName), OtelKeyPath: viper.GetString(otelKeyPathFlagName), OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName), + OtelHeaders: otelHeaders, + OtelProtocol: otelProtocol, OtelCAPath: viper.GetString(otelCAPathFlagName), ServiceCertPath: viper.GetString(serverCertPathFlagName), ServiceKeyPath: viper.GetString(serverKeyPathFlagName), diff --git a/flagd/pkg/runtime/from_config.go b/flagd/pkg/runtime/from_config.go index 08fcc5b0c..c31e66f0b 100644 --- a/flagd/pkg/runtime/from_config.go +++ b/flagd/pkg/runtime/from_config.go @@ -31,6 +31,8 @@ type Config struct { OtelCertPath string OtelKeyPath string OtelCAPath string + OtelHeaders string + OtelProtocol string OtelReloadInterval time.Duration ServiceCertPath string ServiceKeyPath string @@ -59,6 +61,8 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, KeyPath: config.OtelKeyPath, CAPath: config.OtelCAPath, ReloadInterval: config.OtelReloadInterval, + Headers: config.OtelHeaders, + Protocol: config.OtelProtocol, }, } @@ -105,9 +109,9 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, // ofrep service ofrepService, err := ofrep.NewOfrepService(jsonEvaluator, config.CORS, ofrep.SvcConfiguration{ - Logger: logger.WithFields(zap.String("component", "OFREPService")), - Port: config.OfrepServicePort, - ServiceName: svcName, + Logger: logger.WithFields(zap.String("component", "OFREPService")), + Port: config.OfrepServicePort, + ServiceName: svcName, MetricsRecorder: recorder, }, config.ContextValues, From f002fb4965aa07b3a842a6f90aa81fd25334c812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:53:56 +0100 Subject: [PATCH 3/7] feat: add timeout configuration and header parsing for OTLP in telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 58 ++++++++++++++++++++++++++++++-- flagd/cmd/start.go | 17 ++++++++++ flagd/pkg/runtime/from_config.go | 2 ++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index cf7ece73c..4a76918c8 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "fmt" "os" + "strings" "time" "connectrpc.com/connect" @@ -40,6 +41,7 @@ type CollectorConfig struct { CAPath string Headers string Protocol string + Timeout time.Duration } // Config of the telemetry runtime. These are expected to be mapped to start-up arguments @@ -162,6 +164,26 @@ func buildTransportCredentials(_ context.Context, cfg CollectorConfig) (credenti return creds, nil } +// parseOTelHeaders parses the OTEL_EXPORTER_OTLP_HEADERS format (key1=value1,key2=value2) +// into a map[string]string +func parseOTelHeaders(headersStr string) map[string]string { + headers := make(map[string]string) + if headersStr == "" { + return headers + } + + // Split by comma to get individual key=value pairs + pairs := strings.Split(headersStr, ",") + for _, pair := range pairs { + // Split by = to separate key and value + kv := strings.SplitN(strings.TrimSpace(pair), "=", 2) + if len(kv) == 2 { + headers[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + } + return headers +} + // buildMetricReader builds a metric reader based on provided configurations func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { if cfg.MetricsExporter == "" { @@ -191,8 +213,24 @@ func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { return nil, fmt.Errorf("error creating client connection: %w", err) } + // Build OTLP exporter options + exporterOpts := []otlpmetricgrpc.Option{ + otlpmetricgrpc.WithGRPCConn(conn), + } + + // Add headers if provided + if cfg.CollectorConfig.Headers != "" { + headers := parseOTelHeaders(cfg.CollectorConfig.Headers) + exporterOpts = append(exporterOpts, otlpmetricgrpc.WithHeaders(headers)) + } + + // Add timeout if provided + if cfg.CollectorConfig.Timeout > 0 { + exporterOpts = append(exporterOpts, otlpmetricgrpc.WithTimeout(cfg.CollectorConfig.Timeout)) + } + // Otel metric exporter - otelExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn)) + otelExporter, err := otlpmetricgrpc.New(ctx, exporterOpts...) if err != nil { return nil, fmt.Errorf("error creating otel metric exporter: %w", err) } @@ -213,7 +251,23 @@ func buildOtlpExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exp return nil, fmt.Errorf("error creating client connection: %w", err) } - traceClient := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn)) + // Build OTLP trace exporter options + traceOpts := []otlptracegrpc.Option{ + otlptracegrpc.WithGRPCConn(conn), + } + + // Add headers if provided + if cfg.Headers != "" { + headers := parseOTelHeaders(cfg.Headers) + traceOpts = append(traceOpts, otlptracegrpc.WithHeaders(headers)) + } + + // Add timeout if provided + if cfg.Timeout > 0 { + traceOpts = append(traceOpts, otlptracegrpc.WithTimeout(cfg.Timeout)) + } + + traceClient := otlptracegrpc.NewClient(traceOpts...) exporter, err := otlptrace.New(ctx, traceClient) if err != nil { return nil, fmt.Errorf("error starting otel exporter: %w", err) diff --git a/flagd/cmd/start.go b/flagd/cmd/start.go index faa12e3f5..2ae8baa07 100644 --- a/flagd/cmd/start.go +++ b/flagd/cmd/start.go @@ -154,6 +154,21 @@ func getOtelProtocol() string { return os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") } +func getOtelTimeout() time.Duration { + timeoutStr := os.Getenv("OTEL_EXPORTER_OTLP_TIMEOUT") + if timeoutStr == "" { + return 0 // No timeout set + } + + // OTEL_EXPORTER_OTLP_TIMEOUT is in milliseconds + timeout, err := time.ParseDuration(timeoutStr + "ms") + if err != nil { + // If parsing fails, return 0 + return 0 + } + return timeout +} + // startCmd represents the start command var startCmd = &cobra.Command{ Use: "start", @@ -210,6 +225,7 @@ var startCmd = &cobra.Command{ var collectorUri = overrideOtelUri() var otelHeaders = getOtelHeaders() var otelProtocol = getOtelProtocol() + var otelTimeout = getOtelTimeout() // Build Runtime ----------------------------------------------------------- rt, err := runtime.FromConfig(logger, Version, runtime.Config{ @@ -223,6 +239,7 @@ var startCmd = &cobra.Command{ OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName), OtelHeaders: otelHeaders, OtelProtocol: otelProtocol, + OtelTimeout: otelTimeout, OtelCAPath: viper.GetString(otelCAPathFlagName), ServiceCertPath: viper.GetString(serverCertPathFlagName), ServiceKeyPath: viper.GetString(serverKeyPathFlagName), diff --git a/flagd/pkg/runtime/from_config.go b/flagd/pkg/runtime/from_config.go index c31e66f0b..c736244d6 100644 --- a/flagd/pkg/runtime/from_config.go +++ b/flagd/pkg/runtime/from_config.go @@ -33,6 +33,7 @@ type Config struct { OtelCAPath string OtelHeaders string OtelProtocol string + OtelTimeout time.Duration OtelReloadInterval time.Duration ServiceCertPath string ServiceKeyPath string @@ -63,6 +64,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, ReloadInterval: config.OtelReloadInterval, Headers: config.OtelHeaders, Protocol: config.OtelProtocol, + Timeout: config.OtelTimeout, }, } From 76a4da4363eb3c3a0b0199a291760491b1e56197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:05:36 +0100 Subject: [PATCH 4/7] refactor: rename buildOtlpExporter to buildGrpcTraceExporter for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index 4a76918c8..fbaff4734 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -86,7 +86,7 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, return nil } - exporter, err := buildOtlpExporter(ctx, cfg.CollectorConfig) + exporter, err := buildGrpcTraceExporter(ctx, cfg.CollectorConfig) if err != nil { return err } @@ -238,8 +238,8 @@ func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { return metric.NewPeriodicReader(otelExporter), nil } -// buildOtlpExporter is a helper to build grpc backed otlp trace exporter -func buildOtlpExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { +// buildGrpcTraceExporter is a helper to build grpc backed otlp trace exporter +func buildGrpcTraceExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { transportCredentials, err := buildTransportCredentials(ctx, cfg) if err != nil { return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) From 6dd1609b616253cca6e8b144ee7ef0252b75f525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:23:56 +0100 Subject: [PATCH 5/7] refactor: rename BuildMetricsRecorder to BuildMetricsProvider for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 310 ++++++++++++++++++----------- core/pkg/telemetry/builder_test.go | 16 +- flagd/pkg/runtime/from_config.go | 2 +- 3 files changed, 205 insertions(+), 123 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index fbaff4734..f00f7c643 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -31,43 +31,50 @@ import ( const ( metricsExporterOtel = "otel" + metricsExporterSDK = "otel-sdk" ) +// CollectorConfig holds the configuration for connecting to an OpenTelemetry collector type CollectorConfig struct { - Target string - CertPath string - KeyPath string - ReloadInterval time.Duration - CAPath string - Headers string - Protocol string - Timeout time.Duration + Target string // The collector endpoint (e.g., "localhost:4317") + CertPath string // Path to the TLS certificate file + KeyPath string // Path to the TLS key file + CAPath string // Path to the CA certificate file + Headers string // Additional headers in OTEL format (key1=value1,key2=value2) + Protocol string // Protocol to use (e.g., "grpc", "http") + ReloadInterval time.Duration // Interval for reloading certificates + Timeout time.Duration // Timeout for exporter operations } // Config of the telemetry runtime. These are expected to be mapped to start-up arguments type Config struct { - MetricsExporter string - CollectorConfig CollectorConfig + MetricsExporter string // Type of metrics exporter ("otel" or empty for default) + CollectorConfig CollectorConfig // Configuration for the collector } +// RegisterErrorHandling sets up a global error handler for OpenTelemetry errors func RegisterErrorHandling(log *logger.Logger) { otel.SetErrorHandler(otelErrorsHandler{ logger: log, }) } -// BuildMetricsRecorder is a helper to build telemetry.MetricsRecorder based on configurations -func BuildMetricsRecorder( +// ============================================================================ +// Public API - Builders +// ============================================================================ + +// BuildMetricsProvider is a helper to build telemetry.MetricsRecorder based on configurations +func BuildMetricsProvider( ctx context.Context, svcName string, svcVersion string, config Config, ) (IMetricsRecorder, error) { - // Build metric reader based on configurations - mReader, err := buildMetricReader(ctx, config) + // Build metric exporter based on configurations + mReader, err := buildMetricExporter(ctx, config) if err != nil { - return nil, fmt.Errorf("failed to setup metric reader: %w", err) + return nil, fmt.Errorf("failed to setup metric exporter: %w", err) } // Build telemetry resource identifier - rsc, err := buildResourceFor(ctx, svcName, svcVersion) + rsc, err := buildResource(ctx, svcName, svcVersion) if err != nil { return nil, fmt.Errorf("failed to setup resource identifier: %w", err) } @@ -75,10 +82,10 @@ func BuildMetricsRecorder( return NewOTelRecorder(mReader, rsc, svcName), nil } -// BuildTraceProvider build and register the trace provider and propagator for the caller runtime. This method -// attempt to register a global TracerProvider backed by batch SpanProcessor.Config. CollectorTarget can be used to -// provide the grpc collector target. Providing empty target results in skipping provider & propagator registration. -// This results in tracers having NoopTracerProvider and propagator having No-Op TextMapPropagator performing no action +// BuildTraceProvider builds and registers the trace provider and propagator for the caller runtime. +// This method attempts to register a global TracerProvider backed by batch SpanProcessor. +// If CollectorConfig.Target is empty, provider & propagator registration is skipped, resulting in +// tracers having NoopTracerProvider and propagator having No-Op TextMapPropagator. func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, svcVersion string, cfg Config) error { if cfg.CollectorConfig.Target == "" { logger.Debug("skipping trace provider setup as collector target is not set." + @@ -88,12 +95,12 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, exporter, err := buildGrpcTraceExporter(ctx, cfg.CollectorConfig) if err != nil { - return err + return fmt.Errorf("failed to build trace exporter: %w", err) } - res, err := buildResourceFor(ctx, svc, svcVersion) + res, err := buildResource(ctx, svc, svcVersion) if err != nil { - return err + return fmt.Errorf("failed to build resource: %w", err) } provider := trace.NewTracerProvider( @@ -102,7 +109,10 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, trace.WithResource(res)) otel.SetTracerProvider(provider) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) return nil } @@ -114,7 +124,7 @@ func BuildConnectOptions(cfg Config) ([]connect.HandlerOption, error) { if cfg.CollectorConfig.Target != "" { interceptor, err := otelconnect.NewInterceptor(otelconnect.WithTrustRemote()) if err != nil { - return nil, fmt.Errorf("error creating interceptor, %w", err) + return nil, fmt.Errorf("error creating interceptor: %w", err) } options = append(options, connect.WithInterceptors(interceptor)) @@ -123,47 +133,96 @@ func BuildConnectOptions(cfg Config) ([]connect.HandlerOption, error) { return options, nil } +// ============================================================================ +// Internal Helpers - Transport & Credentials +// ============================================================================ + +// buildTransportCredentials creates gRPC transport credentials based on the collector configuration. +// Returns insecure credentials if no TLS configuration is provided. func buildTransportCredentials(_ context.Context, cfg CollectorConfig) (credentials.TransportCredentials, error) { - creds := insecure.NewCredentials() - if cfg.KeyPath != "" || cfg.CertPath != "" || cfg.CAPath != "" { - capool := x509.NewCertPool() - if cfg.CAPath != "" { - ca, err := os.ReadFile(cfg.CAPath) + // Use insecure credentials by default + if cfg.KeyPath == "" && cfg.CertPath == "" && cfg.CAPath == "" { + return insecure.NewCredentials(), nil + } + + // Build TLS configuration + capool, err := buildCAPool(cfg.CAPath) + if err != nil { + return nil, err + } + + reloader, err := buildCertReloader(cfg) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + RootCAs: capool, + MinVersion: tls.VersionTLS12, + GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { + certs, err := reloader.GetCertificate() if err != nil { - return nil, fmt.Errorf("can't read ca file from %s", cfg.CAPath) + return nil, fmt.Errorf("failed to reload certs: %w", err) } - if !capool.AppendCertsFromPEM(ca) { - return nil, fmt.Errorf("can't add CA '%s' to pool", cfg.CAPath) - } - } + return certs, nil + }, + } - reloader, err := certreloader.NewCertReloader(certreloader.Config{ - KeyPath: cfg.KeyPath, - CertPath: cfg.CertPath, - ReloadInterval: cfg.ReloadInterval, - }) - if err != nil { - return nil, fmt.Errorf("failed to create certreloader: %w", err) - } + return credentials.NewTLS(tlsConfig), nil +} - tlsConfig := &tls.Config{ - RootCAs: capool, - MinVersion: tls.VersionTLS12, - GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { - certs, err := reloader.GetCertificate() - if err != nil { - return nil, fmt.Errorf("failed to reload certs: %w", err) - } - return certs, nil - }, - } +// buildCAPool creates a certificate pool from the provided CA file path. +// Returns an empty pool if no CA path is provided. +func buildCAPool(caPath string) (*x509.CertPool, error) { + capool := x509.NewCertPool() + if caPath == "" { + return capool, nil + } + + ca, err := os.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("can't read ca file from %s: %w", caPath, err) + } + + if !capool.AppendCertsFromPEM(ca) { + return nil, fmt.Errorf("can't add CA '%s' to pool", caPath) + } + + return capool, nil +} + +// buildCertReloader creates a certificate reloader for automatic certificate rotation +func buildCertReloader(cfg CollectorConfig) (*certreloader.CertReloader, error) { + reloader, err := certreloader.NewCertReloader(certreloader.Config{ + KeyPath: cfg.KeyPath, + CertPath: cfg.CertPath, + ReloadInterval: cfg.ReloadInterval, + }) + if err != nil { + return nil, fmt.Errorf("failed to create certreloader: %w", err) + } + return reloader, nil +} - creds = credentials.NewTLS(tlsConfig) +// buildGrpcConnection creates a gRPC client connection with the appropriate credentials +func buildGrpcConnection(ctx context.Context, target string, cfg CollectorConfig) (*grpc.ClientConn, error) { + transportCredentials, err := buildTransportCredentials(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("failed to build transport credentials: %w", err) + } + + conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + return nil, fmt.Errorf("error creating client connection: %w", err) } - return creds, nil + return conn, nil } +// ============================================================================ +// Internal Helpers - Headers & Options +// ============================================================================ + // parseOTelHeaders parses the OTEL_EXPORTER_OTLP_HEADERS format (key1=value1,key2=value2) // into a map[string]string func parseOTelHeaders(headersStr string) map[string]string { @@ -184,108 +243,127 @@ func parseOTelHeaders(headersStr string) map[string]string { return headers } -// buildMetricReader builds a metric reader based on provided configurations -func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { +// ============================================================================ +// Internal Helpers - Metric Exporter +// ============================================================================ + +// buildMetricExporter builds a metric exporter based on provided configurations. +// Returns a Prometheus exporter by default, or an OTLP exporter if configured. +func buildMetricExporter(ctx context.Context, cfg Config) (metric.Reader, error) { + // Use default (Prometheus) if no exporter is specified if cfg.MetricsExporter == "" { - return buildDefaultMetricReader() + return buildDefaultMetricExporter() } - // Handle metric reader override - if cfg.MetricsExporter != metricsExporterOtel && cfg.MetricsExporter != "otel-sdk" { - return nil, fmt.Errorf("provided metrics operator %s is not supported. currently only support %s", - cfg.MetricsExporter, metricsExporterOtel) + // Validate exporter type + if !isValidMetricsExporter(cfg.MetricsExporter) { + return nil, fmt.Errorf("provided metrics exporter %s is not supported. currently only support %s or %s", + cfg.MetricsExporter, metricsExporterOtel, metricsExporterSDK) } - // Otel override require target configuration + // Validate collector configuration if cfg.CollectorConfig.Target == "" { - return nil, fmt.Errorf("metric exporter is set(%s) without providing otel collector target."+ - " collector target is required for this option", cfg.MetricsExporter) + return nil, fmt.Errorf("metric exporter is set(%s) without providing otel collector target. "+ + "collector target is required for this option", cfg.MetricsExporter) } - transportCredentials, err := buildTransportCredentials(ctx, cfg.CollectorConfig) + return buildOTLPMetricExporter(ctx, cfg.CollectorConfig) +} + +// isValidMetricsExporter checks if the provided exporter type is supported +func isValidMetricsExporter(exporter string) bool { + return exporter == metricsExporterOtel || exporter == metricsExporterSDK +} + +// buildOTLPMetricExporter creates an OTLP metric exporter with gRPC +func buildOTLPMetricExporter(ctx context.Context, cfg CollectorConfig) (metric.Reader, error) { + conn, err := buildGrpcConnection(ctx, cfg.Target, cfg) if err != nil { - return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) + return nil, fmt.Errorf("failed to create gRPC connection: %w", err) } - // Non-blocking, insecure grpc connection - conn, err := grpc.NewClient(cfg.CollectorConfig.Target, grpc.WithTransportCredentials(transportCredentials)) + exporterOpts := buildMetricExporterOptions(cfg, conn) + + otelExporter, err := otlpmetricgrpc.New(ctx, exporterOpts...) if err != nil { - return nil, fmt.Errorf("error creating client connection: %w", err) + return nil, fmt.Errorf("error creating otel metric exporter: %w", err) } - // Build OTLP exporter options - exporterOpts := []otlpmetricgrpc.Option{ + return metric.NewPeriodicReader(otelExporter), nil +} + +// buildMetricExporterOptions builds the options for OTLP metric exporter +func buildMetricExporterOptions(cfg CollectorConfig, conn *grpc.ClientConn) []otlpmetricgrpc.Option { + opts := []otlpmetricgrpc.Option{ otlpmetricgrpc.WithGRPCConn(conn), } - // Add headers if provided - if cfg.CollectorConfig.Headers != "" { - headers := parseOTelHeaders(cfg.CollectorConfig.Headers) - exporterOpts = append(exporterOpts, otlpmetricgrpc.WithHeaders(headers)) + if cfg.Headers != "" { + headers := parseOTelHeaders(cfg.Headers) + opts = append(opts, otlpmetricgrpc.WithHeaders(headers)) } - // Add timeout if provided - if cfg.CollectorConfig.Timeout > 0 { - exporterOpts = append(exporterOpts, otlpmetricgrpc.WithTimeout(cfg.CollectorConfig.Timeout)) + if cfg.Timeout > 0 { + opts = append(opts, otlpmetricgrpc.WithTimeout(cfg.Timeout)) } - // Otel metric exporter - otelExporter, err := otlpmetricgrpc.New(ctx, exporterOpts...) + return opts +} + +// buildDefaultMetricExporter provides the default metric exporter (Prometheus) +func buildDefaultMetricExporter() (metric.Reader, error) { + p, err := prometheus.New() if err != nil { - return nil, fmt.Errorf("error creating otel metric exporter: %w", err) + return nil, fmt.Errorf("unable to create default metric exporter: %w", err) } - - return metric.NewPeriodicReader(otelExporter), nil + return p, nil } -// buildGrpcTraceExporter is a helper to build grpc backed otlp trace exporter +// ============================================================================ +// Internal Helpers - Trace Exporter +// ============================================================================ + +// buildGrpcTraceExporter builds a gRPC-backed OTLP trace exporter func buildGrpcTraceExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { - transportCredentials, err := buildTransportCredentials(ctx, cfg) + conn, err := buildGrpcConnection(ctx, cfg.Target, cfg) if err != nil { - return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) + return nil, fmt.Errorf("failed to create gRPC connection: %w", err) } - // Non-blocking, grpc connection - conn, err := grpc.NewClient(cfg.Target, grpc.WithTransportCredentials(transportCredentials)) + traceOpts := buildTraceExporterOptions(cfg, conn) + + traceClient := otlptracegrpc.NewClient(traceOpts...) + exporter, err := otlptrace.New(ctx, traceClient) if err != nil { - return nil, fmt.Errorf("error creating client connection: %w", err) + return nil, fmt.Errorf("error creating otel trace exporter: %w", err) } + return exporter, nil +} - // Build OTLP trace exporter options - traceOpts := []otlptracegrpc.Option{ +// buildTraceExporterOptions builds the options for OTLP trace exporter +func buildTraceExporterOptions(cfg CollectorConfig, conn *grpc.ClientConn) []otlptracegrpc.Option { + opts := []otlptracegrpc.Option{ otlptracegrpc.WithGRPCConn(conn), } - // Add headers if provided if cfg.Headers != "" { headers := parseOTelHeaders(cfg.Headers) - traceOpts = append(traceOpts, otlptracegrpc.WithHeaders(headers)) + opts = append(opts, otlptracegrpc.WithHeaders(headers)) } - // Add timeout if provided if cfg.Timeout > 0 { - traceOpts = append(traceOpts, otlptracegrpc.WithTimeout(cfg.Timeout)) + opts = append(opts, otlptracegrpc.WithTimeout(cfg.Timeout)) } - traceClient := otlptracegrpc.NewClient(traceOpts...) - exporter, err := otlptrace.New(ctx, traceClient) - if err != nil { - return nil, fmt.Errorf("error starting otel exporter: %w", err) - } - return exporter, nil + return opts } -// buildDefaultMetricReader provides the default metric reader -func buildDefaultMetricReader() (metric.Reader, error) { - p, err := prometheus.New() - if err != nil { - return nil, fmt.Errorf("unable to create default metric reader: %w", err) - } - return p, nil -} +// ============================================================================ +// Internal Helpers - Resource +// ============================================================================ -// buildResourceFor builds a resource identifier with set of resources and service key as attributes -func buildResourceFor(ctx context.Context, serviceName string, serviceVersion string) (*resource.Resource, error) { +// buildResource builds a resource identifier with set of resources and service key as attributes +func buildResource(ctx context.Context, serviceName string, serviceVersion string) (*resource.Resource, error) { r, err := resource.New( ctx, resource.WithOS(), @@ -302,7 +380,11 @@ func buildResourceFor(ctx context.Context, serviceName string, serviceVersion st return r, nil } -// OTelErrorsHandler is a custom error interceptor for OpenTelemetry +// ============================================================================ +// Error Handler +// ============================================================================ + +// otelErrorsHandler is a custom error interceptor for OpenTelemetry type otelErrorsHandler struct { logger *logger.Logger } diff --git a/core/pkg/telemetry/builder_test.go b/core/pkg/telemetry/builder_test.go index c6253012b..c4c2fa239 100644 --- a/core/pkg/telemetry/builder_test.go +++ b/core/pkg/telemetry/builder_test.go @@ -17,9 +17,9 @@ import ( "go.uber.org/zap/zaptest/observer" ) -func TestBuildMetricsRecorder(t *testing.T) { +func TestBuildMetricsProvider(t *testing.T) { // Simple happy-path test - recorder, err := BuildMetricsRecorder(context.Background(), "service", "0.0.1", Config{ + recorder, err := BuildMetricsProvider(context.Background(), "service", "0.0.1", Config{ MetricsExporter: "otel", CollectorConfig: CollectorConfig{ Target: "localhost:8080", @@ -30,7 +30,7 @@ func TestBuildMetricsRecorder(t *testing.T) { require.NotNilf(t, recorder, "expected recorder to be non-nil") } -func TestBuildMetricReader(t *testing.T) { +func TestBuildMetricExporter(t *testing.T) { gCtx := context.TODO() tests := []struct { @@ -39,7 +39,7 @@ func TestBuildMetricReader(t *testing.T) { error bool }{ { - name: "Default configurations produce default reader", + name: "Default configurations produce default exporter", cfg: Config{}, error: false, }, @@ -73,7 +73,7 @@ func TestBuildMetricReader(t *testing.T) { } for _, test := range tests { - reader, err := buildMetricReader(gCtx, test.cfg) + reader, err := buildMetricExporter(gCtx, test.cfg) if test.error { require.NotNil(t, err, "test %s expected non-nil error", test.name) @@ -81,7 +81,7 @@ func TestBuildMetricReader(t *testing.T) { } require.Nilf(t, err, "test %s expected no error, but got: %v", test.name, err) - require.NotNil(t, reader, "test %s expected non-nil reader", test.name) + require.NotNil(t, reader, "test %s expected non-nil exporter", test.name) } } @@ -157,7 +157,7 @@ func TestBuildResourceFor(t *testing.T) { svc := "testSvc" svcVersion := "0.0.1" - resource, err := buildResourceFor(context.Background(), svc, svcVersion) + resource, err := buildResource(context.Background(), svc, svcVersion) require.Nil(t, err, "expected no error, but got: %v", err) attributes := resource.Attributes() @@ -179,7 +179,7 @@ func TestErrorIntercepted(t *testing.T) { log := logger.NewLogger(observedLogger, true) RegisterErrorHandling(log) - // configure a metric reader with an exporter that only returns error + // configure a metric exporter with an exporter that only returns error reader := metric.NewPeriodicReader(&errorExp{}, metric.WithInterval(1*time.Millisecond)) rs := resource.NewWithAttributes("testSchema") NewOTelRecorder(reader, rs, "testSvc") diff --git a/flagd/pkg/runtime/from_config.go b/flagd/pkg/runtime/from_config.go index c736244d6..6aea92bee 100644 --- a/flagd/pkg/runtime/from_config.go +++ b/flagd/pkg/runtime/from_config.go @@ -79,7 +79,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, } // build metrics recorder with startup configurations - recorder, err := telemetry.BuildMetricsRecorder(context.Background(), svcName, version, telCfg) + recorder, err := telemetry.BuildMetricsProvider(context.Background(), svcName, version, telCfg) if err != nil { // log the error but continue logger.Error(fmt.Sprintf("error building metrics recorder: %v", err)) From ab096f2c303af7c324461f9b2e7d53e75eddbe8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:36:35 +0100 Subject: [PATCH 6/7] Revert implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/pkg/telemetry/builder.go | 320 ++++++++--------------------- core/pkg/telemetry/builder_test.go | 16 +- flagd/cmd/start.go | 63 +----- flagd/pkg/runtime/from_config.go | 14 +- 4 files changed, 105 insertions(+), 308 deletions(-) diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index f00f7c643..55baf5a51 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -6,7 +6,6 @@ import ( "crypto/x509" "fmt" "os" - "strings" "time" "connectrpc.com/connect" @@ -31,50 +30,40 @@ import ( const ( metricsExporterOtel = "otel" - metricsExporterSDK = "otel-sdk" ) -// CollectorConfig holds the configuration for connecting to an OpenTelemetry collector type CollectorConfig struct { - Target string // The collector endpoint (e.g., "localhost:4317") - CertPath string // Path to the TLS certificate file - KeyPath string // Path to the TLS key file - CAPath string // Path to the CA certificate file - Headers string // Additional headers in OTEL format (key1=value1,key2=value2) - Protocol string // Protocol to use (e.g., "grpc", "http") - ReloadInterval time.Duration // Interval for reloading certificates - Timeout time.Duration // Timeout for exporter operations + Target string + CertPath string + KeyPath string + ReloadInterval time.Duration + CAPath string } // Config of the telemetry runtime. These are expected to be mapped to start-up arguments type Config struct { - MetricsExporter string // Type of metrics exporter ("otel" or empty for default) - CollectorConfig CollectorConfig // Configuration for the collector + MetricsExporter string + CollectorConfig CollectorConfig } -// RegisterErrorHandling sets up a global error handler for OpenTelemetry errors func RegisterErrorHandling(log *logger.Logger) { otel.SetErrorHandler(otelErrorsHandler{ logger: log, }) } -// ============================================================================ -// Public API - Builders -// ============================================================================ - -// BuildMetricsProvider is a helper to build telemetry.MetricsRecorder based on configurations -func BuildMetricsProvider( +// BuildMetricsRecorder is a helper to build telemetry.MetricsRecorder based on configurations +func BuildMetricsRecorder( ctx context.Context, svcName string, svcVersion string, config Config, ) (IMetricsRecorder, error) { - // Build metric exporter based on configurations - mReader, err := buildMetricExporter(ctx, config) + // Build metric reader based on configurations + mReader, err := buildMetricReader(ctx, config) if err != nil { - return nil, fmt.Errorf("failed to setup metric exporter: %w", err) + return nil, fmt.Errorf("failed to setup metric reader: %w", err) } // Build telemetry resource identifier - rsc, err := buildResource(ctx, svcName, svcVersion) + rsc, err := buildResourceFor(ctx, svcName, svcVersion) if err != nil { return nil, fmt.Errorf("failed to setup resource identifier: %w", err) } @@ -82,10 +71,10 @@ func BuildMetricsProvider( return NewOTelRecorder(mReader, rsc, svcName), nil } -// BuildTraceProvider builds and registers the trace provider and propagator for the caller runtime. -// This method attempts to register a global TracerProvider backed by batch SpanProcessor. -// If CollectorConfig.Target is empty, provider & propagator registration is skipped, resulting in -// tracers having NoopTracerProvider and propagator having No-Op TextMapPropagator. +// BuildTraceProvider build and register the trace provider and propagator for the caller runtime. This method +// attempt to register a global TracerProvider backed by batch SpanProcessor.Config. CollectorTarget can be used to +// provide the grpc collector target. Providing empty target results in skipping provider & propagator registration. +// This results in tracers having NoopTracerProvider and propagator having No-Op TextMapPropagator performing no action func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, svcVersion string, cfg Config) error { if cfg.CollectorConfig.Target == "" { logger.Debug("skipping trace provider setup as collector target is not set." + @@ -93,14 +82,14 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, return nil } - exporter, err := buildGrpcTraceExporter(ctx, cfg.CollectorConfig) + exporter, err := buildOtlpExporter(ctx, cfg.CollectorConfig) if err != nil { - return fmt.Errorf("failed to build trace exporter: %w", err) + return err } - res, err := buildResource(ctx, svc, svcVersion) + res, err := buildResourceFor(ctx, svc, svcVersion) if err != nil { - return fmt.Errorf("failed to build resource: %w", err) + return err } provider := trace.NewTracerProvider( @@ -109,10 +98,7 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, trace.WithResource(res)) otel.SetTracerProvider(provider) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( - propagation.TraceContext{}, - propagation.Baggage{}, - )) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) return nil } @@ -124,7 +110,7 @@ func BuildConnectOptions(cfg Config) ([]connect.HandlerOption, error) { if cfg.CollectorConfig.Target != "" { interceptor, err := otelconnect.NewInterceptor(otelconnect.WithTrustRemote()) if err != nil { - return nil, fmt.Errorf("error creating interceptor: %w", err) + return nil, fmt.Errorf("error creating interceptor, %w", err) } options = append(options, connect.WithInterceptors(interceptor)) @@ -133,158 +119,78 @@ func BuildConnectOptions(cfg Config) ([]connect.HandlerOption, error) { return options, nil } -// ============================================================================ -// Internal Helpers - Transport & Credentials -// ============================================================================ - -// buildTransportCredentials creates gRPC transport credentials based on the collector configuration. -// Returns insecure credentials if no TLS configuration is provided. func buildTransportCredentials(_ context.Context, cfg CollectorConfig) (credentials.TransportCredentials, error) { - // Use insecure credentials by default - if cfg.KeyPath == "" && cfg.CertPath == "" && cfg.CAPath == "" { - return insecure.NewCredentials(), nil - } - - // Build TLS configuration - capool, err := buildCAPool(cfg.CAPath) - if err != nil { - return nil, err - } - - reloader, err := buildCertReloader(cfg) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{ - RootCAs: capool, - MinVersion: tls.VersionTLS12, - GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { - certs, err := reloader.GetCertificate() + creds := insecure.NewCredentials() + if cfg.KeyPath != "" || cfg.CertPath != "" || cfg.CAPath != "" { + capool := x509.NewCertPool() + if cfg.CAPath != "" { + ca, err := os.ReadFile(cfg.CAPath) if err != nil { - return nil, fmt.Errorf("failed to reload certs: %w", err) + return nil, fmt.Errorf("can't read ca file from %s", cfg.CAPath) } - return certs, nil - }, - } - - return credentials.NewTLS(tlsConfig), nil -} - -// buildCAPool creates a certificate pool from the provided CA file path. -// Returns an empty pool if no CA path is provided. -func buildCAPool(caPath string) (*x509.CertPool, error) { - capool := x509.NewCertPool() - if caPath == "" { - return capool, nil - } - - ca, err := os.ReadFile(caPath) - if err != nil { - return nil, fmt.Errorf("can't read ca file from %s: %w", caPath, err) - } - - if !capool.AppendCertsFromPEM(ca) { - return nil, fmt.Errorf("can't add CA '%s' to pool", caPath) - } - - return capool, nil -} - -// buildCertReloader creates a certificate reloader for automatic certificate rotation -func buildCertReloader(cfg CollectorConfig) (*certreloader.CertReloader, error) { - reloader, err := certreloader.NewCertReloader(certreloader.Config{ - KeyPath: cfg.KeyPath, - CertPath: cfg.CertPath, - ReloadInterval: cfg.ReloadInterval, - }) - if err != nil { - return nil, fmt.Errorf("failed to create certreloader: %w", err) - } - return reloader, nil -} - -// buildGrpcConnection creates a gRPC client connection with the appropriate credentials -func buildGrpcConnection(ctx context.Context, target string, cfg CollectorConfig) (*grpc.ClientConn, error) { - transportCredentials, err := buildTransportCredentials(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("failed to build transport credentials: %w", err) - } - - conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(transportCredentials)) - if err != nil { - return nil, fmt.Errorf("error creating client connection: %w", err) - } + if !capool.AppendCertsFromPEM(ca) { + return nil, fmt.Errorf("can't add CA '%s' to pool", cfg.CAPath) + } + } - return conn, nil -} + reloader, err := certreloader.NewCertReloader(certreloader.Config{ + KeyPath: cfg.KeyPath, + CertPath: cfg.CertPath, + ReloadInterval: cfg.ReloadInterval, + }) + if err != nil { + return nil, fmt.Errorf("failed to create certreloader: %w", err) + } -// ============================================================================ -// Internal Helpers - Headers & Options -// ============================================================================ + tlsConfig := &tls.Config{ + RootCAs: capool, + MinVersion: tls.VersionTLS12, + GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { + certs, err := reloader.GetCertificate() + if err != nil { + return nil, fmt.Errorf("failed to reload certs: %w", err) + } + return certs, nil + }, + } -// parseOTelHeaders parses the OTEL_EXPORTER_OTLP_HEADERS format (key1=value1,key2=value2) -// into a map[string]string -func parseOTelHeaders(headersStr string) map[string]string { - headers := make(map[string]string) - if headersStr == "" { - return headers + creds = credentials.NewTLS(tlsConfig) } - // Split by comma to get individual key=value pairs - pairs := strings.Split(headersStr, ",") - for _, pair := range pairs { - // Split by = to separate key and value - kv := strings.SplitN(strings.TrimSpace(pair), "=", 2) - if len(kv) == 2 { - headers[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) - } - } - return headers + return creds, nil } -// ============================================================================ -// Internal Helpers - Metric Exporter -// ============================================================================ - -// buildMetricExporter builds a metric exporter based on provided configurations. -// Returns a Prometheus exporter by default, or an OTLP exporter if configured. -func buildMetricExporter(ctx context.Context, cfg Config) (metric.Reader, error) { - // Use default (Prometheus) if no exporter is specified +// buildMetricReader builds a metric reader based on provided configurations +func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { if cfg.MetricsExporter == "" { - return buildDefaultMetricExporter() + return buildDefaultMetricReader() } - // Validate exporter type - if !isValidMetricsExporter(cfg.MetricsExporter) { - return nil, fmt.Errorf("provided metrics exporter %s is not supported. currently only support %s or %s", - cfg.MetricsExporter, metricsExporterOtel, metricsExporterSDK) + // Handle metric reader override + if cfg.MetricsExporter != metricsExporterOtel { + return nil, fmt.Errorf("provided metrics operator %s is not supported. currently only support %s", + cfg.MetricsExporter, metricsExporterOtel) } - // Validate collector configuration + // Otel override require target configuration if cfg.CollectorConfig.Target == "" { - return nil, fmt.Errorf("metric exporter is set(%s) without providing otel collector target. "+ - "collector target is required for this option", cfg.MetricsExporter) + return nil, fmt.Errorf("metric exporter is set(%s) without providing otel collector target."+ + " collector target is required for this option", cfg.MetricsExporter) } - return buildOTLPMetricExporter(ctx, cfg.CollectorConfig) -} - -// isValidMetricsExporter checks if the provided exporter type is supported -func isValidMetricsExporter(exporter string) bool { - return exporter == metricsExporterOtel || exporter == metricsExporterSDK -} - -// buildOTLPMetricExporter creates an OTLP metric exporter with gRPC -func buildOTLPMetricExporter(ctx context.Context, cfg CollectorConfig) (metric.Reader, error) { - conn, err := buildGrpcConnection(ctx, cfg.Target, cfg) + transportCredentials, err := buildTransportCredentials(ctx, cfg.CollectorConfig) if err != nil { - return nil, fmt.Errorf("failed to create gRPC connection: %w", err) + return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) } - exporterOpts := buildMetricExporterOptions(cfg, conn) + // Non-blocking, insecure grpc connection + conn, err := grpc.NewClient(cfg.CollectorConfig.Target, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + return nil, fmt.Errorf("error creating client connection: %w", err) + } - otelExporter, err := otlpmetricgrpc.New(ctx, exporterOpts...) + // Otel metric exporter + otelExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn)) if err != nil { return nil, fmt.Errorf("error creating otel metric exporter: %w", err) } @@ -292,78 +198,38 @@ func buildOTLPMetricExporter(ctx context.Context, cfg CollectorConfig) (metric.R return metric.NewPeriodicReader(otelExporter), nil } -// buildMetricExporterOptions builds the options for OTLP metric exporter -func buildMetricExporterOptions(cfg CollectorConfig, conn *grpc.ClientConn) []otlpmetricgrpc.Option { - opts := []otlpmetricgrpc.Option{ - otlpmetricgrpc.WithGRPCConn(conn), - } - - if cfg.Headers != "" { - headers := parseOTelHeaders(cfg.Headers) - opts = append(opts, otlpmetricgrpc.WithHeaders(headers)) - } - - if cfg.Timeout > 0 { - opts = append(opts, otlpmetricgrpc.WithTimeout(cfg.Timeout)) - } - - return opts -} - -// buildDefaultMetricExporter provides the default metric exporter (Prometheus) -func buildDefaultMetricExporter() (metric.Reader, error) { - p, err := prometheus.New() +// buildOtlpExporter is a helper to build grpc backed otlp trace exporter +func buildOtlpExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { + transportCredentials, err := buildTransportCredentials(ctx, cfg) if err != nil { - return nil, fmt.Errorf("unable to create default metric exporter: %w", err) + return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) } - return p, nil -} - -// ============================================================================ -// Internal Helpers - Trace Exporter -// ============================================================================ -// buildGrpcTraceExporter builds a gRPC-backed OTLP trace exporter -func buildGrpcTraceExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { - conn, err := buildGrpcConnection(ctx, cfg.Target, cfg) + // Non-blocking, grpc connection + conn, err := grpc.NewClient(cfg.Target, grpc.WithTransportCredentials(transportCredentials)) if err != nil { - return nil, fmt.Errorf("failed to create gRPC connection: %w", err) + return nil, fmt.Errorf("error creating client connection: %w", err) } - traceOpts := buildTraceExporterOptions(cfg, conn) - - traceClient := otlptracegrpc.NewClient(traceOpts...) + traceClient := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn)) exporter, err := otlptrace.New(ctx, traceClient) if err != nil { - return nil, fmt.Errorf("error creating otel trace exporter: %w", err) + return nil, fmt.Errorf("error starting otel exporter: %w", err) } return exporter, nil } -// buildTraceExporterOptions builds the options for OTLP trace exporter -func buildTraceExporterOptions(cfg CollectorConfig, conn *grpc.ClientConn) []otlptracegrpc.Option { - opts := []otlptracegrpc.Option{ - otlptracegrpc.WithGRPCConn(conn), - } - - if cfg.Headers != "" { - headers := parseOTelHeaders(cfg.Headers) - opts = append(opts, otlptracegrpc.WithHeaders(headers)) - } - - if cfg.Timeout > 0 { - opts = append(opts, otlptracegrpc.WithTimeout(cfg.Timeout)) +// buildDefaultMetricReader provides the default metric reader +func buildDefaultMetricReader() (metric.Reader, error) { + p, err := prometheus.New() + if err != nil { + return nil, fmt.Errorf("unable to create default metric reader: %w", err) } - - return opts + return p, nil } -// ============================================================================ -// Internal Helpers - Resource -// ============================================================================ - -// buildResource builds a resource identifier with set of resources and service key as attributes -func buildResource(ctx context.Context, serviceName string, serviceVersion string) (*resource.Resource, error) { +// buildResourceFor builds a resource identifier with set of resources and service key as attributes +func buildResourceFor(ctx context.Context, serviceName string, serviceVersion string) (*resource.Resource, error) { r, err := resource.New( ctx, resource.WithOS(), @@ -380,11 +246,7 @@ func buildResource(ctx context.Context, serviceName string, serviceVersion strin return r, nil } -// ============================================================================ -// Error Handler -// ============================================================================ - -// otelErrorsHandler is a custom error interceptor for OpenTelemetry +// OTelErrorsHandler is a custom error interceptor for OpenTelemetry type otelErrorsHandler struct { logger *logger.Logger } diff --git a/core/pkg/telemetry/builder_test.go b/core/pkg/telemetry/builder_test.go index c4c2fa239..c6253012b 100644 --- a/core/pkg/telemetry/builder_test.go +++ b/core/pkg/telemetry/builder_test.go @@ -17,9 +17,9 @@ import ( "go.uber.org/zap/zaptest/observer" ) -func TestBuildMetricsProvider(t *testing.T) { +func TestBuildMetricsRecorder(t *testing.T) { // Simple happy-path test - recorder, err := BuildMetricsProvider(context.Background(), "service", "0.0.1", Config{ + recorder, err := BuildMetricsRecorder(context.Background(), "service", "0.0.1", Config{ MetricsExporter: "otel", CollectorConfig: CollectorConfig{ Target: "localhost:8080", @@ -30,7 +30,7 @@ func TestBuildMetricsProvider(t *testing.T) { require.NotNilf(t, recorder, "expected recorder to be non-nil") } -func TestBuildMetricExporter(t *testing.T) { +func TestBuildMetricReader(t *testing.T) { gCtx := context.TODO() tests := []struct { @@ -39,7 +39,7 @@ func TestBuildMetricExporter(t *testing.T) { error bool }{ { - name: "Default configurations produce default exporter", + name: "Default configurations produce default reader", cfg: Config{}, error: false, }, @@ -73,7 +73,7 @@ func TestBuildMetricExporter(t *testing.T) { } for _, test := range tests { - reader, err := buildMetricExporter(gCtx, test.cfg) + reader, err := buildMetricReader(gCtx, test.cfg) if test.error { require.NotNil(t, err, "test %s expected non-nil error", test.name) @@ -81,7 +81,7 @@ func TestBuildMetricExporter(t *testing.T) { } require.Nilf(t, err, "test %s expected no error, but got: %v", test.name, err) - require.NotNil(t, reader, "test %s expected non-nil exporter", test.name) + require.NotNil(t, reader, "test %s expected non-nil reader", test.name) } } @@ -157,7 +157,7 @@ func TestBuildResourceFor(t *testing.T) { svc := "testSvc" svcVersion := "0.0.1" - resource, err := buildResource(context.Background(), svc, svcVersion) + resource, err := buildResourceFor(context.Background(), svc, svcVersion) require.Nil(t, err, "expected no error, but got: %v", err) attributes := resource.Attributes() @@ -179,7 +179,7 @@ func TestErrorIntercepted(t *testing.T) { log := logger.NewLogger(observedLogger, true) RegisterErrorHandling(log) - // configure a metric exporter with an exporter that only returns error + // configure a metric reader with an exporter that only returns error reader := metric.NewPeriodicReader(&errorExp{}, metric.WithInterval(1*time.Millisecond)) rs := resource.NewWithAttributes("testSchema") NewOTelRecorder(reader, rs, "testSvc") diff --git a/flagd/cmd/start.go b/flagd/cmd/start.go index 2ae8baa07..83745dd5a 100644 --- a/flagd/cmd/start.go +++ b/flagd/cmd/start.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "log" - "os" "strings" "time" @@ -120,55 +119,6 @@ func bindFlags(flags *pflag.FlagSet) { _ = viper.BindPFlag(disableSyncMetadata, flags.Lookup(disableSyncMetadata)) } -func overrideMetricsExporter() string { - var metricsExporter = viper.GetString(metricsExporter) - if metricsExporter != "" { - return metricsExporter - } - - if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { - metricsExporter = "otel-sdk" - } - - return metricsExporter -} - -func overrideOtelUri() string { - var collectorUri = viper.GetString(otelCollectorURI) - if collectorUri != "" { - return collectorUri - } - - if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { - collectorUri = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") - } - - return collectorUri -} - -func getOtelHeaders() string { - return os.Getenv("OTEL_EXPORTER_OTLP_HEADERS") -} - -func getOtelProtocol() string { - return os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") -} - -func getOtelTimeout() time.Duration { - timeoutStr := os.Getenv("OTEL_EXPORTER_OTLP_TIMEOUT") - if timeoutStr == "" { - return 0 // No timeout set - } - - // OTEL_EXPORTER_OTLP_TIMEOUT is in milliseconds - timeout, err := time.ParseDuration(timeoutStr + "ms") - if err != nil { - // If parsing fails, return 0 - return 0 - } - return timeout -} - // startCmd represents the start command var startCmd = &cobra.Command{ Use: "start", @@ -221,25 +171,16 @@ var startCmd = &cobra.Command{ headerToContextKeyMappings[k] = v } - var metricsExporter = overrideMetricsExporter() - var collectorUri = overrideOtelUri() - var otelHeaders = getOtelHeaders() - var otelProtocol = getOtelProtocol() - var otelTimeout = getOtelTimeout() - // Build Runtime ----------------------------------------------------------- rt, err := runtime.FromConfig(logger, Version, runtime.Config{ CORS: viper.GetStringSlice(corsFlagName), - MetricExporter: metricsExporter, + MetricExporter: viper.GetString(metricsExporter), ManagementPort: viper.GetUint16(managementPortFlagName), OfrepServicePort: viper.GetUint16(ofrepPortFlagName), - OtelCollectorURI: collectorUri, + OtelCollectorURI: viper.GetString(otelCollectorURI), OtelCertPath: viper.GetString(otelCertPathFlagName), OtelKeyPath: viper.GetString(otelKeyPathFlagName), OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName), - OtelHeaders: otelHeaders, - OtelProtocol: otelProtocol, - OtelTimeout: otelTimeout, OtelCAPath: viper.GetString(otelCAPathFlagName), ServiceCertPath: viper.GetString(serverCertPathFlagName), ServiceKeyPath: viper.GetString(serverKeyPathFlagName), diff --git a/flagd/pkg/runtime/from_config.go b/flagd/pkg/runtime/from_config.go index 6aea92bee..08fcc5b0c 100644 --- a/flagd/pkg/runtime/from_config.go +++ b/flagd/pkg/runtime/from_config.go @@ -31,9 +31,6 @@ type Config struct { OtelCertPath string OtelKeyPath string OtelCAPath string - OtelHeaders string - OtelProtocol string - OtelTimeout time.Duration OtelReloadInterval time.Duration ServiceCertPath string ServiceKeyPath string @@ -62,9 +59,6 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, KeyPath: config.OtelKeyPath, CAPath: config.OtelCAPath, ReloadInterval: config.OtelReloadInterval, - Headers: config.OtelHeaders, - Protocol: config.OtelProtocol, - Timeout: config.OtelTimeout, }, } @@ -79,7 +73,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, } // build metrics recorder with startup configurations - recorder, err := telemetry.BuildMetricsProvider(context.Background(), svcName, version, telCfg) + recorder, err := telemetry.BuildMetricsRecorder(context.Background(), svcName, version, telCfg) if err != nil { // log the error but continue logger.Error(fmt.Sprintf("error building metrics recorder: %v", err)) @@ -111,9 +105,9 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, // ofrep service ofrepService, err := ofrep.NewOfrepService(jsonEvaluator, config.CORS, ofrep.SvcConfiguration{ - Logger: logger.WithFields(zap.String("component", "OFREPService")), - Port: config.OfrepServicePort, - ServiceName: svcName, + Logger: logger.WithFields(zap.String("component", "OFREPService")), + Port: config.OfrepServicePort, + ServiceName: svcName, MetricsRecorder: recorder, }, config.ContextValues, From 6d015b6928923161f00defcdcf152e75e967a398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:35:59 +0100 Subject: [PATCH 7/7] Refactor telemetry builder to use autoexport for OpenTelemetry configuration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- core/go.mod | 75 +++++++------ core/go.sum | 156 +++++++++++++++----------- core/pkg/telemetry/builder.go | 169 ++++++++--------------------- core/pkg/telemetry/builder_test.go | 23 +--- 4 files changed, 186 insertions(+), 237 deletions(-) diff --git a/core/go.mod b/core/go.mod index ebf0b4e0b..8b5573c61 100644 --- a/core/go.mod +++ b/core/go.mod @@ -16,36 +16,35 @@ require ( github.com/hashicorp/go-memdb v1.3.5 github.com/open-feature/flagd-schemas v0.2.13 github.com/open-feature/open-feature-operator/apis v0.2.45 - github.com/prometheus/client_golang v1.22.0 + github.com/prometheus/client_golang v1.23.0 github.com/robfig/cron v1.2.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/twmb/murmur3 v1.1.8 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 - go.opentelemetry.io/otel/exporters/prometheus v0.59.0 - go.opentelemetry.io/otel/metric v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 - go.opentelemetry.io/otel/sdk/metric v1.37.0 - go.opentelemetry.io/otel/trace v1.37.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/mock v0.5.2 go.uber.org/zap v1.27.0 gocloud.dev v0.42.0 - golang.org/x/crypto v0.39.0 + golang.org/x/crypto v0.41.0 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac - golang.org/x/mod v0.25.0 - golang.org/x/sync v0.15.0 - google.golang.org/grpc v1.73.0 - google.golang.org/protobuf v1.36.6 + golang.org/x/mod v0.26.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.16.0 + google.golang.org/grpc v1.75.0 + google.golang.org/protobuf v1.36.8 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.33.2 k8s.io/client-go v0.33.2 ) require ( - cel.dev/expr v0.23.0 // indirect + cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.121.1 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -60,7 +59,7 @@ require ( github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/aws/aws-sdk-go v1.55.6 // indirect @@ -85,9 +84,9 @@ require ( github.com/aws/smithy-go v1.22.3 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect @@ -95,7 +94,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -109,7 +108,8 @@ require ( github.com/google/wire v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect @@ -127,7 +127,8 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -137,23 +138,35 @@ require ( github.com/zeebo/errs v1.4.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.235.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.33.2 // indirect diff --git a/core/go.sum b/core/go.sum index b8a5e5d4a..031be1a8d 100644 --- a/core/go.sum +++ b/core/go.sum @@ -2,8 +2,8 @@ buf.build/gen/go/open-feature/flagd/grpc/go v1.5.1-20250529171031-ebdc14163473.2 buf.build/gen/go/open-feature/flagd/grpc/go v1.5.1-20250529171031-ebdc14163473.2/go.mod h1:4u0WLwfkLob3dC/F8qNctqhtiEv2Mlyi8YgCDDzgYDs= buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.6-20250529171031-ebdc14163473.1 h1:LdC4xAuUaNdduzQr5VvhjsgrCfpW9IYxYsjyCF0ANs0= buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.6-20250529171031-ebdc14163473.1/go.mod h1:cCQ49+ttXE2MZ/ciRNb0tCG+F3kj2ZVbP+0/psbhrLY= -cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= -cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= @@ -50,8 +50,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= @@ -102,15 +102,15 @@ github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLo github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= 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/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -145,8 +145,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -216,8 +216,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= @@ -274,15 +276,17 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= @@ -304,8 +308,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -326,34 +330,56 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4= +go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY= +go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILrLVgHxjA/AMjqHKes+uzo= -go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -370,8 +396,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= @@ -384,8 +410,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -401,8 +427,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= @@ -415,8 +441,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -430,16 +456,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -447,8 +473,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -463,8 +489,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -473,6 +499,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -482,17 +510,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -502,8 +530,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/core/pkg/telemetry/builder.go b/core/pkg/telemetry/builder.go index 55baf5a51..302e001c2 100644 --- a/core/pkg/telemetry/builder.go +++ b/core/pkg/telemetry/builder.go @@ -2,20 +2,15 @@ package telemetry import ( "context" - "crypto/tls" - "crypto/x509" "fmt" "os" "time" "connectrpc.com/connect" "connectrpc.com/otelconnect" - "github.com/open-feature/flagd/core/pkg/certreloader" "github.com/open-feature/flagd/core/pkg/logger" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/metric" @@ -23,9 +18,6 @@ import ( "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.34.0" "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" ) const ( @@ -72,24 +64,33 @@ func BuildMetricsRecorder( } // BuildTraceProvider build and register the trace provider and propagator for the caller runtime. This method -// attempt to register a global TracerProvider backed by batch SpanProcessor.Config. CollectorTarget can be used to -// provide the grpc collector target. Providing empty target results in skipping provider & propagator registration. -// This results in tracers having NoopTracerProvider and propagator having No-Op TextMapPropagator performing no action +// uses autoexport to automatically handle OTEL environment variables for trace exporters. +// Providing empty collector target results in using environment variables or falling back to noop. func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, svcVersion string, cfg Config) error { - if cfg.CollectorConfig.Target == "" { - logger.Debug("skipping trace provider setup as collector target is not set." + - " Traces will use NoopTracerProvider provider and propagator will use no-Op TextMapPropagator") - return nil + // For backwards compatibility: set environment variables from flagd configuration + // before calling autoexport if they are provided via flags + if cfg.CollectorConfig.Target != "" { + setEnvIfNotSet("OTEL_TRACES_EXPORTER", "otlp") + setEnvIfNotSet("OTEL_EXPORTER_OTLP_ENDPOINT", cfg.CollectorConfig.Target) + setEnvIfNotSet("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") } - exporter, err := buildOtlpExporter(ctx, cfg.CollectorConfig) + // Use autoexport to handle OTEL environment variables + exporter, err := autoexport.NewSpanExporter(ctx) if err != nil { - return err + return fmt.Errorf("failed to create span exporter: %w", err) + } + + // Skip if noop exporter (when no configuration is provided) + if autoexport.IsNoneSpanExporter(exporter) { + logger.Debug("skipping trace provider setup as no exporter is configured." + + " Traces will use NoopTracerProvider provider and propagator will use no-Op TextMapPropagator") + return nil } res, err := buildResourceFor(ctx, svc, svcVersion) if err != nil { - return err + return fmt.Errorf("failed to build resource: %w", err) } provider := trace.NewTracerProvider( @@ -106,121 +107,38 @@ func BuildTraceProvider(ctx context.Context, logger *logger.Logger, svc string, func BuildConnectOptions(cfg Config) ([]connect.HandlerOption, error) { options := []connect.HandlerOption{} - // add interceptor if configuration is available for collector - if cfg.CollectorConfig.Target != "" { - interceptor, err := otelconnect.NewInterceptor(otelconnect.WithTrustRemote()) - if err != nil { - return nil, fmt.Errorf("error creating interceptor, %w", err) - } - - options = append(options, connect.WithInterceptors(interceptor)) + // Always add interceptor - autoexport will handle whether traces are enabled + interceptor, err := otelconnect.NewInterceptor(otelconnect.WithTrustRemote()) + if err != nil { + return nil, fmt.Errorf("error creating interceptor, %w", err) } - return options, nil -} + options = append(options, connect.WithInterceptors(interceptor)) -func buildTransportCredentials(_ context.Context, cfg CollectorConfig) (credentials.TransportCredentials, error) { - creds := insecure.NewCredentials() - if cfg.KeyPath != "" || cfg.CertPath != "" || cfg.CAPath != "" { - capool := x509.NewCertPool() - if cfg.CAPath != "" { - ca, err := os.ReadFile(cfg.CAPath) - if err != nil { - return nil, fmt.Errorf("can't read ca file from %s", cfg.CAPath) - } - if !capool.AppendCertsFromPEM(ca) { - return nil, fmt.Errorf("can't add CA '%s' to pool", cfg.CAPath) - } - } - - reloader, err := certreloader.NewCertReloader(certreloader.Config{ - KeyPath: cfg.KeyPath, - CertPath: cfg.CertPath, - ReloadInterval: cfg.ReloadInterval, - }) - if err != nil { - return nil, fmt.Errorf("failed to create certreloader: %w", err) - } - - tlsConfig := &tls.Config{ - RootCAs: capool, - MinVersion: tls.VersionTLS12, - GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { - certs, err := reloader.GetCertificate() - if err != nil { - return nil, fmt.Errorf("failed to reload certs: %w", err) - } - return certs, nil - }, - } - - creds = credentials.NewTLS(tlsConfig) - } - - return creds, nil + return options, nil } // buildMetricReader builds a metric reader based on provided configurations +// Uses autoexport to automatically handle OTEL environment variables func buildMetricReader(ctx context.Context, cfg Config) (metric.Reader, error) { - if cfg.MetricsExporter == "" { - return buildDefaultMetricReader() - } - - // Handle metric reader override - if cfg.MetricsExporter != metricsExporterOtel { - return nil, fmt.Errorf("provided metrics operator %s is not supported. currently only support %s", - cfg.MetricsExporter, metricsExporterOtel) - } - - // Otel override require target configuration - if cfg.CollectorConfig.Target == "" { - return nil, fmt.Errorf("metric exporter is set(%s) without providing otel collector target."+ - " collector target is required for this option", cfg.MetricsExporter) - } - - transportCredentials, err := buildTransportCredentials(ctx, cfg.CollectorConfig) - if err != nil { - return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) - } - - // Non-blocking, insecure grpc connection - conn, err := grpc.NewClient(cfg.CollectorConfig.Target, grpc.WithTransportCredentials(transportCredentials)) - if err != nil { - return nil, fmt.Errorf("error creating client connection: %w", err) - } - - // Otel metric exporter - otelExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn)) - if err != nil { - return nil, fmt.Errorf("error creating otel metric exporter: %w", err) + // For backwards compatibility: set environment variables from flagd configuration + // before calling autoexport if they are provided via flags + if cfg.MetricsExporter == metricsExporterOtel && cfg.CollectorConfig.Target != "" { + // Set OTEL environment variables from configuration if not already set + setEnvIfNotSet("OTEL_METRICS_EXPORTER", "otlp") + setEnvIfNotSet("OTEL_EXPORTER_OTLP_ENDPOINT", cfg.CollectorConfig.Target) + setEnvIfNotSet("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") } - return metric.NewPeriodicReader(otelExporter), nil -} - -// buildOtlpExporter is a helper to build grpc backed otlp trace exporter -func buildOtlpExporter(ctx context.Context, cfg CollectorConfig) (*otlptrace.Exporter, error) { - transportCredentials, err := buildTransportCredentials(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("metric export would not build transport credentials: %w", err) - } - - // Non-blocking, grpc connection - conn, err := grpc.NewClient(cfg.Target, grpc.WithTransportCredentials(transportCredentials)) - if err != nil { - return nil, fmt.Errorf("error creating client connection: %w", err) - } - - traceClient := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn)) - exporter, err := otlptrace.New(ctx, traceClient) - if err != nil { - return nil, fmt.Errorf("error starting otel exporter: %w", err) - } - return exporter, nil + // Use autoexport with Prometheus as fallback for backwards compatibility + return autoexport.NewMetricReader( + ctx, + autoexport.WithFallbackMetricReader(buildDefaultMetricReader), + ) } // buildDefaultMetricReader provides the default metric reader -func buildDefaultMetricReader() (metric.Reader, error) { +func buildDefaultMetricReader(ctx context.Context) (metric.Reader, error) { p, err := prometheus.New() if err != nil { return nil, fmt.Errorf("unable to create default metric reader: %w", err) @@ -246,6 +164,13 @@ func buildResourceFor(ctx context.Context, serviceName string, serviceVersion st return r, nil } +// setEnvIfNotSet sets an environment variable only if it's not already set +func setEnvIfNotSet(key, value string) { + if os.Getenv(key) == "" { + os.Setenv(key, value) + } +} + // OTelErrorsHandler is a custom error interceptor for OpenTelemetry type otelErrorsHandler struct { logger *logger.Logger diff --git a/core/pkg/telemetry/builder_test.go b/core/pkg/telemetry/builder_test.go index c6253012b..59d1314b2 100644 --- a/core/pkg/telemetry/builder_test.go +++ b/core/pkg/telemetry/builder_test.go @@ -44,24 +44,7 @@ func TestBuildMetricReader(t *testing.T) { error: false, }, { - name: "Metric exporter overriding require valid overriding parameter", - cfg: Config{ - MetricsExporter: "unsupported", - }, - error: true, - }, - { - name: "Metric exporter overriding require valid configuration combination", - cfg: Config{ - MetricsExporter: metricsExporterOtel, - CollectorConfig: CollectorConfig{ - Target: "", // collector target is unset - }, - }, - error: true, - }, - { - name: "Metric exporter overriding with valid configurations", + name: "Autoexport handles all configurations", cfg: Config{ MetricsExporter: metricsExporterOtel, CollectorConfig: CollectorConfig{ @@ -128,9 +111,9 @@ func TestBuildConnectOptions(t *testing.T) { optionCount int }{ { - name: "No options for empty/default configurations", + name: "Interceptor always added with autoexport", cfg: Config{}, - optionCount: 0, + optionCount: 1, }, { name: "Connect option is set when telemetry target is set",