From 983c09038bd3c24cacd535732ced0f70d5bb2101 Mon Sep 17 00:00:00 2001 From: Gavin Williams Date: Mon, 22 Jun 2026 16:18:29 +0100 Subject: [PATCH] feat(collector): support inline Base64 config via `OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT` environment variable that accepts a standard Base64-encoded YAML collector config. The decoded content is fed into the existing `yamlprovider` (`yaml:` URI scheme), so no new provider is needed. Resolution order: 1. `OPENTELEMETRY_COLLECTOR_CONFIG_URI` (existing, unchanged — warns if `_CONTENT` is also set) 2. `OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT` (new — Base64-decoded YAML) 3. `OPENTELEMETRY_COLLECTOR_CONFIG_FILE` (deprecated fallback, unchanged) 4. `/opt/collector-config/config.yaml` (default) If Base64 decoding fails an error is logged and the extension falls through to the next option. `NewCollector`'s signature is unchanged. --- collector/README.md | 34 ++++++++-- collector/internal/collector/collector.go | 33 ++++++++-- .../internal/collector/collector_test.go | 64 +++++++++++++++++++ 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/collector/README.md b/collector/README.md index 1b1f0100d7..f8ffd57f86 100644 --- a/collector/README.md +++ b/collector/README.md @@ -176,14 +176,40 @@ from an S3 object using a CloudFormation template: Loading configuration from S3 will require that the IAM role attached to your function includes read access to the relevant bucket. +### Inline configuration via Base64 + +You can supply the collector configuration inline as a Base64-encoded string using `OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT`. This is useful when you want to avoid deploying a config file or referencing an external URI: + +``` +aws lambda update-function-configuration --function-name Function \ + --environment Variables={OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT=$(base64 < collector.yaml)} +``` + +Or in a CloudFormation template: + +```yaml + Function: + Type: AWS::Serverless::Function + Properties: + ... + Environment: + Variables: + OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT: +``` + +Use the standard Base64 alphabet (as produced by `base64` on Linux/macOS or `openssl base64`). URL-safe Base64 is not supported. + +`OPENTELEMETRY_COLLECTOR_CONFIG_URI` takes precedence if both variables are set. + ## Environment Variables The following environment variables can be used to configure the OpenTelemetry Collector Lambda extension: -| Variable Name | Value | Description | -| ------------------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENTELEMETRY_COLLECTOR_CONFIG_URI` | URI (e.g., `/var/task/collector.yaml`, `http://...`, `s3://...`) | Specifies the location of the OpenTelemetry Collector configuration file. This can be a path within the function's deployment package, an HTTP URI, or an S3 URI. If loading from S3, the function's IAM role needs read access to the specified S3 object. | -| `OPENTELEMETRY_EXTENSION_LOG_LEVEL` | `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal` (Default: `info`) | Controls the logging level of the OpenTelemetry Lambda extension itself. | +| Variable Name | Value | Description | +| --------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENTELEMETRY_COLLECTOR_CONFIG_URI` | URI (e.g., `/var/task/collector.yaml`, `http://...`, `s3://...`) | Specifies the location of the OpenTelemetry Collector configuration file. This can be a path within the function's deployment package, an HTTP URI, or an S3 URI. If loading from S3, the function's IAM role needs read access to the specified S3 object. | +| `OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT`| Base64-encoded YAML | Supplies the collector configuration inline. The value must be standard Base64-encoded YAML. Ignored if `OPENTELEMETRY_COLLECTOR_CONFIG_URI` is also set (a warning is logged). If decoding fails, an error is logged and the extension falls back to the URI or default path. | +| `OPENTELEMETRY_EXTENSION_LOG_LEVEL` | `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal` (Default: `info`) | Controls the logging level of the OpenTelemetry Lambda extension itself. | ## Auto-Configuration diff --git a/collector/internal/collector/collector.go b/collector/internal/collector/collector.go index d6e83c2c8f..29019a0019 100644 --- a/collector/internal/collector/collector.go +++ b/collector/internal/collector/collector.go @@ -16,8 +16,10 @@ package collector import ( "context" + "encoding/base64" "fmt" "os" + "strings" "github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/s3provider" "github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/secretsmanagerprovider" @@ -49,23 +51,42 @@ type Collector struct { coreFunc func(zapcore.LevelEnabler) zapcore.Core } +const ( + envCollectorConfigURI = "OPENTELEMETRY_COLLECTOR_CONFIG_URI" + envCollectorConfigContent = "OPENTELEMETRY_COLLECTOR_CONFIG_CONTENT" + envCollectorConfigFile = "OPENTELEMETRY_COLLECTOR_CONFIG_FILE" // deprecated +) + func getConfig(logger *zap.Logger) string { - val, ex := os.LookupEnv("OPENTELEMETRY_COLLECTOR_CONFIG_URI") - if ex { + if val, ex := os.LookupEnv(envCollectorConfigURI); ex { + if _, also := os.LookupEnv(envCollectorConfigContent); also { + logger.Warn("Both " + envCollectorConfigURI + " and " + envCollectorConfigContent + " are set; using " + envCollectorConfigURI) + } logger.Info("Using config URI from environment variable", zap.String("uri", val)) return val } + if raw, ex := os.LookupEnv(envCollectorConfigContent); ex { + if trimmed := strings.TrimSpace(raw); trimmed != "" { + decoded, err := base64.StdEncoding.DecodeString(trimmed) + if err != nil { + logger.Error("Failed to decode "+envCollectorConfigContent+" as Base64; ignoring", zap.Error(err)) + } else { + logger.Info("Using inline config from " + envCollectorConfigContent) + return "yaml:" + string(decoded) + } + } + } + // The name of the environment variable was changed // This is the old name, kept for backwards compatibility - oldVal, oldEx := os.LookupEnv("OPENTELEMETRY_COLLECTOR_CONFIG_FILE") - if oldEx { + if oldVal, oldEx := os.LookupEnv(envCollectorConfigFile); oldEx { logger.Info("Using config URI from deprecated environment variable", zap.String("uri", oldVal)) - logger.Warn("The OPENTELEMETRY_COLLECTOR_CONFIG_FILE environment variable is deprecated. Please use OPENTELEMETRY_COLLECTOR_CONFIG_URI instead.") + logger.Warn("The " + envCollectorConfigFile + " environment variable is deprecated. Please use " + envCollectorConfigURI + " instead.") return oldVal } - // If neither environment variable is set, use the default + // If no environment variable is set, use the default defaultVal := "/opt/collector-config/config.yaml" logger.Info("Using default config URI", zap.String("uri", defaultVal)) return defaultVal diff --git a/collector/internal/collector/collector_test.go b/collector/internal/collector/collector_test.go index ec8f696fb0..498f0f5933 100644 --- a/collector/internal/collector/collector_test.go +++ b/collector/internal/collector/collector_test.go @@ -16,6 +16,7 @@ package collector import ( "context" + "encoding/base64" "testing" "github.com/stretchr/testify/assert" @@ -100,6 +101,69 @@ func TestCollectorLogLevelDoesNotSuppressExtensionLogs(t *testing.T) { "extension logs should be controlled by the extension logger, not collector config") } +func TestCollectorConfigContentStartsCollector(t *testing.T) { + yamlContent := ` +receivers: + nop: +exporters: + nop: +service: + telemetry: + logs: + level: info + pipelines: + traces: + receivers: [nop] + exporters: [nop] +` + t.Setenv(envCollectorConfigContent, base64.StdEncoding.EncodeToString([]byte(yamlContent))) + + core, logs := observer.New(zapcore.InfoLevel) + col := NewCollector(zap.New(core), testFactories(t), "test") + + ctx := context.Background() + err := col.Start(ctx) + require.NoError(t, err) + + err = col.Stop() + require.NoError(t, err) + + assert.NotEmpty(t, logs.FilterMessage("Using inline config from "+envCollectorConfigContent).All()) +} + +func TestCollectorConfigURITakesPrecedenceOverContent(t *testing.T) { + t.Setenv(envCollectorConfigURI, "file:testdata/config-info-level.yaml") + t.Setenv(envCollectorConfigContent, base64.StdEncoding.EncodeToString([]byte("irrelevant: true"))) + + core, logs := observer.New(zapcore.WarnLevel) + col := NewCollector(zap.New(core), testFactories(t), "test") + + ctx := context.Background() + err := col.Start(ctx) + require.NoError(t, err) + + err = col.Stop() + require.NoError(t, err) + + warnLogs := logs.FilterMessageSnippet(envCollectorConfigContent + " are set").All() + assert.NotEmpty(t, warnLogs, "expected a warning about both env vars being set") +} + +func TestCollectorConfigContentInvalidBase64FallsBack(t *testing.T) { + t.Setenv(envCollectorConfigContent, "not-valid-base64!@#$") + + core, logs := observer.New(zapcore.ErrorLevel) + col := NewCollector(zap.New(core), testFactories(t), "test") + + ctx := context.Background() + // Collector will fall back to the default path which won't exist — expect start error + err := col.Start(ctx) + assert.Error(t, err, "expected collector to fail when falling back to missing default config") + + errorLogs := logs.FilterMessageSnippet("Failed to decode").All() + assert.NotEmpty(t, errorLogs, "expected an error log about invalid Base64") +} + func testFactories(t *testing.T) otelcol.Factories { receivers, err := otelcol.MakeFactoryMap(receivertest.NewNopFactory()) require.NoError(t, err)