Logging System
TinyKit Pro includes a comprehensive logging system with environment-based log levels, structured output, and performance timing capabilities. The logging sy...
TinyKit Pro includes a comprehensive logging system with environment-based log levels, structured output, and performance timing capabilities. The logging system uses emoji prefixes and console methods that integrate seamlessly with Convex's built-in logging infrastructure.
đ¯ Purpose
The logging system provides:
- Environment-Aware Logging: Automatic log level adjustment based on deployment environment
- Structured Output: Consistent formatting with emoji prefixes for easy visual scanning
- Performance Timing: Built-in timing capabilities for performance monitoring
- Runtime Control: Optional runtime log level override for debugging
- Development Workflow: Verbose logging in development, minimal logging in production
- Enforced Best Practices: ESLint rules prevent
console.*usage and enforce logger utility usage
đī¸ Architecture
Log Levels
TinyKit Pro supports four log levels in order of severity:
| Level | Emoji | Purpose | Production | Development |
|---|---|---|---|---|
| DEBUG | đ | Detailed diagnostic information | Hidden | Visible |
| INFO | âšī¸ | General informational messages | Hidden | Visible |
| WARN | â ī¸ | Warning messages for potential issues | Hidden | Visible |
| ERROR | â | Error messages for failures | Visible | Visible |
Log Level Hierarchy: Each level includes all higher-severity levels. For example, INFO includes WARN and ERROR but excludes DEBUG.
Environment-Based Configuration
The logger automatically determines the appropriate log level based on environment:
CONVEX_ENV="development" â DEBUG (all logs visible)
CONVEX_ENV="preview" â INFO (info, warn, error)
CONVEX_ENV="staging" â INFO (info, warn, error)
CONVEX_ENV="production" â ERROR (only errors visible)Override: Set LOG_LEVEL environment variable to explicitly control log level:
npx convex env set LOG_LEVEL "DEBUG"
npx convex env set LOG_LEVEL "INFO"
npx convex env set LOG_LEVEL "WARN"
npx convex env set LOG_LEVEL "ERROR"đ Usage
Basic Logging
import { logger } from "../../lib/logger";
import { mutation } from "../../lib/triggers/triggers";
import { v } from "convex/values";
export const createPost = mutation({
args: { title: v.string(), content: v.string() },
handler: async (ctx, args) => {
// Debug logging (only in development)
logger.debug("Creating post with args:", args);
// Info logging (development and staging)
logger.info("Post creation started", { title: args.title });
try {
const postId = await ctx.db.insert("posts", args);
// Success info log
logger.info("Post created successfully", { postId });
return { postId };
} catch (error) {
// Error logging (always visible)
logger.error("Failed to create post", error);
throw error;
}
},
});Structured Logging
Always include context objects for better debugging:
// â Poor: String-only logging
logger.info("Processing user ID 123");
// â
Good: Structured logging with context
logger.info("Processing user", { userId: "123", operation: "update" });
// â
Excellent: Rich context for debugging
logger.info("Processing subscription update", {
userId: user._id,
subscriptionId: subscription._id,
previousStatus: subscription.status,
newStatus: "active",
timestamp: Date.now(),
});Webhook and External API Logging
export const handleStripeWebhook = internalAction({
args: { signature: v.string(), payload: v.string() },
handler: async ({ runAction }, { signature, payload }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {});
try {
const event = await stripe.webhooks.constructEventAsync(
payload,
signature,
process.env.STRIPE_WEBHOOKS_SECRET!,
);
// Log incoming webhook
logger.info("Processing Stripe webhook", {
eventType: event.type,
eventId: event.id,
});
// Process event...
await processEvent(event);
logger.info("Webhook processed successfully", {
eventType: event.type,
});
return { success: true };
} catch (error) {
logger.error("Webhook processing failed", {
error,
signature: signature.substring(0, 20) + "...", // Truncate for security
});
return { success: false };
}
},
});Performance Timing
Use logger.time() and logger.timeEnd() for performance monitoring:
export const expensiveOperation = mutation({
args: { dataSize: v.number() },
handler: async (ctx, args) => {
const operationId = `operation-${Date.now()}`;
// Start timing
logger.time(operationId);
try {
// Perform expensive operation
const results = await processLargeDataset(ctx, args.dataSize);
// End timing (logs elapsed time)
logger.timeEnd(operationId);
logger.info("Operation completed", {
dataSize: args.dataSize,
resultCount: results.length,
});
return results;
} catch (error) {
logger.timeEnd(operationId); // Still log timing on error
logger.error("Operation failed", error);
throw error;
}
},
});Output Example:
âšī¸ [INFO] Operation started
âąī¸ [TIMER] operation-1704067200000: 1234ms
âšī¸ [INFO] Operation completed { dataSize: 1000, resultCount: 150 }Runtime Log Level Control
For debugging specific operations, allow runtime log level override:
import { createLogger, logLevel } from "../../lib/logger";
import { mutation } from "../../lib/triggers/triggers";
import { v } from "convex/values";
export const debugOperation = mutation({
args: {
data: v.string(),
// Optional log level override for debugging
logLevel: v.optional(logLevel),
},
handler: async (ctx, args) => {
// Create logger with optional override
const logger = createLogger(args.logLevel);
logger.debug("Operation started with debug logging enabled");
logger.info("Processing data", { dataLength: args.data.length });
// ... operation logic
logger.debug("Operation completed");
},
});Usage from frontend:
// Normal operation (uses environment log level)
await debugOperation({ data: "test" });
// Debug-enabled operation (force DEBUG level)
await debugOperation({ data: "test", logLevel: "DEBUG" });âī¸ Advanced Patterns
Multi-Step Operation Logging
export const processWorkflow = mutation({
args: { workflowId: v.id("workflows") },
handler: async (ctx, args) => {
logger.info("Workflow processing started", { workflowId: args.workflowId });
// Step 1: Validation
logger.debug("Step 1: Validating workflow");
const workflow = await ctx.db.get(args.workflowId);
if (!workflow) {
logger.error("Workflow not found", { workflowId: args.workflowId });
throw new Error("Workflow not found");
}
logger.debug("Step 1: Validation complete");
// Step 2: Processing
logger.debug("Step 2: Processing tasks");
const tasks = await ctx.db
.query("tasks")
.withIndex("by_workflowId", (q) => q.eq("workflowId", args.workflowId))
.collect();
logger.info("Tasks retrieved", { taskCount: tasks.length });
// Step 3: Completion
logger.debug("Step 3: Marking workflow complete");
await ctx.db.patch(args.workflowId, { status: "completed" });
logger.info("Workflow processing completed", {
workflowId: args.workflowId,
taskCount: tasks.length,
});
return { success: true, taskCount: tasks.length };
},
});Error Context Logging
export const updateUserProfile = mutation({
args: { userId: v.string(), name: v.string() },
handler: async (ctx, args) => {
try {
logger.info("Updating user profile", {
userId: args.userId,
newName: args.name,
});
const user = await ctx.db.get(args.userId);
if (!user) {
logger.warn("User not found for update", {
userId: args.userId,
attempted: "profile update",
});
throw new Error("User not found");
}
await ctx.db.patch(args.userId, { name: args.name });
logger.info("Profile updated successfully", {
userId: args.userId,
previousName: user.name,
newName: args.name,
});
} catch (error) {
logger.error("Profile update failed", {
userId: args.userId,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
},
});Conditional Debug Logging
export const searchItems = query({
args: { query: v.string() },
handler: async (ctx, args) => {
const startTime = Date.now();
logger.debug("Search query received", {
query: args.query,
queryLength: args.query.length,
});
const results = await ctx.db
.query("items")
.withSearchIndex("search_name", (q) => q.search("name", args.query))
.take(10);
const duration = Date.now() - startTime;
// Only log slow queries in production (warn threshold: 500ms)
if (duration > 500) {
logger.warn("Slow search query detected", {
query: args.query,
duration,
resultCount: results.length,
});
} else {
logger.debug("Search completed", {
duration,
resultCount: results.length,
});
}
return results;
},
});Background Job Logging
import { internalMutation } from "../../_generated/server";
import { logger } from "../../lib/logger";
export const processScheduledJob = internalMutation({
args: {},
handler: async (ctx) => {
logger.info("Scheduled job started", { timestamp: Date.now() });
try {
// Get items to process
const items = await ctx.db
.query("pendingItems")
.withIndex("by_status", (q) => q.eq("status", "pending"))
.collect();
logger.info("Items to process", { count: items.length });
let successCount = 0;
let errorCount = 0;
for (const item of items) {
try {
await processItem(ctx, item);
successCount++;
} catch (error) {
errorCount++;
logger.error("Item processing failed", {
itemId: item._id,
error,
});
}
}
logger.info("Scheduled job completed", {
total: items.length,
success: successCount,
errors: errorCount,
});
} catch (error) {
logger.error("Scheduled job failed", error);
throw error;
}
},
});đ Log Output Format
Console Output
# DEBUG level (development only)
đ [DEBUG] Operation started { userId: "abc123", operation: "update" }
# INFO level (development and staging)
âšī¸ [INFO] Processing user { userId: "abc123", email: "user@example.com" }
# WARN level (all environments except production)
â ī¸ [WARN] Slow query detected { query: "search term", duration: 523 }
# ERROR level (all environments)
â [ERROR] Database operation failed { error: "Connection timeout" }
# TIMER output (development and staging)
âąī¸ [TIMER] expensive-operation: 1234msStructured Data
Always use objects for context:
// â
Good: Structured context
logger.info("User authenticated", {
userId: user._id,
email: user.email,
provider: "github",
timestamp: Date.now(),
});
// â Bad: String interpolation
logger.info(`User ${user._id} authenticated via ${provider}`);Benefits of structured logging:
- Easy to search in Convex logs dashboard
- Machine-readable for log aggregation tools
- Consistent format across the application
- Better debugging with full context
đ ESLint Enforcement
TinyKit Pro enforces consistent logging practices through ESLint rules to prevent common mistakes and ensure code quality.
Enforced Rules
1. No Direct Console Usage
// â ESLint Error: no-console
console.log("User created");
console.info("Processing...");
console.warn("Warning message");
console.error("Error occurred");
// â
Correct: Use logger utility
logger.info("User created");
logger.debug("Processing...");
logger.warn("Warning message");
logger.error("Error occurred");Rule: "no-console": "error"
Applies to: All files except:
convex/lib/logger.ts- Logger implementation itselfsrc/lib/logger.ts- Frontend logger (if needed)scripts/**/*.ts- Development scriptsconvex/betterAuth/**/*.ts- BetterAuth component files
2. No console.error in Catch Blocks
// â ESLint Error: Pollutes browser console and fails PageSpeed metrics
try {
await riskyOperation();
} catch (error) {
console.error(error); // ESLint error: no-restricted-syntax
}
// â
Correct: Use logger.error
try {
await riskyOperation();
} catch (error) {
logger.error("Operation failed", { error, operation: "riskyOperation" });
throw error; // Re-throw if needed
}
// â
Also acceptable: Silent fail with comment
try {
await nonCriticalOperation();
} catch (error) {
// Silently fail - non-critical operation, user experience unaffected
// Error logged to monitoring service via error boundary
}Rule: Custom no-restricted-syntax for console.error in catch blocks
Reason: console.error() in catch blocks:
- Pollutes browser console with non-actionable errors
- Fails Google PageSpeed metrics
- Clutters production logs
- Makes debugging harder by mixing expected and unexpected errors
3. No Empty Catch Blocks
// â ESLint Error: Empty catch blocks swallow errors
try {
await operation();
} catch (error) {
// Empty - no handling
}
// â
Correct: Log the error or add explanatory comment
try {
await operation();
} catch (error) {
logger.error("Operation failed", { error });
}
// â
Also acceptable: Silent fail with clear justification
try {
await nonCriticalOperation();
} catch (error) {
// Intentionally silent - non-critical background operation
// Logged via global error boundary for monitoring
}Rule: Custom no-restricted-syntax for empty catch blocks
Exemptions
Certain directories are exempt from console rules:
- Logger implementations:
convex/lib/logger.ts,src/lib/logger.ts - Scripts:
scripts/**/*.ts(development utilities) - BetterAuth:
convex/betterAuth/**/*.ts(third-party component)
Linting Commands
# Check for logging violations
bun lint
# Auto-fix some issues (won't fix console â logger)
bun lint:fix
# Run before committing (enforced by pre-commit hook)
bun typecheck && bun lintđ§ Configuration
Environment Setup
Set the deployment environment:
# Development (DEBUG level)
npx convex env set CONVEX_ENV "development"
# Staging (INFO level)
npx convex env set CONVEX_ENV "staging"
# Production (ERROR level)
npx convex env set CONVEX_ENV "production"Override Log Level
Force a specific log level regardless of environment:
# Force DEBUG in production (temporary debugging)
npx convex env set LOG_LEVEL "DEBUG"
# Remove override (use environment-based level)
npx convex env remove LOG_LEVELViewing Logs
# Stream live logs from Convex
npx convex logs --tail
# Filter logs by keyword
npx convex logs --tail | grep "ERROR"
npx convex logs --tail | grep "user\|auth"
# View logs in Convex Dashboard
npx convex dashboardđ¯ Best Practices
1. Choose the Right Log Level
// DEBUG: Detailed diagnostic information
logger.debug("Function arguments", { args });
logger.debug("Intermediate calculation result", { value: intermediateResult });
// INFO: General operational messages
logger.info("User created", { userId, email });
logger.info("Webhook received", { eventType, eventId });
// WARN: Unexpected but recoverable conditions
logger.warn("Deprecated API used", { endpoint, userId });
logger.warn("Rate limit approaching", { userId, remaining: 10 });
// ERROR: Failures requiring attention
logger.error("Database write failed", { error, userId });
logger.error("External API unavailable", { service: "Stripe", error });2. Include Relevant Context
// â Insufficient context
logger.error("Update failed");
// â
Rich context for debugging
logger.error("User profile update failed", {
userId: user._id,
email: user.email,
attemptedChanges: { name: newName },
error: error.message,
timestamp: Date.now(),
});3. Avoid Logging Sensitive Data
// â NEVER log sensitive data
logger.info("User signed in", {
password: args.password, // NEVER
apiKey: process.env.API_KEY, // NEVER
creditCard: user.creditCard, // NEVER
});
// â
Log safely with redaction
logger.info("User signed in", {
userId: user._id,
email: user.email.replace(/@.*/, "@***"), // Partially redacted
provider: "password",
hasValidSession: true,
});4. Use Performance Timing for Expensive Operations
export const generateReport = mutation({
handler: async (ctx) => {
logger.time("generateReport");
// Expensive operation
const data = await fetchLargeDataset(ctx);
const processed = await processData(data);
const report = await createReport(processed);
logger.timeEnd("generateReport");
return report;
},
});5. Log State Transitions
export const updateSubscription = mutation({
args: { subscriptionId: v.string(), newStatus: v.string() },
handler: async (ctx, args) => {
const subscription = await ctx.db.get(args.subscriptionId);
logger.info("Subscription status change", {
subscriptionId: args.subscriptionId,
previousStatus: subscription?.status,
newStatus: args.newStatus,
userId: subscription?.userId,
});
await ctx.db.patch(args.subscriptionId, { status: args.newStatus });
},
});đ¨ Common Mistakes
// â DON'T: Use console.log directly (ESLint will prevent this)
console.log("User created"); // ESLint error: no-console
// â
DO: Use logger methods
logger.info("User created", { userId });
// â DON'T: Log in loops without aggregation
for (const item of items) {
logger.info("Processing item", { itemId: item._id });
}
// â
DO: Aggregate and log summary
logger.info("Processing items", { count: items.length });
// ... process items
logger.info("Items processed", { total: items.length, success: successCount });
// â DON'T: Log passwords or secrets
logger.debug("Auth payload", { username, password, apiKey });
// â
DO: Redact sensitive data
logger.debug("Auth payload", { username, hasPassword: !!password });
// â DON'T: Use string concatenation
logger.info(`User ${userId} updated profile`);
// â
DO: Use structured objects
logger.info("User updated profile", { userId });
// â DON'T: Use console.error in catch blocks (ESLint will prevent this)
try {
// some operation
} catch (error) {
console.error(error); // ESLint error: no-restricted-syntax
}
// â
DO: Use logger.error for error handling
try {
// some operation
} catch (error) {
logger.error("Operation failed", { error, context: "someOperation" });
throw error; // Re-throw if needed
}đ§Ē Testing
Verify Log Output
// Test that logs appear at correct levels
export const testLogging = mutation({
args: { testLevel: logLevel },
handler: async (ctx, args) => {
const logger = createLogger(args.testLevel);
logger.debug("This is a debug message");
logger.info("This is an info message");
logger.warn("This is a warning message");
logger.error("This is an error message");
return { logLevel: args.testLevel };
},
});
// Call with different levels to verify filtering
await testLogging({ testLevel: "DEBUG" }); // All messages
await testLogging({ testLevel: "INFO" }); // Info, warn, error
await testLogging({ testLevel: "WARN" }); // Warn, error
await testLogging({ testLevel: "ERROR" }); // Error onlyđ API Reference
createLogger()
function createLogger(level?: LogLevel): Logger;Parameters:
level(optional): Log level ("DEBUG" | "INFO" | "WARN" | "ERROR")- Defaults to environment-based level via
getDefaultLogLevel()
- Defaults to environment-based level via
Returns: Logger instance with methods:
debug(...args: unknown[]): voidinfo(...args: unknown[]): voidwarn(...args: unknown[]): voiderror(...args: unknown[]): voidtime(label: string): voidtimeEnd(label: string): void
logger (default instance)
const logger: Logger;Pre-configured logger instance using environment-based log level.
getDefaultLogLevel()
function getDefaultLogLevel(): LogLevel;Returns the appropriate log level based on:
LOG_LEVELenvironment variable (if set)CONVEX_ENVmapping:"development"â"DEBUG""preview"|"staging"â"INFO""production"â"ERROR"undefinedâ"INFO"(with warning)
đ Related Documentation
- Rate Limiting - Rate limiting and throttling
- Convex Best Practices - Optimizing database operations
- Convex Documentation - Convex logging infrastructure
Validation System
TinyKit Pro uses Zod for runtime validation across both frontend forms and backend mutations, providing type-safe validation with a single source of truth.
Rate Limiting
TinyKit Pro includes a comprehensive rate limiting system using @convex-dev/rate-limiter to protect against abuse, prevent excessive resource usage, and ensu...