@composed-di/observability 0.5.0-alpha → 0.6.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 +19 -11
  3. package/dist/dashboardClient.d.ts.map +1 -1
  4. package/dist/dashboardClient.js +30 -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 +42 -0
  15. package/dist/dashboardInstrumentation.d.ts.map +1 -0
  16. package/dist/dashboardInstrumentation.js +116 -0
  17. package/dist/dashboardInstrumentation.js.map +1 -0
  18. package/dist/dashboardServer.d.ts +14 -8
  19. package/dist/dashboardServer.d.ts.map +1 -1
  20. package/dist/dashboardServer.js +54 -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 +96 -83
  34. package/src/dashboardHtml.ts +620 -100
  35. package/src/dashboardInstrumentation.ts +151 -0
  36. package/src/dashboardServer.ts +185 -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,70 @@
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
30
+ * `instrument()` and this client's `instrumentation`, then `attach` the
31
+ * module to register its dependency graph with the server. Span events are
32
+ * buffered and shipped in batches; export failures are retried on the next
33
+ * flush 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
+ * instrument(factories, {
40
+ * instrumentation: client.instrumentation,
41
+ * // Show call arguments and results in the dashboard; leave these
42
+ * // off when values may contain secrets.
43
+ * captureArguments: true,
44
+ * captureResults: true,
45
+ * }),
46
+ * );
36
47
  * client.attach(module);
