@gobing-ai/ts-infra 0.3.4 → 0.3.6
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 +196 -48
- package/dist/application/index.d.ts +55 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +173 -0
- package/dist/application/plugins/builtins.d.ts +64 -0
- package/dist/application/plugins/builtins.d.ts.map +1 -0
- package/dist/application/plugins/builtins.js +146 -0
- package/dist/application/plugins/host.d.ts +61 -0
- package/dist/application/plugins/host.d.ts.map +1 -0
- package/dist/application/plugins/host.js +131 -0
- package/dist/application/plugins/index.d.ts +3 -0
- package/dist/application/plugins/index.d.ts.map +1 -0
- package/dist/application/plugins/index.js +2 -0
- package/dist/application/plugins/types.d.ts +69 -0
- package/dist/application/plugins/types.d.ts.map +1 -0
- package/dist/application/plugins/types.js +10 -0
- package/dist/application/types.d.ts +195 -0
- package/dist/application/types.d.ts.map +1 -0
- package/dist/application/types.js +9 -0
- package/dist/application-node.d.ts +68 -0
- package/dist/application-node.d.ts.map +1 -0
- package/dist/application-node.js +228 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +9 -2
- package/dist/scheduler/factory.d.ts +4 -8
- package/dist/scheduler/factory.d.ts.map +1 -1
- package/dist/scheduler/factory.js +12 -22
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/wrap-handler.d.ts +8 -4
- package/dist/scheduler/wrap-handler.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.js +8 -4
- package/dist/telemetry/index.d.ts +1 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +1 -2
- package/dist/telemetry/metrics.d.ts +9 -1
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +22 -1
- package/dist/telemetry/sdk.d.ts +33 -1
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +14 -1
- package/package.json +16 -3
- package/src/application/index.ts +248 -0
- package/src/application/plugins/builtins.ts +178 -0
- package/src/application/plugins/host.ts +143 -0
- package/src/application/plugins/index.ts +3 -0
- package/src/application/plugins/types.ts +86 -0
- package/src/application/types.ts +210 -0
- package/src/application-node.ts +311 -0
- package/src/index.ts +0 -2
- package/src/scheduler/cloudflare.ts +16 -5
- package/src/scheduler/factory.ts +15 -26
- package/src/scheduler/index.ts +1 -1
- package/src/scheduler/wrap-handler.ts +8 -4
- package/src/telemetry/index.ts +9 -2
- package/src/telemetry/metrics.ts +22 -1
- package/src/telemetry/sdk.ts +51 -2
- package/dist/telemetry/config.d.ts +0 -41
- package/dist/telemetry/config.d.ts.map +0 -1
- package/dist/telemetry/config.js +0 -21
- package/src/telemetry/config.ts +0 -59
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node/Bun convenience bootstrap for `@gobing-ai/ts-infra`.
|
|
3
|
+
*
|
|
4
|
+
* Composes the portable `runApplication` with runtime-specific wiring:
|
|
5
|
+
* - YAML config loading via `@gobing-ai/ts-runtime`
|
|
6
|
+
* - Application-specific config validation with caller-provided validator
|
|
7
|
+
* - File log sink via `node:fs`
|
|
8
|
+
* - Bun SQLite DB adapter creation from config
|
|
9
|
+
* - Optional Node OTel exporter initialization
|
|
10
|
+
* - Optional Node scheduler adapter
|
|
11
|
+
*
|
|
12
|
+
* This subpath may import runtime-specific adapters. The portable bootstrap
|
|
13
|
+
* subpath (`@gobing-ai/ts-infra/application`) must not.
|
|
14
|
+
*
|
|
15
|
+
* @module application-node
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { createDbAdapter, type DbAdapter } from '@gobing-ai/ts-db';
|
|
22
|
+
import { interpolateTree, parseYamlObject } from '@gobing-ai/ts-runtime';
|
|
23
|
+
|
|
24
|
+
import { runApplication } from './application/index';
|
|
25
|
+
import { dbPlugin } from './application/plugins/builtins';
|
|
26
|
+
import type { Plugin } from './application/plugins/types';
|
|
27
|
+
import type {
|
|
28
|
+
ApplicationBootstrapOptions,
|
|
29
|
+
ApplicationConfigLoader,
|
|
30
|
+
ApplicationConfigValidator,
|
|
31
|
+
ApplicationRuntime,
|
|
32
|
+
ApplicationStopReason,
|
|
33
|
+
ConfigValidationResult,
|
|
34
|
+
DbAdapterLike,
|
|
35
|
+
EventMap,
|
|
36
|
+
InfraEvents,
|
|
37
|
+
LoggingOptions,
|
|
38
|
+
SchedulerOptions,
|
|
39
|
+
TelemetryOptions,
|
|
40
|
+
} from './application/types';
|
|
41
|
+
import { NodeSchedulerAdapter } from './scheduler-node';
|
|
42
|
+
import { initNodeTelemetry, shutdownNodeTelemetry } from './telemetry/otel-node';
|
|
43
|
+
|
|
44
|
+
// ── Errors ────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** Error thrown when application config validation fails. Includes file path and section name. */
|
|
47
|
+
export class ConfigValidationError extends Error {
|
|
48
|
+
constructor(message: string) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'ConfigValidationError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Config validation helper ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate a raw config section using the caller-provided validator.
|
|
58
|
+
* Supports structural `safeParse`, `validate()`, `parse()`, and bare function forms.
|
|
59
|
+
*/
|
|
60
|
+
function validateAppConfig<TAppConfig>(
|
|
61
|
+
validator: ApplicationConfigValidator<TAppConfig>,
|
|
62
|
+
raw: unknown,
|
|
63
|
+
section: string,
|
|
64
|
+
filePath: string | undefined,
|
|
65
|
+
): TAppConfig {
|
|
66
|
+
if (typeof validator === 'object' && 'safeParse' in validator) {
|
|
67
|
+
const result: ConfigValidationResult<TAppConfig> = validator.safeParse(raw);
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
const details = result.errors?.map((e) => `${e.path}: ${e.message}`).join('; ') ?? 'unknown error';
|
|
70
|
+
throw new ConfigValidationError(
|
|
71
|
+
`Application config validation failed in section "${section}"` +
|
|
72
|
+
(filePath ? ` (file: ${filePath})` : '') +
|
|
73
|
+
`: ${details}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return result.data as TAppConfig;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof validator === 'function') {
|
|
80
|
+
return (validator as (raw: unknown) => TAppConfig)(raw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof validator === 'object' && 'validate' in validator) {
|
|
84
|
+
return validator.validate(raw);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof validator === 'object' && 'parse' in validator) {
|
|
88
|
+
return validator.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new ConfigValidationError(`Unsupported validator shape for section "${section}"`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── File sink helper ──────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function createFileSink(filePath: string): (line: string) => void {
|
|
97
|
+
return (line: string) => {
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
100
|
+
appendFileSync(filePath, line);
|
|
101
|
+
} catch {
|
|
102
|
+
// Best-effort — don't crash on log write failure
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Config loading ────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
interface LoadedConfig<TAppConfig> {
|
|
110
|
+
bootstrapConfig: Record<string, unknown>;
|
|
111
|
+
appConfig: TAppConfig | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadYamlConfig<TAppConfig>(
|
|
115
|
+
loader: ApplicationConfigLoader<TAppConfig>,
|
|
116
|
+
fileContent: string | undefined,
|
|
117
|
+
): LoadedConfig<TAppConfig> {
|
|
118
|
+
const bootstrapSection = loader.bootstrapSection ?? 'bootstrap';
|
|
119
|
+
|
|
120
|
+
let raw: Record<string, unknown>;
|
|
121
|
+
if (fileContent !== undefined) {
|
|
122
|
+
raw = parseYamlObject(fileContent);
|
|
123
|
+
} else if (loader.overrides) {
|
|
124
|
+
raw = { ...loader.overrides };
|
|
125
|
+
} else {
|
|
126
|
+
raw = {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Interpolate environment variables (Node/Bun only)
|
|
130
|
+
raw = interpolateTree(raw) as Record<string, unknown>;
|
|
131
|
+
|
|
132
|
+
// Extract bootstrap section
|
|
133
|
+
const bootstrapSection_ = raw[bootstrapSection];
|
|
134
|
+
const bootstrapConfig: Record<string, unknown> =
|
|
135
|
+
typeof bootstrapSection_ === 'object' && bootstrapSection_ !== null
|
|
136
|
+
? (bootstrapSection_ as Record<string, unknown>)
|
|
137
|
+
: {};
|
|
138
|
+
|
|
139
|
+
// Extract app config section
|
|
140
|
+
let appConfig: TAppConfig | undefined;
|
|
141
|
+
if (loader.appConfig) {
|
|
142
|
+
const appSection = loader.appSection;
|
|
143
|
+
const appRaw = appSection
|
|
144
|
+
? raw[appSection]
|
|
145
|
+
: (() => {
|
|
146
|
+
// Default: full remaining object minus bootstrap section
|
|
147
|
+
const remaining: Record<string, unknown> = {};
|
|
148
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
149
|
+
if (key !== bootstrapSection) {
|
|
150
|
+
remaining[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return remaining;
|
|
154
|
+
})();
|
|
155
|
+
|
|
156
|
+
appConfig = validateAppConfig(loader.appConfig, appRaw, appSection ?? '*', loader.configFile);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { bootstrapConfig, appConfig };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Node/Bun options ──────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/** Options for the Node/Bun convenience {@link runNodeApplication}. */
|
|
165
|
+
export interface NodeApplicationOptions<TAppConfig = unknown, TEvents extends EventMap = InfraEvents> {
|
|
166
|
+
/** YAML config loading options. When omitted, uses defaults. */
|
|
167
|
+
readonly configLoader?: ApplicationConfigLoader<TAppConfig>;
|
|
168
|
+
/** Inline bootstrap config (overrides YAML-loaded config). */
|
|
169
|
+
readonly config?: ApplicationBootstrapOptions<TAppConfig, TEvents>['config'];
|
|
170
|
+
/** Pre-built services to inject. */
|
|
171
|
+
readonly services?: ApplicationBootstrapOptions<TAppConfig, TEvents>['services'];
|
|
172
|
+
/** User callback: application logic. */
|
|
173
|
+
readonly start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<void> | void;
|
|
174
|
+
/** User callback: cleanup before services shut down. */
|
|
175
|
+
readonly stop?: (
|
|
176
|
+
app: ApplicationRuntime<TAppConfig, TEvents>,
|
|
177
|
+
reason: ApplicationStopReason,
|
|
178
|
+
) => Promise<void> | void;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Node/Bun convenience application bootstrap.
|
|
185
|
+
*
|
|
186
|
+
* Extends the portable `runApplication` with:
|
|
187
|
+
* - YAML config file loading with section splitting
|
|
188
|
+
* - Application-specific config validation
|
|
189
|
+
* - File log sink creation from `logging.filePath`
|
|
190
|
+
* - Bun SQLite DB adapter creation when `database.enabled` + `database.driver`
|
|
191
|
+
* - Optional Node OTel telemetry exporter
|
|
192
|
+
* - Optional Node scheduler adapter
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* import { runNodeApplication } from '@gobing-ai/ts-infra/application-node';
|
|
197
|
+
*
|
|
198
|
+
* await runNodeApplication({
|
|
199
|
+
* configLoader: {
|
|
200
|
+
* configFile: 'config/app.yaml',
|
|
201
|
+
* bootstrapSection: 'bootstrap',
|
|
202
|
+
* appSection: 'billing',
|
|
203
|
+
* appConfig: {
|
|
204
|
+
* safeParse(raw) {
|
|
205
|
+
* return billingSchema.safeParse(raw);
|
|
206
|
+
* },
|
|
207
|
+
* },
|
|
208
|
+
* },
|
|
209
|
+
* async start(app) {
|
|
210
|
+
* app.logger.info('started');
|
|
211
|
+
* },
|
|
212
|
+
* });
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export async function runNodeApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(
|
|
216
|
+
options: NodeApplicationOptions<TAppConfig, TEvents>,
|
|
217
|
+
): Promise<ApplicationRuntime<TAppConfig, TEvents>> {
|
|
218
|
+
// ── Load config ─────────────────────────────────────────────────────
|
|
219
|
+
let loadedAppConfig: TAppConfig | undefined;
|
|
220
|
+
let yamlBootstrap: Record<string, unknown> = {};
|
|
221
|
+
|
|
222
|
+
if (options.configLoader?.configFile) {
|
|
223
|
+
const fileContent = readFileSync(options.configLoader.configFile, 'utf-8');
|
|
224
|
+
const loaded = loadYamlConfig(options.configLoader, fileContent);
|
|
225
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
226
|
+
loadedAppConfig = loaded.appConfig;
|
|
227
|
+
} else if (options.configLoader) {
|
|
228
|
+
const loaded = loadYamlConfig(options.configLoader, undefined);
|
|
229
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
230
|
+
loadedAppConfig = loaded.appConfig;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Resolve bootstrap config from YAML + inline options ────────────
|
|
234
|
+
const yamlLog = yamlBootstrap.logging as Partial<LoggingOptions> | undefined;
|
|
235
|
+
const yamlTel = yamlBootstrap.telemetry as Partial<TelemetryOptions> | undefined;
|
|
236
|
+
const yamlSched = yamlBootstrap.scheduler as Partial<SchedulerOptions> | undefined;
|
|
237
|
+
const databaseOpts = (yamlBootstrap.database ?? {}) as Record<string, unknown>;
|
|
238
|
+
|
|
239
|
+
const loggingOpts: Partial<LoggingOptions> = { ...yamlLog, ...options.config?.logging };
|
|
240
|
+
const telemetryOpts: Partial<TelemetryOptions> = { ...yamlTel, ...options.config?.telemetry };
|
|
241
|
+
const schedulerOpts: Partial<SchedulerOptions> = { ...yamlSched, ...options.config?.scheduler };
|
|
242
|
+
|
|
243
|
+
const logFilePath = (yamlBootstrap.logging as Record<string, unknown> | undefined)?.filePath as string | undefined;
|
|
244
|
+
const loggingConfig: Partial<LoggingOptions> =
|
|
245
|
+
typeof logFilePath === 'string' ? { ...loggingOpts, fileSink: createFileSink(logFilePath) } : loggingOpts;
|
|
246
|
+
|
|
247
|
+
// ── Scheduler adapter ───────────────────────────────────────────────
|
|
248
|
+
const schedulerConfig: SchedulerOptions = {};
|
|
249
|
+
const rawSched = { ...schedulerOpts } as Record<string, unknown>;
|
|
250
|
+
if (rawSched.enabled === true) {
|
|
251
|
+
schedulerConfig.enabled = true;
|
|
252
|
+
schedulerConfig.autoStart = schedulerOpts.autoStart;
|
|
253
|
+
schedulerConfig.adapter = new NodeSchedulerAdapter();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Node-owned plugins ──────────────────────────────────────────────
|
|
257
|
+
const plugins: Plugin[] = [];
|
|
258
|
+
let dbAdapter: DbAdapterLike | undefined = options.services?.db;
|
|
259
|
+
const rawTel = { ...telemetryOpts } as Record<string, unknown>;
|
|
260
|
+
|
|
261
|
+
// Node OTel telemetry as a failFast plugin
|
|
262
|
+
if (rawTel.enabled !== false && rawTel.endpoint) {
|
|
263
|
+
plugins.push({
|
|
264
|
+
name: 'builtin:node-telemetry',
|
|
265
|
+
version: '0.0.0',
|
|
266
|
+
failFast: true,
|
|
267
|
+
onLoad: async () => {},
|
|
268
|
+
onStart: async () => {
|
|
269
|
+
initNodeTelemetry({
|
|
270
|
+
serviceName: (rawTel.serviceName as string | undefined) ?? 'ts-libs',
|
|
271
|
+
endpoint: rawTel.endpoint as string,
|
|
272
|
+
headers: rawTel.headers as Record<string, string> | undefined,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
onStop: async () => {
|
|
276
|
+
await shutdownNodeTelemetry();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// DB adapter (owned — registered as a plugin with fail-soft close)
|
|
282
|
+
if (!dbAdapter && databaseOpts.enabled === true) {
|
|
283
|
+
const driver = databaseOpts.driver as string | undefined;
|
|
284
|
+
if (driver === 'bun-sqlite') {
|
|
285
|
+
const adapter = await createDbAdapter({
|
|
286
|
+
driver: 'bun-sqlite',
|
|
287
|
+
url: databaseOpts.url as string | undefined,
|
|
288
|
+
});
|
|
289
|
+
dbAdapter = adapter as DbAdapter;
|
|
290
|
+
plugins.push(dbPlugin(dbAdapter));
|
|
291
|
+
} else {
|
|
292
|
+
throw new ConfigValidationError(
|
|
293
|
+
`database.enabled is true but driver ${driver ? `"${driver}"` : 'is missing'} is not supported ` +
|
|
294
|
+
`(expected "bun-sqlite"). Provide a supported driver or inject a DbAdapter via services.db.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Delegate to portable runApplication ─────────────────────────────
|
|
300
|
+
// Node-specific cleanup is handled by plugins in the service ring —
|
|
301
|
+
// node-telemetry onStop, owned-db onStop. No manual try/catch or stop
|
|
302
|
+
// override needed.
|
|
303
|
+
return await runApplication<TAppConfig, TEvents>({
|
|
304
|
+
config: { ...options.config, logging: loggingConfig, telemetry: telemetryOpts, scheduler: schedulerConfig },
|
|
305
|
+
appConfig: loadedAppConfig,
|
|
306
|
+
services: { ...options.services, ...(dbAdapter ? { db: dbAdapter } : {}) },
|
|
307
|
+
start: options.start,
|
|
308
|
+
stop: options.stop,
|
|
309
|
+
plugins: plugins.length ? plugins : undefined,
|
|
310
|
+
});
|
|
311
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -52,7 +52,6 @@ export {
|
|
|
52
52
|
ActionRegistry,
|
|
53
53
|
type CreateDefaultRegistryOptions,
|
|
54
54
|
createDefaultRegistry,
|
|
55
|
-
getSchedulerAdapter,
|
|
56
55
|
HealthPingAction,
|
|
57
56
|
type HealthPingWriter,
|
|
58
57
|
initScheduler,
|
|
@@ -63,7 +62,6 @@ export {
|
|
|
63
62
|
type ScheduledAction,
|
|
64
63
|
type SchedulerAction,
|
|
65
64
|
type SchedulerAdapter,
|
|
66
|
-
setSchedulerAdapter,
|
|
67
65
|
toScheduledAction,
|
|
68
66
|
wrapScheduledHandler,
|
|
69
67
|
} from './scheduler/index';
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* Cloudflare Workers scheduler adapter using Cron Triggers.
|
|
3
3
|
* Uses minimal local type declarations — no @cloudflare/workers-types dependency.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getSchedulerJobDuration,
|
|
7
|
+
getSchedulerJobExecutedTotal,
|
|
8
|
+
getSchedulerJobFailedTotal,
|
|
9
|
+
} from '../telemetry/metrics';
|
|
6
10
|
import type { ScheduledAction, SchedulerAdapter } from './types';
|
|
7
11
|
|
|
8
12
|
interface CfScheduledEvent {
|
|
@@ -46,12 +50,19 @@ export class CloudflareSchedulerAdapter implements SchedulerAdapter {
|
|
|
46
50
|
handleScheduledEvent(event: CfScheduledEvent, ctx: CfEventContext): void {
|
|
47
51
|
const action = this.entries.get(event.cron);
|
|
48
52
|
if (action) {
|
|
53
|
+
const startMs = performance.now();
|
|
49
54
|
getSchedulerJobExecutedTotal().add(1, { cron: event.cron });
|
|
50
55
|
ctx.waitUntil(
|
|
51
|
-
action()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
action()
|
|
57
|
+
.catch((error: unknown) => {
|
|
58
|
+
getSchedulerJobFailedTotal().add(1, { cron: event.cron });
|
|
59
|
+
throw error;
|
|
60
|
+
})
|
|
61
|
+
.finally(() => {
|
|
62
|
+
// Duration parity with NodeSchedulerAdapter — record the job
|
|
63
|
+
// duration metric keyed by cron for both runtimes.
|
|
64
|
+
getSchedulerJobDuration().record(performance.now() - startMs, { cron: event.cron });
|
|
65
|
+
}),
|
|
55
66
|
);
|
|
56
67
|
}
|
|
57
68
|
}
|
package/src/scheduler/factory.ts
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scheduler factory —
|
|
2
|
+
* Scheduler factory — initializes an adapter and registers cron entries.
|
|
3
|
+
*
|
|
4
|
+
* The adapter is passed in explicitly (dependency injection); there is no
|
|
5
|
+
* process-global adapter state. Callers that don't supply one get a
|
|
6
|
+
* {@link NoopSchedulerAdapter}.
|
|
3
7
|
*/
|
|
4
8
|
import { NoopSchedulerAdapter } from './noop';
|
|
5
9
|
import type { ScheduledAction, SchedulerAdapter } from './types';
|
|
6
10
|
|
|
7
|
-
let runtimeAdapter: SchedulerAdapter | undefined;
|
|
8
|
-
|
|
9
|
-
/** Set the runtime scheduler adapter. Call before {@link initScheduler}. */
|
|
10
|
-
export function setSchedulerAdapter(adapter: SchedulerAdapter): void {
|
|
11
|
-
runtimeAdapter = adapter;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Reset the scheduler adapter singleton. For testing. */
|
|
15
|
-
export function resetSchedulerAdapter(): void {
|
|
16
|
-
runtimeAdapter = undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Get the currently configured scheduler adapter, or `undefined` if not set. */
|
|
20
|
-
export function getSchedulerAdapter(): SchedulerAdapter | undefined {
|
|
21
|
-
return runtimeAdapter;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
11
|
/**
|
|
25
12
|
* Initialize the scheduler adapter and register cron entries.
|
|
26
13
|
*
|
|
@@ -28,19 +15,21 @@ export function getSchedulerAdapter(): SchedulerAdapter | undefined {
|
|
|
28
15
|
* running, newly registered entries will NOT be started until the next
|
|
29
16
|
* `start()` call.
|
|
30
17
|
*
|
|
31
|
-
*
|
|
18
|
+
* @param adapter - Adapter to use. Defaults to a {@link NoopSchedulerAdapter}.
|
|
19
|
+
* @param cronEntries - `[cron, action]` pairs to register on the adapter.
|
|
20
|
+
* @returns The configured adapter.
|
|
32
21
|
*/
|
|
33
|
-
export function initScheduler(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
export function initScheduler(
|
|
23
|
+
adapter?: SchedulerAdapter,
|
|
24
|
+
cronEntries?: Array<[string, ScheduledAction]>,
|
|
25
|
+
): SchedulerAdapter {
|
|
26
|
+
const resolved = adapter ?? new NoopSchedulerAdapter();
|
|
38
27
|
|
|
39
28
|
if (cronEntries) {
|
|
40
29
|
for (const [cron, action] of cronEntries) {
|
|
41
|
-
|
|
30
|
+
resolved.register(cron, action);
|
|
42
31
|
}
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
return
|
|
34
|
+
return resolved;
|
|
46
35
|
}
|
package/src/scheduler/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
type SchedulerAction,
|
|
11
11
|
toScheduledAction,
|
|
12
12
|
} from './action';
|
|
13
|
-
export {
|
|
13
|
+
export { initScheduler } from './factory';
|
|
14
14
|
export { NoopSchedulerAdapter } from './noop';
|
|
15
15
|
export type { ScheduledAction, SchedulerAdapter } from './types';
|
|
16
16
|
export { wrapScheduledHandler } from './wrap-handler';
|
|
@@ -7,10 +7,14 @@ import type { ScheduledAction } from './types';
|
|
|
7
7
|
* Wrap a scheduled action with OTel tracing, duration measurement, and
|
|
8
8
|
* `scheduler.job.executed` event emission.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Observability is split by design across two axes, not duplicated:
|
|
11
|
+
* - The adapters (`NodeSchedulerAdapter`, `CloudflareSchedulerAdapter`) record
|
|
12
|
+
* executed/failed/duration **metrics keyed by `cron`** for aggregate dashboards.
|
|
13
|
+
* - This opt-in wrapper adds a named **tracing span + lifecycle event keyed by
|
|
14
|
+
* the human `name`**, for per-job diagnosis. Its timer measures the inner action
|
|
15
|
+
* scope; the adapter's measures the full tick — nested, not double-counted.
|
|
16
|
+
*
|
|
17
|
+
* Wrap an action before registering it when you want the named span/event.
|
|
14
18
|
*
|
|
15
19
|
* @param name - Job name, surfaced as `scheduler.job_name` on the span/event.
|
|
16
20
|
* @param action - The action to wrap (new no-arg `ScheduledAction` signature).
|
package/src/telemetry/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { getTelemetryConfig, type TelemetryConfig, type TelemetryConfigPartial } from './config';
|
|
2
1
|
export { extractSqlOperation, sanitizeSql } from './db-sanitize';
|
|
3
2
|
export {
|
|
4
3
|
type Counter,
|
|
@@ -18,6 +17,14 @@ export {
|
|
|
18
17
|
initMetrics,
|
|
19
18
|
shutdownMetrics,
|
|
20
19
|
} from './metrics';
|
|
21
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
getTelemetryConfig,
|
|
22
|
+
getTracer,
|
|
23
|
+
initTelemetry,
|
|
24
|
+
isTelemetryEnabled,
|
|
25
|
+
shutdownTelemetry,
|
|
26
|
+
type TelemetryConfig,
|
|
27
|
+
type TelemetryConfigPartial,
|
|
28
|
+
} from './sdk';
|
|
22
29
|
export type { Span, SpanOptions, Tracer } from './tracing';
|
|
23
30
|
export { addSpanAttributes, addSpanEvent, getActiveSpan, traceAsync, traceSync, withSpan } from './tracing';
|
package/src/telemetry/metrics.ts
CHANGED
|
@@ -109,9 +109,30 @@ export function getSchedulerJobFailedTotal(): Counter {
|
|
|
109
109
|
|
|
110
110
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Pre-warm every instrument against the currently-registered meter and mark the
|
|
114
|
+
* subsystem initialized. Idempotent.
|
|
115
|
+
*
|
|
116
|
+
* Instruments are otherwise created lazily on first getter call (so metrics keep
|
|
117
|
+
* working even if this is never called — see the module contract). Calling this
|
|
118
|
+
* during bootstrap eagerly materializes them, so `isMetricsInitialized()` reflects
|
|
119
|
+
* real wiring rather than being a flag that gates nothing.
|
|
120
|
+
*/
|
|
113
121
|
export function initMetrics(): void {
|
|
114
122
|
if (metricsInitialized) return;
|
|
123
|
+
// Eagerly materialize all instruments against the live meter.
|
|
124
|
+
getHttpClientRequestTotal();
|
|
125
|
+
getHttpClientRequestDuration();
|
|
126
|
+
getHttpClientRequestErrors();
|
|
127
|
+
getEventbusEmitsTotal();
|
|
128
|
+
getEventbusErrorsTotal();
|
|
129
|
+
getQueueJobEnqueuedTotal();
|
|
130
|
+
getQueueJobCompletedTotal();
|
|
131
|
+
getQueueJobFailedTotal();
|
|
132
|
+
getQueueJobProcessingDuration();
|
|
133
|
+
getSchedulerJobExecutedTotal();
|
|
134
|
+
getSchedulerJobDuration();
|
|
135
|
+
getSchedulerJobFailedTotal();
|
|
115
136
|
metricsInitialized = true;
|
|
116
137
|
}
|
|
117
138
|
|
package/src/telemetry/sdk.ts
CHANGED
|
@@ -8,8 +8,57 @@
|
|
|
8
8
|
* keeps the main barrel free of any SDK runtime dependency.
|
|
9
9
|
*/
|
|
10
10
|
import { type Tracer, trace } from '@opentelemetry/api';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Full telemetry configuration: master enable switch, service name,
|
|
16
|
+
* environment, and debug-level DB statement capture.
|
|
17
|
+
*/
|
|
18
|
+
export interface TelemetryConfig {
|
|
19
|
+
/** Master switch — when false, all tracing degrades to no-ops. */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Logical service name emitted on every span. */
|
|
22
|
+
serviceName: string;
|
|
23
|
+
/** Deployment environment (development, staging, production). */
|
|
24
|
+
environment: string;
|
|
25
|
+
/**
|
|
26
|
+
* Debug-only DB statement capture.
|
|
27
|
+
*
|
|
28
|
+
* When true, DB spans may include sanitized SQL text in a `db.statement`
|
|
29
|
+
* attribute. SQL text is redacted — parameter values, literals, and
|
|
30
|
+
* identifiers are stripped before capture.
|
|
31
|
+
*
|
|
32
|
+
* Default: `false`. Controlled by `OTEL_DB_STATEMENT_DEBUG` env var.
|
|
33
|
+
*/
|
|
34
|
+
dbStatementDebug: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Partial telemetry config from the centralized config system. */
|
|
38
|
+
export interface TelemetryConfigPartial {
|
|
39
|
+
enabled?: boolean | undefined;
|
|
40
|
+
serviceName?: string | undefined;
|
|
41
|
+
environment?: string | undefined;
|
|
42
|
+
dbStatementDebug?: boolean | undefined;
|
|
43
|
+
/** Deployment environment fallback (from app.env). */
|
|
44
|
+
appEnv?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CONFIG_DEFAULTS = {
|
|
48
|
+
enabled: true as const,
|
|
49
|
+
serviceName: 'ts-libs' as const,
|
|
50
|
+
environment: 'development' as const,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Resolve the full telemetry config by merging a partial override with defaults. */
|
|
54
|
+
export function getTelemetryConfig(configPartial: TelemetryConfigPartial = {}): TelemetryConfig {
|
|
55
|
+
return {
|
|
56
|
+
enabled: configPartial.enabled ?? CONFIG_DEFAULTS.enabled,
|
|
57
|
+
serviceName: configPartial.serviceName ?? CONFIG_DEFAULTS.serviceName,
|
|
58
|
+
environment: configPartial.environment ?? configPartial.appEnv ?? CONFIG_DEFAULTS.environment,
|
|
59
|
+
dbStatementDebug: configPartial.dbStatementDebug ?? false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
13
62
|
|
|
14
63
|
const TRACER_NAME = '@gobing-ai/ts-infra';
|
|
15
64
|
const TRACER_VERSION = '0.1.0';
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telemetry configuration interface.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Full telemetry configuration: master enable switch, service name,
|
|
6
|
-
* environment, and debug-level DB statement capture.
|
|
7
|
-
*/
|
|
8
|
-
export interface TelemetryConfig {
|
|
9
|
-
/** Master switch — when false, all tracing degrades to no-ops. */
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
/** Logical service name emitted on every span. */
|
|
12
|
-
serviceName: string;
|
|
13
|
-
/** Deployment environment (development, staging, production). */
|
|
14
|
-
environment: string;
|
|
15
|
-
/**
|
|
16
|
-
* Debug-only DB statement capture.
|
|
17
|
-
*
|
|
18
|
-
* When true, DB spans may include sanitized SQL text in a `db.statement`
|
|
19
|
-
* attribute. SQL text is redacted — parameter values, literals, and
|
|
20
|
-
* identifiers are stripped before capture.
|
|
21
|
-
*
|
|
22
|
-
* Default: `false`. Controlled by `OTEL_DB_STATEMENT_DEBUG` env var.
|
|
23
|
-
*/
|
|
24
|
-
dbStatementDebug: boolean;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Partial telemetry config from the centralized config system.
|
|
28
|
-
*/
|
|
29
|
-
export interface TelemetryConfigPartial {
|
|
30
|
-
enabled?: boolean | undefined;
|
|
31
|
-
serviceName?: string | undefined;
|
|
32
|
-
environment?: string | undefined;
|
|
33
|
-
dbStatementDebug?: boolean | undefined;
|
|
34
|
-
/** Deployment environment fallback (from app.env). */
|
|
35
|
-
appEnv?: string | undefined;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Resolve the full telemetry config by merging a partial override with defaults.
|
|
39
|
-
*/
|
|
40
|
-
export declare function getTelemetryConfig(configPartial?: TelemetryConfigPartial): TelemetryConfig;
|
|
41
|
-
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/telemetry/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC5B,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAQD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,GAAE,sBAA2B,GAAG,eAAe,CAU9F"}
|
package/dist/telemetry/config.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telemetry configuration interface.
|
|
3
|
-
*/
|
|
4
|
-
const DEFAULTS = {
|
|
5
|
-
enabled: true,
|
|
6
|
-
serviceName: 'ts-libs',
|
|
7
|
-
environment: 'development',
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Resolve the full telemetry config by merging a partial override with defaults.
|
|
11
|
-
*/
|
|
12
|
-
export function getTelemetryConfig(configPartial = {}) {
|
|
13
|
-
const enabled = configPartial.enabled ?? DEFAULTS.enabled;
|
|
14
|
-
const serviceName = configPartial.serviceName ?? DEFAULTS.serviceName;
|
|
15
|
-
return {
|
|
16
|
-
enabled,
|
|
17
|
-
serviceName,
|
|
18
|
-
environment: configPartial.environment ?? configPartial.appEnv ?? DEFAULTS.environment,
|
|
19
|
-
dbStatementDebug: configPartial.dbStatementDebug ?? false,
|
|
20
|
-
};
|
|
21
|
-
}
|