feat(metrics): Prometheus queue metrics for BullMQ (RFC-004 Phase 3 WS3a)
Adds a 5 s polling collector that publishes BullMQ queue depth as the goodgo_queue_depth gauge (labels: queue, state) and a goodgo_queue_job_outcomes_total counter for processor hooks. The collector fails-soft on Redis errors so a queue blip cannot crash the app. - New constants: QUEUE_DEPTH_GAUGE, QUEUE_JOB_OUTCOMES_TOTAL, QUEUE_METRICS_QUEUE_NAMES (extend as Phase 2 adds queues) - New QueueMetricsCollector with injectable timer/clock for tests - MetricsModule.withQueueMetrics() dynamic module wires queue tokens via getQueueToken + factory provider; re-imports BullModule.registerQueue so ordering between MetricsModule and feature modules does not matter - AppModule mounts MetricsModule.withQueueMetrics() alongside MetricsModule - 4 unit tests cover sample → gauge mapping, Redis-down fail-soft, recordJobOutcome, and timer init/destroy Bull Board UI mount split into WS3b (needs @bull-board/* deps). Refs: GOO-175 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule, getQueueToken } from '@nestjs/bullmq';
|
||||
import { Module, type DynamicModule } from '@nestjs/common';
|
||||
import {
|
||||
makeCounterProvider,
|
||||
makeHistogramProvider,
|
||||
makeGaugeProvider,
|
||||
} from '@willsoto/nestjs-prometheus';
|
||||
import type { Queue } from 'bullmq';
|
||||
import { MetricsService } from './infrastructure/metrics.service';
|
||||
import {
|
||||
QueueMetricsCollector,
|
||||
QUEUE_METRICS_COLLECTOR_OPTIONS,
|
||||
QUEUE_METRICS_COLLECTOR_QUEUES,
|
||||
type QueueLike,
|
||||
type QueueMetricsCollectorOptions,
|
||||
} from './infrastructure/queue-metrics.collector';
|
||||
import {
|
||||
QUEUE_DEPTH_GAUGE,
|
||||
QUEUE_JOB_OUTCOMES_TOTAL,
|
||||
QUEUE_METRICS_QUEUE_NAMES,
|
||||
} from './infrastructure/queue-metrics.constants';
|
||||
import {
|
||||
GOODGO_API_REQUEST_DURATION,
|
||||
GOODGO_LISTINGS_CREATED_TOTAL,
|
||||
@@ -141,4 +155,48 @@ import { HttpMetricsInterceptor } from './presentation/interceptors/http-metrics
|
||||
controllers: [WebVitalsController],
|
||||
exports: [MetricsService, HttpMetricsInterceptor],
|
||||
})
|
||||
export class MetricsModule {}
|
||||
export class MetricsModule {
|
||||
/**
|
||||
* Register the queue-metrics collector with a fixed list of BullMQ queue
|
||||
* names. Each name must already be registered via BullModule.registerQueue
|
||||
* somewhere in the app (root or feature module).
|
||||
*
|
||||
* RFC-004 Phase 3 — workstream 3a.
|
||||
*/
|
||||
static withQueueMetrics(
|
||||
queueNames: readonly string[] = QUEUE_METRICS_QUEUE_NAMES,
|
||||
options: QueueMetricsCollectorOptions = {},
|
||||
): DynamicModule {
|
||||
const queueTokens = queueNames.map((name) => getQueueToken(name));
|
||||
return {
|
||||
module: MetricsModule,
|
||||
imports: [
|
||||
// Re-register each queue here so the collector can resolve them via
|
||||
// BullMQ's standard token even if MetricsModule is imported before
|
||||
// the feature module that owns the queue. BullMQ deduplicates the
|
||||
// queue instance under the hood.
|
||||
...queueNames.map((name) => BullModule.registerQueue({ name })),
|
||||
],
|
||||
providers: [
|
||||
makeGaugeProvider({
|
||||
name: QUEUE_DEPTH_GAUGE,
|
||||
help: 'BullMQ queue depth by state (waiting, active, completed, failed, delayed)',
|
||||
labelNames: ['queue', 'state'],
|
||||
}),
|
||||
makeCounterProvider({
|
||||
name: QUEUE_JOB_OUTCOMES_TOTAL,
|
||||
help: 'BullMQ job outcomes (completed, failed) by queue',
|
||||
labelNames: ['queue', 'outcome'],
|
||||
}),
|
||||
{
|
||||
provide: QUEUE_METRICS_COLLECTOR_QUEUES,
|
||||
inject: queueTokens,
|
||||
useFactory: (...queues: Queue[]): QueueLike[] => queues,
|
||||
},
|
||||
{ provide: QUEUE_METRICS_COLLECTOR_OPTIONS, useValue: options },
|
||||
QueueMetricsCollector,
|
||||
],
|
||||
exports: [QueueMetricsCollector],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user