Database Monitoring
Database Monitoring
Learn how to monitor your Solo Kit database for optimal performance, health, and reliability. This guide covers health checks, performance metrics, alerting strategies, and comprehensive observability practices with Convex.
Monitoring Overview
Solo Kit Monitoring Architecture
Solo Kit implements comprehensive database observability with:
- Real-time health checks: Continuous database connectivity monitoring
- Performance metrics: Query response times and function execution
- Function monitoring: Convex Dashboard function analytics
- Error tracking: Automated error detection and alerting
- Operational dashboards: Visual monitoring and trending
Monitoring Components
Health Check APIs (/api/health/database, /api/healthz)
↓
Performance Metrics (latency, function execution, query stats)
↓
Alerting & Notifications (Sentry, custom alerts)
↓
Dashboards & Visualization (Convex Dashboard)Three-tier monitoring:
- Application-level: Health checks and performance metrics
- Function-level: Convex function execution and logs
- Platform-level: Convex Dashboard monitoring
Health Check Implementation
Built-in Health Check System
Solo Kit provides comprehensive health monitoring endpoints:
// convex/health.ts
import { query } from './_generated/server';
export const check = query({
handler: async (ctx) => {
const timestamp = Date.now();
try {
// Test database connectivity
await ctx.db.query('users').take(1);
return {
status: 'healthy',
timestamp,
provider: 'convex',
};
} catch (error) {
return {
status: 'unhealthy',
timestamp,
error: error instanceof Error ? error.message : 'Unknown error',
provider: 'convex',
};
}
},
});
export const detailed = query({
handler: async (ctx) => {
const startTime = Date.now();
try {
// Test various operations
const [users, sessions] = await Promise.all([
ctx.db.query('users').take(1),
ctx.db.query('sessions').take(1),
]);
const latency = Date.now() - startTime;
return {
status: 'healthy',
latency,
timestamp: Date.now(),
checks: {
users: users.length >= 0,
sessions: sessions.length >= 0,
},
};
} catch (error) {
return {
status: 'unhealthy',
latency: Date.now() - startTime,
timestamp: Date.now(),
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
});Health Check API Endpoints
Database-specific health check:
// apps/web/app/api/(dev)/health/database/route.ts
import { getConvexClient } from '@/lib/convex-server';
import { api } from '@/convex/_generated/api';
export async function GET() {
try {
const startTime = Date.now();
const convex = getConvexClient();
const health = await convex.query(api.health.check);
const responseTime = Date.now() - startTime;
const status = health.status === 'healthy' ? 200 : 503;
return Response.json(
{
database: {
status: health.status,
responseTime: `${responseTime}ms`,
timestamp: new Date().toISOString(),
provider: 'convex',
},
environment: process.env.NODE_ENV,
configured: true,
},
{
status,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
}
);
} catch (error) {
return Response.json(
{
database: {
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Health check failed',
timestamp: new Date().toISOString(),
provider: 'convex',
},
environment: process.env.NODE_ENV,
configured: false,
},
{ status: 503 }
);
}
}Comprehensive application health check:
// apps/web/app/api/(dev)/healthz/route.ts
import { NextRequest } from 'next/server';
import { getConvexClient } from '@/lib/convex-server';
import { api } from '@/convex/_generated/api';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const checkType = searchParams.get('type') || 'readiness';
try {
// Basic liveness check
if (checkType === 'liveness') {
return Response.json({
status: 'healthy',
timestamp: new Date().toISOString(),
type: 'liveness',
});
}
// Full readiness check
const [databaseHealth, memoryHealth] = await Promise.all([
checkDatabaseHealth(),
Promise.resolve(getMemoryUsage()),
]);
const overallStatus =
databaseHealth.status === 'healthy' && memoryHealth.status === 'healthy'
? 'healthy'
: 'unhealthy';
return Response.json(
{
status: overallStatus,
timestamp: new Date().toISOString(),
checks: {
database: databaseHealth,
memory: memoryHealth,
},
},
{ status: overallStatus === 'healthy' ? 200 : 503 }
);
} catch (error) {
return Response.json(
{
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 503 }
);
}
}
async function checkDatabaseHealth() {
try {
const convex = getConvexClient();
const startTime = Date.now();
const health = await convex.query(api.health.check);
const responseTime = Date.now() - startTime;
if (responseTime > 2000) {
console.warn(`Slow database health check: ${responseTime}ms`);
}
return {
status: health.status === 'healthy' ? 'healthy' : 'unhealthy',
responseTime,
provider: 'convex',
};
} catch (error) {
return {
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Health check failed',
provider: 'convex',
};
}
}
function getMemoryUsage() {
const usage = process.memoryUsage();
const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
const heapTotalMB = Math.round(usage.heapTotal / 1024 / 1024);
return {
status: heapUsedMB < heapTotalMB * 0.9 ? 'healthy' : 'warning',
heapUsed: `${heapUsedMB}MB`,
heapTotal: `${heapTotalMB}MB`,
percentage: Math.round((heapUsedMB / heapTotalMB) * 100),
};
}Performance Monitoring
Response Time Tracking
Monitor query performance in Convex functions:
// convex/monitoring.ts
import { v } from 'convex/values';
import { query, internalMutation } from './_generated/server';
export const measureQueryPerformance = query({
args: { queryName: v.string() },
handler: async (ctx, args) => {
const startTime = Date.now();
// Execute the query being measured
let result;
switch (args.queryName) {
case 'users':
result = await ctx.db.query('users').take(10);
break;
case 'sessions':
result = await ctx.db.query('sessions').take(10);
break;
default:
result = [];
}
const duration = Date.now() - startTime;
// Log slow queries
if (duration > 1000) {
console.warn(`Slow query detected: ${args.queryName} took ${duration}ms`);
}
return {
queryName: args.queryName,
duration,
resultCount: result.length,
timestamp: Date.now(),
};
},
});
// Store performance metrics
export const recordMetric = internalMutation({
args: {
name: v.string(),
value: v.number(),
tags: v.optional(v.any()),
},
handler: async (ctx, args) => {
await ctx.db.insert('metrics', {
name: args.name,
value: args.value,
tags: args.tags,
timestamp: Date.now(),
});
},
});
// Get recent metrics
export const getRecentMetrics = query({
args: {
name: v.string(),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
return await ctx.db
.query('metrics')
.filter((q) => q.eq(q.field('name'), args.name))
.order('desc')
.take(args.limit ?? 100);
},
});Function Performance Monitoring
Leverage Convex Dashboard for function metrics:
// convex/users.ts
import { query } from './_generated/server';
// Convex automatically tracks execution time
export const getUserWithTiming = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
console.time(`getUserWithTiming:${args.userId}`);
const user = await ctx.db.get(args.userId);
console.timeEnd(`getUserWithTiming:${args.userId}`);
return user;
},
});Convex Dashboard Monitoring
Built-in Monitoring Features
Access Convex Dashboard:
# Open dashboard
npx convex dashboard
# View production dashboard
npx convex dashboard --prodDashboard provides:
- Functions tab: Execution times, invocation counts, errors
- Logs tab: Real-time function logs and errors
- Data tab: Document counts and storage usage
- Deployments: Deployment history and status
Metrics Available
Function Metrics:
- Invocation count
- Average execution time
- P50, P95, P99 latency
- Error rate
- Cache hit rate
Data Metrics:
- Document count per table
- Storage usage
- Index usage
Alerting & Notifications
Performance-Based Alerting
Create alerting functions:
// convex/alerts.ts
import { v } from 'convex/values';
import { internalMutation, query } from './_generated/server';
interface Alert {
type: string;
severity: 'warning' | 'critical';
message: string;
timestamp: number;
metadata?: Record<string, any>;
}
export const checkPerformanceAlerts = query({
handler: async (ctx) => {
const alerts: Alert[] = [];
// Check recent function performance
const recentMetrics = await ctx.db
.query('metrics')
.filter((q) => q.eq(q.field('name'), 'query_duration'))
.order('desc')
.take(100);
if (recentMetrics.length > 0) {
const avgDuration = recentMetrics.reduce((sum, m) => sum + m.value, 0) / recentMetrics.length;
if (avgDuration > 5000) {
alerts.push({
type: 'performance',
severity: 'critical',
message: `Very slow queries detected: avg ${avgDuration.toFixed(0)}ms`,
timestamp: Date.now(),
metadata: { avgDuration },
});
} else if (avgDuration > 1000) {
alerts.push({
type: 'performance',
severity: 'warning',
message: `Slow queries detected: avg ${avgDuration.toFixed(0)}ms`,
timestamp: Date.now(),
metadata: { avgDuration },
});
}
}
return alerts;
},
});
export const logAlert = internalMutation({
args: {
type: v.string(),
severity: v.union(v.literal('warning'), v.literal('critical')),
message: v.string(),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
await ctx.db.insert('alerts', {
...args,
timestamp: Date.now(),
acknowledged: false,
});
// Log to console for Convex Dashboard
const emoji = args.severity === 'critical' ? 'CRITICAL' : 'WARNING';
console.log(`[${emoji}] ${args.type}: ${args.message}`);
},
});Error Tracking Integration
Integrate with Sentry:
// lib/monitoring.ts
import * as Sentry from '@sentry/nextjs';
export function trackDatabaseError(error: Error, context?: Record<string, any>) {
Sentry.captureException(error, {
tags: {
component: 'database',
provider: 'convex',
},
extra: context,
});
}
export function trackSlowQuery(queryName: string, duration: number, threshold: number = 1000) {
if (duration > threshold) {
Sentry.captureMessage('Slow database query detected', {
level: duration > 5000 ? 'error' : 'warning',
tags: {
query_name: queryName,
performance_issue: 'true',
},
extra: {
duration_ms: duration,
threshold_ms: threshold,
},
});
}
}Custom Monitoring Dashboard
Monitoring API Endpoint
// apps/web/app/api/(dev)/monitoring/database/route.ts
import { getConvexClient } from '@/lib/convex-server';
import { api } from '@/convex/_generated/api';
export async function GET() {
try {
const convex = getConvexClient();
const [health, alerts, metrics] = await Promise.all([
convex.query(api.health.detailed),
convex.query(api.alerts.checkPerformanceAlerts),
convex.query(api.monitoring.getRecentMetrics, {
name: 'query_duration',
limit: 50,
}),
]);
return Response.json({
status: 'success',
timestamp: new Date().toISOString(),
data: {
health,
alerts,
metrics,
summary: {
healthy: health.status === 'healthy',
latency: health.latency,
alertCount: alerts.length,
criticalAlerts: alerts.filter((a: any) => a.severity === 'critical').length,
},
},
});
} catch (error) {
return Response.json(
{
status: 'error',
error: error instanceof Error ? error.message : 'Monitoring failed',
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
}Scheduled Health Checks
Use Convex cron jobs for scheduled monitoring:
// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';
const crons = cronJobs();
// Health check every minute
crons.interval('health-check', { minutes: 1 }, internal.monitoring.runHealthCheck);
// Performance metrics every 5 minutes
crons.interval('collect-metrics', { minutes: 5 }, internal.monitoring.collectMetrics);
// Alert check every minute
crons.interval('check-alerts', { minutes: 1 }, internal.alerts.checkAndNotify);
export default crons;// convex/monitoring.ts
export const runHealthCheck = internalMutation({
handler: async (ctx) => {
const startTime = Date.now();
try {
// Test database operations
await ctx.db.query('users').take(1);
const latency = Date.now() - startTime;
// Log health status
if (latency > 1000) {
console.warn(`Health check slow: ${latency}ms`);
}
// Record metric
await ctx.db.insert('metrics', {
name: 'health_check_latency',
value: latency,
timestamp: Date.now(),
});
} catch (error) {
console.error('Health check failed:', error);
}
},
});
export const collectMetrics = internalMutation({
handler: async (ctx) => {
// Collect document counts
const tables = ['users', 'sessions', 'subscriptions'];
for (const table of tables) {
const count = (await ctx.db.query(table as any).collect()).length;
await ctx.db.insert('metrics', {
name: `document_count_${table}`,
value: count,
timestamp: Date.now(),
});
}
},
});Best Practices
1. Monitoring Strategy
Implement the four golden signals:
- Latency: How long requests take
- Traffic: How many requests you're getting
- Errors: Rate of requests that fail
- Saturation: How full your service is
2. Alert Fatigue Prevention
Design meaningful alerts:
// Good: Actionable alerts with context
if (responseTime > 5000) {
console.error(`CRITICAL: Database response time ${responseTime}ms exceeds 5s threshold`);
}
// Bad: Noisy alerts without context
if (responseTime > 100) {
console.log('Slow database');
}3. Log Effectively
Use structured logging:
// convex/logging.ts
export function logStructured(
level: 'info' | 'warn' | 'error',
message: string,
context?: Record<string, any>
) {
const logEntry = {
level,
message,
timestamp: new Date().toISOString(),
...context,
};
switch (level) {
case 'error':
console.error(JSON.stringify(logEntry));
break;
case 'warn':
console.warn(JSON.stringify(logEntry));
break;
default:
console.log(JSON.stringify(logEntry));
}
}4. Monitor What Matters
Focus on business-critical metrics:
- User authentication success rate
- Payment processing latency
- API response times
- Error rates by endpoint
Viewing Logs
Convex CLI Logs
# Stream logs in real-time
npx convex logs
# Stream production logs
npx convex logs --prod
# Filter by log level
npx convex logs --success # Only successful operations
npx convex logs --error # Only errorsDashboard Logs
Access detailed logs in Convex Dashboard:
- Open
npx convex dashboard - Navigate to "Logs" tab
- Filter by function, time range, or log level
- Search for specific messages
Next Steps
Complete your database mastery with the final guide:
- Advanced - Advanced Convex features and patterns
- Security - Advanced security monitoring
- Performance - Performance monitoring integration
- Backup & Restore - Backup monitoring
Comprehensive database monitoring ensures your Solo Kit application maintains optimal performance, reliability, and user experience at scale.