@@ -2,6 +2,8 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
2
2
import { z } from "zod" ;
3
3
import { SimpleStructuredLogger } from "../utils/structuredLogger.js" ;
4
4
import { HttpReply , getJsonBody } from "../apps/http.js" ;
5
+ import { Registry , Histogram , Counter } from "prom-client" ;
6
+ import { tryCatch } from "../../utils.js" ;
5
7
6
8
const logger = new SimpleStructuredLogger ( "worker-http" ) ;
7
9
@@ -51,21 +53,72 @@ type RouteMap = Partial<{
51
53
type HttpServerOptions = {
52
54
port : number ;
53
55
host : string ;
56
+ metrics ?: {
57
+ register ?: Registry ;
58
+ expose ?: boolean ;
59
+ collect ?: boolean ;
60
+ } ;
54
61
} ;
55
62
56
63
export class HttpServer {
64
+ private static httpRequestDuration ?: Histogram ;
65
+ private static httpRequestTotal ?: Counter ;
66
+
67
+ private readonly metricsRegister ?: Registry ;
68
+
57
69
private readonly port : number ;
58
70
private readonly host : string ;
59
71
private routes : RouteMap = { } ;
60
-
61
72
public readonly server : ReturnType < typeof createServer > ;
62
73
63
74
constructor ( options : HttpServerOptions ) {
64
75
this . port = options . port ;
65
76
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
+ }
66
118
67
119
this . server = createServer ( async ( req , res ) => {
68
120
const reply = new HttpReply ( res ) ;
121
+ const startTime = process . hrtime ( ) ;
69
122
70
123
try {
71
124
const { url, method } = req ;
@@ -98,13 +151,6 @@ export class HttpServer {
98
151
99
152
const routeDefinition = this . routes [ route ] ?. [ httpMethod . data ] ;
100
153
101
- // logger.debug("Matched route", {
102
- // url,
103
- // method,
104
- // route,
105
- // routeDefinition,
106
- // });
107
-
108
154
if ( ! routeDefinition ) {
109
155
logger . error ( "Invalid method" , { url, method, parsedMethod : httpMethod . data } ) ;
110
156
return reply . empty ( 405 ) ;
@@ -141,25 +187,28 @@ export class HttpServer {
141
187
return reply . json ( { ok : false , error : "Invalid body" } , false , 400 ) ;
142
188
}
143
189
144
- try {
145
- await handler ( {
190
+ const [ error ] = await tryCatch (
191
+ handler ( {
146
192
reply,
147
193
req,
148
194
res,
149
195
params : parsedParams . data ,
150
196
queryParams : parsedQueryParams . data ,
151
197
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 } ) ;
155
203
return reply . empty ( 500 ) ;
156
204
}
157
205
} catch ( error ) {
158
206
logger . error ( "Failed to handle request" , { error } ) ;
159
207
return reply . empty ( 500 ) ;
208
+ } finally {
209
+ this . collectMetrics ( req , res , startTime ) ;
210
+ return ;
160
211
}
161
-
162
- return ;
163
212
} ) ;
164
213
165
214
this . server . on ( "clientError" , ( _ , socket ) => {
@@ -197,6 +246,25 @@ export class HttpServer {
197
246
} ) ;
198
247
}
199
248
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
+
200
268
private optionalSchema <
201
269
TSchema extends z . ZodFirstPartySchemaTypes | undefined ,
202
270
TData extends TSchema extends z . ZodFirstPartySchemaTypes ? z . TypeOf < TSchema > : TData ,
0 commit comments