@decocms/start 2.28.1 → 2.29.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 +178 -79
- package/package.json +16 -14
- package/src/cms/resolve.ts +81 -63
- package/src/cms/sectionLoaders.ts +11 -0
- package/src/index.ts +3 -0
- package/src/sdk/cachedLoader.ts +36 -13
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/instrumentedFetch.ts +56 -0
- package/src/sdk/logger.test.ts +135 -0
- package/src/sdk/logger.ts +166 -0
- package/src/sdk/observability.ts +75 -0
- package/src/sdk/otel.test.ts +59 -0
- package/src/sdk/otel.ts +270 -29
- package/src/sdk/otelAdapters.test.ts +135 -0
- package/src/sdk/otelAdapters.ts +401 -0
- package/src/sdk/sampler.test.ts +127 -0
- package/src/sdk/sampler.ts +183 -0
- package/src/sdk/workerEntry.ts +541 -476
- package/src/vite/plugin.js +6 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL-based head sampler — port of `deco-cx/deco/observability/otel/samplers/urlBased.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Lets ops dial sampling rates per URL pattern without redeploying. Reads
|
|
5
|
+
* `OTEL_SAMPLING_CONFIG` (base64-encoded JSON) at boot and decides each
|
|
6
|
+
* trace's sample rate based on the matching pattern.
|
|
7
|
+
*
|
|
8
|
+
* Wrapped in `ParentBasedSampler` so a span inherits its parent's sampling
|
|
9
|
+
* decision when one exists (i.e. distributed traces are kept consistent end
|
|
10
|
+
* to end).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```jsonc
|
|
14
|
+
* // base64-encode this and set as OTEL_SAMPLING_CONFIG:
|
|
15
|
+
* {
|
|
16
|
+
* "default": 0.05,
|
|
17
|
+
* "rules": [
|
|
18
|
+
* { "pattern": "^/checkout", "ratio": 1.0 },
|
|
19
|
+
* { "pattern": "^/api/health", "ratio": 0.0 },
|
|
20
|
+
* { "pattern": "/p$", "ratio": 0.1 }
|
|
21
|
+
* ]
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type Attributes, type Context, type Link, type SpanKind, trace } from "@opentelemetry/api";
|
|
27
|
+
import {
|
|
28
|
+
AlwaysOffSampler,
|
|
29
|
+
AlwaysOnSampler,
|
|
30
|
+
ParentBasedSampler,
|
|
31
|
+
type Sampler,
|
|
32
|
+
type SamplingResult,
|
|
33
|
+
TraceIdRatioBasedSampler,
|
|
34
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
35
|
+
|
|
36
|
+
export interface SamplingRule {
|
|
37
|
+
/** ECMA RegExp pattern matched against the URL path. */
|
|
38
|
+
pattern: string;
|
|
39
|
+
/** Ratio in [0, 1]. */
|
|
40
|
+
ratio: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SamplingConfig {
|
|
44
|
+
/** Default sample ratio applied when no rule matches. Defaults to 1.0 (always sample). */
|
|
45
|
+
default?: number;
|
|
46
|
+
/** Ordered list of rules. First match wins. */
|
|
47
|
+
rules?: SamplingRule[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CompiledRule {
|
|
51
|
+
re: RegExp;
|
|
52
|
+
sampler: Sampler;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* URL-pattern-driven head sampler. Implements the OTel `Sampler` interface
|
|
57
|
+
* directly so it can be plugged into `ParentBasedSampler`'s `root` slot.
|
|
58
|
+
*/
|
|
59
|
+
export class URLBasedSampler implements Sampler {
|
|
60
|
+
private readonly defaultSampler: Sampler;
|
|
61
|
+
private readonly rules: CompiledRule[];
|
|
62
|
+
|
|
63
|
+
constructor(config: SamplingConfig = {}) {
|
|
64
|
+
this.defaultSampler = ratioToSampler(config.default ?? 1.0);
|
|
65
|
+
this.rules = (config.rules ?? []).map((rule) => ({
|
|
66
|
+
re: new RegExp(rule.pattern),
|
|
67
|
+
sampler: ratioToSampler(rule.ratio),
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
shouldSample(
|
|
72
|
+
context: Context,
|
|
73
|
+
traceId: string,
|
|
74
|
+
spanName: string,
|
|
75
|
+
spanKind: SpanKind,
|
|
76
|
+
attributes: Attributes,
|
|
77
|
+
links: Link[],
|
|
78
|
+
): SamplingResult {
|
|
79
|
+
const path = extractPath(attributes);
|
|
80
|
+
if (path) {
|
|
81
|
+
for (const rule of this.rules) {
|
|
82
|
+
if (rule.re.test(path)) {
|
|
83
|
+
return rule.sampler.shouldSample(context, traceId, spanName, spanKind, attributes, links);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return this.defaultSampler.shouldSample(
|
|
88
|
+
context,
|
|
89
|
+
traceId,
|
|
90
|
+
spanName,
|
|
91
|
+
spanKind,
|
|
92
|
+
attributes,
|
|
93
|
+
links,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toString(): string {
|
|
98
|
+
return `URLBasedSampler(${this.rules.length} rules)`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ratioToSampler(ratio: number): Sampler {
|
|
103
|
+
if (ratio >= 1) return new AlwaysOnSampler();
|
|
104
|
+
if (ratio <= 0) return new AlwaysOffSampler();
|
|
105
|
+
return new TraceIdRatioBasedSampler(ratio);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractPath(attrs: Attributes): string | null {
|
|
109
|
+
// Prefer the OTel-standard `url.path` (semconv >= 1.21), fall back to
|
|
110
|
+
// legacy `http.target` and `http.url`.
|
|
111
|
+
const direct = attrs["url.path"] ?? attrs["http.target"];
|
|
112
|
+
if (typeof direct === "string") return direct;
|
|
113
|
+
|
|
114
|
+
const httpUrl = attrs["http.url"];
|
|
115
|
+
if (typeof httpUrl === "string") {
|
|
116
|
+
try {
|
|
117
|
+
return new URL(httpUrl).pathname;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Boot helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Decode a base64-encoded `OTEL_SAMPLING_CONFIG` value into a `SamplingConfig`.
|
|
131
|
+
* Returns `null` (caller falls back to default ratio 1.0) on:
|
|
132
|
+
* - missing / empty input
|
|
133
|
+
* - invalid base64
|
|
134
|
+
* - JSON parse failure
|
|
135
|
+
* - schema-mismatched payload
|
|
136
|
+
*
|
|
137
|
+
* Logs a warning to console when the env var is set but unparseable so the
|
|
138
|
+
* mistake is visible in CF Logs without crashing the worker boot.
|
|
139
|
+
*/
|
|
140
|
+
export function decodeSamplingConfig(raw: string | undefined): SamplingConfig | null {
|
|
141
|
+
if (!raw) return null;
|
|
142
|
+
try {
|
|
143
|
+
const json = atob(raw);
|
|
144
|
+
const parsed = JSON.parse(json) as unknown;
|
|
145
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
146
|
+
const obj = parsed as { default?: unknown; rules?: unknown };
|
|
147
|
+
|
|
148
|
+
const defaultRatio = typeof obj.default === "number" ? obj.default : undefined;
|
|
149
|
+
const rawRules = Array.isArray(obj.rules) ? obj.rules : [];
|
|
150
|
+
const rules: SamplingRule[] = [];
|
|
151
|
+
for (const r of rawRules) {
|
|
152
|
+
if (!r || typeof r !== "object") continue;
|
|
153
|
+
const rec = r as { pattern?: unknown; ratio?: unknown };
|
|
154
|
+
if (typeof rec.pattern !== "string" || typeof rec.ratio !== "number") continue;
|
|
155
|
+
try {
|
|
156
|
+
// Eagerly validate the regex so a bad pattern fails at boot, not
|
|
157
|
+
// on the first matching request.
|
|
158
|
+
new RegExp(rec.pattern);
|
|
159
|
+
rules.push({ pattern: rec.pattern, ratio: rec.ratio });
|
|
160
|
+
} catch {
|
|
161
|
+
console.warn(`[sampler] dropping invalid pattern: ${rec.pattern}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { default: defaultRatio, rules };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn(`[sampler] failed to decode OTEL_SAMPLING_CONFIG`, String(err));
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a `ParentBasedSampler` rooted at our URL-based sampler.
|
|
174
|
+
* Use as the `headSampler` for `@microlabs/otel-cf-workers`.
|
|
175
|
+
*/
|
|
176
|
+
export function createUrlBasedHeadSampler(config: SamplingConfig | null): Sampler {
|
|
177
|
+
const root = new URLBasedSampler(config ?? {});
|
|
178
|
+
return new ParentBasedSampler({ root });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Re-export OTel API helper so callers can read `traceId` / build tags off
|
|
182
|
+
// the active span without importing @opentelemetry/api directly.
|
|
183
|
+
export { trace as _otelTrace };
|