@composed-di/observability 0.5.0-alpha → 0.7.0-alpha

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 (40) hide show
  1. package/dist/cli.js.map +1 -1
  2. package/dist/dashboardClient.d.ts +17 -11
  3. package/dist/dashboardClient.d.ts.map +1 -1
  4. package/dist/dashboardClient.js +28 -11
  5. package/dist/dashboardClient.js.map +1 -1
  6. package/dist/dashboardEventListener.d.ts +27 -8
  7. package/dist/dashboardEventListener.d.ts.map +1 -1
  8. package/dist/dashboardEventListener.js +38 -16
  9. package/dist/dashboardEventListener.js.map +1 -1
  10. package/dist/dashboardHtml.d.ts +6 -0
  11. package/dist/dashboardHtml.d.ts.map +1 -1
  12. package/dist/dashboardHtml.js +618 -98
  13. package/dist/dashboardHtml.js.map +1 -1
  14. package/dist/dashboardInstrumentation.d.ts +44 -0
  15. package/dist/dashboardInstrumentation.d.ts.map +1 -0
  16. package/dist/dashboardInstrumentation.js +119 -0
  17. package/dist/dashboardInstrumentation.js.map +1 -0
  18. package/dist/dashboardServer.d.ts +12 -8
  19. package/dist/dashboardServer.d.ts.map +1 -1
  20. package/dist/dashboardServer.js +52 -14
  21. package/dist/dashboardServer.js.map +1 -1
  22. package/dist/events.d.ts +20 -1
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/moduleGraph.d.ts.map +1 -1
  30. package/dist/moduleGraph.js.map +1 -1
  31. package/package.json +24 -23
  32. package/src/cli.ts +19 -19
  33. package/src/dashboardClient.ts +94 -83
  34. package/src/dashboardHtml.ts +620 -100
  35. package/src/dashboardInstrumentation.ts +153 -0
  36. package/src/dashboardServer.ts +183 -148
  37. package/src/events.ts +55 -35
  38. package/src/index.ts +5 -5
  39. package/src/moduleGraph.ts +9 -9
  40. package/src/dashboardEventListener.ts +0 -104
package/src/cli.ts CHANGED
@@ -6,37 +6,37 @@
6
6
  *
7
7
  * Applications export their service events to it with DashboardClient.
8
8
  */
9
- import { ServiceDashboard } from './dashboardServer';
9
+ import { ServiceDashboard } from './dashboardServer'
10
10
 
11
11
  function argValue(flag: string): string | undefined {
12
- const index = process.argv.indexOf(flag);
13
- return index >= 0 ? process.argv[index + 1] : undefined;
12
+ const index = process.argv.indexOf(flag)
13
+ return index >= 0 ? process.argv[index + 1] : undefined
14
14
  }
15
15
 
16
16
  async function main() {
17
- const port = Number(argValue('--port') ?? process.env.PORT ?? 4321);
18
- const host = argValue('--host') ?? '127.0.0.1';
17
+ const port = Number(argValue('--port') ?? process.env.PORT ?? 4321)
18
+ const host = argValue('--host') ?? '127.0.0.1'
19
19
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
20
- console.error(`Invalid port: ${argValue('--port') ?? process.env.PORT}`);
21
- process.exit(1);
20
+ console.error(`Invalid port: ${argValue('--port') ?? process.env.PORT}`)
21
+ process.exit(1)
22
22
  }
23
23
 
24
- const dashboard = new ServiceDashboard();
25
- const url = await dashboard.listen(port, host);
26
- console.log(`composed-di dashboard listening at ${url}`);
24
+ const dashboard = new ServiceDashboard()
25
+ const url = await dashboard.listen(port, host)
26
+ console.log(`composed-di dashboard listening at ${url}`)
27
27
  console.log(
28
28
  'Waiting for an application to connect (see DashboardClient in @composed-di/observability).',
29
- );
29
+ )
30
30
 
