| name | logging |
| description | 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. |
Plugin Logging Guide
Implement structured, JSONL logging in SideQuest plugins using the @side-quest/core/logging factory.
When to Use This Skill
- Adding logging to a new plugin
- Migrating a plugin to structured logging
- Debugging plugin behavior via logs
- Setting up log rotation or analysis
- Questions about logging best practices
Quick Start
1. Add Dependency
{
"dependencies": {
"@side-quest/core": "workspace:*"
}
}
2. Create Logger Module
import { createPluginLogger } from "@side-quest/core/logging";
export const {
initLogger,
createCorrelationId,
getSubsystemLogger,
logDir,
logFile,
} = createPluginLogger({
name: "my-plugin",
subsystems: ["scraper", "api", "cache"],
});
export const scraperLogger = getSubsystemLogger("scraper");
export const apiLogger = getSubsystemLogger("api");
export const cacheLogger = getSubsystemLogger("cache");
3. Initialize at Entry Points
import { initLogger, createCorrelationId, scraperLogger } from "./logger";
await initLogger();
const cid = createCorrelationId();
scraperLogger.info`Starting scrape operation ${cid}`;
4. Use Correlation IDs
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;
}
}
Log Levels
| 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 |
Checklist: Adding Logging to a Plugin
Setup Phase
Entry Points
Logging Calls
Error Handling
Testing
File Locations
| Item | Path |
|---|
| Log directory | ~/.<plugin-name>/logs/ |
| Log file | <plugin-name>.jsonl |
| Rotated files | <plugin-name>.1.jsonl, <plugin-name>.2.jsonl, etc. |
Configuration Defaults
{
maxSize: 1048576,
maxFiles: 5,
level: "debug",
extension: ".jsonl"
}
Override in createPluginLogger():
createPluginLogger({
name: "my-plugin",
subsystems: ["api"],
maxSize: 5 * 1024 * 1024,
maxFiles: 10,
lowestLevel: "info",
});
Log Entry Format
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"category": ["my-plugin", "scraper"],
"message": ["Starting scrape operation", "abc123"]
}
Viewing Logs
Tail Recent Logs
tail -f ~/.my-plugin/logs/my-plugin.jsonl | jq .
Filter by Correlation ID
grep "abc123" ~/.my-plugin/logs/my-plugin.jsonl | jq .
Filter by Level
jq 'select(.level == "error")' ~/.my-plugin/logs/my-plugin.jsonl
Filter by Subsystem
jq 'select(.category[1] == "scraper")' ~/.my-plugin/logs/my-plugin.jsonl
Common Patterns
Timing Operations
const start = Date.now();
const durationMs = Date.now() - start;
logger.info`Operation completed in ${durationMs}ms ${cid}`;
Structured Context
logger.info`Processed url=${url} count=${items.length} ${cid}`;
Error Context
try {
await riskyOperation();
} catch (error) {
logger.error`Failed operation=${opName} error=${error.message} ${cid}`;
throw error;
}
Request Tracing
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;
}
}
Example: Full Plugin Setup
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");
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;
}
}
Troubleshooting
| 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 |
Related Resources
/kit:logs - View kit plugin logs
@logtape/logtape - Underlying logging framework
CLAUDE.md - Plugin-specific logging configuration