Help us improve
Share bugs, ideas, or general feedback.
From plugin-template
Guide for implementing structured logging in SideQuest plugins using @side-quest/core. Use when adding logging to new plugins, debugging existing plugins, or setting up log analysis.
npx claudepluginhub nathanvale/side-quest-marketplace-old --plugin plugin-templateHow this skill is triggered — by the user, by Claude, or both
Slash command
/plugin-template:loggingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implement structured, JSONL logging in SideQuest plugins using the `@side-quest/core/logging` factory.
Implements structured API request logging with correlation IDs, performance metrics, PII redaction, and security audit trails for debugging, analysis, and compliance.
Guides on using walkerOS logger patterns in sources/destinations: when to log, DRY principles, migration from console.log, and available methods (error, warn, info, debug, json, scope).
Provides structured JSON logging patterns with correlation IDs, context propagation, log levels, and required fields for observability and production incident debugging.
Share bugs, ideas, or general feedback.
Implement structured, JSONL logging in SideQuest plugins using the @side-quest/core/logging factory.
// package.json
{
"dependencies": {
"@side-quest/core": "workspace:*"
}
}
// src/logger.ts
import { createPluginLogger } from "@side-quest/core/logging";
export const {
initLogger,
createCorrelationId,
getSubsystemLogger,
logDir,
logFile,
} = createPluginLogger({
name: "my-plugin",
subsystems: ["scraper", "api", "cache"],
});
// Export typed subsystem loggers
export const scraperLogger = getSubsystemLogger("scraper");
export const apiLogger = getSubsystemLogger("api");
export const cacheLogger = getSubsystemLogger("cache");
// src/index.ts (CLI entry point)
import { initLogger, createCorrelationId, scraperLogger } from "./logger";
await initLogger();
const cid = createCorrelationId();
scraperLogger.info`Starting scrape operation ${cid}`;
// Pass cid through function calls
async function scrape(url: string, cid: string) {
scraperLogger.debug`Fetching ${url} ${cid}`;
try {
const result = await fetch(url);
scraperLogger.info`Fetched ${url} status=${result.status} ${cid}`;
return result;
} catch (error) {
scraperLogger.error`Failed to fetch ${url}: ${error} ${cid}`;
throw error;
}
}
| Level | When to Use |
|---|---|
debug | Detailed diagnostic info: selector attempts, parsing steps, cache hits |
info | Normal operations: start/complete, item counts, timing |
warning | Degraded operation: fallbacks, edge cases, retries |
error | Failures: exceptions, validation errors, unrecoverable states |
@side-quest/core: workspace:* to dependenciessrc/logger.ts with plugin name and subsystemsinitLogger() at the start of CLI toolsinitLogger() in MCP server startuplogger.info\message``${cid}~/.<plugin-name>/logs/| Item | Path |
|---|---|
| Log directory | ~/.<plugin-name>/logs/ |
| Log file | <plugin-name>.jsonl |
| Rotated files | <plugin-name>.1.jsonl, <plugin-name>.2.jsonl, etc. |
{
maxSize: 1048576, // 1 MiB before rotation
maxFiles: 5, // Keep 5 rotated files
level: "debug", // Capture all levels
extension: ".jsonl" // JSON Lines format
}
Override in createPluginLogger():
createPluginLogger({
name: "my-plugin",
subsystems: ["api"],
maxSize: 5 * 1024 * 1024, // 5 MiB
maxFiles: 10,
lowestLevel: "info", // Production: skip debug logs
});
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"category": ["my-plugin", "scraper"],
"message": ["Starting scrape operation", "abc123"]
}
tail -f ~/.my-plugin/logs/my-plugin.jsonl | jq .
grep "abc123" ~/.my-plugin/logs/my-plugin.jsonl | jq .
jq 'select(.level == "error")' ~/.my-plugin/logs/my-plugin.jsonl
jq 'select(.category[1] == "scraper")' ~/.my-plugin/logs/my-plugin.jsonl
const start = Date.now();
// ... operation ...
const durationMs = Date.now() - start;
logger.info`Operation completed in ${durationMs}ms ${cid}`;
// Include key-value pairs in message
logger.info`Processed url=${url} count=${items.length} ${cid}`;
try {
await riskyOperation();
} catch (error) {
logger.error`Failed operation=${opName} error=${error.message} ${cid}`;
throw error;
}
// MCP tool handler
async function handleTool(args: ToolArgs) {
const cid = createCorrelationId();
logger.info`Tool invoked tool=${args.name} ${cid}`;
try {
const result = await process(args, cid);
logger.info`Tool completed tool=${args.name} ${cid}`;
return result;
} catch (error) {
logger.error`Tool failed tool=${args.name} ${error} ${cid}`;
throw error;
}
}
// src/logger.ts
import { createPluginLogger } from "@side-quest/core/logging";
export const {
initLogger,
createCorrelationId,
getSubsystemLogger,
} = createPluginLogger({
name: "cinema-bandit",
subsystems: ["scraper", "pricing", "gmail"],
});
export const scraperLogger = getSubsystemLogger("scraper");
export const pricingLogger = getSubsystemLogger("pricing");
export const gmailLogger = getSubsystemLogger("gmail");
// src/scraper.ts
import { scraperLogger, createCorrelationId } from "./logger";
export async function scrapeShowtimes(cinema: string) {
const cid = createCorrelationId();
const start = Date.now();
scraperLogger.info`Starting scrape cinema=${cinema} ${cid}`;
try {
const page = await fetchPage(cinema, cid);
const shows = await parseShowtimes(page, cid);
const durationMs = Date.now() - start;
scraperLogger.info`Scrape complete shows=${shows.length} durationMs=${durationMs} ${cid}`;
return shows;
} catch (error) {
scraperLogger.error`Scrape failed cinema=${cinema} ${error} ${cid}`;
throw error;
}
}
| Issue | Solution |
|---|---|
| Logs not created | Check initLogger() called before logging |
| Empty logs | Verify await on async operations |
| Missing correlation ID | Pass cid through all function calls |
| Logs too verbose | Set level: "info" in production |
| Disk space issues | Reduce maxFiles or maxSize |
/kit:logs - View kit plugin logs@logtape/logtape - Underlying logging frameworkCLAUDE.md - Plugin-specific logging configuration