37
48
  * ```
38
49
  */
39
50
  export class DashboardClient {
40
- /** Pass this as the second argument to ServiceModule.from. */
41
- readonly listener = new DashboardEventListener();
51
+ /** Pass this to `instrument()` when composing the module. */
52
+ readonly instrumentation: DashboardInstrumentation
42
53
 
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;
54
+ private readonly url: string
55
+ private readonly flushIntervalMs: number
56
+ private readonly maxBatchSize: number
57
+ private readonly maxQueueSize: number
58
+ private readonly onError: (error: Error) => void
48
59
 
49
- private queue: SpanEvent[] = [];
50
- private graph: ModuleGraph | null = null;
51
- private graphSent = false;
52
- private timer: NodeJS.Timeout | null = null;
60
+ private queue: SpanEvent[] = []
61
+ private graph: ModuleGraph | null = null
62
+ private graphSent = false
63
+ private timer: NodeJS.Timeout | null = null
53
64
  /** Serializes exports so events reach the server in order. */
54
- private exporting: Promise<void> = Promise.resolve();
55
- private warned = false;
56
- private closed = false;
65
+ private exporting: Promise<void> = Promise.resolve()
66
+ private warned = false
67
+ private closed = false
57
68
 
58
69
  constructor({
59
70
  url,
@@ -61,107 +72,109 @@ export class DashboardClient {
61
72
  maxBatchSize = 64,
62
73
  maxQueueSize = 5000,
63
74
  onError,
75
+ ...instrumentationOptions
64
76
  }: DashboardClientOptions) {
65
- this.url = url.replace(/\/$/, '');
66
- this.flushIntervalMs = flushIntervalMs;
67
- this.maxBatchSize = maxBatchSize;
68
- this.maxQueueSize = maxQueueSize;
77
+ this.instrumentation = new DashboardInstrumentation(instrumentationOptions)
78
+ this.url = url.replace(/\/$/, '')
79
+ this.flushIntervalMs = flushIntervalMs
80
+ this.maxBatchSize = maxBatchSize
81
+ this.maxQueueSize = maxQueueSize
69
82
  this.onError =
70
83
  onError ??
71
84
  ((error) => {
72
- if (this.warned) return;
73
- this.warned = true;
85
+ if (this.warned) return
86
+ this.warned = true
74
87
  console.warn(
75
88
  `[composed-di] dashboard export to ${this.url} failed (will keep retrying): ${error.message}`,
76
- );
77
- });
78
- this.listener.subscribe((event) => this.enqueue(event));
89
+ )
90
+ })
91
+ this.instrumentation.subscribe((event) => this.enqueue(event))
79
92
  }
80
93
 
81
94
  /**
82
95
  * Registers the module's dependency graph with the dashboard server.
83
96
  */
84
97
  attach(module: ServiceModule): this {
85
- this.graph = moduleGraph(module);
86
- this.graphSent = false;
87
- void this.flush();
88
- return this;
98
+ this.graph = moduleGraph(module)
99
+ this.graphSent = false
100
+ void this.flush()
101
+ return this
89
102
  }
90
103
 
91
104
  /** Exports everything buffered so far. Resolves once the attempt finishes. */
92
105
  flush(): Promise<void> {
93
- this.exporting = this.exporting.then(() => this.export());
94
- return this.exporting;
106
+ this.exporting = this.exporting.then(() => this.export())
107
+ return this.exporting
95
108
  }
96
109
 
97
110
  /** Flushes remaining events and stops the export timer. */
98
111
  async close(): Promise<void> {
99
- this.closed = true;
112
+ this.closed = true
100
113
  if (this.timer) {
101
- clearTimeout(this.timer);
102
- this.timer = null;
114
+ clearTimeout(this.timer)
115
+ this.timer = null
103
116
  }
104
- await this.flush();
117
+ await this.flush()
105
118
  }
106
119
 
107
120
  private enqueue(event: SpanEvent): void {
108
- if (this.closed) return;
109
- this.queue.push(event);
121
+ if (this.closed) return
122
+ this.queue.push(event)
110
123
  if (this.queue.length > this.maxQueueSize) {
111
- this.queue.splice(0, this.queue.length - this.maxQueueSize);
124
+ this.queue.splice(0, this.queue.length - this.maxQueueSize)
112
125
  }
113
126
  if (this.queue.length >= this.maxBatchSize) {
114
- void this.flush();
115
- return;
127
+ void this.flush()
128
+ return
116
129
  }
117
- this.scheduleFlush(this.flushIntervalMs);
130
+ this.scheduleFlush(this.flushIntervalMs)
118
131
  }
119
132
 
120
133
  private scheduleFlush(delayMs: number): void {
121
- if (this.timer || this.closed) return;
134
+ if (this.timer || this.closed) return
122
135
  this.timer = setTimeout(() => {
123
- this.timer = null;
124
- void this.flush();
125
- }, delayMs);
126
- this.timer.unref?.();
136
+ this.timer = null
137
+ void this.flush()
138
+ }, delayMs)
139
+ this.timer.unref?.()
127
140
  }
128
141
 
129
142
  private async export(): Promise<void> {
130
143
  // The graph must reach the server before any events that reference it.
131
144
  if (this.graph && !this.graphSent) {
132
145
  if ((await this.post('/v1/graph', this.graph)) !== 'ok') {
133
- this.retryLater();
134
- return;
146
+ this.retryLater()
147
+ return
135
148
  }
136
- this.graphSent = true;
149
+ this.graphSent = true
137
150
  }
138
- let reRegistrations = 0;
151
+ let reRegistrations = 0
139
152
  while (this.queue.length > 0) {
140
- const batch = this.queue.slice(0, this.maxBatchSize);
141
- const result = await this.post('/v1/events', { events: batch });
153
+ const batch = this.queue.slice(0, this.maxBatchSize)
154
+ const result = await this.post('/v1/events', { events: batch })
142
155
  // A restarted server lost the graph — re-register and resend the batch.
143
156
  if (result === 'graph-required' && this.graph && reRegistrations < 3) {
144
- reRegistrations += 1;
145
- this.graphSent = false;
157
+ reRegistrations += 1
158
+ this.graphSent = false
146
159
  if ((await this.post('/v1/graph', this.graph)) !== 'ok') {
147
- this.retryLater();
148
- return;
160
+ this.retryLater()
161
+ return
149
162
  }
150
- this.graphSent = true;
151
- continue;
163
+ this.graphSent = true
164
+ continue
152
165
  }
153
166
  if (result !== 'ok') {
154
- this.retryLater();
155
- return;
167
+ this.retryLater()
168
+ return
156
169
  }
157
- this.queue.splice(0, batch.length);
170
+ this.queue.splice(0, batch.length)
158
171
  }
159
172
  }
160
173
 
161
174
  /** After a failed export, retry even if no new events arrive. */
162
175
  private retryLater(): void {
163
176
  if (this.queue.length > 0 || (this.graph && !this.graphSent)) {
164
- this.scheduleFlush(Math.max(this.flushIntervalMs, 2000));
177
+ this.scheduleFlush(Math.max(this.flushIntervalMs, 2000))
165
178
  }
166
179
  }
167
180
 
@@ -174,16 +187,16 @@ export class DashboardClient {
174
187
  method: 'POST',
175
188
  headers: { 'content-type': 'application/json' },
176
189
  body: JSON.stringify(body),
177
- });
178
- if (response.status === 409) return 'graph-required';
190
+ })
191
+ if (response.status === 409) return 'graph-required'
179
192
  if (!response.ok) {
180
- throw new Error(`server responded ${response.status}`);
193
+ throw new Error(`server responded ${response.status}`)
181
194
  }
182
- this.warned = false;
183
- return 'ok';
195
+ this.warned = false
196
+ return 'ok'
184
197
  } catch (error) {
185
- this.onError(error instanceof Error ? error : new Error(String(error)));
186
- return 'failed';
198
+ this.onError(error instanceof Error ? error : new Error(String(error)))
199
+ return 'failed'
187
200
  }
188
201
  }
189
202
  }