Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ Controls the OpenTelemetry **Logs** Exporter. Tracing and Metrics inherit `Endpo
| `Endpoint` | `string` | `""` | `host:port` or URL. URL schemes override `Insecure` setting. |
| `Protocol` | `string` | `"grpc"` | `"grpc"` (recommended) or `"http"`. |
| `Insecure` | `bool` | `false` | Disables TLS (dev only). Ignored if Endpoint starts with `https://`. |
| `Username` | `string` | `""` | Basic Auth username. |
| `Password` | `string` | `""` | Basic Auth password. |
| `Username` | `string` | `""` | Basic Auth username. (Fallback for VPC) |
| `Password` | `string` | `""` | Basic Auth password. (Fallback for VPC) |
| `BatchSize` | `int` | `512` | Max logs per export batch. |
| `ExportInterval` | `Duration` | `5s` | Flush interval. |
| `Level` | `string` | `""` | Optional override for OTEL log level. |
Expand Down Expand Up @@ -420,6 +420,19 @@ cfg.Console.Format = "systemd"
cfg.Console.ErrorsToStderr = true
```

### Full Configuration with Token Auth

```go
cfg := ion.Default()
cfg.OTEL.Enabled = true
cfg.OTEL.Endpoint = "otel.jmdt.io:443"
cfg.OTEL.Headers = map[string]string{
"Authorization": "Bearer 89658fc8a43d1a39cde4c59d1e2772194a8e2b533d53f93ca29659f40972980f",
}
cfg.Tracing.Enabled = true
cfg.Metrics.Enabled = true
```

### Full Stack (Kubernetes)

```go
Expand Down
25 changes: 15 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ type ConsoleConfig struct {
// Default: true
Enabled bool `yaml:"enabled" json:"enabled"`

// Format: "json" for structured JSON, "pretty" for human-readable.
// Format: "json" for structured JSON, "pretty" for human-readable,
// or "systemd" for Journald-optimized output.
// Default: "json" (production), "pretty" (development)
Format string `yaml:"format" json:"format"`

Expand Down Expand Up @@ -116,13 +117,17 @@ type OTELConfig struct {
// Default: false
Insecure bool `yaml:"insecure" json:"insecure"`

// Username for Basic Authentication (optional).
// Username is used for HTTP Basic Authentication if required by the OTLP endpoint.
Username string `yaml:"username" json:"username" env:"OTEL_USERNAME"`

// Password for Basic Authentication (optional).
// Password is used for HTTP Basic Authentication if required by the OTLP endpoint.
Password string `yaml:"password" json:"password" env:"OTEL_PASSWORD"` //nolint:gosec // Required for configuration binding

// Headers are additional headers to send (e.g., auth tokens).
// Headers are custom headers to send to the exporter.
// This map is the standard mechanism for injecting Bearer tokens, API keys, or custom
// routing headers (e.g., {"Authorization": "Bearer <token>"}).
// Note: If an Authorization header is explicitly provided here, it will automatically
// supersede the Username and Password configuration.
Headers map[string]string `yaml:"headers" json:"headers"`

// Timeout is the export timeout.
Expand Down Expand Up @@ -161,13 +166,13 @@ type TracingConfig struct {
// Insecure disables TLS.
Insecure bool `yaml:"insecure" json:"insecure"`

// Username for Basic Authentication (optional).
// Username is used for HTTP Basic Authentication if required by the OTLP endpoint.
Username string `yaml:"username" json:"username" env:"TRACING_USERNAME"`

// Password for Basic Authentication (optional).
// Password is used for HTTP Basic Authentication if required by the OTLP endpoint.
Password string `yaml:"password" json:"password" env:"TRACING_PASSWORD"` //nolint:gosec // Required for configuration binding

// Headers for authentication.
// Headers are custom headers for authentication (supersedes Username/Password if Authorization is set).
Headers map[string]string `yaml:"headers" json:"headers"`

// Timeout for export.
Expand Down Expand Up @@ -204,13 +209,13 @@ type MetricsConfig struct {
// Insecure disables TLS.
Insecure bool `yaml:"insecure" json:"insecure"`

// Username for Basic Authentication (optional).
// Username is used for HTTP Basic Authentication if required by the OTLP endpoint.
Username string `yaml:"username" json:"username" env:"METRICS_USERNAME"`

// Password for Basic Authentication (optional).
// Password is used for HTTP Basic Authentication if required by the OTLP endpoint.
Password string `yaml:"password" json:"password" env:"METRICS_PASSWORD"` //nolint:gosec // Required for configuration binding

// Headers for authentication.
// Headers are custom headers for authentication (supersedes Username/Password if Authorization is set).
Headers map[string]string `yaml:"headers" json:"headers"`

// Timeout for export.
Expand Down
4 changes: 2 additions & 2 deletions internal/core/logger_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ func NewZapLogger(cfg config.Config) (*ZapFactoryResult, error) {

// 1. Setup OTEL if enabled
if cfg.OTEL.Enabled && cfg.OTEL.Endpoint != "" {
// Inject Basic Auth header if credentials provided
cfg.OTEL.Headers = injectBasicAuth(cfg.OTEL.Headers, cfg.OTEL.Username, cfg.OTEL.Password, cfg.OTEL.Protocol)
// Inject Auth header logic: Preferred Headers > Basic Auth fallback
cfg.OTEL.Headers = injectAuth(cfg.OTEL.Headers, cfg.OTEL.Username, cfg.OTEL.Password, cfg.OTEL.Protocol)

otelProvider, err = SetupLogProvider(cfg.OTEL, cfg.ServiceName, cfg.Version)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/core/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func SetupMeterProvider(cfg config.MetricsConfig, serviceName, version string) (
return nil, fmt.Errorf("failed to create OTEL resource: %w", err)
}

// Inject Basic Auth header if credentials provided
headers := injectBasicAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)
// Inject Auth header logic: Preferred Headers > Basic Auth fallback
headers := injectAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)

// Parse/Sanitize endpoint
endpoint, insecure, err := processEndpoint(cfg.Endpoint, cfg.Insecure)
Expand Down
82 changes: 60 additions & 22 deletions internal/core/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func SetupLogProvider(cfg config.OTELConfig, serviceName, version string) (*LogP
resource.WithHost(),
resource.WithOS(),
resource.WithProcess(),
resource.WithTelemetrySDK(),
resource.WithAttributes(attrs...),
)
if err != nil {
Expand All @@ -95,12 +96,10 @@ func SetupLogProvider(cfg config.OTELConfig, serviceName, version string) (*LogP
if err != nil {
return nil, fmt.Errorf("invalid OTEL endpoint: %w", err)
}
// Inject Auth header logic: Preferred Headers > Basic Auth fallback
cfg.Headers = injectAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)

var exporter sdklog.Exporter

// Inject Basic Auth header if credentials provided
cfg.Headers = injectBasicAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)

switch cfg.Protocol {
case "http":
exporter, err = createHTTPLogExporter(ctx, endpoint, insecure, cfg)
Expand All @@ -116,14 +115,21 @@ func SetupLogProvider(cfg config.OTELConfig, serviceName, version string) (*LogP
if batchSize <= 0 {
batchSize = 512
}
// A robust queue size buffers telemetry against momentary network latency or throughput spikes.
// We use 4x the batch size or the OTEL standard 2048, whichever is larger, to prevent premature drops.
queueSize := batchSize * 4
if queueSize < 2048 {
queueSize = 2048
}

exportInterval := cfg.ExportInterval
if exportInterval <= 0 {
exportInterval = 5 * time.Second
}

processor := sdklog.NewBatchProcessor(
exporter,
sdklog.WithMaxQueueSize(batchSize*2),
sdklog.WithMaxQueueSize(queueSize),
sdklog.WithExportMaxBatchSize(batchSize),
sdklog.WithExportInterval(exportInterval),
)
Expand All @@ -144,15 +150,18 @@ func SetupTracerProvider(cfg config.TracingConfig, serviceName, version string)
if !cfg.Enabled {
return nil, nil
}

// Inject Basic Auth header if credentials provided
cfg.Headers = injectBasicAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)
// Inject Auth header logic: Preferred Headers > Basic Auth fallback
cfg.Headers = injectAuth(cfg.Headers, cfg.Username, cfg.Password, cfg.Protocol)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Resource
res, err := resource.New(ctx,
resource.WithHost(),
resource.WithOS(),
resource.WithProcess(),
resource.WithTelemetrySDK(),
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion(version),
Expand Down Expand Up @@ -188,6 +197,12 @@ func SetupTracerProvider(cfg config.TracingConfig, serviceName, version string)
if batchSize <= 0 {
batchSize = 512
}
// A robust queue size buffers telemetry against momentary network latency or throughput spikes.
queueSize := batchSize * 4
if queueSize < 2048 {
queueSize = 2048
}

exportInterval := cfg.ExportInterval
if exportInterval <= 0 {
exportInterval = 5 * time.Second
Expand All @@ -196,6 +211,7 @@ func SetupTracerProvider(cfg config.TracingConfig, serviceName, version string)
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithBatcher(exporter,
sdktrace.WithMaxQueueSize(queueSize),
sdktrace.WithMaxExportBatchSize(batchSize),
sdktrace.WithBatchTimeout(exportInterval),
),
Expand Down Expand Up @@ -346,23 +362,45 @@ func processEndpoint(endpoint string, configInsecure bool) (string, bool, error)
return host, insecure, nil
}

// injectBasicAuth adds a Basic Authorization header to the provided headers map
// if username and password are provided. Returns the updated headers map.
// Protocol should be "http" or "grpc" - gRPC requires lowercase "authorization" key.
func injectBasicAuth(headers map[string]string, username, password, protocol string) map[string]string {
if headers == nil {
headers = make(map[string]string)
// injectAuth ensures the provided headers map contains appropriate authentication credentials.
// It returns a newly allocated map to guarantee the original configuration remains immutable.
//
// Hierarchy:
// 1. Header-First: If the provided headers map already contains an "Authorization" (or "authorization") key, it is preserved.
// 2. Basic Auth Fallback: If no authorization header is present, it constructs a Basic Auth credential using the provided username and password.
//
// Protocol should be "http" or "grpc". gRPC requires the lowercase "authorization" key to comply with HTTP/2 and gRPC metadata semantics.
func injectAuth(headers map[string]string, username, password, protocol string) map[string]string {
// Deep copy to ensure we do not mutate shared configuration maps across components
out := make(map[string]string, len(headers)+1)
for k, v := range headers {
out[k] = v
}

key := "Authorization"
altKey := "authorization"
if protocol != "http" {
key = "authorization"
altKey = "Authorization"
}

// 1. Header-First: Check if authentication is already explicitly provided in the headers
if _, hasPrimary := out[key]; hasPrimary {
return out
}
if val, hasAlt := out[altKey]; hasAlt {
// Normalize to the protocol's required case
out[key] = val
delete(out, altKey)
return out
}

// 2. Fallback: Generate Basic Auth if credentials are provided
if username != "" && password != "" {
auth := fmt.Sprintf("%s:%s", username, password)
encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth))

// Use lowercase "authorization" for gRPC to comply with HTTP/2 and gRPC metadata specs.
key := "Authorization"
if protocol != "http" {
key = "authorization"
}
headers[key] = "Basic " + encodedAuth
out[key] = "Basic " + encodedAuth
}
return headers

return out
}
Loading
Loading