# Monitoring Expert

> Expert-level observability and monitoring. Use when setting up Prometheus, Grafana, Datadog, OpenTelemetry, distributed tracing, structured logging, alerting, SLOs/SLAs, or diagnosing production incidents. Also use when the user mentions 'Prometheus', 'Grafana', 'OpenTelemetry', 'distributed tracing', 'SLO', 'alerting', 'metrics', 'logs', 'traces', or 'production incident'.

- Skill: `luokai0/monitoring-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add luokai0/monitoring-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/luokai0/monitoring-expert/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: luokai0 (https://skillmd.com/u/luokai0)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/luokai0/monitoring-expert

---


# Observability & Monitoring Expert

You are an expert in observability and monitoring with deep knowledge of the three pillars (metrics, logs, traces), Prometheus, Grafana, OpenTelemetry, and SLO-based alerting.

## Before Starting

1. **Stack** — Prometheus/Grafana, Datadog, New Relic, CloudWatch?
2. **Environment** — Kubernetes, Docker, bare metal, serverless?
3. **Language** — Node.js, Python, Go, Java?
4. **Problem type** — setting up monitoring, writing alerts, debugging incident, SLO design?
5. **Scale** — single service or distributed microservices?

---

## Core Expertise Areas

- **Metrics**: Prometheus (counters, gauges, histograms, summaries), PromQL, recording rules
- **Logging**: structured logging (JSON), log levels, correlation IDs, ELK/Loki stack
- **Tracing**: OpenTelemetry SDK, distributed tracing, Jaeger, Tempo, trace context propagation
- **Alerting**: alerting rules, alert routing (Alertmanager), PagerDuty/Slack integration
- **Dashboards**: Grafana panels, variables, annotations, dashboard-as-code
- **SLOs**: SLI definition, error budget calculation, burn rate alerts
- **Incident response**: runbooks, postmortems, on-call practices
- **Kubernetes monitoring**: kube-state-metrics, node-exporter, cAdvisor

---

## Key Patterns & Code

### The Three Pillars of Observability
```
Metrics  → What is happening? (aggregated numbers over time)
Logs     → Why is it happening? (detailed event records)
Traces   → Where is it happening? (request flow across services)

Golden Signals (Google SRE):
  Latency   → How long do requests take?
  Traffic   → How much demand is there?
  Errors    → What is the error rate?
  Saturation → How full is the service?

RED Method (for services):
  Rate      → Requests per second
  Errors    → Error rate
  Duration  → Latency distribution

USE Method (for resources):
  Utilization  → How busy is the resource?
  Saturation   → How much extra work is queued?
  Errors       → Error rate of the resource
```

### Prometheus Metrics — Node.js
```javascript
import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics } from 'prom-client';

const register = new Registry();

// Collect default Node.js metrics (CPU, memory, event loop lag)
collectDefaultMetrics({ register });

// Counter — only goes up (requests, errors, events)
const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// Histogram — distribution of values (latency, request size)
const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
  registers: [register],
});

// Gauge — can go up and down (active connections, queue size)
const activeConnections = new Gauge({
  name: 'active_connections',
  help: 'Number of active connections',
  registers: [register],
});

// Middleware to record metrics
export function metricsMiddleware(req, res, next) {
  const start = Date.now();
  const route = req.route?.path ?? req.path;

  activeConnections.inc();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const labels = {
      method: req.method,
      route,
      status_code: res.statusCode.toString(),
    };

    httpRequestsTotal.inc(labels);
    httpRequestDuration.observe(labels, duration);
    activeConnections.dec();
  });

  next();
}

// Expose /metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});
```

### Prometheus Metrics — Python (FastAPI)
```python
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Request, Response
import time

app = FastAPI()

# Metrics
REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency',
    ['method', 'endpoint'],
    buckets=[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
)

ACTIVE_REQUESTS = Gauge(
    'active_requests',
    'Number of active requests'
)

@app.middleware('http')
async def metrics_middleware(request: Request, call_next):
    ACTIVE_REQUESTS.inc()
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start

    REQUEST_COUNT.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()

    REQUEST_LATENCY.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)

    ACTIVE_REQUESTS.dec()
    return response

@app.get('/metrics')
async def metrics():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
```

### PromQL — Essential Queries
```promql
# ── Request Rate ──────────────────────────────────────────────────────────────
# Requests per second over last 5 minutes
rate(http_requests_total[5m])

# Error rate percentage
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m])) * 100

# ── Latency ───────────────────────────────────────────────────────────────────
# p50, p95, p99 latency
histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

# p99 latency per route
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route)
)

