Skip to content

Commit 2810d31

Browse files
fix(otel): harden dynamic header helper
1 parent b65dd2e commit 2810d31

3 files changed

Lines changed: 32 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,11 @@ The helper must be executable and print a JSON object to stdout:
129129

130130
```bash
131131
#!/bin/sh
132-
printf '{"Authorization":"Bearer %s"}' "$(gcloud auth print-access-token)"
132+
printf '{"Authorization":"Bearer %s"}' "$(get-token.sh)"
133133
```
134134

135+
For a Cloud Run collector using IAM authentication, `get-token.sh` might be `gcloud auth print-identity-token`.
136+
135137
If `OPENCODE_OTLP_HEADERS` is also set, helper-provided headers override static headers with the same name. Header values are never logged.
136138

137139
### Disabling specific metrics

src/headers.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { createRequire } from "module"
22
import { ExportResultCode, type ExportResult } from "@opentelemetry/core"
3-
import type { PushMetricExporter, ResourceMetrics, InstrumentType } from "@opentelemetry/sdk-metrics"
4-
import type { AggregationOption } from "@opentelemetry/sdk-metrics/build/src/view/AggregationOption"
5-
import type { AggregationTemporality } from "@opentelemetry/sdk-metrics/build/src/export/AggregationTemporality"
3+
import type { PushMetricExporter, ResourceMetrics } from "@opentelemetry/sdk-metrics"
64
import type { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"
75
import type { LogRecordExporter, ReadableLogRecord } from "@opentelemetry/sdk-logs"
86
import type { Metadata } from "@grpc/grpc-js"
97

108
const require = createRequire(import.meta.url)
9+
const DEFAULT_HELPER_TIMEOUT_MS = 5000
1110

1211
type Exporter<T> = {
1312
export(items: T, resultCallback: (result: ExportResult) => void): void
1413
shutdown(): Promise<void>
1514
forceFlush?(): Promise<void>
1615
}
1716

17+
type SelectAggregation = NonNullable<PushMetricExporter["selectAggregation"]>
18+
type SelectAggregationTemporality = NonNullable<PushMetricExporter["selectAggregationTemporality"]>
19+
1820
export type HeadersMap = Record<string, string>
1921

2022
export function parseOtlpHeaders(raw: string | undefined): HeadersMap {
@@ -55,6 +57,7 @@ export class DynamicHeaders {
5557
constructor(
5658
private readonly staticHeaders: HeadersMap,
5759
private readonly helper: string | undefined,
60+
private readonly helperTimeoutMs = DEFAULT_HELPER_TIMEOUT_MS,
5861
) {
5962
this.headers = { ...staticHeaders }
6063
}
@@ -86,12 +89,20 @@ export class DynamicHeaders {
8689
}
8790

8891
private async runHelper(): Promise<HeadersMap> {
89-
const proc = Bun.spawn([this.helper!], { stdout: "pipe", stderr: "pipe" })
92+
const proc = Bun.spawn([this.helper!], {
93+
stdout: "pipe",
94+
stderr: "pipe",
95+
timeout: this.helperTimeoutMs,
96+
killSignal: "SIGTERM",
97+
})
9098
const [stdout, stderr, exitCode] = await Promise.all([
9199
new Response(proc.stdout).text(),
92100
new Response(proc.stderr).text(),
93101
proc.exited,
94102
])
103+
if (proc.signalCode) {
104+
throw new Error(`OTLP headers helper was terminated by ${proc.signalCode}`)
105+
}
95106
if (exitCode !== 0) {
96107
const detail = stderr.trim() || `exit code ${exitCode}`
97108
throw new Error(`OTLP headers helper failed: ${detail}`)
@@ -133,11 +144,13 @@ export class RefreshingMetricExporter implements PushMetricExporter {
133144
return this.exporter.shutdown()
134145
}
135146

136-
selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality {
147+
selectAggregationTemporality(
148+
instrumentType: Parameters<SelectAggregationTemporality>[0],
149+
): ReturnType<SelectAggregationTemporality> {
137150
return this.exporter.selectAggregationTemporality!(instrumentType)
138151
}
139152

140-
selectAggregation(instrumentType: InstrumentType): AggregationOption {
153+
selectAggregation(instrumentType: Parameters<SelectAggregation>[0]): ReturnType<SelectAggregation> {
141154
return this.exporter.selectAggregation!(instrumentType)
142155
}
143156

tests/headers.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ describe("DynamicHeaders", () => {
9292
await Promise.all([headers.refresh(), headers.refresh(), headers.refresh()])
9393
expect(await Bun.file(countFile).text()).toBe("1")
9494
})
95+
96+
test("fails helper refresh when the helper times out", async () => {
97+
tempDir = await mkdtemp(join(tmpdir(), "otel-headers-"))
98+
const helper = join(tempDir, "helper.sh")
99+
await Bun.write(helper, "#!/bin/sh\nsleep 1\n")
100+
await Bun.spawn(["chmod", "+x", helper]).exited
101+
102+
const headers = new DynamicHeaders({}, helper, 50)
103+
await expect(headers.refresh()).rejects.toThrow("OTLP headers helper was terminated")
104+
})
95105
})
96106

97107
describe("RefreshingSpanExporter", () => {

0 commit comments

Comments
 (0)