Skip to content

Observability

Both api and ingestion expose a /metrics endpoint and emit structured JSON logs — everything below works the same whether you’re running a single docker compose install or a full Kubernetes deployment.

All metrics are prefixed axiaops_ and labelled to avoid cardinality blowup — route labels are the URL pattern (/v1/accounts/{id}), never a raw ID.

Group Metrics Labels
HTTP _http_requests_total, _http_request_duration_seconds, _http_requests_in_flight, _http_responses_total, _http_errors_total (5xx only) method, route, status
Database _db_query_duration_seconds, _db_query_errors_total, _db_connections_active, _db_transaction_duration_seconds operation, type
AWS / ingestion _aws_api_call_duration_seconds, _aws_api_errors_total, _cost_records_fetched_total, _resources_analyzed_total, _zombies_detected, _potential_monthly_savings_usd service, provider, organization_id
Scan lifecycle _scan_duration_seconds, _scan_errors_total, _scan_queue_depth, _accounts_scanning stage, account_id, error_type
Application _application_uptime_seconds, _application_errors_total —

Example Prometheus scrape config (docker-compose / single-host)

Section titled “Example Prometheus scrape config (docker-compose / single-host)”
scrape_configs:
- job_name: 'axiaops-api'
static_configs: [{ targets: ['localhost:8080'] }]
- job_name: 'axiaops-ingestion'
static_configs: [{ targets: ['localhost:8081'] }]

A fuller example with alerting rule stanzas ships at deploy/observability/prometheus.yml.example.

No config needed — the chart’s api/ingestion Services already carry prometheus.io/scrape/port/path annotations, picked up automatically by the kubernetes-service-endpoints job most Prometheus Helm charts ship by default.

A ready-to-import dashboard covering request rate, error rate, p95 latency, the zombie-detection/savings trend, and AWS API error rate lives at deploy/observability/grafana-dashboard.json.

Both services log structured JSON via log/slog, auto-attaching service, env, version, and commit_sha to every line. Set LOG_OUTPUT=text for human-readable output during local development.

The ingestion pipeline classifies every AWS error so retries and circuit-breaking are consistent instead of ad hoc per call site:

Category Examples Retryable Fails the scan
Credentials InvalidAccessKeyId, ExpiredToken ❌ ✅
Permissions AccessDenied, UnauthorizedOperation ❌ ✅
Throttling RequestLimitExceeded, Throttling ✅ ❌
Network connection timeout, DNS errors ✅ ❌
Data unavailable Cost Explorer not enabled ❌ ✅
Internal AWS ServiceUnavailable ✅ ❌

Retries use exponential backoff (3 attempts, 100ms → 200ms → 400ms, capped at 5s). A circuit breaker opens after 3 consecutive failures, blocking further scan attempts for a 30s cooldown, then allows 2 test scans through before fully closing again. A failure in one part of a scan doesn’t abort the whole thing — cost-fetch failing skips that provider and continues with others; a single resource failing is skipped, not fatal.

This surfaces in the dashboard as the account’s status: connected (green, normal), error (red — check credentials/permissions), scan_timeout (orange — exceeded the 10-minute scan budget), circuit_breaker_open (purple — auto-clears after the 30s cooldown).

The full metrics reference, code-level usage examples, and the retry/circuit-breaker implementation details live in the repo’s docs/OBSERVABILITY.md.