@ekkolyth/logging 0.1.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/LICENSE +21 -0
- package/README.md +751 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +27 -0
- package/dist/browser.js.map +1 -0
- package/dist/context-BU9CEJZ3.js +25 -0
- package/dist/context-BU9CEJZ3.js.map +1 -0
- package/dist/context-seam-DzbwDxiY.js +12 -0
- package/dist/context-seam-DzbwDxiY.js.map +1 -0
- package/dist/context.browser.d.ts +7 -0
- package/dist/context.browser.js +14 -0
- package/dist/context.browser.js.map +1 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +2 -0
- package/dist/env-Dur55C_F.js +50 -0
- package/dist/env-Dur55C_F.js.map +1 -0
- package/dist/http.browser.d.ts +16 -0
- package/dist/http.browser.js +23 -0
- package/dist/http.browser.js.map +1 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +112 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-BxxJhfg8.js +446 -0
- package/dist/logger-BxxJhfg8.js.map +1 -0
- package/dist/logger-V7XF9Wuo.d.ts +35 -0
- package/dist/outbound-CQkyIyxd.d.ts +10 -0
- package/dist/outbound-YYy-EfTF.js +216 -0
- package/dist/outbound-YYy-EfTF.js.map +1 -0
- package/dist/throttler-s0TCHZNK.js +45 -0
- package/dist/throttler-s0TCHZNK.js.map +1 -0
- package/dist/types-CieIrR8p.d.ts +7 -0
- package/package.json +56 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region src/http/correlation.ts
|
|
2
|
+
const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
3
|
+
const HEX_VERSION = /^[0-9a-f]{2}$/;
|
|
4
|
+
const HEX_TRACE_ID = /^[0-9a-f]{32}$/;
|
|
5
|
+
const HEX_SPAN_ID = /^[0-9a-f]{16}$/;
|
|
6
|
+
const HEX_FLAGS = /^[0-9a-f]{2}$/;
|
|
7
|
+
const ZERO_TRACE_ID = "0".repeat(32);
|
|
8
|
+
const ZERO_SPAN_ID = "0".repeat(16);
|
|
9
|
+
const TRACESTATE_MEMBER = /^[a-z0-9][a-z0-9_*/-]{0,255}(@[a-z][a-z0-9_*/-]{0,240})?=[\x20-\x2b\x2d-\x3c\x3e-\x7e]*[\x21-\x2b\x2d-\x3c\x3e-\x7e]$/;
|
|
10
|
+
function isValidRequestId(value) {
|
|
11
|
+
return typeof value === "string" && REQUEST_ID_PATTERN.test(value);
|
|
12
|
+
}
|
|
13
|
+
function isValidTracestate(value) {
|
|
14
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 512) return false;
|
|
15
|
+
const members = value.split(",").map((member) => member.trim()).filter((member) => member.length > 0);
|
|
16
|
+
if (members.length === 0 || members.length > 32) return false;
|
|
17
|
+
return members.every((member) => TRACESTATE_MEMBER.test(member));
|
|
18
|
+
}
|
|
19
|
+
function randomHex(byteLength) {
|
|
20
|
+
const bytes = new Uint8Array(byteLength);
|
|
21
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
22
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
}
|
|
24
|
+
function generateTraceId() {
|
|
25
|
+
let id = randomHex(16);
|
|
26
|
+
while (id === ZERO_TRACE_ID) id = randomHex(16);
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
function generateSpanId() {
|
|
30
|
+
let id = randomHex(8);
|
|
31
|
+
while (id === ZERO_SPAN_ID) id = randomHex(8);
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
function generateRequestId() {
|
|
35
|
+
return randomHex(8);
|
|
36
|
+
}
|
|
37
|
+
function parseTraceparent(header) {
|
|
38
|
+
if (!header) return void 0;
|
|
39
|
+
const parts = header.split("-");
|
|
40
|
+
if (parts.length < 4) return void 0;
|
|
41
|
+
const [version, traceId, parentId, flags] = parts;
|
|
42
|
+
if (!HEX_VERSION.test(version) || version === "ff") return void 0;
|
|
43
|
+
if (version === "00" && parts.length !== 4) return void 0;
|
|
44
|
+
if (!HEX_TRACE_ID.test(traceId) || traceId === ZERO_TRACE_ID) return void 0;
|
|
45
|
+
if (!HEX_SPAN_ID.test(parentId) || parentId === ZERO_SPAN_ID) return void 0;
|
|
46
|
+
if (!HEX_FLAGS.test(flags)) return void 0;
|
|
47
|
+
return {
|
|
48
|
+
traceId,
|
|
49
|
+
parentId,
|
|
50
|
+
flags
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function buildTraceparentHeader(correlation) {
|
|
54
|
+
return `00-${correlation.traceId}-${correlation.spanId}-${correlation.flags}`;
|
|
55
|
+
}
|
|
56
|
+
function deriveInboundCorrelation(headers) {
|
|
57
|
+
const parsed = parseTraceparent(headers.get("traceparent"));
|
|
58
|
+
const traceId = parsed?.traceId ?? generateTraceId();
|
|
59
|
+
const spanId = generateSpanId();
|
|
60
|
+
const flags = parsed?.flags ?? "00";
|
|
61
|
+
const tracestateHeader = headers.get("tracestate");
|
|
62
|
+
const tracestate = isValidTracestate(tracestateHeader) ? tracestateHeader : void 0;
|
|
63
|
+
const requestIdHeader = headers.get("x-request-id");
|
|
64
|
+
return {
|
|
65
|
+
requestId: isValidRequestId(requestIdHeader) ? requestIdHeader : generateRequestId(),
|
|
66
|
+
traceId,
|
|
67
|
+
spanId,
|
|
68
|
+
flags,
|
|
69
|
+
tracestate
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function resolveOutboundCorrelation(headers, ambient) {
|
|
73
|
+
const parsed = parseTraceparent(headers.get("traceparent"));
|
|
74
|
+
let traceId;
|
|
75
|
+
let spanId;
|
|
76
|
+
let flags;
|
|
77
|
+
if (parsed) {
|
|
78
|
+
traceId = parsed.traceId;
|
|
79
|
+
spanId = parsed.parentId;
|
|
80
|
+
flags = parsed.flags;
|
|
81
|
+
} else if (ambient?.traceId) {
|
|
82
|
+
traceId = ambient.traceId;
|
|
83
|
+
spanId = generateSpanId();
|
|
84
|
+
flags = ambient.flags ?? "00";
|
|
85
|
+
} else {
|
|
86
|
+
traceId = generateTraceId();
|
|
87
|
+
spanId = generateSpanId();
|
|
88
|
+
flags = "00";
|
|
89
|
+
}
|
|
90
|
+
const tracestateHeader = headers.get("tracestate");
|
|
91
|
+
const tracestate = isValidTracestate(tracestateHeader) ? tracestateHeader : ambient?.tracestate;
|
|
92
|
+
const requestIdHeader = headers.get("x-request-id");
|
|
93
|
+
return {
|
|
94
|
+
requestId: isValidRequestId(requestIdHeader) ? requestIdHeader : ambient?.requestId ?? generateRequestId(),
|
|
95
|
+
traceId,
|
|
96
|
+
spanId,
|
|
97
|
+
flags,
|
|
98
|
+
tracestate
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function injectCorrelationHeaders(headers, correlation) {
|
|
102
|
+
if (!headers.has("traceparent")) headers.set("traceparent", buildTraceparentHeader(correlation));
|
|
103
|
+
if (!headers.has("tracestate") && correlation.tracestate) headers.set("tracestate", correlation.tracestate);
|
|
104
|
+
if (!headers.has("x-request-id")) headers.set("x-request-id", correlation.requestId);
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/http/sampling.ts
|
|
108
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
109
|
+
const FNV_PRIME = 16777619;
|
|
110
|
+
const UINT32_SPACE = 2 ** 32;
|
|
111
|
+
const textEncoder = new TextEncoder();
|
|
112
|
+
function fnv1a32(identifier) {
|
|
113
|
+
let hash = FNV_OFFSET_BASIS;
|
|
114
|
+
for (const byte of textEncoder.encode(identifier)) {
|
|
115
|
+
hash ^= byte;
|
|
116
|
+
hash = Math.imul(hash, FNV_PRIME) >>> 0;
|
|
117
|
+
}
|
|
118
|
+
return hash >>> 0;
|
|
119
|
+
}
|
|
120
|
+
function shouldSample(identifier, sampleRate) {
|
|
121
|
+
if (sampleRate >= 1) return true;
|
|
122
|
+
if (sampleRate <= 0) return false;
|
|
123
|
+
return fnv1a32(identifier) / UINT32_SPACE < sampleRate;
|
|
124
|
+
}
|
|
125
|
+
function resolveSampleRate(explicit) {
|
|
126
|
+
if (explicit === void 0) return 1;
|
|
127
|
+
if (explicit <= 0 || explicit > 1) throw new TypeError(`@ekkolyth/logging: invalid sampleRate ${explicit}, must be within (0, 1]`);
|
|
128
|
+
return explicit;
|
|
129
|
+
}
|
|
130
|
+
function resolveSlowThresholdMs(explicit) {
|
|
131
|
+
if (explicit === void 0 || explicit === 0) return 1e3;
|
|
132
|
+
if (explicit < 0) throw new TypeError(`@ekkolyth/logging: invalid slowThresholdMs ${explicit}, must be non-negative`);
|
|
133
|
+
return explicit;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/http/outbound.ts
|
|
137
|
+
function samplingIdentifier(correlation) {
|
|
138
|
+
return correlation.traceId || correlation.requestId;
|
|
139
|
+
}
|
|
140
|
+
function classifyOutboundLevel(input) {
|
|
141
|
+
const { correlation, didFail, durationMs, sampleRate, slowThresholdMs, status } = input;
|
|
142
|
+
if (didFail) return "error";
|
|
143
|
+
if (status !== void 0 && status >= 500) return "error";
|
|
144
|
+
if (status !== void 0 && status >= 400) return "warn";
|
|
145
|
+
if (durationMs >= slowThresholdMs) return "warn";
|
|
146
|
+
if (!shouldSample(samplingIdentifier(correlation), sampleRate)) return void 0;
|
|
147
|
+
return "info";
|
|
148
|
+
}
|
|
149
|
+
function emitOutboundRecord(input) {
|
|
150
|
+
const { correlation, didFail, durationMs, failure, log, options, request, response, sampleRate, slowThresholdMs } = input;
|
|
151
|
+
const level = classifyOutboundLevel({
|
|
152
|
+
correlation,
|
|
153
|
+
didFail,
|
|
154
|
+
durationMs,
|
|
155
|
+
sampleRate,
|
|
156
|
+
slowThresholdMs,
|
|
157
|
+
status: response?.status
|
|
158
|
+
});
|
|
159
|
+
if (level === void 0) return;
|
|
160
|
+
const url = new URL(request.url);
|
|
161
|
+
const attributes = {};
|
|
162
|
+
if (options.provider) attributes.provider = options.provider;
|
|
163
|
+
attributes.method = request.method;
|
|
164
|
+
attributes.host = url.host;
|
|
165
|
+
attributes.path = url.pathname;
|
|
166
|
+
if (response) attributes.status = response.status;
|
|
167
|
+
attributes.duration_ms = Math.trunc(durationMs);
|
|
168
|
+
attributes.request_id = correlation.requestId;
|
|
169
|
+
attributes.trace_id = correlation.traceId;
|
|
170
|
+
attributes.span_id = correlation.spanId;
|
|
171
|
+
if (level === "info" && sampleRate < 1) attributes.sample_rate = sampleRate;
|
|
172
|
+
if (didFail) attributes.error = failure;
|
|
173
|
+
log[level]("outbound request completed", attributes);
|
|
174
|
+
}
|
|
175
|
+
function createWrapFetch(getAmbientCorrelation) {
|
|
176
|
+
return function wrapFetch(log, fetchFn, options = {}) {
|
|
177
|
+
const sampleRate = resolveSampleRate(options.sampleRate);
|
|
178
|
+
const slowThresholdMs = resolveSlowThresholdMs(options.slowThresholdMs);
|
|
179
|
+
const wrapped = async (input, init) => {
|
|
180
|
+
const request = new Request(input, init);
|
|
181
|
+
const ambient = getAmbientCorrelation(log);
|
|
182
|
+
const correlation = resolveOutboundCorrelation(request.headers, ambient);
|
|
183
|
+
injectCorrelationHeaders(request.headers, correlation);
|
|
184
|
+
const start = performance.now();
|
|
185
|
+
let response;
|
|
186
|
+
let failure;
|
|
187
|
+
let didFail = false;
|
|
188
|
+
try {
|
|
189
|
+
response = await fetchFn(request);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
failure = error;
|
|
192
|
+
didFail = true;
|
|
193
|
+
}
|
|
194
|
+
const durationMs = performance.now() - start;
|
|
195
|
+
emitOutboundRecord({
|
|
196
|
+
correlation,
|
|
197
|
+
didFail,
|
|
198
|
+
durationMs,
|
|
199
|
+
failure,
|
|
200
|
+
log,
|
|
201
|
+
options,
|
|
202
|
+
request,
|
|
203
|
+
response,
|
|
204
|
+
sampleRate,
|
|
205
|
+
slowThresholdMs
|
|
206
|
+
});
|
|
207
|
+
if (didFail) throw failure;
|
|
208
|
+
return response;
|
|
209
|
+
};
|
|
210
|
+
return wrapped;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
export { shouldSample as a, resolveSlowThresholdMs as i, samplingIdentifier as n, deriveInboundCorrelation as o, resolveSampleRate as r, createWrapFetch as t };
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=outbound-YYy-EfTF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-YYy-EfTF.js","names":[],"sources":["../src/http/correlation.ts","../src/http/sampling.ts","../src/http/outbound.ts"],"sourcesContent":["const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/\nconst HEX_VERSION = /^[0-9a-f]{2}$/\nconst HEX_TRACE_ID = /^[0-9a-f]{32}$/\nconst HEX_SPAN_ID = /^[0-9a-f]{16}$/\nconst HEX_FLAGS = /^[0-9a-f]{2}$/\nconst ZERO_TRACE_ID = '0'.repeat(32)\nconst ZERO_SPAN_ID = '0'.repeat(16)\nconst TRACESTATE_MEMBER =\n /^[a-z0-9][a-z0-9_*/-]{0,255}(@[a-z][a-z0-9_*/-]{0,240})?=[\\x20-\\x2b\\x2d-\\x3c\\x3e-\\x7e]*[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]$/\n\ninterface Correlation {\n flags: string\n requestId: string\n spanId: string\n traceId: string\n tracestate?: string\n}\n\ninterface AmbientCorrelation {\n flags?: string\n requestId?: string\n spanId?: string\n traceId?: string\n tracestate?: string\n}\n\ninterface ParsedTraceparent {\n flags: string\n parentId: string\n traceId: string\n}\n\nfunction isValidRequestId(value: string | null | undefined): value is string {\n return typeof value === 'string' && REQUEST_ID_PATTERN.test(value)\n}\n\nfunction isValidTracestate(value: string | null | undefined): value is string {\n if (typeof value !== 'string' || value.length === 0 || value.length > 512)\n return false\n const members = value\n .split(',')\n .map((member) => member.trim())\n .filter((member) => member.length > 0)\n if (members.length === 0 || members.length > 32) return false\n return members.every((member) => TRACESTATE_MEMBER.test(member))\n}\n\nfunction randomHex(byteLength: number): string {\n const bytes = new Uint8Array(byteLength)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(\n ''\n )\n}\n\nfunction generateTraceId(): string {\n let id = randomHex(16)\n while (id === ZERO_TRACE_ID) id = randomHex(16)\n return id\n}\n\nfunction generateSpanId(): string {\n let id = randomHex(8)\n while (id === ZERO_SPAN_ID) id = randomHex(8)\n return id\n}\n\nfunction generateRequestId(): string {\n return randomHex(8)\n}\n\n// parseTraceparent validates a header per W3C Trace Context: version 00\n// requires exactly 4 dash-separated fields; a later version may carry\n// trailing fields a parser must ignore.\nfunction parseTraceparent(\n header: string | null | undefined\n): ParsedTraceparent | undefined {\n if (!header) return undefined\n const parts = header.split('-')\n if (parts.length < 4) return undefined\n const [version, traceId, parentId, flags] = parts as [\n string,\n string,\n string,\n string,\n ]\n if (!HEX_VERSION.test(version) || version === 'ff') return undefined\n if (version === '00' && parts.length !== 4) return undefined\n if (!HEX_TRACE_ID.test(traceId) || traceId === ZERO_TRACE_ID)\n return undefined\n if (!HEX_SPAN_ID.test(parentId) || parentId === ZERO_SPAN_ID)\n return undefined\n if (!HEX_FLAGS.test(flags)) return undefined\n return { traceId, parentId, flags }\n}\n\nfunction buildTraceparentHeader(\n correlation: Pick<Correlation, 'flags' | 'spanId' | 'traceId'>\n): string {\n return `00-${correlation.traceId}-${correlation.spanId}-${correlation.flags}`\n}\n\n// deriveInboundCorrelation reads an inbound request's correlation headers,\n// keeping a valid trace ID and flags while minting a fresh local span ID,\n// and mints a full root correlation when no valid traceparent is present.\nfunction deriveInboundCorrelation(headers: Headers): Correlation {\n const parsed = parseTraceparent(headers.get('traceparent'))\n const traceId = parsed?.traceId ?? generateTraceId()\n const spanId = generateSpanId()\n const flags = parsed?.flags ?? '00'\n\n const tracestateHeader = headers.get('tracestate')\n const tracestate = isValidTracestate(tracestateHeader)\n ? tracestateHeader\n : undefined\n\n const requestIdHeader = headers.get('x-request-id')\n const requestId = isValidRequestId(requestIdHeader)\n ? requestIdHeader\n : generateRequestId()\n\n return { requestId, traceId, spanId, flags, tracestate }\n}\n\n// resolveOutboundCorrelation picks the record's own trace_id/span_id/\n// request_id: a valid explicit traceparent header seeds it directly (the\n// wrapper never rewrites a header the caller already set); otherwise the\n// ambient correlation supplies a trace ID under which a fresh child span is\n// minted; with neither, a brand-new root correlation is minted so sampling\n// still has a stable identifier.\nfunction resolveOutboundCorrelation(\n headers: Headers,\n ambient: AmbientCorrelation | undefined\n): Correlation {\n const parsed = parseTraceparent(headers.get('traceparent'))\n\n let traceId: string\n let spanId: string\n let flags: string\n if (parsed) {\n traceId = parsed.traceId\n spanId = parsed.parentId\n flags = parsed.flags\n } else if (ambient?.traceId) {\n traceId = ambient.traceId\n spanId = generateSpanId()\n flags = ambient.flags ?? '00'\n } else {\n traceId = generateTraceId()\n spanId = generateSpanId()\n flags = '00'\n }\n\n const tracestateHeader = headers.get('tracestate')\n const tracestate = isValidTracestate(tracestateHeader)\n ? tracestateHeader\n : ambient?.tracestate\n\n const requestIdHeader = headers.get('x-request-id')\n const requestId = isValidRequestId(requestIdHeader)\n ? requestIdHeader\n : (ambient?.requestId ?? generateRequestId())\n\n return { requestId, traceId, spanId, flags, tracestate }\n}\n\n// injectCorrelationHeaders writes traceparent/tracestate/X-Request-Id only\n// onto headers the caller has not already set.\nfunction injectCorrelationHeaders(\n headers: Headers,\n correlation: Correlation\n): void {\n if (!headers.has('traceparent')) {\n headers.set('traceparent', buildTraceparentHeader(correlation))\n }\n if (!headers.has('tracestate') && correlation.tracestate) {\n headers.set('tracestate', correlation.tracestate)\n }\n if (!headers.has('x-request-id')) {\n headers.set('x-request-id', correlation.requestId)\n }\n}\n\nexport type { AmbientCorrelation, Correlation }\nexport {\n buildTraceparentHeader,\n deriveInboundCorrelation,\n injectCorrelationHeaders,\n isValidRequestId,\n isValidTracestate,\n parseTraceparent,\n resolveOutboundCorrelation,\n}\n","const FNV_OFFSET_BASIS = 2166136261\nconst FNV_PRIME = 16777619\nconst UINT32_SPACE = 2 ** 32\n\nconst textEncoder = new TextEncoder()\n\n// fnv1a32 hashes identifier's UTF-8 bytes with unsigned FNV-1a 32-bit — the\n// same algorithm Go's hash/fnv implements, so a given identifier hashes\n// identically on both runtimes.\nfunction fnv1a32(identifier: string): number {\n let hash = FNV_OFFSET_BASIS\n for (const byte of textEncoder.encode(identifier)) {\n hash ^= byte\n hash = Math.imul(hash, FNV_PRIME) >>> 0\n }\n return hash >>> 0\n}\n\nfunction shouldSample(identifier: string, sampleRate: number): boolean {\n if (sampleRate >= 1) return true\n if (sampleRate <= 0) return false\n return fnv1a32(identifier) / UINT32_SPACE < sampleRate\n}\n\n// JS distinguishes \"unset\" (undefined) from a real explicit value, so only\n// undefined defaults to 1.0 — an explicit 0 is out of (0, 1] and invalid.\nfunction resolveSampleRate(explicit: number | undefined): number {\n if (explicit === undefined) return 1\n if (explicit <= 0 || explicit > 1) {\n throw new TypeError(\n `@ekkolyth/logging: invalid sampleRate ${explicit}, must be within (0, 1]`\n )\n }\n return explicit\n}\n\nfunction resolveSlowThresholdMs(explicit: number | undefined): number {\n if (explicit === undefined || explicit === 0) return 1000\n if (explicit < 0) {\n throw new TypeError(\n `@ekkolyth/logging: invalid slowThresholdMs ${explicit}, must be non-negative`\n )\n }\n return explicit\n}\n\nexport { fnv1a32, resolveSampleRate, resolveSlowThresholdMs, shouldSample }\n","import type { Logger } from '../logger'\nimport {\n type AmbientCorrelation,\n type Correlation,\n injectCorrelationHeaders,\n resolveOutboundCorrelation,\n} from './correlation'\nimport {\n resolveSampleRate,\n resolveSlowThresholdMs,\n shouldSample,\n} from './sampling'\n\n// internal only — not re-exported by index.ts/browser.ts; http.ts and\n// http.browser.ts each parameterize createWrapFetch with their own ambient-\n// correlation source (AsyncLocalStorage on Node, the logger's own bound\n// attributes in the browser) and re-export the result as their public\n// wrapFetch.\ntype FetchLike = (\n input: RequestInfo | URL,\n init?: RequestInit\n) => Promise<Response>\n\ninterface FetchOptions {\n provider?: string\n sampleRate?: number\n slowThresholdMs?: number\n}\n\ntype Level = 'error' | 'info' | 'warn'\n\nfunction samplingIdentifier(\n correlation: Pick<Correlation, 'requestId' | 'traceId'>\n): string {\n return correlation.traceId || correlation.requestId\n}\n\nfunction classifyOutboundLevel(input: {\n correlation: Pick<Correlation, 'requestId' | 'traceId'>\n didFail: boolean\n durationMs: number\n sampleRate: number\n slowThresholdMs: number\n status: number | undefined\n}): Level | undefined {\n const {\n correlation,\n didFail,\n durationMs,\n sampleRate,\n slowThresholdMs,\n status,\n } = input\n if (didFail) return 'error'\n if (status !== undefined && status >= 500) return 'error'\n if (status !== undefined && status >= 400) return 'warn'\n if (durationMs >= slowThresholdMs) return 'warn'\n if (!shouldSample(samplingIdentifier(correlation), sampleRate))\n return undefined\n return 'info'\n}\n\nfunction emitOutboundRecord(input: {\n correlation: Correlation\n didFail: boolean\n durationMs: number\n failure: unknown\n log: Logger\n options: FetchOptions\n request: Request\n response: Response | undefined\n sampleRate: number\n slowThresholdMs: number\n}): void {\n const {\n correlation,\n didFail,\n durationMs,\n failure,\n log,\n options,\n request,\n response,\n sampleRate,\n slowThresholdMs,\n } = input\n\n const level = classifyOutboundLevel({\n correlation,\n didFail,\n durationMs,\n sampleRate,\n slowThresholdMs,\n status: response?.status,\n })\n if (level === undefined) return\n\n const url = new URL(request.url)\n const attributes: Record<string, unknown> = {}\n if (options.provider) attributes.provider = options.provider\n attributes.method = request.method\n attributes.host = url.host\n attributes.path = url.pathname\n if (response) attributes.status = response.status\n attributes.duration_ms = Math.trunc(durationMs)\n attributes.request_id = correlation.requestId\n attributes.trace_id = correlation.traceId\n attributes.span_id = correlation.spanId\n\n if (level === 'info' && sampleRate < 1) attributes.sample_rate = sampleRate\n if (didFail) attributes.error = failure\n\n log[level]('outbound request completed', attributes)\n}\n\n// getAmbientCorrelation is called once per wrapped fetch invocation, not\n// once at wrapFetch() setup — required on Node, where it reads the\n// request-scoped AsyncLocalStorage store; harmless in the browser, where the\n// logger's bound attributes it reads don't change between calls.\nfunction createWrapFetch(\n getAmbientCorrelation: (log: Logger) => AmbientCorrelation | undefined\n) {\n return function wrapFetch(\n log: Logger,\n fetchFn: FetchLike,\n options: FetchOptions = {}\n ): FetchLike {\n const sampleRate = resolveSampleRate(options.sampleRate)\n const slowThresholdMs = resolveSlowThresholdMs(options.slowThresholdMs)\n\n const wrapped = async (\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> => {\n const request = new Request(input, init)\n const ambient = getAmbientCorrelation(log)\n const correlation = resolveOutboundCorrelation(\n request.headers,\n ambient\n )\n injectCorrelationHeaders(request.headers, correlation)\n\n const start = performance.now()\n let response: Response | undefined\n let failure: unknown\n let didFail = false\n try {\n response = await fetchFn(request)\n } catch (error) {\n failure = error\n didFail = true\n }\n const durationMs = performance.now() - start\n\n emitOutboundRecord({\n correlation,\n didFail,\n durationMs,\n failure,\n log,\n options,\n request,\n response,\n sampleRate,\n slowThresholdMs,\n })\n\n if (didFail) throw failure\n return response as Response\n }\n\n return wrapped\n }\n}\n\nexport type { FetchLike, FetchOptions, Level }\nexport { classifyOutboundLevel, createWrapFetch, samplingIdentifier }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/throttler.ts
|
|
2
|
+
var Throttler = class {
|
|
3
|
+
window;
|
|
4
|
+
last = /* @__PURE__ */ new Map();
|
|
5
|
+
constructor(window) {
|
|
6
|
+
this.window = window;
|
|
7
|
+
}
|
|
8
|
+
shouldEmit(...keys) {
|
|
9
|
+
if (this.window === 0) return true;
|
|
10
|
+
const dedupe = keys.join("|");
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const prev = this.last.get(dedupe);
|
|
13
|
+
if (prev !== void 0 && now - prev < this.window) return false;
|
|
14
|
+
this.last.set(dedupe, now);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function parseWindow(window) {
|
|
19
|
+
if (typeof window === "number") return window;
|
|
20
|
+
const match = /^(\d+)(ms|s|m|h)$/.exec(window);
|
|
21
|
+
if (!match) throw new TypeError(`invalid window: ${window}`);
|
|
22
|
+
const value = Number(match[1]);
|
|
23
|
+
switch (match[2]) {
|
|
24
|
+
case "ms": return value;
|
|
25
|
+
case "s": return value * 1e3;
|
|
26
|
+
case "m": return value * 6e4;
|
|
27
|
+
default: return value * 36e5;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const everyThrottlers = /* @__PURE__ */ new Map();
|
|
31
|
+
function throttlerForWindow(windowMs) {
|
|
32
|
+
let throttler = everyThrottlers.get(windowMs);
|
|
33
|
+
if (throttler === void 0) {
|
|
34
|
+
throttler = new Throttler(windowMs);
|
|
35
|
+
everyThrottlers.set(windowMs, throttler);
|
|
36
|
+
}
|
|
37
|
+
return throttler;
|
|
38
|
+
}
|
|
39
|
+
function shouldEmitEvery(level, windowMs, key, msg) {
|
|
40
|
+
return throttlerForWindow(windowMs).shouldEmit(level, key, msg);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { parseWindow as n, shouldEmitEvery as r, Throttler as t };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=throttler-s0TCHZNK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttler-s0TCHZNK.js","names":[],"sources":["../src/throttler.ts"],"sourcesContent":["type Window =\n | number\n | `${number}ms`\n | `${number}s`\n | `${number}m`\n | `${number}h`\n\n// drops repeat events within window, keyed by shouldEmit's args.\n// A zero window emits every call.\nclass Throttler {\n private readonly window: number\n private readonly last = new Map<string, number>()\n\n constructor(window: number) {\n this.window = window\n }\n\n shouldEmit(...keys: string[]): boolean {\n if (this.window === 0) return true\n const dedupe = keys.join('|')\n const now = Date.now()\n const prev = this.last.get(dedupe)\n if (prev !== undefined && now - prev < this.window) return false\n this.last.set(dedupe, now)\n return true\n }\n}\n\nfunction parseWindow(window: Window): number {\n if (typeof window === 'number') return window\n const match = /^(\\d+)(ms|s|m|h)$/.exec(window)\n if (!match) throw new TypeError(`invalid window: ${window}`)\n const value = Number(match[1])\n switch (match[2]) {\n case 'ms':\n return value\n case 's':\n return value * 1000\n case 'm':\n return value * 60_000\n default:\n return value * 3_600_000\n }\n}\n\n// one throttler per window so every() call sites sharing a window share\n// suppression state, exactly like the pre-rewrite package\nconst everyThrottlers = new Map<number, Throttler>()\n\nfunction throttlerForWindow(windowMs: number): Throttler {\n let throttler = everyThrottlers.get(windowMs)\n if (throttler === undefined) {\n throttler = new Throttler(windowMs)\n everyThrottlers.set(windowMs, throttler)\n }\n return throttler\n}\n\nfunction shouldEmitEvery(\n level: string,\n windowMs: number,\n key: string,\n msg: string\n): boolean {\n return throttlerForWindow(windowMs).shouldEmit(level, key, msg)\n}\n\nexport type { Window }\nexport { parseWindow, shouldEmitEvery, Throttler }\n"],"mappings":";AASA,IAAM,YAAN,MAAgB;CACZ;CACA,uBAAwB,IAAI,IAAoB;CAEhD,YAAY,QAAgB;EACxB,KAAK,SAAS;CAClB;CAEA,WAAW,GAAG,MAAyB;EACnC,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,MAAM,SAAS,KAAK,KAAK,GAAG;EAC5B,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,OAAO,KAAK,KAAK,IAAI,MAAM;EACjC,IAAI,SAAS,KAAA,KAAa,MAAM,OAAO,KAAK,QAAQ,OAAO;EAC3D,KAAK,KAAK,IAAI,QAAQ,GAAG;EACzB,OAAO;CACX;AACJ;AAEA,SAAS,YAAY,QAAwB;CACzC,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,MAAM,QAAQ,oBAAoB,KAAK,MAAM;CAC7C,IAAI,CAAC,OAAO,MAAM,IAAI,UAAU,mBAAmB,QAAQ;CAC3D,MAAM,QAAQ,OAAO,MAAM,EAAE;CAC7B,QAAQ,MAAM,IAAd;EACI,KAAK,MACD,OAAO;EACX,KAAK,KACD,OAAO,QAAQ;EACnB,KAAK,KACD,OAAO,QAAQ;EACnB,SACI,OAAO,QAAQ;CACvB;AACJ;AAIA,MAAM,kCAAkB,IAAI,IAAuB;AAEnD,SAAS,mBAAmB,UAA6B;CACrD,IAAI,YAAY,gBAAgB,IAAI,QAAQ;CAC5C,IAAI,cAAc,KAAA,GAAW;EACzB,YAAY,IAAI,UAAU,QAAQ;EAClC,gBAAgB,IAAI,UAAU,SAAS;CAC3C;CACA,OAAO;AACX;AAEA,SAAS,gBACL,OACA,UACA,KACA,KACO;CACP,OAAO,mBAAmB,QAAQ,CAAC,CAAC,WAAW,OAAO,KAAK,GAAG;AAClE"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
3
|
+
type LogFormat = 'auto' | 'json' | 'pretty';
|
|
4
|
+
type LogAttributes = Record<string, unknown>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { LogFormat as n, LogLevel as r, LogAttributes as t };
|
|
7
|
+
//# sourceMappingURL=types-CieIrR8p.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekkolyth/logging",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency structured logging for JavaScript and TypeScript, with context propagation and HTTP correlation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"logging",
|
|
7
|
+
"logger",
|
|
8
|
+
"structured-logging",
|
|
9
|
+
"observability",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"browser": {
|
|
24
|
+
"types": "./dist/browser.d.ts",
|
|
25
|
+
"default": "./dist/browser.js"
|
|
26
|
+
},
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./context": {
|
|
31
|
+
"types": "./dist/context.d.ts",
|
|
32
|
+
"browser": {
|
|
33
|
+
"types": "./dist/context.browser.d.ts",
|
|
34
|
+
"default": "./dist/context.browser.js"
|
|
35
|
+
},
|
|
36
|
+
"import": "./dist/context.js",
|
|
37
|
+
"default": "./dist/context.js"
|
|
38
|
+
},
|
|
39
|
+
"./http": {
|
|
40
|
+
"types": "./dist/http.d.ts",
|
|
41
|
+
"browser": {
|
|
42
|
+
"types": "./dist/http.browser.d.ts",
|
|
43
|
+
"default": "./dist/http.browser.js"
|
|
44
|
+
},
|
|
45
|
+
"import": "./dist/http.js",
|
|
46
|
+
"default": "./dist/http.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.0.0 <25.0.0"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|