@@ -2,6 +2,8 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
22import { z } from "zod" ;
33import { SimpleStructuredLogger } from "../utils/structuredLogger.js" ;
44import { HttpReply , getJsonBody } from "../apps/http.js" ;
5+ import { Registry , Histogram , Counter } from "prom-client" ;
6+ import { tryCatch } from "../../utils.js" ;
57
68const logger = new SimpleStructuredLogger ( "worker-http" ) ;
79
@@ -51,21 +53,72 @@ type RouteMap = Partial<{
5153type HttpServerOptions = {
5254 port : number ;
5355 host : string ;
56+ metrics ?: {
57+ register ?: Registry ;
58+ expose ?: boolean ;
59+ collect ?: boolean ;
60+ } ;
5461} ;
5562
5663export class HttpServer {
64+ private static httpRequestDuration ?: Histogram ;
65+ private static httpRequestTotal ?: Counter ;
66+
67+ private readonly metricsRegister ?: Registry ;
68+
5769 private readonly port : number ;
5870 private readonly host : string ;
5971 private routes : RouteMap = { } ;
60-
6172 public readonly server : ReturnType < typeof createServer > ;
6273
6374 constructor ( options : HttpServerOptions ) {
6475 this . port = options . port ;
6576 this . host = options . host ;
77+ this . metricsRegister = options . metrics ?. register ;
78+ const collectMetrics = options . metrics ?. collect ?? true ;
79+ const exposeMetrics = options . metrics ?. expose ?? false ;
80+
81+ // Initialize metrics only if registry is provided and not already initialized
82+ if ( this . metricsRegister && collectMetrics ) {
83+ if ( ! HttpServer . httpRequestDuration ) {
84+ HttpServer . httpRequestDuration = new Histogram ( {
85+ name : "http_request_duration_seconds" ,
86+ help : "Duration of HTTP requests in seconds" ,
87+ labelNames : [ "method" , "route" , "status" , "port" , "host" ] ,
88+ registers : [ this . metricsRegister ] ,
89+ } ) ;
90+ }
91+
92+ if ( ! HttpServer . httpRequestTotal ) {
93+ HttpServer . httpRequestTotal = new Counter ( {
94+ name : "http_requests_total" ,
95+ help : "Total number of HTTP requests" ,
96+ labelNames : [ "method" , "route" , "status" , "port" , "host" ] ,
97+ registers : [ this . metricsRegister ] ,
98+ } ) ;
99+ }
100+ }
101+
102+ if ( exposeMetrics ) {
103+ // Register metrics route
104+ this . route ( "/metrics" , "GET" , {
105+ handler : async ( { reply } ) => {
106+ if ( ! this . metricsRegister ) {
107+ return reply . text ( "Metrics registry not found" , 500 ) ;
108+ }
109+
110+ return reply . text (
111+ await this . metricsRegister . metrics ( ) ,
112+ 200 ,
113+ this . metricsRegister . contentType
114+ ) ;
115+ } ,
116+ } ) ;
117+ }
66118
67119 this . server = createServer ( async ( req , res ) => {
68120 const reply = new HttpReply ( res ) ;
121+ const startTime = process . hrtime ( ) ;
69122
70123 try {
71124 const { url, method } = req ;
@@ -98,13 +151,6 @@ export class HttpServer {
98151
99152 const routeDefinition = this . routes [ route ] ?. [ httpMethod . data ] ;
100153
101- // logger.debug("Matched route", {
102- // url,
103- // method,
104- // route,
105- // routeDefinition,
106- // });
107-
108154 if ( ! routeDefinition ) {
109155 logger . error ( "Invalid method" , { url, method, parsedMethod : httpMethod . data } ) ;
110156 return reply . empty ( 405 ) ;
@@ -141,25 +187,28 @@ export class HttpServer {
141187 return reply . json ( { ok : false , error : "Invalid body" } , false , 400 ) ;
142188 }
143189
144- try {
145- await handler ( {
190+ const [ error ] = await tryCatch (
191+ handler ( {
146192 reply,
147193 req,
148194 res,
149195 params : parsedParams . data ,
150196 queryParams : parsedQueryParams . data ,
151197 body : parsedBody . data ,
152- } ) ;
153- } catch ( handlerError ) {
154- logger . error ( "Route handler error" , { error : handlerError } ) ;
198+ } )
199+ ) ;
200+
201+ if ( error ) {
202+ logger . error ( "Route handler error" , { error } ) ;
155203 return reply . empty ( 500 ) ;
156204 }
157205 } catch ( error ) {
158206 logger . error ( "Failed to handle request" , { error } ) ;
159207 return reply . empty ( 500 ) ;
208+ } finally {
209+ this . collectMetrics ( req , res , startTime ) ;
210+ return ;
160211 }
161-
162- return ;
163212 } ) ;
164213
165214 this . server . on ( "clientError" , ( _ , socket ) => {
@@ -197,6 +246,25 @@ export class HttpServer {
197246 } ) ;
198247 }
199248
249+ private collectMetrics ( req : IncomingMessage , res : ServerResponse , startTime : [ number , number ] ) {
250+ if ( ! this . metricsRegister || ! HttpServer . httpRequestDuration || ! HttpServer . httpRequestTotal ) {
251+ return ;
252+ }
253+
254+ const [ seconds , nanoseconds ] = process . hrtime ( startTime ) ;
255+ const duration = seconds + nanoseconds / 1e9 ;
256+
257+ const route = this . findRoute ( req . url ?? "" ) ?? "unknown" ;
258+ const method = req . method ?? "unknown" ;
259+ const status = res . statusCode . toString ( ) ;
260+
261+ HttpServer . httpRequestDuration . observe (
262+ { method, route, status, port : this . port , host : this . host } ,
263+ duration
264+ ) ;
265+ HttpServer . httpRequestTotal . inc ( { method, route, status, port : this . port , host : this . host } ) ;
266+ }
267+
200268 private optionalSchema <
201269 TSchema extends z . ZodFirstPartySchemaTypes | undefined ,
202270 TData extends TSchema extends z . ZodFirstPartySchemaTypes ? z . TypeOf < TSchema > : TData ,
0 commit comments