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 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
 
@@ -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/*', jwtAuth({secret: 'api-secret'}))
798
- router.use('/api/*', rateLimit({max: 1000}))
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, StepFunction} from '../../common'
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
@@ -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
- const {jwtVerify, createRemoteJWKSet, errors} = require('jose')
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 if (error instanceof errors.JWTExpired) {
306
- message = 'Token expired'
307
- } else if (error instanceof errors.JWTInvalid) {
308
- message = 'Invalid token format'
309
- } else if (error instanceof errors.JWKSNoMatchingKey) {
310
- message = 'Token signature verification failed'
311
- } else if (error.message.includes('audience')) {
312
- message = 'Invalid token audience'
313
- } else if (error.message.includes('issuer')) {
314
- message = 'Invalid token issuer'
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}), {
@@ -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: pino.stdTimeFunctions.isoTime,
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: pino.stdSerializers.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 || pino(finalPinoOptions)
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.1",
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.2",
32
- "bun-types": "^1.2.15",
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",