From coding-agent
Structured logging for any application — language-specific logger setup, log levels, what to log at each layer, request tracing. Mandatory for all implementations. Evaluator checks for it.
npx claudepluginhub devjarus/coding-agentThis skill uses the workspace's default tool permissions.
Logging is mandatory for every application, not optional. An app without logging is undebuggable in production. The evaluator checks for it, and missing logging is a finding.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Logging is mandatory for every application, not optional. An app without logging is undebuggable in production. The evaluator checks for it, and missing logging is a finding.
Pick the right logger. Always structured JSON, always configurable level, always request tracing.
| Language | Logger | Key Feature |
|---|---|---|
| Node.js/TS | pino + pino-http | Fast, JSON, child loggers, redact option |
| Python | structlog | Context binding, async-safe via contextvars |
| Java | SLF4J + Logback + logstash-logback-encoder | MDC for request tracing, Spring profile support |
| Go | log/slog (stdlib, 1.21+) | Structured, zero deps, JSON handler |
| Swift/iOS | os.Logger | Native, Instruments integration, Console.app filtering |
| React Native | react-native-logs + Sentry | Severity-based, namespaced, crash reporting |
For framework-specific patterns (Spring Boot, Next.js, Django, FastAPI, Express, Gin), see rules/frameworks.md.
For cloud platform integration (AWS CloudWatch, GCP, Datadog, Vercel, OpenTelemetry), see rules/cloud-platforms.md.
Every request: method, path, status, duration, requestId
Errors: stack trace, request context, user context
Startup: port, config (non-secret), connected services
Shutdown: reason, pending requests count
Operations: what's happening, with what input (redact secrets)
Decisions: branching logic that affects output
External calls: URL, status, duration, response size
Errors: full context, not just message
Queries: operation type, table/collection, duration (NOT the data itself)
Migrations: version applied, duration, success/failure
Connection: pool stats, reconnects, failures
Tool calls: name, params, result summary, duration
LLM calls: model, token usage (prompt + completion), duration
Subagents: which agent, what task, duration, result summary
Errors: full context + what the agent was trying to do
| Level | When | Example |
|---|---|---|
| error | Requires immediate attention. Something failed and can't recover. | DB connection lost, unhandled exception, critical API down |
| warn | Unexpected but handled. Worth investigating later. | Rate limited, retrying, fallback used, deprecated API called |
| info | Key state changes. The "story" of what happened. | Request start/end, user action, deployment started, query completed |
| debug | Developer diagnostics. Noisy. Off in production. | Full request/response body, cache hit/miss, SQL queries, tool params |
Rules:
info or warndebugLOG_LEVEL) or CLI flag (--log-level)The implementor must set up logging in Wave 1 (foundation):
The evaluator checks for logging in every review:
console.log / print / println)logger.info({ userId, action }) not console.log("user did thing")try? without logging.logs/ directory (gitignored) is for debug telemetry. reports/, output/, dist/ are for application products. Never write logs to the output directory. CLIs write logs to stderr and results to stdout so piping works.info entry on startup with runtime version, platform, cwd, log file path, env-var presence (as booleans, never values), and upstream service URLs. Makes "works on my machine" bugs obvious from the first line of the log.CLI tools have a unique constraint: the process exits quickly. Async file writers that buffer logs in memory will lose those logs when process.exit() is called before the flush completes. This is a real bug, not a theoretical one.
Pattern for Node.js CLIs with pino:
// WRONG — async file stream, logs lost on exit
const dest = pino.destination({ dest: logFile, sync: false });
// Crash on exit: "sonic boom is not ready yet"
// Symptom: log file is 0 bytes
// RIGHT — sync file stream for CLIs
const dest = pino.destination({ dest: logFile, sync: true });
// For a CLI that runs one query and exits, the sync overhead is negligible
// (hundreds of log lines, not millions). Async requires explicit flush
// handlers at every exit point, which is error-prone.
For long-running servers, prefer sync: false — the async path is faster and the process stays alive long enough for flushes to complete. Install a graceful shutdown handler that calls logger.flush() before exit.
Equivalent gotchas in other runtimes:
logging.shutdown() in an atexit hook, or use structlog with a stream handler.os.Stderr by default — no flush needed. Custom async handlers need their own flush on shutdown.addShutdownHook="true" in the configuration, or call ((LoggerContext) LoggerFactory.getILoggerFactory()).stop() in a shutdown hook.Libraries, tools, and services often accept an optional logger parameter so callers can wire one in without forcing the dependency on everyone. The idiomatic fallback is a real-but-silent logger instance, not null or undefined checks at every call site.
Node.js / pino — use the built-in silent level:
// src/utils/logger.ts
import pino from "pino";
export type Logger = pino.Logger;
// Standard Null Object pattern — a real pino instance that discards everything.
// Supports .child(), serializers, etc. without any runtime branching.
export const silentLogger: Logger = pino({ level: "silent" });
// src/tools/web-search.ts
import { silentLogger } from "../utils/logger.js";
import type { Logger } from "../utils/logger.js";
export function createWebSearch(parentLogger?: Logger) {
const log = parentLogger?.child({ tool: "web_search" }) ?? silentLogger;
// ...log is always a real logger; no `if (log)` branches anywhere
}
Python / structlog:
import structlog
# structlog.get_logger() with no configuration is effectively a no-op
# until configured. For explicit silent fallback:
from structlog.testing import LogCapture
silent_logger = structlog.wrap_logger(None, processors=[])
Go / slog:
// Use io.Discard for a silent handler
silentLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
Rules for the Null Object pattern:
{ info: () => {} } object. Real instances support .child(), serializers, and level checks without special-casing.silentLogger from utils/logger.ts (or equivalent) and import it everywhere. Hand-rolled no-ops in every file is a code smell.silentLogger: Logger means callers get full type safety without | null unions.Module-level singletons make logger injection awkward. Use factories that accept an optional logger and return the configured component. Keep a backward-compatible default export so existing imports don't break.
// Factory — primary API
export function createWebSearch(parentLogger?: Logger) {
const log = parentLogger?.child({ tool: "web_search" }) ?? silentLogger;
return tool(
async ({ query }) => {
const start = performance.now();
log.debug({ query }, "web_search started");
try {
const results = await fetch(/* ... */);
const elapsed = Math.round(performance.now() - start);
log.info({ query, resultCount: results.length, elapsed }, "web_search complete");
return formatResults(results);
} catch (err) {
const elapsed = Math.round(performance.now() - start);
log.error({ err, query, elapsed }, "web_search failed");
return `Search failed: ${(err as Error).message}`;
}
},
{ name: "web_search", description: "...", schema }
);
}
// Backward-compatible default — existing tests/imports keep working
export const web_search = createWebSearch();
Benefits:
{ tool: "web_search" }, making filtering trivial.Every app should emit exactly one startup log entry with the runtime and environment context. This turns "I don't know what's different on your machine" into a one-line grep.
logger.info(
{
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
logFile,
env: {
// Presence booleans — NEVER log the values themselves
hasAnthropicKey: Boolean(process.env.ANTHROPIC_API_KEY),
hasOpenAIKey: Boolean(process.env.OPENAI_API_KEY),
SEARXNG_URL: process.env.SEARXNG_URL ?? "http://localhost:8080",
},
},
"App starting"
);
Log this once, at the very top of the logger factory so it's guaranteed to fire. Include:
package.json, pyproject.toml, etc.)This single line answers "is my environment set up correctly" without interactive debugging.