@fougere/observability 0.9.2-alpha.0 → 0.10.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OtlpExporter.d.ts +12 -0
- package/dist/OtlpExporter.d.ts.map +1 -0
- package/dist/{otlp.js → OtlpExporter.js} +9 -4
- package/dist/OtlpExporter.js.map +1 -0
- package/dist/{otlp.d.ts → OtlpOptions.d.ts} +2 -12
- package/dist/OtlpOptions.d.ts.map +1 -0
- package/dist/OtlpOptions.js +2 -0
- package/dist/OtlpOptions.js.map +1 -0
- package/dist/extension.d.ts +19 -0
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +32 -9
- package/dist/extension.js.map +1 -1
- package/dist/index.d.ts +90 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -13
- package/dist/index.js.map +1 -1
- package/dist/logs/CapturedLog.d.ts +7 -0
- package/dist/logs/CapturedLog.d.ts.map +1 -0
- package/dist/logs/CapturedLog.js +2 -0
- package/dist/logs/CapturedLog.js.map +1 -0
- package/dist/logs/LogExporter.d.ts +12 -0
- package/dist/logs/LogExporter.d.ts.map +1 -0
- package/dist/{logs.js → logs/LogExporter.js} +4 -5
- package/dist/logs/LogExporter.js.map +1 -0
- package/dist/logs/LogsOptions.d.ts +13 -0
- package/dist/logs/LogsOptions.d.ts.map +1 -0
- package/dist/logs/LogsOptions.js +2 -0
- package/dist/logs/LogsOptions.js.map +1 -0
- package/dist/metrics/Bucketed.d.ts +16 -0
- package/dist/metrics/Bucketed.d.ts.map +1 -0
- package/dist/metrics/Bucketed.js +2 -0
- package/dist/metrics/Bucketed.js.map +1 -0
- package/dist/metrics/Metrics.d.ts +42 -0
- package/dist/metrics/Metrics.d.ts.map +1 -0
- package/dist/{metrics.js → metrics/Metrics.js} +97 -28
- package/dist/metrics/Metrics.js.map +1 -0
- package/dist/metrics/MetricsSnapshot.d.ts +14 -0
- package/dist/metrics/MetricsSnapshot.d.ts.map +1 -0
- package/dist/metrics/MetricsSnapshot.js +2 -0
- package/dist/metrics/MetricsSnapshot.js.map +1 -0
- package/dist/otlp/OtlpExporter.d.ts +12 -0
- package/dist/otlp/OtlpExporter.d.ts.map +1 -0
- package/dist/otlp/OtlpExporter.js +64 -0
- package/dist/otlp/OtlpExporter.js.map +1 -0
- package/dist/otlp/OtlpOptions.d.ts +16 -0
- package/dist/otlp/OtlpOptions.d.ts.map +1 -0
- package/dist/otlp/OtlpOptions.js +2 -0
- package/dist/otlp/OtlpOptions.js.map +1 -0
- package/package.json +14 -7
- package/src/{otlp.ts → OtlpExporter.ts} +12 -18
- package/src/OtlpOptions.ts +16 -0
- package/src/extension.ts +53 -9
- package/src/index.ts +189 -14
- package/src/logs/CapturedLog.ts +7 -0
- package/src/{logs.ts → logs/LogExporter.ts} +5 -23
- package/src/logs/LogsOptions.ts +12 -0
- package/src/metrics/Bucketed.ts +15 -0
- package/src/{metrics.ts → metrics/Metrics.ts} +100 -53
- package/src/metrics/MetricsSnapshot.ts +14 -0
- package/src/otlp/OtlpExporter.ts +87 -0
- package/src/otlp/OtlpOptions.ts +16 -0
- package/dist/logs.d.ts +0 -28
- package/dist/logs.d.ts.map +0 -1
- package/dist/logs.js.map +0 -1
- package/dist/metrics.d.ts +0 -59
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js.map +0 -1
- package/dist/otlp.d.ts.map +0 -1
- package/dist/otlp.js.map +0 -1
package/src/extension.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import { frond, loggerMiddleware, Logger, LogLine, type App, type Extension, type LogSink } from '@fougere/core';
|
|
3
3
|
import ExportHandler from './ExportHandler.js';
|
|
4
4
|
import { traceContext } from '#trace-context';
|
|
5
|
-
import { registerFlush } from './index.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { logs } from './logs.js';
|
|
5
|
+
import { registerFlush, statementsUnder, tracing, type SpanSink } from './index.js';
|
|
6
|
+
import { metrics, serveTopology } from './metrics/Metrics.js';
|
|
7
|
+
import { otlp } from './otlp/OtlpExporter.js';
|
|
8
|
+
import { logs } from './logs/LogExporter.js';
|
|
10
9
|
|
|
11
10
|
export interface ObservabilityOptions {
|
|
12
11
|
/** What a dashboard groups this process by, and the name a log line carries. */
|
|
@@ -23,6 +22,45 @@ export interface ObservabilityOptions {
|
|
|
23
22
|
* extension took.
|
|
24
23
|
*/
|
|
25
24
|
onSpan?: SpanSink;
|
|
25
|
+
/**
|
|
26
|
+
* A span of its own for every statement, beside the count every operation already carries.
|
|
27
|
+
* Off by default — see `TracingOptions.spanPerStatement`.
|
|
28
|
+
*/
|
|
29
|
+
spanPerStatement?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* What share of the operations that stay in this process are traced, `0` to `1`. Default `1`.
|
|
32
|
+
*
|
|
33
|
+
* A rate is a budget, which no reading of the code answers. What the code does answer is
|
|
34
|
+
* which spans a budget may not touch — see `TracingOptions.sample`.
|
|
35
|
+
*/
|
|
36
|
+
sample?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Bucket bounds in SECONDS for the duration histograms. Default: the OpenTelemetry
|
|
39
|
+
* recommendation for request durations, which assumes a healthy op lives under 100 ms.
|
|
40
|
+
*
|
|
41
|
+
* One list for the whole process, deliberately — see `metrics.ts`.
|
|
42
|
+
*/
|
|
43
|
+
bounds?: readonly number[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How far each operation's work goes, read off the model ONCE at boot.
|
|
48
|
+
*
|
|
49
|
+
* `EffectiveOperation.reach` is resolved when the app is built, so the middleware does not
|
|
50
|
+
* recompute it per call — it looks one name up. An address the model does not hold answers zero,
|
|
51
|
+
* which is what `rpc` and a brought frond's own ops are.
|
|
52
|
+
*/
|
|
53
|
+
function hopsIn(app: App): (entity: string, operation: string) => number {
|
|
54
|
+
const hops = new Map<string, number>();
|
|
55
|
+
for (const frond of app.fronds) {
|
|
56
|
+
for (const handler of frond.handlers) {
|
|
57
|
+
for (const [name, op] of app.operationsFor(handler.address) ?? []) {
|
|
58
|
+
hops.set(`${handler.address}.${name}`, op.reach.hops);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (entity, operation) => hops.get(`${entity}.${operation}`) ?? 0;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
66
|
/** Observe this process — one member of the ascent. */
|
|
@@ -55,17 +93,23 @@ export function observability(options: ObservabilityOptions = {}): Extension {
|
|
|
55
93
|
}],
|
|
56
94
|
})],
|
|
57
95
|
|
|
58
|
-
up(app: App) {
|
|
96
|
+
async up(app: App) {
|
|
59
97
|
// Held across the ascent: the exporter exists only when `otlp` is declared, and the
|
|
60
98
|
// handler resolves this either way.
|
|
61
99
|
app.container.registerValue('LogExport', exporting);
|
|
62
100
|
const undo: (() => void | Promise<void>)[] = [];
|
|
63
101
|
undoing.set(app, undo);
|
|
64
|
-
// Order matters: `
|
|
102
|
+
// Order matters: `tracing()` opens the span that every log line written inside the
|
|
65
103
|
// call will carry. Installed the other way round, the lines leave uncorrelated.
|
|
66
104
|
const spans: SpanSink[] = options.onSpan ? [options.onSpan] : [];
|
|
67
105
|
takers.set(app, spans);
|
|
68
|
-
|
|
106
|
+
const tracer = tracing(spans, {
|
|
107
|
+
spanPerStatement: options.spanPerStatement ?? false,
|
|
108
|
+
...(options.sample === undefined ? {} : { sample: options.sample }),
|
|
109
|
+
hopsOf: hopsIn(app),
|
|
110
|
+
});
|
|
111
|
+
app.use(tracer.middleware);
|
|
112
|
+
undo.push(await statementsUnder(tracer));
|
|
69
113
|
// The app's own logger, named — NOT `new Logger(service)`: a logger built here has
|
|
70
114
|
// no `Carry`, so its lines printed and announced nothing. 20 on the console, 0 in
|
|
71
115
|
// the ring, measured on `demos/observability`.
|
|
@@ -84,7 +128,7 @@ export function observability(options: ObservabilityOptions = {}): Extension {
|
|
|
84
128
|
);
|
|
85
129
|
}
|
|
86
130
|
|
|
87
|
-
const measured = metrics(app);
|
|
131
|
+
const measured = metrics(app, options.bounds);
|
|
88
132
|
spans.push(measured.sink);
|
|
89
133
|
serveTopology(app, measured);
|
|
90
134
|
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,24 @@ import { parseTraceparent, traceparentOf, randomHex, type SpanContext } from './
|
|
|
6
6
|
/** A step while it runs. */
|
|
7
7
|
interface Running extends SpanContext {
|
|
8
8
|
frond: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* When this step began, on both clocks — carried so what runs UNDER it is dated in ITS
|
|
11
|
+
* frame rather than on a clock of its own. `Date.now()` resolves to the millisecond and a
|
|
12
|
+
* whole call can take a fraction of one, so a statement dating itself lands anywhere:
|
|
13
|
+
* measured on SigNoz 2026-09-13, six queries drawn 17 µs to the LEFT of the call that made
|
|
14
|
+
* them, then all six stacked on its first instant. Offset from the monotonic clock, they
|
|
15
|
+
* fall where they ran.
|
|
16
|
+
*/
|
|
17
|
+
startedAt: number;
|
|
18
|
+
start: number;
|
|
9
19
|
}
|
|
10
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What a span is a span OF. An operation was dispatched and has an address; a statement ran
|
|
23
|
+
* underneath one and has a table. A reader that counts operations has to say which it takes.
|
|
24
|
+
*/
|
|
25
|
+
export type SpanKind = 'operation' | 'statement';
|
|
26
|
+
|
|
11
27
|
/** A step that has finished, and what it did. */
|
|
12
28
|
export interface FinishedSpan extends SpanContext {
|
|
13
29
|
parentId: string | undefined;
|
|
@@ -18,17 +34,80 @@ export interface FinishedSpan extends SpanContext {
|
|
|
18
34
|
callerFrond: string | undefined;
|
|
19
35
|
/** Which frond owned the op — the deployment unit, so the first thing a reader groups by. */
|
|
20
36
|
frond: string | undefined;
|
|
37
|
+
kind: SpanKind;
|
|
21
38
|
entity: string;
|
|
22
39
|
operation: string;
|
|
23
40
|
/** When it started, in epoch milliseconds — an INSTANT, not an offset. */
|
|
24
41
|
startedAt: number;
|
|
25
42
|
ms: number;
|
|
43
|
+
/**
|
|
44
|
+
* `ms` minus what its OBSERVED children account for — the time this step spent itself. An op
|
|
45
|
+
* that delegates everything reports a duration and almost no self, which is how a report
|
|
46
|
+
* names the step that is actually slow instead of the one that waited for it.
|
|
47
|
+
*
|
|
48
|
+
* Never below zero, and that is not a guard: children running TOGETHER — the four queries
|
|
49
|
+
* behind one `Promise.all` — add up to more than the step that holds them. What is
|
|
50
|
+
* measured is the time nothing else accounts for, and there is no less of it than none.
|
|
51
|
+
*
|
|
52
|
+
* A child that finishes AFTER its parent is not deducted either: nothing was observed when
|
|
53
|
+
* the subtraction ran. The figure stays true, it is only cleaner than it should be.
|
|
54
|
+
*/
|
|
55
|
+
selfMs: number;
|
|
56
|
+
/** How many statements ran under this step — one per row is the shape of an N+1. */
|
|
57
|
+
statements: number;
|
|
26
58
|
/** The FougereError code when it refused, absent when it answered. */
|
|
27
59
|
error: string | undefined;
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
export type SpanSink = (span: FinishedSpan) => void;
|
|
31
63
|
|
|
64
|
+
/** A statement as its producer describes it — never the values it was given. */
|
|
65
|
+
export interface Statement {
|
|
66
|
+
/** What it read or wrote: the table, or the source when the statement names none. */
|
|
67
|
+
subject: string;
|
|
68
|
+
/** `select`, `insert`, `update`, `delete`. */
|
|
69
|
+
verb: string;
|
|
70
|
+
ms: number;
|
|
71
|
+
failed: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What an app's tracer answers for. */
|
|
75
|
+
export interface Tracing {
|
|
76
|
+
/** One span per operation — handed to `app.use`. */
|
|
77
|
+
middleware: AppMiddleware;
|
|
78
|
+
/** A statement that ran under the operation in flight: its time charged upward, and its own span when asked. */
|
|
79
|
+
statement(ran: Statement): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TracingOptions {
|
|
83
|
+
/**
|
|
84
|
+
* A span of its OWN for every statement, and not only the time charged to the operation
|
|
85
|
+
* above it. Off by default, and the default is not prudence: `selfMs` and `statements` are
|
|
86
|
+
* counted either way, so what this adds is WHICH queries ran — a diagnosis, against a
|
|
87
|
+
* volume that multiplies by however many rows the page held. A backend charges per span.
|
|
88
|
+
*
|
|
89
|
+
* The pair splits the way profiling splits from monitoring: the count DETECTS, and it runs
|
|
90
|
+
* always; the detail EXPLAINS, and it is turned on over the operation the count named.
|
|
91
|
+
*/
|
|
92
|
+
spanPerStatement?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* What share of the operations that stay in this process are traced, `0` to `1`.
|
|
95
|
+
*
|
|
96
|
+
* A rate is a BUDGET — how much a backend is worth per day — and no reading of the code
|
|
97
|
+
* answers it, so it is the operator's to set. What the code does answer is which spans the
|
|
98
|
+
* budget may not touch: an operation that crosses a process produces the one signal nothing
|
|
99
|
+
* else carries, since the difference between the caller's span and the callee's IS the wire
|
|
100
|
+
* cost, and neither process can measure it alone. So a rate below one thins what is already
|
|
101
|
+
* described by its own histogram, and never what only a trace can show.
|
|
102
|
+
*
|
|
103
|
+
* Default `1`, which is what a process did before this existed. The gain is not the default:
|
|
104
|
+
* it is that turning the rate down no longer drops the traces that matter most.
|
|
105
|
+
*/
|
|
106
|
+
sample?: number;
|
|
107
|
+
/** How far an operation's work goes, by `entity.op` — the boot reads it off the model. */
|
|
108
|
+
hopsOf?: (entity: string, operation: string) => number;
|
|
109
|
+
}
|
|
110
|
+
|
|
32
111
|
/** The step running here and now. */
|
|
33
112
|
|
|
34
113
|
/** The step running right now, if any. */
|
|
@@ -68,15 +147,33 @@ export function registerFlush(send: () => Promise<void>): () => void {
|
|
|
68
147
|
}
|
|
69
148
|
|
|
70
149
|
/**
|
|
71
|
-
* The
|
|
150
|
+
* The tracer of ONE app, and it is HANDED who takes the spans it finishes.
|
|
72
151
|
*
|
|
73
152
|
* The list belongs to the app the middleware was installed on, never to the process: a
|
|
74
153
|
* discarded app went on feeding the takers of the app that replaced it, so every metric
|
|
75
154
|
* counted twice. A module-level array made that a leak you had to remember to undo; a list
|
|
76
155
|
* held beside the app cannot outlive it. Read at every end, so a taker added later counts.
|
|
156
|
+
*
|
|
157
|
+
* Two facades and not one, because a statement is not dispatched: nothing calls a middleware
|
|
158
|
+
* around a query. What both need is the same table — the time a step's children accounted
|
|
159
|
+
* for — so the two sit in one closure rather than reaching for each other.
|
|
160
|
+
*
|
|
161
|
+
* Documented: [observability](https://fougere.dev/docs/infra/observability).
|
|
77
162
|
*/
|
|
78
|
-
export function
|
|
79
|
-
|
|
163
|
+
export function tracing(takers: readonly SpanSink[], options: TracingOptions = {}): Tracing {
|
|
164
|
+
/** What a step's children have accounted for, by the step they ran under. */
|
|
165
|
+
const charged = new Map<string, { ms: number; statements: number }>();
|
|
166
|
+
const rate = options.sample ?? 1;
|
|
167
|
+
const hopsOf = options.hopsOf ?? (() => 0);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Whether a root span is kept. An operation that leaves the process always is — the budget is
|
|
171
|
+
* for the ones a histogram already describes.
|
|
172
|
+
*/
|
|
173
|
+
const keeps = (entity: string, operation: string) =>
|
|
174
|
+
rate >= 1 || hopsOf(entity, operation) > 0 || Math.random() < rate;
|
|
175
|
+
|
|
176
|
+
const middleware: AppMiddleware = (ctx, next) => {
|
|
80
177
|
if (takers.length === 0) return next();
|
|
81
178
|
// An op that CARRIES a line is not a call this process made: counting it puts the
|
|
82
179
|
// delivery of a log line in the saturation figure, and spanning it puts a line about
|
|
@@ -88,11 +185,20 @@ export function trace(takers: readonly SpanSink[]): AppMiddleware {
|
|
|
88
185
|
const inherited = parseTraceparent(ctx.invocation?.trace);
|
|
89
186
|
const ambient = traceContext.current<Running>();
|
|
90
187
|
const parent = inherited ?? ambient;
|
|
188
|
+
// Both, and they are not the same measurement: the wall clock says WHEN so two
|
|
189
|
+
// processes land on one timeline, the monotonic one says HOW LONG without being
|
|
190
|
+
// moved by an NTP correction mid-call.
|
|
191
|
+
const startedAt = Date.now();
|
|
192
|
+
const start = performance.now();
|
|
91
193
|
const span: Running = {
|
|
92
194
|
traceId: parent?.traceId ?? randomHex(16),
|
|
93
195
|
spanId: randomHex(8),
|
|
94
|
-
|
|
196
|
+
// A trace is whole or it is nothing: a call under a sampled parent is sampled, whatever
|
|
197
|
+
// this process would have decided on its own. Only a root is decided here.
|
|
198
|
+
sampled: parent?.sampled ?? keeps(ctx.entity, ctx.operation),
|
|
95
199
|
frond: ctx.frond,
|
|
200
|
+
startedAt,
|
|
201
|
+
start,
|
|
96
202
|
};
|
|
97
203
|
// An edge exists only when the parent is IN this process and belongs to another frond.
|
|
98
204
|
// `inherited` won means the parent is across a wire, and it did not name its frond.
|
|
@@ -105,23 +211,26 @@ export function trace(takers: readonly SpanSink[]): AppMiddleware {
|
|
|
105
211
|
// carries the invocation.
|
|
106
212
|
if (ctx.invocation) ctx.invocation = { ...ctx.invocation, trace: traceparentOf(span) };
|
|
107
213
|
|
|
108
|
-
// Both, and they are not the same measurement: the wall clock says WHEN so two
|
|
109
|
-
// processes land on one timeline, the monotonic one says HOW LONG without being
|
|
110
|
-
// moved by an NTP correction mid-call.
|
|
111
|
-
const startedAt = Date.now();
|
|
112
|
-
const start = performance.now();
|
|
113
214
|
active += 1;
|
|
114
215
|
const finish = (error: string | undefined) => {
|
|
115
216
|
active -= 1;
|
|
217
|
+
const ms = performance.now() - start;
|
|
218
|
+
// Read and dropped in one gesture: a step ends once, and what its children owed it
|
|
219
|
+
// is of no use to anyone after that.
|
|
220
|
+
const owed = charged.get(span.spanId);
|
|
221
|
+
charged.delete(span.spanId);
|
|
116
222
|
const done: FinishedSpan = {
|
|
117
223
|
...span,
|
|
118
224
|
parentId: parent?.spanId,
|
|
119
225
|
callerFrond,
|
|
120
226
|
frond: ctx.frond,
|
|
227
|
+
kind: 'operation',
|
|
121
228
|
entity: ctx.entity,
|
|
122
229
|
operation: ctx.operation,
|
|
123
230
|
startedAt,
|
|
124
|
-
ms
|
|
231
|
+
ms,
|
|
232
|
+
selfMs: Math.max(0, ms - (owed?.ms ?? 0)),
|
|
233
|
+
statements: owed?.statements ?? 0,
|
|
125
234
|
error,
|
|
126
235
|
};
|
|
127
236
|
// A taker that throws is a broken exporter, never a broken call.
|
|
@@ -139,6 +248,70 @@ export function trace(takers: readonly SpanSink[]): AppMiddleware {
|
|
|
139
248
|
}
|
|
140
249
|
});
|
|
141
250
|
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
middleware,
|
|
254
|
+
|
|
255
|
+
statement(ran: Statement): void {
|
|
256
|
+
if (takers.length === 0) return;
|
|
257
|
+
// A statement with no operation above it — a migration, a seed — belongs to no step,
|
|
258
|
+
// and inventing a trace for it would put a root span in the viewer per row planted.
|
|
259
|
+
const under = traceContext.current<Running>();
|
|
260
|
+
if (!under) return;
|
|
261
|
+
|
|
262
|
+
const owed = charged.get(under.spanId) ?? { ms: 0, statements: 0 };
|
|
263
|
+
owed.ms += ran.ms;
|
|
264
|
+
owed.statements += 1;
|
|
265
|
+
charged.set(under.spanId, owed);
|
|
266
|
+
|
|
267
|
+
// The op above now reports what it waited for and how many times, which is the whole
|
|
268
|
+
// of what a dashboard reads. WHICH queries ran leaves only when someone asks.
|
|
269
|
+
if (!options.spanPerStatement) return;
|
|
270
|
+
|
|
271
|
+
const done: FinishedSpan = {
|
|
272
|
+
traceId: under.traceId,
|
|
273
|
+
spanId: randomHex(8),
|
|
274
|
+
sampled: under.sampled,
|
|
275
|
+
parentId: under.spanId,
|
|
276
|
+
callerFrond: undefined,
|
|
277
|
+
frond: under.frond,
|
|
278
|
+
kind: 'statement',
|
|
279
|
+
entity: ran.subject,
|
|
280
|
+
operation: ran.verb,
|
|
281
|
+
startedAt: under.startedAt + Math.max(0, performance.now() - under.start - ran.ms),
|
|
282
|
+
ms: ran.ms,
|
|
283
|
+
selfMs: ran.ms,
|
|
284
|
+
statements: 0,
|
|
285
|
+
error: ran.failed ? 'STATEMENT_FAILED' : undefined,
|
|
286
|
+
};
|
|
287
|
+
for (const take of takers) { try { take(done); } catch { /* observing never refuses */ } }
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The join this package exists to make: a statement runs SYNCHRONOUSLY inside the async
|
|
294
|
+
* context `tracing()` opened around the operation, so the sink finds the step it belongs to
|
|
295
|
+
* without the app ever holding its sources — which is what made the two ends of `onQuery`
|
|
296
|
+
* unable to see each other.
|
|
297
|
+
*
|
|
298
|
+
* `@fougere/adapter-sql` is optional, and dynamic for the reason `@fougere/calls` states:
|
|
299
|
+
* this package declares no dependency on it. An app on another storage observes no
|
|
300
|
+
* statement and every operation still reports its own time.
|
|
301
|
+
*/
|
|
302
|
+
export async function statementsUnder(tracer: Tracing): Promise<() => void> {
|
|
303
|
+
try {
|
|
304
|
+
const { onQuery } = await import('@fougere/adapter-sql');
|
|
305
|
+
|
|
306
|
+
return onQuery((event) => tracer.statement({
|
|
307
|
+
subject: event.subject,
|
|
308
|
+
verb: event.verb,
|
|
309
|
+
ms: event.ms,
|
|
310
|
+
failed: event.failed,
|
|
311
|
+
}));
|
|
312
|
+
} catch {
|
|
313
|
+
return () => {};
|
|
314
|
+
}
|
|
142
315
|
}
|
|
143
316
|
|
|
144
317
|
function codeOf(err: unknown): string {
|
|
@@ -146,8 +319,10 @@ function codeOf(err: unknown): string {
|
|
|
146
319
|
return typeof code === 'string' ? code : ((err as Error)?.name ?? 'error');
|
|
147
320
|
}
|
|
148
321
|
|
|
149
|
-
export { otlp } from './otlp.js';
|
|
150
|
-
export { metrics } from './metrics.js';
|
|
151
|
-
export type { Metrics
|
|
152
|
-
export {
|
|
322
|
+
export { otlp } from './otlp/OtlpExporter.js';
|
|
323
|
+
export { metrics } from './metrics/Metrics.js';
|
|
324
|
+
export type { Metrics } from './metrics/Metrics.js';
|
|
325
|
+
export type { TopologyReport } from '@fougere/core';
|
|
326
|
+
export type { FrondPlacement, Edge } from '@fougere/core';
|
|
327
|
+
export { logs } from './logs/LogExporter.js';
|
|
153
328
|
export { observability } from './extension.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { currentSpan } from './index.js';
|
|
1
|
+
import { Beat } from '../Beat.js';
|
|
2
|
+
import { Endpoint } from '../Endpoint.js';
|
|
3
|
+
import { currentSpan } from '../index.js';
|
|
5
4
|
import type { LogRecord } from '@fougere/core';
|
|
5
|
+
import type { CapturedLog } from './CapturedLog.js';
|
|
6
|
+
import type { LogsOptions } from './LogsOptions.js';
|
|
6
7
|
|
|
7
8
|
/** OTLP severity numbers — the scale is 1–24, these are the canonical rungs. */
|
|
8
9
|
const SEVERITY: Record<string, { number: number; text: string }> = {
|
|
@@ -12,12 +13,6 @@ const SEVERITY: Record<string, { number: number; text: string }> = {
|
|
|
12
13
|
error: { number: 17, text: 'ERROR' },
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
/** A record, plus what it could only be told at the moment it was written. */
|
|
16
|
-
export interface CapturedLog extends LogRecord {
|
|
17
|
-
traceId: string | undefined;
|
|
18
|
-
spanId: string | undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
export interface LogExporter {
|
|
22
17
|
/** Hand to `onLog`. */
|
|
23
18
|
sink: (record: LogRecord) => void;
|
|
@@ -27,19 +22,6 @@ export interface LogExporter {
|
|
|
27
22
|
stop(): Promise<void>;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
export interface LogsOptions {
|
|
31
|
-
/** Which service these lines belong to. */
|
|
32
|
-
service: string;
|
|
33
|
-
/** Collector endpoint. Default: the OTLP/HTTP convention on localhost. */
|
|
34
|
-
url?: string;
|
|
35
|
-
/** How often a batch leaves. Default: every second. */
|
|
36
|
-
flushMs?: number;
|
|
37
|
-
/** Told when a batch could not be sent. Default: silence. */
|
|
38
|
-
onError?: (err: unknown) => void;
|
|
39
|
-
/** Drop anything below this level before it leaves the process. */
|
|
40
|
-
minimum?: 'debug' | 'info' | 'warn' | 'error';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
25
|
export function logs(options: LogsOptions): LogExporter {
|
|
44
26
|
const collector = Endpoint.at(options.url ?? 'http://localhost:4318/v1/logs', options.onError);
|
|
45
27
|
const floor = options.minimum ? SEVERITY[options.minimum].number : 0;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LogsOptions {
|
|
2
|
+
/** Which service these lines belong to. */
|
|
3
|
+
service: string;
|
|
4
|
+
/** Collector endpoint. Default: the OTLP/HTTP convention on localhost. */
|
|
5
|
+
url?: string;
|
|
6
|
+
/** How often a batch leaves. Default: every second. */
|
|
7
|
+
flushMs?: number;
|
|
8
|
+
/** Told when a batch could not be sent. Default: silence. */
|
|
9
|
+
onError?: (err: unknown) => void;
|
|
10
|
+
/** Drop anything below this level before it leaves the process. */
|
|
11
|
+
minimum?: 'debug' | 'info' | 'warn' | 'error';
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Bucketed {
|
|
2
|
+
frond: string | undefined;
|
|
3
|
+
entity: string;
|
|
4
|
+
operation: string;
|
|
5
|
+
error: string | undefined;
|
|
6
|
+
count: number;
|
|
7
|
+
sum: number;
|
|
8
|
+
/** One more than the bounds: the last holds everything above the highest bound. */
|
|
9
|
+
buckets: number[];
|
|
10
|
+
/** The same measurement over `selfMs` — what the op did rather than what it waited for. */
|
|
11
|
+
selfSum: number;
|
|
12
|
+
selfBuckets: number[];
|
|
13
|
+
/** Every statement these calls ran. Against `count`, it is statements per call. */
|
|
14
|
+
statements: number;
|
|
15
|
+
}
|