0http-bun 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/middleware/README.md +281 -2
- package/lib/middleware/index.d.ts +66 -2
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/jwt-auth.js +29 -11
- package/lib/middleware/logger.js +19 -4
- package/lib/middleware/prometheus.js +491 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -206,11 +206,14 @@ Bun.serve({
|
|
|
206
206
|
|
|
207
207
|
0http-bun includes a comprehensive middleware system with built-in middlewares for common use cases:
|
|
208
208
|
|
|
209
|
+
> 📦 **Note**: Starting with v1.2.2, some middleware dependencies are optional. Install only what you need: `jose` (JWT), `pino` (Logger), `prom-client` (Prometheus).
|
|
210
|
+
|
|
209
211
|
- **[Body Parser](./lib/middleware/README.md#body-parser)** - Automatic request body parsing (JSON, form data, text)
|
|
210
212
|
- **[CORS](./lib/middleware/README.md#cors)** - Cross-Origin Resource Sharing with flexible configuration
|
|
211
213
|
- **[JWT Authentication](./lib/middleware/README.md#jwt-authentication)** - JSON Web Token authentication and authorization
|
|
212
214
|
- **[Logger](./lib/middleware/README.md#logger)** - Request logging with multiple output formats
|
|
213
215
|
- **[Rate Limiting](./lib/middleware/README.md#rate-limiting)** - Flexible rate limiting with sliding window support
|
|
216
|
+
- **[Prometheus Metrics](./lib/middleware/README.md#prometheus-metrics)** - Export metrics for monitoring and alerting
|
|
214
217
|
|
|
215
218
|
### Quick Example
|
|
216
219
|
|
package/lib/middleware/README.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
0http-bun provides a comprehensive middleware system with built-in middlewares for common use cases. All middleware functions are TypeScript-ready and follow the standard middleware pattern.
|
|
4
4
|
|
|
5
|
+
## Dependency Installation
|
|
6
|
+
|
|
7
|
+
⚠️ **Important**: Starting with v1.2.2, middleware dependencies are now **optional** and must be installed separately when needed. This reduces the framework's footprint and improves startup performance through lazy loading.
|
|
8
|
+
|
|
9
|
+
Install only the dependencies you need:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# For JWT Authentication middleware
|
|
13
|
+
bun install jose
|
|
14
|
+
|
|
15
|
+
# For Logger middleware
|
|
16
|
+
bun install pino
|
|
17
|
+
|
|
18
|
+
# For Prometheus Metrics middleware
|
|
19
|
+
bun install prom-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Benefits of Lazy Loading:**
|
|
23
|
+
|
|
24
|
+
- 📦 **Smaller Bundle**: Only install what you use
|
|
25
|
+
- ⚡ **Faster Startup**: Dependencies loaded only when middleware is used
|
|
26
|
+
- 💾 **Lower Memory**: Reduced initial memory footprint
|
|
27
|
+
- 🔧 **Better Control**: Explicit dependency management
|
|
28
|
+
|
|
5
29
|
## Table of Contents
|
|
6
30
|
|
|
7
31
|
- [Middleware Pattern](#middleware-pattern)
|
|
@@ -10,6 +34,7 @@
|
|
|
10
34
|
- [CORS](#cors)
|
|
11
35
|
- [JWT Authentication](#jwt-authentication)
|
|
12
36
|
- [Logger](#logger)
|
|
37
|
+
- [Prometheus Metrics](#prometheus-metrics)
|
|
13
38
|
- [Rate Limiting](#rate-limiting)
|
|
14
39
|
- [Creating Custom Middleware](#creating-custom-middleware)
|
|
15
40
|
|
|
@@ -50,6 +75,7 @@ import {
|
|
|
50
75
|
createLogger,
|
|
51
76
|
createJWTAuth,
|
|
52
77
|
createRateLimit,
|
|
78
|
+
createPrometheusIntegration,
|
|
53
79
|
} from '0http-bun/lib/middleware'
|
|
54
80
|
```
|
|
55
81
|
|
|
@@ -65,6 +91,9 @@ const {
|
|
|
65
91
|
createJWTAuth,
|
|
66
92
|
createLogger,
|
|
67
93
|
createRateLimit,
|
|
94
|
+
createPrometheusMiddleware,
|
|
95
|
+
createMetricsHandler,
|
|
96
|
+
createPrometheusIntegration,
|
|
68
97
|
} = require('0http-bun/lib/middleware')
|
|
69
98
|
```
|
|
70
99
|
|
|
@@ -78,6 +107,9 @@ import {
|
|
|
78
107
|
createJWTAuth,
|
|
79
108
|
createLogger,
|
|
80
109
|
createRateLimit,
|
|
110
|
+
createPrometheusMiddleware,
|
|
111
|
+
createMetricsHandler,
|
|
112
|
+
createPrometheusIntegration,
|
|
81
113
|
} from '0http-bun/lib/middleware'
|
|
82
114
|
|
|
83
115
|
// Import types
|
|
@@ -94,6 +126,8 @@ import type {
|
|
|
94
126
|
|
|
95
127
|
Automatically parses request bodies based on Content-Type header.
|
|
96
128
|
|
|
129
|
+
> ✅ **No additional dependencies required** - Uses Bun's built-in parsing capabilities.
|
|
130
|
+
|
|
97
131
|
```javascript
|
|
98
132
|
const {createBodyParser} = require('0http-bun/lib/middleware')
|
|
99
133
|
|
|
@@ -142,6 +176,8 @@ router.use(createBodyParser(bodyParserOptions))
|
|
|
142
176
|
|
|
143
177
|
Cross-Origin Resource Sharing middleware with flexible configuration.
|
|
144
178
|
|
|
179
|
+
> ✅ **No additional dependencies required** - Built-in CORS implementation.
|
|
180
|
+
|
|
145
181
|
```javascript
|
|
146
182
|
const {createCORS} = require('0http-bun/lib/middleware')
|
|
147
183
|
|
|
@@ -194,6 +230,8 @@ router.use(createCORS(corsOptions))
|
|
|
194
230
|
|
|
195
231
|
JSON Web Token authentication and authorization middleware with support for static secrets, JWKS endpoints, and API key authentication.
|
|
196
232
|
|
|
233
|
+
> 📦 **Required dependency**: `bun install jose`
|
|
234
|
+
|
|
197
235
|
#### Basic JWT with Static Secret
|
|
198
236
|
|
|
199
237
|
```javascript
|
|
@@ -445,6 +483,9 @@ router.get('/api/profile', (req) => {
|
|
|
445
483
|
|
|
446
484
|
Request logging middleware with customizable output formats.
|
|
447
485
|
|
|
486
|
+
> 📦 **Required dependency for structured logging**: `bun install pino`
|
|
487
|
+
> ✅ **Simple logger** (`simpleLogger`) has no dependencies - uses `console.log`
|
|
488
|
+
|
|
448
489
|
```javascript
|
|
449
490
|
const {createLogger, simpleLogger} = require('0http-bun/lib/middleware')
|
|
450
491
|
|
|
@@ -503,10 +544,228 @@ router.use(createLogger(loggerOptions))
|
|
|
503
544
|
- `tiny` - Minimal output
|
|
504
545
|
- `dev` - Development-friendly colored output
|
|
505
546
|
|
|
547
|
+
### Prometheus Metrics
|
|
548
|
+
|
|
549
|
+
Comprehensive Prometheus metrics integration for monitoring and observability with built-in security and performance optimizations.
|
|
550
|
+
|
|
551
|
+
> 📦 **Required dependency**: `bun install prom-client`
|
|
552
|
+
|
|
553
|
+
```javascript
|
|
554
|
+
const {
|
|
555
|
+
createPrometheusMiddleware,
|
|
556
|
+
createMetricsHandler,
|
|
557
|
+
createPrometheusIntegration,
|
|
558
|
+
} = require('0http-bun/lib/middleware')
|
|
559
|
+
|
|
560
|
+
// Simple setup with default metrics
|
|
561
|
+
const prometheus = createPrometheusIntegration()
|
|
562
|
+
|
|
563
|
+
router.use(prometheus.middleware)
|
|
564
|
+
router.get('/metrics', prometheus.metricsHandler)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### Default Metrics Collected
|
|
568
|
+
|
|
569
|
+
The Prometheus middleware automatically collects:
|
|
570
|
+
|
|
571
|
+
- **HTTP Request Duration** - Histogram of request durations in seconds (buckets: 0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10)
|
|
572
|
+
- **HTTP Request Count** - Counter of total requests by method, route, and status
|
|
573
|
+
- **HTTP Request Size** - Histogram of request body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
|
|
574
|
+
- **HTTP Response Size** - Histogram of response body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
|
|
575
|
+
- **Active Connections** - Gauge of currently active HTTP connections
|
|
576
|
+
- **Node.js Metrics** - Memory usage, CPU, garbage collection (custom buckets), event loop lag (5ms precision)
|
|
577
|
+
|
|
578
|
+
#### Advanced Configuration
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
const {
|
|
582
|
+
createPrometheusMiddleware,
|
|
583
|
+
createMetricsHandler,
|
|
584
|
+
createPrometheusIntegration,
|
|
585
|
+
} = require('0http-bun/lib/middleware')
|
|
586
|
+
|
|
587
|
+
const prometheus = createPrometheusIntegration({
|
|
588
|
+
// Control default Node.js metrics collection
|
|
589
|
+
collectDefaultMetrics: true,
|
|
590
|
+
|
|
591
|
+
// Exclude paths from metrics collection (optimized for performance)
|
|
592
|
+
excludePaths: ['/health', '/ping', '/favicon.ico'],
|
|
593
|
+
|
|
594
|
+
// Skip certain HTTP methods
|
|
595
|
+
skipMethods: ['OPTIONS'],
|
|
596
|
+
|
|
597
|
+
// Custom route normalization with security controls
|
|
598
|
+
normalizeRoute: (req) => {
|
|
599
|
+
const url = new URL(req.url, 'http://localhost')
|
|
600
|
+
return url.pathname
|
|
601
|
+
.replace(/\/users\/\d+/, '/users/:id')
|
|
602
|
+
.replace(/\/api\/v\d+/, '/api/:version')
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Add custom labels with automatic sanitization
|
|
606
|
+
extractLabels: (req, response) => {
|
|
607
|
+
return {
|
|
608
|
+
user_type: req.headers.get('x-user-type') || 'anonymous',
|
|
609
|
+
api_version: req.headers.get('x-api-version') || 'v1',
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
// Use custom metrics object instead of default metrics
|
|
614
|
+
metrics: customMetricsObject,
|
|
615
|
+
})
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### Custom Business Metrics
|
|
619
|
+
|
|
620
|
+
```javascript
|
|
621
|
+
const {
|
|
622
|
+
createPrometheusIntegration,
|
|
623
|
+
} = require('0http-bun/lib/middleware')
|
|
624
|
+
|
|
625
|
+
// Get the prometheus client from the integration
|
|
626
|
+
const prometheus = createPrometheusIntegration()
|
|
627
|
+
const {promClient} = prometheus
|
|
628
|
+
|
|
629
|
+
// Create custom metrics
|
|
630
|
+
const orderCounter = new promClient.Counter({
|
|
631
|
+
name: 'orders_total',
|
|
632
|
+
help: 'Total number of orders processed',
|
|
633
|
+
labelNames: ['status', 'payment_method'],
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const orderValue = new promClient.Histogram({
|
|
637
|
+
name: 'order_value_dollars',
|
|
638
|
+
help: 'Value of orders in dollars',
|
|
639
|
+
labelNames: ['payment_method'],
|
|
640
|
+
buckets: [10, 50, 100, 500, 1000, 5000],
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
// Use in your routes
|
|
644
|
+
router.post('/orders', async (req) => {
|
|
645
|
+
const order = await processOrder(req.body)
|
|
646
|
+
|
|
647
|
+
// Record custom metrics
|
|
648
|
+
orderCounter.inc({
|
|
649
|
+
status: order.status,
|
|
650
|
+
payment_method: order.payment_method,
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
if (order.status === 'completed') {
|
|
654
|
+
orderValue.observe(
|
|
655
|
+
{
|
|
656
|
+
payment_method: order.payment_method,
|
|
657
|
+
},
|
|
658
|
+
order.amount,
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return Response.json(order)
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
#### Metrics Endpoint Options
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
const {createMetricsHandler} = require('0http-bun/lib/middleware')
|
|
670
|
+
|
|
671
|
+
// Custom metrics endpoint
|
|
672
|
+
const metricsHandler = createMetricsHandler({
|
|
673
|
+
endpoint: '/custom-metrics', // Default: '/metrics'
|
|
674
|
+
registry: customRegistry, // Default: promClient.register
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
router.get('/custom-metrics', metricsHandler)
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### Route Normalization & Security
|
|
681
|
+
|
|
682
|
+
The middleware automatically normalizes routes and implements security measures to prevent high cardinality and potential attacks:
|
|
683
|
+
|
|
684
|
+
```javascript
|
|
685
|
+
// URLs like these:
|
|
686
|
+
// /users/123, /users/456, /users/789
|
|
687
|
+
// Are normalized to: /users/:id
|
|
688
|
+
|
|
689
|
+
// /products/abc-123, /products/def-456
|
|
690
|
+
// Are normalized to: /products/:slug
|
|
691
|
+
|
|
692
|
+
// /api/v1/data, /api/v2/data
|
|
693
|
+
// Are normalized to: /api/:version/data
|
|
694
|
+
|
|
695
|
+
// Route sanitization examples:
|
|
696
|
+
// /users/:id → _users__id (special characters replaced with underscores)
|
|
697
|
+
// /api/v1/orders → _api_v1_orders
|
|
698
|
+
// Very long tokens → _api__token (pattern-based normalization)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Route Sanitization:**
|
|
702
|
+
|
|
703
|
+
- Special characters (`/`, `:`, etc.) are replaced with underscores (`_`) for Prometheus compatibility
|
|
704
|
+
- UUIDs are automatically normalized to `:id` patterns
|
|
705
|
+
- Long tokens (>20 characters) are normalized to `:token` patterns
|
|
706
|
+
- Numeric IDs are normalized to `:id` patterns
|
|
707
|
+
- Route complexity is limited to 10 segments maximum
|
|
708
|
+
|
|
709
|
+
**Security Features:**
|
|
710
|
+
|
|
711
|
+
- **Label Sanitization**: Removes potentially dangerous characters from metric labels and truncates values to 100 characters
|
|
712
|
+
- **Cardinality Limits**: Prevents memory exhaustion from too many unique metric combinations
|
|
713
|
+
- **Route Complexity Limits**: Caps the number of route segments to 10 to prevent DoS attacks
|
|
714
|
+
- **Size Limits**: Limits request/response body size processing (up to 100MB) to prevent memory issues
|
|
715
|
+
- **Header Processing Limits**: Caps the number of headers processed per request (50 for requests, 20 for responses)
|
|
716
|
+
- **URL Processing**: Handles both full URLs and pathname-only URLs with proper fallback handling
|
|
717
|
+
|
|
718
|
+
#### Performance Optimizations
|
|
719
|
+
|
|
720
|
+
- **Fast Path for Excluded Routes**: Bypasses all metric collection for excluded paths with smart URL parsing
|
|
721
|
+
- **Lazy Evaluation**: Only processes metrics when actually needed
|
|
722
|
+
- **Efficient Size Calculation**: Optimized request/response size measurement with capping at 1MB estimation
|
|
723
|
+
- **Error Handling**: Graceful handling of malformed URLs and invalid data with fallback mechanisms
|
|
724
|
+
- **Header Count Limits**: Prevents excessive header processing overhead (50 request headers, 20 response headers)
|
|
725
|
+
- **Smart URL Parsing**: Handles both full URLs and pathname-only URLs efficiently
|
|
726
|
+
|
|
727
|
+
#### Production Considerations
|
|
728
|
+
|
|
729
|
+
- **Performance**: Adds <1ms overhead per request with optimized fast paths
|
|
730
|
+
- **Memory**: Metrics stored in memory with cardinality controls; use recording rules for high cardinality
|
|
731
|
+
- **Security**: Built-in protections against label injection and cardinality bombs
|
|
732
|
+
- **Cardinality**: Automatic limits prevent high cardinality issues
|
|
733
|
+
- **Monitoring**: Consider protecting `/metrics` endpoint in production
|
|
734
|
+
|
|
735
|
+
#### Integration with Monitoring
|
|
736
|
+
|
|
737
|
+
```yaml
|
|
738
|
+
# prometheus.yml
|
|
739
|
+
scrape_configs:
|
|
740
|
+
- job_name: '0http-bun-app'
|
|
741
|
+
static_configs:
|
|
742
|
+
- targets: ['localhost:3000']
|
|
743
|
+
scrape_interval: 15s
|
|
744
|
+
metrics_path: /metrics
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
#### Troubleshooting
|
|
748
|
+
|
|
749
|
+
**Common Issues:**
|
|
750
|
+
|
|
751
|
+
- **High Memory Usage**: Check for high cardinality metrics. Route patterns should be normalized (e.g., `/users/:id` not `/users/12345`)
|
|
752
|
+
- **Missing Metrics**: Ensure paths aren't in `excludePaths` and HTTP methods aren't in `skipMethods`
|
|
753
|
+
- **Route Sanitization**: Routes are automatically sanitized (special characters become underscores: `/users/:id` → `_users__id`)
|
|
754
|
+
- **URL Parsing Errors**: The middleware handles both full URLs and pathname-only URLs with graceful fallback
|
|
755
|
+
|
|
756
|
+
**Performance Tips:**
|
|
757
|
+
|
|
758
|
+
- Use `excludePaths` for health checks and static assets
|
|
759
|
+
- Consider using `skipMethods` for OPTIONS requests
|
|
760
|
+
- Monitor memory usage in production for metric cardinality
|
|
761
|
+
- Use Prometheus recording rules for high-cardinality aggregations
|
|
762
|
+
|
|
506
763
|
### Rate Limiting
|
|
507
764
|
|
|
508
765
|
Configurable rate limiting middleware with multiple store options.
|
|
509
766
|
|
|
767
|
+
> ✅ **No additional dependencies required** - Uses built-in memory store.
|
|
768
|
+
|
|
510
769
|
```javascript
|
|
511
770
|
const {createRateLimit, MemoryStore} = require('0http-bun/lib/middleware')
|
|
512
771
|
|
|
@@ -794,8 +1053,8 @@ Apply middleware only to specific paths:
|
|
|
794
1053
|
|
|
795
1054
|
```typescript
|
|
796
1055
|
// API-only middleware
|
|
797
|
-
router.use('/api/*',
|
|
798
|
-
router.use('/api/*',
|
|
1056
|
+
router.use('/api/*', createJWTAuth({secret: 'api-secret'}))
|
|
1057
|
+
router.use('/api/*', createRateLimit({max: 1000}))
|
|
799
1058
|
|
|
800
1059
|
// Admin-only middleware
|
|
801
1060
|
router.use('/admin/*', adminAuthMiddleware)
|
|
@@ -867,4 +1126,24 @@ router.get('/api/public/status', () => Response.json({status: 'ok'}))
|
|
|
867
1126
|
router.get('/api/protected/data', (req) => Response.json({user: req.user}))
|
|
868
1127
|
```
|
|
869
1128
|
|
|
1129
|
+
## Dependency Summary
|
|
1130
|
+
|
|
1131
|
+
For your convenience, here's a quick reference of which dependencies you need to install for each middleware:
|
|
1132
|
+
|
|
1133
|
+
| Middleware | Dependencies Required | Install Command |
|
|
1134
|
+
| ----------------------- | --------------------- | ------------------------- |
|
|
1135
|
+
| **Body Parser** | ✅ None | Built-in |
|
|
1136
|
+
| **CORS** | ✅ None | Built-in |
|
|
1137
|
+
| **Rate Limiting** | ✅ None | Built-in |
|
|
1138
|
+
| **Logger** (simple) | ✅ None | Built-in |
|
|
1139
|
+
| **Logger** (structured) | 📦 `pino` | `bun install pino` |
|
|
1140
|
+
| **JWT Authentication** | 📦 `jose` | `bun install jose` |
|
|
1141
|
+
| **Prometheus Metrics** | 📦 `prom-client` | `bun install prom-client` |
|
|
1142
|
+
|
|
1143
|
+
**Install all optional dependencies at once:**
|
|
1144
|
+
|
|
1145
|
+
```bash
|
|
1146
|
+
bun install pino jose prom-client
|
|
1147
|
+
```
|
|
1148
|
+
|
|
870
1149
|
This middleware stack provides a solid foundation for most web applications with security, logging, and performance features built-in.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {RequestHandler, ZeroRequest
|
|
2
|
-
import {Logger} from 'pino'
|
|
1
|
+
import {RequestHandler, ZeroRequest} from '../../common'
|
|
3
2
|
|
|
4
3
|
// Logger middleware types
|
|
5
4
|
export interface LoggerOptions {
|
|
@@ -234,3 +233,68 @@ export function createMultipartParser(
|
|
|
234
233
|
export function createBodyParser(options?: BodyParserOptions): RequestHandler
|
|
235
234
|
export function hasBody(req: ZeroRequest): boolean
|
|
236
235
|
export function shouldParse(req: ZeroRequest, type: string): boolean
|
|
236
|
+
|
|
237
|
+
// Prometheus metrics middleware types
|
|
238
|
+
export interface PrometheusMetrics {
|
|
239
|
+
httpRequestDuration: any // prom-client Histogram
|
|
240
|
+
httpRequestTotal: any // prom-client Counter
|
|
241
|
+
httpRequestSize: any // prom-client Histogram
|
|
242
|
+
httpResponseSize: any // prom-client Histogram
|
|
243
|
+
httpActiveConnections: any // prom-client Gauge
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface PrometheusMiddlewareOptions {
|
|
247
|
+
/** Custom metrics object to use instead of default metrics */
|
|
248
|
+
metrics?: PrometheusMetrics
|
|
249
|
+
/** Paths to exclude from metrics collection (default: ['/health', '/ping', '/favicon.ico', '/metrics']) */
|
|
250
|
+
excludePaths?: string[]
|
|
251
|
+
/** Whether to collect default Node.js metrics (default: true) */
|
|
252
|
+
collectDefaultMetrics?: boolean
|
|
253
|
+
/** Custom route normalization function */
|
|
254
|
+
normalizeRoute?: (req: ZeroRequest) => string
|
|
255
|
+
/** Custom label extraction function */
|
|
256
|
+
extractLabels?: (
|
|
257
|
+
req: ZeroRequest,
|
|
258
|
+
response: Response,
|
|
259
|
+
) => Record<string, string>
|
|
260
|
+
/** HTTP methods to skip from metrics collection */
|
|
261
|
+
skipMethods?: string[]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface MetricsHandlerOptions {
|
|
265
|
+
/** The endpoint path for metrics (default: '/metrics') */
|
|
266
|
+
endpoint?: string
|
|
267
|
+
/** Custom Prometheus registry to use */
|
|
268
|
+
registry?: any // prom-client Registry
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface PrometheusIntegration {
|
|
272
|
+
/** The middleware function */
|
|
273
|
+
middleware: RequestHandler
|
|
274
|
+
/** The metrics handler function */
|
|
275
|
+
metricsHandler: RequestHandler
|
|
276
|
+
/** The Prometheus registry */
|
|
277
|
+
registry: any // prom-client Registry
|
|
278
|
+
/** The prom-client module */
|
|
279
|
+
promClient: any
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function createPrometheusMiddleware(
|
|
283
|
+
options?: PrometheusMiddlewareOptions,
|
|
284
|
+
): RequestHandler
|
|
285
|
+
export function createMetricsHandler(
|
|
286
|
+
options?: MetricsHandlerOptions,
|
|
287
|
+
): RequestHandler
|
|
288
|
+
export function createPrometheusIntegration(
|
|
289
|
+
options?: PrometheusMiddlewareOptions & MetricsHandlerOptions,
|
|
290
|
+
): PrometheusIntegration
|
|
291
|
+
export function createDefaultMetrics(): PrometheusMetrics
|
|
292
|
+
export function extractRoutePattern(req: ZeroRequest): string
|
|
293
|
+
|
|
294
|
+
// Simple interface exports for common use cases
|
|
295
|
+
export const logger: typeof createLogger
|
|
296
|
+
export const jwtAuth: typeof createJWTAuth
|
|
297
|
+
export const rateLimit: typeof createRateLimit
|
|
298
|
+
export const cors: typeof createCORS
|
|
299
|
+
export const bodyParser: typeof createBodyParser
|
|
300
|
+
export const prometheus: typeof createPrometheusIntegration
|
package/lib/middleware/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const jwtAuthModule = require('./jwt-auth')
|
|
|
4
4
|
const rateLimitModule = require('./rate-limit')
|
|
5
5
|
const corsModule = require('./cors')
|
|
6
6
|
const bodyParserModule = require('./body-parser')
|
|
7
|
+
const prometheusModule = require('./prometheus')
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
// Simple interface for common use cases (matches test expectations)
|
|
@@ -12,6 +13,7 @@ module.exports = {
|
|
|
12
13
|
rateLimit: rateLimitModule.createRateLimit,
|
|
13
14
|
cors: corsModule.createCORS,
|
|
14
15
|
bodyParser: bodyParserModule.createBodyParser,
|
|
16
|
+
prometheus: prometheusModule.createPrometheusIntegration,
|
|
15
17
|
|
|
16
18
|
// Complete factory functions for advanced usage
|
|
17
19
|
createLogger: loggerModule.createLogger,
|
|
@@ -42,4 +44,11 @@ module.exports = {
|
|
|
42
44
|
createBodyParser: bodyParserModule.createBodyParser,
|
|
43
45
|
hasBody: bodyParserModule.hasBody,
|
|
44
46
|
shouldParse: bodyParserModule.shouldParse,
|
|
47
|
+
|
|
48
|
+
// Prometheus metrics middleware
|
|
49
|
+
createPrometheusMiddleware: prometheusModule.createPrometheusMiddleware,
|
|
50
|
+
createMetricsHandler: prometheusModule.createMetricsHandler,
|
|
51
|
+
createPrometheusIntegration: prometheusModule.createPrometheusIntegration,
|
|
52
|
+
createDefaultMetrics: prometheusModule.createDefaultMetrics,
|
|
53
|
+
extractRoutePattern: prometheusModule.extractRoutePattern,
|
|
45
54
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Lazy load jose to improve startup performance
|
|
2
|
+
let joseLib = null
|
|
3
|
+
function loadJose() {
|
|
4
|
+
if (!joseLib) {
|
|
5
|
+
try {
|
|
6
|
+
joseLib = require('jose')
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'jose is required for JWT middleware. Install it with: bun install jose',
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return joseLib
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
/**
|
|
4
17
|
* Creates JWT authentication middleware
|
|
@@ -60,6 +73,7 @@ function createJWTAuth(options = {}) {
|
|
|
60
73
|
keyLike = jwks
|
|
61
74
|
}
|
|
62
75
|
} else if (jwksUri) {
|
|
76
|
+
const {createRemoteJWKSet} = loadJose()
|
|
63
77
|
keyLike = createRemoteJWKSet(new URL(jwksUri))
|
|
64
78
|
} else if (typeof secret === 'function') {
|
|
65
79
|
keyLike = secret
|
|
@@ -143,6 +157,7 @@ function createJWTAuth(options = {}) {
|
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
// Verify JWT token
|
|
160
|
+
const {jwtVerify} = loadJose()
|
|
146
161
|
const {payload, protectedHeader} = await jwtVerify(
|
|
147
162
|
token,
|
|
148
163
|
keyLike,
|
|
@@ -302,16 +317,19 @@ function handleAuthError(error, handlers = {}, req) {
|
|
|
302
317
|
message = 'Invalid API key'
|
|
303
318
|
} else if (error.message === 'JWT verification not configured') {
|
|
304
319
|
message = 'JWT verification not configured'
|
|
305
|
-
} else
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
} else {
|
|
321
|
+
const {errors} = loadJose()
|
|
322
|
+
if (error instanceof errors.JWTExpired) {
|
|
323
|
+
message = 'Token expired'
|
|
324
|
+
} else if (error instanceof errors.JWTInvalid) {
|
|
325
|
+
message = 'Invalid token format'
|
|
326
|
+
} else if (error instanceof errors.JWKSNoMatchingKey) {
|
|
327
|
+
message = 'Token signature verification failed'
|
|
328
|
+
} else if (error.message.includes('audience')) {
|
|
329
|
+
message = 'Invalid token audience'
|
|
330
|
+
} else if (error.message.includes('issuer')) {
|
|
331
|
+
message = 'Invalid token issuer'
|
|
332
|
+
}
|
|
315
333
|
}
|
|
316
334
|
|
|
317
335
|
return new Response(JSON.stringify({error: message}), {
|
package/lib/middleware/logger.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
const pino = require('pino')
|
|
2
1
|
const crypto = require('crypto')
|
|
3
2
|
|
|
3
|
+
// Lazy load pino to improve startup performance
|
|
4
|
+
let pino = null
|
|
5
|
+
function loadPino() {
|
|
6
|
+
if (!pino) {
|
|
7
|
+
try {
|
|
8
|
+
pino = require('pino')
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'pino is required for logger middleware. Install it with: bun install pino',
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return pino
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
/**
|
|
5
19
|
* Creates a logging middleware using Pino logger
|
|
6
20
|
* @param {Object} options - Logger configuration options
|
|
@@ -27,9 +41,10 @@ function createLogger(options = {}) {
|
|
|
27
41
|
} = options
|
|
28
42
|
|
|
29
43
|
// Build final pino options with proper precedence
|
|
44
|
+
const pinoLib = loadPino()
|
|
30
45
|
const finalPinoOptions = {
|
|
31
46
|
level: level || pinoOptions.level || process.env.LOG_LEVEL || 'info',
|
|
32
|
-
timestamp:
|
|
47
|
+
timestamp: pinoLib.stdTimeFunctions.isoTime,
|
|
33
48
|
formatters: {
|
|
34
49
|
level: (label) => ({level: label.toUpperCase()}),
|
|
35
50
|
},
|
|
@@ -41,7 +56,7 @@ function createLogger(options = {}) {
|
|
|
41
56
|
...(logBody && req.body ? {body: req.body} : {}),
|
|
42
57
|
}),
|
|
43
58
|
// Default res serializer removed to allow logResponse to handle it fully
|
|
44
|
-
err:
|
|
59
|
+
err: pinoLib.stdSerializers.err,
|
|
45
60
|
// Merge in custom serializers if provided
|
|
46
61
|
...(serializers || {}),
|
|
47
62
|
},
|
|
@@ -49,7 +64,7 @@ function createLogger(options = {}) {
|
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
// Use injected logger if provided (for tests), otherwise create a new one
|
|
52
|
-
const logger = injectedLogger ||
|
|
67
|
+
const logger = injectedLogger || pinoLib(finalPinoOptions)
|
|
53
68
|
|
|
54
69
|
return function loggerMiddleware(req, next) {
|
|
55
70
|
const startTime = process.hrtime.bigint()
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// Lazy load prom-client to improve startup performance
|
|
2
|
+
let promClient = null
|
|
3
|
+
function loadPromClient() {
|
|
4
|
+
if (!promClient) {
|
|
5
|
+
try {
|
|
6
|
+
promClient = require('prom-client')
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'prom-client is required for Prometheus middleware. Install it with: bun install prom-client',
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return promClient
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Security: Limit label cardinality
|
|
17
|
+
const MAX_LABEL_VALUE_LENGTH = 100
|
|
18
|
+
const MAX_ROUTE_SEGMENTS = 10
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sanitize label values to prevent high cardinality
|
|
22
|
+
*/
|
|
23
|
+
function sanitizeLabelValue(value) {
|
|
24
|
+
if (typeof value !== 'string') {
|
|
25
|
+
value = String(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Truncate long values
|
|
29
|
+
if (value.length > MAX_LABEL_VALUE_LENGTH) {
|
|
30
|
+
value = value.substring(0, MAX_LABEL_VALUE_LENGTH)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Replace invalid characters
|
|
34
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate route pattern to prevent injection attacks
|
|
39
|
+
*/
|
|
40
|
+
function validateRoute(route) {
|
|
41
|
+
if (typeof route !== 'string' || route.length === 0) {
|
|
42
|
+
return '/unknown'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Limit route complexity
|
|
46
|
+
const segments = route.split('/').filter(Boolean)
|
|
47
|
+
if (segments.length > MAX_ROUTE_SEGMENTS) {
|
|
48
|
+
return '/' + segments.slice(0, MAX_ROUTE_SEGMENTS).join('/')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sanitizeLabelValue(route)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Default Prometheus metrics for HTTP requests
|
|
56
|
+
*/
|
|
57
|
+
function createDefaultMetrics() {
|
|
58
|
+
const client = loadPromClient()
|
|
59
|
+
|
|
60
|
+
// HTTP request duration histogram
|
|
61
|
+
const httpRequestDuration = new client.Histogram({
|
|
62
|
+
name: 'http_request_duration_seconds',
|
|
63
|
+
help: 'Duration of HTTP requests in seconds',
|
|
64
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
65
|
+
buckets: [0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// HTTP request counter
|
|
69
|
+
const httpRequestTotal = new client.Counter({
|
|
70
|
+
name: 'http_requests_total',
|
|
71
|
+
help: 'Total number of HTTP requests',
|
|
72
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// HTTP request size histogram
|
|
76
|
+
const httpRequestSize = new client.Histogram({
|
|
77
|
+
name: 'http_request_size_bytes',
|
|
78
|
+
help: 'Size of HTTP requests in bytes',
|
|
79
|
+
labelNames: ['method', 'route'],
|
|
80
|
+
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// HTTP response size histogram
|
|
84
|
+
const httpResponseSize = new client.Histogram({
|
|
85
|
+
name: 'http_response_size_bytes',
|
|
86
|
+
help: 'Size of HTTP responses in bytes',
|
|
87
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
88
|
+
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Active HTTP connections gauge
|
|
92
|
+
const httpActiveConnections = new client.Gauge({
|
|
93
|
+
name: 'http_active_connections',
|
|
94
|
+
help: 'Number of active HTTP connections',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
httpRequestDuration,
|
|
99
|
+
httpRequestTotal,
|
|
100
|
+
httpRequestSize,
|
|
101
|
+
httpResponseSize,
|
|
102
|
+
httpActiveConnections,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract route pattern from request
|
|
108
|
+
* This function attempts to extract a meaningful route pattern from the request
|
|
109
|
+
* for use in Prometheus metrics labels
|
|
110
|
+
*/
|
|
111
|
+
function extractRoutePattern(req) {
|
|
112
|
+
try {
|
|
113
|
+
// If route pattern is available from router context
|
|
114
|
+
if (req.ctx && req.ctx.route) {
|
|
115
|
+
return validateRoute(req.ctx.route)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If params exist, try to reconstruct the pattern
|
|
119
|
+
if (req.params && Object.keys(req.params).length > 0) {
|
|
120
|
+
const url = new URL(req.url, 'http://localhost')
|
|
121
|
+
let pattern = url.pathname
|
|
122
|
+
|
|
123
|
+
// Replace parameter values with parameter names
|
|
124
|
+
Object.entries(req.params).forEach(([key, value]) => {
|
|
125
|
+
if (typeof key === 'string' && typeof value === 'string') {
|
|
126
|
+
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
127
|
+
pattern = pattern.replace(
|
|
128
|
+
new RegExp(`/${escapedValue}(?=/|$)`),
|
|
129
|
+
`/:${sanitizeLabelValue(key)}`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return validateRoute(pattern)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Try to normalize common patterns
|
|
138
|
+
const url = new URL(req.url, 'http://localhost')
|
|
139
|
+
let pathname = url.pathname
|
|
140
|
+
|
|
141
|
+
// Replace UUIDs, numbers, and other common ID patterns
|
|
142
|
+
pathname = pathname
|
|
143
|
+
.replace(
|
|
144
|
+
/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
145
|
+
'/:id',
|
|
146
|
+
)
|
|
147
|
+
.replace(/\/\d+/g, '/:id')
|
|
148
|
+
.replace(/\/[a-zA-Z0-9_-]{20,}/g, '/:token')
|
|
149
|
+
|
|
150
|
+
return validateRoute(pathname)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Fallback for malformed URLs
|
|
153
|
+
return '/unknown'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get request size in bytes - optimized for performance
|
|
159
|
+
*/
|
|
160
|
+
function getRequestSize(req) {
|
|
161
|
+
try {
|
|
162
|
+
const contentLength = req.headers.get('content-length')
|
|
163
|
+
if (contentLength) {
|
|
164
|
+
const size = parseInt(contentLength, 10)
|
|
165
|
+
return size >= 0 && size <= 100 * 1024 * 1024 ? size : 0 // Max 100MB
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fast estimation based on headers only
|
|
169
|
+
let size = 0
|
|
170
|
+
const url = req.url || ''
|
|
171
|
+
size += req.method.length + url.length + 12 // HTTP/1.1 + spaces
|
|
172
|
+
|
|
173
|
+
// Quick header size estimation
|
|
174
|
+
if (req.headers && typeof req.headers.forEach === 'function') {
|
|
175
|
+
let headerCount = 0
|
|
176
|
+
req.headers.forEach((value, key) => {
|
|
177
|
+
if (headerCount < 50) {
|
|
178
|
+
// Limit header processing for performance
|
|
179
|
+
size += key.length + value.length + 4 // ": " + "\r\n"
|
|
180
|
+
headerCount++
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Math.min(size, 1024 * 1024) // Cap at 1MB for estimation
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return 0
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get response size in bytes - optimized for performance
|
|
193
|
+
*/
|
|
194
|
+
function getResponseSize(response) {
|
|
195
|
+
try {
|
|
196
|
+
// Check content-length header first (fastest)
|
|
197
|
+
const contentLength = response.headers?.get('content-length')
|
|
198
|
+
if (contentLength) {
|
|
199
|
+
const size = parseInt(contentLength, 10)
|
|
200
|
+
return size >= 0 && size <= 100 * 1024 * 1024 ? size : 0 // Max 100MB
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Try to estimate from response body if available
|
|
204
|
+
if (
|
|
205
|
+
response._bodyForLogger &&
|
|
206
|
+
typeof response._bodyForLogger === 'string'
|
|
207
|
+
) {
|
|
208
|
+
return Math.min(
|
|
209
|
+
Buffer.byteLength(response._bodyForLogger, 'utf8'),
|
|
210
|
+
1024 * 1024,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fast estimation for headers only
|
|
215
|
+
let size = 15 // "HTTP/1.1 200 OK\r\n"
|
|
216
|
+
|
|
217
|
+
if (response.headers && typeof response.headers.forEach === 'function') {
|
|
218
|
+
let headerCount = 0
|
|
219
|
+
response.headers.forEach((value, key) => {
|
|
220
|
+
if (headerCount < 20) {
|
|
221
|
+
// Limit for performance
|
|
222
|
+
size += key.length + value.length + 4 // ": " + "\r\n"
|
|
223
|
+
headerCount++
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Math.min(size, 1024) // Cap header estimation at 1KB
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates a Prometheus metrics middleware
|
|
236
|
+
* @param {Object} options - Prometheus middleware configuration
|
|
237
|
+
* @param {Object} options.metrics - Custom metrics object (optional)
|
|
238
|
+
* @param {Array<string>} options.excludePaths - Paths to exclude from metrics
|
|
239
|
+
* @param {boolean} options.collectDefaultMetrics - Whether to collect default Node.js metrics
|
|
240
|
+
* @param {Function} options.normalizeRoute - Custom route normalization function
|
|
241
|
+
* @param {Function} options.extractLabels - Custom label extraction function
|
|
242
|
+
* @param {Array<string>} options.skipMethods - HTTP methods to skip from metrics
|
|
243
|
+
* @returns {Function} Middleware function
|
|
244
|
+
*/
|
|
245
|
+
function createPrometheusMiddleware(options = {}) {
|
|
246
|
+
const {
|
|
247
|
+
metrics: customMetrics,
|
|
248
|
+
excludePaths = ['/health', '/ping', '/favicon.ico', '/metrics'],
|
|
249
|
+
collectDefaultMetrics = true,
|
|
250
|
+
normalizeRoute = extractRoutePattern,
|
|
251
|
+
extractLabels,
|
|
252
|
+
skipMethods = [],
|
|
253
|
+
} = options
|
|
254
|
+
|
|
255
|
+
// Collect default Node.js metrics
|
|
256
|
+
if (collectDefaultMetrics) {
|
|
257
|
+
const client = loadPromClient()
|
|
258
|
+
client.collectDefaultMetrics({
|
|
259
|
+
timeout: 5000,
|
|
260
|
+
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
|
|
261
|
+
eventLoopMonitoringPrecision: 5,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Use custom metrics or create default ones
|
|
266
|
+
const metrics = customMetrics || createDefaultMetrics()
|
|
267
|
+
|
|
268
|
+
return async function prometheusMiddleware(req, next) {
|
|
269
|
+
const startHrTime = process.hrtime()
|
|
270
|
+
|
|
271
|
+
// Skip metrics collection for excluded paths (performance optimization)
|
|
272
|
+
const url = req.url || ''
|
|
273
|
+
let pathname
|
|
274
|
+
try {
|
|
275
|
+
// Handle both full URLs and pathname-only URLs
|
|
276
|
+
if (url.startsWith('http')) {
|
|
277
|
+
pathname = new URL(url).pathname
|
|
278
|
+
} else {
|
|
279
|
+
pathname = url.split('?')[0] // Fast pathname extraction
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
pathname = url.split('?')[0] // Fallback to simple splitting
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (excludePaths.some((path) => pathname.startsWith(path))) {
|
|
286
|
+
return next()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Skip metrics collection for specified methods
|
|
290
|
+
const method = req.method?.toUpperCase() || 'GET'
|
|
291
|
+
if (skipMethods.includes(method)) {
|
|
292
|
+
return next()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Increment active connections
|
|
296
|
+
if (metrics.httpActiveConnections) {
|
|
297
|
+
metrics.httpActiveConnections.inc()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Get request size (lazy evaluation)
|
|
302
|
+
let requestSize = 0
|
|
303
|
+
|
|
304
|
+
// Execute the request
|
|
305
|
+
const response = await next()
|
|
306
|
+
|
|
307
|
+
// Calculate duration (high precision)
|
|
308
|
+
const duration = process.hrtime(startHrTime)
|
|
309
|
+
const durationInSeconds = duration[0] + duration[1] * 1e-9
|
|
310
|
+
|
|
311
|
+
// Extract route pattern (cached/optimized)
|
|
312
|
+
const route = normalizeRoute(req)
|
|
313
|
+
const statusCode = sanitizeLabelValue(
|
|
314
|
+
response?.status?.toString() || 'unknown',
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// Create base labels with sanitized values
|
|
318
|
+
let labels = {
|
|
319
|
+
method: sanitizeLabelValue(method),
|
|
320
|
+
route: route,
|
|
321
|
+
status_code: statusCode,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add custom labels if extractor provided
|
|
325
|
+
if (extractLabels && typeof extractLabels === 'function') {
|
|
326
|
+
try {
|
|
327
|
+
const customLabels = extractLabels(req, response)
|
|
328
|
+
if (customLabels && typeof customLabels === 'object') {
|
|
329
|
+
// Sanitize custom labels
|
|
330
|
+
Object.entries(customLabels).forEach(([key, value]) => {
|
|
331
|
+
if (typeof key === 'string' && key.length <= 50) {
|
|
332
|
+
labels[sanitizeLabelValue(key)] = sanitizeLabelValue(
|
|
333
|
+
String(value),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Ignore custom label extraction errors
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Record metrics efficiently
|
|
344
|
+
if (metrics.httpRequestDuration) {
|
|
345
|
+
metrics.httpRequestDuration.observe(
|
|
346
|
+
{
|
|
347
|
+
method: labels.method,
|
|
348
|
+
route: labels.route,
|
|
349
|
+
status_code: labels.status_code,
|
|
350
|
+
},
|
|
351
|
+
durationInSeconds,
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (metrics.httpRequestTotal) {
|
|
356
|
+
metrics.httpRequestTotal.inc({
|
|
357
|
+
method: labels.method,
|
|
358
|
+
route: labels.route,
|
|
359
|
+
status_code: labels.status_code,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (metrics.httpRequestSize) {
|
|
364
|
+
requestSize = getRequestSize(req)
|
|
365
|
+
if (requestSize > 0) {
|
|
366
|
+
metrics.httpRequestSize.observe(
|
|
367
|
+
{method: labels.method, route: labels.route},
|
|
368
|
+
requestSize,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (metrics.httpResponseSize) {
|
|
374
|
+
const responseSize = getResponseSize(response)
|
|
375
|
+
if (responseSize > 0) {
|
|
376
|
+
metrics.httpResponseSize.observe(
|
|
377
|
+
{
|
|
378
|
+
method: labels.method,
|
|
379
|
+
route: labels.route,
|
|
380
|
+
status_code: labels.status_code,
|
|
381
|
+
},
|
|
382
|
+
responseSize,
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return response
|
|
388
|
+
} catch (error) {
|
|
389
|
+
// Record error metrics
|
|
390
|
+
const duration = process.hrtime(startHrTime)
|
|
391
|
+
const durationInSeconds = duration[0] + duration[1] * 1e-9
|
|
392
|
+
const route = normalizeRoute(req)
|
|
393
|
+
const sanitizedMethod = sanitizeLabelValue(method)
|
|
394
|
+
|
|
395
|
+
if (metrics.httpRequestDuration) {
|
|
396
|
+
metrics.httpRequestDuration.observe(
|
|
397
|
+
{method: sanitizedMethod, route: route, status_code: '500'},
|
|
398
|
+
durationInSeconds,
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (metrics.httpRequestTotal) {
|
|
403
|
+
metrics.httpRequestTotal.inc({
|
|
404
|
+
method: sanitizedMethod,
|
|
405
|
+
route: route,
|
|
406
|
+
status_code: '500',
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
throw error
|
|
411
|
+
} finally {
|
|
412
|
+
// Decrement active connections
|
|
413
|
+
if (metrics.httpActiveConnections) {
|
|
414
|
+
metrics.httpActiveConnections.dec()
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Creates a metrics endpoint handler that serves Prometheus metrics
|
|
422
|
+
* @param {Object} options - Metrics endpoint configuration
|
|
423
|
+
* @param {string} options.endpoint - The endpoint path (default: '/metrics')
|
|
424
|
+
* @param {Object} options.registry - Custom Prometheus registry
|
|
425
|
+
* @returns {Function} Request handler function
|
|
426
|
+
*/
|
|
427
|
+
function createMetricsHandler(options = {}) {
|
|
428
|
+
const client = loadPromClient()
|
|
429
|
+
const {endpoint = '/metrics', registry = client.register} = options
|
|
430
|
+
|
|
431
|
+
return async function metricsHandler(req) {
|
|
432
|
+
const url = new URL(req.url, 'http://localhost')
|
|
433
|
+
|
|
434
|
+
if (url.pathname === endpoint) {
|
|
435
|
+
try {
|
|
436
|
+
const metrics = await registry.metrics()
|
|
437
|
+
return new Response(metrics, {
|
|
438
|
+
status: 200,
|
|
439
|
+
headers: {
|
|
440
|
+
'Content-Type': registry.contentType,
|
|
441
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
442
|
+
Pragma: 'no-cache',
|
|
443
|
+
Expires: '0',
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return new Response('Error collecting metrics', {
|
|
448
|
+
status: 500,
|
|
449
|
+
headers: {'Content-Type': 'text/plain'},
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null // Let other middleware handle the request
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Simple helper to create both middleware and metrics endpoint
|
|
460
|
+
* @param {Object} options - Combined configuration options
|
|
461
|
+
* @returns {Object} Object containing middleware and handler functions
|
|
462
|
+
*/
|
|
463
|
+
function createPrometheusIntegration(options = {}) {
|
|
464
|
+
const middleware = createPrometheusMiddleware(options)
|
|
465
|
+
const metricsHandler = createMetricsHandler(options)
|
|
466
|
+
const client = loadPromClient()
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
middleware,
|
|
470
|
+
metricsHandler,
|
|
471
|
+
// Expose the registry for custom metrics
|
|
472
|
+
registry: client.register,
|
|
473
|
+
// Expose prom-client for creating custom metrics
|
|
474
|
+
promClient: client,
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = {
|
|
479
|
+
createPrometheusMiddleware,
|
|
480
|
+
createMetricsHandler,
|
|
481
|
+
createPrometheusIntegration,
|
|
482
|
+
createDefaultMetrics,
|
|
483
|
+
extractRoutePattern,
|
|
484
|
+
// Export lazy loader functions to maintain compatibility
|
|
485
|
+
get promClient() {
|
|
486
|
+
return loadPromClient()
|
|
487
|
+
},
|
|
488
|
+
get register() {
|
|
489
|
+
return loadPromClient().register
|
|
490
|
+
},
|
|
491
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0http-bun",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "0http for Bun",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"fast-querystring": "^1.1.2",
|
|
14
|
-
"trouter": "^4.0.0"
|
|
15
|
-
"jose": "^6.0.11",
|
|
16
|
-
"pino": "^9.7.0"
|
|
14
|
+
"trouter": "^4.0.0"
|
|
17
15
|
},
|
|
18
16
|
"repository": {
|
|
19
17
|
"type": "git",
|
|
@@ -28,11 +26,14 @@
|
|
|
28
26
|
"lib/"
|
|
29
27
|
],
|
|
30
28
|
"devDependencies": {
|
|
31
|
-
"0http-bun": "^1.1
|
|
32
|
-
"bun-types": "^1.2.
|
|
29
|
+
"0http-bun": "^1.2.1",
|
|
30
|
+
"bun-types": "^1.2.16",
|
|
33
31
|
"mitata": "^1.0.34",
|
|
34
32
|
"prettier": "^3.5.3",
|
|
35
|
-
"typescript": "^5.8.3"
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"jose": "^6.0.11",
|
|
35
|
+
"pino": "^9.7.0",
|
|
36
|
+
"prom-client": "^15.1.3"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"http",
|