31
31
  const shutdown = async () => {
32
- await dashboard.close();
33
- process.exit(0);
34
- };
35
- process.on('SIGINT', shutdown);
36
- process.on('SIGTERM', shutdown);
32
+ await dashboard.close()
33
+ process.exit(0)
34
+ }
35
+ process.on('SIGINT', shutdown)
36
+ process.on('SIGTERM', shutdown)
37
37
  }
38
38
 
39
39
  main().catch((error) => {
40
- console.error(error);
41
- process.exit(1);
42
- });
40
+ console.error(error)
41
+ process.exit(1)
42
+ })
@@ -1,59 +1,68 @@
1
- import { ServiceModule } from '@composed-di/core';
2
- import { DashboardEventListener } from './dashboardEventListener';
3
- import { SpanEvent } from './events';
4
- import { ModuleGraph, moduleGraph } from './moduleGraph';
1
+ import { ServiceModule } from '@composed-di/core'
2
+ import {
3
+ DashboardInstrumentation,
4
+ DashboardInstrumentationOptions,
5
+ } from './dashboardInstrumentation'
6
+ import { SpanEvent } from './events'
7
+ import { ModuleGraph, moduleGraph } from './moduleGraph'
5
8
 
6
- export interface DashboardClientOptions {
9
+ export interface DashboardClientOptions extends DashboardInstrumentationOptions {
7
10
  /** Base URL of the standalone dashboard server, e.g. "http://localhost:4321". */
8
- url: string;
11
+ url: string
9
12
  /** How long to buffer events before exporting a batch. Default 250ms. */
10
- flushIntervalMs?: number;
13
+ flushIntervalMs?: number
11
14
  /** Export immediately once this many events are buffered. Default 64. */
12
- maxBatchSize?: number;
15
+ maxBatchSize?: number
13
16
  /** Events kept while the server is unreachable; oldest are dropped. Default 5000. */
14
- maxQueueSize?: number;
17
+ maxQueueSize?: number
15
18
  /**
16
19
  * Called when an export fails. Defaults to a single console.warn per
17
20
  * failure streak; exports never throw into application code.
18
21
  */
19
- onError?: (error: Error) => void;
22
+ onError?: (error: Error) => void
20
23
  }
21
24
 
22
25
  /**
23
26
  * Exports service observability to a standalone dashboard server, the way
24
27
  * an OpenTelemetry SDK exports spans to a collector.
25
28
  *
26
- * The application owns only this client: pass `client.listener` to
27
- * `ServiceModule.from`, then `attach` the module to register its dependency
28
- * graph with the server. Span events are buffered and shipped in batches;
29
- * export failures are retried on the next flush and never affect the
30
- * application.
29
+ * The application owns only this client: wrap the factories with this
30
+ * client's `instrumentation.instrument()`, then `attach` the module to
31
+ * register its dependency graph with the server. Span events are buffered
32
+ * and shipped in batches; export failures are retried on the next flush
33
+ * and never affect the application.
31
34
  *
32
35
  * @example
33
36
  * ```ts
34
37
  * const client = new DashboardClient({ url: 'http://localhost:4321' });
35
- * const module = ServiceModule.from(factories, client.listener);
38
+ * const module = ServiceModule.from(
39
+ * client.instrumentation.instrument(factories, {
40
+ * // Show call arguments and results in the dashboard; leave these
41
+ * // off when values may contain secrets.
42
+ * capture: { arguments: true, results: true },
43
+ * }),
44
+ * );
36
45
  * client.attach(module);
37
46
  * ```
38
47
  */
39
48
  export class DashboardClient {
40
- /** Pass this as the second argument to ServiceModule.from. */
41
- readonly listener = new DashboardEventListener();
49
+ /** Call `.instrument()` on this to wrap the factories composing the module. */
50
+ readonly instrumentation: DashboardInstrumentation
42
51
 
43
- private readonly url: string;
44
- private readonly flushIntervalMs: number;
45
- private readonly maxBatchSize: number;
46
- private readonly maxQueueSize: number;
47
- private readonly onError: (error: Error) => void;
52
+ private readonly url: string
53
+ private readonly flushIntervalMs: number
54
+ private readonly maxBatchSize: number
55
+ private readonly maxQueueSize: number
56
+ private readonly onError: (error: Error) => void
48
57
 
49
- private queue: SpanEvent[] = [];
50
- private graph: ModuleGraph | null = null;
51
- private graphSent = false;
52
- private timer: NodeJS.Timeout | null = null;
58
+ private queue: SpanEvent[] = []
59
+ private graph: ModuleGraph | null = null
60
+ private graphSent = false
61
+ private timer: NodeJS.Timeout | null = null
53
62
  /** Serializes exports so events reach the server in order. */
54
- private exporting: Promise<void> = Promise.resolve();
55
- private warned = false;
56
- private closed = false;
63
+ private exporting: Promise<void> = Promise.resolve()
64
+ private warned = false
65
+ private closed = false
57
66
 
58
67
  constructor({
59
68
  url,
@@ -61,107 +70,109 @@ export class DashboardClient {
61
70
  maxBatchSize = 64,
62
71
  maxQueueSize = 5000,
63
72
  onError,
73
+ ...instrumentationOptions
64
74
  }: DashboardClientOptions) {
65
- this.url = url.replace(/\/$/, '');
66
- this.flushIntervalMs = flushIntervalMs;
67
- this.maxBatchSize = maxBatchSize;
68
- this.maxQueueSize = maxQueueSize;
75
+ this.instrumentation = new DashboardInstrumentation(instrumentationOptions)
76
+ this.url = url.replace(/\/$/, '')
77
+ this.flushIntervalMs = flushIntervalMs
78
+ this.maxBatchSize = maxBatchSize
79
+ this.maxQueueSize = maxQueueSize
69
80
  this.onError =
70
81
  onError ??
71
82
  ((error) => {
72
- if (this.warned) return;
73
- this.warned = true;
83
+ if (this.warned) return
84
+ this.warned = true
74
85
  console.warn(
75
86
  `[composed-di] dashboard export to ${this.url} failed (will keep retrying): ${error.message}`,
76
- );
77
- });
78
- this.listener.subscribe((event) => this.enqueue(event));
87
+ )
88
+ })
89
+ this.instrumentation.subscribe((event) => this.enqueue(event))
79
90
  }
