Logging and Observability
Structured Logging
Always use structured logging (JSON format) instead of unstructured text. Structured logs are machine-parseable, searchable, and can be aggregated across services.
Standard Log Entry Format
{
"timestamp": "2024-03-15T14:30:22.123Z",
"level": "INFO",
"message": "Order processed successfully",
"service": "order-service",
"version": "1.4.2",
"correlationId": "req-abc123",
"userId": "user-456",
"orderId": "ord-789",
"durationMs": 234,
"environment": "production"
}
Required Fields
Every log entry must include:
| Field |
Type |
Description |
timestamp |
ISO 8601 string |
When the event occurred (UTC) |
level |
string |
Log severity level |
message |
string |
Human-readable description of the event |
service |
string |
Name of the service emitting the log |
Recommended Fields
| Field |
Type |
Description |
correlationId |
string |
Request or trace identifier for cross-service correlation |
version |
string |
Application version or commit hash |
environment |
string |
Deployment environment (production, staging, etc.) |
userId |
string |
Authenticated user identifier (if applicable) |
durationMs |
number |
Operation duration in milliseconds |
error |
object |
Error details when level is ERROR |
Implementation Examples
// Node.js with pino
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: { level: (label) => ({ level: label.toUpperCase() }) },
timestamp: pino.stdTimeFunctions.isoTime,
base: { service: 'order-service', version: process.env.APP_VERSION || 'unknown' },
});
logger.info({ orderId: 'ord-789', durationMs: 234 }, 'Order processed successfully');
logger.error({ err, orderId: 'ord-789' }, 'Failed to process order');
# Python with structlog
import structlog
structlog.configure(processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer(),
])
logger = structlog.get_logger(service="order-service")
logger.info("order_processed", order_id="ord-789", duration_ms=234)
Log Levels Usage Guide
Choose the correct log level based on the audience and urgency:
| Level |
When to Use |
Examples |
Audience |
| ERROR |
Something failed and requires attention. The operation could not complete. |
Unhandled exception, database connection lost, payment processing failed, external API returned 5xx |
On-call engineer (triggers alert) |
| WARN |
Something unexpected happened but the operation continued. May need attention soon. |
Deprecated API used, retry succeeded after failure, cache miss on hot path, connection pool near limit |
Engineering team (review in daily triage) |
| INFO |
Normal operational events worth recording. The happy path. |
Request handled, order created, user logged in, migration completed, deploy started |
Operations team (dashboard monitoring) |
| DEBUG |
Detailed information useful for diagnosing issues. Not enabled in production by default. |
SQL query with parameters, HTTP request/response details, cache hit/miss, algorithm intermediate steps |
Developers (enabled during investigation) |
| TRACE |
Very fine-grained detail. Rarely used outside local development. |
Function entry/exit, loop iterations, variable state at each step |
Developers (local debugging only) |
Level Selection Rules
- If it means waking someone up at 3 AM, it is ERROR.
- If it means something is degraded but not broken, it is WARN.
- If it is the normal outcome of a user or system action, it is INFO.
- If it would help debug a problem but is too noisy for production, it is DEBUG.
- If you would only want it while stepping through code, it is TRACE.
What to Log and What NOT to Log
Log These Events
| Category |
Events |
| Requests |
Incoming HTTP requests (method, path, status, duration). API calls to external services. |
| Authentication |
Login success, login failure, logout, session expiry, MFA challenge. |
| Authorization |
Access denied events with user, resource, and required permission. |
| Errors |
Unhandled exceptions, failed operations, timeout events, circuit breaker trips. |
| State Changes |
Record creation/update/deletion, status transitions, configuration changes. |
| Performance |
Slow queries (above threshold), response time percentiles, queue depth. |
| Business Events |
Order placed, payment processed, subscription changed, export completed. |
Never Log These
| Category |
Reason |
Alternative |
| Passwords |
Credential exposure |
Log event type only ("login_attempt") |
| API keys and tokens |
Credential exposure |
Log last 4 characters at most |
| Credit card numbers |
PCI compliance violation |
Log last 4 digits only |
| Social Security Numbers |
PII regulation violation |
Log event type only |
| Session tokens |
Session hijacking risk |
Log session ID prefix or hash |
| Personal health data |
HIPAA violation |
Log event type with anonymized reference |
| Full request bodies with sensitive fields |
Data exposure |
Log sanitized version or field names only |
Correlation IDs and Request Tracing
A correlation ID (also called request ID or trace ID) links all log entries for a single request, even across multiple services.
Middleware Implementation
const { randomUUID } = require('crypto');
function correlationIdMiddleware(req, res, next) {
const correlationId = req.headers['x-correlation-id'] || randomUUID();
req.correlationId = correlationId;
res.setHeader('x-correlation-id', correlationId);
req.log = logger.child({ correlationId }); // Bind to logger context
next();
}
app.use(correlationIdMiddleware);
// Usage in route handlers
app.get('/api/orders/:id', async (req, res) => {
req.log.info({ orderId: req.params.id }, 'Fetching order');
const order = await orderService.getById(req.params.id);
res.json(order);
});
Propagation Across Services
Forward the correlation ID in outgoing requests:
async function callPaymentService(orderId, correlationId) {
return fetch('https://payment-service/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-correlation-id': correlationId },
body: JSON.stringify({ orderId }),
}).then(r => r.json());
}
Distributed Tracing
Core Concepts
| Concept |
Description |
| Trace |
End-to-end record of a request flowing through the system. Contains multiple spans. |
| Span |
A single unit of work (e.g., an HTTP request, a database query, a function call). Has a start time, duration, and metadata. |
| Context Propagation |
Passing trace context (trace ID, span ID, flags) between services via headers. |
| Parent-Child Relationship |
Spans form a tree: an outgoing HTTP call creates a child span of the handler span. |
OpenTelemetry Setup
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: 'order-service',
});
sdk.start();
Custom Spans
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service');
async function processOrder(orderId) {
return tracer.startActiveSpan('processOrder', async (span) => {
try {
span.setAttribute('order.id', orderId);
const order = await tracer.startActiveSpan('validateOrder', async (child) => {
const result = await validateOrder(orderId);
child.end();
return result;
});
await tracer.startActiveSpan('chargePayment', async (child) => {
await chargePayment(order);
child.end();
});
span.setStatus({ code: trace.SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
Metrics
Metric Types
| Type |
Description |
Use Case |
Example |
| Counter |
Monotonically increasing value. Only goes up (or resets to zero). |
Counting events, total requests, errors. |
http_requests_total{method="GET", status="200"} |
| Gauge |
Value that can go up or down. Point-in-time measurement. |
Current state, queue depth, active connections. |
db_connections_active{pool="primary"} |
| Histogram |
Samples observations into configurable buckets. Tracks distribution. |
Request latency, response size, query duration. |
http_request_duration_seconds_bucket{le="0.5"} |
Implementation Example (Prometheus Client)
const client = require('prom-client');
// Counter
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status'],
});
// Gauge
const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Number of active database connections',
});
// Histogram
const requestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'path'],
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
});
// Middleware to collect metrics
app.use((req, res, next) => {
const end = requestDuration.startTimer({ method: req.method, path: req.route?.path || req.path });
res.on('finish', () => {
end();
httpRequestsTotal.inc({ method: req.method, path: req.route?.path || req.path, status: res.statusCode });
});
next();
});
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
Alert Design Principles
Well-designed alerts reduce noise and improve response times.
Principles
| Principle |
Description |
| Actionable |
Every alert must have a clear action the responder can take. If there is no action, it is not an alert. |
| Not noisy |
Alerts that fire frequently without requiring action cause fatigue. Tune thresholds and suppress flapping. |
| Runbook linked |
Every alert includes a link to a runbook with investigation and resolution steps. |
| Severity-based |
Critical alerts page on-call; warnings create tickets; informational alerts appear on dashboards. |
| Symptom-based |
Alert on user-facing symptoms (error rate, latency), not causes (CPU usage, memory). |
| Include context |
Alert message includes service name, environment, current value, threshold, and dashboard link. |
Alert Template
alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "High 5xx error rate on {{ $labels.service }}"
description: |
Error rate is {{ $value | humanizePercentage }} (threshold: 5%).
Service: {{ $labels.service }}
Environment: {{ $labels.environment }}
dashboard: "https://grafana.example.com/d/http-overview"
runbook: "https://wiki.example.com/runbooks/high-error-rate"
Health Check Endpoints
Implement three standard health check endpoints:
| Endpoint |
Purpose |
Checks |
Response |
/health |
Basic liveness — is the process running? |
Process is alive, can serve HTTP |
200 OK with {"status": "ok"} |
/ready |
Readiness — can the service handle traffic? |
Database connected, cache reachable, dependencies healthy |
200 OK or 503 Service Unavailable |
/live |
Liveness probe — is the process stuck? |
Not deadlocked, event loop responsive |
200 OK |
Implementation
app.get('/health', (req, res) => {
res.json({ status: 'ok', version: process.env.APP_VERSION });
});
app.get('/ready', async (req, res) => {
const checks = {
database: await ping(db, 'SELECT 1'),
cache: await ping(redis, 'PING'),
};
const allHealthy = Object.values(checks).every(c => c.status === 'ok');
res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'ready' : 'not_ready', checks });
});
app.get('/live', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
async function ping(client, command) {
try {
await client.query(command);
return { status: 'ok' };
} catch (error) {
return { status: 'error', message: error.message };
}
}
Log Aggregation Platforms
| Platform |
Strengths |
Query Language |
Cost Model |
| ELK (Elasticsearch, Logstash, Kibana) |
Self-hosted, flexible, powerful full-text search |
KQL, Lucene |
Infrastructure cost (self-managed) |
| Datadog |
Unified logs, metrics, traces; strong APM |
Datadog query syntax |
Per-ingested-GB |
| CloudWatch Logs |
Native AWS integration, no infrastructure to manage |
CloudWatch Insights |
Per-ingested-GB + storage |
| Grafana Loki |
Lightweight log aggregation, label-based indexing |
LogQL |
Infrastructure cost (low storage) |
| Splunk |
Enterprise-grade search, compliance features |
SPL |
Per-ingested-GB (premium) |
Choosing a Platform
- Small team, AWS-native: CloudWatch Logs with Insights queries.
- Multi-cloud or hybrid: Datadog or self-hosted ELK.
- Already using Grafana for metrics: Add Loki for logs.
- Enterprise with compliance needs: Splunk or Datadog.
Observability Checklist
When implementing observability for a new service:
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: logging-observability3description: Logging and observability best practices — structured logging, log levels, correlation IDs, metrics, tracing, and alerting. Reference when implementing logging or monitoring. Use when this capability is needed.4---56# Logging and Observability78## Structured Logging910Always use structured logging (JSON format) instead of unstructured text. Structured logs are machine-parseable, searchable, and can be aggregated across services.1112### Standard Log Entry Format1314```json15{16 "timestamp": "2024-03-15T14:30:22.123Z",17 "level": "INFO",18 "message": "Order processed successfully",19 "service": "order-service",20 "version": "1.4.2",21 "correlationId": "req-abc123",22 "userId": "user-456",23 "orderId": "ord-789",24 "durationMs": 234,25 "environment": "production"26}27```2829### Required Fields3031Every log entry must include:3233| Field | Type | Description |34|-------|------|-------------|35| `timestamp` | ISO 8601 string | When the event occurred (UTC) |36| `level` | string | Log severity level |37| `message` | string | Human-readable description of the event |38| `service` | string | Name of the service emitting the log |3940### Recommended Fields4142| Field | Type | Description |43|-------|------|-------------|44| `correlationId` | string | Request or trace identifier for cross-service correlation |45| `version` | string | Application version or commit hash |46| `environment` | string | Deployment environment (production, staging, etc.) |47| `userId` | string | Authenticated user identifier (if applicable) |48| `durationMs` | number | Operation duration in milliseconds |49| `error` | object | Error details when level is ERROR |5051### Implementation Examples5253```javascript54// Node.js with pino55const pino = require('pino');56const logger = pino({57 level: process.env.LOG_LEVEL || 'info',58 formatters: { level: (label) => ({ level: label.toUpperCase() }) },59 timestamp: pino.stdTimeFunctions.isoTime,60 base: { service: 'order-service', version: process.env.APP_VERSION || 'unknown' },61});6263logger.info({ orderId: 'ord-789', durationMs: 234 }, 'Order processed successfully');64logger.error({ err, orderId: 'ord-789' }, 'Failed to process order');65```6667```python68# Python with structlog69import structlog70structlog.configure(processors=[71 structlog.processors.TimeStamper(fmt="iso"),72 structlog.processors.add_log_level,73 structlog.processors.JSONRenderer(),74])75logger = structlog.get_logger(service="order-service")76logger.info("order_processed", order_id="ord-789", duration_ms=234)77```7879## Log Levels Usage Guide8081Choose the correct log level based on the audience and urgency:8283| Level | When to Use | Examples | Audience |84|-------|-------------|----------|----------|85| **ERROR** | Something failed and requires attention. The operation could not complete. | Unhandled exception, database connection lost, payment processing failed, external API returned 5xx | On-call engineer (triggers alert) |86| **WARN** | Something unexpected happened but the operation continued. May need attention soon. | Deprecated API used, retry succeeded after failure, cache miss on hot path, connection pool near limit | Engineering team (review in daily triage) |87| **INFO** | Normal operational events worth recording. The happy path. | Request handled, order created, user logged in, migration completed, deploy started | Operations team (dashboard monitoring) |88| **DEBUG** | Detailed information useful for diagnosing issues. Not enabled in production by default. | SQL query with parameters, HTTP request/response details, cache hit/miss, algorithm intermediate steps | Developers (enabled during investigation) |89| **TRACE** | Very fine-grained detail. Rarely used outside local development. | Function entry/exit, loop iterations, variable state at each step | Developers (local debugging only) |9091### Level Selection Rules9293- If it means waking someone up at 3 AM, it is **ERROR**.94- If it means something is degraded but not broken, it is **WARN**.95- If it is the normal outcome of a user or system action, it is **INFO**.96- If it would help debug a problem but is too noisy for production, it is **DEBUG**.97- If you would only want it while stepping through code, it is **TRACE**.9899## What to Log and What NOT to Log100101### Log These Events102103| Category | Events |104|----------|--------|105| Requests | Incoming HTTP requests (method, path, status, duration). API calls to external services. |106| Authentication | Login success, login failure, logout, session expiry, MFA challenge. |107| Authorization | Access denied events with user, resource, and required permission. |108| Errors | Unhandled exceptions, failed operations, timeout events, circuit breaker trips. |109| State Changes | Record creation/update/deletion, status transitions, configuration changes. |110| Performance | Slow queries (above threshold), response time percentiles, queue depth. |111| Business Events | Order placed, payment processed, subscription changed, export completed. |112113### Never Log These114115| Category | Reason | Alternative |116|----------|--------|-------------|117| Passwords | Credential exposure | Log event type only ("login_attempt") |118| API keys and tokens | Credential exposure | Log last 4 characters at most |119| Credit card numbers | PCI compliance violation | Log last 4 digits only |120| Social Security Numbers | PII regulation violation | Log event type only |121| Session tokens | Session hijacking risk | Log session ID prefix or hash |122| Personal health data | HIPAA violation | Log event type with anonymized reference |123| Full request bodies with sensitive fields | Data exposure | Log sanitized version or field names only |124125## Correlation IDs and Request Tracing126127A correlation ID (also called request ID or trace ID) links all log entries for a single request, even across multiple services.128129### Middleware Implementation130131```javascript132const { randomUUID } = require('crypto');133134function correlationIdMiddleware(req, res, next) {135 const correlationId = req.headers['x-correlation-id'] || randomUUID();136 req.correlationId = correlationId;137 res.setHeader('x-correlation-id', correlationId);138 req.log = logger.child({ correlationId }); // Bind to logger context139 next();140}141app.use(correlationIdMiddleware);142143// Usage in route handlers144app.get('/api/orders/:id', async (req, res) => {145 req.log.info({ orderId: req.params.id }, 'Fetching order');146 const order = await orderService.getById(req.params.id);147 res.json(order);148});149```150151### Propagation Across Services152153Forward the correlation ID in outgoing requests:154155```javascript156async function callPaymentService(orderId, correlationId) {157 return fetch('https://payment-service/api/charge', {158 method: 'POST',159 headers: { 'Content-Type': 'application/json', 'x-correlation-id': correlationId },160 body: JSON.stringify({ orderId }),161 }).then(r => r.json());162}163```164165## Distributed Tracing166167### Core Concepts168169| Concept | Description |170|---------|-------------|171| **Trace** | End-to-end record of a request flowing through the system. Contains multiple spans. |172| **Span** | A single unit of work (e.g., an HTTP request, a database query, a function call). Has a start time, duration, and metadata. |173| **Context Propagation** | Passing trace context (trace ID, span ID, flags) between services via headers. |174| **Parent-Child Relationship** | Spans form a tree: an outgoing HTTP call creates a child span of the handler span. |175176### OpenTelemetry Setup177178```javascript179const { NodeSDK } = require('@opentelemetry/sdk-node');180const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');181const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');182183const sdk = new NodeSDK({184 traceExporter: new OTLPTraceExporter({185 url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',186 }),187 instrumentations: [getNodeAutoInstrumentations()],188 serviceName: 'order-service',189});190sdk.start();191```192193### Custom Spans194195```javascript196const { trace } = require('@opentelemetry/api');197const tracer = trace.getTracer('order-service');198199async function processOrder(orderId) {200 return tracer.startActiveSpan('processOrder', async (span) => {201 try {202 span.setAttribute('order.id', orderId);203 const order = await tracer.startActiveSpan('validateOrder', async (child) => {204 const result = await validateOrder(orderId);205 child.end();206 return result;207 });208 await tracer.startActiveSpan('chargePayment', async (child) => {209 await chargePayment(order);210 child.end();211 });212 span.setStatus({ code: trace.SpanStatusCode.OK });213 } catch (error) {214 span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message });215 span.recordException(error);216 throw error;217 } finally {218 span.end();219 }220 });221}222```223224## Metrics225226### Metric Types227228| Type | Description | Use Case | Example |229|------|-------------|----------|---------|230| **Counter** | Monotonically increasing value. Only goes up (or resets to zero). | Counting events, total requests, errors. | `http_requests_total{method="GET", status="200"}` |231| **Gauge** | Value that can go up or down. Point-in-time measurement. | Current state, queue depth, active connections. | `db_connections_active{pool="primary"}` |232| **Histogram** | Samples observations into configurable buckets. Tracks distribution. | Request latency, response size, query duration. | `http_request_duration_seconds_bucket{le="0.5"}` |233234### Implementation Example (Prometheus Client)235236```javascript237const client = require('prom-client');238239// Counter240const httpRequestsTotal = new client.Counter({241 name: 'http_requests_total',242 help: 'Total number of HTTP requests',243 labelNames: ['method', 'path', 'status'],244});245246// Gauge247const activeConnections = new client.Gauge({248 name: 'active_connections',249 help: 'Number of active database connections',250});251252// Histogram253const requestDuration = new client.Histogram({254 name: 'http_request_duration_seconds',255 help: 'HTTP request duration in seconds',256 labelNames: ['method', 'path'],257 buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],258});259260// Middleware to collect metrics261app.use((req, res, next) => {262 const end = requestDuration.startTimer({ method: req.method, path: req.route?.path || req.path });263 res.on('finish', () => {264 end();265 httpRequestsTotal.inc({ method: req.method, path: req.route?.path || req.path, status: res.statusCode });266 });267 next();268});269270// Metrics endpoint271app.get('/metrics', async (req, res) => {272 res.set('Content-Type', client.register.contentType);273 res.end(await client.register.metrics());274});275```276277## Alert Design Principles278279Well-designed alerts reduce noise and improve response times.280281### Principles282283| Principle | Description |284|-----------|-------------|285| **Actionable** | Every alert must have a clear action the responder can take. If there is no action, it is not an alert. |286| **Not noisy** | Alerts that fire frequently without requiring action cause fatigue. Tune thresholds and suppress flapping. |287| **Runbook linked** | Every alert includes a link to a runbook with investigation and resolution steps. |288| **Severity-based** | Critical alerts page on-call; warnings create tickets; informational alerts appear on dashboards. |289| **Symptom-based** | Alert on user-facing symptoms (error rate, latency), not causes (CPU usage, memory). |290| **Include context** | Alert message includes service name, environment, current value, threshold, and dashboard link. |291292### Alert Template293294```yaml295alert: HighErrorRate296expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05297for: 5m298labels:299 severity: critical300 team: backend301annotations:302 summary: "High 5xx error rate on {{ $labels.service }}"303 description: |304 Error rate is {{ $value | humanizePercentage }} (threshold: 5%).305 Service: {{ $labels.service }}306 Environment: {{ $labels.environment }}307 dashboard: "https://grafana.example.com/d/http-overview"308 runbook: "https://wiki.example.com/runbooks/high-error-rate"309```310311## Health Check Endpoints312313Implement three standard health check endpoints:314315| Endpoint | Purpose | Checks | Response |316|----------|---------|--------|----------|317| `/health` | Basic liveness — is the process running? | Process is alive, can serve HTTP | `200 OK` with `{"status": "ok"}` |318| `/ready` | Readiness — can the service handle traffic? | Database connected, cache reachable, dependencies healthy | `200 OK` or `503 Service Unavailable` |319| `/live` | Liveness probe — is the process stuck? | Not deadlocked, event loop responsive | `200 OK` |320321### Implementation322323```javascript324app.get('/health', (req, res) => {325 res.json({ status: 'ok', version: process.env.APP_VERSION });326});327328app.get('/ready', async (req, res) => {329 const checks = {330 database: await ping(db, 'SELECT 1'),331 cache: await ping(redis, 'PING'),332 };333 const allHealthy = Object.values(checks).every(c => c.status === 'ok');334 res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'ready' : 'not_ready', checks });335});336337app.get('/live', (req, res) => {338 res.json({ status: 'ok', uptime: process.uptime() });339});340341async function ping(client, command) {342 try {343 await client.query(command);344 return { status: 'ok' };345 } catch (error) {346 return { status: 'error', message: error.message };347 }348}349```350351## Log Aggregation Platforms352353| Platform | Strengths | Query Language | Cost Model |354|----------|-----------|---------------|------------|355| ELK (Elasticsearch, Logstash, Kibana) | Self-hosted, flexible, powerful full-text search | KQL, Lucene | Infrastructure cost (self-managed) |356| Datadog | Unified logs, metrics, traces; strong APM | Datadog query syntax | Per-ingested-GB |357| CloudWatch Logs | Native AWS integration, no infrastructure to manage | CloudWatch Insights | Per-ingested-GB + storage |358| Grafana Loki | Lightweight log aggregation, label-based indexing | LogQL | Infrastructure cost (low storage) |359| Splunk | Enterprise-grade search, compliance features | SPL | Per-ingested-GB (premium) |360361### Choosing a Platform362363- **Small team, AWS-native**: CloudWatch Logs with Insights queries.364- **Multi-cloud or hybrid**: Datadog or self-hosted ELK.365- **Already using Grafana for metrics**: Add Loki for logs.366- **Enterprise with compliance needs**: Splunk or Datadog.367368## Observability Checklist369370When implementing observability for a new service:371372- [ ] Structured JSON logging with consistent field names.373- [ ] Log levels used appropriately (see table above).374- [ ] Correlation IDs generated and propagated across services.375- [ ] No secrets, PII, or credentials in log output.376- [ ] Health check endpoints implemented (`/health`, `/ready`, `/live`).377- [ ] Key metrics exposed (request rate, error rate, duration histograms).378- [ ] Distributed tracing configured with OpenTelemetry.379- [ ] Alerts defined for error rate, latency, and availability.380- [ ] Every alert linked to a runbook.381- [ ] Dashboards created for service overview and key flows.382- [ ] Log retention and rotation configured.383- [ ] Log aggregation pipeline verified end-to-end.384385---386> Converted and distributed by [TomeVault](https://tomevault.io/claim/claude-code-community-ireland) — claim your Tome and manage your conversions.387<!-- tomevault:4.0:skill_md:2026-04-13 -->