@capxul/observability 4.20.0-beta.23 → 4.20.0-beta.26
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 +79 -0
- package/dist/engineering.d.mts +22 -16
- package/dist/engineering.mjs +315 -258
- package/dist/index.d.mts +24798 -3392
- package/dist/index.mjs +2 -2
- package/dist/src-CX-Shigb.mjs +9526 -0
- package/package.json +12 -9
- package/dist/src-CCuVZt-H.mjs +0 -3463
package/dist/engineering.mjs
CHANGED
|
@@ -1,8 +1,190 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { I as CLI_CAPABILITIES, L as CLI_COMMAND_NAMES, R as CLI_INVOCATION_KINDS, dn as redactSecrets, fn as redactUrlSecrets, hn as FAILURE_MODES, ln as ORGANIZATION_SETUP_REASONS, mn as CAPXUL_ERROR_CODES, pn as isCredentialField, sn as isCapxulOperation, un as ORGANIZATION_SETUP_STATUSES } from "./src-CX-Shigb.mjs";
|
|
2
2
|
import { Cause, Clock, Context, Deferred, Duration, Effect, Exit, FiberSet, Layer, LogLevel, Logger, Metric, Option, References, Schedule, Stream, Tracer } from "effect";
|
|
3
|
-
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/
|
|
4
|
-
import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/
|
|
5
|
-
//#region src/
|
|
3
|
+
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
|
|
4
|
+
import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/observability";
|
|
5
|
+
//#region ../protocol/src/primitives/record.ts
|
|
6
|
+
/** A non-null object that is not an array: a value whose fields can be read by name. */
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
/** The field `key` of `value` when `value` is a record, else undefined. */
|
|
11
|
+
function readField(value, key) {
|
|
12
|
+
return isRecord(value) ? value[key] : void 0;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/delivery/admission.ts
|
|
16
|
+
function newDeliveryState(budget, ownsMetrics) {
|
|
17
|
+
return {
|
|
18
|
+
budget,
|
|
19
|
+
ownsMetrics,
|
|
20
|
+
closed: Deferred.makeUnsafe(),
|
|
21
|
+
isClosed: false,
|
|
22
|
+
metricsRunning: false,
|
|
23
|
+
logs: [],
|
|
24
|
+
spans: /* @__PURE__ */ new Map(),
|
|
25
|
+
terminalBodies: /* @__PURE__ */ new WeakSet(),
|
|
26
|
+
bodyLeases: /* @__PURE__ */ new WeakMap(),
|
|
27
|
+
pendingBodies: /* @__PURE__ */ new Set()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Hold the records a body carries until its request finishes or the body is discarded. */
|
|
31
|
+
function holdBody(state, body, releases) {
|
|
32
|
+
if (releases.length === 0) return;
|
|
33
|
+
const lease = {
|
|
34
|
+
started: false,
|
|
35
|
+
release: () => {
|
|
36
|
+
if (!state.pendingBodies.delete(lease)) return;
|
|
37
|
+
state.bodyLeases.delete(body);
|
|
38
|
+
for (const release of releases) release();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
state.bodyLeases.set(body, lease);
|
|
42
|
+
state.pendingBodies.add(lease);
|
|
43
|
+
}
|
|
44
|
+
const discardBody = (state, body) => state.bodyLeases.get(body)?.release();
|
|
45
|
+
const drainLogs = (state) => {
|
|
46
|
+
for (const release of state.logs.splice(0)) release();
|
|
47
|
+
};
|
|
48
|
+
function closeDelivery(state) {
|
|
49
|
+
state.isClosed = true;
|
|
50
|
+
Deferred.doneUnsafe(state.closed, Effect.void);
|
|
51
|
+
for (const lease of state.pendingBodies) if (!lease.started) lease.release();
|
|
52
|
+
drainLogs(state);
|
|
53
|
+
for (const release of state.spans.values()) release();
|
|
54
|
+
state.spans.clear();
|
|
55
|
+
if (state.ownsMetrics) state.budget.metricRegistry.clear();
|
|
56
|
+
}
|
|
57
|
+
const text = (value) => typeof value === "string" ? value.slice(0, 128) : "capxul.engineering";
|
|
58
|
+
const attributes = (input, limit = 32) => {
|
|
59
|
+
const out = {};
|
|
60
|
+
let count = 0;
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
try {
|
|
63
|
+
for (const key in input) {
|
|
64
|
+
if (count++ >= limit) break;
|
|
65
|
+
if (key.length > 64) continue;
|
|
66
|
+
const value = Object.getOwnPropertyDescriptor(input, key)?.value;
|
|
67
|
+
const scalar = typeof value === "string" ? value.slice(0, 128) : typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
68
|
+
if (scalar === void 0) continue;
|
|
69
|
+
bytes += new TextEncoder().encode(JSON.stringify([key, scalar])).length;
|
|
70
|
+
if (bytes > (limit >= 24 ? 2e3 : 300)) break;
|
|
71
|
+
out[key] = scalar;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
const terminalName = (name) => name === "cli.command.completed" || name === "cli.wallet.step" || CLI_COMMAND_NAMES.some((command) => name === `cli.command.${command}`);
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/delivery/records.ts
|
|
79
|
+
function admittedLogger(state, delegate) {
|
|
80
|
+
return Logger.make((options) => {
|
|
81
|
+
if (state.isClosed) return;
|
|
82
|
+
let release;
|
|
83
|
+
try {
|
|
84
|
+
const message = text(Array.isArray(options.message) ? Object.getOwnPropertyDescriptor(options.message, "0")?.value : options.message);
|
|
85
|
+
release = state.budget.admitRecord(terminalName(message));
|
|
86
|
+
if (release === void 0) return;
|
|
87
|
+
const context = Context.add(options.fiber.context, References.CurrentLogAnnotations, attributes(options.fiber.getRef(References.CurrentLogAnnotations)));
|
|
88
|
+
const fiber = Object.create(options.fiber);
|
|
89
|
+
Object.defineProperty(fiber, "getRef", { value: (ref) => Context.get(context, ref) });
|
|
90
|
+
state.logs.push(release);
|
|
91
|
+
delegate.log({
|
|
92
|
+
...options,
|
|
93
|
+
message,
|
|
94
|
+
cause: Cause.empty,
|
|
95
|
+
fiber
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
if (release !== void 0) {
|
|
99
|
+
const index = state.logs.indexOf(release);
|
|
100
|
+
if (index !== -1) state.logs.splice(index, 1);
|
|
101
|
+
release();
|
|
102
|
+
}
|
|
103
|
+
state.budget.drop();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function admittedTracer(state, delegate) {
|
|
108
|
+
return Tracer.make({
|
|
109
|
+
span(options) {
|
|
110
|
+
const name = text(options.name);
|
|
111
|
+
const release = state.isClosed ? void 0 : state.budget.admitRecord(terminalName(name));
|
|
112
|
+
const spanOptions = {
|
|
113
|
+
...options,
|
|
114
|
+
name,
|
|
115
|
+
annotations: Context.empty(),
|
|
116
|
+
links: []
|
|
117
|
+
};
|
|
118
|
+
const span = release === void 0 ? new Tracer.NativeSpan({
|
|
119
|
+
...spanOptions,
|
|
120
|
+
sampled: false
|
|
121
|
+
}) : delegate.span(spanOptions);
|
|
122
|
+
if (release !== void 0) state.spans.set(span.spanId, release);
|
|
123
|
+
const wrapped = boundedSpan(state, span);
|
|
124
|
+
wrapped.addLinks(options.links ?? []);
|
|
125
|
+
return wrapped;
|
|
126
|
+
},
|
|
127
|
+
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** The span as a producer sees it: at most 32 fields and 2000 attribute bytes, 2 events, 2 links. */
|
|
131
|
+
function boundedSpan(state, span) {
|
|
132
|
+
const { budget } = state;
|
|
133
|
+
const wrapped = Object.create(span);
|
|
134
|
+
let fields = 0, events = 0, links = 0, attributeBytes = 0;
|
|
135
|
+
let ended = false;
|
|
136
|
+
Object.defineProperties(wrapped, {
|
|
137
|
+
attribute: { value: (key, value) => {
|
|
138
|
+
if (fields >= 32 && !span.attributes.has(key)) {
|
|
139
|
+
budget.drop();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const safe = attributes({ [key]: value }, 1);
|
|
143
|
+
if (!Object.hasOwn(safe, key)) return;
|
|
144
|
+
const previous = span.attributes.has(key) ? new TextEncoder().encode(JSON.stringify([key, span.attributes.get(key)])).length : 0;
|
|
145
|
+
const next = new TextEncoder().encode(JSON.stringify([key, safe[key]])).length;
|
|
146
|
+
if (attributeBytes - previous + next > 2e3) {
|
|
147
|
+
budget.drop();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
attributeBytes += next - previous;
|
|
151
|
+
if (!span.attributes.has(key)) fields += 1;
|
|
152
|
+
span.attribute(key, safe[key]);
|
|
153
|
+
} },
|
|
154
|
+
event: { value: (eventName, time, attrs) => {
|
|
155
|
+
if (events++ >= 2) {
|
|
156
|
+
budget.drop();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
span.event(text(eventName), time, attributes(attrs ?? {}, 4));
|
|
160
|
+
} },
|
|
161
|
+
addLinks: { value: (items) => {
|
|
162
|
+
for (const item of items.slice(0, Math.max(0, 2 - links))) {
|
|
163
|
+
if (!/^[a-f0-9]{32}$/u.test(item.span.traceId) || !/^[a-f0-9]{16}$/u.test(item.span.spanId)) {
|
|
164
|
+
budget.drop();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
span.addLinks([{
|
|
168
|
+
span: Tracer.externalSpan({
|
|
169
|
+
traceId: item.span.traceId,
|
|
170
|
+
spanId: item.span.spanId,
|
|
171
|
+
sampled: item.span.sampled
|
|
172
|
+
}),
|
|
173
|
+
attributes: attributes(item.attributes, 4)
|
|
174
|
+
}]);
|
|
175
|
+
links += 1;
|
|
176
|
+
}
|
|
177
|
+
} },
|
|
178
|
+
end: { value: (time, exit) => {
|
|
179
|
+
if (ended) return;
|
|
180
|
+
ended = true;
|
|
181
|
+
span.end(time, Exit.isFailure(exit) ? Exit.fail("CLI operation failed") : Exit.void);
|
|
182
|
+
} }
|
|
183
|
+
});
|
|
184
|
+
return wrapped;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/delivery/budget.ts
|
|
6
188
|
/** Fixed admission limits for one CLI invocation and its SDK client. */
|
|
7
189
|
const CLI_ENGINEERING_LIMITS = Object.freeze({
|
|
8
190
|
records: 80,
|
|
@@ -52,9 +234,8 @@ var CliMetricRegistry = class extends Map {
|
|
|
52
234
|
return this;
|
|
53
235
|
}
|
|
54
236
|
const labels = Object.entries(entry.attributes ?? {});
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (!["Counter", "Histogram"].includes(entry.type) || (entry.description?.length ?? 0) > 128 || labels.length > 6 || labels.some(([name, value]) => !metricValues[name]?.includes(String(value))) || entry.type === "Histogram" && (!Array.isArray(buckets) || buckets.length > 32)) {
|
|
237
|
+
const buckets = readField(entry.hooks.get(Context.empty()), "buckets");
|
|
238
|
+
if (!["Counter", "Histogram"].includes(entry.type) || (entry.description?.length ?? 0) > 128 || labels.length > 6 || labels.some(([name, value]) => !metricValues[name]?.includes(value)) || entry.type === "Histogram" && (!Array.isArray(buckets) || buckets.length > 32)) {
|
|
58
239
|
this.dropped();
|
|
59
240
|
return this;
|
|
60
241
|
}
|
|
@@ -118,266 +299,123 @@ function makeCliEngineeringBudget() {
|
|
|
118
299
|
}
|
|
119
300
|
});
|
|
120
301
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
bytes += new TextEncoder().encode(JSON.stringify([key, scalar])).length;
|
|
134
|
-
if (bytes > (limit >= 24 ? 2e3 : 300)) break;
|
|
135
|
-
out[key] = scalar;
|
|
136
|
-
}
|
|
137
|
-
} catch {}
|
|
138
|
-
return out;
|
|
139
|
-
};
|
|
140
|
-
const terminalName = (name) => name === "cli.command.completed" || name === "cli.wallet.step" || CLI_COMMAND_NAMES.some((command) => name === `cli.command.${command}`);
|
|
141
|
-
/** Native exporters retain their serialization and scheduling; this owns admission only. */
|
|
142
|
-
function cliEngineeringDelivery(budget, ownsMetrics) {
|
|
143
|
-
const closed = Deferred.makeUnsafe();
|
|
144
|
-
let isClosed = false;
|
|
145
|
-
let metricsRunning = false;
|
|
146
|
-
const logs = [];
|
|
147
|
-
const spans = /* @__PURE__ */ new Map();
|
|
148
|
-
const terminalBodies = /* @__PURE__ */ new WeakSet();
|
|
149
|
-
const bodyLeases = /* @__PURE__ */ new WeakMap();
|
|
150
|
-
const pendingBodies = /* @__PURE__ */ new Set();
|
|
151
|
-
const holdBody = (body, releases) => {
|
|
152
|
-
if (releases.length === 0) return;
|
|
153
|
-
const lease = {
|
|
154
|
-
started: false,
|
|
155
|
-
release: () => {
|
|
156
|
-
if (!pendingBodies.delete(lease)) return;
|
|
157
|
-
bodyLeases.delete(body);
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/delivery/requests.ts
|
|
304
|
+
function admittedSerialization(state, delegate) {
|
|
305
|
+
return {
|
|
306
|
+
logs(data) {
|
|
307
|
+
const releases = state.logs.splice(0);
|
|
308
|
+
try {
|
|
309
|
+
const body = delegate.logs(data);
|
|
310
|
+
if (data.resourceLogs.some((r) => r.scopeLogs.some((s) => s.logRecords?.some((l) => terminalName(l.body?.stringValue ?? ""))))) state.terminalBodies.add(body);
|
|
311
|
+
holdBody(state, body, releases);
|
|
312
|
+
return body;
|
|
313
|
+
} catch (error) {
|
|
158
314
|
for (const release of releases) release();
|
|
315
|
+
throw error;
|
|
159
316
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
317
|
+
},
|
|
318
|
+
traces(data) {
|
|
319
|
+
const releases = [];
|
|
320
|
+
for (const resource of data.resourceSpans) for (const scope of resource.scopeSpans) for (const span of scope.spans) {
|
|
321
|
+
const release = state.spans.get(span.spanId);
|
|
322
|
+
if (release !== void 0) releases.push(release);
|
|
323
|
+
state.spans.delete(span.spanId);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const body = delegate.traces(data);
|
|
327
|
+
if (data.resourceSpans.some((r) => r.scopeSpans.some((s) => s.spans.some((span) => terminalName(span.name))))) state.terminalBodies.add(body);
|
|
328
|
+
holdBody(state, body, releases);
|
|
329
|
+
return body;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
for (const release of releases) release();
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
metrics(data) {
|
|
336
|
+
const body = delegate.metrics(data);
|
|
337
|
+
state.terminalBodies.add(body);
|
|
338
|
+
return body;
|
|
339
|
+
}
|
|
176
340
|
};
|
|
341
|
+
}
|
|
342
|
+
/** A 429's Retry-After, bounded to two seconds; any other failure retries at once. */
|
|
343
|
+
const retryAfter = Schedule.spaced("100 millis").pipe(Schedule.addDelay(({ input }) => Effect.map(Clock.currentTimeMillis, (now) => {
|
|
344
|
+
if (!HttpClientError.isHttpClientError(input) || input.reason._tag !== "StatusCodeError" || input.reason.response.status !== 429) return Duration.zero;
|
|
345
|
+
const header = input.reason.response.headers["retry-after"];
|
|
346
|
+
const numeric = Number(header);
|
|
347
|
+
const delay = header === void 0 ? 2e3 : Number.isFinite(numeric) ? numeric * 1e3 : Date.parse(header) - now;
|
|
348
|
+
return Duration.millis(Number.isFinite(delay) ? Math.min(2e3, Math.max(0, delay)) : 2e3);
|
|
349
|
+
})));
|
|
350
|
+
function admittedTransport(state, client, enabled = () => true) {
|
|
351
|
+
const { budget } = state;
|
|
352
|
+
const retrying = HttpClient.transform(client, (response, request) => Effect.suspend(() => {
|
|
353
|
+
if (enabled()) return response;
|
|
354
|
+
budget.drop();
|
|
355
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
356
|
+
})).pipe(HttpClient.transformResponse((response) => Effect.tap(response, (result) => {
|
|
357
|
+
let received = 0;
|
|
358
|
+
return Stream.runForEach(result.stream, (chunk) => Effect.sync(() => {
|
|
359
|
+
received += chunk.byteLength;
|
|
360
|
+
if (received > 65536) throw new Error("Engineering response limit exceeded");
|
|
361
|
+
})).pipe(Effect.catchReason("HttpClientError", "EmptyBodyError", () => Effect.void));
|
|
362
|
+
}))).pipe(HttpClient.filterStatusOk, HttpClient.retryTransient({
|
|
363
|
+
times: 1,
|
|
364
|
+
schedule: retryAfter
|
|
365
|
+
}));
|
|
366
|
+
return HttpClient.transform(retrying, (response, request) => Effect.suspend(() => {
|
|
367
|
+
if (state.isClosed) {
|
|
368
|
+
discardBody(state, request.body);
|
|
369
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
370
|
+
}
|
|
371
|
+
const droppedResponse = () => HttpClientResponse.fromWeb(request, new Response(null, { status: 200 }));
|
|
372
|
+
if (request.body._tag === "Empty") {
|
|
373
|
+
discardBody(state, request.body);
|
|
374
|
+
return Effect.succeed(droppedResponse());
|
|
375
|
+
}
|
|
376
|
+
const bytes = request.body.contentLength;
|
|
377
|
+
const release = budget.admitRequest(bytes ?? Number.POSITIVE_INFINITY, state.terminalBodies.has(request.body));
|
|
378
|
+
if (release === void 0) {
|
|
379
|
+
discardBody(state, request.body);
|
|
380
|
+
return Effect.succeed(droppedResponse());
|
|
381
|
+
}
|
|
382
|
+
const lease = state.bodyLeases.get(request.body);
|
|
383
|
+
if (lease !== void 0) lease.started = true;
|
|
384
|
+
return response.pipe(Effect.asVoid, Effect.timeoutOption(CLI_ENGINEERING_LIMITS.requestTimeoutMs), Effect.raceFirst(Deferred.await(state.closed).pipe(Effect.andThen(Effect.interrupt))), Effect.tap((result) => Effect.sync(() => {
|
|
385
|
+
if (Option.isNone(result)) budget.drop();
|
|
386
|
+
})), Effect.catchCause(() => Effect.sync(() => {
|
|
387
|
+
budget.drop();
|
|
388
|
+
})), Effect.as(droppedResponse()), Effect.ensuring(Effect.sync(() => {
|
|
389
|
+
release();
|
|
390
|
+
discardBody(state, request.body);
|
|
391
|
+
})));
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/engineering-delivery.ts
|
|
396
|
+
/** Native exporters retain their serialization and scheduling; this owns admission only. */
|
|
397
|
+
function cliEngineeringDelivery(budget, ownsMetrics) {
|
|
398
|
+
const state = newDeliveryState(budget, ownsMetrics);
|
|
177
399
|
return {
|
|
178
|
-
close,
|
|
179
|
-
discardBody,
|
|
400
|
+
close: () => closeDelivery(state),
|
|
401
|
+
discardBody: (body) => discardBody(state, body),
|
|
180
402
|
metrics(run) {
|
|
181
403
|
return Effect.suspend(() => {
|
|
182
|
-
if (isClosed || metricsRunning) return Effect.void;
|
|
183
|
-
metricsRunning = true;
|
|
404
|
+
if (state.isClosed || state.metricsRunning) return Effect.void;
|
|
405
|
+
state.metricsRunning = true;
|
|
184
406
|
return run.pipe(Effect.ensuring(Effect.sync(() => {
|
|
185
|
-
metricsRunning = false;
|
|
407
|
+
state.metricsRunning = false;
|
|
186
408
|
})));
|
|
187
409
|
});
|
|
188
410
|
},
|
|
189
|
-
logger(delegate)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const message = text(Array.isArray(options.message) ? Object.getOwnPropertyDescriptor(options.message, "0")?.value : options.message);
|
|
195
|
-
release = budget.admitRecord(terminalName(message));
|
|
196
|
-
if (release === void 0) return;
|
|
197
|
-
const context = Context.add(options.fiber.context, References.CurrentLogAnnotations, attributes(options.fiber.getRef(References.CurrentLogAnnotations)));
|
|
198
|
-
const fiber = Object.create(options.fiber);
|
|
199
|
-
Object.defineProperty(fiber, "getRef", { value: (ref) => Context.get(context, ref) });
|
|
200
|
-
logs.push(release);
|
|
201
|
-
delegate.log({
|
|
202
|
-
...options,
|
|
203
|
-
message,
|
|
204
|
-
cause: Cause.empty,
|
|
205
|
-
fiber
|
|
206
|
-
});
|
|
207
|
-
} catch {
|
|
208
|
-
if (release !== void 0) {
|
|
209
|
-
const index = logs.indexOf(release);
|
|
210
|
-
if (index !== -1) logs.splice(index, 1);
|
|
211
|
-
release();
|
|
212
|
-
}
|
|
213
|
-
budget.drop();
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
},
|
|
217
|
-
tracer(delegate) {
|
|
218
|
-
return Tracer.make({
|
|
219
|
-
span(options) {
|
|
220
|
-
const name = text(options.name);
|
|
221
|
-
const release = isClosed ? void 0 : budget.admitRecord(terminalName(name));
|
|
222
|
-
const spanOptions = {
|
|
223
|
-
...options,
|
|
224
|
-
name,
|
|
225
|
-
annotations: Context.empty(),
|
|
226
|
-
links: []
|
|
227
|
-
};
|
|
228
|
-
const span = release === void 0 ? new Tracer.NativeSpan({
|
|
229
|
-
...spanOptions,
|
|
230
|
-
sampled: false
|
|
231
|
-
}) : delegate.span(spanOptions);
|
|
232
|
-
if (release !== void 0) spans.set(span.spanId, release);
|
|
233
|
-
const wrapped = Object.create(span);
|
|
234
|
-
let fields = 0, events = 0, links = 0, attributeBytes = 0;
|
|
235
|
-
let ended = false;
|
|
236
|
-
Object.defineProperties(wrapped, {
|
|
237
|
-
attribute: { value: (key, value) => {
|
|
238
|
-
if (fields >= 32 && !span.attributes.has(key)) {
|
|
239
|
-
budget.drop();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const safe = attributes({ [key]: value }, 1);
|
|
243
|
-
if (!Object.hasOwn(safe, key)) return;
|
|
244
|
-
const previous = span.attributes.has(key) ? new TextEncoder().encode(JSON.stringify([key, span.attributes.get(key)])).length : 0;
|
|
245
|
-
const next = new TextEncoder().encode(JSON.stringify([key, safe[key]])).length;
|
|
246
|
-
if (attributeBytes - previous + next > 2e3) {
|
|
247
|
-
budget.drop();
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
attributeBytes += next - previous;
|
|
251
|
-
if (!span.attributes.has(key)) fields += 1;
|
|
252
|
-
span.attribute(key, safe[key]);
|
|
253
|
-
} },
|
|
254
|
-
event: { value: (eventName, time, attrs) => {
|
|
255
|
-
if (events++ >= 2) {
|
|
256
|
-
budget.drop();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
span.event(text(eventName), time, attributes(attrs ?? {}, 4));
|
|
260
|
-
} },
|
|
261
|
-
addLinks: { value: (items) => {
|
|
262
|
-
for (const item of items.slice(0, Math.max(0, 2 - links))) {
|
|
263
|
-
if (!/^[a-f0-9]{32}$/u.test(item.span.traceId) || !/^[a-f0-9]{16}$/u.test(item.span.spanId)) {
|
|
264
|
-
budget.drop();
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
span.addLinks([{
|
|
268
|
-
span: Tracer.externalSpan({
|
|
269
|
-
traceId: item.span.traceId,
|
|
270
|
-
spanId: item.span.spanId,
|
|
271
|
-
sampled: item.span.sampled
|
|
272
|
-
}),
|
|
273
|
-
attributes: attributes(item.attributes, 4)
|
|
274
|
-
}]);
|
|
275
|
-
links += 1;
|
|
276
|
-
}
|
|
277
|
-
} },
|
|
278
|
-
end: { value: (time, exit) => {
|
|
279
|
-
if (ended) return;
|
|
280
|
-
ended = true;
|
|
281
|
-
span.end(time, Exit.isFailure(exit) ? Exit.fail("CLI operation failed") : Exit.void);
|
|
282
|
-
} }
|
|
283
|
-
});
|
|
284
|
-
wrapped.addLinks(options.links ?? []);
|
|
285
|
-
return wrapped;
|
|
286
|
-
},
|
|
287
|
-
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
288
|
-
});
|
|
289
|
-
},
|
|
290
|
-
serialization(delegate) {
|
|
291
|
-
return {
|
|
292
|
-
logs(data) {
|
|
293
|
-
const releases = logs.splice(0);
|
|
294
|
-
try {
|
|
295
|
-
const body = delegate.logs(data);
|
|
296
|
-
if (data.resourceLogs.some((r) => r.scopeLogs.some((s) => s.logRecords?.some((l) => terminalName(l.body?.stringValue ?? ""))))) terminalBodies.add(body);
|
|
297
|
-
holdBody(body, releases);
|
|
298
|
-
return body;
|
|
299
|
-
} catch (error) {
|
|
300
|
-
for (const release of releases) release();
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
},
|
|
304
|
-
traces(data) {
|
|
305
|
-
const releases = [];
|
|
306
|
-
for (const resource of data.resourceSpans) for (const scope of resource.scopeSpans) for (const span of scope.spans) {
|
|
307
|
-
const release = spans.get(span.spanId);
|
|
308
|
-
if (release !== void 0) releases.push(release);
|
|
309
|
-
spans.delete(span.spanId);
|
|
310
|
-
}
|
|
311
|
-
try {
|
|
312
|
-
const body = delegate.traces(data);
|
|
313
|
-
if (data.resourceSpans.some((r) => r.scopeSpans.some((s) => s.spans.some((span) => terminalName(span.name))))) terminalBodies.add(body);
|
|
314
|
-
holdBody(body, releases);
|
|
315
|
-
return body;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
for (const release of releases) release();
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
metrics(data) {
|
|
322
|
-
const body = delegate.metrics(data);
|
|
323
|
-
terminalBodies.add(body);
|
|
324
|
-
return body;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
},
|
|
328
|
-
transport(client, enabled = () => true) {
|
|
329
|
-
const retrying = HttpClient.transform(client, (response, request) => Effect.suspend(() => {
|
|
330
|
-
if (enabled()) return response;
|
|
331
|
-
budget.drop();
|
|
332
|
-
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
333
|
-
})).pipe(HttpClient.transformResponse((response) => Effect.tap(response, (result) => {
|
|
334
|
-
let received = 0;
|
|
335
|
-
return Stream.runForEach(result.stream, (chunk) => Effect.sync(() => {
|
|
336
|
-
received += chunk.byteLength;
|
|
337
|
-
if (received > 65536) throw new Error("Engineering response limit exceeded");
|
|
338
|
-
})).pipe(Effect.catchTag("HttpClientError", (error) => error.reason._tag === "EmptyBodyError" ? Effect.void : Effect.fail(error)));
|
|
339
|
-
}))).pipe(HttpClient.filterStatusOk, HttpClient.retryTransient({
|
|
340
|
-
times: 1,
|
|
341
|
-
schedule: Schedule.spaced("100 millis").pipe(Schedule.addDelay(({ input }) => Effect.map(Clock.currentTimeMillis, (now) => {
|
|
342
|
-
if (!HttpClientError.isHttpClientError(input) || input.reason._tag !== "StatusCodeError" || input.reason.response.status !== 429) return Duration.zero;
|
|
343
|
-
const header = input.reason.response.headers["retry-after"];
|
|
344
|
-
const numeric = Number(header);
|
|
345
|
-
const delay = header === void 0 ? 2e3 : Number.isFinite(numeric) ? numeric * 1e3 : Date.parse(header) - now;
|
|
346
|
-
return Duration.millis(Number.isFinite(delay) ? Math.min(2e3, Math.max(0, delay)) : 2e3);
|
|
347
|
-
})))
|
|
348
|
-
}));
|
|
349
|
-
return HttpClient.transform(retrying, (response, request) => Effect.suspend(() => {
|
|
350
|
-
if (isClosed) {
|
|
351
|
-
discardBody(request.body);
|
|
352
|
-
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
353
|
-
}
|
|
354
|
-
const droppedResponse = () => HttpClientResponse.fromWeb(request, new Response(null, { status: 200 }));
|
|
355
|
-
if (request.body._tag === "Empty") {
|
|
356
|
-
discardBody(request.body);
|
|
357
|
-
return Effect.succeed(droppedResponse());
|
|
358
|
-
}
|
|
359
|
-
const bytes = request.body.contentLength;
|
|
360
|
-
const release = budget.admitRequest(bytes ?? Number.POSITIVE_INFINITY, terminalBodies.has(request.body));
|
|
361
|
-
if (release === void 0) {
|
|
362
|
-
discardBody(request.body);
|
|
363
|
-
return Effect.succeed(droppedResponse());
|
|
364
|
-
}
|
|
365
|
-
const lease = bodyLeases.get(request.body);
|
|
366
|
-
if (lease !== void 0) lease.started = true;
|
|
367
|
-
return response.pipe(Effect.asVoid, Effect.timeoutOption(CLI_ENGINEERING_LIMITS.requestTimeoutMs), Effect.raceFirst(Deferred.await(closed).pipe(Effect.andThen(Effect.interrupt))), Effect.tap((result) => Effect.sync(() => {
|
|
368
|
-
if (Option.isNone(result)) budget.drop();
|
|
369
|
-
})), Effect.catchCause(() => Effect.sync(() => {
|
|
370
|
-
budget.drop();
|
|
371
|
-
})), Effect.as(droppedResponse()), Effect.ensuring(Effect.sync(() => {
|
|
372
|
-
release();
|
|
373
|
-
discardBody(request.body);
|
|
374
|
-
})));
|
|
375
|
-
}));
|
|
376
|
-
}
|
|
411
|
+
logger: (delegate) => admittedLogger(state, delegate),
|
|
412
|
+
tracer: (delegate) => admittedTracer(state, delegate),
|
|
413
|
+
serialization: (delegate) => admittedSerialization(state, delegate),
|
|
414
|
+
transport: (client, enabled) => admittedTransport(state, client, enabled)
|
|
377
415
|
};
|
|
378
416
|
}
|
|
379
417
|
//#endregion
|
|
380
|
-
//#region src/engineering.ts
|
|
418
|
+
//#region src/engineering/config.ts
|
|
381
419
|
const ENGINEERING_CAPXUL_ENVS = [
|
|
382
420
|
"development",
|
|
383
421
|
"staging",
|
|
@@ -385,7 +423,10 @@ const ENGINEERING_CAPXUL_ENVS = [
|
|
|
385
423
|
"local"
|
|
386
424
|
];
|
|
387
425
|
const traceHeaderFilter = (_name) => true;
|
|
388
|
-
const credentialHeaderPattern = Object.assign(/./u, {
|
|
426
|
+
const credentialHeaderPattern = Object.assign(/./u, { exec: (name) => isCredentialField(name, "header") ? Object.assign([name], {
|
|
427
|
+
index: 0,
|
|
428
|
+
input: name
|
|
429
|
+
}) : null });
|
|
389
430
|
const postHogOtlpEndpoints = (host) => {
|
|
390
431
|
const base = host.replace(/\/+$/, "");
|
|
391
432
|
return {
|
|
@@ -424,6 +465,8 @@ const validateEngineeringTelemetryConfig = (config) => {
|
|
|
424
465
|
headers: Object.freeze({ authorization: headerEntries[0][1] })
|
|
425
466
|
};
|
|
426
467
|
};
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/engineering/tracer.ts
|
|
427
470
|
const failureText = (failure, key, limit, source) => {
|
|
428
471
|
if (failure === null || typeof failure !== "object") return void 0;
|
|
429
472
|
try {
|
|
@@ -457,7 +500,7 @@ const browserExportClient = (client) => HttpClient.transform(client, (response,
|
|
|
457
500
|
if (request.body._tag === "Stream" || request.body._tag === "FormData" || bytes === void 0 || !Number.isSafeInteger(bytes) || bytes < 0 || unfinishedBrowserExportBytes + bytes > BROWSER_KEEPALIVE_BYTES) return;
|
|
458
501
|
unfinishedBrowserExportBytes += bytes;
|
|
459
502
|
return bytes;
|
|
460
|
-
}), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.
|
|
503
|
+
}), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.catchIf((error) => error.reason._tag === "EmptyBodyError", () => Effect.void)))), (bytes) => Effect.sync(() => {
|
|
461
504
|
if (bytes !== void 0) unfinishedBrowserExportBytes -= bytes;
|
|
462
505
|
})));
|
|
463
506
|
const spanUpstream = (span) => {
|
|
@@ -477,11 +520,15 @@ function redactHttpPath(path, fullUrl, source) {
|
|
|
477
520
|
const prefix = path.startsWith("/") ? origin : "";
|
|
478
521
|
return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
|
|
479
522
|
}
|
|
523
|
+
/** A span whose prototype is `span`: it answers every Span member until an own property overrides it. */
|
|
524
|
+
function inheritSpan(span) {
|
|
525
|
+
return Object.create(span);
|
|
526
|
+
}
|
|
480
527
|
/** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
|
|
481
528
|
const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
|
|
482
529
|
span(options) {
|
|
483
530
|
const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
|
|
484
|
-
const wrapped =
|
|
531
|
+
const wrapped = inheritSpan(span);
|
|
485
532
|
Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
|
|
486
533
|
const source = spanUpstream(span);
|
|
487
534
|
const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
|
|
@@ -521,6 +568,8 @@ const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer
|
|
|
521
568
|
},
|
|
522
569
|
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
523
570
|
});
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/engineering/minimal.ts
|
|
524
573
|
const MINIMAL_LOG_NAMES = /* @__PURE__ */ new Set([
|
|
525
574
|
"capxul_sdk_operation",
|
|
526
575
|
"identity.transition",
|
|
@@ -670,6 +719,8 @@ function minimalAttributes(attributes, metric = false) {
|
|
|
670
719
|
}] : [];
|
|
671
720
|
});
|
|
672
721
|
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/engineering/metric-points.ts
|
|
673
724
|
const finiteProtocolNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
674
725
|
const protocolCount = (value, maximum = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum ? value : void 0;
|
|
675
726
|
const protocolFlags = (value) => protocolCount(Object.getOwnPropertyDescriptor(value, "flags")?.value, 4294967295);
|
|
@@ -719,6 +770,8 @@ function minimalHistogramPoints(points) {
|
|
|
719
770
|
}];
|
|
720
771
|
});
|
|
721
772
|
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/engineering/serialization.ts
|
|
722
775
|
function minimalSerialization(config) {
|
|
723
776
|
const service = config.serviceName ?? "capxul-sdk";
|
|
724
777
|
const resource = { attributes: Object.entries({
|
|
@@ -809,7 +862,7 @@ function minimalSerialization(config) {
|
|
|
809
862
|
const gauge = metric.gauge === void 0 ? void 0 : { dataPoints: minimalNumberPoints(metric.gauge.dataPoints) };
|
|
810
863
|
const sum = metric.sum === void 0 ? void 0 : {
|
|
811
864
|
aggregationTemporality: protocolCount(metric.sum.aggregationTemporality, 2) ?? 0,
|
|
812
|
-
isMonotonic: metric.sum.isMonotonic
|
|
865
|
+
isMonotonic: metric.sum.isMonotonic,
|
|
813
866
|
dataPoints: minimalNumberPoints(metric.sum.dataPoints)
|
|
814
867
|
};
|
|
815
868
|
const histogram = metric.histogram === void 0 ? void 0 : {
|
|
@@ -839,6 +892,8 @@ function minimalSerialization(config) {
|
|
|
839
892
|
}
|
|
840
893
|
};
|
|
841
894
|
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/engineering/logs.ts
|
|
842
897
|
/** Attempt to drain all engineering signals; completion does not prove ingestion. */
|
|
843
898
|
const flushEngineeringTelemetry = Effect.flatMap(OtlpExporter.Flusher, (flusher) => flusher.flush);
|
|
844
899
|
var EngineeringLog = class extends Context.Service()("capxul/EngineeringLog") {};
|
|
@@ -847,6 +902,8 @@ const logEngineeringInfo = (message, attributes) => {
|
|
|
847
902
|
const record = Effect.logInfo(message).pipe(Effect.annotateLogs(attributes));
|
|
848
903
|
return Effect.flatMap(LogLevel.isEnabled("Info"), (visible) => visible ? record : Effect.flatMap(Effect.serviceOption(EngineeringLog), (logger) => logger._tag === "None" ? Effect.void : record.pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([logger.value])), Effect.provideService(References.MinimumLogLevel, "All"))));
|
|
849
904
|
};
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/engineering.ts
|
|
850
907
|
/** Shared browser/server OTLP layer. OtlpLogger merges with incumbent loggers once. */
|
|
851
908
|
const makeEngineeringTelemetryLayer = (config) => {
|
|
852
909
|
const validated = validateEngineeringTelemetryConfig(config);
|