Skip to content

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 development
this.env.logger.debug("Processing user input", { inputKeys: Object.keys(data) });
// Info - normal application events
this.env.logger.info("User session started", { userId: user.id });
// Warn - unusual conditions that don't break functionality
this.env.logger.warn("API response slow", { responseTime: 3000 });
// Error - failures requiring attention
this.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 levels
enum LogLevel {
DEBUG = 'DEBUG', // Detailed debugging information
INFO = 'INFO', // Normal application events
WARN = 'WARN', // Warning conditions
ERROR = 'ERROR' // Error conditions
}
// Structured data type for log fields
type LogFields = Record<string, number | string | boolean | object | undefined | null | Error | LogFields>;
// Log message structure
interface 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 event
  • fields: 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 event
  • fields: 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 condition
  • fields: LogFields - Optional context about the condition

error()

Use for failures that need immediate attention:

// Log error messages
this.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 description
  • fields: 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 context
  • field: 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 log
  • fields: 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): LogMessage
messageAtLevel(level: LogLevel, message: string, fields?: LogFields): LogMessage

Parameters:

  • level: LogLevel - The log level (for messageAtLevel)
  • message: string - The message text
  • fields: LogFields - Optional structured data
// Create message objects for batching or conditional logging
const 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 conditions
if (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 context
const requestLogger = this.env.logger.with({
requestId: crypto.randomUUID(),
userId: session.userId,
endpoint: request.url
});
// All logs include request context automatically
requestLogger.info("Processing payment"); // Includes requestId, userId, endpoint
requestLogger.warn("Invalid card number"); // Includes requestId, userId, endpoint
requestLogger.error("Payment gateway timeout"); // Includes requestId, userId, endpoint
// Nest loggers for additional context
const paymentLogger = requestLogger.with({
orderId: order.id,
amount: order.total
});
paymentLogger.info("Starting payment flow"); // Includes all parent fields + orderId, amount
paymentLogger.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:

Terminal window
raindrop logs tail
raindrop 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:

Terminal window
# Query last hour (default)
raindrop logs query
# Query specific time ranges
raindrop logs query --last 1h
raindrop logs query --last 30m
raindrop logs query --last 2d
# Query explicit time range
raindrop logs query --start-time 1638360000000 --end-time 1638363600000
raindrop logs query --start-time "2024-01-15T10:00:00Z" --end-time "2024-01-15T11:00:00Z"
# Filter by application and version
raindrop logs query --application my-app --version v1.2.3
# Filter by trace ID or status
raindrop logs query --trace-id abc123def
raindrop logs query --status error
# Limit results and format output
raindrop 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 demonstration
class 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');
}
}