Claude Code Structured Logging (2026)
Structured logging transforms how developers debug and monitor applications. When combined with Claude Code’s capabilities, you gain powerful insights into your development workflow. This guide covers practical patterns for implementing structured logging that integrate smoothly with Claude Code and various skills like supermemory for knowledge management and tdd for test-driven development workflows.
Why Structured Logging Matters
Traditional console logging produces human-readable but machine-parseable text. Structured logging instead outputs JSON or similarly formatted data that tools can search, filter, and analyze programmatically. This approach becomes essential when debugging complex interactions with Claude Code’s tool execution or when maintaining audit trails across development sessions.
Consider the difference between these two approaches:
// Traditional logging
console.log("User " + userId + " purchased " + item + " for " + price);
// Structured logging
console.log(JSON.stringify({
event: "purchase_completed",
userId: userId,
item: item,
price: price,
timestamp: new Date().toISOString()
}));
The structured version enables filtering all purchases by a specific user, aggregating spending patterns, or triggering alerts when purchase values exceed thresholds. More importantly, when something breaks at 2am, you can run a single jq command against your logs instead of reading through thousands of lines of free-form text.
Traditional vs. Structured Logging: A Comparison
| Aspect | Traditional Logging | Structured Logging |
|---|---|---|
| Format | Free-form text | JSON / key-value pairs |
| Machine readability | Requires regex parsing | Native. any JSON tool works |
| Searchability | Slow, brittle | Fast, deterministic |
| Aggregation | Manual extraction | Direct field access |
| Log platforms | Limited support | Native ingestion (Datadog, Loki, etc.) |
| Cost at scale | High (verbose text) | Lower (compact, compressible) |
| Developer onboarding | Easy to read immediately | Slight initial setup cost |
The tradeoff is clear: structured logging has a small setup cost but compounds into significant operational benefits as your codebase grows.
Implementing Structured Logging in Claude Code Projects
When working with Claude Code, you often execute commands and scripts that generate output. Capturing this output in structured format provides long-term benefits for debugging and knowledge retention.
JSON Logger Implementation
Create a reusable logger module for your projects:
const logger = {
log: (level, message, context = {}) => {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...context,
// Include Claude Code session context when available
sessionId: process.env.CLAUDE_SESSION_ID || null
};
console.log(JSON.stringify(entry));
},
info: (message, context) => logger.log("INFO", message, context),
warn: (message, context) => logger.log("WARN", message, context),
error: (message, context) => logger.log("ERROR", message, context),
debug: (message, context) => logger.log("DEBUG", message, context)
};
module.exports = logger;
This is intentionally minimal. You do not need a heavy library like Winston or Bunyan to get the benefits of structured logging. a small object with consistent field shape is enough to unlock jq queries and log platform ingestion. If your project already uses a logging library, most support structured output via a configuration option (e.g., Winston’s json format or Pino’s default output).
Extending the Logger for Production
Once you have the basic pattern working, you can layer on production concerns without restructuring:
const os = require("os");
const baseContext = {
service: process.env.SERVICE_NAME || "unknown",
env: process.env.NODE_ENV || "development",
host: os.hostname(),
pid: process.pid
};
const logger = {
log: (level, message, context = {}) => {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...baseContext,
...context,
sessionId: process.env.CLAUDE_SESSION_ID || null
};
// In production, write to stderr so stdout stays clean for app output
const stream = level === "ERROR" ? process.stderr : process.stdout;
stream.write(JSON.stringify(entry) + "\n");
},
info: (message, context) => logger.log("INFO", message, context),
warn: (message, context) => logger.log("WARN", message, context),
error: (message, context) => logger.log("ERROR", message, context),
debug: (message, context) => {
// Only emit DEBUG entries when explicitly enabled
if (process.env.LOG_LEVEL === "debug") {
logger.log("DEBUG", message, context);
}
}
};
module.exports = logger;
The baseContext pattern means every log entry automatically carries service name, environment, and host. information that becomes critical when aggregating logs from multiple services or deployment targets.
Integrating with Claude Code Sessions
When Claude Code executes tools, you can capture structured metadata about the interaction. This proves particularly valuable when using skills like supermemory to retain context across sessions or when debugging complex workflows involving pdf processing or frontend-design tasks.
// Capture Claude Code tool execution
const toolLogger = (toolName, input, output, duration) => {
logger.info("claude_tool_executed", {
tool: toolName,
inputKeys: Object.keys(input),
success: !output.error,
durationMs: duration,
// Tag by skill context if available
skillContext: process.env.CLAUDE_SKILL_CONTEXT || "general"
});
};
You can wrap this around any shell commands or file operations Claude Code invokes on your behalf. Over time, you build a searchable audit trail: which tools ran, how long they took, and whether they succeeded. This is especially useful when a multi-step Claude Code task fails partway through. you can replay the log to see exactly where the pipeline broke.
Best Practices for Development Workflows
Consistent Field Naming
Establish naming conventions early in your project. Use camelCase for field names and include units for numeric values. Inconsistent naming is the most common reason structured logs fail to deliver value. if half your codebase uses duration and the other half uses durationMs, your aggregation queries produce wrong numbers silently.
logger.info("request_processed", {
requestId: "req_abc123",
processingTimeMs: 145,
recordCount: 10,
status: "success"
});
A few concrete conventions that pay off:
- Always suffix time values with
Ms,Sec, orNsto make units explicit - Use
snake_casefor event names (user_login,file_uploaded) andcamelCasefor field names - Prefer
booleanfields over string"true"/"false". they sort and filter correctly in every log platform - Use
nullfor absent optional values rather than omitting the field entirely, so your schema stays consistent
Contextual Enrichment
Include relevant context in every log entry. This means adding user identifiers, request IDs, and environmental information that help trace issues:
function withContext(handler) {
return (req, res) => {
const requestId = req.headers["x-request-id"] || crypto.randomUUID();
const startTime = Date.now();
// Wrap the handler to add automatic logging
try {
const result = handler(req, res);
logger.info("request_completed", {
requestId,
method: req.method,
path: req.path,
durationMs: Date.now() - startTime,
statusCode: res.statusCode
});
return result;
} catch (error) {
logger.error("request_failed", {
requestId,
method: req.method,
path: req.path,
durationMs: Date.now() - startTime,
error: error.message,
stack: error.stack
});
throw error;
}
};
}
The requestId thread is particularly important. When you can attach a single ID to all log entries generated by one request. including downstream calls to other services. you can reconstruct the entire lifecycle of a request from logs alone. Many teams use a middleware package like express-request-id to generate and propagate these IDs automatically, but the manual pattern above works well in Claude Code projects where you want full control.
Log Levels and When to Use Them
Reserve ERROR for actual failures requiring intervention. Use WARN for recoverable issues or deprecated usage patterns. INFO captures normal workflow milestones, while DEBUG provides detailed execution traces useful during active development.
| Level | Use case | Example |
|---|---|---|
| ERROR | Unrecoverable failures, requires immediate attention | Database connection lost, uncaught exception |
| WARN | Recoverable issues, something is wrong | Retry attempt 2/3, deprecated API used |
| INFO | Normal operational milestones | Request completed, job started, user authenticated |
| DEBUG | Detailed execution context for development | SQL query text, intermediate computation values |
When using Claude Code with tdd workflows, structured logging helps track test execution patterns:
// In test setup
logger.info("test_started", {
testFile: __filename,
testName: "should_process_user_input",
framework: "jest"
});
logger.info("test_completed", {
testFile: __filename,
testName: "should_process_user_input",
passed: true,
durationMs: elapsed
});
This produces a machine-readable test execution history that you can feed back into Claude Code to ask questions like “which tests are consistently slow?” or “which test files have the most failures this week?”.
Avoiding Common Pitfalls
Do not log sensitive data. Passwords, API keys, credit card numbers, and PII should never appear in log entries regardless of level. Build a sanitizer for known sensitive fields:
const SENSITIVE_KEYS = new Set(["password", "token", "apiKey", "ssn", "creditCard"]);
function sanitize(obj) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k,
SENSITIVE_KEYS.has(k) ? "[REDACTED]" : v
])
);
}
logger.info("user_updated", sanitize(userPayload));
Do not stringify objects manually. Calling JSON.stringify on nested errors loses the stack trace because Error objects do not serialize cleanly. Instead, explicitly extract the fields you need:
logger.error("operation_failed", {
error: err.message,
stack: err.stack,
code: err.code || null
});
Querying and Analysis
Structured logs become powerful when you can search them effectively. Use tools like jq for command-line analysis:
Find all errors from the past hour
cat logs/app.log | jq 'select(.level == "ERROR" and
.timestamp > "2026-03-14T14:00:00Z")'
Aggregate purchase values
cat logs/app.log | jq -s 'map(select(.event == "purchase_completed")) |
map(.price) | add'
Count errors by service
cat logs/app.log | jq -s 'map(select(.level == "ERROR")) |
group_by(.service) |
map({service: .[0].service, count: length}) |
sort_by(-.count)'
Find slow requests (over 500ms)
cat logs/app.log | jq 'select(.event == "request_completed" and .durationMs > 500) |
{path, durationMs, requestId}'
For larger-scale analysis, ship structured logs to platforms like Elasticsearch, Datadog, or Loki. Many Claude Code users combine this with supermemory to maintain searchable archives of development session insights.
Real-World Scenario: Debugging a Failing Background Job
Imagine a background job that processes uploaded files is silently failing for a subset of users. With traditional logging, you might have entries like "Processing file for user 4421" and "Job failed" with no connection between them. With structured logging, you can run:
Find all jobs that failed for a specific user
cat logs/worker.log | jq 'select(.userId == "4421" and .level == "ERROR")'
Check if failures correlate with file type
cat logs/worker.log | jq -s '
map(select(.event == "job_failed")) |
group_by(.fileType) |
map({fileType: .[0].fileType, failures: length})'
Within minutes you have concrete data: failures correlate entirely with .heic files uploaded from iPhones. The fix is targeted and the investigation took minutes rather than hours of log grepping.
Conclusion
Structured logging represents an investment in debuggability and observability that pays dividends throughout your project’s lifecycle. By implementing consistent JSON logging, enriching entries with contextual information, and integrating with Claude Code’s execution model, you create a foundation for effective troubleshooting and knowledge retention.
Start by adding structured logging to new features and gradually migrate existing code. Tools like jq make it easy to begin querying your logs immediately, while platforms like Elasticsearch handle larger-scale analysis needs as your application grows. The naming conventions and sanitization patterns above are worth establishing on day one. retrofitting them into a large codebase is far more expensive than getting them right from the start.
The goal is not perfect logs. It is logs that let you answer specific questions quickly. Every structured field you add is a future query you can run without touching the code.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Related Reading
- AI Assisted Code Review Workflow Best Practices
- Claude Code Audit Logging for Enterprise Compliance Workflow
- Claude Code Cypress Custom Commands Workflow Best Practices
- Claude Code for Instructor Structured LLM — Guide
Built by theluckystrike. More at zovo.one
Know your costs → Use our Claude Code Cost Calculator to estimate your monthly spend.