
Monitoring Setup
- 36 installs
- 8 repo stars
- Updated February 6, 2026
- hieutrtr/ai1-skills
Application monitoring and observability setup for Python/React: structured logging, Prometheus metrics, health checks, alerting, and Sentry.
About
Covers structlog structured logging, Prometheus metrics for FastAPI, health check endpoints, alert thresholds, Grafana dashboards, and Sentry error tracking. A developer uses it when configuring observability for a Python/React project.
- structlog logging and Prometheus metrics for FastAPI
- Alert threshold design, Grafana dashboards, and Sentry error tracking
Monitoring Setup by the numbers
- 36 all-time installs (skills.sh)
- Ranked #839 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hieutrtr/ai1-skills --skill monitoring-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 8 |
| Last updated | February 6, 2026 |
| Repository | hieutrtr/ai1-skills ↗ |
What it does
Application monitoring and observability setup for Python/React: structured logging, Prometheus metrics, health checks, alerting, and Sentry.
Files
Monitoring Setup
When to Use
Activate this skill when:
- Setting up structured logging for a Python/FastAPI application
- Configuring Prometheus metrics collection and custom counters/histograms
- Implementing health check endpoints (liveness and readiness)
- Designing alert rules and thresholds for production services
- Creating Grafana dashboards for service monitoring
- Integrating Sentry for error tracking and performance monitoring
- Implementing distributed tracing with OpenTelemetry
- Reviewing or improving existing observability coverage
Output: Write observability configuration summary to monitoring-config.md documenting what was set up (metrics, alerts, dashboards, health checks).
Do NOT use this skill for:
- Responding to active production incidents (use
incident-response) - Deploying monitoring infrastructure (use
deployment-pipeline) - Writing application business logic (use
python-backend-expert) - Docker container configuration (use
docker-best-practices)
Instructions
Four Pillars of Observability
Every production service must implement all four pillars.
┌─────────────────────────────────────────────────────────────────┐
│ OBSERVABILITY │
├────────────────┬───────────────┬──────────────┬────────────────┤
│ METRICS │ LOGGING │ TRACING │ ALERTING │
│ │ │ │ │
│ Prometheus │ structlog │ OpenTelemetry│ Alert rules │
│ counters, │ structured │ distributed │ thresholds, │
│ histograms, │ JSON logs, │ trace spans, │ notification │
│ gauges │ context │ correlation │ channels │
├────────────────┴───────────────┴──────────────┴────────────────┤
│ DASHBOARDS (Grafana) │
│ Visualize metrics, logs, and traces in one place │
└─────────────────────────────────────────────────────────────────┘Pillar 1: Metrics (Prometheus)
Use the RED method for request-driven services and USE method for resources.
RED Method (for every API endpoint):
- Rate -- Requests per second
- Errors -- Failed requests per second
- Duration -- Request latency distribution
USE Method (for infrastructure resources):
- Utilization -- Percentage of resource used (CPU, memory, disk)
- Saturation -- Work queued or waiting (connection pool, queue depth)
- Errors -- Error events (OOM kills, connection failures)
Key metrics to instrument:
from prometheus_client import Counter, Histogram, Gauge, Info
# RED metrics
REQUEST_COUNT = Counter(
"http_requests_total",
"Total HTTP requests",
labelnames=["method", "endpoint", "status_code"],
)
REQUEST_DURATION = Histogram(
"http_request_duration_seconds",
"HTTP request duration in seconds",
labelnames=["method", "endpoint"],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
)
# USE metrics
DB_POOL_USAGE = Gauge(
"db_connection_pool_usage",
"Database connection pool utilization",
labelnames=["pool_name"],
)
DB_POOL_SIZE = Gauge(
"db_connection_pool_size",
"Database connection pool max size",
labelnames=["pool_name"],
)
REDIS_CONNECTIONS = Gauge(
"redis_active_connections",
"Active Redis connections",
)
# Business metrics
ACTIVE_USERS = Gauge(
"active_users_total",
"Currently active users",
)
APP_INFO = Info(
"app",
"Application metadata",
)FastAPI middleware for automatic metrics:
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class PrometheusMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
method = request.method
endpoint = request.url.path
start_time = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start_time
status_code = str(response.status_code)
REQUEST_COUNT.labels(
method=method, endpoint=endpoint, status_code=status_code
).inc()
REQUEST_DURATION.labels(
method=method, endpoint=endpoint
).observe(duration)
return responseSee references/metrics-config-template.py for the complete setup.
Pillar 2: Logging (structlog)
Use structured JSON logging with contextual information. Never use print() or unstructured logging in production.
Logging principles: 1. Structured -- JSON format, machine-parseable 2. Contextual -- Include request ID, user ID, trace ID in every log 3. Leveled -- Use appropriate log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) 4. Actionable -- Every WARNING/ERROR log should indicate what to investigate
Log levels and when to use them:
| Level | When to Use | Example |
|---|---|---|
| DEBUG | Detailed diagnostic info, disabled in production | Processing item 42 of 100 |
| INFO | Normal operations, significant events | User created, Payment processed |
| WARNING | Unexpected but handled situation | Retry attempt 2 of 3, Cache miss |
| ERROR | Operation failed, needs attention | Database query failed, External API timeout |
| CRITICAL | System-level failure, immediate action | Cannot connect to database, Out of memory |
structlog setup:
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)Adding request context:
from starlette.middleware.base import BaseHTTPMiddleware
import structlog
import uuid
class LoggingContextMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(
request_id=request_id,
method=request.method,
path=request.url.path,
)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return responseSee references/logging-config-template.py for the complete setup.
Pillar 3: Tracing (OpenTelemetry)
Distributed tracing connects logs and metrics across service boundaries.
Trace setup for FastAPI:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
def setup_tracing(app, service_name: str = "backend"):
resource = Resource.create({"service.name": service_name})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Auto-instrument FastAPI, SQLAlchemy, Redis
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument()
RedisInstrumentor().instrument()Custom spans for business logic:
tracer = trace.get_tracer(__name__)
async def process_order(order_id: str):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
with tracer.start_as_current_span("validate_order"):
await validate_order(order_id)
with tracer.start_as_current_span("charge_payment"):
result = await charge_payment(order_id)
span.set_attribute("payment.status", result.status)
with tracer.start_as_current_span("send_confirmation"):
await send_confirmation(order_id)Pillar 4: Alerting
Alerts must be actionable. Every alert should indicate what is broken and what to do.
Alert design principles: 1. Page only for user-impacting issues -- Do not page for non-urgent warnings 2. Set thresholds based on SLOs -- Not arbitrary numbers 3. Avoid alert fatigue -- If an alert fires often without action, fix or remove it 4. Include runbook links -- Every alert should link to a remediation guide 5. Use multi-window burn rates -- Detect issues faster without false positives
Alert thresholds for a typical FastAPI application:
| Alert | Condition | Severity | Action |
|---|---|---|---|
| High error rate | http_requests_total{status=~"5.."} > 5% of total for 5 min | SEV2 | Check logs, consider rollback |
| High latency | http_request_duration_seconds p99 > 2s for 5 min | SEV3 | Check DB queries, dependencies |
| Service down | Health check fails for 2 min | SEV1 | Restart, check logs, escalate |
| DB connections high | Pool usage > 80% for 5 min | SEV3 | Check for connection leaks |
| DB connections critical | Pool usage > 95% for 2 min | SEV2 | Restart app, investigate |
| Memory high | Container memory > 85% for 10 min | SEV3 | Check for memory leaks |
| Disk space low | Disk usage > 85% | SEV3 | Clean logs, expand volume |
| Certificate expiry | SSL cert expires in < 14 days | SEV4 | Renew certificate |
See references/alert-rules-template.yml for Prometheus alerting rules.
Health Check Endpoints
Every service must expose two health endpoints.
Liveness (`/health`): Is the process running? Returns 200 if the application is alive.
Readiness (`/health/ready`): Can the service handle requests? Checks all dependencies.
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from datetime import datetime, timezone
router = APIRouter(tags=["health"])
@router.get("/health")
async def liveness():
"""Liveness probe -- is the process running?"""
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"version": settings.APP_VERSION,
}
@router.get("/health/ready")
async def readiness(db: AsyncSession = Depends(get_db)):
"""Readiness probe -- can we handle traffic?"""
checks = {}
# Check database
try:
await db.execute(text("SELECT 1"))
checks["database"] = {"status": "ok", "latency_ms": 0}
except Exception as e:
checks["database"] = {"status": "error", "error": str(e)}
# Check Redis
try:
start = time.perf_counter()
await redis.ping()
latency = (time.perf_counter() - start) * 1000
checks["redis"] = {"status": "ok", "latency_ms": round(latency, 2)}
except Exception as e:
checks["redis"] = {"status": "error", "error": str(e)}
all_ok = all(c["status"] == "ok" for c in checks.values())
return JSONResponse(
status_code=200 if all_ok else 503,
content={
"status": "ready" if all_ok else "not_ready",
"checks": checks,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)Error Tracking with Sentry
Sentry captures unhandled exceptions and performance data.
Setup:
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
sentry_sdk.init(
dsn=settings.SENTRY_DSN,
environment=settings.APP_ENV,
release=settings.APP_VERSION,
traces_sample_rate=0.1, # 10% of requests for performance monitoring
profiles_sample_rate=0.1,
integrations=[
FastApiIntegration(),
SqlalchemyIntegration(),
],
# Do not send PII
send_default_pii=False,
# Filter out health check noise
before_send=filter_health_checks,
)
def filter_health_checks(event, hint):
"""Do not send health check errors to Sentry."""
if "request" in event and event["request"].get("url", "").endswith("/health"):
return None
return eventDashboard Design
Grafana dashboards should follow a consistent layout pattern.
Standard dashboard sections: 1. Overview row -- Key SLIs at a glance (error rate, latency, throughput) 2. RED metrics row -- Rate, Errors, Duration for each endpoint 3. Infrastructure row -- CPU, memory, disk, network 4. Dependencies row -- Database, Redis, external API health 5. Business metrics row -- Application-specific KPIs
Dashboard layout:
┌─────────────────────────────────────────────────────────┐
│ Service Overview │
│ [Error Rate %] [p99 Latency] [Requests/s] [Uptime] │
├────────────────────────┬────────────────────────────────┤
│ Request Rate │ Error Rate │
│ (by endpoint) │ (by endpoint, status code) │
├────────────────────────┼────────────────────────────────┤
│ Latency (p50/p95/p99) │ Active Connections │
│ (by endpoint) │ (DB pool, Redis) │
├────────────────────────┴────────────────────────────────┤
│ Infrastructure │
│ [CPU %] [Memory %] [Disk %] [Network IO] │
├─────────────────────────────────────────────────────────┤
│ Dependencies │
│ [DB Latency] [Redis Latency] [External API Status] │
└─────────────────────────────────────────────────────────┘See references/dashboard-template.json for a complete Grafana dashboard template.
Uptime Monitoring
External uptime monitoring validates the service from a user's perspective.
What to monitor externally:
/healthendpoint from multiple geographic regions- Key user-facing pages (login, dashboard, API docs)
- SSL certificate validity and expiration
- DNS resolution time
Recommended check intervals:
| Check | Interval | Timeout | Regions |
|---|---|---|---|
| Health endpoint | 30 seconds | 10 seconds | 3+ regions |
| Key pages | 1 minute | 15 seconds | 2+ regions |
| SSL certificate | 6 hours | 30 seconds | 1 region |
| DNS resolution | 5 minutes | 5 seconds | 3+ regions |
Quick Reference
See references/ for complete templates: logging-config-template.py, metrics-config-template.py, alert-rules-template.yml, dashboard-template.json.
Monitoring Checklist for New Services
- [ ] structlog configured with JSON output
- [ ] Request logging middleware with request ID correlation
- [ ] Prometheus metrics endpoint exposed at
/metrics - [ ] RED metrics instrumented (request count, errors, duration)
- [ ] Health check endpoints implemented (
/health,/health/ready) - [ ] Sentry SDK initialized with environment and release tags
- [ ] Alert rules defined for error rate, latency, and availability
- [ ] Grafana dashboard created with standard sections
- [ ] External uptime monitoring configured
- [ ] Log retention policy defined (default: 30 days)
Output File
Write monitoring configuration summary to monitoring-config.md:
# Monitoring Configuration: [Service Name]
## Metrics
| Metric | Type | Labels | Purpose |
|--------|------|--------|---------|
| http_requests_total | Counter | method, endpoint, status | RED: Request rate |
| http_request_duration_seconds | Histogram | method, endpoint | RED: Latency |
## Alerts
| Alert | Condition | Severity | Runbook |
|-------|-----------|----------|---------|
| HighErrorRate | error_rate > 5% for 5m | SEV2 | docs/runbooks/high-error-rate.md |
## Health Checks
- `/health` — Liveness probe
- `/health/ready` — Readiness probe (checks DB, Redis)
## Dashboards
- Grafana: Service Overview (imported from references/dashboard-template.json)
## Next Steps
- Run `/deployment-pipeline` to deploy with monitoring enabled
- Run `/incident-response` if alerts fire# =============================================================================
# Prometheus Alert Rules Template
# =============================================================================
# Alert rules for a FastAPI + PostgreSQL + Redis application stack.
# Copy to your Prometheus configuration and adjust thresholds as needed.
#
# Severity labels:
# critical: Page immediately (SEV1/SEV2)
# warning: Notify via Slack (SEV3)
# info: Log, no notification (SEV4)
#
# Each alert includes:
# - description: What is happening
# - runbook_url: Link to remediation steps
# - dashboard_url: Link to relevant Grafana dashboard
# =============================================================================
groups:
# ─── Application Health ──────────────────────────────────────────────────
- name: application_health
rules:
# Service is completely down
- alert: ServiceDown
expr: up{job="backend"} == 0
for: 2m
labels:
severity: critical
service: backend
annotations:
summary: "Backend service is down"
description: >-
The backend service has been unreachable for more than 2 minutes.
Health check endpoint is not responding.
runbook_url: "https://wiki.example.com/runbooks/service-down"
dashboard_url: "https://grafana.example.com/d/service-overview"
# Health check endpoint returning errors
- alert: HealthCheckFailing
expr: probe_success{job="blackbox", target=~".*health.*"} == 0
for: 3m
labels:
severity: critical
service: backend
annotations:
summary: "Health check endpoint failing"
description: >-
Health check at {{ $labels.target }} has been failing
for more than 3 minutes.
runbook_url: "https://wiki.example.com/runbooks/health-check-failure"
# ─── HTTP Request Metrics (RED) ──────────────────────────────────────────
- name: http_requests
rules:
# High error rate (5xx errors)
- alert: HighErrorRate
expr: >-
(
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
) > 0.05
for: 5m
labels:
severity: critical
service: backend
annotations:
summary: "High HTTP error rate (> 5%)"
description: >-
More than 5% of HTTP requests are returning 5xx errors
over the last 5 minutes. Current error rate: {{ $value | humanizePercentage }}.
runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
dashboard_url: "https://grafana.example.com/d/http-metrics"
# Elevated error rate (warning threshold)
- alert: ElevatedErrorRate
expr: >-
(
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
) > 0.01
for: 10m
labels:
severity: warning
service: backend
annotations:
summary: "Elevated HTTP error rate (> 1%)"
description: >-
More than 1% of HTTP requests are returning 5xx errors
over the last 10 minutes. Current error rate: {{ $value | humanizePercentage }}.
runbook_url: "https://wiki.example.com/runbooks/elevated-error-rate"
# High latency (p99)
- alert: HighLatencyP99
expr: >-
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
> 2.0
for: 5m
labels:
severity: warning
service: backend
annotations:
summary: "High p99 latency (> 2 seconds)"
description: >-
The 99th percentile request latency has exceeded 2 seconds
for the last 5 minutes. Current p99: {{ $value | humanizeDuration }}.
runbook_url: "https://wiki.example.com/runbooks/high-latency"
dashboard_url: "https://grafana.example.com/d/http-metrics"
# Very high latency (p99) - critical
- alert: CriticalLatencyP99
expr: >-
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
> 5.0
for: 3m
labels:
severity: critical
service: backend
annotations:
summary: "Critical p99 latency (> 5 seconds)"
description: >-
The 99th percentile request latency has exceeded 5 seconds.
This likely indicates a systemic issue.
runbook_url: "https://wiki.example.com/runbooks/critical-latency"
# Low request rate (potential outage indicator)
- alert: LowRequestRate
expr: >-
sum(rate(http_requests_total[5m])) < 0.1
for: 10m
labels:
severity: warning
service: backend
annotations:
summary: "Unusually low request rate"
description: >-
Request rate has dropped below 0.1 req/s for 10 minutes.
This may indicate a routing issue or upstream failure.
runbook_url: "https://wiki.example.com/runbooks/low-traffic"
# ─── Database Metrics ───────────────────────────────────────────────────
- name: database
rules:
# Connection pool nearly exhausted
- alert: DBConnectionPoolHigh
expr: >-
(db_connection_pool_checked_out / db_connection_pool_size) > 0.8
for: 5m
labels:
severity: warning
service: database
annotations:
summary: "Database connection pool usage > 80%"
description: >-
The database connection pool is {{ $value | humanizePercentage }} utilized.
Pool exhaustion will cause request failures.
runbook_url: "https://wiki.example.com/runbooks/db-pool-high"
# Connection pool critical
- alert: DBConnectionPoolCritical
expr: >-
(db_connection_pool_checked_out / db_connection_pool_size) > 0.95
for: 2m
labels:
severity: critical
service: database
annotations:
summary: "Database connection pool usage > 95%"
description: >-
Database connection pool is nearly exhausted.
New requests will start failing.
runbook_url: "https://wiki.example.com/runbooks/db-pool-critical"
# Database not reachable
- alert: DatabaseUnreachable
expr: pg_up == 0
for: 1m
labels:
severity: critical
service: database
annotations:
summary: "PostgreSQL database is unreachable"
description: >-
Cannot connect to the PostgreSQL database. All database-dependent
operations will fail.
runbook_url: "https://wiki.example.com/runbooks/db-unreachable"
# ─── Redis Metrics ──────────────────────────────────────────────────────
- name: redis
rules:
# Redis not reachable
- alert: RedisUnreachable
expr: redis_up == 0
for: 1m
labels:
severity: critical
service: redis
annotations:
summary: "Redis is unreachable"
description: >-
Cannot connect to Redis. Caching and session management
will be affected.
runbook_url: "https://wiki.example.com/runbooks/redis-unreachable"
# Redis memory high
- alert: RedisMemoryHigh
expr: >-
redis_memory_used_bytes / redis_memory_max_bytes > 0.85
for: 10m
labels:
severity: warning
service: redis
annotations:
summary: "Redis memory usage > 85%"
description: >-
Redis is using {{ $value | humanizePercentage }} of available memory.
Evictions may start occurring.
runbook_url: "https://wiki.example.com/runbooks/redis-memory-high"
# ─── Infrastructure Metrics ─────────────────────────────────────────────
- name: infrastructure
rules:
# High CPU usage
- alert: HighCPUUsage
expr: >-
(
rate(container_cpu_usage_seconds_total{name=~"app-.*"}[5m])
/
container_spec_cpu_quota{name=~"app-.*"}
* container_spec_cpu_period{name=~"app-.*"}
) > 0.85
for: 10m
labels:
severity: warning
service: infrastructure
annotations:
summary: "High CPU usage on {{ $labels.name }}"
description: >-
Container {{ $labels.name }} CPU usage is above 85%
for the last 10 minutes.
runbook_url: "https://wiki.example.com/runbooks/high-cpu"
# High memory usage
- alert: HighMemoryUsage
expr: >-
container_memory_usage_bytes{name=~"app-.*"}
/
container_spec_memory_limit_bytes{name=~"app-.*"}
> 0.85
for: 10m
labels:
severity: warning
service: infrastructure
annotations:
summary: "High memory usage on {{ $labels.name }}"
description: >-
Container {{ $labels.name }} memory usage is above 85%.
Risk of OOM kill.
runbook_url: "https://wiki.example.com/runbooks/high-memory"
# Disk space low
- alert: DiskSpaceLow
expr: >-
(node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})
< 0.15
for: 10m
labels:
severity: warning
service: infrastructure
annotations:
summary: "Disk space low (< 15% remaining)"
description: >-
Root filesystem has less than 15% free space.
Clean up logs or expand the volume.
runbook_url: "https://wiki.example.com/runbooks/disk-space-low"
# SSL certificate expiring soon
- alert: SSLCertificateExpiringSoon
expr: >-
probe_ssl_earliest_cert_expiry - time() < 14 * 24 * 3600
for: 1h
labels:
severity: warning
service: infrastructure
annotations:
summary: "SSL certificate expires in less than 14 days"
description: >-
SSL certificate for {{ $labels.target }} expires
in {{ $value | humanizeDuration }}. Renew immediately.
runbook_url: "https://wiki.example.com/runbooks/ssl-renewal"
{
"__doc__": "Grafana Dashboard Template -- Import via Grafana UI: Dashboards > Import > Upload JSON",
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
},
{
"datasource": "Prometheus",
"enable": true,
"expr": "changes(process_start_time_seconds{job=\"backend\"}[1m]) > 0",
"iconColor": "red",
"name": "Service Restarts",
"titleFormat": "Service Restart"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"__section__": "Overview Row",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0},
"id": 1,
"title": "Service Overview",
"type": "row"
},
{
"title": "Error Rate",
"description": "Percentage of 5xx responses over total requests",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 1},
"id": 2,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total{status_code=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m])) * 100",
"legendFormat": "Error Rate %"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 1},
{"color": "red", "value": 5}
]
}
}
}
},
{
"title": "p99 Latency",
"description": "99th percentile request duration",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 1},
"id": 3,
"datasource": "Prometheus",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p99"
}
],
"fieldConfig": {
"defaults": {
"unit": "s",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 1},
{"color": "red", "value": 2}
]
}
}
}
},
{
"title": "Request Rate",
"description": "Requests per second",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 1},
"id": 4,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m]))",
"legendFormat": "req/s"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 1},
{"color": "green", "value": 10}
]
}
}
}
},
{
"title": "Active Requests",
"description": "HTTP requests currently being processed",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1},
"id": 5,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(http_requests_in_progress)",
"legendFormat": "In Progress"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 50},
{"color": "red", "value": 100}
]
}
}
}
},
{
"__section__": "RED Metrics Row",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5},
"id": 10,
"title": "RED Metrics",
"type": "row"
},
{
"title": "Request Rate by Endpoint",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 6},
"id": 11,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (endpoint)",
"legendFormat": "{{endpoint}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10
}
}
}
},
{
"title": "Error Rate by Endpoint",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 6},
"id": 12,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total{status_code=~\"5..\"}[5m])) by (endpoint)",
"legendFormat": "{{endpoint}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"drawStyle": "bars",
"fillOpacity": 50
}
}
}
},
{
"title": "Request Duration (p50 / p95 / p99)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 14},
"id": 13,
"datasource": "Prometheus",
"targets": [
{
"expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p50"
},
{
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p95"
},
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p99"
}
],
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 5
}
}
}
},
{
"title": "Response Status Codes",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 14},
"id": 14,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (status_code)",
"legendFormat": "{{status_code}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"drawStyle": "bars",
"stacking": {"mode": "normal"},
"fillOpacity": 80
}
}
}
},
{
"__section__": "Infrastructure Row",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 22},
"id": 20,
"title": "Infrastructure",
"type": "row"
},
{
"title": "CPU Usage by Container",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 23},
"id": 21,
"datasource": "Prometheus",
"targets": [
{
"expr": "rate(container_cpu_usage_seconds_total{name=~\"app-.*\"}[5m]) * 100",
"legendFormat": "{{name}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"max": 100,
"custom": {
"drawStyle": "line",
"fillOpacity": 20
}
}
}
},
{
"title": "Memory Usage by Container",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 23},
"id": 22,
"datasource": "Prometheus",
"targets": [
{
"expr": "container_memory_usage_bytes{name=~\"app-.*\"}",
"legendFormat": "{{name}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"fillOpacity": 20
}
}
}
},
{
"__section__": "Dependencies Row",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 31},
"id": 30,
"title": "Dependencies",
"type": "row"
},
{
"title": "DB Connection Pool",
"type": "timeseries",
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 32},
"id": 31,
"datasource": "Prometheus",
"targets": [
{
"expr": "db_connection_pool_checked_out",
"legendFormat": "In Use"
},
{
"expr": "db_connection_pool_checked_in",
"legendFormat": "Available"
},
{
"expr": "db_connection_pool_size",
"legendFormat": "Max Size"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10
}
}
}
},
{
"title": "Redis Memory Usage",
"type": "gauge",
"gridPos": {"h": 8, "w": 8, "x": 8, "y": 32},
"id": 32,
"datasource": "Prometheus",
"targets": [
{
"expr": "redis_memory_used_bytes / redis_memory_max_bytes * 100",
"legendFormat": "Memory %"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 70},
{"color": "red", "value": 85}
]
}
}
}
},
{
"title": "Cache Hit Rate",
"type": "timeseries",
"gridPos": {"h": 8, "w": 8, "x": 16, "y": 32},
"id": 33,
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(cache_hits_total[5m])) / (sum(rate(cache_hits_total[5m])) + sum(rate(cache_misses_total[5m]))) * 100",
"legendFormat": "Hit Rate %"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"fillOpacity": 20
}
}
}
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": ["backend", "fastapi", "monitoring"],
"templating": {
"list": [
{
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
"hide": 0,
"includeAll": false,
"label": "Datasource",
"name": "datasource",
"type": "datasource",
"query": "prometheus"
}
]
},
"time": {"from": "now-1h", "to": "now"},
"timepicker": {},
"timezone": "utc",
"title": "Backend Service Dashboard",
"uid": "backend-service-overview",
"version": 1
}
"""
logging-config-template.py -- structlog setup for FastAPI applications.
Configures structured JSON logging with request context, correlation IDs,
and proper log level handling. Drop this into your FastAPI project and call
setup_logging() during application startup.
Usage:
from logging_config import setup_logging
setup_logging(log_level="INFO", json_format=True)
"""
import logging
import sys
import uuid
from typing import Any
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
def setup_logging(
log_level: str = "INFO",
json_format: bool = True,
service_name: str = "backend",
) -> None:
"""
Configure structured logging for the application.
Args:
log_level: Minimum log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
json_format: If True, output JSON logs. If False, output colored console logs.
service_name: Name of the service (included in every log entry).
"""
# Choose renderer based on format preference
if json_format:
renderer = structlog.processors.JSONRenderer()
else:
renderer = structlog.dev.ConsoleRenderer(colors=True)
# Shared processors for both structlog and stdlib loggers
shared_processors: list[Any] = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.ExtraAdder(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
_add_service_name(service_name),
]
# Configure structlog
structlog.configure(
processors=[
*shared_processors,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
# Configure stdlib logging to use structlog formatting
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
renderer,
],
foreign_pre_chain=shared_processors,
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
# Configure root logger
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.addHandler(handler)
root_logger.setLevel(getattr(logging, log_level.upper()))
# Quiet noisy third-party loggers
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
def _add_service_name(service_name: str):
"""Create a processor that adds the service name to every log entry."""
def processor(logger, method_name, event_dict):
event_dict["service"] = service_name
return event_dict
return processor
class LoggingContextMiddleware(BaseHTTPMiddleware):
"""
Middleware that adds request context to all log entries.
Binds request_id, method, path, and client_ip to structlog context
variables so they appear in every log entry during the request lifecycle.
"""
async def dispatch(self, request: Request, call_next) -> Response:
# Get or generate request ID
request_id = request.headers.get(
"X-Request-ID", str(uuid.uuid4())
)
# Clear and bind context variables for this request
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(
request_id=request_id,
method=request.method,
path=request.url.path,
client_ip=request.client.host if request.client else "unknown",
)
logger = structlog.get_logger()
# Log request start
logger.info(
"request_started",
query_params=str(request.query_params) if request.query_params else None,
)
try:
response = await call_next(request)
except Exception as exc:
logger.error(
"request_failed",
error=str(exc),
error_type=type(exc).__name__,
)
raise
# Add request ID to response headers for correlation
response.headers["X-Request-ID"] = request_id
# Log request completion
logger.info(
"request_completed",
status_code=response.status_code,
)
return response
# ---------------------------------------------------------------------------
# Usage example (in main.py):
# ---------------------------------------------------------------------------
#
# from fastapi import FastAPI
# from logging_config import setup_logging, LoggingContextMiddleware
#
# app = FastAPI()
#
# # Initialize logging
# setup_logging(
# log_level=os.getenv("LOG_LEVEL", "INFO"),
# json_format=os.getenv("APP_ENV") != "development",
# service_name="my-backend",
# )
#
# # Add logging context middleware
# app.add_middleware(LoggingContextMiddleware)
#
# # Use structlog throughout the application
# import structlog
# logger = structlog.get_logger()
#
# @app.get("/users/{user_id}")
# async def get_user(user_id: int):
# logger.info("fetching_user", user_id=user_id)
# user = await user_service.get(user_id)
# if not user:
# logger.warning("user_not_found", user_id=user_id)
# raise HTTPException(404, "User not found")
# return user
#
# ---------------------------------------------------------------------------
# Example log output (JSON format):
# ---------------------------------------------------------------------------
#
# {
# "request_id": "abc-123-def",
# "method": "GET",
# "path": "/users/42",
# "client_ip": "10.0.0.1",
# "service": "my-backend",
# "event": "fetching_user",
# "user_id": 42,
# "level": "info",
# "timestamp": "2024-01-15T14:30:00.123456Z",
# "logger": "my_module"
# }
"""
metrics-config-template.py -- Prometheus metrics setup for FastAPI applications.
Configures Prometheus client metrics with RED method (Rate, Errors, Duration)
and USE method (Utilization, Saturation, Errors) metrics. Includes middleware
for automatic HTTP request instrumentation.
Usage:
from metrics_config import setup_metrics
setup_metrics(app)
"""
import time
from typing import Callable
from fastapi import FastAPI, Request, Response
from prometheus_client import (
Counter,
Gauge,
Histogram,
Info,
generate_latest,
CONTENT_TYPE_LATEST,
CollectorRegistry,
REGISTRY,
)
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response as StarletteResponse
# ---------------------------------------------------------------------------
# RED Metrics (Request-driven)
# ---------------------------------------------------------------------------
# Rate: Total number of requests
HTTP_REQUEST_TOTAL = Counter(
"http_requests_total",
"Total number of HTTP requests",
labelnames=["method", "endpoint", "status_code"],
)
# Duration: Request latency distribution
HTTP_REQUEST_DURATION = Histogram(
"http_request_duration_seconds",
"HTTP request duration in seconds",
labelnames=["method", "endpoint"],
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
)
# Errors: Specifically track 5xx errors
HTTP_SERVER_ERRORS = Counter(
"http_server_errors_total",
"Total number of HTTP 5xx server errors",
labelnames=["method", "endpoint", "status_code"],
)
# Request size
HTTP_REQUEST_SIZE = Histogram(
"http_request_size_bytes",
"HTTP request body size in bytes",
labelnames=["method", "endpoint"],
buckets=[100, 1000, 10000, 100000, 1000000],
)
# Response size
HTTP_RESPONSE_SIZE = Histogram(
"http_response_size_bytes",
"HTTP response body size in bytes",
labelnames=["method", "endpoint"],
buckets=[100, 1000, 10000, 100000, 1000000],
)
# Requests in progress
HTTP_REQUESTS_IN_PROGRESS = Gauge(
"http_requests_in_progress",
"Number of HTTP requests currently being processed",
labelnames=["method"],
)
# ---------------------------------------------------------------------------
# USE Metrics (Resource-driven)
# ---------------------------------------------------------------------------
# Database connection pool
DB_POOL_SIZE = Gauge(
"db_connection_pool_size",
"Maximum size of the database connection pool",
labelnames=["pool_name"],
)
DB_POOL_CHECKED_IN = Gauge(
"db_connection_pool_checked_in",
"Number of connections currently available in the pool",
labelnames=["pool_name"],
)
DB_POOL_CHECKED_OUT = Gauge(
"db_connection_pool_checked_out",
"Number of connections currently in use from the pool",
labelnames=["pool_name"],
)
DB_POOL_OVERFLOW = Gauge(
"db_connection_pool_overflow",
"Number of connections in overflow beyond pool size",
labelnames=["pool_name"],
)
# Redis connections
REDIS_CONNECTIONS_ACTIVE = Gauge(
"redis_connections_active",
"Number of active Redis connections",
)
# Cache metrics
CACHE_HITS = Counter(
"cache_hits_total",
"Total number of cache hits",
labelnames=["cache_name"],
)
CACHE_MISSES = Counter(
"cache_misses_total",
"Total number of cache misses",
labelnames=["cache_name"],
)
# ---------------------------------------------------------------------------
# Business Metrics
# ---------------------------------------------------------------------------
ACTIVE_USERS = Gauge(
"active_users_total",
"Number of currently active users",
)
BACKGROUND_TASKS_QUEUED = Gauge(
"background_tasks_queued",
"Number of background tasks waiting to be processed",
labelnames=["task_type"],
)
BACKGROUND_TASKS_PROCESSED = Counter(
"background_tasks_processed_total",
"Total number of background tasks processed",
labelnames=["task_type", "status"],
)
# Application info
APP_INFO = Info(
"app",
"Application metadata",
)
# ---------------------------------------------------------------------------
# Prometheus Middleware
# ---------------------------------------------------------------------------
class PrometheusMiddleware(BaseHTTPMiddleware):
"""
Middleware that automatically instruments HTTP requests with Prometheus metrics.
Records request count, duration, errors, and in-progress gauges for every
HTTP request.
"""
# Endpoints to exclude from metrics (avoid noise from internal endpoints)
EXCLUDED_PATHS = {"/metrics", "/health", "/health/ready"}
async def dispatch(self, request: Request, call_next: Callable) -> Response:
method = request.method
path = request.url.path
# Skip metrics for excluded paths
if path in self.EXCLUDED_PATHS:
return await call_next(request)
# Normalize path to avoid high-cardinality labels
# e.g., /users/123 -> /users/{id}
endpoint = self._normalize_path(path)
# Track in-progress requests
HTTP_REQUESTS_IN_PROGRESS.labels(method=method).inc()
# Record request size
content_length = request.headers.get("content-length")
if content_length:
HTTP_REQUEST_SIZE.labels(
method=method, endpoint=endpoint
).observe(int(content_length))
# Time the request
start_time = time.perf_counter()
try:
response = await call_next(request)
except Exception:
# Record unhandled exception as 500
HTTP_REQUEST_TOTAL.labels(
method=method, endpoint=endpoint, status_code="500"
).inc()
HTTP_SERVER_ERRORS.labels(
method=method, endpoint=endpoint, status_code="500"
).inc()
raise
finally:
duration = time.perf_counter() - start_time
HTTP_REQUEST_DURATION.labels(
method=method, endpoint=endpoint
).observe(duration)
HTTP_REQUESTS_IN_PROGRESS.labels(method=method).dec()
status_code = str(response.status_code)
# Record request count
HTTP_REQUEST_TOTAL.labels(
method=method, endpoint=endpoint, status_code=status_code
).inc()
# Record server errors
if response.status_code >= 500:
HTTP_SERVER_ERRORS.labels(
method=method, endpoint=endpoint, status_code=status_code
).inc()
# Record response size
response_size = response.headers.get("content-length")
if response_size:
HTTP_RESPONSE_SIZE.labels(
method=method, endpoint=endpoint
).observe(int(response_size))
return response
@staticmethod
def _normalize_path(path: str) -> str:
"""
Normalize URL path to reduce label cardinality.
Replaces numeric path segments with {id} placeholder.
/users/123/posts/456 -> /users/{id}/posts/{id}
"""
parts = path.strip("/").split("/")
normalized = []
for part in parts:
if part.isdigit():
normalized.append("{id}")
else:
try:
# Check for UUID-like segments
if len(part) == 36 and part.count("-") == 4:
normalized.append("{id}")
else:
normalized.append(part)
except (ValueError, AttributeError):
normalized.append(part)
return "/" + "/".join(normalized)
# ---------------------------------------------------------------------------
# Setup Function
# ---------------------------------------------------------------------------
def setup_metrics(
app: FastAPI,
app_version: str = "unknown",
app_env: str = "development",
) -> None:
"""
Set up Prometheus metrics for a FastAPI application.
Adds the PrometheusMiddleware and creates the /metrics endpoint.
Args:
app: The FastAPI application instance.
app_version: Application version string.
app_env: Application environment (development, staging, production).
"""
# Set application info
APP_INFO.info({
"version": app_version,
"environment": app_env,
})
# Add metrics middleware
app.add_middleware(PrometheusMiddleware)
# Add /metrics endpoint
@app.get("/metrics", include_in_schema=False)
async def metrics():
"""Prometheus metrics endpoint."""
return StarletteResponse(
content=generate_latest(REGISTRY),
media_type=CONTENT_TYPE_LATEST,
)
# ---------------------------------------------------------------------------
# Database Pool Metrics Collector
# ---------------------------------------------------------------------------
def update_db_pool_metrics(engine, pool_name: str = "default") -> None:
"""
Update database connection pool metrics from SQLAlchemy engine.
Call this periodically or in a middleware to keep metrics current.
Args:
engine: SQLAlchemy engine instance.
pool_name: Label for the connection pool.
"""
pool = engine.pool
DB_POOL_SIZE.labels(pool_name=pool_name).set(pool.size())
DB_POOL_CHECKED_IN.labels(pool_name=pool_name).set(pool.checkedin())
DB_POOL_CHECKED_OUT.labels(pool_name=pool_name).set(pool.checkedout())
DB_POOL_OVERFLOW.labels(pool_name=pool_name).set(pool.overflow())
# ---------------------------------------------------------------------------
# Usage example (in main.py):
# ---------------------------------------------------------------------------
#
# from fastapi import FastAPI
# from metrics_config import setup_metrics
#
# app = FastAPI()
# setup_metrics(app, app_version="1.2.3", app_env="production")
#
# # Use metrics in your code:
# from metrics_config import CACHE_HITS, CACHE_MISSES
#
# async def get_cached_data(key: str):
# value = await redis.get(key)
# if value:
# CACHE_HITS.labels(cache_name="user_cache").inc()
# return value
# CACHE_MISSES.labels(cache_name="user_cache").inc()
# value = await fetch_from_db(key)
# await redis.set(key, value, ex=300)
# return value