Add BearerTokenFunc to support dynamic bearer token authentication for OTLP gRPC exporters. Tokens are injected via gRPC PerRPCCredentials on each export request. - Add BearerTokenFunc type and Config field in tracerconfig - Implement bearerCredentials (gRPC) and bearerRoundTripper (HTTP) - Wire bearer auth into all three gRPC exporter creation functions - Add token verification before flushing buffered logs - Fix race condition in buffering exporter initialization Note: HTTP exporters don't support dynamic bearer tokens due to OpenTelemetry SDK limitations (no WithHTTPClient option). Use gRPC protocol for dynamic tokens.
409 lines
15 KiB
Go
409 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/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 fully supported for gRPC exporters
|
|
// via PerRPCCredentials. HTTP exporters do not currently support dynamic bearer
|
|
// tokens due to OpenTelemetry SDK limitations (no WithHTTPClient option).
|
|
// For HTTP, use OTEL_EXPORTER_OTLP_HEADERS environment variable for static tokens,
|
|
// or switch to gRPC protocol (OTEL_EXPORTER_OTLP_PROTOCOL=grpc) for dynamic tokens.
|
|
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
|
|
}
|
|
|
|
// 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 tlsConfig != nil {
|
|
opts = append(opts, otlploghttp.WithTLSClientConfig(tlsConfig))
|
|
}
|
|
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 tlsConfig != nil {
|
|
opts = append(opts, otlpmetrichttp.WithTLSClientConfig(tlsConfig))
|
|
}
|
|
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 tlsConfig != nil {
|
|
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
|
|
}
|
|
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)
|
|
}
|