@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.
Files changed (101) hide show
  1. package/README.md +52 -29
  2. package/dist/api-client.d.ts +13 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +15 -4
  5. package/dist/event-bus/default-observers.d.ts +53 -0
  6. package/dist/event-bus/default-observers.d.ts.map +1 -0
  7. package/dist/event-bus/default-observers.js +107 -0
  8. package/dist/event-bus/event-bus.d.ts.map +1 -1
  9. package/dist/event-bus/event-bus.js +1 -0
  10. package/dist/event-bus/file-observer.d.ts +25 -0
  11. package/dist/event-bus/file-observer.d.ts.map +1 -0
  12. package/dist/event-bus/file-observer.js +110 -0
  13. package/dist/event-bus/index.d.ts +2 -0
  14. package/dist/event-bus/index.d.ts.map +1 -1
  15. package/dist/event-bus/index.js +2 -0
  16. package/dist/event-bus/types.d.ts +6 -0
  17. package/dist/event-bus/types.d.ts.map +1 -1
  18. package/dist/events.d.ts +100 -0
  19. package/dist/events.d.ts.map +1 -0
  20. package/dist/events.js +12 -0
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -5
  24. package/dist/job-queue/db-job-queue.d.ts.map +1 -1
  25. package/dist/job-queue/db-job-queue.js +45 -34
  26. package/dist/job-queue/index.d.ts +0 -1
  27. package/dist/job-queue/index.d.ts.map +1 -1
  28. package/dist/job-queue/index.js +0 -1
  29. package/dist/job-queue/types.d.ts +7 -0
  30. package/dist/job-queue/types.d.ts.map +1 -1
  31. package/dist/job-queue-db.d.ts +2 -0
  32. package/dist/job-queue-db.d.ts.map +1 -0
  33. package/dist/job-queue-db.js +1 -0
  34. package/dist/logger.d.ts +39 -7
  35. package/dist/logger.d.ts.map +1 -1
  36. package/dist/logger.js +76 -73
  37. package/dist/scheduler/action.d.ts +97 -1
  38. package/dist/scheduler/action.d.ts.map +1 -1
  39. package/dist/scheduler/action.js +111 -0
  40. package/dist/scheduler/cloudflare.d.ts +6 -0
  41. package/dist/scheduler/cloudflare.d.ts.map +1 -1
  42. package/dist/scheduler/cloudflare.js +6 -0
  43. package/dist/scheduler/factory.d.ts +2 -0
  44. package/dist/scheduler/factory.d.ts.map +1 -1
  45. package/dist/scheduler/factory.js +2 -0
  46. package/dist/scheduler/index.d.ts +2 -2
  47. package/dist/scheduler/index.d.ts.map +1 -1
  48. package/dist/scheduler/index.js +2 -2
  49. package/dist/scheduler/node.d.ts +4 -0
  50. package/dist/scheduler/node.d.ts.map +1 -1
  51. package/dist/scheduler/node.js +4 -0
  52. package/dist/scheduler/noop.d.ts +1 -0
  53. package/dist/scheduler/noop.d.ts.map +1 -1
  54. package/dist/scheduler/noop.js +1 -0
  55. package/dist/scheduler/wrap-handler.d.ts +18 -0
  56. package/dist/scheduler/wrap-handler.d.ts.map +1 -0
  57. package/dist/scheduler/wrap-handler.js +41 -0
  58. package/dist/scheduler-cloudflare.d.ts +2 -0
  59. package/dist/scheduler-cloudflare.d.ts.map +1 -0
  60. package/dist/scheduler-cloudflare.js +1 -0
  61. package/dist/scheduler-node.d.ts +2 -0
  62. package/dist/scheduler-node.d.ts.map +1 -0
  63. package/dist/scheduler-node.js +1 -0
  64. package/dist/telemetry/config.d.ts +4 -0
  65. package/dist/telemetry/config.d.ts.map +1 -1
  66. package/dist/telemetry/metrics.d.ts +19 -0
  67. package/dist/telemetry/metrics.d.ts.map +1 -1
  68. package/dist/telemetry/metrics.js +19 -0
  69. package/dist/telemetry/sdk.d.ts +4 -0
  70. package/dist/telemetry/sdk.d.ts.map +1 -1
  71. package/dist/telemetry/sdk.js +4 -0
  72. package/dist/telemetry/tracing.d.ts +12 -0
  73. package/dist/telemetry/tracing.d.ts.map +1 -1
  74. package/dist/telemetry/tracing.js +12 -0
  75. package/package.json +19 -2
  76. package/src/api-client.ts +15 -4
  77. package/src/event-bus/default-observers.ts +117 -0
  78. package/src/event-bus/event-bus.ts +1 -0
  79. package/src/event-bus/file-observer.ts +142 -0
  80. package/src/event-bus/index.ts +7 -0
  81. package/src/event-bus/types.ts +6 -0
  82. package/src/events.ts +108 -0
  83. package/src/index.ts +44 -7
  84. package/src/job-queue/db-job-queue.ts +50 -38
  85. package/src/job-queue/index.ts +0 -1
  86. package/src/job-queue/types.ts +7 -0
  87. package/src/job-queue-db.ts +1 -0
  88. package/src/logger.ts +102 -77
  89. package/src/scheduler/action.ts +164 -1
  90. package/src/scheduler/cloudflare.ts +6 -0
  91. package/src/scheduler/factory.ts +2 -0
  92. package/src/scheduler/index.ts +13 -2
  93. package/src/scheduler/node.ts +4 -0
  94. package/src/scheduler/noop.ts +1 -0
  95. package/src/scheduler/wrap-handler.ts +50 -0
  96. package/src/scheduler-cloudflare.ts +1 -0
  97. package/src/scheduler-node.ts +1 -0
  98. package/src/telemetry/config.ts +4 -0
  99. package/src/telemetry/metrics.ts +19 -0
  100. package/src/telemetry/sdk.ts +4 -0
  101. package/src/telemetry/tracing.ts +12 -0
