Logging
This content is for the 0.6.2 version. Switch to the latest version for up-to-date documentation.
Logging
Overview
Raindrop provides a structured logging system that automatically captures application behavior and enables custom event tracking. Every component (services, actors, observers, tasks) receives a logger instance through this.env.logger
that records events with timestamps, trace correlation, and structured field data.
The system combines automatic resource interaction logging with manual application logging. Automatic logging captures all database queries, HTTP requests, AI model calls, and storage operations without requiring instrumentation. Manual logging allows you to add business events, debugging information, and custom metrics.
Key benefits include zero-configuration setup, automatic resource interaction capture, hierarchical contextual logging with shared fields, real-time log streaming via CLI, and historical query capabilities with time-based filtering.
Prerequisites
- Active Raindrop application with services, actors, or other components
- Basic understanding of application debugging and monitoring concepts
- Familiarity with TypeScript async patterns and error handling
- Knowledge of log levels and when to use different severity levels
Creating/Getting Started
Logging requires no configuration - every Raindrop component automatically receives a logger instance. Access the logger through this.env.logger
in services, actors, observers, and tasks:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { // Logger is automatically available through this.env.logger this.env.logger.info("Request received", { method: request.method, url: request.url, userAgent: request.headers.get('User-Agent') });
return new Response("Hello World"); }}
Log Levels Available:
// Debug - detailed information for developmentthis.env.logger.debug("Processing user input", { inputKeys: Object.keys(data) });
// Info - normal application eventsthis.env.logger.info("User session started", { userId: user.id });
// Warn - unusual conditions that don't break functionalitythis.env.logger.warn("API response slow", { responseTime: 3000 });
// Error - failures requiring attentionthis.env.logger.error("Database connection failed", { attempt: retryCount });
Accessing/Basic Usage
Use the logger throughout your application components to track events and debug issues:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
// Log request start this.env.logger.info("Processing request", { path: url.pathname, method: request.method });
try { if (url.pathname === '/users') { const users = await this.getUserList();
// Log successful operation this.env.logger.info("Retrieved user list", { count: users.length, duration: Date.now() });
return Response.json(users); }
return new Response("Not found", { status: 404 });
} catch (error) { // Log errors with context this.env.logger.error(error, { operation: "get_users", path: url.pathname });
return new Response("Internal error", { status: 500 }); } }
private async getUserList(): Promise<any[]> { this.env.logger.debug("Querying database for users"); // Database query logic here return []; }}
Core Concepts
The Raindrop logging system provides structured logging with automatic context capture and multiple severity levels for different types of events.
Structured Logging: All log entries accept a LogFields
object containing key-value data alongside the text message. This structured approach enables powerful filtering, searching, and analysis compared to plain text logging.
Contextual Hierarchy: Create child logger instances using with()
that automatically include common fields in all subsequent log entries. This creates a hierarchy where parent context flows down to child loggers.
Automatic Correlation: Every log entry receives automatic timestamps and trace correlation IDs that group related operations together. This enables you to follow request flows across distributed components.
Zero-Configuration Resource Logging: Raindrop automatically captures and logs all resource interactions (SQL queries, object storage, AI models, queues) without requiring manual instrumentation or configuration.
Logger Interface
The Logger
interface provides structured logging capabilities with contextual hierarchy support:
interface Logger { // Context creation methods with(fields?: LogFields): Logger; withError(error: unknown, field?: LogFields): Logger;
// Standard logging methods debug(message: string, fields?: LogFields): void; info(message: string, fields?: LogFields): void; warn(message: string, fields?: LogFields): void; error(message: string, fields?: LogFields): void; log(message: string, fields?: LogFields): void; // defaults to INFO level
// Advanced logging methods logAtLevel(level: LogLevel, message: string, fields?: LogFields): void; message(message: string, fields?: LogFields): LogMessage; messageAtLevel(level: LogLevel, message: string, fields?: LogFields): LogMessage;}
// Log severity levelsenum LogLevel { DEBUG = 'DEBUG', // Detailed debugging information INFO = 'INFO', // Normal application events WARN = 'WARN', // Warning conditions ERROR = 'ERROR' // Error conditions}
// Structured data type for log fieldstype LogFields = Record<string, number | string | boolean | object | undefined | null | Error | LogFields>;
// Log message structureinterface LogMessage { level: LogLevel; message: string; fields: LogFields;}
Log Levels and Usage
Different log levels serve different purposes in application monitoring and debugging.
debug()
Use for detailed information during development. These logs are typically disabled in production:
this.env.logger.debug("Validating user input", { inputKeys: Object.keys(formData), validationRules: activeRules});
this.env.logger.debug("Cache hit for key", { key: cacheKey, ttl: remainingTTL});
Parameters:
message
:string
- Descriptive message for the debug eventfields
:LogFields
- Optional structured data for context
info()
Use for normal application events that should be recorded:
this.env.logger.info("User session started", { userId: user.id, loginMethod: "oauth", ipAddress: request.headers.get('cf-connecting-ip')});
this.env.logger.info("Order completed", { orderId: order.id, totalAmount: order.total, processingTime: Date.now() - startTime});
Parameters:
message
:string
- Clear description of the normal eventfields
:LogFields
- Optional context data
warn()
Use for unusual conditions that don’t break functionality:
this.env.logger.warn("API response slow", { endpoint: "/api/users", responseTime: 5000, threshold: 2000});
this.env.logger.warn("Approaching rate limit", { currentRequests: 95, limit: 100, window: "1 minute"});
Parameters:
message
:string
- Description of the unusual conditionfields
:LogFields
- Optional context about the condition
error()
Use for failures that need immediate attention:
// Log error messagesthis.env.logger.error("Database connection failed", { database: "users", attempt: retryCount, lastError: connectionError.message});
// Log Error objects (captures stack traces)try { await processPayment(order);} catch (error) { this.env.logger.error(error, { orderId: order.id, paymentMethod: order.paymentMethod, stage: "charge_processing" }); throw error;}
Parameters:
message
:string
- Error descriptionfields
:LogFields
- Optional context about the failure
withError()
Create a logger with error context attached:
withError(error: unknown, field?: LogFields): Logger
Parameters:
error
:unknown
- Error object to attach to logger contextfield
:LogFields
- Optional additional fields
try { await processPayment(order);} catch (error) { const errorLogger = this.env.logger.withError(error, { orderId: order.id, paymentMethod: order.paymentMethod });
errorLogger.error("Payment processing failed"); errorLogger.warn("Attempting retry with backup gateway");}
logAtLevel()
Log a message at a specific log level programmatically:
logAtLevel(level: LogLevel, message: string, fields?: LogFields): void
Parameters:
level
:LogLevel
- The log level (DEBUG, INFO, WARN, ERROR)message
:string
- The message to logfields
:LogFields
- Optional structured data
const currentLevel = determineLogLevel();this.env.logger.logAtLevel(currentLevel, "Dynamic log message", { calculatedLevel: currentLevel, systemHealth: "degraded"});
message() and messageAtLevel()
Create log messages without immediately sending them:
message(message: string, fields?: LogFields): LogMessagemessageAtLevel(level: LogLevel, message: string, fields?: LogFields): LogMessage
Parameters:
level
:LogLevel
- The log level (for messageAtLevel)message
:string
- The message textfields
:LogFields
- Optional structured data
// Create message objects for batching or conditional loggingconst userAction = this.env.logger.message("User action completed", { action: "purchase", userId: user.id});
const systemEvent = this.env.logger.messageAtLevel(LogLevel.WARN, "System maintenance", { maintenanceWindow: "2024-01-15T02:00:00Z"});
// Messages can be processed or sent later based on conditionsif (shouldLogUserActions) { // Send the message to configured log sink}
Contextual Logging
Create child loggers that automatically include common fields in all log entries.
with()
Create a new logger instance that includes specified fields in all subsequent log entries:
with(fields?: LogFields): Logger
Parameters:
fields
:LogFields
- Optional key-value pairs to include in all log entries
// Create logger with request contextconst requestLogger = this.env.logger.with({ requestId: crypto.randomUUID(), userId: session.userId, endpoint: request.url});
// All logs include request context automaticallyrequestLogger.info("Processing payment"); // Includes requestId, userId, endpointrequestLogger.warn("Invalid card number"); // Includes requestId, userId, endpointrequestLogger.error("Payment gateway timeout"); // Includes requestId, userId, endpoint
// Nest loggers for additional contextconst paymentLogger = requestLogger.with({ orderId: order.id, amount: order.total});
paymentLogger.info("Starting payment flow"); // Includes all parent fields + orderId, amountpaymentLogger.error("Card declined"); // Includes all parent fields + orderId, amount
CLI Log Access
Access logs from deployed applications using the Raindrop CLI with real-time streaming and historical querying capabilities.
Real-Time Log Streaming
Stream live logs as they occur:
raindrop logs tailraindrop logs tail --application my-app --version v1.2.3
Stream Features:
- Groups related events by trace ID
- Shows execution duration for each operation
- Displays structured field data with formatting
- Includes automatic resource interaction logs
Historical Log Querying
Query logs with time-based filtering and search criteria:
# Query last hour (default)raindrop logs query
# Query specific time rangesraindrop logs query --last 1hraindrop logs query --last 30mraindrop logs query --last 2d
# Query explicit time rangeraindrop logs query --start-time 1638360000000 --end-time 1638363600000raindrop logs query --start-time "2024-01-15T10:00:00Z" --end-time "2024-01-15T11:00:00Z"
# Filter by application and versionraindrop logs query --application my-app --version v1.2.3
# Filter by trace ID or statusraindrop logs query --trace-id abc123defraindrop logs query --status error
# Limit results and format outputraindrop logs query --limit 50 --output json
Query Parameters:
--last
: Duration from now (“1h”, “30m”, “2d”, “1w”)--start-time
: Start time (Unix timestamp ms or ISO string)--end-time
: End time (Unix timestamp ms or ISO string)--application
: Filter by application name--version
: Filter by application version--trace-id
: Filter by specific trace ID--status
: Filter by status (ok
,error
)--limit
: Max events to return (default: 100)--output
: Format (text
,json
)
Log Output Format
Logs display with trace grouping and structured formatting:
[1/15/2024, 2:30:25 PM] 🔗 Trace: abc123def (250ms total) Org: my-org, App: my-app, v1.2.3, Script: api-service 1. ✅ HTTP Request (45ms) http.method: POST http.url: /api/users ✅ http.status: 200 2. ✅ Database Query (180ms) 🗄️ SQL Query: SELECT * FROM users WHERE active = true 📊 Rows Read: 150 🏦 Database: primary 📊 meta: 180ms, 150 rows read, us-east-1, DB: 2.3KB 3. ✅ Custom Log Event (5ms) userId: "12345" action: "user_lookup" result: "success"
Automatic Resource Logging
Raindrop automatically logs all resource interactions without requiring manual instrumentation. This provides complete visibility into your application’s behavior.
Automatically Logged Resources:
- HTTP Services: Request method, URL, status code, response time, user agent
- SQL Databases: Query text, execution time, rows read/written, database name, region
- Vector Indexes: Query vectors, similarity scores, metadata filters, result counts
- Object Storage: Object keys, metadata, content types, upload/download operations
- Key-Value Cache: Keys, operation types (get/put/delete), hit/miss status
- Message Queues: Message content, send/receive timestamps, retry attempts
- SmartBuckets: Search queries, document processing, result relevance scores
- SmartMemory: Memory operations, retrieval queries, storage updates
- AI Models: Model names, prompts, responses, token usage, processing time, costs
- Actors: Lifecycle events, state changes, message passing, persistence operations
Code Examples
Complete logging implementations demonstrating best practices and common patterns.
Service with Structured Logging
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const startTime = Date.now(); const requestId = crypto.randomUUID();
// Create request logger with context const requestLogger = this.env.logger.with({ requestId, method: request.method, url: request.url, userAgent: request.headers.get('User-Agent') });
requestLogger.info("Request started");
try { const result = await this.handleRequest(request, requestLogger);
requestLogger.info("Request completed", { status: result.status, duration: Date.now() - startTime });
return result;
} catch (error) { requestLogger.error(error, { duration: Date.now() - startTime, stage: "request_processing" });
return new Response("Internal Server Error", { status: 500 }); } }
private async handleRequest(request: Request, logger: Logger): Promise<Response> { const url = new URL(request.url);
// Route to specific handlers if (url.pathname === '/api/users') { return this.handleUsers(request, logger); }
if (url.pathname === '/api/orders') { return this.handleOrders(request, logger); }
// Log unknown endpoints for monitoring logger.warn("Unknown endpoint requested", { path: url.pathname, query: url.search, method: request.method, userAgent: request.headers.get('User-Agent') });
return new Response("Not Found", { status: 404 }); }
private async handleUsers(request: Request, logger: Logger): Promise<Response> { const userLogger = logger.with({ operation: "list_users" });
userLogger.debug("Querying database for users");
try { // Simulate database query const users = await this.queryUsers();
userLogger.info("Users retrieved successfully", { count: users.length, hasNext: users.length === 100 });
return Response.json({ users, total: users.length });
} catch (error) { userLogger.error(error, { errorType: "database_error", table: "users" });
throw error; } }
private async handleOrders(request: Request, logger: Logger): Promise<Response> { const orderLogger = logger.with({ operation: "list_orders" });
if (request.method === 'GET') { return this.getOrders(orderLogger); }
if (request.method === 'POST') { return this.createOrder(request, orderLogger); }
orderLogger.warn("Unsupported method for orders endpoint", { method: request.method, allowedMethods: ["GET", "POST"] });
return new Response("Method Not Allowed", { status: 405 }); }
private async getOrders(logger: Logger): Promise<Response> { logger.debug("Retrieving order list");
const orders = await this.queryOrders();
logger.info("Orders retrieved", { count: orders.length, totalValue: orders.reduce((sum, order) => sum + order.total, 0) });
return Response.json({ orders }); }
private async createOrder(request: Request, logger: Logger): Promise<Response> { const orderData = await request.json();
const orderLogger = logger.with({ customerId: orderData.customerId, itemCount: orderData.items?.length || 0, total: orderData.total });
orderLogger.info("Creating new order");
try { // Validate order data this.validateOrder(orderData, orderLogger);
// Process payment const paymentResult = await this.processPayment(orderData, orderLogger);
// Create order record const order = await this.createOrderRecord(orderData, orderLogger);
orderLogger.info("Order created successfully", { orderId: order.id, paymentId: paymentResult.id });
return Response.json({ success: true, orderId: order.id });
} catch (error) { orderLogger.error(error, { stage: "order_creation", orderData: JSON.stringify(orderData) });
return Response.json({ success: false, error: "Failed to create order" }, { status: 400 }); } }
private validateOrder(orderData: any, logger: Logger): void { logger.debug("Validating order data", { requiredFields: ["customerId", "items", "total"], providedFields: Object.keys(orderData) });
if (!orderData.customerId) { logger.error("Order validation failed", { error: "missing_customer_id" }); throw new Error("Customer ID is required"); }
if (!orderData.items || orderData.items.length === 0) { logger.error("Order validation failed", { error: "no_items" }); throw new Error("Order must contain at least one item"); }
logger.info("Order validation passed"); }
private async processPayment(orderData: any, logger: Logger): Promise<any> { const paymentLogger = logger.with({ paymentMethod: orderData.paymentMethod, amount: orderData.total });
paymentLogger.info("Processing payment");
try { // Simulate payment processing const result = await this.chargePayment(orderData);
paymentLogger.info("Payment processed successfully", { transactionId: result.transactionId, processingTime: result.processingTime });
return result;
} catch (error) { paymentLogger.error(error, { stage: "payment_charge", gateway: "stripe" });
throw new Error("Payment processing failed"); } }
private async createOrderRecord(orderData: any, logger: Logger): Promise<any> { logger.debug("Creating order record in database");
const order = { id: `ORDER-${Date.now()}`, customerId: orderData.customerId, items: orderData.items, total: orderData.total, createdAt: new Date().toISOString() };
// Simulate database save await this.saveOrder(order);
logger.info("Order record created", { orderId: order.id, recordSize: JSON.stringify(order).length });
return order; }
// Mock methods for example private async queryUsers(): Promise<any[]> { return []; } private async queryOrders(): Promise<any[]> { return []; } private async chargePayment(orderData: any): Promise<any> { return { transactionId: "txn_123", processingTime: 150 }; } private async saveOrder(order: any): Promise<void> {}}
Error Handling with Context
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { try { return await this.processRequest(request); } catch (error) { return this.handleError(error, request); } }
private async processRequest(request: Request): Promise<Response> { const url = new URL(request.url); const operation = this.getOperation(url.pathname);
const logger = this.env.logger.with({ operation, path: url.pathname, method: request.method });
try { logger.info("Operation started");
const result = await this.executeOperation(operation, request, logger);
logger.info("Operation completed successfully", { resultType: typeof result, resultSize: result ? JSON.stringify(result).length : 0 });
return Response.json(result);
} catch (error) { logger.error(error, { operation, stage: "execution", inputSize: request.headers.get('content-length') || 0 });
// Re-throw to be handled by main error handler throw error; } }
private handleError(error: unknown, request: Request): Response { const errorLogger = this.env.logger.with({ url: request.url, method: request.method, userAgent: request.headers.get('User-Agent') });
if (error instanceof ValidationError) { errorLogger.warn("Validation error occurred", { errorType: "validation", validationErrors: error.errors });
return Response.json({ error: "Validation failed", details: error.errors }, { status: 400 }); }
if (error instanceof AuthenticationError) { errorLogger.warn("Authentication failed", { errorType: "authentication", reason: error.reason });
return Response.json({ error: "Authentication required" }, { status: 401 }); }
if (error instanceof NotFoundError) { errorLogger.info("Resource not found", { errorType: "not_found", resource: error.resource });
return Response.json({ error: "Resource not found" }, { status: 404 }); }
// Unknown error - log with full details errorLogger.error(error as Error, { errorType: "unknown", stack: (error as Error).stack });
return Response.json({ error: "Internal server error" }, { status: 500 }); }
private getOperation(pathname: string): string { if (pathname.startsWith('/api/users')) return 'user_management'; if (pathname.startsWith('/api/orders')) return 'order_processing'; if (pathname.startsWith('/api/products')) return 'product_catalog'; return 'unknown'; }
private async executeOperation(operation: string, request: Request, logger: Logger): Promise<any> { switch (operation) { case 'user_management': return this.handleUserOperation(request, logger); case 'order_processing': return this.handleOrderOperation(request, logger); case 'product_catalog': return this.handleProductOperation(request, logger); default: throw new NotFoundError(`Unknown operation: ${operation}`); } }
private async handleUserOperation(request: Request, logger: Logger): Promise<any> { logger.debug("Processing user operation"); // Implementation here return { users: [] }; }
private async handleOrderOperation(request: Request, logger: Logger): Promise<any> { logger.debug("Processing order operation"); // Implementation here return { orders: [] }; }
private async handleProductOperation(request: Request, logger: Logger): Promise<any> { logger.debug("Processing product operation"); // Implementation here return { products: [] }; }}
// Custom error classes for demonstrationclass ValidationError extends Error { constructor(public errors: string[]) { super('Validation failed'); }}
class AuthenticationError extends Error { constructor(public reason: string) { super('Authentication failed'); }}
class NotFoundError extends Error { constructor(public resource: string) { super('Resource not found'); }}