@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.
Files changed (69) hide show
  1. package/dist/OtlpExporter.d.ts +12 -0
  2. package/dist/OtlpExporter.d.ts.map +1 -0
  3. package/dist/{otlp.js → OtlpExporter.js} +9 -4
  4. package/dist/OtlpExporter.js.map +1 -0
  5. package/dist/{otlp.d.ts → OtlpOptions.d.ts} +2 -12
  6. package/dist/OtlpOptions.d.ts.map +1 -0
  7. package/dist/OtlpOptions.js +2 -0
  8. package/dist/OtlpOptions.js.map +1 -0
  9. package/dist/extension.d.ts +19 -0
  10. package/dist/extension.d.ts.map +1 -1
  11. package/dist/extension.js +32 -9
  12. package/dist/extension.js.map +1 -1
  13. package/dist/index.d.ts +90 -6
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +106 -13
  16. package/dist/index.js.map +1 -1
  17. package/dist/logs/CapturedLog.d.ts +7 -0
  18. package/dist/logs/CapturedLog.d.ts.map +1 -0
  19. package/dist/logs/CapturedLog.js +2 -0
  20. package/dist/logs/CapturedLog.js.map +1 -0
  21. package/dist/logs/LogExporter.d.ts +12 -0
  22. package/dist/logs/LogExporter.d.ts.map +1 -0
  23. package/dist/{logs.js → logs/LogExporter.js} +4 -5
  24. package/dist/logs/LogExporter.js.map +1 -0
  25. package/dist/logs/LogsOptions.d.ts +13 -0
  26. package/dist/logs/LogsOptions.d.ts.map +1 -0
  27. package/dist/logs/LogsOptions.js +2 -0
  28. package/dist/logs/LogsOptions.js.map +1 -0
  29. package/dist/metrics/Bucketed.d.ts +16 -0
  30. package/dist/metrics/Bucketed.d.ts.map +1 -0
  31. package/dist/metrics/Bucketed.js +2 -0
  32. package/dist/metrics/Bucketed.js.map +1 -0
  33. package/dist/metrics/Metrics.d.ts +42 -0
  34. package/dist/metrics/Metrics.d.ts.map +1 -0
  35. package/dist/{metrics.js → metrics/Metrics.js} +97 -28
  36. package/dist/metrics/Metrics.js.map +1 -0
  37. package/dist/metrics/MetricsSnapshot.d.ts +14 -0
  38. package/dist/metrics/MetricsSnapshot.d.ts.map +1 -0
  39. package/dist/metrics/MetricsSnapshot.js +2 -0
  40. package/dist/metrics/MetricsSnapshot.js.map +1 -0
  41. package/dist/otlp/OtlpExporter.d.ts +12 -0
  42. package/dist/otlp/OtlpExporter.d.ts.map +1 -0
  43. package/dist/otlp/OtlpExporter.js +64 -0
  44. package/dist/otlp/OtlpExporter.js.map +1 -0
  45. package/dist/otlp/OtlpOptions.d.ts +16 -0
  46. package/dist/otlp/OtlpOptions.d.ts.map +1 -0
  47. package/dist/otlp/OtlpOptions.js +2 -0
  48. package/dist/otlp/OtlpOptions.js.map +1 -0
  49. package/package.json +14 -7
  50. package/src/{otlp.ts → OtlpExporter.ts} +12 -18
  51. package/src/OtlpOptions.ts +16 -0
  52. package/src/extension.ts +53 -9
  53. package/src/index.ts +189 -14
  54. package/src/logs/CapturedLog.ts +7 -0
  55. package/src/{logs.ts → logs/LogExporter.ts} +5 -23
  56. package/src/logs/LogsOptions.ts +12 -0
  57. package/src/metrics/Bucketed.ts +15 -0
  58. package/src/{metrics.ts → metrics/Metrics.ts} +100 -53
  59. package/src/metrics/MetricsSnapshot.ts +14 -0
  60. package/src/otlp/OtlpExporter.ts +87 -0
  61. package/src/otlp/OtlpOptions.ts +16 -0
  62. package/dist/logs.d.ts +0 -28
  63. package/dist/logs.d.ts.map +0 -1
  64. package/dist/logs.js.map +0 -1
  65. package/dist/metrics.d.ts +0 -59
  66. package/dist/metrics.d.ts.map +0 -1
  67. package/dist/metrics.js.map +0 -1
  68. package/dist/otlp.d.ts.map +0 -1
  69. 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 { trace, type SpanSink } from './index.js';
7
- import { metrics, serveTopology } from './metrics.js';
8
- import { otlp } from './otlp.js';
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: `trace()` opens the span that every log line written inside the
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
- app.use(trace(spans));
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 middleware, and it is HANDED who takes the spans it finishes.
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 trace(takers: readonly SpanSink[]): AppMiddleware {
79
- return (ctx, next) => {
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
- sampled: parent?.sampled ?? true,
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: performance.now() - start,
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, TopologyReport, FrondPlacement, Edge } from './metrics.js';
152
- export { logs } from './logs.js';
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';
@@ -0,0 +1,7 @@
1
+ import type { LogRecord } from '@fougere/core';
2
+
3
+ /** A record, plus what it could only be told at the moment it was written. */
4
+ export interface CapturedLog extends LogRecord {
5
+ traceId: string | undefined;
6
+ spanId: string | undefined;
7
+ }
@@ -1,8 +1,9 @@
1
- /** Log export — the third signal, and the only one whose value is entirely in its links. */
2
- import { Beat } from './Beat.js';
3
- import { Endpoint } from './Endpoint.js';
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
+ }