Skip to content

Commit be85c5b

Browse files
committed
Forward OTLP headers on browser trace proxy
1 parent 7eea850 commit be85c5b

5 files changed

Lines changed: 106 additions & 0 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,27 @@ All commands accept `--wasteland <org/db>` when multiple wastelands are joined a
522522
| `DOLTHUB_ORG` | Your DoltHub org/username (required for DoltHub provider) |
523523
| `DOLTHUB_SESSION_TOKEN` | DoltHub session token (alternative auth for REST fork API) |
524524
| `PORT` | Override default listen port for `wl serve` |
525+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Hosted OTLP HTTP traces endpoint, e.g. `https://otel.cloud.gascityhall.com/v1/traces` |
526+
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Hosted OTLP HTTP metrics endpoint, e.g. `https://otel.cloud.gascityhall.com/v1/metrics` |
527+
| `OTEL_EXPORTER_OTLP_HEADERS` | Static OTLP headers for hosted exporters, e.g. `X-OTLP-Shared-Token=<token>` |
528+
| `WL_BROWSER_OTLP_TRACES_TARGET` | Optional override for the server-side browser trace proxy target |
529+
| `WL_BROWSER_OTLP_HEADERS` | Optional override for static headers added by the browser trace proxy |
525530
| `XDG_CONFIG_HOME` | Override config dir (default `~/.config`) |
526531
| `XDG_DATA_HOME` | Override data dir (default `~/.local/share`) |
527532

533+
For the shared Gas City collector, hosted Wasteland can use:
534+
535+
```bash
536+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otel.cloud.gascityhall.com/v1/traces
537+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://otel.cloud.gascityhall.com/v1/metrics
538+
OTEL_EXPORTER_OTLP_HEADERS=X-OTLP-Shared-Token=<shared-token>
539+
```
540+
541+
The browser trace proxy reuses `OTEL_EXPORTER_OTLP_HEADERS` automatically, so
542+
browser and server telemetry can share the same ingress token. Use
543+
`WL_BROWSER_OTLP_TRACES_TARGET` or `WL_BROWSER_OTLP_HEADERS` only when the
544+
browser path needs different routing from the server exporters.
545+
528546
## Development
529547

