Logging
For errors and logs, the frontend and API have dedicated logging utils. This gives us structured, level-based logging that can be extended in the future for JSON output or external log management systems.
API Logger
The API uses a structured logger designed for Cloudflare Workers. It outputs colorized logs in development and can be extended for JSON output in production, the code for this is in packages/api/src/lib/logger.ts.
Basic Usage
import { logger, createLogger } from '@/lib/logger';
// Use the default logger
logger.info('Something interesting happened');
logger.warn('This might be a problem, just a heads up!');
logger.error("Something went wrong, that's not ideal", error);
// Create a contextual logger
const log = createLogger({ operation: 'uploadImage', owner: 'alicia' });
log.info('Starting upload');
log.error('Upload failed', error);Log Levels
Four levels are available, in order of severity:
| Level | Method | Use for |
|---|---|---|
debug | logger.debug() | Verbose debugging info |
info | logger.info() | General operational events |
warn | logger.warn() | Potential issues, recoverable errors |
error | logger.error() | Failures that need attention |
Adding Context
You can pass additional context as an object to any log method:
logger.info('Image uploaded', { imageId: 'abc123', size: 1024 });
logger.error('Failed to process', error, { imageId: 'abc123' });Child Loggers
Create child loggers that inherit and extend context:
const requestLogger = createLogger({ requestId: 'abc123' });
const imageLogger = requestLogger.child({ imageId: 'img456' });
imageLogger.info('Processing image');
// Logs with both requestId and imageId in contextIn Route Handlers
The logging middleware automatically creates a logger with request context. Use getLogger() to access it:
import { getLogger } from '@/middleware/logging';
app.get('/images', (c) => {
const logger = getLogger(c);
logger.info('Fetching images');
// ...
});Measuring Slow Operations
Use measureTime() to log warnings for slow operations:
import { measureTime } from '@/lib/logger';
const result = await measureTime(() => expensiveOperation(), logger, 'expensiveOperation');
// Logs a warning if operation takes longer than SLOW_REQUEST_THRESHOLD_MS (1000ms)Frontend Logger
The frontend logger works in both browser and server (SSR) contexts. In the browser, it uses collapsible console groups with styled badges. On the server, it uses standard console methods.
Source: packages/frontend/src/lib/utils/logger.ts
Basic Usage
import { logger } from '$lib/utils/logger';
logger.debug('Some detailed info for debugging');
logger.info('User clicked an important button', { buttonId: 'submit' });
logger.warn('API returned unexpected format');
logger.error('Failed to fetch data', error, { endpoint: '/api/images' });Log Levels
The frontend uses the same four levels as the API. The default level depends on environment:
- Development (
dev: true):debug- shows all logs - Production:
warn- only warnings and errors
You can override this with the PUBLIC_LOG_LEVEL environment variable in your .env:
PUBLIC_LOG_LEVEL=infoControlling Log Level at Runtime
import { logger } from '$lib/utils/logger';
logger.getLevel(); // Returns current level
logger.setLevel('debug'); // Change level
logger.refreshLevel(); // Reset to environment defaultWhen to Log
Do Log
- Request lifecycle events (handled by middleware)
- Operation start/completion for async tasks
- Errors and exceptions with full context
- Slow operations and performance issues
- Security-relevant events (auth failures, permission denials)
- State changes that affect data
Don't Log
- Sensitive data (passwords, tokens, PII)
- High-frequency events in hot paths
- Expected/normal errors (e.g., 404s for missing resources)
- Redundant information already in request logs
Example Patterns
// Good: contextual, actionable
logger.info('Image variant generated', { imageId, variant, duration });
logger.error('Failed to connect to R2', error, { bucket, operation: 'put' });
// Bad: vague, missing context
logger.info('done');
logger.error('error');Viewing Logs
Local Development
Logs appear directly in your terminal when running pnpm dev. The API logs are colorized for easy reading:
10:15:31 INFO Incoming request [req=abc123de GET /v1/images]
10:15:31 INFO Request completed [req=abc123de GET /v1/images status=200 45ms]Errors show the error name, message, and stack trace:
10:15:32 ERROR Upload failed [req=abc123de POST /v1/upload]
^ Error: Network timeout
at uploadImage (file:///...index.js:1234:56)
at async handleRequest (file:///...index.js:789:12)Frontend logs appear in your browser's developer console (F12 or Cmd+Option+I). They're grouped and styled for easy scanning.
Production - Structured JSON Logs
In production, both API and frontend (SSR) automatically output structured JSON logs for parsing by log aggregators:
{
"level": "error",
"message": "Upload failed",
"timestamp": "2026-01-07T12:34:56.789Z",
"requestId": "abc123de",
"path": "/v1/upload",
"method": "POST",
"statusCode": 500,
"duration": 145,
"error": {
"name": "NetworkError",
"message": "Connection timeout",
"stack": "..."
}
}Real-Time Logs (Wrangler Tail)
Stream production logs in real-time using Wrangler:
# API Worker logs
npx wrangler tail --config packages/api/wrangler.production.toml
# Only errors
npx wrangler tail --status error --config packages/api/wrangler.production.toml
# Specific HTTP methods
npx wrangler tail --method POST --config packages/api/wrangler.production.toml
# JSON format for parsing
npx wrangler tail --format json --config packages/api/wrangler.production.tomlCloudflare Dashboard
View logs in the Cloudflare dashboard with query capabilities:
- Go to dash.cloudflare.com
- Navigate to Workers & Pages > your worker
- Click the Logs tab
- Use the query builder to filter by:
- Log level (debug, info, warn, error)
- Time range (last hour, day, week)
- Custom filters (requestId, statusCode, etc.)
Workers Logs Features:
- 30-day retention included (no extra cost with Workers Paid)
- Query with SQL-like syntax
- Export results to CSV
- 10% sampling by default (adjustable in
wrangler.toml)
Long-Term Storage (Logpush to R2)
For compliance, debugging, and historical analysis, Pixelflare can push logs to R2 for unlimited retention:
Setup:
- Create R2 API tokens at dash.cloudflare.com → R2 → Manage R2 API Tokens
- Add to your
.env:bashR2_ACCESS_KEY_ID='your_access_key_id' R2_SECRET_ACCESS_KEY='your_secret_access_key' - Run deployment script - Logpush is configured automatically
Querying R2 Logs:
# List log files
npx wrangler r2 object list your-bucket-name --prefix logs/workers
# Download a specific day's logs
npx wrangler r2 object get your-bucket-name/logs/workers/2026/01/07/file.log
# Query with jq
npx wrangler r2 object get your-bucket-name/logs/workers/2026/01/07/file.log | \
jq '.[] | select(.level == "error")'Cost: ~$0.05 per million log entries + R2 storage (pennies/month)
Frontend Production Logs
Frontend logs in production:
- Browser logs: Only warnings and errors (configurable via
PUBLIC_LOG_LEVEL) - SSR logs: Structured JSON in Cloudflare Pages logs
- View: Dashboard → Workers & Pages → Pages → your project → Functions
Architecture
API Logging Flow
The loggingMiddleware runs on every request:
- Extracts or generates a request ID
- Creates a logger with request context (path, method, requestId)
- Logs the incoming request
- Runs the handler
- Logs completion with status and duration
- Warns if the request was slow
Other middleware extends this:
error-handler.ts- logs errors with appropriate levelsaudit-logger.ts- logs mutations for audit trail
Request Tracing
Every request gets a unique ID (from the X-Request-ID header or auto-generated). This ID appears in all logs for that request, making it easy to trace a request through the system. The ID is also returned in the response headers.