@@ -23,6 +23,12 @@ import {
23
23
createTextParser ,
24
24
createURLEncodedParser ,
25
25
createMultipartParser ,
26
+ // Prometheus middleware functions
27
+ createPrometheusIntegration ,
28
+ createPrometheusMiddleware ,
29
+ createMetricsHandler ,
30
+ createDefaultMetrics ,
31
+ extractRoutePattern ,
26
32
// Type definitions
27
33
JWTAuthOptions ,
28
34
APIKeyAuthOptions ,
@@ -38,6 +44,11 @@ import {
38
44
MultipartParserOptions ,
39
45
JWKSLike ,
40
46
TokenExtractionOptions ,
47
+ // Prometheus type definitions
48
+ PrometheusMiddlewareOptions ,
49
+ MetricsHandlerOptions ,
50
+ PrometheusIntegration ,
51
+ PrometheusMetrics ,
41
52
// Available utility functions
42
53
extractTokenFromHeader ,
43
54
defaultKeyGenerator ,
@@ -390,6 +401,206 @@ const testBodyParserUtilities = (req: ZeroRequest) => {
390
401
const shouldParseJson = shouldParse ( req , 'application/json' )
391
402
}
392
403
404
+ // =============================================================================
405
+ // PROMETHEUS METRICS MIDDLEWARE VALIDATION
406
+ // =============================================================================
407
+
408
+ console . log ( '✅ Prometheus Metrics Middleware' )
409
+
410
+ // Clear the Prometheus registry at the start to avoid conflicts
411
+ try {
412
+ const promClient = require ( 'prom-client' )
413
+ promClient . register . clear ( )
414
+ } catch ( error ) {
415
+ // Ignore if prom-client is not available
416
+ }
417
+
418
+ // Test comprehensive Prometheus middleware options
419
+ const prometheusMiddlewareOptions : PrometheusMiddlewareOptions = {
420
+ // Use custom metrics to avoid registry conflicts
421
+ metrics : undefined , // Will create default metrics once
422
+
423
+ // Paths to exclude from metrics collection
424
+ excludePaths : [ '/health' , '/ping' , '/favicon.ico' , '/metrics' ] ,
425
+
426
+ // Whether to collect default Node.js metrics
427
+ collectDefaultMetrics : false , // Disable to avoid conflicts
428
+
429
+ // Custom route normalization function
430
+ normalizeRoute : ( req : ZeroRequest ) => {
431
+ const url = new URL ( req . url , 'http://localhost' )
432
+ let pathname = url . pathname
433
+
434
+ // Custom normalization logic
435
+ return pathname
436
+ . replace ( / \/ u s e r s \/ \d + / , '/users/:id' )
437
+ . replace ( / \/ a p i \/ v \d + / , '/api/:version' )
438
+ . replace ( / \/ i t e m s \/ [ a - f 0 - 9 - ] { 36 } / , '/items/:uuid' )
439
+ } ,
440
+
441
+ // Custom label extraction function
442
+ extractLabels : ( req : ZeroRequest , response : Response ) => {
443
+ return {
444
+ user_type : req . headers . get ( 'x-user-type' ) || 'anonymous' ,
445
+ api_version : req . headers . get ( 'x-api-version' ) || 'v1' ,
446
+ region : req . headers . get ( 'x-region' ) || 'us-east-1' ,
447
+ }
448
+ } ,
449
+
450
+ // HTTP methods to skip from metrics collection
451
+ skipMethods : [ 'OPTIONS' , 'HEAD' ] ,
452
+ }
453
+
454
+ // Test metrics handler options
455
+ const metricsHandlerOptions : MetricsHandlerOptions = {
456
+ endpoint : '/custom-metrics' ,
457
+ registry : undefined , // Would be prom-client registry in real usage
458
+ }
459
+
460
+ // Test creating individual components (create only once to avoid registry conflicts)
461
+ const defaultMetrics : PrometheusMetrics = createDefaultMetrics ( )
462
+ const prometheusMiddleware = createPrometheusMiddleware ( {
463
+ ...prometheusMiddlewareOptions ,
464
+ metrics : defaultMetrics ,
465
+ } )
466
+ const metricsHandler = createMetricsHandler ( metricsHandlerOptions )
467
+
468
+ // Test the integration function (use existing metrics)
469
+ const prometheusIntegration : PrometheusIntegration =
470
+ createPrometheusIntegration ( {
471
+ ...prometheusMiddlewareOptions ,
472
+ ...metricsHandlerOptions ,
473
+ metrics : defaultMetrics , // Reuse existing metrics
474
+ } )
475
+
476
+ // Test the integration object structure
477
+ const testPrometheusIntegration = ( ) => {
478
+ // Test middleware function
479
+ const middleware : RequestHandler = prometheusIntegration . middleware
480
+
481
+ // Test metrics handler function
482
+ const handler : RequestHandler = prometheusIntegration . metricsHandler
483
+
484
+ // Test registry access
485
+ const registry = prometheusIntegration . registry
486
+
487
+ // Test prom-client access for custom metrics
488
+ const promClient = prometheusIntegration . promClient
489
+ }
490
+
491
+ // Test default metrics structure
492
+ const testDefaultMetrics = ( ) => {
493
+ // Use the already created metrics to avoid registry conflicts
494
+ const metrics = defaultMetrics
495
+
496
+ // Test that all expected metrics are present
497
+ const duration = metrics . httpRequestDuration
498
+ const total = metrics . httpRequestTotal
499
+ const requestSize = metrics . httpRequestSize
500
+ const responseSize = metrics . httpResponseSize
501
+ const activeConnections = metrics . httpActiveConnections
502
+
503
+ // All should be defined (prom-client objects)
504
+ console . assert (
505
+ duration !== undefined ,
506
+ 'httpRequestDuration should be defined' ,
507
+ )
508
+ console . assert ( total !== undefined , 'httpRequestTotal should be defined' )
509
+ console . assert ( requestSize !== undefined , 'httpRequestSize should be defined' )
510
+ console . assert (
511
+ responseSize !== undefined ,
512
+ 'httpResponseSize should be defined' ,
513
+ )
514
+ console . assert (
515
+ activeConnections !== undefined ,
516
+ 'httpActiveConnections should be defined' ,
517
+ )
518
+ }
519
+
520
+ // Test route pattern extraction
521
+ const testRoutePatternExtraction = ( ) => {
522
+ // Mock request objects for testing (using unknown casting for test purposes)
523
+ const reqWithContext = {
524
+ ctx : { route : '/users/:id' } ,
525
+ url : 'http://localhost:3000/users/123' ,
526
+ } as unknown as ZeroRequest
527
+
528
+ const reqWithParams = {
529
+ url : 'http://localhost:3000/users/123' ,
530
+ params : { id : '123' } ,
531
+ } as unknown as ZeroRequest
532
+
533
+ const reqWithUUID = {
534
+ url : 'http://localhost:3000/items/550e8400-e29b-41d4-a716-446655440000' ,
535
+ } as unknown as ZeroRequest
536
+
537
+ const reqWithNumericId = {
538
+ url : 'http://localhost:3000/posts/12345' ,
539
+ } as unknown as ZeroRequest
540
+
541
+ const reqWithLongToken = {
542
+ url : 'http://localhost:3000/auth/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' ,
543
+ } as unknown as ZeroRequest
544
+
545
+ const reqMalformed = {
546
+ url : 'not-a-valid-url' ,
547
+ } as unknown as ZeroRequest
548
+
549
+ // Test route extraction
550
+ const pattern1 = extractRoutePattern ( reqWithContext )
551
+ const pattern2 = extractRoutePattern ( reqWithParams )
552
+ const pattern3 = extractRoutePattern ( reqWithUUID )
553
+ const pattern4 = extractRoutePattern ( reqWithNumericId )
554
+ const pattern5 = extractRoutePattern ( reqWithLongToken )
555
+ const pattern6 = extractRoutePattern ( reqMalformed )
556
+
557
+ // All should return strings (exact patterns depend on implementation)
558
+ console . assert ( typeof pattern1 === 'string' , 'Route pattern should be string' )
559
+ console . assert ( typeof pattern2 === 'string' , 'Route pattern should be string' )
560
+ console . assert ( typeof pattern3 === 'string' , 'Route pattern should be string' )
561
+ console . assert ( typeof pattern4 === 'string' , 'Route pattern should be string' )
562
+ console . assert ( typeof pattern5 === 'string' , 'Route pattern should be string' )
563
+ console . assert ( typeof pattern6 === 'string' , 'Route pattern should be string' )
564
+ }
565
+
566
+ // Test custom metrics scenarios
567
+ const testCustomMetricsScenarios = ( ) => {
568
+ // Create custom metrics object (reuse existing to avoid conflicts)
569
+ const customMetrics : PrometheusMetrics = defaultMetrics
570
+
571
+ // Use custom metrics in middleware
572
+ const middlewareWithCustomMetrics = createPrometheusMiddleware ( {
573
+ metrics : customMetrics ,
574
+ collectDefaultMetrics : false ,
575
+ } )
576
+
577
+ // Test minimal configuration (reuse existing metrics)
578
+ const minimalMiddleware = createPrometheusMiddleware ( {
579
+ metrics : customMetrics ,
580
+ collectDefaultMetrics : false ,
581
+ } )
582
+ const minimalIntegration = createPrometheusIntegration ( {
583
+ metrics : customMetrics ,
584
+ collectDefaultMetrics : false ,
585
+ } )
586
+
587
+ // Test with only specific options
588
+ const selectiveOptions : PrometheusMiddlewareOptions = {
589
+ excludePaths : [ '/api/internal/*' ] ,
590
+ skipMethods : [ 'TRACE' , 'CONNECT' ] ,
591
+ metrics : customMetrics , // Reuse existing
592
+ collectDefaultMetrics : false , // Disable to avoid conflicts
593
+ }
594
+
595
+ const selectiveMiddleware = createPrometheusMiddleware ( selectiveOptions )
596
+ }
597
+
598
+ // Execute Prometheus tests
599
+ testPrometheusIntegration ( )
600
+ testDefaultMetrics ( )
601
+ testRoutePatternExtraction ( )
602
+ testCustomMetricsScenarios ( )
603
+
393
604
// =============================================================================
394
605
// COMPLEX INTEGRATION SCENARIOS
395
606
// =============================================================================
@@ -434,6 +645,27 @@ const fullMiddlewareStack = () => {
434
645
} ) ,
435
646
)
436
647
648
+ // Prometheus metrics middleware (reuse existing metrics to avoid registry conflicts)
649
+ router . use (
650
+ createPrometheusMiddleware ( {
651
+ metrics : defaultMetrics , // Reuse existing metrics
652
+ collectDefaultMetrics : false , // Disable to avoid conflicts
653
+ excludePaths : [ '/health' , '/metrics' ] ,
654
+ extractLabels : ( req : ZeroRequest , response : Response ) => ( {
655
+ user_type : req . ctx ?. user ?. type || 'anonymous' ,
656
+ api_version : req . headers . get ( 'x-api-version' ) || 'v1' ,
657
+ } ) ,
658
+ } ) ,
659
+ )
660
+
661
+ // Metrics endpoint (reuse existing metrics)
662
+ const prometheusIntegration = createPrometheusIntegration ( {
663
+ endpoint : '/metrics' ,
664
+ metrics : defaultMetrics , // Reuse existing metrics
665
+ collectDefaultMetrics : false , // Disable to avoid conflicts
666
+ } )
667
+ router . get ( '/metrics' , prometheusIntegration . metricsHandler )
668
+
437
669
// JWT authentication for API routes
438
670
router . use (
439
671
'/api/*' ,
@@ -554,6 +786,12 @@ const runValidations = async () => {
554
786
testRateLimitUtilities ( mockRequest )
555
787
testCORSUtilities ( mockRequest )
556
788
testBodyParserUtilities ( mockRequest )
789
+
790
+ // Test Prometheus utilities
791
+ testPrometheusIntegration ( )
792
+ testDefaultMetrics ( )
793
+ testRoutePatternExtraction ( )
794
+ testCustomMetricsScenarios ( )
557
795
}
558
796
559
797
// Run all validations
@@ -567,6 +805,7 @@ runValidations()
567
805
console . log ( '✅ Rate limiting middleware' )
568
806
console . log ( '✅ CORS middleware' )
569
807
console . log ( '✅ Body parser middleware' )
808
+ console . log ( '✅ Prometheus metrics middleware' )
570
809
console . log ( '✅ Complex integration scenarios' )
571
810
console . log ( '✅ Error handling scenarios' )
572
811
console . log ( '✅ Async middleware patterns' )
0 commit comments