@@ -1,4 +1,167 @@
1
1
  /**
2
- * Scheduler action type re-export.
2
+ * Named scheduler actions and a registry, plus built-in actions.
3
+ *
4
+ * Opt-in by design: `createDefaultRegistry` only auto-registers the side-effect-
5
+ * free `LogAction`. Actions with side effects (`QueueStatsAction` needs a DAO
6
+ * provider, `HealthPingAction` needs a file writer) are included only when their
7
+ * dependency is supplied — downstream code decides what to enable.
3
8
  */
9
+
10
+ import type { EventBus } from '../event-bus/event-bus';
11
+ import type { QueueEvents } from '../events';
12
+ import type { QueueStats } from '../job-queue/types';
13
+ import { getLogger } from '../logger';
14
+ import type { ScheduledAction } from './types';
15
+
16
+ const logger = getLogger('scheduler.action');
17
+
18
+ /** Lazily provides a DAO with `getStats()`, deferred to execution time. */
19
+ export type QueueStatsDaoProvider = () => Promise<{ getStats(): Promise<QueueStats> }>;
20
+
21
+ /**
22
+ * Minimal writer for {@link HealthPingAction}. ADR-011: ts-infra does not touch
23
+ * `node:fs`; the caller injects a writer (e.g. ts-runtime `FileSystem`).
24
+ */
25
+ export interface HealthPingWriter {
26
+ /** Write `content` to `path`, replacing any existing file. */
27
+ writeFile(path: string, content: string): void | Promise<void>;
28
+ }
29
+
30
+ /**
31
+ * A named action invokable by the scheduler. Registered in an
32
+ * {@link ActionRegistry} and resolved by name; `execute()` matches the
33
+ * adapter's no-arg {@link ScheduledAction} signature.
34
+ */
35
+ export interface SchedulerAction {
36
+ /** Unique action name registered in the registry. */
37
+ readonly name: string;
38
+ /** Execute the action. */
39
+ execute(): Promise<void>;
40
+ }
41
+
42
+ /** Bridge a {@link SchedulerAction} to a plain {@link ScheduledAction} for adapter registration. */
43
+ export function toScheduledAction(action: SchedulerAction): ScheduledAction {
44
+ return () => action.execute();
45
+ }
46
+
47
+ /**
48
+ * Registry of named {@link SchedulerAction} instances, resolved at scheduler
49
+ * startup. Duplicate names are rejected.
50
+ */
51
+ export class ActionRegistry {
52
+ private readonly map = new Map<string, SchedulerAction>();
53
+
54
+ constructor(actions?: SchedulerAction[]) {
55
+ if (actions) {
56
+ for (const action of actions) this.register(action);
57
+ }
58
+ }
59
+
60
+ /** Register an action. @throws if the name is already registered. */
61
+ register(action: SchedulerAction): void {
62
+ if (this.map.has(action.name)) {
63
+ throw new Error(`Duplicate scheduler action name: "${action.name}"`);
64
+ }
65
+ this.map.set(action.name, action);
66
+ }
67
+
68
+ /** Look up an action by name. */
69
+ get(name: string): SchedulerAction | undefined {
70
+ return this.map.get(name);
71
+ }
72
+
73
+ /** Whether an action with the given name exists. */
74
+ has(name: string): boolean {
75
+ return this.map.has(name);
76
+ }
77
+
78
+ /** All registered action names. */
79
+ names(): string[] {
80
+ return [...this.map.keys()];
81
+ }
82
+ }
83
+
84
+ /** Logs a structured info message on every invocation. Registered as `"log"`. */
85
+ export class LogAction implements SchedulerAction {
86
+ readonly name: string;
87
+
88
+ constructor(name = 'log') {
89
+ this.name = name;
90
+ }
91
+
92
+ async execute(): Promise<void> {
93
+ logger.info('Scheduled action executed', { action: this.name });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Queries the queue DAO for stats and logs them; emits `queue.stats` when a
99
+ * system bus is provided. Registered as `"queue-stats"`.
100
+ */
101
+ export class QueueStatsAction implements SchedulerAction {
102
+ readonly name = 'queue-stats';
103
+
104
+ constructor(
105
+ private readonly daoProvider: QueueStatsDaoProvider,
106
+ private readonly systemBus: EventBus<QueueEvents> | null = null,
107
+ ) {}
108
+
109
+ async execute(): Promise<void> {
110
+ const dao = await this.daoProvider();
111
+ const stats = await dao.getStats();
112
+ logger.info('Queue stats snapshot', { action: this.name, ...stats });
113
+ this.systemBus?.emit('queue.stats', stats);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Writes a timestamped heartbeat file on every invocation. Registered as
119
+ * `"health-ping"`. File I/O is delegated to an injected {@link HealthPingWriter}.
120
+ */
121
+ export class HealthPingAction implements SchedulerAction {
122
+ readonly name = 'health-ping';
123
+
124
+ constructor(
125
+ private readonly writer: HealthPingWriter,
126
+ private readonly filePath = 'data/scheduler-heartbeat.json',
127
+ ) {}
128
+
129
+ async execute(): Promise<void> {
130
+ const content = JSON.stringify({
131
+ lastRun: new Date().toISOString(),
132
+ jobName: this.name,
133
+ });
134
+ await this.writer.writeFile(this.filePath, content);
135
+ }
136
+ }
137
+
138
+ /** Options for {@link createDefaultRegistry}. */
139
+ export interface CreateDefaultRegistryOptions {
140
+ /** DAO provider for `QueueStatsAction`. Omit to exclude that action. */
141
+ queueStatsDaoProvider?: QueueStatsDaoProvider;
142
+ /** Writer for `HealthPingAction`. Omit to exclude that action. */
143
+ healthPingWriter?: HealthPingWriter;
144
+ /** File path for `HealthPingAction`. Default `"data/scheduler-heartbeat.json"`. */
145
+ healthPingPath?: string;
146
+ /** System bus forwarded to `QueueStatsAction` for `queue.stats` events. */
147
+ systemBus?: EventBus<QueueEvents> | null;
148
+ }
149
+
150
+ /**
151
+ * Create an {@link ActionRegistry} with the built-in actions. Only `LogAction`
152
+ * is always present (no side effects); `QueueStatsAction` and `HealthPingAction`
153
+ * are included only when their dependency (`queueStatsDaoProvider` /
154
+ * `healthPingWriter`) is supplied — so nothing with side effects is auto-wired.
155
+ */
156
+ export function createDefaultRegistry(options: CreateDefaultRegistryOptions = {}): ActionRegistry {
157
+ const actions: SchedulerAction[] = [new LogAction()];
158
+ if (options.queueStatsDaoProvider) {
159
+ actions.push(new QueueStatsAction(options.queueStatsDaoProvider, options.systemBus ?? null));
160
+ }
161
+ if (options.healthPingWriter) {
162
+ actions.push(new HealthPingAction(options.healthPingWriter, options.healthPingPath));
163
+ }
164
+ return new ActionRegistry(actions);
165
+ }
166
+
4
167
  export type { ScheduledAction } from './types';
@@ -15,6 +15,12 @@ interface CfEventContext {
15
15
  waitUntil(promise: Promise<unknown>): void;
16
16
  }
17
17
 
18
+ /**
19
+ * Scheduler adapter for Cloudflare Workers Cron Triggers.
20
+ *
21
+ * Register cron→action pairs, then call {@link CloudflareSchedulerAdapter.handleScheduledEvent}
22
+ * from the Worker's `scheduled()` export.
23
+ */
18
24
  export class CloudflareSchedulerAdapter implements SchedulerAdapter {
19
25
  private readonly entries = new Map<string, ScheduledAction>();
20
26
 
@@ -6,6 +6,7 @@ import type { ScheduledAction, SchedulerAdapter } from './types';
6
6
 
7
7
  let runtimeAdapter: SchedulerAdapter | undefined;
8
8
 
9
+ /** Set the runtime scheduler adapter. Call before {@link initScheduler}. */
9
10
  export function setSchedulerAdapter(adapter: SchedulerAdapter): void {
10
11
  runtimeAdapter = adapter;
11
12
  }
@@ -15,6 +16,7 @@ export function resetSchedulerAdapter(): void {
15
16
  runtimeAdapter = undefined;
16
17
  }
17
18
 
19
+ /** Get the currently configured scheduler adapter, or `undefined` if not set. */
18
20
  export function getSchedulerAdapter(): SchedulerAdapter | undefined {
19
21
  return runtimeAdapter;
20
22
  }
@@ -1,5 +1,16 @@
1
- export { CloudflareSchedulerAdapter } from './cloudflare';
1
+ export {
2
+ ActionRegistry,
3
+ type CreateDefaultRegistryOptions,
4
+ createDefaultRegistry,
5
+ HealthPingAction,
6
+ type HealthPingWriter,
7
+ LogAction,
8
+ QueueStatsAction,
9
+ type QueueStatsDaoProvider,
10
+ type SchedulerAction,
11
+ toScheduledAction,
12
+ } from './action';
2
13
  export { getSchedulerAdapter, initScheduler, resetSchedulerAdapter, setSchedulerAdapter } from './factory';
3
- export { NodeSchedulerAdapter } from './node';
4
14
  export { NoopSchedulerAdapter } from './noop';
5
15
  export type { ScheduledAction, SchedulerAdapter } from './types';
16
+ export { wrapScheduledHandler } from './wrap-handler';
@@ -36,6 +36,10 @@ interface ScheduledEntry {
36
36
  timer?: ReturnType<typeof setInterval>;
37
37
  }
38
38
 
39
+ /**
40
+ * Scheduler adapter for Node.js using a setInterval-based approach.
41
+ * No external cron library dependency — cron expressions are parsed minimally.
42
+ */
39
43
  export class NodeSchedulerAdapter implements SchedulerAdapter {
40
44
  private readonly entries: ScheduledEntry[] = [];
41
45
  private running = false;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ScheduledAction, SchedulerAdapter } from './types';
5
5
 
6
+ /** Scheduler adapter that discards all registrations — for testing and environments without scheduling. */
6
7
  export class NoopSchedulerAdapter implements SchedulerAdapter {
7
8
  constructor() {}
8
9
 
@@ -0,0 +1,50 @@
1
+ import type { EventBus } from '../event-bus/event-bus';
2
+ import type { SchedulerEvents } from '../events';
3
+ import { addSpanAttributes, addSpanEvent, traceAsync } from '../telemetry/tracing';
4
+ import type { ScheduledAction } from './types';
5
+
6
+ /**
7
+ * Wrap a scheduled action with OTel tracing, duration measurement, and
8
+ * `scheduler.job.executed` event emission.
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.
14
+ *
15
+ * @param name - Job name, surfaced as `scheduler.job_name` on the span/event.
16
+ * @param action - The action to wrap (new no-arg `ScheduledAction` signature).
17
+ * @param systemBus - Optional bus to emit `scheduler.job.executed` on.
18
+ */
19
+ export function wrapScheduledHandler(
20
+ name: string,
21
+ action: ScheduledAction,
22
+ systemBus?: EventBus<SchedulerEvents> | null,
23
+ ): ScheduledAction {
24
+ return async () => {
25
+ const startTime = performance.now();
26
+
27
+ return traceAsync('scheduler.job', async (span) => {
28
+ addSpanAttributes({ 'scheduler.job_name': name });
29
+
30
+ let execError: string | undefined;
31
+ try {
32
+ await action();
33
+ } catch (error) {
34
+ execError = error instanceof Error ? error.message : String(error);
35
+ addSpanEvent('scheduler.job.error', { 'scheduler.error': execError });
36
+ span.setStatus({ code: 2, message: execError }); // ERROR
37
+ throw error;
38
+ } finally {
39
+ const durationMs = Math.round(performance.now() - startTime);
40
+ addSpanAttributes({ 'scheduler.duration_ms': durationMs });
41
+
42
+ systemBus?.emit('scheduler.job.executed', {
43
+ name,
44
+ durationMs,
45
+ ...(execError !== undefined ? { error: execError } : {}),
46
+ });
47
+ }
48
+ });
49
+ };
50
+ }
@@ -0,0 +1 @@
1
+ export { CloudflareSchedulerAdapter } from './scheduler/cloudflare';
@@ -0,0 +1 @@
1
+ export { NodeSchedulerAdapter } from './scheduler/node';
@@ -2,6 +2,10 @@
2
2
  * Telemetry configuration interface.
3
3
  */
4
4
 
5
+ /**
6
+ * Full telemetry configuration: master enable switch, service name,
7
+ * environment, and debug-level DB statement capture.
8
+ */
5
9
  export interface TelemetryConfig {
6
10
  /** Master switch — when false, all tracing degrades to no-ops. */
7
11
  enabled: boolean;
@@ -8,6 +8,7 @@ export type { Counter, Histogram } from '@opentelemetry/api';
8
8
 
9
9
  let metricsInitialized = false;
10
10
 
11
+ /** Whether the metrics subsystem has been initialized via {@link initMetrics}. */
11
12
  export function isMetricsInitialized(): boolean {
12
13
  return metricsInitialized;
13
14
  }
@@ -40,67 +41,81 @@ function getOrCreateHistogram(key: string, name: string, description: string, un
40
41
 
41
42
  // ── HTTP client ─────────────────────────────────────────────────────
42
43
 
44
+ /** Counter for total outbound HTTP requests, tagged by method and status code. */
43
45
  export function getHttpClientRequestTotal(): Counter {
44
46
  return getOrCreateCounter('httpCliReq', 'http.client.request.total', 'Total outbound HTTP requests', '{request}');
45
47
  }
46
48
 
49
+ /** Histogram for outbound HTTP request duration in milliseconds. */
47
50
  export function getHttpClientRequestDuration(): Histogram {
48
51
  return getOrCreateHistogram('httpCliDur', 'http.client.request.duration', 'Outbound HTTP request duration');
49
52
  }
50
53
 
54
+ /** Counter for outbound HTTP errors, tagged by error type. */
51
55
  export function getHttpClientRequestErrors(): Counter {
52
56
  return getOrCreateCounter('httpCliErr', 'http.client.request.errors', 'Outbound HTTP errors', '{error}');
53
57
  }
54
58
 
55
59
  // ── Event bus ───────────────────────────────────────────────────────
56
60
 
61
+ /** Counter for total event bus emits. */
57
62
  export function getEventbusEmitsTotal(): Counter {
58
63
  return getOrCreateCounter('ebEmit', 'eventbus.emits.total', 'Total event bus emits', '{emit}');
59
64
  }
60
65
 
66
+ /** Counter for event bus handler errors. */
61
67
  export function getEventbusErrorsTotal(): Counter {
62
68
  return getOrCreateCounter('ebErr', 'eventbus.errors.total', 'Event bus errors', '{error}');
63
69
  }
64
70
 
65
71
  // ── Queue ───────────────────────────────────────────────────────────
66
72
 
73
+ /** Counter for total jobs enqueued. */
67
74
  export function getQueueJobEnqueuedTotal(): Counter {
68
75
  return getOrCreateCounter('qEnq', 'queue.jobs.enqueued', 'Total jobs enqueued', '{job}');
69
76
  }
70
77
 
78
+ /** Counter for total jobs completed successfully. */
71
79
  export function getQueueJobCompletedTotal(): Counter {
72
80
  return getOrCreateCounter('qComp', 'queue.jobs.completed', 'Total jobs completed', '{job}');
73
81
  }
74
82
 
83
+ /** Counter for total jobs that failed (exhausted retries). */
75
84
  export function getQueueJobFailedTotal(): Counter {
76
85
  return getOrCreateCounter('qFail', 'queue.jobs.failed', 'Total jobs failed', '{job}');
77
86
  }
78
87
 
88
+ /** Histogram for job processing duration in milliseconds. */
79
89
  export function getQueueJobProcessingDuration(): Histogram {
80
90
  return getOrCreateHistogram('qProcDur', 'queue.jobs.processing_duration', 'Job processing duration');
81
91
  }
82
92
 
83
93
  // ── Scheduler ───────────────────────────────────────────────────────
84
94
 
95
+ /** Counter for total scheduled job executions. */
85
96
  export function getSchedulerJobExecutedTotal(): Counter {
86
97
  return getOrCreateCounter('schedExec', 'scheduler.jobs.executed', 'Total scheduled job executions', '{execution}');
87
98
  }
88
99
 
100
+ /** Histogram for scheduled job execution duration in milliseconds. */
89
101
  export function getSchedulerJobDuration(): Histogram {
90
102
  return getOrCreateHistogram('schedDur', 'scheduler.jobs.duration', 'Scheduled job duration');
91
103
  }
92
104
 
105
+ /** Counter for failed scheduled job executions. */
93
106
  export function getSchedulerJobFailedTotal(): Counter {
94
107
  return getOrCreateCounter('schedFail', 'scheduler.jobs.failed', 'Failed scheduled jobs', '{failure}');
95
108
  }
96
109
 
97
110
  // ── Lifecycle ───────────────────────────────────────────────────────
98
111
 
112
+ /** Mark the metrics subsystem as initialized. Idempotent. */
99
113
  export function initMetrics(): void {
100
114
  if (metricsInitialized) return;
101
115
  metricsInitialized = true;
102
116
  }
103
117
 
118
+ /** Clear the instrument cache and mark metrics as uninitialized. */
104
119
  export function shutdownMetrics(): Promise<void> {
105
120
  // The meter provider is owned by whoever registered it globally (the host
106
121
  // app or `@gobing-ai/ts-infra/otel-node`). Here we only drop the instrument
@@ -112,6 +127,10 @@ export function shutdownMetrics(): Promise<void> {
112
127
  return Promise.resolve();
113
128
  }
114
129
 
130
+ /**
131
+ * Reset all metrics state: clear instrument cache, mark uninitialized,
132
+ * and disable the global meter. For testing only.
133
+ */
115
134
  export function _resetMetrics(): void {
116
135
  for (const key of Object.keys(instruments)) {
117
136
  instruments[key] = undefined;
@@ -17,6 +17,7 @@ const TRACER_VERSION = '0.1.0';
17
17
  let telemetryInitialized = false;
18
18
  let resolvedConfig: TelemetryConfig = getTelemetryConfig();
19
19
 
20
+ /** Get the resolved telemetry configuration (defaults + overrides). */
20
21
  export function getResolvedConfig(): TelemetryConfig {
21
22
  return resolvedConfig;
22
23
  }
@@ -31,6 +32,7 @@ export function initTelemetry(config?: Partial<TelemetryConfig>): void {
31
32
  telemetryInitialized = true;
32
33
  }
33
34
 
35
+ /** Mark telemetry as uninitialized. Does not shut down the OTel provider. */
34
36
  export function shutdownTelemetry(): Promise<void> {
35
37
  telemetryInitialized = false;
36
38
  return Promise.resolve();
@@ -41,10 +43,12 @@ export function getTracer(): Tracer {
41
43
  return trace.getTracer(TRACER_NAME, TRACER_VERSION);
42
44
  }
43
45
 
46
+ /** Whether telemetry is initialized and enabled. */
44
47
  export function isTelemetryEnabled(): boolean {
45
48
  return telemetryInitialized && resolvedConfig.enabled;
46
49
  }
47
50
 
51
+ /** Reset the telemetry subsystem to its uninitialized state. For testing. */
48
52
  export function _resetTelemetry(): void {
49
53
  telemetryInitialized = false;
50
54
  resolvedConfig = getTelemetryConfig();
@@ -5,6 +5,10 @@
5
5
  import { context, type Span, type SpanOptions, type Tracer, trace } from '@opentelemetry/api';
6
6
  import { getTracer } from './sdk';
7
7
 
8
+ /**
9
+ * Execute an async function within an active OTel span.
10
+ * The span is automatically ended and its status set on error.
11
+ */
8
12
  export async function traceAsync<T>(
9
13
  name: string,
10
14
  fn: (span: Span) => Promise<T>,
@@ -24,6 +28,10 @@ export async function traceAsync<T>(
24
28
  });
25
29
  }
26
30
 
31
+ /**
32
+ * Execute a synchronous function within an active OTel span.
33
+ * The span is automatically ended and its status set on error.
34
+ */
27
35
  export function traceSync<T>(name: string, fn: (span: Span) => T, options?: SpanOptions, tracer?: Tracer): T {
28
36
  const resolvedTracer = tracer ?? getTracer();
29
37
  return resolvedTracer.startActiveSpan(name, options ?? {}, (span) => {
@@ -38,6 +46,7 @@ export function traceSync<T>(name: string, fn: (span: Span) => T, options?: Span
38
46
  });
39
47
  }
40
48
 
49
+ /** Set key-value attributes on the currently active span. No-op when no span is recording. */
41
50
  export function addSpanAttributes(attributes: Record<string, string | number | boolean>): void {
42
51
  const span = trace.getActiveSpan();
43
52
  if (span?.isRecording()) {
@@ -45,6 +54,7 @@ export function addSpanAttributes(attributes: Record<string, string | number | b
45
54
  }
46
55
  }
47
56
 
57
+ /** Add a named event to the currently active span. No-op when no span is recording. */
48
58
  export function addSpanEvent(name: string, attributes?: Record<string, string | number | boolean>): void {
49
59
  const span = trace.getActiveSpan();
50
60
  if (span?.isRecording()) {
@@ -52,10 +62,12 @@ export function addSpanEvent(name: string, attributes?: Record<string, string |
52
62
  }
53
63
  }
54
64
 
65
+ /** Get the currently active span from context, or `undefined`. */
55
66
  export function getActiveSpan(): Span | undefined {
56
67
  return trace.getActiveSpan() ?? undefined;
57
68
  }
58
69
 
70
+ /** Execute a function with a specific span set as the active span in context. */
59
71
  export function withSpan<T>(span: Span, fn: () => T): T {
60
72
  return context.with(trace.setSpan(context.active(), span), fn);
61
73
  }