# ── Availability ──────────────────────────────────────────────────────────────
# Service availability (success rate)
sum(rate(http_requests_total{status_code!~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

# ── Resource Usage ────────────────────────────────────────────────────────────
# CPU usage by pod
sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (pod)

# Memory usage by pod
sum(container_memory_working_set_bytes{container!=""}) by (pod)

# ── Kubernetes ────────────────────────────────────────────────────────────────
# Pods not running
kube_pod_status_phase{phase!="Running",phase!="Succeeded"} == 1

# Node CPU utilization
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (node)

# Persistent volume usage
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes * 100
```

### Prometheus Alerting Rules
```yaml
# /etc/prometheus/rules/api-alerts.yml
groups:
  - name: api
    interval: 30s
    rules:
      # ── Availability ────────────────────────────────────────────────────
      - 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
          team: backend
        annotations:
          summary: "High error rate on {{ $labels.service }}"
          description: "Error rate is {{ $value | humanizePercentage }} (threshold: 5%)"
          runbook: "https://wiki.example.com/runbooks/high-error-rate"

      # ── Latency ─────────────────────────────────────────────────────────
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
          ) > 1.0
        for: 10m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "High p99 latency on {{ $labels.service }}"
          description: "p99 latency is {{ $value | humanizeDuration }} (threshold: 1s)"

      # ── Availability ────────────────────────────────────────────────────
      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
          page: "true"
        annotations:
          summary: "Service {{ $labels.job }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # ── Kubernetes ──────────────────────────────────────────────────────
      - alert: PodCrashLooping
        expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Pod {{ $labels.pod }} is crash looping"

      - alert: PodNotReady
        expr: kube_pod_status_ready{condition="true"} == 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Pod {{ $labels.pod }} not ready"

      # ── Recording Rules (pre-compute expensive queries) ─────────────────
  - name: recording_rules
    rules:
      - record: job:http_requests_total:rate5m
        expr: sum(rate(http_requests_total[5m])) by (job)

      - record: job:http_errors_total:rate5m
        expr: sum(rate(http_requests_total{status_code=~"5.."}[5m])) by (job)

      - record: job:http_request_duration_seconds:p99_5m
        expr: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job)
          )
```

### Alertmanager Configuration
```yaml
# /etc/alertmanager/alertmanager.yml
global:
  resolve_timeout: 5m
  slack_api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'

templates:
  - '/etc/alertmanager/templates/*.tmpl'

route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait:      30s   # wait before sending first notification
  group_interval:  5m    # wait before sending updated notification
  repeat_interval: 4h    # resend if still firing

  receiver: slack-warnings

  routes:
    # Critical alerts go to PagerDuty + Slack
    - match:
        severity: critical
      receiver: pagerduty-critical
      continue: true

    # Page-worthy alerts wake someone up
    - match:
        page: "true"
      receiver: pagerduty-critical

    # Team-specific routing
    - match:
        team: backend
      receiver: slack-backend

receivers:
  - name: slack-warnings
    slack_configs:
      - channel: '#alerts'
        title: '{{ template "slack.title" . }}'
        text: '{{ template "slack.text" . }}'
        send_resolved: true

  - name: slack-backend
    slack_configs:
      - channel: '#backend-alerts'
        send_resolved: true

  - name: pagerduty-critical
    pagerduty_configs:
      - service_key: 'your-pagerduty-integration-key'
        description: '{{ template "pagerduty.description" . }}'

inhibit_rules:
  # If service is down, suppress other alerts for that service
  - source_match:
      alertname: ServiceDown
    target_match_re:
      alertname: HighErrorRate|HighLatency
    equal: ['job']
```

### OpenTelemetry — Node.js Instrumentation
```javascript
// tracing.js — load BEFORE everything else
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'api',
    [SemanticResourceAttributes.SERVICE_VERSION]: process.env.VERSION ?? '1.0.0',
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV ?? 'production',
  }),

  traceExporter: new OTLPTraceExporter({
    url: 'http://otel-collector:4318/v1/traces',
  }),

  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://otel-collector:4318/v1/metrics',
    }),
    exportIntervalMillis: 15000,
  }),

  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-express': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
      '@opentelemetry/instrumentation-redis': { enabled: true },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => sdk.shutdown());
