-
Notifications
You must be signed in to change notification settings - Fork 2
Compat WebServer Metrics
Reference for the WebServer module's built-in performance metrics.
The WebServer module automatically tracks performance metrics for all requests and WebSocket connections. Metrics are collected in-memory with minimal overhead.
Key features:
- Zero-configuration metrics collection
- Accurate averages using accumulated sums
- Peak tracking for capacity planning
- WebSocket connection and message stats
- Thread-safe metric updates
Access metrics via the metrics property:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const metrics = server.metrics;The returned object is a deep copy to prevent external mutation. Access it as often as needed without affecting performance.
Reset all metrics to initial values:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
server.resetMetrics();interface ServerMetrics {
requests: {
total: number;
active: number;
peakActive: number;
};
statusCodes: {
'1xx': number;
'2xx': number;
'3xx': number;
'4xx': number;
'5xx': number;
};
responseTime: {
min: number;
max: number;
average: number;
};
websocket: {
upgrades: number;
connections: {
total: number;
active: number;
peakActive: number;
};
messages: {
received: number;
sent: number;
};
errors: number;
connectionDuration: {
min: number;
max: number;
average: number;
};
};
}| Field | Type | Description |
|---|---|---|
total |
number |
Total requests processed since start/reset |
active |
number |
Currently processing requests |
peakActive |
number |
Highest concurrent requests observed |
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const { requests } = server.metrics;
console.log(`Total requests: ${requests.total}`);
console.log(`Currently processing: ${requests.active}`);
console.log(`Peak concurrent: ${requests.peakActive}`);Notes:
-
activeis incremented before handler runs, decremented after response -
peakActivehelps with capacity planning and load testing -
totalincludes both successful and failed requests
Status codes are grouped by category:
| Field | Range | Description |
|---|---|---|
1xx |
100-199 | Informational |
2xx |
200-299 | Success |
3xx |
300-399 | Redirection |
4xx |
400-499 | Client errors |
5xx |
500-599 | Server errors |
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const { statusCodes } = server.metrics;
console.log(`Success: ${statusCodes['2xx']}`);
console.log(`Client errors: ${statusCodes['4xx']}`);
console.log(`Server errors: ${statusCodes['5xx']}`);
// Calculate error rate
const total = Object.values(statusCodes).reduce((a, b) => a + b, 0);
const errorRate = (statusCodes['4xx'] + statusCodes['5xx']) / total;
console.log(`Error rate: ${(errorRate * 100).toFixed(2)}%`);Response times are in milliseconds:
| Field | Type | Description |
|---|---|---|
min |
number |
Fastest response time |
max |
number |
Slowest response time |
average |
number |
Average response time |
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const { responseTime } = server.metrics;
console.log(`Min: ${responseTime.min.toFixed(2)}ms`);
console.log(`Max: ${responseTime.max.toFixed(2)}ms`);
console.log(`Avg: ${responseTime.average.toFixed(2)}ms`);Notes:
-
minstarts atInfinityuntil first request - Average is calculated using accumulated sum for precision
- Time includes handler execution plus framework overhead
| Field | Type | Description |
|---|---|---|
upgrades |
number |
Total WebSocket upgrade attempts |
connections.total |
number |
Total connections established |
connections.active |
number |
Currently open connections |
connections.peakActive |
number |
Peak concurrent connections |
messages.received |
number |
Total messages received |
messages.sent |
number |
Total messages sent |
errors |
number |
WebSocket errors |
connectionDuration.min |
number |
Shortest connection (ms) |
connectionDuration.max |
number |
Longest connection (ms) |
connectionDuration.average |
number |
Average connection duration (ms) |
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const { websocket } = server.metrics;
console.log(`Active connections: ${websocket.connections.active}`);
console.log(`Peak connections: ${websocket.connections.peakActive}`);
console.log(`Messages in: ${websocket.messages.received}`);
console.log(`Messages out: ${websocket.messages.sent}`);
console.log(
`Avg duration: ${(websocket.connectionDuration.average / 1000).toFixed(1)}s`,
);Expose metrics via HTTP:
import { WebServer } from '@tundralibs/compat/webserver';
const server: WebServer = new WebServer('API', {
mode: 'TCP',
port: 8080,
handler: (req): Response => {
const url = new URL(req.url);
if (url.pathname === '/metrics') {
return Response.json(server.metrics);
}
if (url.pathname === '/health') {
return Response.json({
status: 'healthy',
uptime: process.uptime(),
requests: server.metrics.requests.total,
});
}
return new Response('OK');
},
});Export in Prometheus text format:
import type { ServerMetrics, WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
function formatPrometheus(metrics: ServerMetrics): string {
const lines: string[] = [];
// Request metrics
lines.push(`# HELP http_requests_total Total HTTP requests`);
lines.push(`# TYPE http_requests_total counter`);
lines.push(`http_requests_total ${metrics.requests.total}`);
lines.push(`# HELP http_requests_active Active HTTP requests`);
lines.push(`# TYPE http_requests_active gauge`);
lines.push(`http_requests_active ${metrics.requests.active}`);
// Status codes
lines.push(`# HELP http_responses_total HTTP responses by status`);
lines.push(`# TYPE http_responses_total counter`);
for (const [code, count] of Object.entries(metrics.statusCodes)) {
lines.push(`http_responses_total{status="${code}"} ${count}`);
}
// Response time
lines.push(`# HELP http_response_time_ms Response time in milliseconds`);
lines.push(`# TYPE http_response_time_ms summary`);
lines.push(
`http_response_time_ms{quantile="min"} ${metrics.responseTime.min}`,
);
lines.push(
`http_response_time_ms{quantile="max"} ${metrics.responseTime.max}`,
);
lines.push(
`http_response_time_ms{quantile="avg"} ${metrics.responseTime.average}`,
);
return lines.join('\n');
}
// In handler:
function handler(req: Request): Response {
const url = new URL(req.url);
if (
url.pathname === '/metrics' &&
req.headers.get('accept')?.includes('text/plain')
) {
return new Response(formatPrometheus(server.metrics), {
headers: { 'Content-Type': 'text/plain' },
});
}
return new Response('OK');
}Log metrics periodically:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
setInterval(() => {
const m = server.metrics;
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
requests: m.requests.total,
active: m.requests.active,
errors: m.statusCodes['5xx'],
avgResponseMs: m.responseTime.average.toFixed(2),
wsConnections: m.websocket.connections.active,
}));
}, 60000);Reset metrics periodically for rolling stats:
import type { ServerMetrics, WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
let hourlyStats: ServerMetrics[] = [];
setInterval(() => {
// Save current metrics
hourlyStats.push(server.metrics);
// Keep last 24 hours
if (hourlyStats.length > 24) {
hourlyStats.shift();
}
// Reset for next hour
server.resetMetrics();
}, 3600000);
// Calculate daily totals
function getDailyStats() {
return hourlyStats.reduce((acc, m) => ({
requests: acc.requests + m.requests.total,
errors: acc.errors + m.statusCodes['5xx'],
}), { requests: 0, errors: 0 });
}Alert on thresholds:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
setInterval(() => {
const m = server.metrics;
// High error rate
const errorRate = m.statusCodes['5xx'] / m.requests.total;
if (errorRate > 0.05) {
alert(`High error rate: ${(errorRate * 100).toFixed(1)}%`);
}
// Slow responses
if (m.responseTime.average > 1000) {
alert(`Slow responses: avg ${m.responseTime.average.toFixed(0)}ms`);
}
// Many active requests (potential bottleneck)
if (m.requests.active > 100) {
alert(`High concurrency: ${m.requests.active} active requests`);
}
}, 30000);Avoid resetting metrics while processing requests - the active count will be inaccurate:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
// Bad - resets active count mid-request
server.resetMetrics();
// Good - reset during quiet periods
if (server.metrics.requests.active === 0) {
server.resetMetrics();
}The metrics object is a copy, so it's safe to use asynchronously:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
declare function saveToDatabase(snapshot: unknown): Promise<void>;
// Safe - works with a snapshot
const snapshot = server.metrics;
await saveToDatabase(snapshot);min starts at Infinity before any requests:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
const { responseTime } = server.metrics;
const minDisplay = responseTime.min === Infinity
? 'N/A'
: `${responseTime.min.toFixed(2)}ms`;Metrics are lightweight, but if you're storing historical data:
import type { WebServer } from '@tundralibs/compat/webserver';
declare const server: WebServer;
// Store only essential fields
const snapshot = {
timestamp: Date.now(),
total: server.metrics.requests.total,
errors: server.metrics.statusCodes['5xx'],
avgMs: server.metrics.responseTime.average,
};For production, consider a dedicated metrics endpoint:
// Main API server
const apiServer = new WebServer('API', { port: 8080, ... });
// Metrics server (different port, internal only)
const metricsServer = new WebServer('Metrics', {
mode: 'TCP',
port: 9090,
hostname: '127.0.0.1', // Internal only
handler: () => Response.json(apiServer.metrics),
});
metricsServer.start();
metricsServer.unref(); // Don't block shutdown