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 };