@pattern-stack/codegen 0.4.4 → 0.4.5

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 (34) hide show
  1. package/dist/runtime/subsystems/index.d.ts +7 -0
  2. package/dist/runtime/subsystems/index.js +905 -208
  3. package/dist/runtime/subsystems/index.js.map +1 -1
  4. package/dist/runtime/subsystems/observability/index.d.ts +10 -0
  5. package/dist/runtime/subsystems/observability/index.js +895 -0
  6. package/dist/runtime/subsystems/observability/index.js.map +1 -0
  7. package/dist/runtime/subsystems/observability/observability.drizzle-backend.d.ts +15 -0
  8. package/dist/runtime/subsystems/observability/observability.drizzle-backend.js +465 -0
  9. package/dist/runtime/subsystems/observability/observability.drizzle-backend.js.map +1 -0
  10. package/dist/runtime/subsystems/observability/observability.memory-backend.d.ts +28 -0
  11. package/dist/runtime/subsystems/observability/observability.memory-backend.js +75 -0
  12. package/dist/runtime/subsystems/observability/observability.memory-backend.js.map +1 -0
  13. package/dist/runtime/subsystems/observability/observability.module.d.ts +56 -0
  14. package/dist/runtime/subsystems/observability/observability.module.js +887 -0
  15. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -0
  16. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +155 -0
  17. package/dist/runtime/subsystems/observability/observability.protocol.js +1 -0
  18. package/dist/runtime/subsystems/observability/observability.protocol.js.map +1 -0
  19. package/dist/runtime/subsystems/observability/observability.tokens.d.ts +19 -0
  20. package/dist/runtime/subsystems/observability/observability.tokens.js +8 -0
  21. package/dist/runtime/subsystems/observability/observability.tokens.js.map +1 -0
  22. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +79 -0
  23. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js +425 -0
  24. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js.map +1 -0
  25. package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +4 -4
  26. package/package.json +6 -1
  27. package/runtime/subsystems/index.ts +23 -0
  28. package/runtime/subsystems/observability/index.ts +35 -0
  29. package/runtime/subsystems/observability/observability.drizzle-backend.ts +223 -0
  30. package/runtime/subsystems/observability/observability.memory-backend.ts +111 -0
  31. package/runtime/subsystems/observability/observability.module.ts +115 -0
  32. package/runtime/subsystems/observability/observability.protocol.ts +167 -0
  33. package/runtime/subsystems/observability/observability.tokens.ts +18 -0
  34. package/runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts +222 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * BridgeMetricsReporter — periodic structured-log sampler for the
