The astro-basics project includes a sophisticated production-ready logging system that provides structured logging, distributed tracing, performance monitoring, and persistent log storage through Axiom integration.
Overview
Section titled “Overview”The logging system is designed for modern serverless architectures with automatic security features, dual-mode output (development vs production), and comprehensive observability capabilities.
🔒 Security First
Automatic PII redaction and context sanitization prevents sensitive data leakage in logs.
📊 Distributed Tracing
Correlation IDs track requests across services for end-to-end debugging.
⚡ Performance Monitoring
Built-in request duration tracking with automatic slow request detection.
☁️ Axiom Integration
Persistent log storage with powerful querying and real-time alerting.
Quick Start
Section titled “Quick Start”Basic Usage
Section titled “Basic Usage”import { logger } from '#utils/logger'
// Info level - general application flowawait logger.info('User logged in', { userId: 'user123' })
// Warning - potential issuesawait logger.warn('API rate limit approaching', { current: 450, limit: 500 })
// Error - failures requiring attentionawait logger.error('Database connection failed', { error: err.message })import type { APIRoute } from 'astro'import { logger } from '#utils/logger'
export const GET: APIRoute = async ({ locals }) => { const ctx = logger.apiRequest( '/api/users', 'GET', locals.userId, locals.correlationId )
try { const data = await fetchUsers() await logger.apiComplete(ctx, 200) return new Response(JSON.stringify(data)) } catch (error) { await logger.error('Failed to fetch users', { ...ctx, error: error.message }) return new Response(JSON.stringify({ error: 'Server error' }), { status: 500 }) } finally { await logger.flush() // CRITICAL for serverless! }}// Include rich context for better debuggingawait logger.info('Order processed', { orderId: 'order-789', userId: 'user123', total: 99.99, items: 3, correlationId: locals.correlationId})
// Sensitive data is automatically redactedawait logger.info('Payment processed', { userId: 'user123', token: 'secret-token', // Becomes [REDACTED] amount: 49.99})Core Features
Section titled “Core Features”1. Dual-Mode Logging
Section titled “1. Dual-Mode Logging”The logger automatically adapts its output based on the environment:
Human-friendly console output with emojis:
ℹ️ [INFO] User authenticated { userId: 'user123', method: 'oauth' }⚠️ [WARN] Slow request detected { endpoint: '/api/data', duration: 2500 }❌ [ERROR] Database query failed { error: 'Connection timeout' }Benefits:
- Quick visual scanning
- Color-coded severity levels
- Full context display
- Stack traces for errors
Structured JSON for machine parsing:
{ "timestamp": "2025-10-11T12:34:56.789Z", "level": "info", "message": "User authenticated", "context": { "userId": "user123", "endpoint": "/api/auth", "method": "POST" }}Benefits:
- Log aggregation compatible
- Easy parsing by monitoring tools
- Consistent format
- Filtered output (warn/error only to console)
2. Security & Sanitization
Section titled “2. Security & Sanitization”Protected Fields:
token→[REDACTED]password→[REDACTED]secret→[REDACTED]clerkToken→[REDACTED]
Production Context Filtering:
In production, only essential debugging fields are retained:
userId(user correlation)endpoint(API tracking)method(HTTP method)
All other fields are stripped to minimize data exposure and reduce log size.
3. Distributed Tracing
Section titled “3. Distributed Tracing”Track requests across your entire stack using correlation IDs:
- Middleware generates a unique correlation ID for each request
- API routes receive the correlation ID via
locals.correlationId - Service layer propagates the ID through function calls
- All logs tagged with the same correlation ID
- Query Axiom to trace the complete request lifecycle
Example Flow:
// Middleware (automatic)context.locals.correlationId = logger.createCorrelationId()
// API Routeconst ctx = logger.apiRequest('/api/orders', 'GET', userId, locals.correlationId)
// Service Layerasync function processOrder(orderId: string, correlationId: string) { await logger.info('Processing order', { orderId, correlationId })
// Pass to database layer const order = await db.getOrder(orderId, { correlationId })
// Pass to external services await paymentService.charge(order, { correlationId })
return order}
// Query in Axiom['astro-basics']| where correlationId == "550e8400-e29b-41d4-a716-446655440000"| order by _time asc4. Performance Monitoring
Section titled “4. Performance Monitoring”Automatic request timing and slow request detection:
// Start trackingconst ctx = logger.apiRequest('/api/data', 'GET', userId, correlationId)
// Your business logicawait processData()
// Automatic duration calculationawait logger.apiComplete(ctx, 200)// Logs: { requestDuration: 1250, endpoint: '/api/data', status: 200 }
// If duration > 2000ms, automatically logs warning:// ⚠️ Slow API request detected { requestDuration: 2500, threshold: 2000 }API Reference
Section titled “API Reference”Log Levels
Section titled “Log Levels”Debug-level information for detailed troubleshooting.
- Development: Shown in console
- Production: Filtered from console, sent to Axiom
await logger.debug('Processing data', { step: 1, itemCount: 42, correlationId})Informational messages about normal application flow.
- Development: Shown with ℹ️ icon
- Production: Filtered from console, sent to Axiom
await logger.info('User logged in', { userId: 'user123', method: 'oauth', correlationId})Warning conditions that may require attention.
- Development: Shown with ⚠️ icon
- Production: Shown in console and sent to Axiom
await logger.warn('API rate limit approaching', { current: 450, limit: 500, correlationId})Error conditions requiring immediate attention.
- Development: Shown with ❌ icon
- Production: Shown in console and sent to Axiom
await logger.error('Database query failed', { error: err.message, query: 'SELECT...', correlationId})API Helpers
Section titled “API Helpers”apiRequest()
Section titled “apiRequest()”Creates enriched context for API request tracking with performance monitoring.
Signature:
logger.apiRequest( endpoint: string, method: string, userId?: string, correlationId?: string): LogContext & { startTime: number }Example:
const ctx = logger.apiRequest('/api/posts', 'GET', 'user123', correlationId)// Use ctx throughout the request lifecycleapiComplete()
Section titled “apiComplete()”Logs API request completion with automatic duration calculation.
Signature:
logger.apiComplete( requestContext: LogContext & { startTime: number }, status: number): Promise<void>Features:
- Calculates request duration automatically
- Warns if duration exceeds 2000ms (slow request threshold)
- Logs completion with full context and status code
Example:
const ctx = logger.apiRequest('/api/posts', 'GET', 'user123', correlationId)// ... process request ...await logger.apiComplete(ctx, 200)flush()
Section titled “flush()”Flushes pending logs to Axiom. CRITICAL for serverless environments.
await logger.flush()Correct Pattern:
export const POST: APIRoute = async ({ request, locals }) => { const ctx = logger.apiRequest('/api/endpoint', 'POST', locals.userId, locals.correlationId)
try { // Business logic await logger.apiComplete(ctx, 200) return response } catch (error) { await logger.error('Request failed', { ...ctx, error: error.message }) return errorResponse } finally { await logger.flush() // ALWAYS flush before exit }}Advanced Patterns
Section titled “Advanced Patterns”Error Handling
Section titled “Error Handling”export const POST: APIRoute = async ({ request, locals }) => { const correlationId = locals.correlationId || logger.createCorrelationId() const ctx = logger.apiRequest('/api/users', 'POST', locals.userId, correlationId)
try { const body = await request.json()
// Validation logging if (!body.email) { await logger.warn('Invalid request - missing email', { ...ctx, body }) await logger.apiComplete(ctx, 400) await logger.flush() return new Response(JSON.stringify({ error: 'Missing email' }), { status: 400 }) }
// Success path const user = await createUser(body) await logger.info('User created', { ...ctx, userId: user.id }) await logger.apiComplete(ctx, 201)
return new Response(JSON.stringify(user), { status: 201 }) } catch (error) { // Error logging with full context await logger.error('User creation failed', { ...ctx, error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }) await logger.apiComplete(ctx, 500)
return new Response(JSON.stringify({ error: 'Server error' }), { status: 500 }) } finally { await logger.flush() }}Nested Operations
Section titled “Nested Operations”async function parentOperation(correlationId: string) { await logger.info('Parent operation started', { correlationId, operation: 'parent' })
// Pass correlation through call stack const result1 = await childOperation1(correlationId) const result2 = await childOperation2(correlationId)
await logger.info('Parent operation completed', { correlationId, operation: 'parent', results: [result1, result2] })}
async function childOperation1(correlationId: string) { await logger.debug('Child operation 1 started', { correlationId, operation: 'child1' })
// Work...
await logger.debug('Child operation 1 completed', { correlationId, operation: 'child1' })}Cross-Service Integration
Section titled “Cross-Service Integration”// Clerk operation correlationconst clerkUser = await clerkClient.users.getUser(userId)await logger.info('Clerk user fetched', { correlationId, clerkTraceId: clerkUser.metadata?.traceId, userId})
// Supabase query correlationconst { data, error } = await supabase.from('users').select('*')await logger.info('Supabase query executed', { correlationId, supabaseQueryId: error?.hint, // If available rows: data?.length})Axiom Queries
Section titled “Axiom Queries”Query your logs in Axiom using APL (Axiom Processing Language):
['astro-basics']| where correlationId == "550e8400-e29b-41d4-a716-446655440000"| order by _time asc['astro-basics']| where level == "error"| where _time > ago(24h)| summarize count() by endpoint, error| order by count_ desc['astro-basics']| where requestDuration > 2000| where _time > ago(1h)| order by requestDuration desc| project _time, endpoint, requestDuration, correlationId['astro-basics']| where userId == "user_2ABC123"| where _time > ago(7d)| order by _time desc['astro-basics']| where endpoint != ""| where requestDuration > 0| summarize avg_duration = avg(requestDuration), p95_duration = percentile(requestDuration, 95), request_count = count() by endpoint| order by avg_duration descBest Practices
Section titled “Best Practices”- Use correlation IDs from middleware/locals
- Include relevant context in every log
- Always flush in serverless functions
- Use apiRequest/apiComplete for automatic timing
- Log errors with full context including stack traces
- Pass correlation IDs through service layers
- Use appropriate log levels for better filtering
// ✅ Goodconst correlationId = locals.correlationIdawait logger.info('User action', { userId, action: 'login', correlationId})await logger.flush()❌ DON’T
Section titled “❌ DON’T”- Don’t use console. directly* - Use logger instead
- Don’t log sensitive data intentionally - Even with auto-redaction
- Don’t forget to flush in serverless - Logs may be lost
- Don’t skip correlation IDs - Can’t trace requests
- Don’t use string interpolation - Use context objects
// ❌ Badconsole.log('User logged in') // No correlation, not in Axiomawait logger.info('Payment', { creditCard: '1234...' }) // Sensitive data// return response without flush() ❌ Logs may be lostTroubleshooting
Section titled “Troubleshooting”Logs Not Appearing in Axiom
Section titled “Logs Not Appearing in Axiom”-
Check environment variables:
Terminal window echo $AXIOM_TOKENecho $AXIOM_DATASET -
Look for initialization message:
- ✅ “Axiom logging initialized” (success)
- ⚠️ “Axiom not configured” (missing credentials)
-
Verify flush is called:
await logger.flush() // Required in serverless! -
Check Axiom dashboard for ingestion errors
Missing Correlation IDs
Section titled “Missing Correlation IDs”- Ensure
correlationMiddlewareis in the middleware sequence - Verify
src/middleware.tsgenerates correlation IDs - Check
locals.correlationIdis available in routes
Performance Issues
Section titled “Performance Issues”- Axiom logging is fire-and-forget (non-blocking)
- Flush typically takes less than 100ms
- Check Axiom service status if seeing delays
Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”# Required for Axiom integrationAXIOM_TOKEN=xaat-your-token-hereAXIOM_DATASET=astro-basics
# Optional: Organization ID for team accountsAXIOM_ORG_ID=your-org-idTypeScript Configuration
Section titled “TypeScript Configuration”Ensure src/env.d.ts includes:
declare namespace App { interface Locals { correlationId: string userId?: string // ... other locals }}Related Resources
Section titled “Related Resources”Axiom Setup
Architecture Details
Usage Examples
Implementation
Summary
Section titled “Summary”The logging system provides:
- 🔒 Security-first design with automatic PII redaction
- 📊 Distributed tracing via correlation IDs
- ⚡ Performance monitoring with automatic slow request detection
- ☁️ Axiom integration for persistent, queryable logs
- 🎯 Dual-mode output optimized for development and production
- 🛡️ Serverless-ready with critical flush mechanism
Key Takeaway: Always use logger instead of console.*, include correlation IDs, and flush before exiting serverless functions!