feat(tracing): add bearer token authentication for OTLP exporters

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
This commit is contained in:
2025-12-27 12:52:37 -08:00
parent d43ff0f2a9
commit 1df4b0d4b4
7 changed files with 744 additions and 137 deletions

View File

@@ -9,6 +9,7 @@ import (
"crypto/x509"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
@@ -25,6 +26,7 @@ import (
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"
)
@@ -109,6 +111,19 @@ func ValidateAndStore(ctx context.Context, cfg *Config, logFactory LogExporterFa
// 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.
@@ -119,6 +134,7 @@ type Config struct {
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.
@@ -229,6 +245,27 @@ func getProtocol(signalSpecificEnv string) string {
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)
@@ -242,6 +279,10 @@ func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, e
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))
}
@@ -254,8 +295,8 @@ func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, e
opts := []otlploghttp.Option{
otlploghttp.WithCompression(otlploghttp.GzipCompression),
}
if tlsConfig != nil {
opts = append(opts, otlploghttp.WithTLSClientConfig(tlsConfig))
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))
@@ -290,6 +331,10 @@ func CreateOTLPMetricExporter(ctx context.Context, cfg *Config) (sdkmetric.Expor
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))
}
@@ -302,8 +347,8 @@ func CreateOTLPMetricExporter(ctx context.Context, cfg *Config) (sdkmetric.Expor
opts := []otlpmetrichttp.Option{
otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression),
}
if tlsConfig != nil {
opts = append(opts, otlpmetrichttp.WithTLSClientConfig(tlsConfig))
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))
@@ -340,6 +385,10 @@ func CreateOTLPTraceExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExp
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))
}
@@ -352,8 +401,8 @@ func CreateOTLPTraceExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExp
opts := []otlptracehttp.Option{
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
}
if tlsConfig != nil {
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
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))