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.
/plugin marketplace add nathanvale/side-quest-marketplace/plugin install firecrawl@side-quest-marketplaceThis 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 configurationBuild robust backtesting systems for trading strategies with proper handling of look-ahead bias, survivorship bias, and transaction costs. Use when developing trading algorithms, validating strategies, or building backtesting infrastructure.