@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.
Files changed (65) hide show
  1. package/README.md +196 -48
  2. package/dist/application/index.d.ts +55 -0
  3. package/dist/application/index.d.ts.map +1 -0
  4. package/dist/application/index.js +173 -0
  5. package/dist/application/plugins/builtins.d.ts +64 -0
  6. package/dist/application/plugins/builtins.d.ts.map +1 -0
  7. package/dist/application/plugins/builtins.js +146 -0
  8. package/dist/application/plugins/host.d.ts +61 -0
  9. package/dist/application/plugins/host.d.ts.map +1 -0
  10. package/dist/application/plugins/host.js +131 -0
  11. package/dist/application/plugins/index.d.ts +3 -0
  12. package/dist/application/plugins/index.d.ts.map +1 -0
  13. package/dist/application/plugins/index.js +2 -0
  14. package/dist/application/plugins/types.d.ts +69 -0
  15. package/dist/application/plugins/types.d.ts.map +1 -0
  16. package/dist/application/plugins/types.js +10 -0
  17. package/dist/application/types.d.ts +195 -0
  18. package/dist/application/types.d.ts.map +1 -0
  19. package/dist/application/types.js +9 -0
  20. package/dist/application-node.d.ts +68 -0
  21. package/dist/application-node.d.ts.map +1 -0
  22. package/dist/application-node.js +228 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/scheduler/cloudflare.d.ts.map +1 -1
  27. package/dist/scheduler/cloudflare.js +9 -2
  28. package/dist/scheduler/factory.d.ts +4 -8
  29. package/dist/scheduler/factory.d.ts.map +1 -1
  30. package/dist/scheduler/factory.js +12 -22
  31. package/dist/scheduler/index.d.ts +1 -1
  32. package/dist/scheduler/index.d.ts.map +1 -1
  33. package/dist/scheduler/index.js +1 -1
  34. package/dist/scheduler/wrap-handler.d.ts +8 -4
  35. package/dist/scheduler/wrap-handler.d.ts.map +1 -1
  36. package/dist/scheduler/wrap-handler.js +8 -4
  37. package/dist/telemetry/index.d.ts +1 -2
  38. package/dist/telemetry/index.d.ts.map +1 -1
  39. package/dist/telemetry/index.js +1 -2
  40. package/dist/telemetry/metrics.d.ts +9 -1
  41. package/dist/telemetry/metrics.d.ts.map +1 -1
  42. package/dist/telemetry/metrics.js +22 -1
  43. package/dist/telemetry/sdk.d.ts +33 -1
  44. package/dist/telemetry/sdk.d.ts.map +1 -1
  45. package/dist/telemetry/sdk.js +14 -1
  46. package/package.json +16 -3
  47. package/src/application/index.ts +248 -0
  48. package/src/application/plugins/builtins.ts +178 -0
  49. package/src/application/plugins/host.ts +143 -0
  50. package/src/application/plugins/index.ts +3 -0
  51. package/src/application/plugins/types.ts +86 -0
  52. package/src/application/types.ts +210 -0
  53. package/src/application-node.ts +311 -0
  54. package/src/index.ts +0 -2
  55. package/src/scheduler/cloudflare.ts +16 -5
  56. package/src/scheduler/factory.ts +15 -26
  57. package/src/scheduler/index.ts +1 -1
  58. package/src/scheduler/wrap-handler.ts +8 -4
  59. package/src/telemetry/index.ts +9 -2
  60. package/src/telemetry/metrics.ts +22 -1
  61. package/src/telemetry/sdk.ts +51 -2
  62. package/dist/telemetry/config.d.ts +0 -41
  63. package/dist/telemetry/config.d.ts.map +0 -1
  64. package/dist/telemetry/config.js +0 -21
  65. 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 { getSchedulerJobExecutedTotal, getSchedulerJobFailedTotal } from '../telemetry/metrics';
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().catch((error: unknown) => {
52
- getSchedulerJobFailedTotal().add(1, { cron: event.cron });
53
- throw error;
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
  }
@@ -1,26 +1,13 @@
1
1
  /**
2
- * Scheduler factory — selects adapter based on runtime.
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
- * Returns the configured adapter (defaults to noop if none set).
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(cronEntries?: Array<[string, ScheduledAction]>): SchedulerAdapter {
34
- // Default: create a noop adapter. Apps inject their own via setSchedulerAdapter.
35
- if (!runtimeAdapter) {
36
- runtimeAdapter = new NoopSchedulerAdapter();
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
- runtimeAdapter.register(cron, action);
30
+ resolved.register(cron, action);
42
31
  }
43
32
  }
44
33
 
45
- return runtimeAdapter;
34
+ return resolved;
46
35
  }
@@ -10,7 +10,7 @@ export {
10
10
  type SchedulerAction,
11
11
  toScheduledAction,
12
12
  } from './action';
13
- export { getSchedulerAdapter, initScheduler, resetSchedulerAdapter, setSchedulerAdapter } from './factory';
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
- * Composes *on top of* the adapter's inline metrics (executed/failed/duration
11
- * counters live in the Node/Cloudflare adapters) — this wrapper adds the named
12
- * tracing span and the lifecycle event, neither of which the adapters provide.
13
- * Opt-in: wrap an action before registering it when you want that visibility.
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).
@@ -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 { getTracer, initTelemetry, isTelemetryEnabled, shutdownTelemetry } from './sdk';
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';
@@ -109,9 +109,30 @@ export function getSchedulerJobFailedTotal(): Counter {
109
109
 
110
110
  // ── Lifecycle ───────────────────────────────────────────────────────
111
111
 
112
- /** Mark the metrics subsystem as initialized. Idempotent. */
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
 
@@ -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
- import type { TelemetryConfig } from './config';
12
- import { getTelemetryConfig } from './config';
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"}
@@ -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
- }