From af317f05ef775bd174f60f954582077bce91577c Mon Sep 17 00:00:00 2001 From: Srijan Rastogi <44723623+srijan-27@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:33:32 +0530 Subject: [PATCH] Add WithHeaders option for Zipkin exporter (#5530) - Added `WithHeaders` option func, which allows user to set custom http request headers while exporting spans. - Closes: #3474 --------- Co-authored-by: Tyler Yahn --- CHANGELOG.md | 1 + exporters/zipkin/zipkin.go | 37 ++++++++++++++++++++------ exporters/zipkin/zipkin_test.go | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41609fae5f5..feec37287e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Store and provide the emitted `context.Context` in `ScopeRecords` of `go.opentelemetry.io/otel/sdk/log/logtest`. (#5468) - `SimpleProcessor.OnEmit` in `go.opentelemetry.io/otel/sdk/log` no longer allocates a slice which makes it possible to have a zero-allocation log processing using `SimpleProcessor`. (#5493) - The `AssertRecordEqual` method to `go.opentelemetry.io/otel/log/logtest` to allow comparison of two log records in tests. (#5499) +- The `WithHeaders` option to `go.opentelemetry.io/otel/exporters/zipkin` to allow configuring custom http headers while exporting spans. (#5530) - `service.instance.id` is populated for a `Resource` created with `"go.opentelemetry.io/otel/sdk/resource".Default` with a default value when `OTEL_GO_X_RESOURCE` is set. (#5520) ### Changed diff --git a/exporters/zipkin/zipkin.go b/exporters/zipkin/zipkin.go index 2b81fa92e2d..27ef3642a9b 100644 --- a/exporters/zipkin/zipkin.go +++ b/exporters/zipkin/zipkin.go @@ -12,6 +12,7 @@ import ( "log" "net/http" "net/url" + "strings" "sync" "github.com/go-logr/logr" @@ -26,9 +27,10 @@ const ( // Exporter exports spans to the zipkin collector. type Exporter struct { - url string - client *http.Client - logger logr.Logger + url string + client *http.Client + logger logr.Logger + headers map[string]string stoppedMu sync.RWMutex stopped bool @@ -40,8 +42,9 @@ var emptyLogger = logr.Logger{} // Options contains configuration for the exporter. type config struct { - client *http.Client - logger logr.Logger + client *http.Client + logger logr.Logger + headers map[string]string } // Option defines a function that configures the exporter. @@ -70,6 +73,14 @@ func WithLogr(logger logr.Logger) Option { }) } +// WithHeaders configures the exporter to use the passed HTTP request headers. +func WithHeaders(headers map[string]string) Option { + return optionFunc(func(cfg config) config { + cfg.headers = headers + return cfg + }) +} + // WithClient configures the exporter to use the passed HTTP client. func WithClient(client *http.Client) Option { return optionFunc(func(cfg config) config { @@ -101,9 +112,10 @@ func New(collectorURL string, opts ...Option) (*Exporter, error) { cfg.client = http.DefaultClient } return &Exporter{ - url: collectorURL, - client: cfg.client, - logger: cfg.logger, + url: collectorURL, + client: cfg.client, + logger: cfg.logger, + headers: cfg.headers, }, nil } @@ -132,6 +144,15 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpa return e.errf("failed to create request to %s: %v", e.url, err) } req.Header.Set("Content-Type", "application/json") + + for k, v := range e.headers { + if strings.ToLower(k) == "host" { + req.Host = v + } else { + req.Header.Set(k, v) + } + } + resp, err := e.client.Do(req) if err != nil { return e.errf("request to %s failed: %v", e.url, err) diff --git a/exporters/zipkin/zipkin_test.go b/exporters/zipkin/zipkin_test.go index 0ebc2dfd3e9..1cdd7619ca6 100644 --- a/exporters/zipkin/zipkin_test.go +++ b/exporters/zipkin/zipkin_test.go @@ -12,6 +12,7 @@ import ( "log" "net" "net/http" + "net/http/httptest" "sync" "testing" "time" @@ -375,3 +376,48 @@ func TestLogrFormatting(t *testing.T) { got := buf.String() assert.Equal(t, want, got) } + +func TestWithHeaders(t *testing.T) { + headers := map[string]string{ + "name1": "value1", + "name2": "value2", + "host": "example", + } + + exp, err := New("", WithHeaders(headers)) + require.NoError(t, err) + + want := headers + got := exp.headers + assert.Equal(t, want, got) + + spans := tracetest.SpanStubs{ + { + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, + SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}, + }), + }, + }.Snapshots() + + var req *http.Request + handler := func(w http.ResponseWriter, r *http.Request) { + req = r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + e := &Exporter{ + url: srv.URL, + client: srv.Client(), + headers: headers, + } + + _ = e.ExportSpans(context.Background(), spans) + + assert.Equal(t, headers["host"], req.Host) + assert.Equal(t, headers["name1"], req.Header.Get("name1")) + assert.Equal(t, headers["name2"], req.Header.Get("name2")) +}