13 KiB
model, title
| model | title |
|---|---|
| accounts/fireworks/routers/kimi-k2p5-turbo | Add Proper Server Logs for MCP Server |
Plan: Add Proper Server Logs for Ask262 MCP Server
Overview
Implement structured logging with OpenTelemetry-style tracing for both stdio and HTTP MCP servers, using Pino + @opentelemetry/api + pino-roll, writing JSON Lines to file, with DuckDB for querying.
Goals
- Trace nested calls (parent-child span relationships)
- Log all operations with timing information
- Structured format for external consumption
- Zero runtime dependencies for querying (DuckDB is CLI-only)
- Minimal footprint architecture
Architecture
┌─────────────────────────────────────┐
│ Pino + @opentelemetry/api │
│ ↓ │
│ JSON Lines → ./logs/ask262.jsonl │
│ ↓ │
│ DuckDB CLI (query when needed) │
└─────────────────────────────────────┘
Configuration
| Variable | Default | Description |
|---|---|---|
ASK262_LOG_LEVEL |
debug (HTTP), info (stdio) |
File log level: trace/debug/info/warn/error |
ASK262_LOG_DIR |
./logs |
Log directory path |
ASK262_LOG_MAX_SIZE |
100 |
Rotation threshold in MB |
Console level is calculated as max('info', ASK262_LOG_LEVEL) - never shows debug, follows ASK262_LOG_LEVEL if higher.
Transport-aware defaults:
- HTTP server:
ASK262_LOG_LEVEL=debug(verbose, includes all operations) - Stdio server:
ASK262_LOG_LEVEL=info(standard, shows operations but not debug details)
File Structure
- Format: JSON Lines (one JSON object per line)
- Path:
${ASK262_LOG_DIR}/ask262.jsonl - Rotation: By system logrotate (100MB, managed by Coolify cron)
- Rotated files:
ask262.jsonl.1,ask262.jsonl.2, etc. (numbered, not timestamped) - Retention: Delete old files via Coolify cron (30 days)
- Permissions: Default (0o644)
- Sync: Synchronous writes (guaranteed durability)
Log Schema
| Field | Type | Description |
|---|---|---|
timestamp |
ISO8601 | With milliseconds |
level |
number | 10=trace, 20=debug, 30=info, 40=warn, 50=error |
trace_id |
string | Request correlation ID |
span_id |
string | Unique operation ID |
parent_span_id |
string|null | Parent operation ID |
component |
string | http-server, stdio-server, search-tool, get-section-tool, engine262-runner, reranker, graph-explorer |
operation |
string | mcp_request, vector_search, section_fetch, code_execution, embedding_generate |
duration_ms |
number|null | Operation duration |
msg |
string | Human-readable message |
client_ip |
string|null | Request IP (HTTP only) |
attributes |
object | Key=value metadata |
err |
object|null | Error details with stack trace |
Components
Trace ID Strategy
- HTTP: Generate new UUID per request (from header if available)
- Stdio: One process-scoped trace_id for entire session
Logger API
// Get component logger
const log = logger.forComponent('search-tool');
// Simple log
log.info('operation_started', { query: 'how does array.map work' });
// Timed operation (auto duration)
const op = log.start('vector_search', { query: 'how does array.map work' });
const results = await doSearch(query);
op.end({ results: results.length, provider: 'fireworks' });
// Logs on end with duration_ms automatically calculated
Dependencies
{
"pino": "^8.x",
"@opentelemetry/api": "^1.x"
// pino-roll removed - using system logrotate via Coolify cron
}
Coolify/System Requirements:
logrotateinstalled in container (standard in most Linux images)
Files to Create
1. coolify.yaml
Coolify deployment configuration with log rotation and cleanup cron jobs.
# coolify.yaml - Coolify deployment configuration
version: 1
services:
- name: ask262
cronjobs:
- name: "log-rotation"
schedule: "*/5 * * * *" # Every 5 minutes
command: "logrotate -f /app/logrotate.conf 2>/dev/null || true"
- name: "log-cleanup"
schedule: "0 0 * * *" # Daily at midnight
command: "find ${ASK262_LOG_DIR:-/app/logs} -name 'ask262.jsonl.*' -mtime +${ASK262_LOG_RETENTION_DAYS:-30} -delete 2>/dev/null || true"
2. logrotate.conf
Log rotation configuration for system logrotate.
${ASK262_LOG_DIR}/ask262.jsonl {
size 100M
rotate 10
compress
delaycompress
copytruncate
notifempty
missingok
}
3. src/lib/logger.ts
Central logging module with component binding and auto-duration operations.
Key features:
- Component-bound loggers:
const log = logger.forComponent('search-tool') - Timed operations:
const op = log.start('vector_search', attrs); op.end(resultAttrs) - Automatic trace context injection via @opentelemetry/api
- Dual transport: JSON Lines to file, pretty to console (filtered)
- Redaction of sensitive fields
Pino configuration:
const logger = pino({
level: getFileLogLevel(), // From ASK262_LOG_LEVEL env
redact: {
paths: ['FIREWORKS_API_KEY', 'api_key', 'authorization'],
remove: true
},
mixin() {
const span = trace.getSpan(context.active());
if (span) {
const ctx = span.spanContext();
return {
trace_id: ctx.traceId,
span_id: ctx.spanId,
trace_flags: ctx.traceFlags,
};
}
return {};
},
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
}, pino.multistream([
// File: JSON, all levels per ASK262_LOG_LEVEL (no rotation here - system handles it)
{
stream: pino.destination({
dest: join(ASK262_LOG_DIR, 'ask262.jsonl'),
sync: true // Synchronous writes
}),
level: getFileLogLevel()
},
// Console: Pretty, filtered to max('info', ASK262_LOG_LEVEL)
{
stream: pretty({ colorize: true }),
level: getConsoleLogLevel() // max('info', ASK262_LOG_LEVEL)
}
]));
4. src/lib/tracing.ts
OpenTelemetry trace context management using AsyncLocalStorage.
Key features:
createTraceContext(traceId?: string)- Initialize trace at entry pointwithSpan(operation, attributes, fn)- Wrap operations with automatic parent-child linking- Uses @opentelemetry/api for context propagation
- Automatic span ID generation
Files to Modify
5. src/mcp-server-http.ts
Changes:
- Import logger and tracing modules
- Create trace context at request entry point
- Extract client IP from request headers
- Add startup/shutdown logs (minimal)
- Add logging around tool invocations
// Startup
log.info('server_started', { port: ASK262_PORT, transport: 'http' });
// Per request
const traceId = req.headers['x-request-id'] || crypto.randomUUID();
await withSpan('mcp_request', { tool: toolName, client_ip: clientIp }, async () => {
const log = logger.forComponent('http-server');
const op = log.start('handle_request', { tool: toolName });
const result = await toolHandler(args);
op.end({ status: 'success', result_count: result.length });
return result;
});
// Shutdown
log.info('server_shutting_down', { signal: 'SIGTERM' });
6. src/mcp-server-stdio.ts
Changes:
- Import logger and tracing modules
- Create process-scoped trace_id on startup
- Add startup/shutdown logs (minimal)
- Add logging matching HTTP server pattern (no IP)
// Startup - one trace_id for entire session
const sessionTraceId = crypto.randomUUID();
log.info('server_started', { transport: 'stdio', trace_id: sessionTraceId });
// Per message - uses same trace_id via AsyncLocalStorage
await withSpan('mcp_request', { tool: toolName }, async () => {
// Same pattern as HTTP
});
7. src/agent-tools/searchSpecSections.ts
Changes:
- Import component logger
- Add operation spans for:
- Vector search query
- Embedding generation (if cached vs fresh)
- Reranking (if enabled)
- Result formatting
- Log query parameters, result counts, timing
- Log errors with full context
8. src/agent-tools/getSectionContent.ts
Changes:
- Add logging for section lookup
- Log section IDs found/not found
- Log content size
- Log timing for LanceDB queries
9. src/agent-tools/evaluateInEngine262.ts + runner
Changes:
- Add parent-child span tracking across process boundary
- Pass trace context to runner via env or message
- Log code execution request
- Runner logs as child spans
- Log execution time
- Log errors with stack traces
10. src/agent-tools/graphExplorer.ts
Changes:
- Log graph traversal operations
- Log node/edge counts
- Log query timing
11. src/agent-tools/reranker.ts
Changes:
- Log reranking requests
- Log API success/failure
- Log timing
12. src/lib/embeddings-factory.ts
Changes:
- Log provider selection
- Log embedding generation batches
- Log errors with provider context
13. src/lib/fireworks-embeddings.ts
Changes:
- Log rate limit retries
- Log API timing
- Log batch sizes
14. .env.example
Add:
# Logging configuration
# HTTP server defaults to 'debug', stdio defaults to 'info'
ASK262_LOG_LEVEL=info # trace, debug, info, warn, error
ASK262_LOG_DIR=./logs # Log file directory
ASK262_LOG_MAX_SIZE=100 # Max file size in MB before rotation
ASK262_LOG_RETENTION_DAYS=30 # Days to keep rotated logs (0=keep forever)
15. .gitignore
Add:
# Logs
/logs/
*.log
*.jsonl
DuckDB Query Examples
Installation
brew install duckdb # macOS
# or download from https://duckdb.org/
Common Queries
-- View all logs for a trace
SELECT timestamp, component, operation, duration_ms, msg
FROM 'logs/ask262.jsonl'
WHERE trace_id = '4bf92f3577b34da6a3ce929d0e0e4736'
ORDER BY time;
-- Find slow operations
SELECT component, operation,
AVG(duration_ms) as avg_ms,
MAX(duration_ms) as max_ms,
COUNT(*) as count
FROM 'logs/ask262.jsonl'
WHERE duration_ms IS NOT NULL
GROUP BY component, operation
ORDER BY avg_ms DESC;
-- Error analysis
SELECT component, operation, COUNT(*) as errors
FROM 'logs/ask262.jsonl'
WHERE level >= 40
GROUP BY component, operation;
-- Request volume over time
SELECT date_trunc('hour', timestamp::TIMESTAMP) as hour,
COUNT(*) as requests
FROM 'logs/ask262.jsonl'
WHERE component = 'http-server' AND operation = 'mcp_request'
GROUP BY hour
ORDER BY hour;
-- Trace duration (time from first to last span)
SELECT trace_id,
MIN(timestamp) as start_time,
MAX(timestamp) as end_time,
MAX(timestamp)::TIMESTAMP - MIN(timestamp)::TIMESTAMP as total_duration
FROM 'logs/ask262.jsonl'
WHERE trace_id IS NOT NULL
GROUP BY trace_id
ORDER BY total_duration DESC
LIMIT 10;
Implementation Order
- Coolify config (
coolify.yaml,logrotate.conf) - Deployment and rotation setup - Core logger (
src/lib/logger.ts) - Pino setup, transports, redaction - Tracing module (
src/lib/tracing.ts) - OTel context, span management - HTTP server (
src/mcp-server-http.ts) - Entry point, test end-to-end - Stdio server (
src/mcp-server-stdio.ts) - Same pattern as HTTP - Agent tools - searchSpecSections, getSectionContent, evaluateInEngine262, graphExplorer, reranker
- Library files - embeddings-factory, fireworks-embeddings
- Documentation - .env.example, .gitignore
- Testing - Verify all components log correctly
Testing Plan
- Unit tests: Verify logger creates correct JSON structure
- Integration tests:
- Make MCP requests, verify logs created
- Check trace_id consistency across nested calls
- Verify span parent-child relationships
- Manual verification:
- Query logs with DuckDB
- Verify timing calculations
- Check error logging includes stack traces
- Test log rotation at 100MB
- Verify console pretty output
Verification Steps
After implementation:
- Install dependencies:
bun add pino @opentelemetry/api pino-roll - Run HTTP server:
bun run ask262-http - Check logs directory created:
ls -la logs/ - Make test request via MCP Inspector or curl
- Query with DuckDB:
duckdb -c "SELECT * FROM 'logs/ask262.jsonl' LIMIT 5" - Check console output is pretty-printed
- Run stdio server:
bun run ask262-stdio - Send MCP message via stdin
- Verify both servers produce consistent log format
- Test rotation: Send many requests, verify rotation at 100MB
- Test error logging: Trigger error, verify stack trace in logs
Error Handling
Log directory unwritable: Server crashes on startup with clear error message.
Log rotation fails: Continue with current file, log warning (don't crash on rotation failure).
Disk full: Synchronous writes will fail, error propagated up and logged to console.
Success Criteria
- All tool operations logged with timing
- Nested calls have trace_id and parent_span_id relationships
- HTTP server logs include client IP
- Stdio server has process-scoped trace_id
- Console shows pretty output, filtered to appropriate level
- File has JSON Lines format
- DuckDB can query logs without errors
- Log rotation works at 100MB with timestamp suffix
- Error logs include full context and stack traces
- API keys redacted from all logs
- No impact on MCP protocol communication
- Server crashes if log directory unwritable
- Minimal startup/shutdown logs present