From 0ddd09eb9b8b9e23644c199e6e9db7b8b5fadfba Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 22 Sep 2025 10:20:05 -0700 Subject: [PATCH] add metrics middleware to ogcard service --- bskyogcard/package.json | 3 +++ bskyogcard/src/config.ts | 4 ++++ bskyogcard/src/index.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/bskyogcard/package.json b/bskyogcard/package.json index 176f9d8c49..5a2f02ca2a 100644 --- a/bskyogcard/package.json +++ b/bskyogcard/package.json @@ -14,13 +14,16 @@ "@atproto/common": "^0.4.0", "@resvg/resvg-js": "^2.6.2", "express": "^4.19.2", + "express-prom-bundle": "^7.0.0", "http-terminator": "^3.2.0", "pino": "^9.2.0", + "prom-client": "^15.1.3", "react": "^18.3.1", "satori": "^0.10.13", "twemoji": "^14.0.2" }, "devDependencies": { + "@types/express": "^4.17.21", "@types/node": "^20.14.3", "ts-node": "^10.9.2", "typescript": "^5.4.5" diff --git a/bskyogcard/src/config.ts b/bskyogcard/src/config.ts index fafa18e743..306f6fe9e4 100644 --- a/bskyogcard/src/config.ts +++ b/bskyogcard/src/config.ts @@ -6,6 +6,7 @@ export type Config = { export type ServiceConfig = { port: number + metricsPort: number version?: string appviewUrl: string originVerify?: string @@ -13,6 +14,7 @@ export type ServiceConfig = { export type Environment = { port?: number + metricsPort?: number version?: string appviewUrl?: string originVerify?: string @@ -21,6 +23,7 @@ export type Environment = { export const readEnv = (): Environment => { return { port: envInt('CARD_PORT'), + metricsPort: envInt('CARD_METRICS_PORT'), version: envStr('CARD_VERSION'), appviewUrl: envStr('CARD_APPVIEW_URL'), originVerify: envStr('CARD_ORIGIN_VERIFY'), @@ -30,6 +33,7 @@ export const readEnv = (): Environment => { export const envToCfg = (env: Environment): Config => { const serviceCfg: ServiceConfig = { port: env.port ?? 3000, + metricsPort: env.metricsPort ?? 3001, version: env.version, appviewUrl: env.appviewUrl ?? 'https://api.bsky.app', originVerify: env.originVerify, diff --git a/bskyogcard/src/index.ts b/bskyogcard/src/index.ts index 139af01f73..5dd9022dbd 100644 --- a/bskyogcard/src/index.ts +++ b/bskyogcard/src/index.ts @@ -2,7 +2,9 @@ import events from 'node:events' import type http from 'node:http' import express from 'express' +import promBundle from 'express-prom-bundle' import {createHttpTerminator, type HttpTerminator} from 'http-terminator' +import {register} from 'prom-client' import {type Config} from './config.js' import {AppContext} from './context.js' @@ -13,7 +15,9 @@ export * from './logger.js' export class CardService { public server?: http.Server + public metricsServer?: http.Server private terminator?: HttpTerminator + private metricsTerminator?: HttpTerminator constructor( public app: express.Application, @@ -24,6 +28,23 @@ export class CardService { let app = express() const ctx = await AppContext.fromConfig(cfg) + + // Add Prometheus middleware for automatic HTTP instrumentation + const metricsMiddleware = promBundle({ + includeMethod: true, + includePath: true, + includeStatusCode: true, + includeUp: true, + promClient: { + collectDefaultMetrics: { + timeout: 5000, + }, + }, + // Don't expose /metrics on main app - we'll use separate server + autoregister: false, + }) + app.use(metricsMiddleware) + app = routes(ctx, app) app.use(errorHandler) @@ -31,14 +52,27 @@ export class CardService { } async start() { + // Start main application server this.server = this.app.listen(this.ctx.cfg.service.port) this.server.keepAliveTimeout = 90000 this.terminator = createHttpTerminator({server: this.server}) await events.once(this.server, 'listening') + + // Start separate metrics server + const metricsApp = express() + metricsApp.get('/metrics', (_req, res) => { + res.set('Content-Type', register.contentType) + res.end(register.metrics()) + }) + + this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort) + this.metricsTerminator = createHttpTerminator({server: this.metricsServer}) + await events.once(this.metricsServer, 'listening') } async destroy() { this.ctx.abortController.abort() await this.terminator?.terminate() + await this.metricsTerminator?.terminate() } }