Monitoring

September 8, 2026 · View on GitHub

NORA exposes Prometheus metrics at /metrics. This page documents all available metrics and provides a ready-to-import Grafana dashboard.

Quick Start

# prometheus.yml
scrape_configs:
  - job_name: nora
    static_configs:
      - targets: ['nora:4000']
    scrape_interval: 15s

Import dist/grafana-dashboard.json into Grafana (Dashboards > Import > Upload JSON file).

Metrics Reference

HTTP (RED signals)

MetricTypeLabelsDescription
nora_http_requests_totalcounterregistry, method, statusTotal HTTP requests
nora_http_request_duration_secondshistogramregistry, methodRequest latency (buckets: 1ms–10s)

Cache

MetricTypeLabelsDescription
nora_cache_requests_totalcounterregistry, resultCache lookups (result: hit / miss)

Upstream Proxy

MetricTypeLabelsDescription
nora_upstream_request_duration_secondshistogramregistry, statusUpstream proxy latency (buckets: 1ms–30s)

Artifacts & Traffic

MetricTypeLabelsDescription
nora_artifacts_totalcounterregistryTotal artifacts stored
nora_downloads_totalcounterregistryTotal artifact downloads
nora_uploads_totalcounterregistryTotal artifact uploads

Storage

MetricTypeLabelsDescription
nora_storage_bytesgaugeregistryStorage size in bytes per registry
nora_storage_operations_totalcounteroperation, statusEvery backend round-trip: put, get, get_range, get_reader, copy, delete, stat, pin, list, list_with_meta. On an object store each one is a network request, so the rate of this counter against the request rate is what shows a handler paying a round-trip per file. status="miss" on stat/pin is an absent object or an unpinned one — not an error. status="integrity_fail"/"verify_error" on operation="get" mean a stored artifact failed hash-pin verification and was refused (fail-closed, #582) — see Integrity recovery.

Circuit Breaker

MetricTypeLabelsDescription
nora_circuit_breaker_stategaugeregistry0 = closed, 1 = open, 2 = half_open
nora_circuit_breaker_rejections_totalcounterregistryRequests rejected by open circuit breaker

Security

MetricTypeLabelsDescription
nora_response_upstream_url_leak_totalcounterregistryUpstream hostname detected in outgoing response body

Retention

MetricTypeLabelsDescription
nora_retention_versions_deleted_totalcounter—Versions removed by retention policy
nora_retention_bytes_freed_totalcounter—Bytes freed by retention
nora_retention_duration_secondshistogram—Retention sweep duration
nora_retention_last_run_timestampgauge—Unix timestamp of last retention run

Garbage Collection

MetricTypeLabelsDescription
nora_gc_blobs_removed_totalcounter—Orphan blobs removed by GC
nora_gc_bytes_freed_totalcounter—Bytes freed by GC
nora_gc_duration_secondshistogram—GC sweep duration
nora_gc_last_run_timestampgauge—Unix timestamp of last GC run
nora_gc_metadata_phantoms_totalcounter—Metadata entries without corresponding blobs

Grafana Dashboard

The included dashboard (dist/grafana-dashboard.json) provides:

  • Row 1 — Key stats: request rate, error rate, p50/p99 latency, cache hit rate, storage used
  • Row 2 — Request rate by registry, HTTP latency percentiles (p50/p95/p99)
  • Row 3 — Error rate by registry, upstream proxy latency by registry
  • Row 4 — Cache hit/miss rate, downloads/uploads by registry
  • Row 5 — Storage by registry, circuit breaker state table, security alerts (URL leaks, CB rejections)
  • Row 6 — Retention & GC bytes freed, last run timestamps, storage operations

The dashboard includes a registry template variable to filter by specific protocol.

Alerting Recommendations

# alertmanager rules (example)
groups:
  - name: nora
    rules:
      - alert: NoraHighErrorRate
        expr: >
          sum(rate(nora_http_requests_total{status=~"5.."}[5m]))
          / sum(rate(nora_http_requests_total[5m])) > 0.05
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "NORA error rate above 5%"

      - alert: NoraHighLatency
        expr: >
          histogram_quantile(0.99, sum(rate(nora_http_request_duration_seconds_bucket[5m])) by (le)) > 5
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "NORA p99 latency above 5s"

      - alert: NoraCircuitBreakerOpen
        expr: nora_circuit_breaker_state == 1
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "NORA circuit breaker OPEN for {{ $labels.registry }}"

      - alert: NoraCacheLowHitRate
        expr: >
          sum(rate(nora_cache_requests_total{result="hit"}[15m]))
          / sum(rate(nora_cache_requests_total[15m])) < 0.5
        for: 15m
        labels: { severity: warning }
        annotations:
          summary: "NORA cache hit rate below 50%"

      - alert: NoraUpstreamUrlLeak
        expr: sum(rate(nora_response_upstream_url_leak_total[5m])) > 0
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "Upstream URL leak detected in NORA responses"

      - alert: NoraIntegrityFailure
        expr: increase(nora_storage_operations_total{operation="get",status=~"integrity_fail|verify_error"}[5m]) > 0
        for: 0m
        labels: { severity: critical }
        annotations:
          summary: "NORA refusing to serve an artifact (hash-pin integrity failure)"

A ready-to-load version of these rules ships at deploy/prometheus-rules.yml; point Prometheus at it via rule_files:.

Integrity recovery

When nora_storage_operations_total{operation="get",status="integrity_fail"} fires, a stored artifact's bytes no longer match its hash pin (bit rot or tampering) and Storage::get() returns 5xx on every read (fail-closed, #582). The offending key is in the integrity violation error log line.

  • Cache/proxy artifacts self-heal: the next request treats the failure as a cache miss, re-fetches from upstream, and re-pins.

  • Locally-authored artifacts (e.g. uploaded raw blobs) have no upstream. Verify the on-disk bytes against a hash you trust, then:

    # Dry run — shows old → new pin without writing:
    nora re-pin raw/myorg/app-1.0.0.bin --expected <sha256>
    # Apply once you've confirmed:
    nora re-pin raw/myorg/app-1.0.0.bin --expected <sha256> --yes
    

    --expected is mandatory: re-pin updates the pin only if the on-disk bytes already hash to it. If they do not, the disk is genuinely corrupt — the command refuses (re-pin cannot heal corruption); restore from backup first.