@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.
- package/dist/runtime/subsystems/index.d.ts +7 -0
- package/dist/runtime/subsystems/index.js +905 -208
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +10 -0
- package/dist/runtime/subsystems/observability/index.js +895 -0
- package/dist/runtime/subsystems/observability/index.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.d.ts +15 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.js +465 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.d.ts +28 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.js +75 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.module.d.ts +56 -0
- package/dist/runtime/subsystems/observability/observability.module.js +887 -0
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +155 -0
- package/dist/runtime/subsystems/observability/observability.protocol.js +1 -0
- package/dist/runtime/subsystems/observability/observability.protocol.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.tokens.d.ts +19 -0
- package/dist/runtime/subsystems/observability/observability.tokens.js +8 -0
- package/dist/runtime/subsystems/observability/observability.tokens.js.map +1 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +79 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js +425 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js.map +1 -0
- package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +4 -4
- package/package.json +6 -1
- package/runtime/subsystems/index.ts +23 -0
- package/runtime/subsystems/observability/index.ts +35 -0
- package/runtime/subsystems/observability/observability.drizzle-backend.ts +223 -0
- package/runtime/subsystems/observability/observability.memory-backend.ts +111 -0
- package/runtime/subsystems/observability/observability.module.ts +115 -0
- package/runtime/subsystems/observability/observability.protocol.ts +167 -0
- package/runtime/subsystems/observability/observability.tokens.ts +18 -0
- 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
|
+
}
|