Guide for implementing structured logging in SideQuest plugins using @sidequest/core. Use when adding logging to new plugins, debugging existing plugins, or setting up log analysis.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Implement structured, JSONL logging in SideQuest plugins using the @sidequest/core/logging factory.
// package.json
{
"dependencies": {
"@sidequest/core": "workspace:*"
}
}
// src/logger.ts
import { createPluginLogger } from "@sidequest/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 |
@sidequest/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 "@sidequest/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