Skip to content

Commit 72f351b

Browse files
authored
Adding prometheus middleware (#10)
* feat: adding prometheus middleware
1 parent da276df commit 72f351b

File tree

8 files changed

+1742
-4
lines changed

8 files changed

+1742
-4
lines changed

demos/prometheus.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import http from '../index'
2+
import {createPrometheusIntegration} from '../lib/middleware/prometheus'
3+
4+
// Create Prometheus integration with custom options
5+
const prometheus = createPrometheusIntegration({
6+
// Collect default Node.js metrics (memory, CPU, etc.)
7+
collectDefaultMetrics: true,
8+
9+
// Exclude certain paths from metrics
10+
excludePaths: ['/health', '/favicon.ico'],
11+
12+
// Skip metrics for certain HTTP methods
13+
skipMethods: ['OPTIONS'],
14+
15+
// Custom route normalization
16+
normalizeRoute: (req) => {
17+
const url = new URL(req.url, 'http://localhost')
18+
let pathname = url.pathname
19+
20+
// Custom patterns for this demo
21+
pathname = pathname
22+
.replace(/\/users\/\d+/, '/users/:id')
23+
.replace(/\/products\/[a-zA-Z0-9-]+/, '/products/:slug')
24+
.replace(/\/api\/v\d+/, '/api/:version')
25+
26+
return pathname
27+
},
28+
29+
// Add custom labels to metrics
30+
extractLabels: (req, response) => {
31+
const labels: Record<string, string> = {}
32+
33+
// Add user agent category
34+
const userAgent = req.headers.get('user-agent') || ''
35+
if (userAgent.includes('curl')) {
36+
labels.client_type = 'curl'
37+
} else if (userAgent.includes('Chrome')) {
38+
labels.client_type = 'browser'
39+
} else {
40+
labels.client_type = 'other'
41+
}
42+
43+
// Add response type
44+
const contentType = response?.headers?.get('content-type') || ''
45+
if (contentType.includes('json')) {
46+
labels.response_type = 'json'
47+
} else if (contentType.includes('html')) {
48+
labels.response_type = 'html'
49+
} else {
50+
labels.response_type = 'other'
51+
}
52+
53+
return labels
54+
},
55+
})
56+
57+
// Create custom metrics for business logic
58+
const {promClient} = prometheus
59+
60+
const orderCounter = new promClient.Counter({
61+
name: 'orders_total',
62+
help: 'Total number of orders processed',
63+
labelNames: ['status', 'payment_method'],
64+
})
65+
66+
const orderValue = new promClient.Histogram({
67+
name: 'order_value_dollars',
68+
help: 'Value of orders in dollars',
69+
labelNames: ['payment_method'],
70+
buckets: [10, 50, 100, 500, 1000, 5000],
71+
})
72+
73+
const activeUsers = new promClient.Gauge({
74+
name: 'active_users',
75+
help: 'Number of currently active users',
76+
})
77+
78+
// Simulate some active users
79+
let userCount = 0
80+
setInterval(() => {
81+
userCount = Math.floor(Math.random() * 100) + 50
82+
activeUsers.set(userCount)
83+
}, 5000)
84+
85+
// Configure the server
86+
const {router} = http({})
87+
88+
// Apply Prometheus middleware
89+
router.use(prometheus.middleware)
90+
91+
// Health check endpoint (excluded from metrics)
92+
router.get('/health', () => {
93+
return new Response(
94+
JSON.stringify({
95+
status: 'healthy',
96+
timestamp: new Date().toISOString(),
97+
uptime: process.uptime(),
98+
}),
99+
{
100+
headers: {'Content-Type': 'application/json'},
101+
},
102+
)
103+
})
104+
105+
// User endpoints
106+
router.get('/users/:id', (req) => {
107+
const id = req.params?.id
108+
return new Response(
109+
JSON.stringify({
110+
id: parseInt(id),
111+
name: `User ${id}`,
112+
email: `user${id}@example.com`,
113+
created_at: new Date().toISOString(),
114+
}),
115+
{
116+
headers: {'Content-Type': 'application/json'},
117+
},
118+
)
119+
})
120+
121+
router.post('/users', async (req) => {
122+
const body = await req.json()
123+
const user = {
124+
id: Math.floor(Math.random() * 1000),
125+
name: body.name || 'Anonymous',
126+
email: body.email || `user${Date.now()}@example.com`,
127+
created_at: new Date().toISOString(),
128+
}
129+
130+
return new Response(JSON.stringify(user), {
131+
status: 201,
132+
headers: {'Content-Type': 'application/json'},
133+
})
134+
})
135+
136+
// Product endpoints
137+
router.get('/products/:slug', (req) => {
138+
const slug = req.params?.slug
139+
return new Response(
140+
JSON.stringify({
141+
slug,
142+
name: slug
143+
.split('-')
144+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
145+
.join(' '),
146+
price: Math.floor(Math.random() * 500) + 10,
147+
in_stock: Math.random() > 0.2,
148+
}),
149+
{
150+
headers: {'Content-Type': 'application/json'},
151+
},
152+
)
153+
})
154+
155+
// Order endpoint with custom metrics
156+
router.post('/orders', async (req) => {
157+
try {
158+
const body = await req.json()
159+
const amount = body.amount || 0
160+
const method = body.method || 'unknown'
161+
162+
// Simulate order processing
163+
const success = Math.random() > 0.1 // 90% success rate
164+
const status = success ? 'completed' : 'failed'
165+
166+
// Record custom metrics
167+
orderCounter.inc({status, payment_method: method})
168+
169+
if (success && amount > 0) {
170+
orderValue.observe({payment_method: method}, amount)
171+
}
172+
173+
const order = {
174+
id: `order_${Date.now()}`,
175+
amount,
176+
payment_method: method,
177+
status,
178+
created_at: new Date().toISOString(),
179+
}
180+
181+
return new Response(JSON.stringify(order), {
182+
status: success ? 201 : 402,
183+
headers: {'Content-Type': 'application/json'},
184+
})
185+
} catch (error) {
186+
return new Response(
187+
JSON.stringify({
188+
error: 'Invalid JSON body',
189+
}),
190+
{
191+
status: 400,
192+
headers: {'Content-Type': 'application/json'},
193+
},
194+
)
195+
}
196+
})
197+
198+
// Slow endpoint for testing duration metrics
199+
router.get('/slow', async () => {
200+
// Random delay between 1-3 seconds
201+
const delay = Math.floor(Math.random() * 2000) + 1000
202+
await new Promise((resolve) => setTimeout(resolve, delay))
203+
204+
return new Response(
205+
JSON.stringify({
206+
message: `Processed after ${delay}ms`,
207+
timestamp: new Date().toISOString(),
208+
}),
209+
{
210+
headers: {'Content-Type': 'application/json'},
211+
},
212+
)
213+
})
214+
215+
// Error endpoint for testing error metrics
216+
router.get('/error', () => {
217+
// Randomly throw different types of errors
218+
const errorType = Math.floor(Math.random() * 3)
219+
220+
switch (errorType) {
221+
case 0:
222+
return new Response('Not Found', {status: 404})
223+
case 1:
224+
return new Response('Internal Server Error', {status: 500})
225+
case 2:
226+
throw new Error('Unhandled error for testing')
227+
default:
228+
return new Response('Bad Request', {status: 400})
229+
}
230+
})
231+
232+
// Versioned API endpoint
233+
router.get('/api/:version/data', (req) => {
234+
const version = req.params?.version
235+
return new Response(
236+
JSON.stringify({
237+
api_version: version,
238+
data: {message: 'Hello from versioned API'},
239+
timestamp: new Date().toISOString(),
240+
}),
241+
{
242+
headers: {'Content-Type': 'application/json'},
243+
},
244+
)
245+
})
246+
247+
// Metrics endpoint - this should be added last
248+
router.get('/metrics', prometheus.metricsHandler)
249+
250+
// Server startup logic
251+
const port = process.env.PORT || 3003
252+
253+
console.log('🚀 Starting Prometheus Demo Server')
254+
console.log('=====================================')
255+
console.log(`📊 Metrics endpoint: http://localhost:${port}/metrics`)
256+
console.log(`🏠 Demo page: http://localhost:${port}/`)
257+
console.log(`� Health check: http://localhost:${port}/health`)
258+
console.log(`🔧 Port: ${port}`)
259+
console.log('=====================================')
260+
console.log('')
261+
console.log('Try these commands to generate metrics:')
262+
console.log('curl http://localhost:' + port + '/metrics')
263+
console.log('curl http://localhost:' + port + '/users/123')
264+
console.log('curl http://localhost:' + port + '/products/awesome-widget')
265+
console.log(
266+
'curl -X POST http://localhost:' +
267+
port +
268+
'/orders -H \'Content-Type: application/json\' -d \'{"amount": 99.99, "method": "card"}\'',
269+
)
270+
console.log('curl http://localhost:' + port + '/slow')
271+
console.log('curl http://localhost:' + port + '/error')
272+
console.log('')
273+
274+
console.log(`✅ Server running at http://localhost:${port}/`)
275+
276+
export default {
277+
port,
278+
fetch: router.fetch.bind(router),
279+
}

0 commit comments

Comments
 (0)