@sylphx/sdk 0.11.2 → 0.12.1
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/health/index.d.ts +731 -52
- package/dist/health/index.mjs +854 -2
- package/dist/health/index.mjs.map +1 -1
- package/dist/index.mjs +9 -2
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.d.ts +10 -0
- package/dist/nextjs/index.mjs +22 -2
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.mjs +9 -2
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.mjs +7 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/web-analytics.mjs +7 -1
- package/dist/web-analytics.mjs.map +1 -1
- package/package.json +7 -2
package/dist/health/index.mjs
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
+
// src/health/causality.ts
|
|
2
|
+
var BAGGAGE_KEY_CAUSE = "sylphx.health.cause";
|
|
3
|
+
var BAGGAGE_KEY_CHAIN = "sylphx.health.cause_chain";
|
|
4
|
+
var DEFAULT_CAUSE_THRESHOLD = 0.5;
|
|
5
|
+
var MAX_CHAIN_LENGTH = 5;
|
|
6
|
+
var cachedApi;
|
|
7
|
+
async function tryLoadCausalityApi() {
|
|
8
|
+
if (cachedApi !== void 0) return cachedApi;
|
|
9
|
+
try {
|
|
10
|
+
const api = await import("@opentelemetry/api");
|
|
11
|
+
if (!api?.context || !api?.propagation) {
|
|
12
|
+
cachedApi = null;
|
|
13
|
+
return cachedApi;
|
|
14
|
+
}
|
|
15
|
+
cachedApi = { context: api.context, propagation: api.propagation };
|
|
16
|
+
} catch {
|
|
17
|
+
cachedApi = null;
|
|
18
|
+
}
|
|
19
|
+
return cachedApi;
|
|
20
|
+
}
|
|
21
|
+
function resolveServiceNameFromEnv(env) {
|
|
22
|
+
if (env.OTEL_SERVICE_NAME) return env.OTEL_SERVICE_NAME;
|
|
23
|
+
if (env.SERVICE_NAME) return env.SERVICE_NAME;
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
function defaultServiceName() {
|
|
27
|
+
if (typeof process !== "undefined" && process.env) {
|
|
28
|
+
return resolveServiceNameFromEnv(process.env);
|
|
29
|
+
}
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
function isSafeIdentifier(part) {
|
|
33
|
+
return part.length > 0 && !/[,@\s]/.test(part);
|
|
34
|
+
}
|
|
35
|
+
function encodeCauseFromScore(score, threshold, serviceName) {
|
|
36
|
+
const factors = score.signalFactors;
|
|
37
|
+
if (!factors) return null;
|
|
38
|
+
const entries = [];
|
|
39
|
+
for (const [signalName, factor] of Object.entries(factors)) {
|
|
40
|
+
if (typeof factor !== "number" || !Number.isFinite(factor)) continue;
|
|
41
|
+
if (factor >= threshold) continue;
|
|
42
|
+
if (!isSafeIdentifier(signalName) || !isSafeIdentifier(serviceName)) continue;
|
|
43
|
+
entries.push(`${signalName}@${serviceName}`);
|
|
44
|
+
}
|
|
45
|
+
if (entries.length === 0) return null;
|
|
46
|
+
return entries.join(",");
|
|
47
|
+
}
|
|
48
|
+
function parseCauseValue(value) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const raw of value.split(",")) {
|
|
51
|
+
const piece = raw.trim();
|
|
52
|
+
if (piece.length === 0) continue;
|
|
53
|
+
const atIdx = piece.indexOf("@");
|
|
54
|
+
if (atIdx <= 0 || atIdx === piece.length - 1) continue;
|
|
55
|
+
const signal = piece.slice(0, atIdx);
|
|
56
|
+
const service = piece.slice(atIdx + 1);
|
|
57
|
+
if (!isSafeIdentifier(signal) || !isSafeIdentifier(service)) continue;
|
|
58
|
+
out.push({ signal, service });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function parseChainValue(value) {
|
|
63
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && isSafeIdentifier(s));
|
|
64
|
+
}
|
|
65
|
+
function appendChain(prev, serviceName, maxLen) {
|
|
66
|
+
if (!isSafeIdentifier(serviceName)) return prev;
|
|
67
|
+
if (prev[prev.length - 1] === serviceName) return prev;
|
|
68
|
+
const next = [...prev, serviceName];
|
|
69
|
+
if (next.length <= maxLen) return next;
|
|
70
|
+
return next.slice(next.length - maxLen);
|
|
71
|
+
}
|
|
72
|
+
var CausalityHandleImpl = class {
|
|
73
|
+
_state = "loading";
|
|
74
|
+
apiRef = null;
|
|
75
|
+
threshold;
|
|
76
|
+
serviceName;
|
|
77
|
+
propagateChain;
|
|
78
|
+
constructor(opts) {
|
|
79
|
+
const threshold = opts.threshold ?? DEFAULT_CAUSE_THRESHOLD;
|
|
80
|
+
if (!Number.isFinite(threshold) || threshold <= 0 || threshold > 1) {
|
|
81
|
+
throw new Error(`createCausalityHandle: threshold must be in (0, 1], got ${threshold}`);
|
|
82
|
+
}
|
|
83
|
+
this.threshold = threshold;
|
|
84
|
+
this.serviceName = opts.serviceName ?? defaultServiceName();
|
|
85
|
+
this.propagateChain = opts.propagateChain !== false;
|
|
86
|
+
void this.initialize();
|
|
87
|
+
}
|
|
88
|
+
get state() {
|
|
89
|
+
return this._state;
|
|
90
|
+
}
|
|
91
|
+
async initialize() {
|
|
92
|
+
const api = await tryLoadCausalityApi();
|
|
93
|
+
if (this._state === "disposed") return;
|
|
94
|
+
if (api === null) {
|
|
95
|
+
this._state = "unavailable";
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.apiRef = api;
|
|
99
|
+
this._state = "ready";
|
|
100
|
+
}
|
|
101
|
+
readCause() {
|
|
102
|
+
if (this._state !== "ready" || !this.apiRef) return null;
|
|
103
|
+
try {
|
|
104
|
+
const baggage = this.apiRef.propagation.getActiveBaggage();
|
|
105
|
+
if (!baggage) return null;
|
|
106
|
+
const causeEntry = baggage.getEntry(BAGGAGE_KEY_CAUSE);
|
|
107
|
+
if (!causeEntry || typeof causeEntry.value !== "string") return null;
|
|
108
|
+
const causes = parseCauseValue(causeEntry.value);
|
|
109
|
+
if (causes.length === 0) return null;
|
|
110
|
+
const chainEntry = baggage.getEntry(BAGGAGE_KEY_CHAIN);
|
|
111
|
+
const chain = chainEntry && typeof chainEntry.value === "string" ? parseChainValue(chainEntry.value) : [];
|
|
112
|
+
return { causes, chain };
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
encodeCause(score) {
|
|
118
|
+
try {
|
|
119
|
+
return encodeCauseFromScore(score, this.threshold, this.serviceName);
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async runWithCause(fn, score) {
|
|
125
|
+
if (this._state !== "ready" || !this.apiRef) {
|
|
126
|
+
return await fn();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const localCause = this.encodeCause(score);
|
|
130
|
+
const existing = this.apiRef.propagation.getActiveBaggage();
|
|
131
|
+
const existingChain = existing?.getEntry(BAGGAGE_KEY_CHAIN)?.value ? parseChainValue(existing.getEntry(BAGGAGE_KEY_CHAIN).value) : [];
|
|
132
|
+
if (!localCause && !existing) {
|
|
133
|
+
return await fn();
|
|
134
|
+
}
|
|
135
|
+
const baseBaggage = existing ?? this.apiRef.propagation.createBaggage();
|
|
136
|
+
let nextBaggage = baseBaggage;
|
|
137
|
+
if (localCause) {
|
|
138
|
+
const newEntry = { value: localCause };
|
|
139
|
+
const upstream = existing?.getEntry(BAGGAGE_KEY_CAUSE)?.value;
|
|
140
|
+
const mergedValue = upstream ? mergeCauseValues(upstream, localCause) : localCause;
|
|
141
|
+
nextBaggage = nextBaggage.setEntry(BAGGAGE_KEY_CAUSE, {
|
|
142
|
+
...newEntry,
|
|
143
|
+
value: mergedValue
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (this.propagateChain && (localCause || existingChain.length > 0)) {
|
|
147
|
+
const nextChain = localCause ? appendChain(existingChain, this.serviceName, MAX_CHAIN_LENGTH) : existingChain;
|
|
148
|
+
if (nextChain.length > 0) {
|
|
149
|
+
nextBaggage = nextBaggage.setEntry(BAGGAGE_KEY_CHAIN, {
|
|
150
|
+
value: nextChain.join(",")
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const ctxWithBaggage = this.apiRef.propagation.setBaggage(
|
|
155
|
+
this.apiRef.context.active(),
|
|
156
|
+
nextBaggage
|
|
157
|
+
);
|
|
158
|
+
return await this.apiRef.context.with(ctxWithBaggage, fn);
|
|
159
|
+
} catch {
|
|
160
|
+
return await fn();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispose() {
|
|
164
|
+
this._state = "disposed";
|
|
165
|
+
this.apiRef = null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function mergeCauseValues(existing, local) {
|
|
169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const piece of [...existing.split(","), ...local.split(",")]) {
|
|
172
|
+
const trimmed = piece.trim();
|
|
173
|
+
if (trimmed.length === 0 || seen.has(trimmed)) continue;
|
|
174
|
+
seen.add(trimmed);
|
|
175
|
+
out.push(trimmed);
|
|
176
|
+
}
|
|
177
|
+
return out.join(",");
|
|
178
|
+
}
|
|
179
|
+
function createCausalityHandle(opts = {}) {
|
|
180
|
+
return new CausalityHandleImpl(opts);
|
|
181
|
+
}
|
|
182
|
+
|
|
1
183
|
// src/health/effects.ts
|
|
2
184
|
import { Effect } from "effect";
|
|
3
185
|
|
|
@@ -70,6 +252,377 @@ function createNodeHandler(source) {
|
|
|
70
252
|
};
|
|
71
253
|
}
|
|
72
254
|
|
|
255
|
+
// src/health/history.ts
|
|
256
|
+
var GENESIS_PREV_HASH = "0".repeat(64);
|
|
257
|
+
var DEFAULT_HISTORY_CAPACITY = 1e3;
|
|
258
|
+
function canonicalizeSignals(signals) {
|
|
259
|
+
const keys = Object.keys(signals).sort();
|
|
260
|
+
const pairs = keys.map((k) => {
|
|
261
|
+
const value = signals[k];
|
|
262
|
+
return [k, value];
|
|
263
|
+
});
|
|
264
|
+
return JSON.stringify(Object.fromEntries(pairs));
|
|
265
|
+
}
|
|
266
|
+
function encodeScore(score) {
|
|
267
|
+
return score.toFixed(6);
|
|
268
|
+
}
|
|
269
|
+
async function sha256Hex(input) {
|
|
270
|
+
const subtle = globalThis.crypto?.subtle;
|
|
271
|
+
if (!subtle || typeof subtle.digest !== "function") {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
const buf = new TextEncoder().encode(input);
|
|
275
|
+
const digest = await subtle.digest("SHA-256", buf);
|
|
276
|
+
const bytes = new Uint8Array(digest);
|
|
277
|
+
let out = "";
|
|
278
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
279
|
+
out += (bytes[i] | 0).toString(16).padStart(2, "0");
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
async function computeEntryHash(prehash) {
|
|
284
|
+
const canonical = [
|
|
285
|
+
prehash.sequence.toString(10),
|
|
286
|
+
prehash.evaluatedAt,
|
|
287
|
+
encodeScore(prehash.score),
|
|
288
|
+
prehash.signalsDigest,
|
|
289
|
+
prehash.prevHash
|
|
290
|
+
].join("|");
|
|
291
|
+
return await sha256Hex(canonical);
|
|
292
|
+
}
|
|
293
|
+
var InMemoryHistoryStoreImpl = class {
|
|
294
|
+
capacity;
|
|
295
|
+
buf = [];
|
|
296
|
+
constructor(opts = {}) {
|
|
297
|
+
const cap = opts.capacity ?? DEFAULT_HISTORY_CAPACITY;
|
|
298
|
+
if (!Number.isFinite(cap) || cap <= 0 || cap > 1e7) {
|
|
299
|
+
throw new Error(`InMemoryHistoryStore: capacity must be in (0, 10000000], got ${cap}`);
|
|
300
|
+
}
|
|
301
|
+
this.capacity = cap;
|
|
302
|
+
}
|
|
303
|
+
append(entry) {
|
|
304
|
+
this.buf.push(entry);
|
|
305
|
+
if (this.buf.length > this.capacity) {
|
|
306
|
+
this.buf.shift();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
range(query) {
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const e of this.buf) {
|
|
312
|
+
if (query.fromSequence !== void 0 && e.sequence < query.fromSequence) continue;
|
|
313
|
+
if (query.toSequence !== void 0 && e.sequence > query.toSequence) continue;
|
|
314
|
+
if (query.fromTimestamp !== void 0 && e.evaluatedAt < query.fromTimestamp) continue;
|
|
315
|
+
if (query.toTimestamp !== void 0 && e.evaluatedAt > query.toTimestamp) continue;
|
|
316
|
+
out.push(e);
|
|
317
|
+
if (query.limit !== void 0 && out.length >= query.limit) break;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
latest() {
|
|
322
|
+
return this.buf[this.buf.length - 1] ?? null;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function createInMemoryHistoryStore(opts = {}) {
|
|
326
|
+
return new InMemoryHistoryStoreImpl(opts);
|
|
327
|
+
}
|
|
328
|
+
var HistoryRecorderImpl = class {
|
|
329
|
+
store;
|
|
330
|
+
nextSequence = 0;
|
|
331
|
+
prevHash = GENESIS_PREV_HASH;
|
|
332
|
+
disposed = false;
|
|
333
|
+
initPromise;
|
|
334
|
+
/**
|
|
335
|
+
* Serializes record() calls so concurrent `evaluate()` invocations
|
|
336
|
+
* don't race on `nextSequence` / `prevHash`. Each new call chains off
|
|
337
|
+
* the previous; the chain itself never propagates errors (catch in the
|
|
338
|
+
* tail so one failed record doesn't poison subsequent ones).
|
|
339
|
+
*/
|
|
340
|
+
inFlight = Promise.resolve();
|
|
341
|
+
constructor(opts) {
|
|
342
|
+
this.store = opts.store ?? createInMemoryHistoryStore({ capacity: opts.capacity });
|
|
343
|
+
this.initPromise = this.warmStart();
|
|
344
|
+
}
|
|
345
|
+
async warmStart() {
|
|
346
|
+
try {
|
|
347
|
+
const latest = await this.store.latest();
|
|
348
|
+
if (latest && Number.isFinite(latest.sequence)) {
|
|
349
|
+
this.nextSequence = latest.sequence + 1;
|
|
350
|
+
this.prevHash = latest.entryHash || GENESIS_PREV_HASH;
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async recordInternal(score) {
|
|
356
|
+
if (this.disposed) return null;
|
|
357
|
+
await this.initPromise;
|
|
358
|
+
try {
|
|
359
|
+
const signalsDigest = await sha256Hex(canonicalizeSignals(score.signals));
|
|
360
|
+
const prehash = {
|
|
361
|
+
sequence: this.nextSequence,
|
|
362
|
+
evaluatedAt: score.lastTickAt,
|
|
363
|
+
score: score.score,
|
|
364
|
+
signalsDigest,
|
|
365
|
+
prevHash: this.prevHash
|
|
366
|
+
};
|
|
367
|
+
const entryHash = await computeEntryHash(prehash);
|
|
368
|
+
const entry = { ...prehash, entryHash };
|
|
369
|
+
await this.store.append(entry);
|
|
370
|
+
this.nextSequence += 1;
|
|
371
|
+
this.prevHash = entryHash || GENESIS_PREV_HASH;
|
|
372
|
+
return entry;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async record(score) {
|
|
378
|
+
const job = this.inFlight.then(() => this.recordInternal(score));
|
|
379
|
+
this.inFlight = job.catch(() => void 0);
|
|
380
|
+
return await job;
|
|
381
|
+
}
|
|
382
|
+
async range(query) {
|
|
383
|
+
try {
|
|
384
|
+
return await this.store.range(query);
|
|
385
|
+
} catch {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async latest() {
|
|
390
|
+
try {
|
|
391
|
+
return await this.store.latest();
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async verify(query) {
|
|
397
|
+
const entries = await this.range(query);
|
|
398
|
+
return await verifyChain(entries);
|
|
399
|
+
}
|
|
400
|
+
dispose() {
|
|
401
|
+
this.disposed = true;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function createHistoryRecorder(opts = {}) {
|
|
405
|
+
return new HistoryRecorderImpl(opts);
|
|
406
|
+
}
|
|
407
|
+
async function verifyChain(entries) {
|
|
408
|
+
if (entries.length === 0) {
|
|
409
|
+
return { verified: true, entriesChecked: 0, firstBreakAtSequence: null, reason: null };
|
|
410
|
+
}
|
|
411
|
+
let expectedPrev = null;
|
|
412
|
+
let checked = 0;
|
|
413
|
+
for (let i = 0; i < entries.length; i++) {
|
|
414
|
+
const entry = entries[i];
|
|
415
|
+
if (i === 0 && entry.sequence === 0 && entry.prevHash !== GENESIS_PREV_HASH) {
|
|
416
|
+
return {
|
|
417
|
+
verified: false,
|
|
418
|
+
entriesChecked: checked,
|
|
419
|
+
firstBreakAtSequence: entry.sequence,
|
|
420
|
+
reason: `genesis entry must have prevHash="${GENESIS_PREV_HASH}", got ${entry.prevHash}`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (i > 0 && expectedPrev !== null && entry.prevHash !== expectedPrev) {
|
|
424
|
+
return {
|
|
425
|
+
verified: false,
|
|
426
|
+
entriesChecked: checked,
|
|
427
|
+
firstBreakAtSequence: entry.sequence,
|
|
428
|
+
reason: `prevHash mismatch at sequence ${entry.sequence}: expected ${expectedPrev}, got ${entry.prevHash}`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const recomputedHash = await computeEntryHash({
|
|
432
|
+
sequence: entry.sequence,
|
|
433
|
+
evaluatedAt: entry.evaluatedAt,
|
|
434
|
+
score: entry.score,
|
|
435
|
+
signalsDigest: entry.signalsDigest,
|
|
436
|
+
prevHash: entry.prevHash
|
|
437
|
+
});
|
|
438
|
+
if (entry.entryHash && recomputedHash && recomputedHash !== entry.entryHash) {
|
|
439
|
+
return {
|
|
440
|
+
verified: false,
|
|
441
|
+
entriesChecked: checked,
|
|
442
|
+
firstBreakAtSequence: entry.sequence,
|
|
443
|
+
reason: `entryHash mismatch at sequence ${entry.sequence}: stored=${entry.entryHash} recomputed=${recomputedHash}`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
expectedPrev = entry.entryHash || null;
|
|
447
|
+
checked += 1;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
verified: true,
|
|
451
|
+
entriesChecked: checked,
|
|
452
|
+
firstBreakAtSequence: null,
|
|
453
|
+
reason: null
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/health/otel.ts
|
|
458
|
+
var SDK_NAME = "@sylphx/sdk/health";
|
|
459
|
+
var SDK_VERSION = "0.11.0";
|
|
460
|
+
var METRIC_NAMES = {
|
|
461
|
+
score: "health.score",
|
|
462
|
+
signalFactor: "health.signal.factor",
|
|
463
|
+
signalValue: "health.signal.value",
|
|
464
|
+
signalValueState: "health.signal.value_state",
|
|
465
|
+
/** ADR-143 §3.3 — increments per evaluation when an upstream cause is observed. */
|
|
466
|
+
downstreamCause: "health.downstream_cause"
|
|
467
|
+
};
|
|
468
|
+
var SPAN_ATTRIBUTES = {
|
|
469
|
+
score: "service.health.score",
|
|
470
|
+
signalFactor: (name) => `service.health.signal.${name}.factor`,
|
|
471
|
+
/** ADR-143 §3.2 — the upstream service that originated the failure chain. */
|
|
472
|
+
causeRoot: "service.health.cause_root",
|
|
473
|
+
/** ADR-143 §3.2 — the signal at the root that failed. */
|
|
474
|
+
causeSignal: "service.health.cause_signal",
|
|
475
|
+
/** ADR-143 §3.2 — comma-delimited hop chain (oldest to newest). */
|
|
476
|
+
causeChain: "service.health.cause_chain"
|
|
477
|
+
};
|
|
478
|
+
var EVENT_NAMES = {
|
|
479
|
+
evaluated: "health.evaluated"
|
|
480
|
+
};
|
|
481
|
+
function createOtelEmitter(opts = {}) {
|
|
482
|
+
return new OtelEmitterImpl(opts);
|
|
483
|
+
}
|
|
484
|
+
var OtelEmitterImpl = class {
|
|
485
|
+
constructor(opts) {
|
|
486
|
+
this.opts = opts;
|
|
487
|
+
this.staticAttrs = opts.attributes ?? {};
|
|
488
|
+
this.events = opts.events === true;
|
|
489
|
+
if (opts.meter) {
|
|
490
|
+
this.instruments = buildInstruments(opts.meter);
|
|
491
|
+
this._state = "ready";
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
void this.initialize();
|
|
495
|
+
}
|
|
496
|
+
_state = "loading";
|
|
497
|
+
instruments = null;
|
|
498
|
+
traceApi = null;
|
|
499
|
+
staticAttrs;
|
|
500
|
+
events;
|
|
501
|
+
get state() {
|
|
502
|
+
return this._state;
|
|
503
|
+
}
|
|
504
|
+
async initialize() {
|
|
505
|
+
const api = await tryLoadOtelApi();
|
|
506
|
+
if (api === null) {
|
|
507
|
+
this._state = "unavailable";
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (this._state === "disposed") return;
|
|
511
|
+
try {
|
|
512
|
+
const meter = api.metrics.getMeter(SDK_NAME, SDK_VERSION);
|
|
513
|
+
this.instruments = buildInstruments(meter);
|
|
514
|
+
this.traceApi = api.trace;
|
|
515
|
+
this._state = "ready";
|
|
516
|
+
} catch {
|
|
517
|
+
this._state = "unavailable";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
emit(score, upstreamCause) {
|
|
521
|
+
if (this._state !== "ready" || !this.instruments) return;
|
|
522
|
+
const baseAttrs = { ...this.staticAttrs };
|
|
523
|
+
try {
|
|
524
|
+
this.instruments.scoreGauge.record(score.score, baseAttrs);
|
|
525
|
+
for (const [signalName, rawValue] of Object.entries(score.signals)) {
|
|
526
|
+
const signalAttrs = { ...baseAttrs, "signal.name": signalName };
|
|
527
|
+
const factor = score.signalFactors?.[signalName];
|
|
528
|
+
if (typeof factor === "number" && Number.isFinite(factor)) {
|
|
529
|
+
this.instruments.factorGauge.record(factor, signalAttrs);
|
|
530
|
+
}
|
|
531
|
+
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
|
532
|
+
this.instruments.valueGauge.record(rawValue, signalAttrs);
|
|
533
|
+
} else {
|
|
534
|
+
const stateAttrs = {
|
|
535
|
+
...signalAttrs,
|
|
536
|
+
state: String(rawValue)
|
|
537
|
+
};
|
|
538
|
+
this.instruments.valueStateCounter.add(1, stateAttrs);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (upstreamCause && upstreamCause.causes.length > 0) {
|
|
542
|
+
for (const cause of upstreamCause.causes) {
|
|
543
|
+
this.instruments.downstreamCauseCounter.add(1, {
|
|
544
|
+
...baseAttrs,
|
|
545
|
+
cause_root: cause.service,
|
|
546
|
+
cause_signal: cause.signal
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (this.traceApi) {
|
|
551
|
+
const span = this.traceApi.getActiveSpan?.();
|
|
552
|
+
maybeAttachToSpan(span, score, this.events, upstreamCause);
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
dispose() {
|
|
558
|
+
this._state = "disposed";
|
|
559
|
+
this.instruments = null;
|
|
560
|
+
this.traceApi = null;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
var cachedApi2;
|
|
564
|
+
async function tryLoadOtelApi() {
|
|
565
|
+
if (cachedApi2 !== void 0) return cachedApi2;
|
|
566
|
+
try {
|
|
567
|
+
cachedApi2 = await import("@opentelemetry/api");
|
|
568
|
+
} catch {
|
|
569
|
+
cachedApi2 = null;
|
|
570
|
+
}
|
|
571
|
+
return cachedApi2;
|
|
572
|
+
}
|
|
573
|
+
function buildInstruments(meter) {
|
|
574
|
+
return {
|
|
575
|
+
scoreGauge: meter.createGauge(METRIC_NAMES.score, {
|
|
576
|
+
description: "Multi-signal health score per ADR-141 \xA74. Range [0, 1]. Higher = healthier.",
|
|
577
|
+
unit: "1"
|
|
578
|
+
}),
|
|
579
|
+
factorGauge: meter.createGauge(METRIC_NAMES.signalFactor, {
|
|
580
|
+
description: "Per-signal health factor; folded into health.score by the SDK scoring strategy (default: weightedProduct, ADR-111 \xA74.3).",
|
|
581
|
+
unit: "1"
|
|
582
|
+
}),
|
|
583
|
+
valueGauge: meter.createGauge(METRIC_NAMES.signalValue, {
|
|
584
|
+
description: "Raw observation behind a health signal (ms, count, rate). Numeric values only; string-valued signals emit health.signal.value_state instead."
|
|
585
|
+
}),
|
|
586
|
+
valueStateCounter: meter.createCounter(METRIC_NAMES.signalValueState, {
|
|
587
|
+
description: 'Per-evaluation counter for non-numeric signal readings (e.g. "ok" / "timeout"). state attribute carries the string label.'
|
|
588
|
+
}),
|
|
589
|
+
downstreamCauseCounter: meter.createCounter(METRIC_NAMES.downstreamCause, {
|
|
590
|
+
description: "Per-evaluation counter incremented when this service observes an upstream cause via OTel baggage (ADR-143). Operators read rate() to size the blast radius of an incident."
|
|
591
|
+
})
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function maybeAttachToSpan(span, score, events, upstreamCause) {
|
|
595
|
+
if (!span || typeof span !== "object") return;
|
|
596
|
+
const setAttribute = span.setAttribute;
|
|
597
|
+
if (typeof setAttribute === "function") {
|
|
598
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.score, score.score);
|
|
599
|
+
for (const [signalName, factor] of Object.entries(score.signalFactors ?? {})) {
|
|
600
|
+
if (typeof factor === "number" && Number.isFinite(factor)) {
|
|
601
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.signalFactor(signalName), factor);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (upstreamCause && upstreamCause.causes.length > 0) {
|
|
605
|
+
const primary = upstreamCause.causes[0];
|
|
606
|
+
if (primary) {
|
|
607
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeRoot, primary.service);
|
|
608
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeSignal, primary.signal);
|
|
609
|
+
}
|
|
610
|
+
if (upstreamCause.chain.length > 0) {
|
|
611
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeChain, upstreamCause.chain.join(","));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (events) {
|
|
616
|
+
const addEvent = span.addEvent;
|
|
617
|
+
if (typeof addEvent === "function") {
|
|
618
|
+
addEvent.call(span, EVENT_NAMES.evaluated, {
|
|
619
|
+
[SPAN_ATTRIBUTES.score]: score.score,
|
|
620
|
+
"service.health.lastTickAt": score.lastTickAt
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
73
626
|
// src/health/scoring.ts
|
|
74
627
|
function clamp01(x) {
|
|
75
628
|
if (!Number.isFinite(x)) return 0;
|
|
@@ -195,6 +748,167 @@ function startUnixSocketServer(source, opts = {}) {
|
|
|
195
748
|
return handle;
|
|
196
749
|
}
|
|
197
750
|
|
|
751
|
+
// src/health/middleware.ts
|
|
752
|
+
var DEFAULT_PATH = "/healthz";
|
|
753
|
+
var DEFAULT_SOCKET_PATH = "/var/run/sylphx/health.sock";
|
|
754
|
+
var AUTO_SOCKET_HANDLES = /* @__PURE__ */ new WeakMap();
|
|
755
|
+
function resolveInstance(opts) {
|
|
756
|
+
if (opts.instance) return opts.instance;
|
|
757
|
+
const { path: _path, instance: _instance, unixSocket: _unixSocket, ...sylphxOpts } = opts;
|
|
758
|
+
return sylphxHealth(sylphxOpts);
|
|
759
|
+
}
|
|
760
|
+
function normalizePath(input) {
|
|
761
|
+
if (!input) return DEFAULT_PATH;
|
|
762
|
+
return input.startsWith("/") ? input : `/${input}`;
|
|
763
|
+
}
|
|
764
|
+
function getEnvSocketPath() {
|
|
765
|
+
const proc = globalThis.process;
|
|
766
|
+
const value = proc?.env?.SYLPHX_HEALTH_APP_SOCKET;
|
|
767
|
+
return value && value.trim().length > 0 ? value.trim() : void 0;
|
|
768
|
+
}
|
|
769
|
+
function resolveSocketPath(explicit, envPath) {
|
|
770
|
+
if (explicit) return explicit;
|
|
771
|
+
if (envPath) return envPath;
|
|
772
|
+
return DEFAULT_SOCKET_PATH;
|
|
773
|
+
}
|
|
774
|
+
function resolveUnixSocketOption(input) {
|
|
775
|
+
if (input === false) return null;
|
|
776
|
+
const envPath = getEnvSocketPath();
|
|
777
|
+
if (input === void 0) {
|
|
778
|
+
return envPath ? { path: envPath, required: false } : null;
|
|
779
|
+
}
|
|
780
|
+
if (input === true) {
|
|
781
|
+
return { path: envPath ?? DEFAULT_SOCKET_PATH, required: false };
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
path: resolveSocketPath(input.path, envPath),
|
|
785
|
+
required: input.required ?? false,
|
|
786
|
+
...input.unlinkOnShutdown !== void 0 ? { unlinkOnShutdown: input.unlinkOnShutdown } : {}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function bindUnixSocketIfRequested(health, opts) {
|
|
790
|
+
const socket = resolveUnixSocketOption(opts.unixSocket);
|
|
791
|
+
if (!socket) return () => void 0;
|
|
792
|
+
const existing = AUTO_SOCKET_HANDLES.get(health);
|
|
793
|
+
if (existing) return () => void 0;
|
|
794
|
+
try {
|
|
795
|
+
const handle = health.serveUnixSocket({
|
|
796
|
+
path: socket.path,
|
|
797
|
+
...socket.unlinkOnShutdown !== void 0 ? { unlinkOnShutdown: socket.unlinkOnShutdown } : {}
|
|
798
|
+
});
|
|
799
|
+
AUTO_SOCKET_HANDLES.set(health, handle);
|
|
800
|
+
return () => {
|
|
801
|
+
if (AUTO_SOCKET_HANDLES.get(health) === handle) {
|
|
802
|
+
AUTO_SOCKET_HANDLES.delete(health);
|
|
803
|
+
}
|
|
804
|
+
void handle.shutdown();
|
|
805
|
+
};
|
|
806
|
+
} catch (err) {
|
|
807
|
+
if (socket.required) throw err;
|
|
808
|
+
return () => void 0;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function honoHelper(opts = {}) {
|
|
812
|
+
const path = normalizePath(opts.path);
|
|
813
|
+
const health = resolveInstance(opts);
|
|
814
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
815
|
+
const respond = health.handler();
|
|
816
|
+
const middleware = (async (c, next) => {
|
|
817
|
+
if (c.req.path === path) return respond();
|
|
818
|
+
await health.runWithCause(() => next());
|
|
819
|
+
});
|
|
820
|
+
Object.defineProperty(middleware, "health", { value: health, enumerable: true });
|
|
821
|
+
Object.defineProperty(middleware, "dispose", {
|
|
822
|
+
value: () => {
|
|
823
|
+
disposeSocket();
|
|
824
|
+
health.dispose();
|
|
825
|
+
},
|
|
826
|
+
enumerable: false
|
|
827
|
+
});
|
|
828
|
+
return middleware;
|
|
829
|
+
}
|
|
830
|
+
function pathOf(url) {
|
|
831
|
+
if (!url) return "/";
|
|
832
|
+
const q = url.indexOf("?");
|
|
833
|
+
return q === -1 ? url : url.slice(0, q);
|
|
834
|
+
}
|
|
835
|
+
function expressHelper(opts = {}) {
|
|
836
|
+
const path = normalizePath(opts.path);
|
|
837
|
+
const health = resolveInstance(opts);
|
|
838
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
839
|
+
const respond = health.nodeHandler();
|
|
840
|
+
const middleware = ((req, res, next) => {
|
|
841
|
+
if (pathOf(req.url) === path) {
|
|
842
|
+
void respond(req, res);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
void health.runWithCause(async () => next());
|
|
846
|
+
});
|
|
847
|
+
Object.defineProperty(middleware, "health", { value: health, enumerable: true });
|
|
848
|
+
Object.defineProperty(middleware, "dispose", {
|
|
849
|
+
value: () => {
|
|
850
|
+
disposeSocket();
|
|
851
|
+
health.dispose();
|
|
852
|
+
},
|
|
853
|
+
enumerable: false
|
|
854
|
+
});
|
|
855
|
+
return middleware;
|
|
856
|
+
}
|
|
857
|
+
function fastifyHelper(opts = {}) {
|
|
858
|
+
const path = normalizePath(opts.path);
|
|
859
|
+
const health = resolveInstance(opts);
|
|
860
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
861
|
+
const respond = health.handler();
|
|
862
|
+
const plugin = (async (instance) => {
|
|
863
|
+
instance.get(path, async (_req, reply) => {
|
|
864
|
+
const response = await respond();
|
|
865
|
+
const body = await response.text();
|
|
866
|
+
reply.code(response.status);
|
|
867
|
+
reply.header("Content-Type", "application/json");
|
|
868
|
+
reply.header("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
869
|
+
reply.send(body);
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
Object.defineProperty(plugin, "health", { value: health, enumerable: true });
|
|
873
|
+
Object.defineProperty(plugin, "dispose", {
|
|
874
|
+
value: () => {
|
|
875
|
+
disposeSocket();
|
|
876
|
+
health.dispose();
|
|
877
|
+
},
|
|
878
|
+
enumerable: false
|
|
879
|
+
});
|
|
880
|
+
return plugin;
|
|
881
|
+
}
|
|
882
|
+
function fetchHelper(opts = {}) {
|
|
883
|
+
const health = resolveInstance(opts);
|
|
884
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
885
|
+
const respond = health.handler();
|
|
886
|
+
const handler = ((req) => respond(req));
|
|
887
|
+
Object.defineProperty(handler, "health", { value: health, enumerable: true });
|
|
888
|
+
Object.defineProperty(handler, "dispose", {
|
|
889
|
+
value: () => {
|
|
890
|
+
disposeSocket();
|
|
891
|
+
health.dispose();
|
|
892
|
+
},
|
|
893
|
+
enumerable: false
|
|
894
|
+
});
|
|
895
|
+
return handler;
|
|
896
|
+
}
|
|
897
|
+
var withHealth = {
|
|
898
|
+
/** Hono middleware. Mount with `app.use('*', withHealth.hono())`. */
|
|
899
|
+
hono: honoHelper,
|
|
900
|
+
/** Express / Connect / Polka middleware. Mount with `app.use(withHealth.express())`. */
|
|
901
|
+
express: expressHelper,
|
|
902
|
+
/** Fastify plugin. Register with `await fastify.register(withHealth.fastify())`. */
|
|
903
|
+
fastify: fastifyHelper,
|
|
904
|
+
/**
|
|
905
|
+
* Pure Web Fetch handler. Works with Bun.serve, Next.js route
|
|
906
|
+
* handlers, Deno, Cloudflare Workers, and any other `Request →
|
|
907
|
+
* Response` runtime.
|
|
908
|
+
*/
|
|
909
|
+
fetch: fetchHelper
|
|
910
|
+
};
|
|
911
|
+
|
|
198
912
|
// src/health/signals/error-rate.ts
|
|
199
913
|
function parseWindow(w) {
|
|
200
914
|
if (typeof w === "number") {
|
|
@@ -362,6 +1076,60 @@ function memoryPressureSignal(opts = {}) {
|
|
|
362
1076
|
};
|
|
363
1077
|
}
|
|
364
1078
|
|
|
1079
|
+
// src/health/signals/ping.ts
|
|
1080
|
+
var DEFAULT_TIMEOUT_MS = 500;
|
|
1081
|
+
function pingSignal(opts) {
|
|
1082
|
+
if (typeof opts.ping !== "function") {
|
|
1083
|
+
throw new Error(`pingSignal[${opts.name}]: ping must be a function`);
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
1086
|
+
throw new Error("pingSignal: name must be a non-empty string");
|
|
1087
|
+
}
|
|
1088
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1089
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1090
|
+
throw new Error(`pingSignal[${opts.name}]: timeoutMs must be > 0, got ${timeoutMs}`);
|
|
1091
|
+
}
|
|
1092
|
+
const weight = opts.weight ?? 1;
|
|
1093
|
+
return {
|
|
1094
|
+
name: opts.name,
|
|
1095
|
+
weight,
|
|
1096
|
+
async read() {
|
|
1097
|
+
let timer;
|
|
1098
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1099
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1100
|
+
});
|
|
1101
|
+
try {
|
|
1102
|
+
const result = await Promise.race([opts.ping().then(() => "ok"), timeoutPromise]);
|
|
1103
|
+
if (result === "timeout") {
|
|
1104
|
+
return { value: "timeout", healthFactor: 0 };
|
|
1105
|
+
}
|
|
1106
|
+
return { value: "ok", healthFactor: 1 };
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1109
|
+
return { value: message.slice(0, 120), healthFactor: 0 };
|
|
1110
|
+
} finally {
|
|
1111
|
+
if (timer) clearTimeout(timer);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function databaseSignal(opts) {
|
|
1117
|
+
return pingSignal({
|
|
1118
|
+
name: opts.name ?? "database",
|
|
1119
|
+
ping: opts.ping,
|
|
1120
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
1121
|
+
...opts.weight !== void 0 ? { weight: opts.weight } : {}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function redisSignal(opts) {
|
|
1125
|
+
return pingSignal({
|
|
1126
|
+
name: opts.name ?? "redis",
|
|
1127
|
+
ping: opts.ping,
|
|
1128
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
1129
|
+
...opts.weight !== void 0 ? { weight: opts.weight } : {}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
365
1133
|
// src/health/signals/queue-depth.ts
|
|
366
1134
|
function queueDepthSignal(opts) {
|
|
367
1135
|
if (typeof opts.getter !== "function") {
|
|
@@ -407,15 +1175,41 @@ function queueDepthSignal(opts) {
|
|
|
407
1175
|
}
|
|
408
1176
|
|
|
409
1177
|
// src/health/index.ts
|
|
1178
|
+
function resolveOtelEmitter(otelOpt) {
|
|
1179
|
+
if (otelOpt === false) return null;
|
|
1180
|
+
const emitterOpts = otelOpt && typeof otelOpt === "object" ? otelOpt : {};
|
|
1181
|
+
return createOtelEmitter(emitterOpts);
|
|
1182
|
+
}
|
|
1183
|
+
function resolveCausalityHandle(opt) {
|
|
1184
|
+
if (opt === false) return null;
|
|
1185
|
+
const causalityOpts = opt && typeof opt === "object" ? opt : {};
|
|
1186
|
+
return createCausalityHandle(causalityOpts);
|
|
1187
|
+
}
|
|
1188
|
+
function resolveHistoryRecorder(opt) {
|
|
1189
|
+
if (opt === false) return null;
|
|
1190
|
+
const recorderOpts = opt && typeof opt === "object" ? opt : {};
|
|
1191
|
+
return createHistoryRecorder(recorderOpts);
|
|
1192
|
+
}
|
|
1193
|
+
function clampHealthFactor(value) {
|
|
1194
|
+
if (!Number.isFinite(value)) return 0;
|
|
1195
|
+
if (value < 0) return 0;
|
|
1196
|
+
if (value > 1) return 1;
|
|
1197
|
+
return value;
|
|
1198
|
+
}
|
|
410
1199
|
function sylphxHealth(opts = {}) {
|
|
411
1200
|
const signals = opts.signals && opts.signals.length > 0 ? [...opts.signals] : [eventLoopLagSignal({ degradedMs: 5e3, deadMs: 3e4, weight: 1 })];
|
|
412
1201
|
const scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy();
|
|
413
1202
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1203
|
+
const otelEmitter = resolveOtelEmitter(opts.otel);
|
|
1204
|
+
const causality = resolveCausalityHandle(opts.causality);
|
|
1205
|
+
const historyRecorder = resolveHistoryRecorder(opts.history);
|
|
414
1206
|
let disposed = false;
|
|
1207
|
+
let lastScore = null;
|
|
415
1208
|
const evaluate = async () => {
|
|
416
1209
|
if (disposed) {
|
|
417
1210
|
throw new Error("sylphxHealth: instance disposed");
|
|
418
1211
|
}
|
|
1212
|
+
const upstreamCause = causality ? causality.readCause() : null;
|
|
419
1213
|
const readings = await Promise.all(
|
|
420
1214
|
signals.map(async (signal) => ({
|
|
421
1215
|
signal,
|
|
@@ -424,17 +1218,47 @@ function sylphxHealth(opts = {}) {
|
|
|
424
1218
|
);
|
|
425
1219
|
const score = scoringStrategy(readings);
|
|
426
1220
|
const signalsMap = {};
|
|
1221
|
+
const signalFactors = {};
|
|
427
1222
|
for (const { signal, reading } of readings) {
|
|
428
1223
|
signalsMap[signal.name] = reading.value;
|
|
1224
|
+
signalFactors[signal.name] = reading.unknown === true ? 1 : clampHealthFactor(reading.healthFactor);
|
|
429
1225
|
}
|
|
430
|
-
|
|
1226
|
+
const result = {
|
|
431
1227
|
score,
|
|
432
1228
|
signals: signalsMap,
|
|
1229
|
+
signalFactors,
|
|
433
1230
|
lastTickAt: now().toISOString()
|
|
434
1231
|
};
|
|
1232
|
+
lastScore = result;
|
|
1233
|
+
if (otelEmitter) {
|
|
1234
|
+
otelEmitter.emit(result, upstreamCause);
|
|
1235
|
+
}
|
|
1236
|
+
if (historyRecorder) {
|
|
1237
|
+
void historyRecorder.record(result);
|
|
1238
|
+
}
|
|
1239
|
+
return result;
|
|
435
1240
|
};
|
|
436
1241
|
const evaluator = { evaluate };
|
|
437
1242
|
const evaluateEffect2 = evaluateEffect(evaluate);
|
|
1243
|
+
const runWithCause = async (fn) => {
|
|
1244
|
+
if (!causality || !lastScore) return await fn();
|
|
1245
|
+
return await causality.runWithCause(fn, lastScore);
|
|
1246
|
+
};
|
|
1247
|
+
const getHistory = async (query = {}) => {
|
|
1248
|
+
if (!historyRecorder) return [];
|
|
1249
|
+
return await historyRecorder.range(query);
|
|
1250
|
+
};
|
|
1251
|
+
const verifyHistory = async (query = {}) => {
|
|
1252
|
+
if (!historyRecorder) {
|
|
1253
|
+
return {
|
|
1254
|
+
verified: false,
|
|
1255
|
+
entriesChecked: 0,
|
|
1256
|
+
firstBreakAtSequence: null,
|
|
1257
|
+
reason: "history recording is disabled on this instance"
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return await historyRecorder.verify(query);
|
|
1261
|
+
};
|
|
438
1262
|
return {
|
|
439
1263
|
signals,
|
|
440
1264
|
evaluate,
|
|
@@ -448,9 +1272,18 @@ function sylphxHealth(opts = {}) {
|
|
|
448
1272
|
serveUnixSocket(unixOpts) {
|
|
449
1273
|
return startUnixSocketServer(evaluator, unixOpts);
|
|
450
1274
|
},
|
|
1275
|
+
getLastScore() {
|
|
1276
|
+
return lastScore;
|
|
1277
|
+
},
|
|
1278
|
+
runWithCause,
|
|
1279
|
+
getHistory,
|
|
1280
|
+
verifyHistory,
|
|
451
1281
|
dispose() {
|
|
452
1282
|
if (disposed) return;
|
|
453
1283
|
disposed = true;
|
|
1284
|
+
if (otelEmitter) otelEmitter.dispose();
|
|
1285
|
+
if (causality) causality.dispose();
|
|
1286
|
+
if (historyRecorder) historyRecorder.dispose();
|
|
454
1287
|
for (const s of signals) {
|
|
455
1288
|
try {
|
|
456
1289
|
s.dispose?.();
|
|
@@ -461,15 +1294,34 @@ function sylphxHealth(opts = {}) {
|
|
|
461
1294
|
};
|
|
462
1295
|
}
|
|
463
1296
|
export {
|
|
1297
|
+
BAGGAGE_KEY_CAUSE,
|
|
1298
|
+
BAGGAGE_KEY_CHAIN,
|
|
1299
|
+
DEFAULT_CAUSE_THRESHOLD,
|
|
1300
|
+
DEFAULT_HISTORY_CAPACITY,
|
|
1301
|
+
GENESIS_PREV_HASH,
|
|
464
1302
|
HealthError,
|
|
1303
|
+
MAX_CHAIN_LENGTH,
|
|
1304
|
+
EVENT_NAMES as OTEL_EVENT_NAMES,
|
|
1305
|
+
METRIC_NAMES as OTEL_METRIC_NAMES,
|
|
1306
|
+
SPAN_ATTRIBUTES as OTEL_SPAN_ATTRIBUTES,
|
|
1307
|
+
canonicalizeSignals,
|
|
1308
|
+
createCausalityHandle,
|
|
1309
|
+
createHistoryRecorder,
|
|
1310
|
+
createInMemoryHistoryStore,
|
|
465
1311
|
createNodeHandler,
|
|
1312
|
+
createOtelEmitter,
|
|
466
1313
|
createWebHandler,
|
|
1314
|
+
databaseSignal,
|
|
467
1315
|
defaultScoringStrategy,
|
|
468
1316
|
errorRateSignal,
|
|
469
1317
|
eventLoopLagSignal,
|
|
470
1318
|
memoryPressureSignal,
|
|
1319
|
+
pingSignal,
|
|
471
1320
|
queueDepthSignal,
|
|
1321
|
+
redisSignal,
|
|
472
1322
|
sylphxHealth,
|
|
473
|
-
|
|
1323
|
+
verifyChain,
|
|
1324
|
+
weightedProduct,
|
|
1325
|
+
withHealth
|
|
474
1326
|
};
|
|
475
1327
|
//# sourceMappingURL=index.mjs.map
|