Add BearerTokenFunc to support dynamic bearer token authentication for OTLP exporters. Tokens are injected per-request via gRPC PerRPCCredentials and HTTP custom RoundTripper. - Add BearerTokenFunc type and Config field in tracerconfig - Implement bearerCredentials (gRPC) and bearerRoundTripper (HTTP) - Wire bearer auth into all exporter creation functions - Add getHTTPClient helper for DRY HTTP client configuration - Upgrade OpenTelemetry SDK to v1.39.0 for WithHTTPClient support
428 lines
15 KiB
Go
428 lines
15 KiB
Go
// Package tracerconfig provides a bridge to eliminate circular dependencies between
|
|
// the logger and tracing packages. It stores tracer configuration and provides
|
|
// factory functions that can be used by the logger package without importing tracing.
|
|
package tracerconfig
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
)
|
|
|
|
const (
|
|
otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL"
|
|
otelExporterOTLPTracesProtoEnvKey = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
|
|
otelExporterOTLPLogsProtoEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
|
|
otelExporterOTLPMetricsProtoEnvKey = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
|
|
)
|
|
|
|
var errInvalidOTLPProtocol = errors.New("invalid OTLP protocol - should be one of ['grpc', 'http/protobuf']")
|
|
|
|
// newInvalidProtocolError creates a specific error message for invalid protocols
|
|
func newInvalidProtocolError(protocol, signalType string) error {
|
|
return fmt.Errorf("invalid OTLP protocol '%s' for %s - should be one of ['grpc', 'http/protobuf', 'http/json']", protocol, signalType)
|
|
}
|
|
|
|
// Validate checks the configuration for common errors and inconsistencies
|
|
func (c *Config) Validate() error {
|
|
var errs []error
|
|
|
|
// Check that both Endpoint and EndpointURL are not specified
|
|
if c.Endpoint != "" && c.EndpointURL != "" {
|
|
errs = append(errs, errors.New("cannot specify both Endpoint and EndpointURL - use one or the other"))
|
|
}
|
|
|
|
// Validate EndpointURL format if specified
|
|
if c.EndpointURL != "" {
|
|
if _, err := url.Parse(c.EndpointURL); err != nil {
|
|
errs = append(errs, fmt.Errorf("invalid EndpointURL format: %w", err))
|
|
}
|
|
}
|
|
|
|
// Validate Endpoint format if specified
|
|
if c.Endpoint != "" {
|
|
// Basic validation - should not contain protocol scheme
|
|
if strings.Contains(c.Endpoint, "://") {
|
|
errs = append(errs, errors.New("Endpoint should not include protocol scheme (use EndpointURL for full URLs)"))
|
|
}
|
|
// Should not be empty after trimming whitespace
|
|
if strings.TrimSpace(c.Endpoint) == "" {
|
|
errs = append(errs, errors.New("Endpoint cannot be empty or whitespace"))
|
|
}
|
|
}
|
|
|
|
// Validate TLS configuration consistency
|
|
if c.CertificateProvider != nil && c.RootCAs == nil {
|
|
// This is just a warning - client cert without custom CAs is valid
|
|
// but might indicate a configuration issue
|
|
}
|
|
|
|
// Validate service name if specified
|
|
if c.ServiceName != "" && strings.TrimSpace(c.ServiceName) == "" {
|
|
errs = append(errs, errors.New("ServiceName cannot be empty or whitespace"))
|
|
}
|
|
|
|
// Combine all errors
|
|
if len(errs) > 0 {
|
|
var errMsgs []string
|
|
for _, err := range errs {
|
|
errMsgs = append(errMsgs, err.Error())
|
|
}
|
|
return fmt.Errorf("configuration validation failed: %s", strings.Join(errMsgs, "; "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateAndStore validates the configuration before storing it
|
|
func ValidateAndStore(ctx context.Context, cfg *Config, logFactory LogExporterFactory, metricFactory MetricExporterFactory, traceFactory TraceExporterFactory) error {
|
|
if cfg != nil {
|
|
if err := cfg.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
Store(ctx, cfg, logFactory, metricFactory, traceFactory)
|
|
return nil
|
|
}
|
|
|
|
// GetClientCertificate defines a function type for providing client certificates for mutual TLS.
|
|
// This is used when exporting telemetry data to secured OTLP endpoints that require
|
|
// client certificate authentication.
|
|
type GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
|
|
|
// BearerTokenFunc retrieves a bearer token for OTLP authentication.
|
|
// It is called for each export request (traces, logs, metrics).
|
|
// The caller is responsible for caching and token renewal.
|
|
// Returns the token string (without "Bearer " prefix) or an error.
|
|
// An empty string with no error means skip the Authorization header.
|
|
//
|
|
// Thread safety: This function may be called concurrently from multiple
|
|
// goroutines. Implementations must be safe for concurrent use.
|
|
//
|
|
// Protocol support: Bearer authentication is supported for both gRPC
|
|
// (via PerRPCCredentials) and HTTP (via custom RoundTripper) exporters.
|
|
type BearerTokenFunc func(ctx context.Context) (string, error)
|
|
|
|
// Config provides configuration options for OpenTelemetry tracing setup.
|
|
// It supplements standard OpenTelemetry environment variables with additional
|
|
// NTP Pool-specific configuration including TLS settings for secure OTLP export.
|
|
type Config struct {
|
|
ServiceName string // Service name for resource identification (overrides OTEL_SERVICE_NAME)
|
|
Environment string // Deployment environment (development, staging, production)
|
|
Endpoint string // OTLP endpoint hostname/port (e.g., "otlp.example.com:4317")
|
|
EndpointURL string // Complete OTLP endpoint URL (e.g., "https://otlp.example.com:4317/v1/traces")
|
|
CertificateProvider GetClientCertificate // Client certificate provider for mutual TLS
|
|
RootCAs *x509.CertPool // CA certificate pool for server verification
|
|
BearerTokenFunc BearerTokenFunc // Token provider for bearer authentication
|
|
}
|
|
|
|
// LogExporterFactory creates an OTLP log exporter using the provided configuration.
|
|
// This allows the logger package to create exporters without importing the tracing package.
|
|
type LogExporterFactory func(context.Context, *Config) (sdklog.Exporter, error)
|
|
|
|
// MetricExporterFactory creates an OTLP metric exporter using the provided configuration.
|
|
// This allows the metrics package to create exporters without importing the tracing package.
|
|
type MetricExporterFactory func(context.Context, *Config) (sdkmetric.Exporter, error)
|
|
|
|
// TraceExporterFactory creates an OTLP trace exporter using the provided configuration.
|
|
// This allows for consistent trace exporter creation across packages.
|
|
type TraceExporterFactory func(context.Context, *Config) (sdktrace.SpanExporter, error)
|
|
|
|
// Global state for sharing configuration between packages
|
|
var (
|
|
globalConfig *Config
|
|
globalContext context.Context
|
|
logExporterFactory LogExporterFactory
|
|
metricExporterFactory MetricExporterFactory
|
|
traceExporterFactory TraceExporterFactory
|
|
configMu sync.RWMutex
|
|
)
|
|
|
|
// Store saves the tracer configuration and exporter factories for use by other packages.
|
|
// This should be called by the tracing package during initialization.
|
|
func Store(ctx context.Context, cfg *Config, logFactory LogExporterFactory, metricFactory MetricExporterFactory, traceFactory TraceExporterFactory) {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
globalConfig = cfg
|
|
globalContext = ctx
|
|
logExporterFactory = logFactory
|
|
metricExporterFactory = metricFactory
|
|
traceExporterFactory = traceFactory
|
|
}
|
|
|
|
// GetLogExporter returns the stored configuration and log exporter factory.
|
|
// Returns nil values if no configuration has been stored yet.
|
|
func GetLogExporter() (*Config, context.Context, LogExporterFactory) {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
return globalConfig, globalContext, logExporterFactory
|
|
}
|
|
|
|
// GetMetricExporter returns the stored configuration and metric exporter factory.
|
|
// Returns nil values if no configuration has been stored yet.
|
|
func GetMetricExporter() (*Config, context.Context, MetricExporterFactory) {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
return globalConfig, globalContext, metricExporterFactory
|
|
}
|
|
|
|
// GetTraceExporter returns the stored configuration and trace exporter factory.
|
|
// Returns nil values if no configuration has been stored yet.
|
|
func GetTraceExporter() (*Config, context.Context, TraceExporterFactory) {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
return globalConfig, globalContext, traceExporterFactory
|
|
}
|
|
|
|
// Get returns the stored tracer configuration, context, and log exporter factory.
|
|
// This maintains backward compatibility for the logger package.
|
|
// Returns nil values if no configuration has been stored yet.
|
|
func Get() (*Config, context.Context, LogExporterFactory) {
|
|
return GetLogExporter()
|
|
}
|
|
|
|
// IsConfigured returns true if tracer configuration has been stored.
|
|
func IsConfigured() bool {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
return globalConfig != nil && globalContext != nil
|
|
}
|
|
|
|
// Clear removes the stored configuration. This is primarily useful for testing.
|
|
func Clear() {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
globalConfig = nil
|
|
globalContext = nil
|
|
logExporterFactory = nil
|
|
metricExporterFactory = nil
|
|
traceExporterFactory = nil
|
|
}
|
|
|
|
// getTLSConfig creates a TLS configuration from the provided Config.
|
|
func getTLSConfig(cfg *Config) *tls.Config {
|
|
if cfg.CertificateProvider == nil {
|
|
return nil
|
|
}
|
|
return &tls.Config{
|
|
GetClientCertificate: cfg.CertificateProvider,
|
|
RootCAs: cfg.RootCAs,
|
|
}
|
|
}
|
|
|
|
// getProtocol determines the OTLP protocol to use for the given signal type.
|
|
// It follows OpenTelemetry environment variable precedence.
|
|
func getProtocol(signalSpecificEnv string) string {
|
|
proto := os.Getenv(signalSpecificEnv)
|
|
if proto == "" {
|
|
proto = os.Getenv(otelExporterOTLPProtoEnvKey)
|
|
}
|
|
// Fallback to default, http/protobuf.
|
|
if proto == "" {
|
|
proto = "http/protobuf"
|
|
}
|
|
return proto
|
|
}
|
|
|
|
// getHTTPClient builds an HTTP client with bearer auth and/or TLS if configured.
|
|
// Returns nil if neither is configured, allowing the SDK to use its defaults.
|
|
func getHTTPClient(cfg *Config, tlsConfig *tls.Config) *http.Client {
|
|
if cfg.BearerTokenFunc == nil && tlsConfig == nil {
|
|
return nil
|
|
}
|
|
|
|
var transport http.RoundTripper = http.DefaultTransport
|
|
if tlsConfig != nil {
|
|
transport = &http.Transport{TLSClientConfig: tlsConfig}
|
|
}
|
|
if cfg.BearerTokenFunc != nil {
|
|
transport = &bearerRoundTripper{
|
|
base: transport,
|
|
tokenFunc: cfg.BearerTokenFunc,
|
|
}
|
|
}
|
|
|
|
return &http.Client{Transport: transport}
|
|
}
|
|
|
|
// CreateOTLPLogExporter creates an OTLP log exporter using the provided configuration.
|
|
func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, error) {
|
|
tlsConfig := getTLSConfig(cfg)
|
|
proto := getProtocol(otelExporterOTLPLogsProtoEnvKey)
|
|
|
|
switch proto {
|
|
case "grpc":
|
|
opts := []otlploggrpc.Option{
|
|
otlploggrpc.WithCompressor("gzip"),
|
|
}
|
|
if tlsConfig != nil {
|
|
opts = append(opts, otlploggrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
|
}
|
|
if cfg.BearerTokenFunc != nil {
|
|
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
|
opts = append(opts, otlploggrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlploggrpc.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
return otlploggrpc.New(ctx, opts...)
|
|
case "http/protobuf", "http/json":
|
|
opts := []otlploghttp.Option{
|
|
otlploghttp.WithCompression(otlploghttp.GzipCompression),
|
|
}
|
|
if client := getHTTPClient(cfg, tlsConfig); client != nil {
|
|
opts = append(opts, otlploghttp.WithHTTPClient(client))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlploghttp.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
opts = append(opts, otlploghttp.WithRetry(otlploghttp.RetryConfig{
|
|
Enabled: true,
|
|
InitialInterval: 3 * time.Second,
|
|
MaxInterval: 60 * time.Second,
|
|
MaxElapsedTime: 5 * time.Minute,
|
|
}))
|
|
|
|
return otlploghttp.New(ctx, opts...)
|
|
default:
|
|
return nil, newInvalidProtocolError(proto, "logs")
|
|
}
|
|
}
|
|
|
|
// CreateOTLPMetricExporter creates an OTLP metric exporter using the provided configuration.
|
|
func CreateOTLPMetricExporter(ctx context.Context, cfg *Config) (sdkmetric.Exporter, error) {
|
|
tlsConfig := getTLSConfig(cfg)
|
|
proto := getProtocol(otelExporterOTLPMetricsProtoEnvKey)
|
|
|
|
switch proto {
|
|
case "grpc":
|
|
opts := []otlpmetricgrpc.Option{
|
|
otlpmetricgrpc.WithCompressor("gzip"),
|
|
}
|
|
if tlsConfig != nil {
|
|
opts = append(opts, otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
|
}
|
|
if cfg.BearerTokenFunc != nil {
|
|
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
|
opts = append(opts, otlpmetricgrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlpmetricgrpc.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
return otlpmetricgrpc.New(ctx, opts...)
|
|
case "http/protobuf", "http/json":
|
|
opts := []otlpmetrichttp.Option{
|
|
otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression),
|
|
}
|
|
if client := getHTTPClient(cfg, tlsConfig); client != nil {
|
|
opts = append(opts, otlpmetrichttp.WithHTTPClient(client))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlpmetrichttp.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlpmetrichttp.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
opts = append(opts, otlpmetrichttp.WithRetry(otlpmetrichttp.RetryConfig{
|
|
Enabled: true,
|
|
InitialInterval: 3 * time.Second,
|
|
MaxInterval: 60 * time.Second,
|
|
MaxElapsedTime: 5 * time.Minute,
|
|
}))
|
|
|
|
return otlpmetrichttp.New(ctx, opts...)
|
|
default:
|
|
return nil, newInvalidProtocolError(proto, "metrics")
|
|
}
|
|
}
|
|
|
|
// CreateOTLPTraceExporter creates an OTLP trace exporter using the provided configuration.
|
|
func CreateOTLPTraceExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExporter, error) {
|
|
tlsConfig := getTLSConfig(cfg)
|
|
proto := getProtocol(otelExporterOTLPTracesProtoEnvKey)
|
|
|
|
var client otlptrace.Client
|
|
|
|
switch proto {
|
|
case "grpc":
|
|
opts := []otlptracegrpc.Option{
|
|
otlptracegrpc.WithCompressor("gzip"),
|
|
}
|
|
if tlsConfig != nil {
|
|
opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
|
}
|
|
if cfg.BearerTokenFunc != nil {
|
|
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
|
opts = append(opts, otlptracegrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlptracegrpc.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
client = otlptracegrpc.NewClient(opts...)
|
|
case "http/protobuf", "http/json":
|
|
opts := []otlptracehttp.Option{
|
|
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
|
}
|
|
if httpClient := getHTTPClient(cfg, tlsConfig); httpClient != nil {
|
|
opts = append(opts, otlptracehttp.WithHTTPClient(httpClient))
|
|
}
|
|
if len(cfg.Endpoint) > 0 {
|
|
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
|
}
|
|
if len(cfg.EndpointURL) > 0 {
|
|
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.EndpointURL))
|
|
}
|
|
|
|
opts = append(opts, otlptracehttp.WithRetry(otlptracehttp.RetryConfig{
|
|
Enabled: true,
|
|
InitialInterval: 3 * time.Second,
|
|
MaxInterval: 60 * time.Second,
|
|
MaxElapsedTime: 5 * time.Minute,
|
|
}))
|
|
|
|
client = otlptracehttp.NewClient(opts...)
|
|
default:
|
|
return nil, newInvalidProtocolError(proto, "traces")
|
|
}
|
|
|
|
return otlptrace.New(ctx, client)
|
|
}
|