Monitoring & Logging
Application monitoring, logging, and observability for production
📋 Overview
Effective monitoring and logging are essential for maintaining production systems. This guide covers logging strategies, error tracking, performance monitoring, and alerting.
📝 Logging Strategy
Log Levels
| Level | Usage | Example |
|---|---|---|
| debug | Development details | "Query executed in 15ms" |
| info | Normal operations | "User logged in", "Server started" |
| warn | Warning conditions | "Cache miss", "Deprecated API used" |
| error | Error conditions | "Database connection failed" |
| fatal | Critical failures | "Out of memory", "Cannot start" |
Pino Logger Configuration
Setup (src/lib/logger.ts):
import pino from 'pino'
const isProduction = process.env.NODE_ENV === 'production'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Development: pretty print
transport: isProduction ? undefined : {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
}
},
// Production: JSON output
formatters: isProduction ? {
level: (label) => ({ level: label }),
bindings: (bindings) => ({
pid: bindings.pid,
host: bindings.hostname,
}),
} : undefined,
// Add custom fields
base: {
env: process.env.NODE_ENV,
service: 'izri-api',
},
// Redact sensitive data
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'password',
'token',
'apiKey',
],
remove: true,
},
})
Request Logging Middleware
import { Hono } from 'hono'
import { logger as pinoLogger } from 'hono-pino'
import { logger } from './lib/logger'
const app = new Hono()
app.use('*', pinoLogger({
pino: logger,
http: {
// Customize request logging
reqId: () => crypto.randomUUID(),
customSuccessMessage: (ctx) => {
return `${ctx.req.method} ${ctx.req.path} - ${ctx.res.status}`
},
customErrorMessage: (ctx, error) => {
return `${ctx.req.method} ${ctx.req.path} - Error: ${error.message}`
},
},
}))
Structured Logging Examples
// Basic logging
logger.info('User logged in')
// With context
logger.info({ userId: user.id, email: user.email }, 'User logged in')
// Error logging
logger.error({ err: error, userId: user.id }, 'Failed to create project')
// Performance tracking
const start = Date.now()
const result = await heavyOperation()
logger.info({ duration: Date.now() - start }, 'Heavy operation completed')
// Child loggers (add context)
const reqLogger = logger.child({ requestId: ctx.get('requestId') })
reqLogger.info('Processing request')
🔍 Error Tracking
Sentry Integration
Installation:
pnpm add @sentry/node @sentry/tracing
Configuration (src/lib/sentry.ts):
import * as Sentry from '@sentry/node'
import { ProfilingIntegration } from '@sentry/profiling-node'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
// Performance monitoring
tracesSampleRate: 0.1, // 10% of transactions
// Profiling
profilesSampleRate: 0.1,
integrations: [
new ProfilingIntegration(),
],
// Filter out health checks
beforeSend(event, hint) {
const url = event.request?.url
if (url?.includes('/health') || url?.includes('/metrics')) {
return null
}
return event
},
})
Hono Integration:
import { Hono } from 'hono'
import * as Sentry from '@sentry/node'
const app = new Hono()
// Request context
app.use('*', async (c, next) => {
const transaction = Sentry.startTransaction({
op: 'http.server',
name: `${c.req.method} ${c.req.path}`,
})
Sentry.configureScope((scope) => {
scope.setSpan(transaction)
scope.setContext('request', {
method: c.req.method,
url: c.req.url,
headers: Object.fromEntries(c.req.raw.headers),
})
})
try {
await next()
} finally {
transaction.finish()
}
})
// Error handler
app.onError((err, c) => {
Sentry.captureException(err, {
contexts: {
request: {
method: c.req.method,
url: c.req.url,
},
user: {
id: c.get('userId'),
},
},
})
logger.error({ err }, 'Unhandled error')
return c.json({ error: 'Internal server error' }, 500)
})
Manual Error Tracking:
try {
await riskyOperation()
} catch (error) {
// Add custom context
Sentry.withScope((scope) => {
scope.setLevel('error')
scope.setTag('operation', 'createProject')
scope.setContext('details', {
projectId,
userId,
})
Sentry.captureException(error)
})
throw error
}
📊 Metrics Collection
OpenTelemetry Setup
Installation:
pnpm add @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
Configuration (src/lib/telemetry.ts):
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
const sdk = new NodeSDK({
serviceName: 'izri-api',
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/metrics',
}),
exportIntervalMillis: 60000, // Export every minute
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false, // Too noisy
},
}),
],
})
sdk.start()
// Graceful shutdown
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0))
})
Custom Metrics:
import { metrics } from '@opentelemetry/api'
const meter = metrics.getMeter('izri-api')
// Counter
const requestCounter = meter.createCounter('http_requests_total', {
description: 'Total number of HTTP requests',
})
requestCounter.add(1, { method: 'GET', route: '/api/projects' })
// Histogram (for latencies)
const requestDuration = meter.createHistogram('http_request_duration_ms', {
description: 'HTTP request duration in milliseconds',
})
const start = Date.now()
await handleRequest()
requestDuration.record(Date.now() - start, { method: 'GET', route: '/api/projects' })
// Gauge (current values)
const activeConnections = meter.createObservableGauge('active_connections', {
description: 'Number of active connections',
})
activeConnections.addCallback((result) => {
result.observe(getActiveConnectionCount())
})
Custom Metrics Endpoint
import { Hono } from 'hono'
const metrics = new Hono()
let requestCount = 0
let errorCount = 0
const startTime = Date.now()
// Middleware to track metrics
app.use('*', async (c, next) => {
requestCount++
try {
await next()
if (c.res.status >= 400) errorCount++
} catch (error) {
errorCount++
throw error
}
})
// Prometheus-style metrics endpoint
metrics.get('/metrics', (c) => {
const uptime = (Date.now() - startTime) / 1000
return c.text(`
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total ${requestCount}
# HELP http_errors_total Total HTTP errors
# TYPE http_errors_total counter
http_errors_total ${errorCount}
# HELP process_uptime_seconds Process uptime in seconds
# TYPE process_uptime_seconds gauge
process_uptime_seconds ${uptime}
# HELP nodejs_memory_usage_bytes Node.js memory usage
# TYPE nodejs_memory_usage_bytes gauge
nodejs_memory_usage_bytes{type="heapUsed"} ${process.memoryUsage().heapUsed}
nodejs_memory_usage_bytes{type="heapTotal"} ${process.memoryUsage().heapTotal}
nodejs_memory_usage_bytes{type="rss"} ${process.memoryUsage().rss}
`.trim())
})
📈 Application Performance Monitoring (APM)
Custom Performance Tracking
import { performance } from 'perf_hooks'
class PerformanceTracker {
private marks = new Map<string, number>()
start(name: string) {
this.marks.set(name, performance.now())
}
end(name: string) {
const start = this.marks.get(name)
if (!start) return
const duration = performance.now() - start
logger.info({ name, duration }, 'Performance measurement')
// Send to APM
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
// OpenTelemetry will pick this up
}
this.marks.delete(name)
return duration
}
}
export const perf = new PerformanceTracker()
// Usage
perf.start('database.query.projects')
const projects = await db.query.projects.findMany()
perf.end('database.query.projects')
Database Query Monitoring
import { drizzle } from 'drizzle-orm/node-postgres'
import { logger } from './logger'
export const db = drizzle(pool, {
logger: {
logQuery(query, params) {
logger.debug({ query, params }, 'Database query')
},
},
})
// Slow query detection
const SLOW_QUERY_THRESHOLD = 1000 // 1 second
export async function monitoredQuery<T>(
name: string,
query: () => Promise<T>
): Promise<T> {
const start = Date.now()
try {
const result = await query()
const duration = Date.now() - start
if (duration > SLOW_QUERY_THRESHOLD) {
logger.warn({ name, duration }, 'Slow query detected')
}
return result
} catch (error) {
logger.error({ name, err: error }, 'Query failed')
throw error
}
}
// Usage
const projects = await monitoredQuery(
'getProjectsByUserId',
() => db.query.projects.findMany({ where: eq(projects.userId, userId) })
)
🚨 Alerting
Alert Configuration Examples
Uptime monitoring (UptimeRobot, Pingdom, etc.):
monitors:
- name: API Health Check
url: https://api.yourdomain.com/health
interval: 60 # seconds
timeout: 30
expected_status: 200
- name: Web Application
url: https://yourdomain.com
interval: 60
timeout: 30
Error rate alerts (Sentry):
// Sentry Alert Rules
{
conditions: [
{
id: 'sentry.rules.conditions.event_frequency.EventFrequencyCondition',
interval: '5m',
value: 100, // More than 100 errors in 5 minutes
}
],
actions: [
{
id: 'sentry.mail.actions.NotifyEmailAction',
targetType: 'Team',
},
{
id: 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction',
channel: '#alerts',
}
]
}
Custom alerts (via metrics):
import { WebClient } from '@slack/web-api'
const slack = new WebClient(process.env.SLACK_BOT_TOKEN)
async function checkErrorRate() {
const errorRate = errorCount / requestCount
if (errorRate > 0.05) { // 5% error rate
await slack.chat.postMessage({
channel: '#alerts',
text: `🚨 High error rate detected: ${(errorRate * 100).toFixed(2)}%`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Error Rate Alert*\n\nCurrent error rate: ${(errorRate * 100).toFixed(2)}%\nThreshold: 5%\nTotal requests: ${requestCount}\nTotal errors: ${errorCount}`,
},
},
],
})
}
}
// Run every minute
setInterval(checkErrorRate, 60000)
📦 Log Aggregation
Using Docker Logs
# View all logs
docker compose logs -f
# Specific service
docker compose logs -f api
# Since timestamp
docker compose logs --since 2024-01-01T10:00:00
# Tail last 100 lines
docker compose logs --tail=100 api
Centralized Logging (ELK Stack)
Docker Compose with Filebeat:
services:
api:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
filebeat:
image: docker.elastic.co/beats/filebeat:8.11.0
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
Filebeat configuration (filebeat.yml):
filebeat.inputs:
- type: container
paths:
- '/var/lib/docker/containers/*/*.log'
processors:
- add_docker_metadata:
host: "unix:///var/run/docker.sock"
output.elasticsearch:
hosts: ["elasticsearch:9200"]
# Or send to Logstash
output.logstash:
hosts: ["logstash:5044"]
Cloud Logging
AWS CloudWatch:
# docker-compose.yml
services:
api:
logging:
driver: awslogs
options:
awslogs-region: us-east-1
awslogs-group: /izri/api
awslogs-stream: api
Google Cloud Logging:
import { Logging } from '@google-cloud/logging'
const logging = new Logging()
const log = logging.log('izri-api')
function cloudLog(message: string, severity: string = 'INFO') {
const entry = log.entry({
severity,
resource: {
type: 'global',
},
}, message)
log.write(entry)
}
🎯 Monitoring Best Practices
1. Log Levels in Production
// ❌ Don't log everything
logger.debug('Variable value:', someVar)
logger.debug('Entering function')
// ✅ Use appropriate levels
logger.info({ userId, projectId }, 'Project created')
logger.warn({ duration }, 'Slow query detected')
logger.error({ err }, 'Failed to process request')
2. Structured Logging
// ❌ Unstructured
logger.info('User john@example.com created project MyProject with ID 123')
// ✅ Structured
logger.info({
userId: 'user_123',
email: 'john@example.com',
projectId: 'proj_123',
projectName: 'MyProject',
}, 'Project created')
3. Correlation IDs
// Add request ID to all logs for a request
app.use('*', async (c, next) => {
const requestId = crypto.randomUUID()
c.set('requestId', requestId)
// Child logger with requestId
c.set('logger', logger.child({ requestId }))
await next()
})
// Use in handlers
app.get('/projects', async (c) => {
const log = c.get('logger')
log.info('Fetching projects') // Includes requestId
})
4. Sensitive Data Redaction
// Configure in logger
redact: {
paths: [
'password',
'token',
'apiKey',
'authorization',
'cookie',
],
remove: true,
}
// Or manually
function sanitize(obj: any) {
const clean = { ...obj }
delete clean.password
delete clean.token
return clean
}
logger.info(sanitize(user), 'User logged in')
🔗 Related Documentation
- Docker: Docker configuration
- Production: Production deployment
- Backend: Backend logging
- Error Handling: Error handling patterns
Deployment section complete! Continue with Features Documentation →