@gobing-ai/ts-infra 0.3.0 → 0.3.2
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/README.md +52 -29
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +15 -4
- package/dist/event-bus/default-observers.d.ts +53 -0
- package/dist/event-bus/default-observers.d.ts.map +1 -0
- package/dist/event-bus/default-observers.js +107 -0
- package/dist/event-bus/event-bus.d.ts.map +1 -1
- package/dist/event-bus/event-bus.js +1 -0
- package/dist/event-bus/file-observer.d.ts +25 -0
- package/dist/event-bus/file-observer.d.ts.map +1 -0
- package/dist/event-bus/file-observer.js +110 -0
- package/dist/event-bus/index.d.ts +2 -0
- package/dist/event-bus/index.d.ts.map +1 -1
- package/dist/event-bus/index.js +2 -0
- package/dist/event-bus/types.d.ts +6 -0
- package/dist/event-bus/types.d.ts.map +1 -1
- package/dist/events.d.ts +100 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +45 -34
- package/dist/job-queue/index.d.ts +0 -1
- package/dist/job-queue/index.d.ts.map +1 -1
- package/dist/job-queue/index.js +0 -1
- package/dist/job-queue/types.d.ts +7 -0
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/job-queue-db.d.ts +2 -0
- package/dist/job-queue-db.d.ts.map +1 -0
- package/dist/job-queue-db.js +1 -0
- package/dist/logger.d.ts +39 -7
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +76 -73
- package/dist/scheduler/action.d.ts +97 -1
- package/dist/scheduler/action.d.ts.map +1 -1
- package/dist/scheduler/action.js +111 -0
- package/dist/scheduler/cloudflare.d.ts +6 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +6 -0
- package/dist/scheduler/factory.d.ts +2 -0
- package/dist/scheduler/factory.d.ts.map +1 -1
- package/dist/scheduler/factory.js +2 -0
- package/dist/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/node.d.ts +4 -0
- package/dist/scheduler/node.d.ts.map +1 -1
- package/dist/scheduler/node.js +4 -0
- package/dist/scheduler/noop.d.ts +1 -0
- package/dist/scheduler/noop.d.ts.map +1 -1
- package/dist/scheduler/noop.js +1 -0
- package/dist/scheduler/wrap-handler.d.ts +18 -0
- package/dist/scheduler/wrap-handler.d.ts.map +1 -0
- package/dist/scheduler/wrap-handler.js +41 -0
- package/dist/scheduler-cloudflare.d.ts +2 -0
- package/dist/scheduler-cloudflare.d.ts.map +1 -0
- package/dist/scheduler-cloudflare.js +1 -0
- package/dist/scheduler-node.d.ts +2 -0
- package/dist/scheduler-node.d.ts.map +1 -0
- package/dist/scheduler-node.js +1 -0
- package/dist/telemetry/config.d.ts +4 -0
- package/dist/telemetry/config.d.ts.map +1 -1
- package/dist/telemetry/metrics.d.ts +19 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +19 -0
- package/dist/telemetry/sdk.d.ts +4 -0
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +4 -0
- package/dist/telemetry/tracing.d.ts +12 -0
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +12 -0
- package/package.json +19 -2
- package/src/api-client.ts +15 -4
- package/src/event-bus/default-observers.ts +117 -0
- package/src/event-bus/event-bus.ts +1 -0
- package/src/event-bus/file-observer.ts +142 -0
- package/src/event-bus/index.ts +7 -0
- package/src/event-bus/types.ts +6 -0
- package/src/events.ts +108 -0
- package/src/index.ts +44 -7
- package/src/job-queue/db-job-queue.ts +50 -38
- package/src/job-queue/index.ts +0 -1
- package/src/job-queue/types.ts +7 -0
- package/src/job-queue-db.ts +1 -0
- package/src/logger.ts +102 -77
- package/src/scheduler/action.ts +164 -1
- package/src/scheduler/cloudflare.ts +6 -0
- package/src/scheduler/factory.ts +2 -0
- package/src/scheduler/index.ts +13 -2
- package/src/scheduler/node.ts +4 -0
- package/src/scheduler/noop.ts +1 -0
- package/src/scheduler/wrap-handler.ts +50 -0
- package/src/scheduler-cloudflare.ts +1 -0
- package/src/scheduler-node.ts +1 -0
- package/src/telemetry/config.ts +4 -0
- package/src/telemetry/metrics.ts +19 -0
- package/src/telemetry/sdk.ts +4 -0
- package/src/telemetry/tracing.ts +12 -0
|
@@ -4,20 +4,39 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type Counter, type Histogram } from '@opentelemetry/api';
|
|
6
6
|
export type { Counter, Histogram } from '@opentelemetry/api';
|
|
7
|
+
/** Whether the metrics subsystem has been initialized via {@link initMetrics}. */
|
|
7
8
|
export declare function isMetricsInitialized(): boolean;
|
|
9
|
+
/** Counter for total outbound HTTP requests, tagged by method and status code. */
|
|
8
10
|
export declare function getHttpClientRequestTotal(): Counter;
|
|
11
|
+
/** Histogram for outbound HTTP request duration in milliseconds. */
|
|
9
12
|
export declare function getHttpClientRequestDuration(): Histogram;
|
|
13
|
+
/** Counter for outbound HTTP errors, tagged by error type. */
|
|
10
14
|
export declare function getHttpClientRequestErrors(): Counter;
|
|
15
|
+
/** Counter for total event bus emits. */
|
|
11
16
|
export declare function getEventbusEmitsTotal(): Counter;
|
|
17
|
+
/** Counter for event bus handler errors. */
|
|
12
18
|
export declare function getEventbusErrorsTotal(): Counter;
|
|
19
|
+
/** Counter for total jobs enqueued. */
|
|
13
20
|
export declare function getQueueJobEnqueuedTotal(): Counter;
|
|
21
|
+
/** Counter for total jobs completed successfully. */
|
|
14
22
|
export declare function getQueueJobCompletedTotal(): Counter;
|
|
23
|
+
/** Counter for total jobs that failed (exhausted retries). */
|
|
15
24
|
export declare function getQueueJobFailedTotal(): Counter;
|
|
25
|
+
/** Histogram for job processing duration in milliseconds. */
|
|
16
26
|
export declare function getQueueJobProcessingDuration(): Histogram;
|
|
27
|
+
/** Counter for total scheduled job executions. */
|
|
17
28
|
export declare function getSchedulerJobExecutedTotal(): Counter;
|
|
29
|
+
/** Histogram for scheduled job execution duration in milliseconds. */
|
|
18
30
|
export declare function getSchedulerJobDuration(): Histogram;
|
|
31
|
+
/** Counter for failed scheduled job executions. */
|
|
19
32
|
export declare function getSchedulerJobFailedTotal(): Counter;
|
|
33
|
+
/** Mark the metrics subsystem as initialized. Idempotent. */
|
|
20
34
|
export declare function initMetrics(): void;
|
|
35
|
+
/** Clear the instrument cache and mark metrics as uninitialized. */
|
|
21
36
|
export declare function shutdownMetrics(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Reset all metrics state: clear instrument cache, mark uninitialized,
|
|
39
|
+
* and disable the global meter. For testing only.
|
|
40
|
+
*/
|
|
22
41
|
export declare function _resetMetrics(): void;
|
|
23
42
|
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,oBAAoB,CAAC;AAE3E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI7D,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AA8BD,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,4BAA4B,IAAI,SAAS,CAExD;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAID,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAID,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAED,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,wBAAgB,6BAA6B,IAAI,SAAS,CAEzD;AAID,wBAAgB,4BAA4B,IAAI,OAAO,CAEtD;AAED,wBAAgB,uBAAuB,IAAI,SAAS,CAEnD;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAID,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAS/C;AAED,wBAAgB,aAAa,IAAI,IAAI,CAMpC"}
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,oBAAoB,CAAC;AAE3E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI7D,kFAAkF;AAClF,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AA8BD,kFAAkF;AAClF,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,IAAI,SAAS,CAExD;AAED,8DAA8D;AAC9D,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAID,yCAAyC;AACzC,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED,4CAA4C;AAC5C,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAID,uCAAuC;AACvC,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAED,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,8DAA8D;AAC9D,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,6DAA6D;AAC7D,wBAAgB,6BAA6B,IAAI,SAAS,CAEzD;AAID,kDAAkD;AAClD,wBAAgB,4BAA4B,IAAI,OAAO,CAEtD;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,SAAS,CAEnD;AAED,mDAAmD;AACnD,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAID,6DAA6D;AAC7D,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,oEAAoE;AACpE,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAS/C;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAMpC"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { metrics } from '@opentelemetry/api';
|
|
6
6
|
let metricsInitialized = false;
|
|
7
|
+
/** Whether the metrics subsystem has been initialized via {@link initMetrics}. */
|
|
7
8
|
export function isMetricsInitialized() {
|
|
8
9
|
return metricsInitialized;
|
|
9
10
|
}
|
|
@@ -28,51 +29,65 @@ function getOrCreateHistogram(key, name, description, unit = 'ms') {
|
|
|
28
29
|
return instruments[key];
|
|
29
30
|
}
|
|
30
31
|
// ── HTTP client ─────────────────────────────────────────────────────
|
|
32
|
+
/** Counter for total outbound HTTP requests, tagged by method and status code. */
|
|
31
33
|
export function getHttpClientRequestTotal() {
|
|
32
34
|
return getOrCreateCounter('httpCliReq', 'http.client.request.total', 'Total outbound HTTP requests', '{request}');
|
|
33
35
|
}
|
|
36
|
+
/** Histogram for outbound HTTP request duration in milliseconds. */
|
|
34
37
|
export function getHttpClientRequestDuration() {
|
|
35
38
|
return getOrCreateHistogram('httpCliDur', 'http.client.request.duration', 'Outbound HTTP request duration');
|
|
36
39
|
}
|
|
40
|
+
/** Counter for outbound HTTP errors, tagged by error type. */
|
|
37
41
|
export function getHttpClientRequestErrors() {
|
|
38
42
|
return getOrCreateCounter('httpCliErr', 'http.client.request.errors', 'Outbound HTTP errors', '{error}');
|
|
39
43
|
}
|
|
40
44
|
// ── Event bus ───────────────────────────────────────────────────────
|
|
45
|
+
/** Counter for total event bus emits. */
|
|
41
46
|
export function getEventbusEmitsTotal() {
|
|
42
47
|
return getOrCreateCounter('ebEmit', 'eventbus.emits.total', 'Total event bus emits', '{emit}');
|
|
43
48
|
}
|
|
49
|
+
/** Counter for event bus handler errors. */
|
|
44
50
|
export function getEventbusErrorsTotal() {
|
|
45
51
|
return getOrCreateCounter('ebErr', 'eventbus.errors.total', 'Event bus errors', '{error}');
|
|
46
52
|
}
|
|
47
53
|
// ── Queue ───────────────────────────────────────────────────────────
|
|
54
|
+
/** Counter for total jobs enqueued. */
|
|
48
55
|
export function getQueueJobEnqueuedTotal() {
|
|
49
56
|
return getOrCreateCounter('qEnq', 'queue.jobs.enqueued', 'Total jobs enqueued', '{job}');
|
|
50
57
|
}
|
|
58
|
+
/** Counter for total jobs completed successfully. */
|
|
51
59
|
export function getQueueJobCompletedTotal() {
|
|
52
60
|
return getOrCreateCounter('qComp', 'queue.jobs.completed', 'Total jobs completed', '{job}');
|
|
53
61
|
}
|
|
62
|
+
/** Counter for total jobs that failed (exhausted retries). */
|
|
54
63
|
export function getQueueJobFailedTotal() {
|
|
55
64
|
return getOrCreateCounter('qFail', 'queue.jobs.failed', 'Total jobs failed', '{job}');
|
|
56
65
|
}
|
|
66
|
+
/** Histogram for job processing duration in milliseconds. */
|
|
57
67
|
export function getQueueJobProcessingDuration() {
|
|
58
68
|
return getOrCreateHistogram('qProcDur', 'queue.jobs.processing_duration', 'Job processing duration');
|
|
59
69
|
}
|
|
60
70
|
// ── Scheduler ───────────────────────────────────────────────────────
|
|
71
|
+
/** Counter for total scheduled job executions. */
|
|
61
72
|
export function getSchedulerJobExecutedTotal() {
|
|
62
73
|
return getOrCreateCounter('schedExec', 'scheduler.jobs.executed', 'Total scheduled job executions', '{execution}');
|
|
63
74
|
}
|
|
75
|
+
/** Histogram for scheduled job execution duration in milliseconds. */
|
|
64
76
|
export function getSchedulerJobDuration() {
|
|
65
77
|
return getOrCreateHistogram('schedDur', 'scheduler.jobs.duration', 'Scheduled job duration');
|
|
66
78
|
}
|
|
79
|
+
/** Counter for failed scheduled job executions. */
|
|
67
80
|
export function getSchedulerJobFailedTotal() {
|
|
68
81
|
return getOrCreateCounter('schedFail', 'scheduler.jobs.failed', 'Failed scheduled jobs', '{failure}');
|
|
69
82
|
}
|
|
70
83
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
84
|
+
/** Mark the metrics subsystem as initialized. Idempotent. */
|
|
71
85
|
export function initMetrics() {
|
|
72
86
|
if (metricsInitialized)
|
|
73
87
|
return;
|
|
74
88
|
metricsInitialized = true;
|
|
75
89
|
}
|
|
90
|
+
/** Clear the instrument cache and mark metrics as uninitialized. */
|
|
76
91
|
export function shutdownMetrics() {
|
|
77
92
|
// The meter provider is owned by whoever registered it globally (the host
|
|
78
93
|
// app or `@gobing-ai/ts-infra/otel-node`). Here we only drop the instrument
|
|
@@ -83,6 +98,10 @@ export function shutdownMetrics() {
|
|
|
83
98
|
metricsInitialized = false;
|
|
84
99
|
return Promise.resolve();
|
|
85
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Reset all metrics state: clear instrument cache, mark uninitialized,
|
|
103
|
+
* and disable the global meter. For testing only.
|
|
104
|
+
*/
|
|
86
105
|
export function _resetMetrics() {
|
|
87
106
|
for (const key of Object.keys(instruments)) {
|
|
88
107
|
instruments[key] = undefined;
|
package/dist/telemetry/sdk.d.ts
CHANGED
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type Tracer } from '@opentelemetry/api';
|
|
11
11
|
import type { TelemetryConfig } from './config';
|
|
12
|
+
/** Get the resolved telemetry configuration (defaults + overrides). */
|
|
12
13
|
export declare function getResolvedConfig(): TelemetryConfig;
|
|
13
14
|
/**
|
|
14
15
|
* Resolve telemetry config and mark telemetry enabled. Does not register any
|
|
15
16
|
* provider — spans flow to the globally-registered provider (none ⇒ no-op).
|
|
16
17
|
*/
|
|
17
18
|
export declare function initTelemetry(config?: Partial<TelemetryConfig>): void;
|
|
19
|
+
/** Mark telemetry as uninitialized. Does not shut down the OTel provider. */
|
|
18
20
|
export declare function shutdownTelemetry(): Promise<void>;
|
|
19
21
|
/** The infra tracer from the globally-registered provider (no-op when none). */
|
|
20
22
|
export declare function getTracer(): Tracer;
|
|
23
|
+
/** Whether telemetry is initialized and enabled. */
|
|
21
24
|
export declare function isTelemetryEnabled(): boolean;
|
|
25
|
+
/** Reset the telemetry subsystem to its uninitialized state. For testing. */
|
|
22
26
|
export declare function _resetTelemetry(): void;
|
|
23
27
|
export { context, propagation, trace } from '@opentelemetry/api';
|
|
24
28
|
//# sourceMappingURL=sdk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/telemetry/sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,KAAK,MAAM,EAAS,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAShD,wBAAgB,iBAAiB,IAAI,eAAe,CAEnD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAIrE;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGjD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/telemetry/sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,KAAK,MAAM,EAAS,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAShD,uEAAuE;AACvE,wBAAgB,iBAAiB,IAAI,eAAe,CAEnD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAIrE;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGjD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/telemetry/sdk.js
CHANGED
|
@@ -13,6 +13,7 @@ const TRACER_NAME = '@gobing-ai/ts-infra';
|
|
|
13
13
|
const TRACER_VERSION = '0.1.0';
|
|
14
14
|
let telemetryInitialized = false;
|
|
15
15
|
let resolvedConfig = getTelemetryConfig();
|
|
16
|
+
/** Get the resolved telemetry configuration (defaults + overrides). */
|
|
16
17
|
export function getResolvedConfig() {
|
|
17
18
|
return resolvedConfig;
|
|
18
19
|
}
|
|
@@ -26,6 +27,7 @@ export function initTelemetry(config) {
|
|
|
26
27
|
resolvedConfig = { ...getTelemetryConfig(), ...config };
|
|
27
28
|
telemetryInitialized = true;
|
|
28
29
|
}
|
|
30
|
+
/** Mark telemetry as uninitialized. Does not shut down the OTel provider. */
|
|
29
31
|
export function shutdownTelemetry() {
|
|
30
32
|
telemetryInitialized = false;
|
|
31
33
|
return Promise.resolve();
|
|
@@ -34,9 +36,11 @@ export function shutdownTelemetry() {
|
|
|
34
36
|
export function getTracer() {
|
|
35
37
|
return trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
36
38
|
}
|
|
39
|
+
/** Whether telemetry is initialized and enabled. */
|
|
37
40
|
export function isTelemetryEnabled() {
|
|
38
41
|
return telemetryInitialized && resolvedConfig.enabled;
|
|
39
42
|
}
|
|
43
|
+
/** Reset the telemetry subsystem to its uninitialized state. For testing. */
|
|
40
44
|
export function _resetTelemetry() {
|
|
41
45
|
telemetryInitialized = false;
|
|
42
46
|
resolvedConfig = getTelemetryConfig();
|
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
* High-level tracing helpers for application code.
|
|
3
3
|
*/
|
|
4
4
|
import { type Span, type SpanOptions, type Tracer } from '@opentelemetry/api';
|
|
5
|
+
/**
|
|
6
|
+
* Execute an async function within an active OTel span.
|
|
7
|
+
* The span is automatically ended and its status set on error.
|
|
8
|
+
*/
|
|
5
9
|
export declare function traceAsync<T>(name: string, fn: (span: Span) => Promise<T>, options?: SpanOptions, tracer?: Tracer): Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Execute a synchronous function within an active OTel span.
|
|
12
|
+
* The span is automatically ended and its status set on error.
|
|
13
|
+
*/
|
|
6
14
|
export declare function traceSync<T>(name: string, fn: (span: Span) => T, options?: SpanOptions, tracer?: Tracer): T;
|
|
15
|
+
/** Set key-value attributes on the currently active span. No-op when no span is recording. */
|
|
7
16
|
export declare function addSpanAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
17
|
+
/** Add a named event to the currently active span. No-op when no span is recording. */
|
|
8
18
|
export declare function addSpanEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
|
|
19
|
+
/** Get the currently active span from context, or `undefined`. */
|
|
9
20
|
export declare function getActiveSpan(): Span | undefined;
|
|
21
|
+
/** Execute a function with a specific span set as the active span in context. */
|
|
10
22
|
export declare function withSpan<T>(span: Span, fn: () => T): T;
|
|
11
23
|
export type { Span, SpanOptions, Tracer } from '@opentelemetry/api';
|
|
12
24
|
export { context, propagation, trace } from './sdk';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/telemetry/tracing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAW,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAS,MAAM,oBAAoB,CAAC;AAG9F,wBAAsB,UAAU,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,WAAW,EACrB,MAAM,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAY3G;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAK7F;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAKvG;AAED,wBAAgB,aAAa,IAAI,IAAI,GAAG,SAAS,CAEhD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtD;AAED,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/telemetry/tracing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAW,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAS,MAAM,oBAAoB,CAAC;AAG9F;;;GAGG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,WAAW,EACrB,MAAM,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAY3G;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAK7F;AAED,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAKvG;AAED,kEAAkE;AAClE,wBAAgB,aAAa,IAAI,IAAI,GAAG,SAAS,CAEhD;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtD;AAED,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC"}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { context, trace } from '@opentelemetry/api';
|
|
5
5
|
import { getTracer } from './sdk.js';
|
|
6
|
+
/**
|
|
7
|
+
* Execute an async function within an active OTel span.
|
|
8
|
+
* The span is automatically ended and its status set on error.
|
|
9
|
+
*/
|
|
6
10
|
export async function traceAsync(name, fn, options, tracer) {
|
|
7
11
|
const resolvedTracer = tracer ?? getTracer();
|
|
8
12
|
return resolvedTracer.startActiveSpan(name, options ?? {}, async (span) => {
|
|
@@ -18,6 +22,10 @@ export async function traceAsync(name, fn, options, tracer) {
|
|
|
18
22
|
}
|
|
19
23
|
});
|
|
20
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Execute a synchronous function within an active OTel span.
|
|
27
|
+
* The span is automatically ended and its status set on error.
|
|
28
|
+
*/
|
|
21
29
|
export function traceSync(name, fn, options, tracer) {
|
|
22
30
|
const resolvedTracer = tracer ?? getTracer();
|
|
23
31
|
return resolvedTracer.startActiveSpan(name, options ?? {}, (span) => {
|
|
@@ -33,21 +41,25 @@ export function traceSync(name, fn, options, tracer) {
|
|
|
33
41
|
}
|
|
34
42
|
});
|
|
35
43
|
}
|
|
44
|
+
/** Set key-value attributes on the currently active span. No-op when no span is recording. */
|
|
36
45
|
export function addSpanAttributes(attributes) {
|
|
37
46
|
const span = trace.getActiveSpan();
|
|
38
47
|
if (span?.isRecording()) {
|
|
39
48
|
span.setAttributes(attributes);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
51
|
+
/** Add a named event to the currently active span. No-op when no span is recording. */
|
|
42
52
|
export function addSpanEvent(name, attributes) {
|
|
43
53
|
const span = trace.getActiveSpan();
|
|
44
54
|
if (span?.isRecording()) {
|
|
45
55
|
span.addEvent(name, attributes);
|
|
46
56
|
}
|
|
47
57
|
}
|
|
58
|
+
/** Get the currently active span from context, or `undefined`. */
|
|
48
59
|
export function getActiveSpan() {
|
|
49
60
|
return trace.getActiveSpan() ?? undefined;
|
|
50
61
|
}
|
|
62
|
+
/** Execute a function with a specific span set as the active span in context. */
|
|
51
63
|
export function withSpan(span, fn) {
|
|
52
64
|
return context.with(trace.setSpan(context.active(), span), fn);
|
|
53
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -32,9 +32,21 @@
|
|
|
32
32
|
"types": "./dist/index.d.ts",
|
|
33
33
|
"import": "./dist/index.js"
|
|
34
34
|
},
|
|
35
|
+
"./job-queue-db": {
|
|
36
|
+
"types": "./dist/job-queue-db.d.ts",
|
|
37
|
+
"import": "./dist/job-queue-db.js"
|
|
38
|
+
},
|
|
35
39
|
"./otel-node": {
|
|
36
40
|
"types": "./dist/telemetry/otel-node.d.ts",
|
|
37
41
|
"import": "./dist/telemetry/otel-node.js"
|
|
42
|
+
},
|
|
43
|
+
"./scheduler-cloudflare": {
|
|
44
|
+
"types": "./dist/scheduler-cloudflare.d.ts",
|
|
45
|
+
"import": "./dist/scheduler-cloudflare.js"
|
|
46
|
+
},
|
|
47
|
+
"./scheduler-node": {
|
|
48
|
+
"types": "./dist/scheduler-node.d.ts",
|
|
49
|
+
"import": "./dist/scheduler-node.js"
|
|
38
50
|
}
|
|
39
51
|
},
|
|
40
52
|
"files": [
|
|
@@ -54,9 +66,10 @@
|
|
|
54
66
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
55
67
|
},
|
|
56
68
|
"dependencies": {
|
|
57
|
-
"@
|
|
69
|
+
"@logtape/logtape": "^2.0.0"
|
|
58
70
|
},
|
|
59
71
|
"peerDependencies": {
|
|
72
|
+
"@gobing-ai/ts-db": "^0.3.2",
|
|
60
73
|
"@opentelemetry/api": "^1.9.0",
|
|
61
74
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
62
75
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -66,6 +79,9 @@
|
|
|
66
79
|
"@opentelemetry/exporter-metrics-otlp-http": ">=0.200.0"
|
|
67
80
|
},
|
|
68
81
|
"peerDependenciesMeta": {
|
|
82
|
+
"@gobing-ai/ts-db": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
69
85
|
"@opentelemetry/sdk-trace-node": {
|
|
70
86
|
"optional": true
|
|
71
87
|
},
|
|
@@ -83,6 +99,7 @@
|
|
|
83
99
|
}
|
|
84
100
|
},
|
|
85
101
|
"devDependencies": {
|
|
102
|
+
"@gobing-ai/ts-db": "^0.3.2",
|
|
86
103
|
"@types/bun": "1.3.14",
|
|
87
104
|
"@opentelemetry/api": "^1.9.0",
|
|
88
105
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
package/src/api-client.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { traceAsync } from './telemetry/tracing';
|
|
|
16
16
|
|
|
17
17
|
// ── Types ───────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
+
/** Configuration for {@link APIClient}: base URL, default headers, timeout, and optional custom fetch. */
|
|
19
20
|
export interface APIClientConfig {
|
|
20
21
|
baseUrl: string;
|
|
21
22
|
defaultHeaders?: Record<string, string>;
|
|
@@ -23,6 +24,7 @@ export interface APIClientConfig {
|
|
|
23
24
|
fetch?: typeof globalThis.fetch;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** Per-request overrides: headers, timeout, operation name, and abort signal. */
|
|
26
28
|
export interface RequestOptions {
|
|
27
29
|
headers?: Record<string, string>;
|
|
28
30
|
timeout?: number;
|
|
@@ -30,6 +32,7 @@ export interface RequestOptions {
|
|
|
30
32
|
signal?: AbortSignal;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/** HTTP error with status code and response body text. */
|
|
33
36
|
export class APIError extends Error {
|
|
34
37
|
constructor(
|
|
35
38
|
public readonly status: number,
|
|
@@ -42,6 +45,16 @@ export class APIError extends Error {
|
|
|
42
45
|
|
|
43
46
|
// ── Client ──────────────────────────────────────────────────────────
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Typed HTTP client builder wrapping fetch with automatic JSON serialization,
|
|
50
|
+
* timeout support, and OpenTelemetry tracing on every request.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const client = new APIClient({ baseUrl: 'https://api.example.com' });
|
|
55
|
+
* const data = await client.get<User>('/users/1');
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
45
58
|
export class APIClient {
|
|
46
59
|
private readonly baseUrl: string;
|
|
47
60
|
private readonly defaultHeaders: Record<string, string>;
|
|
@@ -97,8 +110,6 @@ export class APIClient {
|
|
|
97
110
|
signal: combinedSignal,
|
|
98
111
|
});
|
|
99
112
|
|
|
100
|
-
if (timer) clearTimeout(timer);
|
|
101
|
-
|
|
102
113
|
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
103
114
|
|
|
104
115
|
getHttpClientRequestTotal().add(1, {
|
|
@@ -128,8 +139,6 @@ export class APIClient {
|
|
|
128
139
|
|
|
129
140
|
return (await response.text()) as unknown as T;
|
|
130
141
|
} catch (error) {
|
|
131
|
-
if (timer) clearTimeout(timer);
|
|
132
|
-
|
|
133
142
|
if (!(error instanceof APIError)) {
|
|
134
143
|
getHttpClientRequestErrors().add(1, {
|
|
135
144
|
'http.request.method': method,
|
|
@@ -138,6 +147,8 @@ export class APIClient {
|
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
throw error;
|
|
150
|
+
} finally {
|
|
151
|
+
if (timer) clearTimeout(timer);
|
|
141
152
|
}
|
|
142
153
|
},
|
|
143
154
|
{ kind: SpanKind.CLIENT },
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default lifecycle observers for EventBus self-observability.
|
|
3
|
+
*
|
|
4
|
+
* Connect `BusLifecycleEvents` to the logging and telemetry infrastructure so
|
|
5
|
+
* operators get visibility without wiring everything by hand. Opt-in: nothing
|
|
6
|
+
* is attached unless you call one of these.
|
|
7
|
+
*
|
|
8
|
+
* **Metrics are intentionally omitted here.** Unlike the original design, the
|
|
9
|
+
* `EventBus` core now increments `eventbus.emits.total` / `eventbus.errors.total`
|
|
10
|
+
* inline on every emit (see `event-bus.ts`). A metrics observer would
|
|
11
|
+
* double-count, so the restored default set is **log + telemetry-trace only**.
|
|
12
|
+
*
|
|
13
|
+
* @example One-liner setup
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { EventBus, createLifecycleBus, attachDefaultObservers } from '@gobing-ai/ts-infra';
|
|
16
|
+
*
|
|
17
|
+
* const lifecycleBus = createLifecycleBus();
|
|
18
|
+
* attachDefaultObservers(lifecycleBus);
|
|
19
|
+
*
|
|
20
|
+
* const appBus = new EventBus<AppEvents>({ lifecycleBus });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getLogger, type Logger } from '../logger';
|
|
25
|
+
import { addSpanAttributes, addSpanEvent, traceAsync } from '../telemetry/tracing';
|
|
26
|
+
import { EventBus } from './event-bus';
|
|
27
|
+
import type { BusLifecycleEvents } from './types';
|
|
28
|
+
|
|
29
|
+
let _obsLogger: Logger | undefined;
|
|
30
|
+
function obsLogger(): Logger {
|
|
31
|
+
if (!_obsLogger) _obsLogger = getLogger('event-bus.observers');
|
|
32
|
+
return _obsLogger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a dedicated `EventBus<BusLifecycleEvents>` for use as the
|
|
37
|
+
* `lifecycleBus` constructor argument on another `EventBus`.
|
|
38
|
+
*/
|
|
39
|
+
export function createLifecycleBus(): EventBus<BusLifecycleEvents> {
|
|
40
|
+
return new EventBus<BusLifecycleEvents>();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register handlers that write structured log entries for every lifecycle event.
|
|
45
|
+
*
|
|
46
|
+
* - `bus.emit.done` → debug (warn if errors > 0)
|
|
47
|
+
* - `bus.emit.noop` → debug
|
|
48
|
+
* - `bus.handler.error` → warn
|
|
49
|
+
* - `bus.handler.async.enqueued` → debug
|
|
50
|
+
*/
|
|
51
|
+
export function attachLogObserver(lifecycleBus: EventBus<BusLifecycleEvents>): void {
|
|
52
|
+
lifecycleBus.on('bus.emit.done', (detail) => {
|
|
53
|
+
if (detail.errors > 0) {
|
|
54
|
+
obsLogger().warn('emit completed with errors', { ...detail });
|
|
55
|
+
} else {
|
|
56
|
+
obsLogger().debug('emit done', { ...detail });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
lifecycleBus.on('bus.emit.noop', (detail) => {
|
|
61
|
+
obsLogger().debug('emit with zero handlers', { ...detail });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
lifecycleBus.on('bus.handler.error', (detail) => {
|
|
65
|
+
obsLogger().warn('handler error', { ...detail });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
lifecycleBus.on('bus.handler.async.enqueued', (detail) => {
|
|
69
|
+
obsLogger().debug('async job enqueued', { ...detail });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Register handlers that create OpenTelemetry spans for emit operations.
|
|
75
|
+
*
|
|
76
|
+
* Each `bus.emit.done` creates a span named `eventbus.emit.{EVENT}` with
|
|
77
|
+
* attributes for sync/async counts, duration, and error count. Degrades
|
|
78
|
+
* gracefully when telemetry is not initialised (`traceAsync` is a pass-through).
|
|
79
|
+
*/
|
|
80
|
+
export function attachTelemetryObserver(lifecycleBus: EventBus<BusLifecycleEvents>): void {
|
|
81
|
+
lifecycleBus.on('bus.emit.done', (detail) => {
|
|
82
|
+
void traceAsync(`eventbus.emit.${detail.event}`, async (_span) => {
|
|
83
|
+
addSpanAttributes({
|
|
84
|
+
'eventbus.event': detail.event,
|
|
85
|
+
'eventbus.sync_count': detail.syncCount,
|
|
86
|
+
'eventbus.async_count': detail.asyncCount,
|
|
87
|
+
'eventbus.duration_ms': detail.emitDurationMs,
|
|
88
|
+
'eventbus.errors': detail.errors,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (detail.errors > 0) {
|
|
92
|
+
addSpanEvent('eventbus.emit.errors', { 'eventbus.error_count': detail.errors });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
lifecycleBus.on('bus.handler.error', (detail) => {
|
|
98
|
+
void traceAsync('eventbus.handler.error', async (span) => {
|
|
99
|
+
addSpanAttributes({
|
|
100
|
+
'eventbus.event': detail.event,
|
|
101
|
+
'eventbus.handler_mode': detail.mode,
|
|
102
|
+
'eventbus.error': detail.error,
|
|
103
|
+
});
|
|
104
|
+
span.setStatus({ code: 2, message: detail.error }); // ERROR
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Attach the built-in observers (log + telemetry) to the lifecycle bus in one
|
|
111
|
+
* call. Recommended default for production deployments. Metrics are emitted by
|
|
112
|
+
* the `EventBus` core inline, so they are not part of this set.
|
|
113
|
+
*/
|
|
114
|
+
export function attachDefaultObservers(lifecycleBus: EventBus<BusLifecycleEvents>): void {
|
|
115
|
+
attachLogObserver(lifecycleBus);
|
|
116
|
+
attachTelemetryObserver(lifecycleBus);
|
|
117
|
+
}
|
|
@@ -118,6 +118,7 @@ export class EventBus<TEvents extends EventMap> {
|
|
|
118
118
|
});
|
|
119
119
|
busLogger().debug('async job enqueued', { event: eventName, jobId, handlerCount: 1 });
|
|
120
120
|
this.publishAsyncEnqueued(eventName, jobId, 1);
|
|
121
|
+
continue;
|
|
121
122
|
} catch (error) {
|
|
122
123
|
errors++;
|
|
123
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getLogger } from '../logger';
|
|
2
|
+
import type { EventBus } from './event-bus';
|
|
3
|
+
import type { BusLifecycleEvents } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal append-only file writer the file observer needs.
|
|
7
|
+
*
|
|
8
|
+
* ADR-011: ts-infra must not import `node:fs`. The caller injects a writer —
|
|
9
|
+
* typically `@gobing-ai/ts-runtime`'s `FileSystem`, which already satisfies
|
|
10
|
+
* this shape (`ensureDir` + `appendFile`).
|
|
11
|
+
*/
|
|
12
|
+
export interface FileObserverWriter {
|
|
13
|
+
/** Ensure the directory exists. Called once with the parent dir of the file. */
|
|
14
|
+
ensureDir(dir: string): void | Promise<void>;
|
|
15
|
+
/** Append `content` to `path`, creating the file if absent. */
|
|
16
|
+
appendFile(path: string, content: string): void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parent directory of a `/`-separated path (no `node:path` dependency). */
|
|
20
|
+
function parentDir(filePath: string): string {
|
|
21
|
+
const idx = filePath.lastIndexOf('/');
|
|
22
|
+
return idx <= 0 ? '.' : filePath.slice(0, idx);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
26
|
+
return value !== null && typeof value === 'object' && 'then' in value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register handlers on `lifecycleBus` that append structured JSON lines to
|
|
31
|
+
* `filePath` for every lifecycle event.
|
|
32
|
+
*
|
|
33
|
+
* @param lifecycleBus - The bus to observe.
|
|
34
|
+
* @param filePath - Path to the JSONL output file.
|
|
35
|
+
* @param writer - Injected file writer (e.g. ts-runtime `FileSystem`).
|
|
36
|
+
*/
|
|
37
|
+
export function attachFileObserver(
|
|
38
|
+
lifecycleBus: EventBus<BusLifecycleEvents>,
|
|
39
|
+
filePath: string,
|
|
40
|
+
writer: FileObserverWriter,
|
|
41
|
+
): void {
|
|
42
|
+
const logger = getLogger('event-bus.file-observer');
|
|
43
|
+
const ensureResult = writer.ensureDir(parentDir(filePath));
|
|
44
|
+
let ready = !isPromiseLike(ensureResult);
|
|
45
|
+
let pending = isPromiseLike(ensureResult);
|
|
46
|
+
let setupFailed = false;
|
|
47
|
+
let sequence = 0;
|
|
48
|
+
let writeChain: Promise<unknown> = isPromiseLike(ensureResult)
|
|
49
|
+
? Promise.resolve(ensureResult)
|
|
50
|
+
.then(() => {
|
|
51
|
+
ready = true;
|
|
52
|
+
})
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
setupFailed = true;
|
|
55
|
+
logger.warn('failed to prepare event bus file observer directory', {
|
|
56
|
+
filePath,
|
|
57
|
+
error: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
})
|
|
60
|
+
: Promise.resolve();
|
|
61
|
+
|
|
62
|
+
function setPending(next: Promise<unknown>): void {
|
|
63
|
+
const current = ++sequence;
|
|
64
|
+
pending = true;
|
|
65
|
+
writeChain = next
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
logger.warn('failed to append event bus lifecycle event', {
|
|
68
|
+
filePath,
|
|
69
|
+
error: error instanceof Error ? error.message : String(error),
|
|
70
|
+
});
|
|
71
|
+
})
|
|
72
|
+
.finally(() => {
|
|
73
|
+
if (sequence === current) pending = false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeLine(line: Record<string, unknown>): void {
|
|
78
|
+
const content = `${JSON.stringify(line)}\n`;
|
|
79
|
+
if (setupFailed) return;
|
|
80
|
+
|
|
81
|
+
if (ready && !pending) {
|
|
82
|
+
const result = writer.appendFile(filePath, content);
|
|
83
|
+
if (isPromiseLike(result)) {
|
|
84
|
+
setPending(Promise.resolve(result));
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setPending(
|
|
90
|
+
writeChain.then(() => {
|
|
91
|
+
if (setupFailed) return;
|
|
92
|
+
return writer.appendFile(filePath, content);
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lifecycleBus.on('bus.emit.done', (d) => {
|
|
98
|
+
const payload =
|
|
99
|
+
d.detail !== null && typeof d.detail === 'object' && !Array.isArray(d.detail)
|
|
100
|
+
? (d.detail as Record<string, unknown>)
|
|
101
|
+
: undefined;
|
|
102
|
+
|
|
103
|
+
writeLine({
|
|
104
|
+
ts: new Date().toISOString(),
|
|
105
|
+
lifecycle: 'bus.emit.done',
|
|
106
|
+
event: d.event,
|
|
107
|
+
syncCount: d.syncCount,
|
|
108
|
+
asyncCount: d.asyncCount,
|
|
109
|
+
emitDurationMs: d.emitDurationMs,
|
|
110
|
+
errors: d.errors,
|
|
111
|
+
...(payload && Object.keys(payload).length > 0 ? { payload } : {}),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
lifecycleBus.on('bus.emit.noop', (d) => {
|
|
116
|
+
writeLine({
|
|
117
|
+
ts: new Date().toISOString(),
|
|
118
|
+
lifecycle: 'bus.emit.noop',
|
|
119
|
+
event: d.event,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
lifecycleBus.on('bus.handler.error', (d) => {
|
|
124
|
+
writeLine({
|
|
125
|
+
ts: new Date().toISOString(),
|
|
126
|
+
lifecycle: 'bus.handler.error',
|
|
127
|
+
event: d.event,
|
|
128
|
+
mode: d.mode,
|
|
129
|
+
error: d.error,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
lifecycleBus.on('bus.handler.async.enqueued', (d) => {
|
|
134
|
+
writeLine({
|
|
135
|
+
ts: new Date().toISOString(),
|
|
136
|
+
lifecycle: 'bus.handler.async.enqueued',
|
|
137
|
+
event: d.event,
|
|
138
|
+
jobId: d.jobId,
|
|
139
|
+
handlerCount: d.handlerCount,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|