This document describes the observability and monitoring features implemented in the lightweight web stack, including structured logging, health checks, and Prometheus metrics.
The application includes production-grade observability features:
- Structured Logging: JSON-based logging with Pino for high-performance, searchable logs
- Health Checks: Kubernetes-compatible liveness, readiness, and startup probes
- Prometheus Metrics: Comprehensive metrics collection for monitoring and alerting
All observability features are located in /lib/observability/:
logger.ts- Centralized logging configurationmetrics.ts- Prometheus metrics definitionshealth.ts- Health check implementationsindex.ts- Convenience exports
- JSON Format: Structured JSON logs in production for easy parsing and indexing
- Pretty Print: Human-readable colored output in development
- Log Levels: Standard levels (trace, debug, info, warn, error, fatal)
- Context: Child loggers with automatic context propagation
- Performance: Built on Pino, one of the fastest Node.js loggers
The logger automatically configures based on NODE_ENV:
- Production: JSON output,
infolevel by default - Development: Pretty-printed output,
debuglevel by default
Override the log level with the LOG_LEVEL environment variable:
LOG_LEVEL=debug npm startAvailable levels (from most to least verbose):
trace(10): External library loggingdebug(20): Verbose application logginginfo(30): General operational logs (default in production)warn(40): Warning conditionserror(50): Error conditionsfatal(60): Application crash conditions
import { logger } from '@/lib/observability/logger';
logger.info('Server started successfully');
logger.warn({ port: 3000 }, 'Using default port');
logger.error({ error }, 'Failed to connect to database');Create child loggers to automatically include context in all log messages:
import { createLogger } from '@/lib/observability/logger';
const wsLogger = createLogger({ service: 'websocket' });
wsLogger.info({ clientId: '123' }, 'Client connected');
// Output: {"level":"info","service":"websocket","clientId":"123","msg":"Client connected"}- Use structured data: Pass objects as the first parameter for structured fields
- Keep messages concise: Log message should be a short description
- Include context: Use child loggers for service/module-specific logs
- Avoid sensitive data: Never log passwords, tokens, or PII
Example:
// Good
logger.info({ userId: user.id, action: 'login' }, 'User logged in successfully');
// Bad
logger.info('User ' + user.email + ' logged in at ' + new Date());Logging is integrated across all services:
- tRPC: All procedure calls are logged with timing and status
- WebSocket: Connection events, messages, and errors
- Queue Worker: Job processing, completion, and failures
- Queue Manager: Job additions and queue operations
The application exposes three health check endpoints for Kubernetes and other orchestration platforms.
GET /api/health/live
Checks if the application process is running. Returns 200 if alive.
Response:
{
"status": "healthy",
"checks": {
"server": {
"status": "up",
"message": "Server process is running"
}
},
"timestamp": "2024-01-15T10:30:00.000Z"
}GET /api/health/ready
Checks if the application is ready to accept traffic. Verifies all critical dependencies (database, Redis) are available.
Response (healthy):
{
"status": "healthy",
"checks": {
"database": {
"status": "up",
"message": "Database connection successful",
"responseTime": 5
},
"redis": {
"status": "up",
"message": "Redis connection successful",
"responseTime": 2
}
},
"timestamp": "2024-01-15T10:30:00.000Z"
}Response (unhealthy):
{
"status": "unhealthy",
"checks": {
"database": {
"status": "down",
"message": "Connection timeout",
"responseTime": 5000
},
"redis": {
"status": "up",
"message": "Redis connection successful",
"responseTime": 2
}
},
"timestamp": "2024-01-15T10:30:00.000Z"
}Returns:
- 200: All checks passed (healthy)
- 503: One or more checks failed (unhealthy)
GET /api/health/startup
Checks if the application has finished starting up. Currently identical to readiness, but can be extended for slow-starting applications.
Example Kubernetes deployment configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: lightweight-web
spec:
template:
spec:
containers:
- name: app
image: lightweight-web:latest
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /api/health/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /api/health/startup
port: 3000
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30The application exposes comprehensive metrics in Prometheus format for monitoring, alerting, and visualization.
GET /api/metrics
Returns all metrics in Prometheus text format.
Example output:
# HELP lightweight_web_http_requests_total Total number of HTTP requests
# TYPE lightweight_web_http_requests_total counter
lightweight_web_http_requests_total{method="GET",route="/api/health/ready",status_code="200"} 145
# HELP lightweight_web_trpc_requests_total Total number of tRPC requests
# TYPE lightweight_web_trpc_requests_total counter
lightweight_web_trpc_requests_total{procedure="user.list",type="query",status="success"} 42
Collected automatically with lightweight_web_ prefix:
- Process CPU usage
- Process memory usage (heap, RSS, external)
- Event loop lag
- Garbage collection duration
- Active handles and requests
lightweight_web_http_requests_total
Labels: method, route, status_code
Total number of HTTP requests
lightweight_web_http_request_duration_seconds
Labels: method, route, status_code
HTTP request duration histogram
Buckets: 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10 seconds
lightweight_web_trpc_requests_total
Labels: procedure, type (query/mutation), status (success/error)
Total number of tRPC procedure calls
lightweight_web_trpc_request_duration_seconds
Labels: procedure, type
tRPC request duration histogram
Buckets: 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5 seconds
lightweight_web_ws_connections_active
Current number of active WebSocket connections
lightweight_web_ws_connections_total
Labels: event (connect/disconnect)
Total number of WebSocket connection events
lightweight_web_ws_messages_total
Labels: direction (inbound/outbound), type (message type)
Total number of WebSocket messages
lightweight_web_ws_channel_subscriptions
Labels: channel
Number of subscriptions per channel
lightweight_web_queue_jobs_total
Labels: queue, status (added/completed/failed)
Total number of queue jobs
lightweight_web_queue_job_duration_seconds
Labels: queue, job_type
Job processing duration histogram
Buckets: 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300 seconds
lightweight_web_queue_jobs_active
Labels: queue
Number of currently active jobs
lightweight_web_queue_jobs_waiting
Labels: queue
Number of jobs waiting in queue
lightweight_web_db_query_total
Labels: operation (select/insert/update/delete)
Total number of database queries
lightweight_web_db_query_duration_seconds
Labels: operation
Database query duration histogram
Buckets: 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5 seconds
lightweight_web_db_connections_active
Number of active database connections
lightweight_web_ssh_operations_total
Labels: operation, status (success/error)
Total number of SSH operations
lightweight_web_ssh_operation_duration_seconds
Labels: operation
SSH operation duration histogram
Buckets: 0.1, 0.5, 1, 2, 5, 10, 30, 60 seconds
lightweight_web_ssh_connection_pool_size
Labels: status (idle/active)
Number of connections in SSH pool
lightweight_web_auth_attempts_total
Labels: method (email/ldap), status (success/failure)
Total number of authentication attempts
lightweight_web_users_total
Total number of registered users
lightweight_web_projects_total
Total number of projects
import { Counter, Histogram } from 'prom-client';
// Create custom metrics
const myCounter = new Counter({
name: 'lightweight_web_custom_events_total',
help: 'Total number of custom events',
labelNames: ['event_type'],
});
const myHistogram = new Histogram({
name: 'lightweight_web_custom_operation_duration_seconds',
help: 'Duration of custom operations',
labelNames: ['operation'],
buckets: [0.1, 0.5, 1, 2, 5],
});
// Use them
myCounter.inc({ event_type: 'user_signup' });
const end = myHistogram.startTimer({ operation: 'data_processing' });
await processData();
end();import { createLogger, timeOperation } from '@/lib/observability';
import { httpRequestDuration } from '@/lib/observability/metrics';
const logger = createLogger({ service: 'api' });
async function handleRequest(req, res) {
const endTimer = timeOperation(httpRequestDuration, {
method: req.method,
route: req.path,
status_code: '200',
});
try {
logger.info({ method: req.method, path: req.path }, 'Request received');
// Handle request...
logger.info({ method: req.method, path: req.path }, 'Request completed');
endTimer();
} catch (error) {
logger.error({ error, method: req.method, path: req.path }, 'Request failed');
endTimer();
throw error;
}
}Add the application to your Prometheus scrape configuration:
scrape_configs:
- job_name: 'lightweight-web'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/api/metrics'
scrape_interval: 15sCreate dashboards to visualize metrics:
Request Rate:
rate(lightweight_web_http_requests_total[5m])
Request Duration (p95):
histogram_quantile(0.95, rate(lightweight_web_http_request_duration_seconds_bucket[5m]))
Error Rate:
rate(lightweight_web_trpc_requests_total{status="error"}[5m])
Active WebSocket Connections:
lightweight_web_ws_connections_active
Queue Job Processing Rate:
rate(lightweight_web_queue_jobs_total{status="completed"}[5m])
Example Prometheus alerting rules:
groups:
- name: lightweight_web
rules:
- alert: HighErrorRate
expr: rate(lightweight_web_trpc_requests_total{status="error"}[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors/second"
- alert: HighRequestLatency
expr: histogram_quantile(0.95, rate(lightweight_web_http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High request latency detected"
description: "P95 latency is {{ $value }} seconds"
- alert: ServiceDown
expr: up{job="lightweight-web"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service is down"
description: "Core Stack web service has been down for more than 1 minute"
- alert: QueueBacklog
expr: lightweight_web_queue_jobs_waiting > 1000
for: 10m
labels:
severity: warning
annotations:
summary: "Queue backlog is growing"
description: "{{ $value }} jobs waiting in queue"For production environments, aggregate logs using:
- ELK Stack (Elasticsearch, Logstash, Kibana)
- Loki (Grafana Loki)
- Cloud providers (CloudWatch, Stackdriver, Azure Monitor)
Example Logstash configuration:
input {
file {
path => "/var/log/lightweight-web/*.log"
codec => json
}
}
filter {
# Add any filtering/enrichment here
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "lightweight-web-%{+YYYY.MM.dd}"
}
}Observability-related environment variables:
# Logging
LOG_LEVEL=info # trace, debug, info, warn, error, fatal
NODE_ENV=production # production (JSON logs) or development (pretty logs)
# Application
PORT=3000 # HTTP server port
WS_PORT=3001 # WebSocket server port
# Dependencies (for health checks)
DATABASE_URL=postgresql://...
REDIS_URL=redis://...-
Monitor Key Metrics
- Request rate and latency (p50, p95, p99)
- Error rates
- Queue depth and processing time
- Database query performance
- Active connections (WebSocket, database, SSH)
-
Set Up Alerts
- High error rates
- Elevated latency
- Service availability
- Resource exhaustion (memory, connections)
- Queue backlog
-
Use Structured Logging
- Always use structured fields for searchability
- Include correlation IDs for request tracing
- Log at appropriate levels
- Avoid logging sensitive information
-
Regular Review
- Review dashboards regularly
- Adjust alert thresholds based on baselines
- Archive or remove obsolete metrics
- Update documentation as system evolves
Check for metrics indicating the cause:
process_resident_memory_bytes
lightweight_web_ws_connections_active
lightweight_web_db_connections_active
Identify slow endpoints:
topk(10, histogram_quantile(0.95,
rate(lightweight_web_trpc_request_duration_seconds_bucket[5m])
))
Check job processing rate vs. addition rate:
rate(lightweight_web_queue_jobs_total{status="added"}[5m])
rate(lightweight_web_queue_jobs_total{status="completed"}[5m])
Review health check logs:
curl http://localhost:3000/api/health/ready | jqCheck connection metrics:
lightweight_web_db_connections_active
lightweight_web_ws_connections_active