Bluesky app fork with some witchin' additions 馃挮
at main 3.0 kB view raw
1import events from 'node:events' 2import type http from 'node:http' 3 4import express from 'express' 5import promBundle from 'express-prom-bundle' 6import {createHttpTerminator, type HttpTerminator} from 'http-terminator' 7import {register} from 'prom-client' 8 9import {type Config} from './config.js' 10import {AppContext} from './context.js' 11import {default as routes, errorHandler} from './routes/index.js' 12 13export * from './config.js' 14export * from './logger.js' 15 16export class CardService { 17 public server?: http.Server 18 public metricsServer?: http.Server 19 private terminator?: HttpTerminator 20 private metricsTerminator?: HttpTerminator 21 22 constructor( 23 public app: express.Application, 24 public ctx: AppContext, 25 ) {} 26 27 static async create(cfg: Config): Promise<CardService> { 28 let app = express() 29 30 const ctx = await AppContext.fromConfig(cfg) 31 32 // Add Prometheus middleware for automatic HTTP instrumentation 33 const metricsMiddleware = promBundle({ 34 includeMethod: true, 35 includePath: true, 36 includeStatusCode: true, 37 includeUp: true, 38 promClient: { 39 collectDefaultMetrics: {}, 40 }, 41 42 autoregister: false, 43 normalizePath: req => { 44 // If we have a matched route, use its path (with :params) instead of the full URL path 45 if (req.route) { 46 return req.route.path 47 } 48 49 // Group all unmatched paths together to reduce cardinality 50 return '<unmatched>' 51 }, 52 }) 53 app.use(metricsMiddleware) 54 55 app = routes(ctx, app) 56 app.use(errorHandler) 57 58 return new CardService(app, ctx) 59 } 60 61 async start() { 62 // Start main application server 63 this.server = this.app.listen(this.ctx.cfg.service.port) 64 this.server.keepAliveTimeout = 90000 65 this.terminator = createHttpTerminator({ 66 server: this.server, 67 gracefulTerminationTimeout: 15000, // 15s timeout for in-flight requests 68 }) 69 await events.once(this.server, 'listening') 70 71 // Start separate metrics server 72 const metricsApp = express() 73 metricsApp.get('/metrics', async (_req, res) => { 74 res.set('Content-Type', register.contentType) 75 res.end(await register.metrics()) 76 }) 77 78 this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort) 79 this.metricsTerminator = createHttpTerminator({ 80 server: this.metricsServer, 81 gracefulTerminationTimeout: 2000, // 2s timeout for metrics server 82 }) 83 await events.once(this.metricsServer, 'listening') 84 } 85 86 async destroy() { 87 const startTime = Date.now() 88 89 this.ctx.abortController.abort() 90 91 const shutdownPromises = [] 92 93 if (this.terminator) { 94 shutdownPromises.push(this.terminator.terminate()) 95 } 96 97 if (this.metricsTerminator) { 98 shutdownPromises.push(this.metricsTerminator.terminate()) 99 } 100 101 await Promise.all(shutdownPromises) 102 103 const elapsed = Date.now() - startTime 104 const {httpLogger} = await import('./logger.js') 105 httpLogger.info(`Graceful shutdown completed in ${elapsed}ms`) 106 } 107}