diff --git a/README.md b/README.md index 6007fdd..9bf682b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It's worth to mention that the extension is using the [OpenTelemetry Go SDK](htt * `K6_OTEL_EXPORT_INTERVAL` - configures the intervening time between metrics exports. Default is `10s`. * `K6_OTEL_EXPORTER_TYPE` - metric exporter type. Default is `grpc`. +* `K6_OTEL_HEADERS` - headers in W3C Correlation-Context format without additional semi-colon delimited metadata (i.e. "k1=v1,k2=v2"). Passes the headers to the exporter. #### TLS configuration diff --git a/pkg/opentelemetry/config.go b/pkg/opentelemetry/config.go index bdf8fcd..daa2523 100644 --- a/pkg/opentelemetry/config.go +++ b/pkg/opentelemetry/config.go @@ -39,6 +39,9 @@ type Config struct { // ExportInterval configures the intervening time between metrics exports ExportInterval types.NullDuration `json:"exportInterval" envconfig:"K6_OTEL_EXPORT_INTERVAL"` + // Headers in W3C Correlation-Context format without additional semi-colon delimited metadata (i.e. "k1=v1,k2=v2") + Headers null.String `json:"headers" envconfig:"K6_OTEL_HEADERS"` + // TLSInsecureSkipVerify disables verification of the server's certificate chain TLSInsecureSkipVerify null.Bool `json:"tlsInsecureSkipVerify" envconfig:"K6_OTEL_TLS_INSECURE_SKIP_VERIFY"` // TLSCertificate is the path to the certificate file (rootCAs) to use for the exporter's TLS connection @@ -179,6 +182,10 @@ func (cfg Config) Apply(v Config) Config { cfg.TLSClientKey = v.TLSClientKey } + if v.Headers.Valid { + cfg.Headers = v.Headers + } + return cfg } diff --git a/pkg/opentelemetry/config_test.go b/pkg/opentelemetry/config_test.go index ba5dd44..b30e347 100644 --- a/pkg/opentelemetry/config_test.go +++ b/pkg/opentelemetry/config_test.go @@ -68,6 +68,7 @@ func TestConfig(t *testing.T) { "K6_OTEL_TLS_CERTIFICATE": "cert_path", "K6_OTEL_TLS_CLIENT_CERTIFICATE": "client_cert_path", "K6_OTEL_TLS_CLIENT_KEY": "client_key_path", + "K6_OTEL_HEADERS": "key1=value1,key2=value2", }, expectedConfig: Config{ ServiceName: null.StringFrom("foo"), @@ -84,6 +85,7 @@ func TestConfig(t *testing.T) { TLSCertificate: null.StringFrom("cert_path"), TLSClientCertificate: null.StringFrom("client_cert_path"), TLSClientKey: null.StringFrom("client_key_path"), + Headers: null.StringFrom("key1=value1,key2=value2"), }, }, @@ -103,7 +105,8 @@ func TestConfig(t *testing.T) { `"tlsInsecureSkipVerify":true,` + `"tlsCertificate":"cert_path",` + `"tlsClientCertificate":"client_cert_path",` + - `"tlsClientKey":"client_key_path"` + + `"tlsClientKey":"client_key_path",` + + `"headers":"key1=value1,key2=value2"` + `}`, ), expectedConfig: Config{ @@ -121,6 +124,7 @@ func TestConfig(t *testing.T) { TLSCertificate: null.StringFrom("cert_path"), TLSClientCertificate: null.StringFrom("client_cert_path"), TLSClientKey: null.StringFrom("client_key_path"), + Headers: null.StringFrom("key1=value1,key2=value2"), }, }, diff --git a/pkg/opentelemetry/exporter.go b/pkg/opentelemetry/exporter.go index 77d937f..8779941 100644 --- a/pkg/opentelemetry/exporter.go +++ b/pkg/opentelemetry/exporter.go @@ -4,6 +4,9 @@ import ( "context" "crypto/tls" "errors" + "fmt" + "net/url" + "strings" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" @@ -27,20 +30,33 @@ func getExporter(cfg Config) (metric.Exporter, error) { return nil, err } + var headers map[string]string + if cfg.Headers.Valid { + headers, err = parseHeaders(cfg.Headers.String) + if err != nil { + return nil, fmt.Errorf("failed to parse headers: %w", err) + } + } + exporterType := cfg.ExporterType.String if exporterType == grpcExporterType { - return buildGRPCExporter(ctx, cfg, tlsConfig) + return buildGRPCExporter(ctx, cfg, tlsConfig, headers) } if exporterType == httpExporterType { - return buildHTTPExporter(ctx, cfg, tlsConfig) + return buildHTTPExporter(ctx, cfg, tlsConfig, headers) } return nil, errors.New("unsupported exporter type " + exporterType + " specified") } -func buildHTTPExporter(ctx context.Context, cfg Config, tlsConfig *tls.Config) (metric.Exporter, error) { +func buildHTTPExporter( + ctx context.Context, + cfg Config, + tlsConfig *tls.Config, + headers map[string]string, +) (metric.Exporter, error) { opts := []otlpmetrichttp.Option{ otlpmetrichttp.WithEndpoint(cfg.HTTPExporterEndpoint.String), otlpmetrichttp.WithURLPath(cfg.HTTPExporterURLPath.String), @@ -50,6 +66,10 @@ func buildHTTPExporter(ctx context.Context, cfg Config, tlsConfig *tls.Config) ( opts = append(opts, otlpmetrichttp.WithInsecure()) } + if len(headers) > 0 { + opts = append(opts, otlpmetrichttp.WithHeaders(headers)) + } + if tlsConfig != nil { opts = append(opts, otlpmetrichttp.WithTLSClientConfig(tlsConfig)) } @@ -57,7 +77,12 @@ func buildHTTPExporter(ctx context.Context, cfg Config, tlsConfig *tls.Config) ( return otlpmetrichttp.New(ctx, opts...) } -func buildGRPCExporter(ctx context.Context, cfg Config, tlsConfig *tls.Config) (metric.Exporter, error) { +func buildGRPCExporter( + ctx context.Context, + cfg Config, + tlsConfig *tls.Config, + headers map[string]string, +) (metric.Exporter, error) { opt := []otlpmetricgrpc.Option{ otlpmetricgrpc.WithEndpoint(cfg.GRPCExporterEndpoint.String), } @@ -66,9 +91,37 @@ func buildGRPCExporter(ctx context.Context, cfg Config, tlsConfig *tls.Config) ( opt = append(opt, otlpmetricgrpc.WithInsecure()) } + if len(headers) > 0 { + opt = append(opt, otlpmetricgrpc.WithHeaders(headers)) + } + if tlsConfig != nil { opt = append(opt, otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig))) } return otlpmetricgrpc.New(ctx, opt...) } + +func parseHeaders(raw string) (map[string]string, error) { + headers := make(map[string]string) + for _, header := range strings.Split(raw, ",") { + rawKey, rawValue, ok := strings.Cut(header, "=") + if !ok { + return nil, fmt.Errorf("invalid header %q, expected format key=value", header) + } + + key, err := url.PathUnescape(rawKey) + if err != nil { + return nil, fmt.Errorf("failed to unescape header key %q: %w", rawKey, err) + } + + value, err := url.PathUnescape(rawValue) + if err != nil { + return nil, fmt.Errorf("failed to unescape header value %q: %w", rawValue, err) + } + + headers[key] = value + } + + return headers, nil +}