🔒 Security First
Automatic PII redaction and context sanitization prevents sensitive data leakage in logs.
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.
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.
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})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:
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:
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.
Track requests across your entire stack using correlation IDs:
locals.correlationIdExample 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 ascAutomatic 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 }Debug-level information for detailed troubleshooting.
await logger.debug('Processing data', { step: 1, itemCount: 42, correlationId})Informational messages about normal application flow.
await logger.info('User logged in', { userId: 'user123', method: 'oauth', correlationId})Warning conditions that may require attention.
await logger.warn('API rate limit approaching', { current: 450, limit: 500, correlationId})Error conditions requiring immediate attention.
await logger.error('Database query failed', { error: err.message, query: 'SELECT...', correlationId})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 lifecycleLogs API request completion with automatic duration calculation.
Signature:
logger.apiComplete( requestContext: LogContext & { startTime: number }, status: number): Promise<void>Features:
Example:
const ctx = logger.apiRequest('/api/posts', 'GET', 'user123', correlationId)// ... process request ...await logger.apiComplete(ctx, 200)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 }}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() }}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' })}// 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})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 desc// ✅ Goodconst correlationId = locals.correlationIdawait logger.info('User action', { userId, action: 'login', correlationId})await logger.flush()// ❌ Badconsole.log('User logged in') // No correlation, not in Axiomawait logger.info('Payment', { creditCard: '1234...' }) // Sensitive data// return response without flush() ❌ Logs may be lostCheck environment variables:
echo $AXIOM_TOKENecho $AXIOM_DATASETLook for initialization message:
Verify flush is called:
await logger.flush() // Required in serverless!Check Axiom dashboard for ingestion errors
correlationMiddleware is in the middleware sequencesrc/middleware.ts generates correlation IDslocals.correlationId is available in routes# Required for Axiom integrationAXIOM_TOKEN=xaat-your-token-hereAXIOM_DATASET=astro-basics
# Optional: Organization ID for team accountsAXIOM_ORG_ID=your-org-idEnsure src/env.d.ts includes:
declare namespace App { interface Locals { correlationId: string userId?: string // ... other locals }}Axiom Setup
Architecture Details
Usage Examples
Implementation
The logging system provides:
Key Takeaway: Always use logger instead of console.*, include correlation IDs, and flush before exiting serverless functions!