@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.
- package/dist/cli.js.map +1 -1
- package/dist/dashboardClient.d.ts +19 -11
- package/dist/dashboardClient.d.ts.map +1 -1
- package/dist/dashboardClient.js +30 -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 +42 -0
- package/dist/dashboardInstrumentation.d.ts.map +1 -0
- package/dist/dashboardInstrumentation.js +116 -0
- package/dist/dashboardInstrumentation.js.map +1 -0
- package/dist/dashboardServer.d.ts +14 -8
- package/dist/dashboardServer.d.ts.map +1 -1
- package/dist/dashboardServer.js +54 -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 +96 -83
- package/src/dashboardHtml.ts +620 -100
- package/src/dashboardInstrumentation.ts +151 -0
- package/src/dashboardServer.ts +185 -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,70 @@
|
|
|
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
|
|
29
|
-
* export failures are retried on the next
|
|
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(
|
|
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
|
|
41
|
-
readonly
|
|
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.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
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.
|
|
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
|
}
|