corpus

self-hosted listenbrainz and last.fm proxy
Log | Files | Refs | README | LICENSE

Metrics.js (6460B)


      1 import {
      2 	Registry,
      3 	Counter,
      4 	Gauge,
      5 	Histogram,
      6 	collectDefaultMetrics,
      7 } from "prom-client";
      8 
      9 // ---------------------------------------------------------------------------
     10 // Prometheus metrics — only active when METRICS_ENABLED=true
     11 // ---------------------------------------------------------------------------
     12 
     13 const makeActiveMetrics = () => {
     14 	const registry = new Registry();
     15 	collectDefaultMetrics({ register: registry });
     16 
     17 	const httpRequestsTotal = new Counter({
     18 		name: "corpus_http_requests_total",
     19 		help: "Total number of HTTP requests",
     20 		labelNames: ["method", "path", "status"],
     21 		registers: [registry],
     22 	});
     23 
     24 	const httpRequestDurationSeconds = new Histogram({
     25 		name: "corpus_http_request_duration_seconds",
     26 		help: "HTTP request duration in seconds",
     27 		labelNames: ["method", "path"],
     28 		buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
     29 		registers: [registry],
     30 	});
     31 
     32 	const syncRunsTotal = new Counter({
     33 		name: "corpus_sync_runs_total",
     34 		help: "Total number of sync runs",
     35 		labelNames: ["user", "source", "result"],
     36 		registers: [registry],
     37 	});
     38 
     39 	const syncScrobblesAddedTotal = new Counter({
     40 		name: "corpus_sync_scrobbles_added_total",
     41 		help: "Total scrobbles added during syncs",
     42 		labelNames: ["user", "source"],
     43 		registers: [registry],
     44 	});
     45 
     46 	const syncLastSuccessSeconds = new Gauge({
     47 		name: "corpus_sync_last_success_seconds",
     48 		help: "Unix timestamp of last successful sync run",
     49 		labelNames: ["user", "source"],
     50 		registers: [registry],
     51 	});
     52 
     53 	const enrichmentFetchesTotal = new Counter({
     54 		name: "corpus_enrichment_fetches_total",
     55 		help: "Total metadata enrichment fetches by user, source, and result",
     56 		labelNames: ["user", "source", "result"],
     57 		registers: [registry],
     58 	});
     59 
     60 	const enrichmentQueueSize = new Gauge({
     61 		name: "corpus_enrichment_queue_size",
     62 		help: "Number of releases pending metadata enrichment per user",
     63 		labelNames: ["user", "type"],
     64 		registers: [registry],
     65 	});
     66 
     67 	const coverRequestsTotal = new Counter({
     68 		name: "corpus_cover_requests_total",
     69 		help: "Total cover art requests by user, source, and result",
     70 		labelNames: ["user", "source", "result"],
     71 		registers: [registry],
     72 	});
     73 
     74 	const cosineRequestsTotal = new Counter({
     75 		name: "corpus_cosine_requests_total",
     76 		help: "Total Cosine Club API requests by user and result",
     77 		labelNames: ["user", "result"],
     78 		registers: [registry],
     79 	});
     80 
     81 	const dbBackupRunsTotal = new Counter({
     82 		name: "corpus_db_backup_runs_total",
     83 		help: "Total database backup runs",
     84 		labelNames: ["user", "result"],
     85 		registers: [registry],
     86 	});
     87 
     88 	const dbBackupLastSuccessSeconds = new Gauge({
     89 		name: "corpus_db_backup_last_success_seconds",
     90 		help: "Unix timestamp of last successful database backup",
     91 		labelNames: ["user"],
     92 		registers: [registry],
     93 	});
     94 
     95 	return {
     96 		getMetrics: () => registry.metrics(),
     97 		contentType: registry.contentType,
     98 		recordHttpRequest: (method, path, status, durationMs) => {
     99 			httpRequestsTotal.inc({ method, path, status: String(status) });
    100 			httpRequestDurationSeconds.observe({ method, path }, durationMs / 1000);
    101 		},
    102 		incSyncRuns: (user, source, result) =>
    103 			syncRunsTotal.inc({ user, source, result }),
    104 		incSyncScrobbles: (user, source, count) =>
    105 			syncScrobblesAddedTotal.inc({ user, source }, count),
    106 		setSyncLastSuccess: (user, source) =>
    107 			syncLastSuccessSeconds.setToCurrentTime({ user, source }),
    108 		incEnrichmentFetch: (user, source, result) =>
    109 			enrichmentFetchesTotal.inc({ user, source, result }),
    110 		setEnrichmentQueueSize: (user, type, size) =>
    111 			enrichmentQueueSize.set({ user, type }, size),
    112 		incCoverRequest: (user, source, result) =>
    113 			coverRequestsTotal.inc({ user, source, result }),
    114 		incCosineRequest: (user, result) =>
    115 			cosineRequestsTotal.inc({ user, result }),
    116 		incDbBackupRun: (user, result) => dbBackupRunsTotal.inc({ user, result }),
    117 		setDbBackupLastSuccess: (user) =>
    118 			dbBackupLastSuccessSeconds.setToCurrentTime({ user }),
    119 	};
    120 };
    121 
    122 const noOp = () => {};
    123 
    124 const makeNoOpMetrics = () => ({
    125 	getMetrics: () => Promise.resolve(""),
    126 	contentType: "text/plain; version=0.0.4; charset=utf-8",
    127 	recordHttpRequest: noOp,
    128 	incSyncRuns: noOp,
    129 	incSyncScrobbles: noOp,
    130 	setSyncLastSuccess: noOp,
    131 	incEnrichmentFetch: noOp,
    132 	setEnrichmentQueueSize: noOp,
    133 	incCoverRequest: noOp,
    134 	incCosineRequest: noOp,
    135 	incDbBackupRun: noOp,
    136 	setDbBackupLastSuccess: noOp,
    137 });
    138 
    139 const activeMetrics =
    140 	process.env.METRICS_ENABLED === "true"
    141 		? makeActiveMetrics()
    142 		: makeNoOpMetrics();
    143 
    144 // ---------------------------------------------------------------------------
    145 // Exports
    146 // ---------------------------------------------------------------------------
    147 
    148 export const getMetricsImpl = (onSuccess, onError) => () => {
    149 	activeMetrics.getMetrics().then(
    150 		(s) => onSuccess(s)(),
    151 		(err) => onError(err.message)(),
    152 	);
    153 };
    154 
    155 export const getContentType = () => activeMetrics.contentType;
    156 
    157 // Attaches a 'finish' listener to record metrics and log the request.
    158 // logFn: String -> Effect Unit — structured logger called on completion.
    159 export const wrapRequestImpl =
    160 	(method, path, logFn, _req, res, handler) => () => {
    161 		const startMs = Date.now();
    162 		res.once("finish", () => {
    163 			const durationMs = Date.now() - startMs;
    164 			const status = res.statusCode || 0;
    165 			activeMetrics.recordHttpRequest(method, path, status, durationMs);
    166 			logFn(`${method} ${path} ${status} ${durationMs}ms`)();
    167 		});
    168 		handler();
    169 	};
    170 
    171 export const incSyncRunsImpl = (user, source, result) => () => {
    172 	activeMetrics.incSyncRuns(user, source, result);
    173 };
    174 
    175 export const incSyncScrobblesImpl = (user, source, count) => () => {
    176 	activeMetrics.incSyncScrobbles(user, source, count);
    177 };
    178 
    179 export const setSyncLastSuccessImpl = (user, source) => () => {
    180 	activeMetrics.setSyncLastSuccess(user, source);
    181 };
    182 
    183 export const incEnrichmentFetchImpl = (user, source, result) => () => {
    184 	activeMetrics.incEnrichmentFetch(user, source, result);
    185 };
    186 
    187 export const setEnrichmentQueueSizeImpl = (user, type, size) => () => {
    188 	activeMetrics.setEnrichmentQueueSize(user, type, size);
    189 };
    190 
    191 export const incCoverRequestImpl = (user, source, result) => () => {
    192 	activeMetrics.incCoverRequest(user, source, result);
    193 };
    194 
    195 export const incCosineRequestImpl = (user, result) => () => {
    196 	activeMetrics.incCosineRequest(user, result);
    197 };
    198 
    199 export const incDbBackupRunImpl = (user, result) => () => {
    200 	activeMetrics.incDbBackupRun(user, result);
    201 };
    202 
    203 export const setDbBackupLastSuccess = (user) => () => {
    204 	activeMetrics.setDbBackupLastSuccess(user);
    205 };