diff --git a/pkg/export/otel/common.go b/pkg/export/otel/common.go index bfe801293..10780f2d6 100644 --- a/pkg/export/otel/common.go +++ b/pkg/export/otel/common.go @@ -230,6 +230,7 @@ type otlpOptions struct { URLPath string SkipTLSVerify bool HTTPHeaders map[string]string + GRPCHeaders map[string]string } func (o *otlpOptions) AsMetricHTTP() []otlpmetrichttp.Option { @@ -261,6 +262,9 @@ func (o *otlpOptions) AsMetricGRPC() []otlpmetricgrpc.Option { if o.SkipTLSVerify { opts = append(opts, otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) } + if len(o.GRPCHeaders) > 0 { + opts = append(opts, otlpmetricgrpc.WithHeaders(o.GRPCHeaders)) + } return opts } @@ -293,6 +297,9 @@ func (o *otlpOptions) AsTraceGRPC() []otlptracegrpc.Option { if o.SkipTLSVerify { opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) } + if len(o.GRPCHeaders) > 0 { + opts = append(opts, otlptracegrpc.WithHeaders(o.GRPCHeaders)) + } return opts } diff --git a/pkg/export/otel/grafana.go b/pkg/export/otel/grafana.go index 9d4e600ad..1118569e5 100644 --- a/pkg/export/otel/grafana.go +++ b/pkg/export/otel/grafana.go @@ -86,5 +86,9 @@ func (cfg *GrafanaOTLP) setupOptions(opt *otlpOptions) { opt.HTTPHeaders = map[string]string{} } opt.HTTPHeaders["Authorization"] = cfg.AuthHeader() + if opt.GRPCHeaders == nil { + opt.GRPCHeaders = map[string]string{} + } + opt.GRPCHeaders["Authorization"] = cfg.AuthHeader() } } diff --git a/pkg/export/otel/metrics_test.go b/pkg/export/otel/metrics_test.go index 3f0ce4a95..faae9a346 100644 --- a/pkg/export/otel/metrics_test.go +++ b/pkg/export/otel/metrics_test.go @@ -93,6 +93,10 @@ func TestHTTPMetricsWithGrafanaOptions(t *testing.T) { // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", }, + GRPCHeaders: map[string]string{ + // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 + "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", + }, }, &mcfg) }) mcfg.CommonEndpoint = "https://localhost:3939" @@ -104,6 +108,10 @@ func TestHTTPMetricsWithGrafanaOptions(t *testing.T) { // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", }, + GRPCHeaders: map[string]string{ + // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 + "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", + }, }, &mcfg) }) } diff --git a/pkg/export/otel/traces.go b/pkg/export/otel/traces.go index 611edb800..554038a02 100644 --- a/pkg/export/otel/traces.go +++ b/pkg/export/otel/traces.go @@ -349,6 +349,7 @@ func getTracesExporter(ctx context.Context, cfg TracesConfig, ctxInfo *global.Co Insecure: opts.Insecure, InsecureSkipVerify: cfg.InsecureSkipVerify, }, + Headers: convertHeaders(opts.GRPCHeaders), } set := getTraceSettings(ctxInfo, t) return factory.CreateTraces(ctx, set, config) @@ -797,7 +798,7 @@ func getHTTPTracesEndpointOptions(cfg *TracesConfig) (otlpOptions, error) { } func getGRPCTracesEndpointOptions(cfg *TracesConfig) (otlpOptions, error) { - opts := otlpOptions{} + opts := otlpOptions{GRPCHeaders: map[string]string{}} log := tlog().With("transport", "grpc") murl, _, err := parseTracesEndpoint(cfg) if err != nil { @@ -817,6 +818,9 @@ func getGRPCTracesEndpointOptions(cfg *TracesConfig) (otlpOptions, error) { opts.SkipTLSVerify = true } + cfg.Grafana.setupOptions(&opts) + maps.Copy(opts.GRPCHeaders, headersFromEnv(envHeaders)) + maps.Copy(opts.GRPCHeaders, headersFromEnv(envTracesHeaders)) return opts, nil } diff --git a/pkg/export/otel/traces_test.go b/pkg/export/otel/traces_test.go index ada0cd8b1..103808849 100644 --- a/pkg/export/otel/traces_test.go +++ b/pkg/export/otel/traces_test.go @@ -96,6 +96,10 @@ func TestHTTPTracesWithGrafanaOptions(t *testing.T) { // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", }, + GRPCHeaders: map[string]string{ + // Basic + output of: echo -n 12345:affafafaafkd | gbase64 -w 0 + "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", + }, }, &mcfg) }) mcfg.CommonEndpoint = "https://localhost:3939" @@ -108,6 +112,10 @@ func TestHTTPTracesWithGrafanaOptions(t *testing.T) { // Base64 representation of 12345:affafafaafkd "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", }, + GRPCHeaders: map[string]string{ + // Base64 representation of 12345:affafafaafkd + "Authorization": "Basic MTIzNDU6YWZmYWZhZmFhZmtk", + }, }, &mcfg) }) } @@ -200,7 +208,7 @@ func TestGRPCTracesEndpointOptions(t *testing.T) { } t.Run("testing with two endpoints", func(t *testing.T) { - testTracesGRPOptions(t, otlpOptions{Endpoint: "localhost:3232"}, &tcfg) + testTracesGRPCOptions(t, otlpOptions{Endpoint: "localhost:3232", GRPCHeaders: map[string]string{}}, &tcfg) }) tcfg = TracesConfig{ @@ -209,7 +217,7 @@ func TestGRPCTracesEndpointOptions(t *testing.T) { } t.Run("testing with only common endpoint", func(t *testing.T) { - testTracesGRPOptions(t, otlpOptions{Endpoint: "localhost:3131"}, &tcfg) + testTracesGRPCOptions(t, otlpOptions{Endpoint: "localhost:3131", GRPCHeaders: map[string]string{}}, &tcfg) }) tcfg = TracesConfig{ @@ -218,7 +226,7 @@ func TestGRPCTracesEndpointOptions(t *testing.T) { Instrumentations: []string{instrumentations.InstrumentationALL}, } t.Run("testing with insecure endpoint", func(t *testing.T) { - testTracesGRPOptions(t, otlpOptions{Endpoint: "localhost:3232", Insecure: true}, &tcfg) + testTracesGRPCOptions(t, otlpOptions{Endpoint: "localhost:3232", Insecure: true, GRPCHeaders: map[string]string{}}, &tcfg) }) tcfg = TracesConfig{ @@ -228,11 +236,58 @@ func TestGRPCTracesEndpointOptions(t *testing.T) { } t.Run("testing with skip TLS verification", func(t *testing.T) { - testTracesGRPOptions(t, otlpOptions{Endpoint: "localhost:3232", SkipTLSVerify: true}, &tcfg) + testTracesGRPCOptions(t, otlpOptions{Endpoint: "localhost:3232", SkipTLSVerify: true, GRPCHeaders: map[string]string{}}, &tcfg) }) } -func testTracesGRPOptions(t *testing.T, expected otlpOptions, tcfg *TracesConfig) { +func TestGRPCTracesEndpointHeaders(t *testing.T) { + type testCase struct { + Description string + Env map[string]string + ExpectedHeaders map[string]string + Grafana GrafanaOTLP + } + for _, tc := range []testCase{ + {Description: "No headers", + ExpectedHeaders: map[string]string{}}, + {Description: "defining common OTLP_HEADERS", + Env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "Foo=Bar ==,Authorization=Base 2222=="}, + ExpectedHeaders: map[string]string{"Foo": "Bar ==", "Authorization": "Base 2222=="}}, + {Description: "defining common OTLP_TRACES_HEADERS", + Env: map[string]string{"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "Foo=Bar ==,Authorization=Base 1234=="}, + ExpectedHeaders: map[string]string{"Foo": "Bar ==", "Authorization": "Base 1234=="}}, + {Description: "OTLP_TRACES_HEADERS takes precedence over OTLP_HEADERS", + Env: map[string]string{ + "OTEL_EXPORTER_OTLP_HEADERS": "Foo=Bar ==,Authorization=Base 3210==", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS": "Authorization=Base 1111==", + }, + ExpectedHeaders: map[string]string{"Foo": "Bar ==", "Authorization": "Base 1111=="}}, + } { + // mutex to avoid running testcases in parallel so we don't mess up with env vars + mt := sync.Mutex{} + t.Run(fmt.Sprint(tc.Description), func(t *testing.T) { + mt.Lock() + restore := restoreEnvAfterExecution() + defer func() { + restore() + mt.Unlock() + }() + for k, v := range tc.Env { + require.NoError(t, os.Setenv(k, v)) + } + + opts, err := getGRPCTracesEndpointOptions(&TracesConfig{ + TracesEndpoint: "https://localhost:1234/v1/traces", + Grafana: &tc.Grafana, + Instrumentations: []string{instrumentations.InstrumentationALL}, + }) + require.NoError(t, err) + assert.Equal(t, tc.ExpectedHeaders, opts.GRPCHeaders) + }) + } +} + +func testTracesGRPCOptions(t *testing.T, expected otlpOptions, tcfg *TracesConfig) { defer restoreEnvAfterExecution()() opts, err := getGRPCTracesEndpointOptions(tcfg) require.NoError(t, err)