80
91
 
81
92
  /**
82
93
  * Registers the module's dependency graph with the dashboard server.
83
94
  */
84
95
  attach(module: ServiceModule): this {
85
- this.graph = moduleGraph(module);
86
- this.graphSent = false;
87
- void this.flush();
88
- return this;
96
+ this.graph = moduleGraph(module)
97
+ this.graphSent = false
98
+ void this.flush()
99
+ return this
89
100
  }
90
101
 
91
102
  /** Exports everything buffered so far. Resolves once the attempt finishes. */
92
103
  flush(): Promise<void> {
93
- this.exporting = this.exporting.then(() => this.export());
94
- return this.exporting;
104
+ this.exporting = this.exporting.then(() => this.export())
105
+ return this.exporting
95
106
  }
96
107
 
97
108
  /** Flushes remaining events and stops the export timer. */
98
109
  async close(): Promise<void> {
99
- this.closed = true;
110
+ this.closed = true
100
111
  if (this.timer) {
101
- clearTimeout(this.timer);
102
- this.timer = null;
112
+ clearTimeout(this.timer)
113
+ this.timer = null
103
114
  }
104
- await this.flush();
115
+ await this.flush()
105
116
  }
106
117
 
107
118
  private enqueue(event: SpanEvent): void {
108
- if (this.closed) return;
109
- this.queue.push(event);
119
+ if (this.closed) return
120
+ this.queue.push(event)
110
121
  if (this.queue.length > this.maxQueueSize) {
111
- this.queue.splice(0, this.queue.length - this.maxQueueSize);
122
+ this.queue.splice(0, this.queue.length - this.maxQueueSize)
112
123
  }
113
124
  if (this.queue.length >= this.maxBatchSize) {
114
- void this.flush();
115
- return;
125
+ void this.flush()
126
+ return
116
127
  }
117
- this.scheduleFlush(this.flushIntervalMs);
128
+ this.scheduleFlush(this.flushIntervalMs)
118
129
  }