```

### Structured Logging
```javascript
// logger.js — structured JSON logging with Pino
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  base: {
    service: 'api',
    version: process.env.VERSION ?? '1.0.0',
    env: process.env.NODE_ENV ?? 'production',
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

// Request logger middleware
export function requestLogger(req, res, next) {
  const start = Date.now();
  const requestId = req.headers['x-request-id'] ?? crypto.randomUUID();

  req.log = logger.child({ requestId });
  res.setHeader('x-request-id', requestId);

  res.on('finish', () => {
    req.log.info({
      type: 'request',
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: Date.now() - start,
      userAgent: req.headers['user-agent'],
    }, 'Request completed');
  });

  next();
}

// Usage — always use structured fields not string interpolation
logger.info({ userId: user.id, action: 'login' }, 'User logged in');
logger.error({ err: error, userId: user.id }, 'Failed to process payment');

// Child logger for request context
const reqLogger = logger.child({ requestId, userId });
reqLogger.info({ orderId }, 'Order created');
```

### SLO Design & Error Budget
```
SLI (Service Level Indicator) — what to measure
  - Availability: % of successful requests
  - Latency: % of requests faster than threshold
  - Throughput: requests per second

SLO (Service Level Objective) — target
  - 99.9% of requests succeed (availability)
  - 99% of requests complete in < 200ms (latency)

Error Budget = 1 - SLO
  - 99.9% SLO = 0.1% error budget = 43.8 min/month downtime allowed

Burn Rate Alert — alert before budget is exhausted
  - 1x burn rate = consuming budget at exactly SLO rate
  - 2x burn rate = will exhaust budget in half the window
  - 14.4x burn rate = will exhaust monthly budget in 2 hours

Multi-window burn rate alerts (Google SRE recommendation):
  Fast burn (page now):  14.4x over 1h AND 14.4x over 5m
  Slow burn (ticket):    1x over 6h AND 1x over 30m
```
```yaml
# SLO-based alerting with Prometheus
groups:
  - name: slo_alerts
    rules:
      # Fast burn — page immediately (exhausts budget in 1 hour)
      - alert: SLOBurnRateFast
        expr: |
          (
            sum(rate(http_requests_total{status_code=~"5.."}[1h]))
            /
            sum(rate(http_requests_total[1h]))
          ) > 14.4 * 0.001
          and
          (
            sum(rate(http_requests_total{status_code=~"5.."}[5m]))
            /
            sum(rate(http_requests_total[5m]))
          ) > 14.4 * 0.001
        labels:
          severity: critical
          page: "true"
        annotations:
          summary: "SLO fast burn — error budget will be exhausted in ~1 hour"

      # Slow burn — create ticket
      - alert: SLOBurnRateSlow
        expr: |
          (
            sum(rate(http_requests_total{status_code=~"5.."}[6h]))
            /
            sum(rate(http_requests_total[6h]))
          ) > 6 * 0.001
        labels:
          severity: warning
        annotations:
          summary: "SLO slow burn — investigate before budget is exhausted"
```

### Grafana Dashboard as Code
```json
{
  "dashboard": {
    "title": "API Overview",
    "tags": ["api", "production"],
    "refresh": "30s",
    "panels": [
      {
        "title": "Request Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total[5m])) by (status_code)",
            "legendFormat": "{{status_code}}"
          }
        ]
      },
      {
        "title": "p99 Latency",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))",
            "legendFormat": "p99"
          }
        ]
      },
      {
        "title": "Error Rate %",
        "type": "stat",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{status_code=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m])) * 100"
          }
        ],
        "thresholds": {
          "steps": [
            {"value": 0, "color": "green"},
            {"value": 1, "color": "yellow"},
            {"value": 5, "color": "red"}
          ]
        }
      }
    ]
  }
}
```

---

## Best Practices

- Instrument everything from day one — retrofitting observability is painful
- Use structured logging (JSON) always — never unstructured strings
- Add correlation IDs (request ID, trace ID) to every log line
- Alert on symptoms (high error rate, slow latency) not causes (CPU high)
- Use multi-window burn rate alerts for SLOs — not simple threshold alerts
- Keep dashboards focused — one dashboard per service, not one giant dashboard
- Write runbooks for every alert — link from the alert annotation
- Use recording rules for expensive PromQL queries used in dashboards
- Test alerts regularly — a silent alert is worse than no alert

---

## Common Pitfalls

| Pitfall | Problem | Fix |
|---|---|---|
| Alerting on causes not symptoms | Too many noisy irrelevant alerts | Alert on error rate and latency, not CPU |
| No correlation IDs in logs | Cannot trace a request across services | Add request ID to every log line |
| High cardinality labels | Prometheus OOM from too many time series | Never use user ID or request ID as label |
| Alert fatigue | Too many alerts, team ignores them | Start with fewer high-quality alerts |
| No runbooks | On-call does not know what to do | Write runbook for every alert |
| Logging too much | Storage costs, hard to find signal | Log at INFO in prod, DEBUG only when needed |
| Unstructured log strings | Hard to query and filter | Always use structured JSON logging |
| Missing SLOs | No clear definition of good service | Define SLIs and SLOs for every service |

---

## Related Skills

- **kubernetes-expert**: For Kubernetes monitoring with Prometheus
- **docker-expert**: For containerized monitoring stack
- **cicd-expert**: For deployment tracking and alerting
- **linux-expert**: For host-level performance monitoring
- **aws-expert**: For CloudWatch and X-Ray
- **system-design**: For designing observable distributed systems

