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:
@@ -23,6 +23,9 @@ type bufferingExporter struct {
|
||||
// Real exporter (created when tracing is configured)
|
||||
exporter otellog.Exporter
|
||||
|
||||
// Track whether buffer has been flushed (separate from exporter creation)
|
||||
bufferFlushed bool
|
||||
|
||||
// Thread-safe initialization state (managed only by checkReadiness)
|
||||
initErr error
|
||||
|
||||
@@ -71,22 +74,63 @@ func (e *bufferingExporter) initialize() error {
|
||||
initCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
exporter, err := factory(initCtx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OTLP exporter: %w", err)
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
e.mu.RUnlock()
|
||||
|
||||
// Create exporter if not already created
|
||||
if !hasExporter {
|
||||
exporter, err := factory(initCtx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OTLP exporter: %w", err)
|
||||
}
|
||||
e.mu.Lock()
|
||||
// Double-check: another goroutine may have created it while we were waiting
|
||||
if e.exporter == nil {
|
||||
e.exporter = exporter
|
||||
} else {
|
||||
// Another goroutine beat us, close the one we created
|
||||
_ = exporter.Shutdown(context.Background())
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Check if we can flush (token verification if configured)
|
||||
if !e.canFlush(initCtx, cfg, false) {
|
||||
return errors.New("waiting for token authentication")
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.exporter = exporter
|
||||
flushErr := e.flushBuffer(initCtx)
|
||||
if !e.bufferFlushed {
|
||||
flushErr := e.flushBuffer(initCtx)
|
||||
if flushErr != nil {
|
||||
e.mu.Unlock()
|
||||
// Log but don't fail initialization
|
||||
Setup().Warn("buffer flush failed during initialization", "error", flushErr)
|
||||
return nil
|
||||
}
|
||||
e.bufferFlushed = true
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
if flushErr != nil {
|
||||
// Log but don't fail initialization
|
||||
Setup().Warn("buffer flush failed during initialization", "error", flushErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// canFlush checks if we're ready to flush buffered logs.
|
||||
// If BearerTokenFunc is configured, it must return without error.
|
||||
// If forceFlush is true (during shutdown with cancelled context), skip token check.
|
||||
func (e *bufferingExporter) canFlush(ctx context.Context, cfg *tracerconfig.Config, forceFlush bool) bool {
|
||||
if cfg.BearerTokenFunc == nil {
|
||||
return true // No token auth configured, can flush immediately
|
||||
}
|
||||
|
||||
return nil
|
||||
if forceFlush {
|
||||
return true // During shutdown, proceed with best-effort flush
|
||||
}
|
||||
|
||||
// Check if token is available (call returns without error)
|
||||
_, err := cfg.BearerTokenFunc(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// bufferRecords adds records to the buffer for later processing
|
||||
@@ -119,16 +163,16 @@ func (e *bufferingExporter) checkReadiness() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Check if we already have a working exporter
|
||||
// Check if we're fully ready (exporter created AND buffer flushed)
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
fullyReady := e.exporter != nil && e.bufferFlushed
|
||||
e.mu.RUnlock()
|
||||
|
||||
if hasExporter {
|
||||
return // Exporter ready, checker no longer needed
|
||||
if fullyReady {
|
||||
return // Fully initialized, checker no longer needed
|
||||
}
|
||||
|
||||
// Try to initialize
|
||||
// Try to initialize (creates exporter and flushes if token ready)
|
||||
err := e.initialize()
|
||||
e.mu.Lock()
|
||||
e.initErr = err
|
||||
@@ -182,18 +226,42 @@ func (e *bufferingExporter) Shutdown(ctx context.Context) error {
|
||||
// Wait for readiness checker goroutine to complete
|
||||
<-e.checkerDone
|
||||
|
||||
cfg, _, _ := tracerconfig.Get()
|
||||
|
||||
// Check if context is cancelled for best-effort flush
|
||||
forceFlush := ctx.Err() != nil
|
||||
|
||||
// Give one final chance for TLS/tracing to become ready for buffer flushing
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
bufferFlushed := e.bufferFlushed
|
||||
e.mu.RUnlock()
|
||||
|
||||
if !hasExporter {
|
||||
err := e.initialize()
|
||||
e.mu.Lock()
|
||||
e.initErr = err
|
||||
hasExporter = e.exporter != nil
|
||||
bufferFlushed = e.bufferFlushed
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// If exporter exists but buffer not flushed, try to flush now
|
||||
if hasExporter && !bufferFlushed {
|
||||
canFlushNow := cfg == nil || e.canFlush(ctx, cfg, forceFlush)
|
||||
if canFlushNow {
|
||||
e.mu.Lock()
|
||||
if !e.bufferFlushed {
|
||||
flushErr := e.flushBuffer(ctx)
|
||||
if flushErr != nil {
|
||||
Setup().Warn("buffer flush failed during shutdown", "error", flushErr)
|
||||
}
|
||||
e.bufferFlushed = true
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user