119
130
 
120
131
  private scheduleFlush(delayMs: number): void {
121
- if (this.timer || this.closed) return;
132
+ if (this.timer || this.closed) return
122
133
  this.timer = setTimeout(() => {
123
- this.timer = null;
124
- void this.flush();
125
- }, delayMs);
126
- this.timer.unref?.();
134
+ this.timer = null
135
+ void this.flush()
136
+ }, delayMs)
137
+ this.timer.unref?.()
127
138
  }
128
139
 
129
140
  private async export(): Promise<void> {
130
141
  // The graph must reach the server before any events that reference it.
131
142
  if (this.graph && !this.graphSent) {
132
143
  if ((await this.post('/v1/graph', this.graph)) !== 'ok') {
133
- this.retryLater();
134
- return;
144
+ this.retryLater()
145
+ return
135
146
  }
136
- this.graphSent = true;
147
+ this.graphSent = true
137
148
  }
138
- let reRegistrations = 0;
149
+ let reRegistrations = 0
139
150
  while (this.queue.length > 0) {
140
- const batch = this.queue.slice(0, this.maxBatchSize);
141
- const result = await this.post('/v1/events', { events: batch });
151
+ const batch = this.queue.slice(0, this.maxBatchSize)
152
+ const result = await this.post('/v1/events', { events: batch })
142
153
  // A restarted server lost the graph — re-register and resend the batch.
143
154
  if (result === 'graph-required' && this.graph && reRegistrations < 3) {
144
- reRegistrations += 1;
145
- this.graphSent = false;
155
+ reRegistrations += 1
156
+ this.graphSent = false
146
157
  if ((await this.post('/v1/graph', this.graph)) !== 'ok') {
147
- this.retryLater();
148
- return;
158
+ this.retryLater()
159
+ return
149
160
  }
150
- this.graphSent = true;
151
- continue;
161
+ this.graphSent = true
162
+ continue
152
163
  }
153
164
  if (result !== 'ok') {
154
- this.retryLater();
155
- return;
165
+ this.retryLater()
166
+ return
156
167
  }
157
- this.queue.splice(0, batch.length);
168
+ this.queue.splice(0, batch.length)
158
169
  }
159
170
  }
160
171
 
161
172
  /** After a failed export, retry even if no new events arrive. */
162
173
  private retryLater(): void {
163
174
  if (this.queue.length > 0 || (this.graph && !this.graphSent)) {
164
- this.scheduleFlush(Math.max(this.flushIntervalMs, 2000));
175
+ this.scheduleFlush(Math.max(this.flushIntervalMs, 2000))
165
176
  }
166
177
  }
167
178
 
@@ -174,16 +185,16 @@ export class DashboardClient {
174
185
  method: 'POST',
175
186
  headers: { 'content-type': 'application/json' },
176
187
  body: JSON.stringify(body),
177
- });
178
- if (response.status === 409) return 'graph-required';
188
+ })
189
+ if (response.status === 409) return 'graph-required'
179
190
  if (!response.ok) {
180
- throw new Error(`server responded ${response.status}`);
191
+ throw new Error(`server responded ${response.status}`)
181
192
  }
182
- this.warned = false;
183
- return 'ok';
193
+ this.warned = false
194
+ return 'ok'
184
195
  } catch (error) {
185
- this.onError(error instanceof Error ? error : new Error(String(error)));
186
- return 'failed';
196
+ this.onError(error instanceof Error ? error : new Error(String(error)))
197
+ return 'failed'
187
198
  }
188
199
  }
189
200
  }