15 KiB
ADR-015: Application Performance Monitoring (APM) and Error Tracking
Date: 2025-12-12
Status: Accepted
Updated: 2026-01-11
Context
While ADR-004 established structured logging with Pino, the application lacks a high-level, aggregated view of its health, performance, and errors. It's difficult to spot trends, identify slow API endpoints, or be proactively notified of new types of errors.
Key requirements:
- Self-hosted: No external SaaS dependencies for error tracking
- Sentry SDK compatible: Leverage mature, well-documented SDKs
- Lightweight: Minimal resource overhead in the dev container
- Production-ready: Same architecture works on bare-metal production servers
- AI-accessible: MCP server integration for Claude Code and other AI tools
Decision
We will implement a self-hosted error tracking stack using Bugsink as the Sentry-compatible backend, with the following components:
1. Error Tracking Backend: Bugsink
Bugsink is a lightweight, self-hosted Sentry alternative that:
- Runs as a single process (no Kafka, Redis, ClickHouse required)
- Is fully compatible with Sentry SDKs
- Supports ARM64 and AMD64 architectures
- Can use SQLite (dev) or PostgreSQL (production)
Deployment:
- Dev container: Installed as a systemd service inside the container
- Production: Runs as a systemd service on bare-metal, listening on localhost only
- Database: Uses PostgreSQL with a dedicated
bugsinkuser andbugsinkdatabase (same PostgreSQL instance as the main application)
2. Backend Integration: @sentry/node
The Express backend will integrate @sentry/node SDK to:
- Capture unhandled exceptions before PM2/process manager restarts
- Report errors with full stack traces and context
- Integrate with Pino logger for breadcrumbs
- Track transaction performance (optional)
3. Frontend Integration: @sentry/react
The React frontend will integrate @sentry/react SDK to:
- Wrap the app in a Sentry Error Boundary
- Capture unhandled JavaScript errors
- Report errors with component stack traces
- Track user session context
4. Log Aggregation: Logstash
Logstash parses application and infrastructure logs, forwarding error patterns to Bugsink:
- Installation: Installed inside the dev container (and on bare-metal prod servers)
- Inputs:
- Pino JSON logs from the Node.js application
- Redis logs (connection errors, memory warnings, slow commands)
- PostgreSQL function logs (future - see Implementation Steps)
- Filter: Identifies error-level logs (5xx responses, unhandled exceptions, Redis errors)
- Output: Sends to Bugsink via Sentry-compatible HTTP API
This provides a secondary error capture path for:
- Errors that occur before Sentry SDK initialization
- Log-based errors that don't throw exceptions
- Redis connection/performance issues
- Database function errors and slow queries
- Historical error analysis from log files
5. MCP Server Integration: sentry-selfhosted-mcp
For AI tool integration (Claude Code, Cursor, etc.), we use the open-source sentry-selfhosted-mcp server:
- No code changes required: Configurable via environment variables
- Capabilities: List projects, get issues, view events, update status, add comments
- Configuration:
SENTRY_URL: Points to Bugsink instanceSENTRY_AUTH_TOKEN: API token from BugsinkSENTRY_ORG_SLUG: Organization identifier
Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ Dev Container / Production Server │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Frontend │ │ Backend │ │
│ │ (React) │ │ (Express) │ │
│ │ @sentry/react │ │ @sentry/node │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ Sentry SDK Protocol │ │
│ └───────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Bugsink │ │
│ │ (localhost:8000) │◄──────────────────┐ │
│ │ │ │ │
│ │ PostgreSQL backend │ │ │
│ └──────────────────────┘ │ │
│ │ │
│ ┌──────────────────────┐ │ │
│ │ Logstash │───────────────────┘ │
│ │ (Log Aggregator) │ Sentry Output │
│ │ │ │
│ │ Inputs: │ │
│ │ - Pino app logs │ │
│ │ - Redis logs │ │
│ │ - PostgreSQL (future) │
│ └──────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌───────────┘ │ └───────────┐ │
│ │ │ │ │
│ ┌────┴─────┐ ┌─────┴────┐ ┌──────┴─────┐ │
│ │ Pino │ │ Redis │ │ PostgreSQL │ │
│ │ Logs │ │ Logs │ │ Logs (TBD) │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ PostgreSQL │ │
│ │ ┌────────────────┐ │ │
│ │ │ flyer_crawler │ │ (main app database) │
│ │ ├────────────────┤ │ │
│ │ │ bugsink │ │ (error tracking database) │
│ │ └────────────────┘ │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
External (Developer Machine):
┌──────────────────────────────────────┐
│ Claude Code / Cursor / VS Code │
│ ┌────────────────────────────────┐ │
│ │ sentry-selfhosted-mcp │ │
│ │ (MCP Server) │ │
│ │ │ │
│ │ SENTRY_URL=http://localhost:8000
│ │ SENTRY_AUTH_TOKEN=... │ │
│ │ SENTRY_ORG_SLUG=... │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
Configuration
Environment Variables
| Variable | Description | Default (Dev) |
|---|---|---|
BUGSINK_DSN |
Sentry-compatible DSN for SDKs | Set after project creation |
BUGSINK_ENABLED |
Enable/disable error reporting | true |
BUGSINK_BASE_URL |
Bugsink web UI URL (internal) | http://localhost:8000 |
PostgreSQL Setup
-- Create dedicated Bugsink database and user
CREATE USER bugsink WITH PASSWORD 'bugsink_dev_password';
CREATE DATABASE bugsink OWNER bugsink;
GRANT ALL PRIVILEGES ON DATABASE bugsink TO bugsink;
Bugsink Configuration
# Environment variables for Bugsink service
SECRET_KEY=<random-50-char-string>
DATABASE_URL=postgresql://bugsink:bugsink_dev_password@localhost:5432/bugsink
BASE_URL=http://localhost:8000
PORT=8000
Logstash Pipeline
# /etc/logstash/conf.d/bugsink.conf
# === INPUTS ===
input {
# Pino application logs
file {
path => "/app/logs/*.log"
codec => json
type => "pino"
tags => ["app"]
}
# Redis logs
file {
path => "/var/log/redis/*.log"
type => "redis"
tags => ["redis"]
}
# PostgreSQL logs (for function logging - future)
# file {
# path => "/var/log/postgresql/*.log"
# type => "postgres"
# tags => ["postgres"]
# }
}
# === FILTERS ===
filter {
# Pino error detection (level 50 = error, 60 = fatal)
if [type] == "pino" and [level] >= 50 {
mutate { add_tag => ["error"] }
}
# Redis error detection
if [type] == "redis" {
grok {
match => { "message" => "%{POSINT:pid}:%{WORD:role} %{MONTHDAY} %{MONTH} %{TIME} %{WORD:loglevel} %{GREEDYDATA:redis_message}" }
}
if [loglevel] in ["WARNING", "ERROR"] {
mutate { add_tag => ["error"] }
}
}
# PostgreSQL function error detection (future)
# if [type] == "postgres" {
# # Parse PostgreSQL log format and detect ERROR/FATAL levels
# }
}
# === OUTPUT ===
output {
if "error" in [tags] {
http {
url => "http://localhost:8000/api/store/"
http_method => "post"
format => "json"
# Sentry envelope format
}
}
}
Implementation Steps
-
Update Dockerfile.dev:
- Install Bugsink (pip package or binary)
- Install Logstash (Elastic APT repository)
- Add systemd service files for both
-
PostgreSQL initialization:
- Add Bugsink user/database creation to
sql/00-init-extensions.sql
- Add Bugsink user/database creation to
-
Backend SDK integration:
- Install
@sentry/node - Initialize in
server.tsbefore Express app - Configure error handler middleware integration
- Install
-
Frontend SDK integration:
- Install
@sentry/react - Wrap
Appcomponent withSentry.ErrorBoundary - Configure in
src/index.tsx
- Install
-
Environment configuration:
- Add Bugsink variables to
src/config/env.ts - Update
.env.exampleandcompose.dev.yml
- Add Bugsink variables to
-
Logstash configuration:
- Create pipeline config for Pino → Bugsink
- Configure Pino to write to log file in addition to stdout
- Configure Redis log monitoring (connection errors, slow commands)
-
MCP server documentation:
- Document
sentry-selfhosted-mcpsetup in CLAUDE.md
- Document
-
PostgreSQL function logging (future):
- Configure PostgreSQL to log function execution errors
- Add Logstash input for PostgreSQL logs
- Define filter rules for function-level error detection
- Note: Ask for implementation details when this step is reached
Consequences
Positive
- Full observability: Aggregated view of errors, trends, and performance
- Self-hosted: No external SaaS dependencies or subscription costs
- SDK compatibility: Leverages mature Sentry SDKs with excellent documentation
- AI integration: MCP server enables Claude Code to query and analyze errors
- Unified architecture: Same setup works in dev container and production
- Lightweight: Bugsink runs in a single process, unlike full Sentry (16GB+ RAM)
Negative
- Additional services: Bugsink and Logstash add complexity to the container
- PostgreSQL overhead: Additional database for error tracking
- Initial setup: Requires configuration of multiple components
- Logstash learning curve: Pipeline configuration requires Logstash knowledge
Alternatives Considered
- Full Sentry self-hosted: Rejected due to complexity (Kafka, Redis, ClickHouse, 16GB+ RAM minimum)
- GlitchTip: Considered, but Bugsink is lighter weight and easier to deploy
- Sentry SaaS: Rejected due to self-hosted requirement
- Custom error aggregation: Rejected in favor of proven Sentry SDK ecosystem