530548
```bash

internal/api/browser_traces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func (s *Server) handleBrowserTraces(w http.ResponseWriter, r *http.Request) {
6161
if enc := r.Header.Get("Content-Encoding"); enc != "" {
6262
req.Header.Set("Content-Encoding", enc)
6363
}
64+
for name, values := range observability.BrowserTraceProxyHeaders() {
65+
for _, value := range values {
66+
req.Header.Add(name, value)
67+
}
68+
}
6469

6570
resp, err := browserTraceProxyClient.Do(req)
6671
if err != nil {

internal/api/browser_traces_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ func TestBrowserTracesHandler_ProxiesPayload(t *testing.T) {
5454
gotBody []byte
5555
gotContentType string
5656
gotContentEncoding string
57+
gotSharedToken string
5758
)
5859
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5960
if r.Method != http.MethodPost {
6061
t.Fatalf("collector method = %s, want POST", r.Method)
6162
}
6263
gotContentType = r.Header.Get("Content-Type")
6364
gotContentEncoding = r.Header.Get("Content-Encoding")
65+
gotSharedToken = r.Header.Get("X-OTLP-Shared-Token")
6466
var err error
6567
gotBody, err = io.ReadAll(r.Body)
6668
if err != nil {
@@ -73,6 +75,7 @@ func TestBrowserTracesHandler_ProxiesPayload(t *testing.T) {
7375
defer collector.Close()
7476

7577
t.Setenv("WL_BROWSER_OTLP_TRACES_TARGET", collector.URL+"/v1/traces")
78+
t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "X-OTLP-Shared-Token=abc123TOKEN")
7679

7780
srv := New(nil)
7881
ts := httptest.NewServer(srv)
@@ -101,6 +104,9 @@ func TestBrowserTracesHandler_ProxiesPayload(t *testing.T) {
101104
if gotContentEncoding != "gzip" {
102105
t.Fatalf("collector content-encoding = %q, want %q", gotContentEncoding, "gzip")
103106
}
107+
if gotSharedToken != "abc123TOKEN" {
108+
t.Fatalf("collector shared token = %q, want %q", gotSharedToken, "abc123TOKEN")
109+
}
104110
if string(gotBody) != "trace-data" {
105111
t.Fatalf("collector body = %q, want %q", string(gotBody), "trace-data")
106112
}

internal/observability/browser.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package observability
22

33
import (
4+
"net/http"
45
"net/url"
56
"os"
67
"strconv"
@@ -12,6 +13,7 @@ const (
1213
BrowserTraceIngressPath = "/api/telemetry/v1/traces"
1314
defaultBrowserTraceSampleRatio = 0.1
1415
browserTraceProxyTargetEnvVar = "WL_BROWSER_OTLP_TRACES_TARGET"
16+
browserTraceHeadersEnvVar = "WL_BROWSER_OTLP_HEADERS"
1517
browserTraceSampleRatioEnvVar = "WL_BROWSER_OTEL_TRACES_SAMPLE_RATIO"
1618
)
1719

@@ -60,6 +62,22 @@ func BrowserTraceProxyTarget() string {
6062
return ""
6163
}
6264

65+
// BrowserTraceProxyHeaders resolves static headers that should be forwarded to
66+
// the upstream collector for browser OTLP traces.
67+
func BrowserTraceProxyHeaders() http.Header {
68+
for _, raw := range []string{
69+
strings.TrimSpace(os.Getenv(browserTraceHeadersEnvVar)),
70+
strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS")),
71+
strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_HEADERS")),
72+
} {
73+
if raw == "" {
74+
continue
75+
}
76+
return parseOTLPHeaders(raw)
77+
}
78+
return nil
79+
}
80+
6381
func appendTracePath(raw string) string {
6482
u, err := url.Parse(raw)
6583
if err != nil {
@@ -75,3 +93,30 @@ func appendTracePath(raw string) string {
7593
u.Path = strings.TrimRight(u.Path, "/") + "/v1/traces"
7694
return u.String()
7795
}
96+
97+
func parseOTLPHeaders(raw string) http.Header {
98+
headers := http.Header{}
99+
for _, pair := range strings.Split(raw, ",") {
100+
pair = strings.TrimSpace(pair)
101+
if pair == "" {
102+
continue
103+
}
104+
name, value, ok := strings.Cut(pair, "=")
105+
if !ok {
106+
continue
107+
}
108+
name = strings.TrimSpace(name)
109+
value = strings.TrimSpace(value)
110+
if name == "" || value == "" {
111+
continue
112+
}
113+
if decoded, err := url.QueryUnescape(value); err == nil {
114+
value = decoded
115+
}
116+
headers.Add(name, value)
117+
}
118+
if len(headers) == 0 {
119+
return nil
120+
}
121+
return headers
122+
}

internal/observability/browser_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,35 @@ func TestBrowserTraceSampleRatio(t *testing.T) {
6767
}
6868
})
6969
}
70+
71+
func TestBrowserTraceProxyHeaders(t *testing.T) {
72+
t.Run("prefers explicit browser headers", func(t *testing.T) {
73+
t.Setenv(browserTraceHeadersEnvVar, "X-OTLP-Shared-Token=browser-token")
74+
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "X-OTLP-Shared-Token=trace-token")
75+
t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "X-OTLP-Shared-Token=generic-token")
76+
headers := BrowserTraceProxyHeaders()
77+
if got := headers.Get("X-OTLP-Shared-Token"); got != "browser-token" {
78+
t.Fatalf("BrowserTraceProxyHeaders() = %q, want explicit browser token", got)
79+
}
80+
})
81+
82+
t.Run("falls back to trace-specific and decodes values", func(t *testing.T) {
83+
t.Setenv(browserTraceHeadersEnvVar, "")
84+
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "Authorization=Bearer%20trace-token")
85+
t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "")
86+
headers := BrowserTraceProxyHeaders()
87+
if got := headers.Get("Authorization"); got != "Bearer trace-token" {
88+
t.Fatalf("BrowserTraceProxyHeaders() = %q, want decoded trace header", got)
89+
}
90+
})
91+
92+
t.Run("falls back to generic otlp headers", func(t *testing.T) {
93+
t.Setenv(browserTraceHeadersEnvVar, "")
94+
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "")
95+
t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "X-OTLP-Shared-Token=generic-token")
96+
headers := BrowserTraceProxyHeaders()
97+
if got := headers.Get("X-OTLP-Shared-Token"); got != "generic-token" {
98+
t.Fatalf("BrowserTraceProxyHeaders() = %q, want generic header", got)
99+
}
100+
})
101+
}

0 commit comments

Comments
 (0)