autotel 5.0.0 → 6.0.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/dist/auto.cjs +1 -1
- package/dist/auto.js +1 -1
- package/dist/correlation-id.cjs +9 -9
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +2 -2
- package/dist/decorators.cjs.map +1 -1
- package/dist/decorators.js +1 -1
- package/dist/event.cjs +4 -4
- package/dist/event.js +1 -1
- package/dist/{functional-Bit6noRu.js → functional-BnDUxnSl.js} +2 -4
- package/dist/{functional-Bit6noRu.js.map → functional-BnDUxnSl.js.map} +1 -1
- package/dist/{functional-Cj7PSItS.cjs → functional-DcLBuCPz.cjs} +12 -14
- package/dist/{functional-Cj7PSItS.cjs.map → functional-DcLBuCPz.cjs.map} +1 -1
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +2 -2
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +23 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -145
- package/dist/index.js.map +1 -1
- package/dist/init-C3RApHKJ.cjs +3371 -0
- package/dist/init-C3RApHKJ.cjs.map +1 -0
- package/dist/{init-CTyqG9qR.d.ts → init-D6n2diBy.d.ts} +21 -2
- package/dist/init-D6n2diBy.d.ts.map +1 -0
- package/dist/init-R-gSEXkJ.js +3190 -0
- package/dist/init-R-gSEXkJ.js.map +1 -0
- package/dist/{init-a4yPDYqC.d.cts → init-XzXzrarP.d.cts} +21 -2
- package/dist/init-XzXzrarP.d.cts.map +1 -0
- package/dist/instrumentation.cjs +1 -1
- package/dist/instrumentation.js +1 -1
- package/dist/messaging.cjs +1 -1
- package/dist/messaging.js +1 -1
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/package.json +2 -3
- package/dist/event-CwIpFdoY.cjs +0 -778
- package/dist/event-CwIpFdoY.cjs.map +0 -1
- package/dist/event-pLMF5b3d.js +0 -761
- package/dist/event-pLMF5b3d.js.map +0 -1
- package/dist/init-B2uvZReD.cjs +0 -1120
- package/dist/init-B2uvZReD.cjs.map +0 -1
- package/dist/init-CTyqG9qR.d.ts.map +0 -1
- package/dist/init-CdEWjhvl.js +0 -1043
- package/dist/init-CdEWjhvl.js.map +0 -1
- package/dist/init-a4yPDYqC.d.cts.map +0 -1
- package/dist/operation-context-CKBoA4Qy.js +0 -59
- package/dist/operation-context-CKBoA4Qy.js.map +0 -1
- package/dist/operation-context-hzKbTKnt.cjs +0 -72
- package/dist/operation-context-hzKbTKnt.cjs.map +0 -1
- package/dist/track-BBcxEJEC.js +0 -1110
- package/dist/track-BBcxEJEC.js.map +0 -1
- package/dist/track-BI6dmZ2X.cjs +0 -1207
- package/dist/track-BI6dmZ2X.cjs.map +0 -1
- package/skills/analyze-traces/SKILL.md +0 -209
- package/skills/autotel-core/SKILL.md +0 -166
- package/skills/autotel-events/SKILL.md +0 -84
- package/skills/autotel-frameworks/SKILL.md +0 -125
- package/skills/autotel-instrumentation/SKILL.md +0 -150
- package/skills/autotel-request-logging/SKILL.md +0 -99
- package/skills/autotel-structured-errors/SKILL.md +0 -125
- package/skills/build-audit-trails/SKILL.md +0 -460
- package/skills/build-audit-trails/references/audit-queries.md +0 -73
- package/skills/build-audit-trails/references/framework-wiring.md +0 -196
- package/skills/debug-missing-spans/SKILL.md +0 -250
- package/skills/migrate-to-autotel/SKILL.md +0 -269
- package/skills/review-otel-patterns/SKILL.md +0 -540
- package/skills/review-otel-patterns/references/code-review.md +0 -76
- package/skills/review-otel-patterns/references/processor-pipeline.md +0 -205
- package/skills/review-otel-patterns/references/structured-errors.md +0 -102
- package/skills/review-otel-patterns/references/wide-spans.md +0 -85
- package/skills/tune-sampling/SKILL.md +0 -290
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# Processor pipeline cookbook
|
|
2
|
-
|
|
3
|
-
Composable building blocks for the autotel pipeline. Each helper is small enough to reason about in isolation and isolates errors so a single bad processor cannot break the others.
|
|
4
|
-
|
|
5
|
-
## Primitives
|
|
6
|
-
|
|
7
|
-
| Helper | Type | Purpose |
|
|
8
|
-
| ---------------------------- | ----------------- | ------------------------------------------------------------ |
|
|
9
|
-
| `defineConfig(config)` | identity | Authoring helper for typed config |
|
|
10
|
-
| `composeSpanProcessors([…])` | `SpanProcessor` | Fan span lifecycle to multiple processors |
|
|
11
|
-
| `composePostProcessors([…])` | `PostProcessorFn` | Chain post-processors (each sees the output of the previous) |
|
|
12
|
-
| `composeSubscribers([…])` | `EdgeSubscriber` | Fire in-process side effects in order |
|
|
13
|
-
|
|
14
|
-
All from `autotel-edge`.
|
|
15
|
-
|
|
16
|
-
## Multi-backend export
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
import { BatchSpanProcessor } from 'autotel/processors';
|
|
20
|
-
import { OTLPHttpJsonExporter } from 'autotel/exporters';
|
|
21
|
-
import { composeSpanProcessors, defineConfig } from 'autotel-edge';
|
|
22
|
-
|
|
23
|
-
const honeycomb = new BatchSpanProcessor(
|
|
24
|
-
new OTLPHttpJsonExporter({
|
|
25
|
-
url: 'https://api.honeycomb.io/v1/traces',
|
|
26
|
-
headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY! },
|
|
27
|
-
}),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const grafana = new BatchSpanProcessor(
|
|
31
|
-
new OTLPHttpJsonExporter({
|
|
32
|
-
url: process.env.GRAFANA_OTLP_URL!,
|
|
33
|
-
headers: { authorization: `Basic ${process.env.GRAFANA_AUTH!}` },
|
|
34
|
-
}),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
export const config = defineConfig({
|
|
38
|
-
service: { name: 'checkout' },
|
|
39
|
-
spanProcessors: composeSpanProcessors([honeycomb, grafana]),
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Tail sampling: keep errors + slow + 10% otherwise
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
import { TailSamplingProcessor } from 'autotel/processors';
|
|
47
|
-
import { composeSpanProcessors } from 'autotel-edge';
|
|
48
|
-
|
|
49
|
-
const tail = new TailSamplingProcessor({
|
|
50
|
-
keep: (trace) => {
|
|
51
|
-
if (trace.localRootSpan.status?.code === SpanStatusCode.ERROR) return true;
|
|
52
|
-
if (trace.localRootSpan.duration[0] > 1) return true; // > 1s
|
|
53
|
-
return Math.random() < 0.1;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
spanProcessors: composeSpanProcessors([new BatchSpanProcessor(otlp), tail]);
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Drop noisy spans before they reach the batcher
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { FilteringSpanProcessor } from 'autotel/processors';
|
|
64
|
-
|
|
65
|
-
const dropHealth = new FilteringSpanProcessor({
|
|
66
|
-
exclude: (span) => /^GET \/(healthz|ready)$/.test(span.name),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
spanProcessors: composeSpanProcessors([
|
|
70
|
-
dropHealth,
|
|
71
|
-
new BatchSpanProcessor(otlp),
|
|
72
|
-
]);
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Bound URL cardinality
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
import { SpanNameNormalizingProcessor } from 'autotel/processors';
|
|
79
|
-
|
|
80
|
-
const normalise = new SpanNameNormalizingProcessor({
|
|
81
|
-
// Replace UUIDs and 24-char hex ids with placeholders
|
|
82
|
-
replacements: [
|
|
83
|
-
{
|
|
84
|
-
match: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
|
|
85
|
-
with: ':id',
|
|
86
|
-
},
|
|
87
|
-
{ match: /[0-9a-f]{24}/g, with: ':id' },
|
|
88
|
-
],
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Now `GET /users/123e4567-e89b-12d3-a456-426614174000/orders` becomes `GET /users/:id/orders` in your traces. Fewer unique span names, dramatically faster queries.
|
|
93
|
-
|
|
94
|
-
## Lift baggage onto every span
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { BaggageSpanProcessor } from 'autotel/processors';
|
|
98
|
-
|
|
99
|
-
// Anything placed in baggage upstream becomes an attribute on every child span
|
|
100
|
-
const baggage = new BaggageSpanProcessor({ keys: ['tenant', 'feature_flags'] });
|
|
101
|
-
spanProcessors: composeSpanProcessors([baggage, new BatchSpanProcessor(otlp)]);
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Subscribers for in-process side effects
|
|
105
|
-
|
|
106
|
-
Subscribers run synchronously in the parent context. Ideal for metrics, audit, and cost calculation that you want recorded **before** the span goes to the batcher.
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
import type { EdgeSubscriber } from 'autotel-edge';
|
|
110
|
-
import { composeSubscribers } from 'autotel-edge';
|
|
111
|
-
|
|
112
|
-
const metricsSubscriber: EdgeSubscriber = (event) => {
|
|
113
|
-
if (
|
|
114
|
-
event.kind === 'span.end' &&
|
|
115
|
-
event.span.attributes['http.response.status_code'] >= 500
|
|
116
|
-
) {
|
|
117
|
-
metrics.errorCounter.add(1, { route: event.span.name });
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const auditSubscriber: EdgeSubscriber = (event) => {
|
|
122
|
-
if (event.kind === 'span.end' && event.span.name.startsWith('admin.')) {
|
|
123
|
-
audit.write({
|
|
124
|
-
kind: event.span.name,
|
|
125
|
-
actor: event.span.attributes['user.id'],
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
subscribers: [composeSubscribers([metricsSubscriber, auditSubscriber])];
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Post-processors for last-mile rewrites
|
|
134
|
-
|
|
135
|
-
Post-processors mutate the array of spans **after** sampling, just before export. Use for redacting stack traces, dropping fields, or annotating with deployment info.
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
import type { PostProcessorFn } from 'autotel-edge';
|
|
139
|
-
import { composePostProcessors } from 'autotel-edge';
|
|
140
|
-
import { createStringRedactor } from 'autotel';
|
|
141
|
-
|
|
142
|
-
const redactStacks = createStringRedactor('strict');
|
|
143
|
-
|
|
144
|
-
const cleanStacks: PostProcessorFn = (spans) =>
|
|
145
|
-
spans.map((s) => {
|
|
146
|
-
if (typeof s.attributes['exception.stacktrace'] === 'string') {
|
|
147
|
-
s.attributes['exception.stacktrace'] = redactStacks(
|
|
148
|
-
s.attributes['exception.stacktrace'],
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
return s;
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const tagDeploy: PostProcessorFn = (spans) =>
|
|
155
|
-
spans.map((s) => ({
|
|
156
|
-
...s,
|
|
157
|
-
attributes: { ...s.attributes, 'deploy.id': process.env.RELEASE! },
|
|
158
|
-
}));
|
|
159
|
-
|
|
160
|
-
postProcessor: composePostProcessors([cleanStacks, tagDeploy]);
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Putting it all together
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
import {
|
|
167
|
-
defineConfig,
|
|
168
|
-
composeSpanProcessors,
|
|
169
|
-
composeSubscribers,
|
|
170
|
-
composePostProcessors,
|
|
171
|
-
} from 'autotel-edge';
|
|
172
|
-
|
|
173
|
-
export const otelConfig = defineConfig({
|
|
174
|
-
service: { name: 'checkout' },
|
|
175
|
-
attributeRedactor: 'strict',
|
|
176
|
-
|
|
177
|
-
spanProcessors: composeSpanProcessors([
|
|
178
|
-
dropHealth, // 1. drop spans we never want
|
|
179
|
-
normaliseUrls, // 2. bound cardinality
|
|
180
|
-
new BatchSpanProcessor(honeycomb),
|
|
181
|
-
new BatchSpanProcessor(grafana),
|
|
182
|
-
tailSampler, // 3. keep errors + slow + 10%
|
|
183
|
-
]),
|
|
184
|
-
|
|
185
|
-
subscribers: [
|
|
186
|
-
composeSubscribers([metricsSubscriber, auditSubscriber, aiCostSubscriber]),
|
|
187
|
-
],
|
|
188
|
-
|
|
189
|
-
postProcessor: composePostProcessors([cleanStacks, tagDeploy]),
|
|
190
|
-
});
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Error isolation
|
|
194
|
-
|
|
195
|
-
Every compose helper catches errors per item and logs to `console.error` with the helper name. A single bad processor cannot break the others. Important when one of your subscribers is a third-party integration (Datadog, PagerDuty, …) that can rate-limit or 502.
|
|
196
|
-
|
|
197
|
-
## Choosing between subscribers and post-processors
|
|
198
|
-
|
|
199
|
-
| You want… | Use |
|
|
200
|
-
| ----------------------------------------- | ---------------------------------------------------------- |
|
|
201
|
-
| Mutate exported span attributes | `postProcessor` |
|
|
202
|
-
| Drop spans entirely | `FilteringSpanProcessor` (early) or tail sampler |
|
|
203
|
-
| Update an in-process metric on every span | `subscribers` |
|
|
204
|
-
| Send an audit log to a DB | `subscribers` (use `log.fork('audit')` if writes are slow) |
|
|
205
|
-
| Re-emit spans to a second backend | second `BatchSpanProcessor` in `composeSpanProcessors` |
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Structured errors
|
|
2
|
-
|
|
3
|
-
`createStructuredError` produces an `Error` carrying enough context to be:
|
|
4
|
-
|
|
5
|
-
- **Recorded onto the active span** (`exception.type`, `exception.message`, `exception.stacktrace`, `span.status = ERROR`).
|
|
6
|
-
- **Returned to clients safely** (`internal` is stripped by `parseError`).
|
|
7
|
-
- **Self-documenting** (`why` explains the cause, `fix` tells the caller what to do, `link` points at runbook docs).
|
|
8
|
-
|
|
9
|
-
## Field reference
|
|
10
|
-
|
|
11
|
-
| Field | Audience | Purpose |
|
|
12
|
-
| ---------- | ----------- | ---------------------------------------------------------- |
|
|
13
|
-
| `message` | Both | Short, stable summary |
|
|
14
|
-
| `status` | Both | HTTP status (drives client behaviour and span status code) |
|
|
15
|
-
| `why` | Both | Human-readable cause (`"Card declined by issuer"`) |
|
|
16
|
-
| `fix` | Client | Remediation hint (`"Use a different payment method"`) |
|
|
17
|
-
| `link` | Client | URL to docs / runbook |
|
|
18
|
-
| `code` | Both | Machine-readable code (`"PAYMENT_DECLINED"`) |
|
|
19
|
-
| `cause` | Server only | The underlying error |
|
|
20
|
-
| `internal` | Server only | Diagnostic metadata (`{ correlationId, resourceId }`) |
|
|
21
|
-
| `details` | Both | Structured payload (e.g. validation errors per field) |
|
|
22
|
-
|
|
23
|
-
## Templates
|
|
24
|
-
|
|
25
|
-
### Validation (400)
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
throw createStructuredError({
|
|
29
|
-
status: 400,
|
|
30
|
-
code: 'VALIDATION_ERROR',
|
|
31
|
-
message: 'Invalid request body',
|
|
32
|
-
why: 'One or more fields failed validation',
|
|
33
|
-
fix: 'Check the `details` field for per-field errors',
|
|
34
|
-
details: { email: 'must be a valid email', age: 'must be ≥ 18' },
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Auth (401 / 403)
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
throw createStructuredError({
|
|
42
|
-
status: 403,
|
|
43
|
-
code: 'FORBIDDEN',
|
|
44
|
-
message: 'Not allowed',
|
|
45
|
-
why: 'You do not have access to this resource',
|
|
46
|
-
fix: 'Ask the workspace owner for access',
|
|
47
|
-
link: 'https://docs.example.com/permissions',
|
|
48
|
-
internal: { resourceId: 'proj_123', userRole: 'member' },
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Payment (402)
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
throw createStructuredError({
|
|
56
|
-
status: 402,
|
|
57
|
-
code: 'PAYMENT_DECLINED',
|
|
58
|
-
message: 'Payment declined',
|
|
59
|
-
why: 'Card declined by issuer — insufficient funds',
|
|
60
|
-
fix: 'Use a different payment method or contact your bank',
|
|
61
|
-
link: 'https://docs.example.com/payments/declined',
|
|
62
|
-
cause: stripeError,
|
|
63
|
-
internal: { stripeChargeId: 'ch_…', riskScore: stripeError.risk_level },
|
|
64
|
-
});
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Upstream failure (502 / 503 / 504)
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
throw createStructuredError({
|
|
71
|
-
status: 502,
|
|
72
|
-
code: 'UPSTREAM_FAILED',
|
|
73
|
-
message: 'Inventory service is unavailable',
|
|
74
|
-
why: 'Could not reach the inventory service',
|
|
75
|
-
fix: 'Retry in a few minutes',
|
|
76
|
-
cause: fetchError,
|
|
77
|
-
internal: { upstream: 'inventory-svc', retryAttempt: 3 },
|
|
78
|
-
});
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## At HTTP boundaries
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
import { parseError } from 'autotel';
|
|
85
|
-
|
|
86
|
-
app.onError((error, c) => {
|
|
87
|
-
// span.status is already ERROR with exception fields recorded
|
|
88
|
-
const parsed = parseError(error);
|
|
89
|
-
// `internal` and `cause` are stripped here — never leak them to clients
|
|
90
|
-
return c.json(parsed, parsed.status);
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Anti-patterns
|
|
95
|
-
|
|
96
|
-
| Anti-pattern | Fix |
|
|
97
|
-
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
98
|
-
| `throw new Error('something went wrong')` | `createStructuredError({ message, status, why, fix })` |
|
|
99
|
-
| Putting support IDs in `message` (`"Failed for user 42"`) | Use `internal: { userId: 42 }` |
|
|
100
|
-
| Returning `details: { error: stack }` to clients | Stack traces stay in `cause` / span; never serialise them out |
|
|
101
|
-
| `console.error(e); throw e` | Just throw: autotel's span will pick up the exception |
|
|
102
|
-
| Two callers throwing different shapes for the same condition | Centralise: `function declined(reason: string) { throw createStructuredError({ … }) }` |
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# Designing wide spans
|
|
2
|
-
|
|
3
|
-
A wide span is a single span per logical unit of work (request, job, message, fork) carrying _all_ the fields you'd ever want to filter or group by. autotel lets you build them with `useLogger().set({ … })`. Fields are flattened to OTel attributes with stable dotted keys.
|
|
4
|
-
|
|
5
|
-
## Anatomy
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { useLogger } from 'autotel';
|
|
9
|
-
|
|
10
|
-
export const POST = withAutotel(async (request) => {
|
|
11
|
-
const log = useLogger();
|
|
12
|
-
|
|
13
|
-
// Identity
|
|
14
|
-
log.set({ user: { id: 'usr_123', plan: 'enterprise', role: 'admin' } });
|
|
15
|
-
|
|
16
|
-
// Inputs
|
|
17
|
-
log.set({ cart: { items: 3, total: 14_999, currency: 'USD' } });
|
|
18
|
-
|
|
19
|
-
// Decisions / branches
|
|
20
|
-
log.set({ promo: { applied: 'SUMMER10', discount: 1_500 } });
|
|
21
|
-
|
|
22
|
-
// Outputs
|
|
23
|
-
log.set({
|
|
24
|
-
payment: { provider: 'stripe', method: 'card', authCode: 'auth_x' },
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return Response.json({ ok: true });
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
OTel attributes recorded:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
user.id=usr_123
|
|
35
|
-
user.plan=enterprise
|
|
36
|
-
user.role=admin
|
|
37
|
-
cart.items=3
|
|
38
|
-
cart.total=14999
|
|
39
|
-
cart.currency=USD
|
|
40
|
-
promo.applied=SUMMER10
|
|
41
|
-
promo.discount=1500
|
|
42
|
-
payment.provider=stripe
|
|
43
|
-
payment.method=card
|
|
44
|
-
payment.authCode=auth_x
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Rules of thumb
|
|
48
|
-
|
|
49
|
-
1. **One wide span per logical unit of work.** Many tiny spans hurt query speed; deep call trees can be opt-in (`autotel-drizzle`, `autotel-mongoose`).
|
|
50
|
-
2. **Group with objects.** `{ user: { id, plan } }` not `userId` / `userPlan`. The flatten step keeps the key shape stable.
|
|
51
|
-
3. **Capture decisions, not just inputs.** Which branch ran, which promo applied, which fallback fired.
|
|
52
|
-
4. **Keep cardinality bounded.** Don't put per-request UUIDs in `span.name`; use `SpanNameNormalizingProcessor`. Free-text labels go in attributes.
|
|
53
|
-
5. **Avoid raw bodies.** Pick the shape: `{ user: { id, plan } }`: never `log.set({ user: requestBody })`.
|
|
54
|
-
6. **Trust the redactor.** PII you forgot to think about (emails, JWTs, cards) gets masked in production. See `attributeRedactor: 'default'`.
|
|
55
|
-
|
|
56
|
-
## When you need correlated child spans
|
|
57
|
-
|
|
58
|
-
Use `trace()` to wrap discrete sub-operations whose duration matters:
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
import { trace } from 'autotel';
|
|
62
|
-
|
|
63
|
-
const fetchInventory = trace(async (sku: string) => {
|
|
64
|
-
/* … */
|
|
65
|
-
});
|
|
66
|
-
const reserveStock = trace(async (sku: string, qty: number) => {
|
|
67
|
-
/* … */
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await fetchInventory(sku);
|
|
71
|
-
await reserveStock(sku, qty);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Each gets its own span with the function name; both are children of the active request span.
|
|
75
|
-
|
|
76
|
-
## When you need background work
|
|
77
|
-
|
|
78
|
-
`log.fork('label', fn)` spawns a child span that emits its own wide event with `_parentCorrelationId` set, even after the parent response has been returned. Pass `lifecycle.onChildEnter / onChildExit` if your framework tracks active loggers (Elysia, etc.).
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
log.fork('audit-write', async () => {
|
|
82
|
-
await audit.write({ kind: 'order.created', orderId });
|
|
83
|
-
});
|
|
84
|
-
return Response.json({ ok: true }); // parent returns immediately
|
|
85
|
-
```
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tune-sampling
|
|
3
|
-
description: >
|
|
4
|
-
Choose a sampling strategy for an autotel-instrumented service. Covers
|
|
5
|
-
head sampling (per-span-kind rates, parent-based, ratio), tail sampling
|
|
6
|
-
(keep errors, slow, AI-aware, debug-headers), cost vs cardinality
|
|
7
|
-
tradeoffs, and the math for picking rates that hit a target spans/second
|
|
8
|
-
budget. Includes recipes for low-volume admin services, high-volume APIs,
|
|
9
|
-
AI agents, and Cloudflare Workers.
|
|
10
|
-
license: MIT
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Tune sampling
|
|
14
|
-
|
|
15
|
-
Untuned tracing is either expensive (100 % at scale costs money + drowns dashboards) or unhelpful (1 % loses the failure modes you need to see). The right answer is almost always **head sample most of the boring traffic, tail keep all the interesting traffic**, with explicit overrides for AI calls and customer escalations.
|
|
16
|
-
|
|
17
|
-
## When to use
|
|
18
|
-
|
|
19
|
-
- Hitting your observability budget
|
|
20
|
-
- Dashboards too sparse to spot anomalies
|
|
21
|
-
- "We have the trace IDs but the spans are gone" complaints
|
|
22
|
-
- New service launching at scale
|
|
23
|
-
- Long-running AI agents producing 50+ spans per request
|
|
24
|
-
|
|
25
|
-
## The mental model
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
Total cost = (spans/sec × $/span) + (storage_GB × $/GB-month)
|
|
29
|
-
↑
|
|
30
|
-
Head sampling reduces this directly.
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Head sampling makes a decision **at span start**: fast but coarse, since it cannot know whether the span will fail.
|
|
34
|
-
Tail sampling makes the decision **at span end**: slower and heavier on storage, but precise.
|
|
35
|
-
|
|
36
|
-
The right mix:
|
|
37
|
-
|
|
38
|
-
- **Head sample at the entry point** to keep volume tractable.
|
|
39
|
-
- **Tail keep** the high-value subset (errors, slow, AI, debug-headered).
|
|
40
|
-
- **Don't sample audit spans**: separate processor, see [`build-audit-trails`](../build-audit-trails/SKILL.md).
|
|
41
|
-
|
|
42
|
-
## Head sampling recipes
|
|
43
|
-
|
|
44
|
-
### Default for a typical web service
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
init({
|
|
48
|
-
service: 'my-app',
|
|
49
|
-
sampling: {
|
|
50
|
-
rates: {
|
|
51
|
-
server: 25, // server entry spans — sample ¼
|
|
52
|
-
client: 5, // outbound HTTP — sample 1/20
|
|
53
|
-
internal: 5, // internal sub-spans — sample 1/20
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Children of a sampled root are **all** kept (parent-based propagation is the default). So `server: 25` means 25 % of _user requests_, complete trace each.
|
|
60
|
-
|
|
61
|
-
### High-volume API (>1 k req/s)
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
sampling: {
|
|
65
|
-
rates: { server: 5, client: 1, internal: 1 }, // 5 % → tail keeps errors anyway
|
|
66
|
-
tail: keepInterestingTraces,
|
|
67
|
-
},
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Low-volume admin / internal service (<10 req/s)
|
|
71
|
-
|
|
72
|
-
100 % is fine. Don't penalise yourself for a service that produces 1 GB of traces a week.
|
|
73
|
-
|
|
74
|
-
### Cloudflare Workers (per-colo budget)
|
|
75
|
-
|
|
76
|
-
Workers run distributed. Head sampling is your friend because there's no central queue:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
defineWorkerFetch(
|
|
80
|
-
{
|
|
81
|
-
service: { name: 'edge' },
|
|
82
|
-
sampling: { rates: { server: 10 } }, // 10 % per colo, scales naturally
|
|
83
|
-
},
|
|
84
|
-
handler,
|
|
85
|
-
);
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Tail sampling: keep interesting traces
|
|
89
|
-
|
|
90
|
-
Tail sampling looks at the full trace (root span + children) before deciding. autotel ships `TailSamplingProcessor`:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
import { TailSamplingProcessor } from 'autotel/processors';
|
|
94
|
-
import { SpanStatusCode } from '@opentelemetry/api';
|
|
95
|
-
|
|
96
|
-
const tail = new TailSamplingProcessor({
|
|
97
|
-
keep: (trace) => {
|
|
98
|
-
// 1. Always keep errors
|
|
99
|
-
if (trace.localRootSpan.status?.code === SpanStatusCode.ERROR) return true;
|
|
100
|
-
if (trace.spans.some((s) => s.status?.code === SpanStatusCode.ERROR))
|
|
101
|
-
return true;
|
|
102
|
-
|
|
103
|
-
// 2. Always keep slow traces (configurable threshold)
|
|
104
|
-
if (durationMs(trace.localRootSpan) > 1_000) return true;
|
|
105
|
-
|
|
106
|
-
// 3. Always keep customer-marked traces
|
|
107
|
-
if (trace.localRootSpan.attributes['debug.trace'] === true) return true;
|
|
108
|
-
|
|
109
|
-
// 4. Always keep AI traces (rare + expensive — full visibility helps)
|
|
110
|
-
if (
|
|
111
|
-
trace.spans.some(
|
|
112
|
-
(s) =>
|
|
113
|
-
typeof s.attributes['gen_ai.provider.name'] === 'string' ||
|
|
114
|
-
// legacy: third-party instrumentations may still emit gen_ai.system
|
|
115
|
-
typeof s.attributes['gen_ai.system'] === 'string',
|
|
116
|
-
)
|
|
117
|
-
)
|
|
118
|
-
return true;
|
|
119
|
-
|
|
120
|
-
// 5. Otherwise: respect head sampling decision
|
|
121
|
-
return false;
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Combining with multi-backend
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
spanProcessors: composeSpanProcessors([
|
|
130
|
-
// Drop nothing here — we want the tail processor to see the full trace
|
|
131
|
-
new BatchSpanProcessor(localExporter),
|
|
132
|
-
tail, // filters before remote export
|
|
133
|
-
new BatchSpanProcessor(expensiveRemoteExporter),
|
|
134
|
-
]);
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## AI / LLM-aware sampling
|
|
138
|
-
|
|
139
|
-
LLM calls produce 5–50 spans per request and are 100× more expensive than a typical handler call. Tradeoffs:
|
|
140
|
-
|
|
141
|
-
- **Don't head-sample AI handlers below 50 %**: debugging "why did the model loop" requires the full chain.
|
|
142
|
-
- **Always tail-keep AI traces**: the `gen_ai.*` attributes flag them.
|
|
143
|
-
- **Cost-aware sampling**: keep all calls above a $ threshold:
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
keep: (trace) => {
|
|
147
|
-
const cost = trace.spans.reduce(
|
|
148
|
-
(acc, s) =>
|
|
149
|
-
acc +
|
|
150
|
-
(typeof s.attributes['gen_ai.usage.cost.usd'] === 'number'
|
|
151
|
-
? (s.attributes['gen_ai.usage.cost.usd'] as number)
|
|
152
|
-
: 0),
|
|
153
|
-
0,
|
|
154
|
-
);
|
|
155
|
-
if (cost > 0.1) return true; // any trace > $0.10 → keep
|
|
156
|
-
if (cost > 0.01) return Math.random() < 0.5; // > $0.01 → 50 %
|
|
157
|
-
return Math.random() < 0.1; // < $0.01 → 10 %
|
|
158
|
-
};
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Customer-driven sampling (debug header)
|
|
162
|
-
|
|
163
|
-
Let support flip on full tracing per request:
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
const tail = new TailSamplingProcessor({
|
|
167
|
-
keep: (trace) => trace.localRootSpan.attributes['x-debug-trace'] === '1' || /* … */,
|
|
168
|
-
})
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
In your middleware:
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
if (request.headers.get('x-debug-trace') === '1') {
|
|
175
|
-
useLogger().set({ 'x-debug-trace': '1' });
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Now any user can mark a request as "trace this fully" by sending the header. Invaluable for reproducing customer reports.
|
|
180
|
-
|
|
181
|
-
## Keeping the trace whole across services
|
|
182
|
-
|
|
183
|
-
`RandomSampler` rolls the dice in each process. The API can keep a trace its
|
|
184
|
-
worker drops, which leaves a waterfall with holes and no way to tell a dropped
|
|
185
|
-
span from a fast one. `DeterministicSampler` hashes a key that travels with the
|
|
186
|
-
request, so every service reaches the same verdict:
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
import { DeterministicSampler } from 'autotel/sampling';
|
|
190
|
-
import { trace } from '@opentelemetry/api';
|
|
191
|
-
|
|
192
|
-
init({
|
|
193
|
-
service: 'checkout',
|
|
194
|
-
sampler: new DeterministicSampler({
|
|
195
|
-
sampleRate: 0.1,
|
|
196
|
-
key: () => trace.getActiveSpan()?.spanContext().traceId,
|
|
197
|
-
}),
|
|
198
|
-
});
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Give every service the same rate and the same key. Different rates break the
|
|
202
|
-
agreement again.
|
|
203
|
-
|
|
204
|
-
## Per-key target rates
|
|
205
|
-
|
|
206
|
-
A single rate cannot fit skewed traffic. At 1 % your busiest endpoint still
|
|
207
|
-
floods storage while the rare tenant whose failures you need disappears.
|
|
208
|
-
`KeyTargetRateSampler` counts traffic per key over a rolling window and gives
|
|
209
|
-
each key its own rate, so every key contributes about the same number of
|
|
210
|
-
events:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
import { KeyTargetRateSampler } from 'autotel/sampling';
|
|
214
|
-
|
|
215
|
-
new KeyTargetRateSampler({
|
|
216
|
-
key: (context) => context.operationName,
|
|
217
|
-
targetPerKey: 10, // ~10 events per key per window
|
|
218
|
-
windowMs: 30_000,
|
|
219
|
-
maxKeys: 1000, // keys past this share one overflow bucket
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
The first window keeps everything, because no traffic history exists yet. Rates
|
|
224
|
-
apply from the second window on.
|
|
225
|
-
|
|
226
|
-
## Recording the rate
|
|
227
|
-
|
|
228
|
-
Sampled counts lie unless you record what you discarded. A query over a 1 %
|
|
229
|
-
sample reports a hundredth of your traffic, and the number looks plausible
|
|
230
|
-
enough to act on.
|
|
231
|
-
|
|
232
|
-
autotel writes `autotel.sampling.rate` on the span as "1 in N", so
|
|
233
|
-
`COUNT * rate` estimates the population. It appears only when N exceeds 1, so
|
|
234
|
-
fully captured spans stay clean. A custom sampler joins in by implementing
|
|
235
|
-
`sampleRate()`:
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
import type { Sampler } from 'autotel/sampling';
|
|
239
|
-
|
|
240
|
-
class TenantSampler implements Sampler {
|
|
241
|
-
shouldSample(context) {
|
|
242
|
-
/* … */
|
|
243
|
-
}
|
|
244
|
-
sampleRate() {
|
|
245
|
-
return 20; // each kept event stands for 20
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Reach for `hashUnitInterval` from `autotel/sampling` when you need the same
|
|
251
|
-
spread for your own keys.
|
|
252
|
-
|
|
253
|
-
## Sizing the rate
|
|
254
|
-
|
|
255
|
-
Target volume:
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
spans/sec ≈ requests/sec × spans_per_request × head_rate × tail_keep_rate
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Worked example for a 100 req/s API with 8 spans/req:
|
|
262
|
-
|
|
263
|
-
| Head rate | Tail keep | Result |
|
|
264
|
-
| --------- | -------------------------------- | ------------------------------------------- |
|
|
265
|
-
| 100 % | 100 % | 800 spans/sec: expensive |
|
|
266
|
-
| 10 % | 100 % (errors + slow + AI ≈ 5 %) | ≈ 110 spans/sec: sweet spot |
|
|
267
|
-
| 1 % | 100 % | ≈ 18 spans/sec: too sparse for p99 alerting |
|
|
268
|
-
|
|
269
|
-
For per-vendor pricing:
|
|
270
|
-
|
|
271
|
-
- **Honeycomb**: $0.000005 / event for paid plans. 110 spans/sec × 86 400 s = 9.5 M events/day = $48/day.
|
|
272
|
-
- **Datadog APM**: ~$1.27/M spans ingested (varies by region). Same volume → ~$12/day.
|
|
273
|
-
- **Grafana Cloud**: 100 GB free tier; 110 spans/sec ≈ 5 GB/day.
|
|
274
|
-
|
|
275
|
-
## Anti-patterns
|
|
276
|
-
|
|
277
|
-
| Anti-pattern | Fix |
|
|
278
|
-
| -------------------------------------------- | ------------------------------------------------------------- |
|
|
279
|
-
| 100 % sampling at scale "to be safe" | You're paying 10–100× without proportional value |
|
|
280
|
-
| 1 % sampling with no tail keep | You'll miss every interesting failure |
|
|
281
|
-
| Forgetting to tail-keep errors | Sampled traces with errors → silent customer pain |
|
|
282
|
-
| Same rate for `server` and `internal` | Internal sub-spans are 5–20× more numerous; sample harder |
|
|
283
|
-
| Ratio-based sampling on service entry point | Use parent-based: children of a sampled trace stay together |
|
|
284
|
-
| Head-sampling AI calls below 50 % | Debugging tool loops requires the full chain |
|
|
285
|
-
| Audit spans subject to sampling | Route them to a separate processor (see `build-audit-trails`) |
|
|
286
|
-
| Tail processor before exporter (loses spans) | Tail processor goes between head sampler and remote exporter |
|
|
287
|
-
| Rate-by-route hand-coded in handlers | Use head sampler + tail keep: declarative, one place |
|
|
288
|
-
| Counting sampled spans as the population | Multiply by `autotel.sampling.rate` |
|
|
289
|
-
| `RandomSampler` on a multi-service trace | `DeterministicSampler` keyed on the trace id keeps it whole |
|
|
290
|
-
| One flat rate over skewed traffic | `KeyTargetRateSampler` gives rare keys their own rate |
|