@heystack/otel 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/workers-sampler.d.ts +13 -0
- package/dist/workers-sampler.js +42 -0
- package/dist/workers.d.ts +18 -1
- package/dist/workers.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,9 +117,27 @@ Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`.
|
|
|
117
117
|
| `apiKey` | `string?` | Defaults to `env.HEYSTACK_API_KEY`. |
|
|
118
118
|
| `getUser` | `(req: Request) => { id?, session?, requestId? } \| undefined` | Called per request. `id` → `enduser.id`, `session` → `session.id`, `requestId` → `http.request.id` (falls back to the `cf-ray` header). |
|
|
119
119
|
| `instrumentBindings` | `boolean \| string[]` | `true` = auto child spans for all detected D1/KV/R2/Vectorize bindings; `string[]` = only the named bindings. Default `false`. |
|
|
120
|
+
| `sampling` | `{ rate?: number }` | Head-sampling rate 0–1. `1` (default) keeps everything. `0.2` keeps ~20% of fresh root traces. Parent-respecting: a request that arrives with an inbound sampled `traceparent` is always recorded regardless of the rate. See [Head sampling](#head-sampling) below. |
|
|
120
121
|
| `waitUntil` | `(p: Promise<unknown>) => void` | Override the isolate keep-alive hook; defaults to the auto-detected `ctx.waitUntil`. |
|
|
121
122
|
| `endpoint` | `string?` | Override the ingest endpoint (advanced). |
|
|
122
123
|
|
|
124
|
+
### Head sampling
|
|
125
|
+
|
|
126
|
+
`sampling: { rate }` performs **head sampling** — the keep/drop decision is made at the start of each fresh root trace, before any export. Traces that are dropped never leave the worker (no egress, no ingest cost, no storage). Traces that are kept are exported in full.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
export default instrument(worker, {
|
|
130
|
+
service: "my-api",
|
|
131
|
+
sampling: { rate: 0.2 }, // keep ~20% of traffic
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The sampler is **deterministic by trace ID**: the same trace always makes the same decision, so parent→child consistency is maintained when this worker is in a call chain sampled at the same rate by another service.
|
|
136
|
+
|
|
137
|
+
**Tradeoff:** head sampling can't guarantee keeping error traces — the drop/keep decision is made before the response status is known. If you need every error captured, keep `rate: 1` (the default). Heystack's ingest-side keep rules (error and slow retention) apply among traces that _are_ exported; they can't rescue a trace that was dropped in the worker before export.
|
|
138
|
+
|
|
139
|
+
The `rate` governs **fresh root traces only** (no inbound `traceparent`, or `traceparent` with `sampled=0`). A request arriving with a sampled inbound `traceparent` (`sampled=1`) is always recorded — the parent's decision is respected, so a distributed trace is never split mid-flight by the child's sample rate.
|
|
140
|
+
|
|
123
141
|
### Automatic tracing
|
|
124
142
|
|
|
125
143
|
`instrument()` traces the following automatically, with no additional config:
|
|
@@ -274,6 +292,7 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
274
292
|
|
|
275
293
|
## Migration / versioning
|
|
276
294
|
|
|
295
|
+
- **`0.6.0`** — **`/workers`: head sampling (`sampling: { rate }`).** New optional `WorkersConfig` field: `sampling.rate` (0–1, default `1`). Keeps a deterministic fraction of fresh root traces — the drop decision is made in the worker before export (no egress, no ingest cost). Parent-respecting: requests arriving with a sampled `traceparent` are always recorded. Consistent with server-side sampling (same trace-ID hash). No breaking changes; all new options are optional. See [Head sampling](#head-sampling).
|
|
277
296
|
- **`0.5.0`** — **`/workers`: identity enrichment, binding tracing, outbound-fetch tracing, manual span helpers.** New `WorkersConfig` options: `getUser` (attach `enduser.id`/`session.id`/`http.request.id` per request from a synchronous callback), `instrumentBindings` (auto child spans for D1/KV/R2/Vectorize — `true` = all detected, or a `string[]` to select). Outbound `fetch` calls made inside a traced handler automatically get CLIENT child spans with `traceparent` injection (distributed tracing across services). New ergonomic exports from `/workers`: `withSpan(name, attrs?, fn)` runs a function inside a named child span (auto-parented, exceptions recorded, `span.end()` in `finally`); `addEvent(name, attrs?)` adds an event to the active span. No breaking changes; all new options are optional.
|
|
278
297
|
- **`0.4.3`** — **feedback-loop guard extended to the Node path (cost fix).** The self-instrumentation loop the Workers path fixed in 0.3.1/0.3.2 was still live on plain Node / Next-on-Node: the OTLP-over-`http` exporter's `POST /v1/traces` was auto-instrumented and re-exported, so ~77% of ingested spans in production were the exporter tracing itself — needless ingest + ClickHouse compute. 0.4.3 ignores ingest-host requests in the HTTP/undici auto-instrumentations **and** filters self-spans at the exporter boundary (covers caller-supplied `instrumentations` too). The hostname matcher is now a shared module used by both `/node` and `/workers`. No API change. **Action: upgrade and redeploy any Node/Next-on-Node app** — it cuts ingested span volume sharply.
|
|
279
298
|
- **`0.3.5`** — **type-constraint fix (Workers).** A Worker whose `queue` consumer is typed with a concrete message body — `queue(batch: MessageBatch<MyJob>, …)`, the normal case — failed to compile against `instrument()` in 0.3.4 (`TS2345`: `MessageBatch<unknown>` not assignable to `MessageBatch<MyJob>`). The `WorkerHandler` constraint declared its entrypoints as arrow properties, whose parameters are checked **contravariantly** under `strictFunctionTypes`, so a narrowed handler wasn't assignable. 0.3.5 declares them with **method syntax** (bivariant parameters) and widens the batch to `MessageBatch<any>`, mirroring Cloudflare's own `ExportedHandler` — a typed-queue Worker now type-checks with a bare `instrument(handler, cfg)`. Runtime behaviour unchanged. Also adds a **consumer type-check gate** (`pnpm consumer-typecheck`, run in `check` and `prepublishOnly`) that compiles a fully-typed Worker against the built `dist` through the public `exports` map and asserts `satisfies ExportedHandler<Env>` — the regression that escaped in 0.3.3/0.3.4 now fails the build before publish.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Sampler, type SamplingResult } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import type { Context, Attributes, Link, SpanKind } from "@opentelemetry/api";
|
|
3
|
+
export declare function fnv01(s: string): number;
|
|
4
|
+
export declare function traceKept(traceId: string, rate: number): boolean;
|
|
5
|
+
export declare class HeystackRatioSampler implements Sampler {
|
|
6
|
+
private readonly rate;
|
|
7
|
+
constructor(rate: number);
|
|
8
|
+
shouldSample(_ctx: Context, traceId: string, _name: string, _kind: SpanKind, _attrs: Attributes, _links: Link[]): SamplingResult;
|
|
9
|
+
toString(): string;
|
|
10
|
+
}
|
|
11
|
+
export declare function makeSampler(sampling?: {
|
|
12
|
+
rate?: number;
|
|
13
|
+
}): Sampler;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SamplingDecision, ParentBasedSampler, AlwaysOnSampler, } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
// FNV-1a 32-bit → [0,1). MUST stay byte-identical to apps/ingest/src/sampling.ts
|
|
3
|
+
// so the SDK and ingest agree on which traces to keep.
|
|
4
|
+
export function fnv01(s) {
|
|
5
|
+
let h = 0x811c9dc5;
|
|
6
|
+
for (let i = 0; i < s.length; i++) {
|
|
7
|
+
h ^= s.charCodeAt(i);
|
|
8
|
+
h = Math.imul(h, 0x01000193);
|
|
9
|
+
}
|
|
10
|
+
return (h >>> 0) / 0x100000000;
|
|
11
|
+
}
|
|
12
|
+
export function traceKept(traceId, rate) {
|
|
13
|
+
if (rate >= 1)
|
|
14
|
+
return true;
|
|
15
|
+
if (rate <= 0)
|
|
16
|
+
return false;
|
|
17
|
+
return fnv01(traceId) < rate;
|
|
18
|
+
}
|
|
19
|
+
export class HeystackRatioSampler {
|
|
20
|
+
rate;
|
|
21
|
+
constructor(rate) {
|
|
22
|
+
this.rate = rate;
|
|
23
|
+
}
|
|
24
|
+
shouldSample(_ctx, traceId, _name, _kind, _attrs, _links) {
|
|
25
|
+
return {
|
|
26
|
+
decision: traceKept(traceId, this.rate)
|
|
27
|
+
? SamplingDecision.RECORD_AND_SAMPLED
|
|
28
|
+
: SamplingDecision.NOT_RECORD,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
toString() {
|
|
32
|
+
return `HeystackRatioSampler{${this.rate}}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function makeSampler(sampling) {
|
|
36
|
+
const rate = sampling?.rate;
|
|
37
|
+
if (rate === undefined || rate >= 1)
|
|
38
|
+
return new AlwaysOnSampler();
|
|
39
|
+
// ParentBased: a sampled/dropped PARENT (local or remote via traceparent) wins,
|
|
40
|
+
// so a distributed trace samples consistently; only the root uses the ratio.
|
|
41
|
+
return new ParentBasedSampler({ root: new HeystackRatioSampler(rate) });
|
|
42
|
+
}
|
package/dist/workers.d.ts
CHANGED
|
@@ -159,6 +159,19 @@ export interface WorkersConfig {
|
|
|
159
159
|
* Defaults to `false` (no binding tracing). Consumed by a later task.
|
|
160
160
|
*/
|
|
161
161
|
instrumentBindings?: boolean | string[];
|
|
162
|
+
/**
|
|
163
|
+
* Head-sampling configuration. When omitted (or `rate` >= 1), all traces are
|
|
164
|
+
* kept. When `rate` < 1, a deterministic FNV-1a hash of the trace ID decides
|
|
165
|
+
* keep/drop at the root span — consistent with the ingest-side sampler so the
|
|
166
|
+
* SDK and backend agree. Error / slow-span retention is an ingest concern.
|
|
167
|
+
*
|
|
168
|
+
* Note: head sampling is parent-respecting, so an incoming request carrying a
|
|
169
|
+
* sampled `traceparent` is still recorded even at `rate: 0` (it is not an
|
|
170
|
+
* absolute kill-switch; it governs only fresh/root traces).
|
|
171
|
+
*/
|
|
172
|
+
sampling?: {
|
|
173
|
+
rate?: number;
|
|
174
|
+
};
|
|
162
175
|
}
|
|
163
176
|
/**
|
|
164
177
|
* A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
|
|
@@ -178,7 +191,11 @@ export type HeystackTracerProvider = BasicTracerProvider & {
|
|
|
178
191
|
* must drain the exporter — `await provider.heystackExporter.forceFlush()` (or
|
|
179
192
|
* `flushHeystack()`), not just `provider.forceFlush()`.
|
|
180
193
|
*/
|
|
181
|
-
export declare function createTracerProvider(config: HeystackOptions
|
|
194
|
+
export declare function createTracerProvider(config: HeystackOptions & {
|
|
195
|
+
sampling?: {
|
|
196
|
+
rate?: number;
|
|
197
|
+
};
|
|
198
|
+
}): HeystackTracerProvider;
|
|
182
199
|
/**
|
|
183
200
|
* An AsyncLocalStorage-backed ContextManager for the /workers entry. When the
|
|
184
201
|
* runtime exposes `globalThis.AsyncLocalStorage` (workerd and Node do), this
|
package/dist/workers.js
CHANGED
|
@@ -16,6 +16,7 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
|
16
16
|
import { buildExporterConfig } from "./core.js";
|
|
17
17
|
import { isSelfSpanAttrs, safeHostname } from "./self-span.js";
|
|
18
18
|
import { instrumentEnv } from "./workers-bindings.js";
|
|
19
|
+
import { makeSampler } from "./workers-sampler.js";
|
|
19
20
|
// `ExportResult` / `ExportResultCode` mirror `@opentelemetry/core`. We define
|
|
20
21
|
// them inline (structurally identical) rather than import them: core is only a
|
|
21
22
|
// transitive dep of sdk-trace-base and isn't reliably resolvable, and keeping it
|
|
@@ -523,6 +524,7 @@ export function createTracerProvider(config) {
|
|
|
523
524
|
const provider = new BasicTracerProvider({
|
|
524
525
|
resource: new Resource({ [ATTR_SERVICE_NAME]: config.service }),
|
|
525
526
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
527
|
+
sampler: makeSampler(config.sampling),
|
|
526
528
|
});
|
|
527
529
|
// Attach the exporter so flush paths can await its in-flight fetches.
|
|
528
530
|
return Object.assign(provider, { heystackExporter: exporter });
|
|
@@ -723,6 +725,7 @@ export function instrument(handler, config) {
|
|
|
723
725
|
service: config.service,
|
|
724
726
|
endpoint: config.endpoint,
|
|
725
727
|
waitUntil: config.waitUntil,
|
|
728
|
+
sampling: config.sampling,
|
|
726
729
|
});
|
|
727
730
|
return { provider, tracer: trace.getTracer("heystack") };
|
|
728
731
|
};
|