
Logging Best Practices
- 636 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
logging-best-practices is a Claude Code skill that helps developers ship structured searchable logs from TypeScript or Node services into centralized systems like ELK or CloudWatch.
About
logging-best-practices is an observability skill for Node.js and TypeScript backends needing centralized logging. It provides docker-compose setups for Elasticsearch 8.0.0, Logstash 8.0.0, and Kibana 8.0.0 with JSON tcp input pipelines, plus patterns for forwarding structured logs to CloudWatch. Developers reach for logging-best-practices when microservices produce unstructured console output, when on-call engineers cannot search production events, or when ELK or CloudWatch aggregation is being introduced. The skill covers logstash.conf tcp json codecs, index-friendly field naming, and operational log shipping suitable for debugging incidents and building dashboards in Kibana or CloudWatch Logs Insights.
- Ready-to-run ELK Stack docker-compose with optimized memory settings
- Logstash pipeline with timestamp parsing and automatic GeoIP enrichment
- Winston transport examples for shipping logs to Logstash and AWS CloudWatch
- Centralized logging setup that works for both local development and production
- Structured JSON logging configuration that supports search and alerting
Logging Best Practices by the numbers
- 636 all-time installs (skills.sh)
- Ranked #232 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill logging-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 636 |
|---|---|
| repo stars | ★ 305 |
| Security audit | 3 / 3 scanners passed |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you set up centralized logging for Node.js services?
Ship structured, searchable logs from any TypeScript or Node service into centralized systems like ELK or CloudWatch.
Who is it for?
Backend developers introducing structured centralized logging with ELK Stack or CloudWatch for TypeScript and Node.js services.
Skip if: Frontend-only projects, teams already standardized on OpenTelemetry tracing without log changes, or non-Node runtimes without adaptation.
When should I use this skill?
The user needs ELK, Logstash, Kibana, or CloudWatch centralized logging setup for Node.js or TypeScript services.
What you get
docker-compose ELK configs, logstash.conf pipelines, structured JSON log output, and CloudWatch shipping patterns.
- docker-compose.yml ELK configuration
- logstash.conf pipeline
- structured JSON logging patterns
By the numbers
- Uses Elasticsearch 8.0.0, Logstash 8.0.0, and Kibana 8.0.0
- Logstash tcp input on port 5000 with JSON codec
Files
Logging Best Practices
Table of Contents
Overview
Comprehensive guide to implementing structured, secure, and performant logging across applications. Covers log levels, structured logging formats, contextual information, PII protection, and centralized logging systems.
When to Use
- Setting up application logging infrastructure
- Implementing structured logging
- Configuring log levels for different environments
- Managing sensitive data in logs
- Setting up centralized logging
- Implementing distributed tracing
- Debugging production issues
- Compliance with logging regulations
Quick Start
Minimal working example:
// logger.ts
enum LogLevel {
DEBUG = 0, // Detailed information for debugging
INFO = 1, // General informational messages
WARN = 2, // Warning messages, potentially harmful
ERROR = 3, // Error messages, application can continue
FATAL = 4, // Critical errors, application must stop
}
class Logger {
constructor(private minLevel: LogLevel = LogLevel.INFO) {}
debug(message: string, context?: object) {
if (this.minLevel <= LogLevel.DEBUG) {
this.log(LogLevel.DEBUG, message, context);
}
}
info(message: string, context?: object) {
if (this.minLevel <= LogLevel.INFO) {
this.log(LogLevel.INFO, message, context);
}
}
warn(message: string, context?: object) {
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Log Levels | Log Levels |
| Structured Logging (JSON) | Structured Logging (JSON) |
| Contextual Logging | Contextual Logging |
| PII and Sensitive Data Handling | PII and Sensitive Data Handling |
| Performance Logging | Performance Logging |
| Centralized Logging | Centralized Logging |
| Distributed Tracing | Distributed Tracing |
| Log Sampling (High-Volume Services) | Log Sampling (High-Volume Services) |
Best Practices
✅ DO
- Use structured logging (JSON) in production
- Include correlation/request IDs in all logs
- Log at appropriate levels (don't overuse DEBUG)
- Redact sensitive data (PII, passwords, tokens)
- Include context (userId, requestId, etc.)
- Log errors with full stack traces
- Use centralized logging in distributed systems
- Set up log rotation to manage disk space
- Monitor log volume and costs
- Use async logging for performance
- Include timestamps in ISO 8601 format
- Log business events (user actions, transactions)
- Set up alerts for error patterns
❌ DON'T
- Log passwords, tokens, or sensitive data
- Use console.log in production
- Log at DEBUG level in production by default
- Log inside tight loops (use sampling)
- Include PII without anonymization
- Ignore log rotation (disk will fill up)
- Use synchronous logging in hot paths
- Log to multiple transports without need
- Forget to include error stack traces
- Log binary data or large objects
- Use string concatenation (use structured fields)
- Log every single request in high-volume APIs
Centralized Logging
Centralized Logging
ELK Stack (Elasticsearch, Logstash, Kibana)
# docker-compose.yml
version: "3"
services:
elasticsearch:
image: elasticsearch:8.0.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
logstash:
image: logstash:8.0.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "5000:5000"
depends_on:
- elasticsearch
kibana:
image: kibana:8.0.0
ports:
- "5601:5601"
depends_on:
- elasticsearch# logstash.conf
input {
tcp {
port => 5000
codec => json
}
}
filter {
# Parse timestamp
date {
match => ["timestamp", "ISO8601"]
}
# Add geo-location if IP present
if [ip] {
geoip {
source => "ip"
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}Ship Logs to ELK
// winston-elk.ts
import winston from "winston";
import "winston-logstash";
const logger = winston.createLogger({
transports: [
new winston.transports.Logstash({
port: 5000,
host: "logstash",
node_name: "user-service",
max_connect_retries: -1,
}),
],
});AWS CloudWatch Logs
// cloudwatch-logger.ts
import winston from "winston";
import WinstonCloudWatch from "winston-cloudwatch";
const logger = winston.createLogger({
transports: [
new WinstonCloudWatch({
logGroupName: "/aws/lambda/user-service",
logStreamName: () => {
const date = new Date().toISOString().split("T")[0];
return `${date}-${process.env.LAMBDA_VERSION}`;
},
awsRegion: "us-east-1",
jsonMessage: true,
}),
],
});Contextual Logging
Contextual Logging
Request Context Middleware
// request-logger.ts
import { v4 as uuidv4 } from "uuid";
import { AsyncLocalStorage } from "async_hooks";
const asyncLocalStorage = new AsyncLocalStorage();
// Middleware to add request context
export function requestLogger(req, res, next) {
const requestId = req.headers["x-request-id"] || uuidv4();
const context = {
requestId,
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.headers["user-agent"],
userId: req.user?.id,
};
asyncLocalStorage.run(context, () => {
logger.info("Request started", context);
// Log response when finished
res.on("finish", () => {
logger.info("Request completed", {
...context,
statusCode: res.statusCode,
duration: Date.now() - req.startTime,
});
});
req.startTime = Date.now();
next();
});
}
// Logger wrapper that includes context
export function getLogger() {
const context = asyncLocalStorage.getStore();
return {
info: (message: string, meta?: object) =>
logger.info(message, { ...context, ...meta }),
error: (message: string, error: Error, meta?: object) =>
logger.error(message, { ...context, error, ...meta }),
warn: (message: string, meta?: object) =>
logger.warn(message, { ...context, ...meta }),
debug: (message: string, meta?: object) =>
logger.debug(message, { ...context, ...meta }),
};
}
// Usage in route handler
app.get("/api/users/:id", async (req, res) => {
const log = getLogger();
log.info("Fetching user", { userId: req.params.id });
try {
const user = await userService.findById(req.params.id);
log.info("User found", { userId: user.id });
res.json(user);
} catch (error) {
log.error("Failed to fetch user", error, { userId: req.params.id });
res.status(500).json({ error: "Internal server error" });
}
});Correlation IDs
// correlation-id.ts
export class CorrelationIdManager {
private static storage = new AsyncLocalStorage<string>();
static run<T>(correlationId: string, callback: () => T): T {
return this.storage.run(correlationId, callback);
}
static get(): string | undefined {
return this.storage.getStore();
}
}
// Middleware
app.use((req, res, next) => {
const correlationId = req.headers["x-correlation-id"] || uuidv4();
res.setHeader("x-correlation-id", correlationId);
CorrelationIdManager.run(correlationId, () => {
next();
});
});
// Enhanced logger
const enhancedLogger = {
info: (message: string, meta?: object) =>
logger.info(message, {
correlationId: CorrelationIdManager.get(),
...meta,
}),
};Distributed Tracing
Distributed Tracing
// tracing.ts
import opentelemetry from "@opentelemetry/api";
import { NodeTracerProvider } from "@opentelemetry/node";
import { SimpleSpanProcessor } from "@opentelemetry/tracing";
import { JaegerExporter } from "@opentelemetry/exporter-jaeger";
// Setup tracer
const provider = new NodeTracerProvider();
provider.addSpanProcessor(
new SimpleSpanProcessor(
new JaegerExporter({
serviceName: "user-service",
endpoint: "http://jaeger:14268/api/traces",
}),
),
);
provider.register();
const tracer = opentelemetry.trace.getTracer("user-service");
// Usage in application
app.get("/api/users/:id", async (req, res) => {
const span = tracer.startSpan("get-user", {
attributes: {
"http.method": req.method,
"http.url": req.url,
"user.id": req.params.id,
},
});
try {
const user = await fetchUser(req.params.id, span);
span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
res.json(user);
} catch (error) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: error.message,
});
res.status(500).json({ error: "Internal server error" });
} finally {
span.end();
}
});
async function fetchUser(userId: string, parentSpan: Span) {
const span = tracer.startSpan("database-query", {
parent: parentSpan,
attributes: { "db.statement": "SELECT * FROM users WHERE id = ?" },
});
try {
const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
return user;
} finally {
span.end();
}
}Log Levels
Log Levels
Standard Log Levels
// logger.ts
enum LogLevel {
DEBUG = 0, // Detailed information for debugging
INFO = 1, // General informational messages
WARN = 2, // Warning messages, potentially harmful
ERROR = 3, // Error messages, application can continue
FATAL = 4, // Critical errors, application must stop
}
class Logger {
constructor(private minLevel: LogLevel = LogLevel.INFO) {}
debug(message: string, context?: object) {
if (this.minLevel <= LogLevel.DEBUG) {
this.log(LogLevel.DEBUG, message, context);
}
}
info(message: string, context?: object) {
if (this.minLevel <= LogLevel.INFO) {
this.log(LogLevel.INFO, message, context);
}
}
warn(message: string, context?: object) {
if (this.minLevel <= LogLevel.WARN) {
this.log(LogLevel.WARN, message, context);
}
}
error(message: string, error?: Error, context?: object) {
if (this.minLevel <= LogLevel.ERROR) {
this.log(LogLevel.ERROR, message, {
...context,
error: {
message: error?.message,
stack: error?.stack,
name: error?.name,
},
});
}
}
fatal(message: string, error?: Error, context?: object) {
this.log(LogLevel.FATAL, message, {
...context,
error: {
message: error?.message,
stack: error?.stack,
name: error?.name,
},
});
process.exit(1);
}
private log(level: LogLevel, message: string, context?: object) {
const logEntry = {
timestamp: new Date().toISOString(),
level: LogLevel[level],
message,
...context,
};
console.log(JSON.stringify(logEntry));
}
}
// Usage
const logger = new Logger(
process.env.NODE_ENV === "production" ? LogLevel.INFO : LogLevel.DEBUG,
);
logger.debug("Processing request", { userId: "123", requestId: "abc" });
logger.info("User logged in", { userId: "123" });
logger.warn("Rate limit approaching", { userId: "123", count: 95 });
logger.error("Database connection failed", dbError, { query: "SELECT ..." });Log Sampling (High-Volume Services)
Log Sampling (High-Volume Services)
// log-sampler.ts
class SamplingLogger {
constructor(
private logger: Logger,
private sampleRate: number = 0.1, // 10% sampling
) {}
info(message: string, meta?: object) {
if (this.shouldSample()) {
this.logger.info(message, meta);
}
}
// Always log warnings and errors
warn(message: string, meta?: object) {
this.logger.warn(message, meta);
}
error(message: string, error: Error, meta?: object) {
this.logger.error(message, error, meta);
}
private shouldSample(): boolean {
return Math.random() < this.sampleRate;
}
// Sample based on user ID (consistent sampling)
infoSampled(userId: string, message: string, meta?: object) {
const hash = this.hashUserId(userId);
if (hash % 100 < this.sampleRate * 100) {
this.logger.info(message, { ...meta, sampled: true });
}
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash << 5) - hash + userId.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
}Performance Logging
Performance Logging
// performance-logger.ts
class PerformanceLogger {
private timers = new Map<string, number>();
start(operation: string) {
this.timers.set(operation, Date.now());
}
end(operation: string, metadata?: object) {
const startTime = this.timers.get(operation);
if (!startTime) return;
const duration = Date.now() - startTime;
this.timers.delete(operation);
logger.info(`Performance: ${operation}`, {
operation,
duration,
durationMs: duration,
...metadata,
});
// Alert if slow
if (duration > 1000) {
logger.warn(`Slow operation: ${operation}`, {
operation,
duration,
threshold: 1000,
...metadata,
});
}
}
async measure<T>(
operation: string,
fn: () => Promise<T>,
metadata?: object,
): Promise<T> {
this.start(operation);
try {
return await fn();
} finally {
this.end(operation, metadata);
}
}
}
// Usage
const perfLogger = new PerformanceLogger();
// Manual timing
perfLogger.start("database-query");
const users = await db.query("SELECT * FROM users");
perfLogger.end("database-query", { count: users.length });
// Automatic timing
const result = await perfLogger.measure(
"complex-operation",
async () => await processData(),
{ userId: "123" },
);PII and Sensitive Data Handling
PII and Sensitive Data Handling
Data Sanitization
// sanitizer.ts
const SENSITIVE_FIELDS = [
"password",
"token",
"apiKey",
"ssn",
"creditCard",
"email", // depending on regulations
"phone", // depending on regulations
];
function sanitize(obj: any): any {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sanitize);
}
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (
SENSITIVE_FIELDS.some((field) =>
key.toLowerCase().includes(field.toLowerCase()),
)
) {
sanitized[key] = "[REDACTED]";
} else if (typeof value === "object") {
sanitized[key] = sanitize(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
// Usage
logger.info(
"User data",
sanitize({
userId: "123",
email: "user@example.com", // Will be redacted
password: "secret123", // Will be redacted
name: "John Doe", // Will be logged
}),
);
// Output:
// {
// "userId": "123",
// "email": "[REDACTED]",
// "password": "[REDACTED]",
// "name": "John Doe"
// }Email/PII Masking
// masking.ts
function maskEmail(email: string): string {
const [local, domain] = email.split("@");
const maskedLocal =
local[0] + "*".repeat(local.length - 2) + local[local.length - 1];
return `${maskedLocal}@${domain}`;
}
function maskPhone(phone: string): string {
return phone.replace(/\d(?=\d{4})/g, "*");
}
function maskCreditCard(cc: string): string {
return cc.replace(/\d(?=\d{4})/g, "*");
}
// Usage
logger.info("User registered", {
userId: user.id,
email: maskEmail(user.email), // u***r@example.com
phone: maskPhone(user.phone), // ******1234
creditCard: maskCreditCard(user.card), // ************1234
});Structured Logging (JSON)
Structured Logging (JSON)
Node.js with Winston
// winston-logger.ts
import winston from "winston";
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
),
defaultMeta: {
service: "user-service",
environment: process.env.NODE_ENV,
},
transports: [
// Write to console
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}),
// Write to file
new winston.transports.File({
filename: "logs/error.log",
level: "error",
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
new winston.transports.File({
filename: "logs/combined.log",
maxsize: 5242880,
maxFiles: 5,
}),
],
});
// Usage
logger.info("User created", {
userId: user.id,
email: user.email,
requestId: req.id,
});
logger.error("Payment processing failed", {
error: error.message,
stack: error.stack,
orderId: order.id,
amount: order.total,
userId: user.id,
});Python with structlog
# logger.py
import structlog
import logging
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
# Usage
logger.info("user_created",
user_id=user.id,
email=user.email,
request_id=request.id
)
logger.error("payment_failed",
error=str(error),
order_id=order.id,
amount=order.total,
user_id=user.id
)Go with zap
// logger.go
package main
import (
"go.uber.org/zap"
"go.uber.org/zap"
)
func main() {
// Production config (JSON)
logger, _ := zap.NewProduction()
defer logger.Sync()
// Development config (human-readable)
// logger, _ := zap.NewDevelopment()
logger.Info("User created",
zap.String("userId", user.ID),
zap.String("email", user.Email),
zap.String("requestId", req.ID),
)
logger.Error("Payment processing failed",
zap.Error(err),
zap.String("orderId", order.ID),
zap.Float64("amount", order.Total),
zap.String("userId", user.ID),
)
// Sugared logger for less structured logs
sugar := logger.Sugar()
sugar.Infow("User login",
"userId", user.ID,
"ip", req.IP,
)
}#!/bin/bash
# health-check.sh - Check service health
# Usage: ./health-check.sh <service_url>
set -euo pipefail
SERVICE_URL="${{1:?Usage: $0 <service_url>}}"
echo "Checking health: $SERVICE_URL"
# TODO: Implement health checks
# - HTTP endpoint check
# - Response time validation
# - Dependency health
# - Resource utilization
# - Error rate check
echo "Health check complete."
# Monitoring Dashboard Configuration
# TODO: Customize for your monitoring platform (Grafana, Datadog, etc.)
dashboard:
title: "Service Dashboard"
refresh: 30s
panels:
- title: "Request Rate"
type: graph
# TODO: Add metric query
- title: "Error Rate"
type: graph
# TODO: Add metric query
- title: "Latency (p50/p95/p99)"
type: graph
# TODO: Add metric query
alerts:
- name: "High Error Rate"
# TODO: Configure alert thresholds
Related skills
How it compares
Use logging-best-practices over performance-profiler when the immediate need is log pipeline setup and searchability rather than CPU flamegraphs or load testing.
FAQ
Which log stack versions does logging-best-practices use?
logging-best-practices examples use Elasticsearch 8.0.0, Logstash 8.0.0, and Kibana 8.0.0 in docker-compose with Logstash listening on tcp port 5000 with a JSON codec for structured ingestion.
Which languages does logging-best-practices target?
logging-best-practices targets TypeScript and Node.js services emitting structured JSON logs shippable to ELK Stack or AWS CloudWatch. Configuration includes logstash.conf pipeline examples and docker-compose service definitions.
Is Logging Best Practices safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.