3
+ * `bridge_delivery` ledger.
4
+ *
5
+ * Runs on a timer (default 60s, configurable via
6
+ * `BRIDGE_METRICS_INTERVAL_MS`) and emits ONE `Logger.log` line per tick
7
+ * describing counts of rows that transitioned through each terminal
8
+ * status in the last tick window, grouped by `(status, event_type,
9
+ * skip_reason)`.
10
+ *
11
+ * # Placement
12
+ *
13
+ * Lives under `observability/reporters/` rather than in the bridge
14
+ * subsystem itself because:
15
+ * 1. It's not part of the bridge's functional surface — a reporter is
16
+ * an observability concern composed on top.
17
+ * 2. Future reporters (Prometheus exporter, OTel bridge, etc.) slot in
18
+ * here with no cross-subsystem import churn.
19
+ *
20
+ * # Opt-in via ObservabilityModule
21
+ *
22
+ * The reporter is NOT provided automatically. Opt in via
23
+ * `ObservabilityModule.forRoot({ backend, reporters: { bridgeMetrics: true } })`
24
+ * — the module only registers the reporter when that flag is set, which
25
+ * keeps consumers without the bridge subsystem free of its schema import
26
+ * tax (tree-shaken; see `observability.module.ts` for the gate).
27
+ *
28
+ * # Why a sampler instead of in-handler logs
29
+ *
30
+ * The bridge subsystem writes the `bridge_delivery` ledger directly; adding
31
+ * per-transition log lines inside the handler would double every row at
32
+ * 1:1 cardinality. Aggregating per-tick produces the "counts per event
33
+ * type of delivered/skipped/failed" shape that ops dashboards want,
34
+ * without touching the bridge runtime.
35
+ *
36
+ * # Why aggregate-per-tick rather than per-row
37
+ *
38
+ * Deliveries flow at bulk-sync cadence (one event per persisted CRM
39
+ * record). Per-row logs would be noisy and duplicative of the ledger
40
+ * itself; aggregates match the "counts per event type of
41
+ * delivered/skipped/failed" operator surface.
42
+ */
43
+ import {
44
+ Inject,
45
+ Injectable,
46
+ Logger,
47
+ type OnModuleDestroy,
48
+ type OnModuleInit,
49
+ } from '@nestjs/common';
50
+ import { SchedulerRegistry } from '@nestjs/schedule';
51
+ import { and, eq, gt, sql } from 'drizzle-orm';
52
+
53
+ import { DRIZZLE } from '../../../constants/tokens';
54
+ import type { DrizzleClient } from '../../../types/drizzle';
55
+ import { bridgeDelivery } from '../../bridge/bridge-delivery.schema';
56
+ import { domainEvents } from '../../events/domain-events.schema';
57
+
58
+ const INTERVAL_NAME = 'bridge-metrics-tick';
59
+
60
+ /** Default sampling interval (1 minute). */
61
+ const DEFAULT_INTERVAL_MS = 60_000;
62
+
63
+ /** Minimum allowed interval — guards against env misconfig producing a hot loop. */
64
+ const MIN_INTERVAL_MS = 1_000;
65
+
66
+ export interface BridgeMetricsRow {
67
+ status: 'pending' | 'delivered' | 'skipped' | 'failed';
68
+ eventType: string;
69
+ skipReason: string | null;
70
+ count: number;
71
+ }
72
+
73
+ export interface BridgeMetricsTick {
74
+ windowStart: Date;
75
+ windowEnd: Date;
76
+ rows: BridgeMetricsRow[];
77
+ }
78
+
79
+ @Injectable()
80
+ export class BridgeMetricsReporter implements OnModuleInit, OnModuleDestroy {
81
+ private readonly logger = new Logger(BridgeMetricsReporter.name);
82
+ private readonly intervalMs: number;
83
+ private lastTickAt: Date;
84
+
85
+ constructor(
86
+ @Inject(DRIZZLE) private readonly db: DrizzleClient,
87
+ private readonly scheduler: SchedulerRegistry,
88
+ ) {
89
+ this.intervalMs = this.resolveIntervalMs();
90
+ // Initialize the window tail at boot so the first tick reports only
91
+ // deliveries that transitioned after the reporter started.
92
+ this.lastTickAt = new Date();
93
+ }
94
+
95
+ onModuleInit(): void {
96
+ this.logger.log(
97
+ `BridgeMetricsReporter starting (intervalMs=${this.intervalMs}).`,
98
+ );
99
+ const timer = setInterval(() => {
100
+ void this.tick();
101
+ }, this.intervalMs);
102
+ // Allow the process to exit naturally in test runs — setInterval
103
+ // otherwise pins the event loop open.
104
+ timer.unref?.();
105
+ this.scheduler.addInterval(INTERVAL_NAME, timer);
106
+ }
107
+
108
+ onModuleDestroy(): void {
109
+ if (this.scheduler.getIntervals().includes(INTERVAL_NAME)) {
110
+ this.scheduler.deleteInterval(INTERVAL_NAME);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Run one sampling tick. Public so tests can drive it deterministically
116
+ * without waiting on the timer.
117
+ */
118
+ async tick(): Promise<BridgeMetricsTick> {
119
+ const windowStart = this.lastTickAt;
120
+ const windowEnd = new Date();
121
+ this.lastTickAt = windowEnd;
122
+
123
+ let rows: BridgeMetricsRow[] = [];
124
+ try {
125
+ rows = await this.sample(windowStart, windowEnd);
126
+ } catch (err) {
127
+ this.logger.error(
128
+ `bridge metrics sample failed: ${(err as Error).message}`,
129
+ );
130
+ return { windowStart, windowEnd, rows: [] };
131
+ }
132
+
133
+ this.emit({ windowStart, windowEnd, rows });
134
+ return { windowStart, windowEnd, rows };
135
+ }
136
+
137
+ private async sample(
138
+ windowStart: Date,
139
+ windowEnd: Date,
140
+ ): Promise<BridgeMetricsRow[]> {
141
+ // Terminal transitions land `delivered_at` for `delivered`, and leave
142
+ // `attempted_at` as the most recent timestamp for `skipped`/`failed`.
143
+ // Window on COALESCE so terminal skipped/failed rows are captured
144
+ // alongside delivered. Upper edge bounded by windowEnd so a long tick
145
+ // can't double-count rows that transitioned between sample and emit.
146
+ const lastTransition = sql<Date>`COALESCE(${bridgeDelivery.deliveredAt}, ${bridgeDelivery.attemptedAt})`;
147
+
148
+ const result = await this.db
149
+ .select({
150
+ status: bridgeDelivery.status,
151
+ eventType: domainEvents.type,
152
+ skipReason: bridgeDelivery.skipReason,
153
+ count: sql<number>`COUNT(*)::int`,
154
+ })
155
+ .from(bridgeDelivery)
156
+ .innerJoin(domainEvents, eq(bridgeDelivery.eventId, domainEvents.id))
157
+ .where(
158
+ and(
159
+ gt(lastTransition, windowStart),
160
+ sql`${lastTransition} <= ${windowEnd}`,
161
+ ),
162
+ )
163
+ .groupBy(
164
+ bridgeDelivery.status,
165
+ domainEvents.type,
166
+ bridgeDelivery.skipReason,
167
+ );
168
+
169
+ return result.map((r) => ({
170
+ status: r.status as BridgeMetricsRow['status'],
171
+ eventType: r.eventType,
172
+ skipReason: r.skipReason,
173
+ count: r.count,
174
+ }));
175
+ }
176
+
177
+ private emit(tick: BridgeMetricsTick): void {
178
+ if (tick.rows.length === 0) {
179
+ // Heartbeat — confirms the sampler is alive when deliveries are idle.
180
+ // Cheap enough at default 60s cadence; operators rely on this signal
181
+ // to distinguish "bridge quiet" from "reporter dead".
182
+ this.logger.log(
183
+ `bridge_metrics tick=empty window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}]`,
184
+ );
185
+ return;
186
+ }
187
+
188
+ const totals = tick.rows.reduce(
189
+ (acc, r) => {
190
+ acc[r.status] = (acc[r.status] ?? 0) + r.count;
191
+ return acc;
192
+ },
193
+ {} as Record<string, number>,
194
+ );
195
+
196
+ const detail = tick.rows
197
+ .map(
198
+ (r) =>
199
+ `${r.eventType}|${r.status}${r.skipReason ? `:${r.skipReason}` : ''}=${r.count}`,
200
+ )
201
+ .join(' ');
202
+
203
+ this.logger.log(
204
+ `bridge_metrics tick window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}] ` +
205
+ `totals=${JSON.stringify(totals)} detail=[${detail}]`,
206
+ );
207
+ }
208
+
209
+ private resolveIntervalMs(): number {
210
+ const raw = process.env['BRIDGE_METRICS_INTERVAL_MS'];
211
+ if (!raw) return DEFAULT_INTERVAL_MS;
212
+ const parsed = Number.parseInt(raw, 10);
213
+ if (!Number.isFinite(parsed) || parsed < MIN_INTERVAL_MS) {
214
+ new Logger(BridgeMetricsReporter.name).warn(
215
+ `Ignoring BRIDGE_METRICS_INTERVAL_MS='${raw}' (invalid or < ${MIN_INTERVAL_MS}ms); ` +
216
+ `using default ${DEFAULT_INTERVAL_MS}ms.`,
217
+ );
218
+ return DEFAULT_INTERVAL_MS;
219
+ }
220
+ return parsed;
221
+ }
222
+ }