@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.
- package/dist/cli.js.map +1 -1
- package/dist/dashboardClient.d.ts +17 -11
- package/dist/dashboardClient.d.ts.map +1 -1
- package/dist/dashboardClient.js +28 -11
- package/dist/dashboardClient.js.map +1 -1
- package/dist/dashboardEventListener.d.ts +27 -8
- package/dist/dashboardEventListener.d.ts.map +1 -1
- package/dist/dashboardEventListener.js +38 -16
- package/dist/dashboardEventListener.js.map +1 -1
- package/dist/dashboardHtml.d.ts +6 -0
- package/dist/dashboardHtml.d.ts.map +1 -1
- package/dist/dashboardHtml.js +618 -98
- package/dist/dashboardHtml.js.map +1 -1
- package/dist/dashboardInstrumentation.d.ts +44 -0
- package/dist/dashboardInstrumentation.d.ts.map +1 -0
- package/dist/dashboardInstrumentation.js +119 -0
- package/dist/dashboardInstrumentation.js.map +1 -0
- package/dist/dashboardServer.d.ts +12 -8
- package/dist/dashboardServer.d.ts.map +1 -1
- package/dist/dashboardServer.js +52 -14
- package/dist/dashboardServer.js.map +1 -1
- package/dist/events.d.ts +20 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/moduleGraph.d.ts.map +1 -1
- package/dist/moduleGraph.js.map +1 -1
- package/package.json +24 -23
- package/src/cli.ts +19 -19
- package/src/dashboardClient.ts +94 -83
- package/src/dashboardHtml.ts +620 -100
- package/src/dashboardInstrumentation.ts +153 -0
- package/src/dashboardServer.ts +183 -148
- package/src/events.ts +55 -35
- package/src/index.ts +5 -5
- package/src/moduleGraph.ts +9 -9
- 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
|
+
})
|
package/src/dashboardClient.ts
CHANGED
|
@@ -1,59 +1,68 @@
|
|
|
1
|
-
import { ServiceModule } from '@composed-di/core'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
27
|
-
* `
|
|
28
|
-
* graph with the server. Span events are buffered
|
|
29
|
-
* export failures are retried on the next flush
|
|
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(
|
|
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
|
-
/**
|
|
41
|
-
readonly
|
|
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.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
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.
|
|
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
|
}
|