astrid-runtime/astrid · error · anyhow::Error

install Prometheus recorder

Error message

install Prometheus recorder: {e}

What it means

After configuring buckets, install_recorder calls PrometheusBuilder::install_recorder() to register the global metrics recorder. If the exporter cannot install (typically a recorder/collector registration conflict in the prometheus registry), the failure is wrapped as 'install Prometheus recorder'. Because the handle is cached in a static, this can only fail on the first call.

Solutions

  1. Ensure install_recorder() is called exactly once, early in process startup, before any other metrics installation
  2. Remove or unify competing global recorder installations (only one global recorder is allowed per process)
  3. Route all metrics setup through install_recorder so the idempotent static handle is used
  4. In tests, isolate recorder installation to one test or use a single shared handle

Example fix

// before
prometheus::default_registry(); // or another lib installs a global recorder first
let handle = metrics::install_recorder()?;
// after
let handle = metrics::install_recorder()?; // single, idempotent installation point
Defensive patterns

Strategy: try-catch

Try / catch

match metrics::install_recorder() {
    Ok(h) => h,
    Err(e) if e.to_string().contains("install Prometheus recorder") => {
        // another global recorder was installed first; find and remove the competing install
        Err(anyhow!("global recorder conflict: {e}"))
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: First invocation of install_recorder() where PrometheusBuilder::install_recorder() fails — usually because a global recorder is already installed (e.g. another exporter such as the prometheus crate's set_boxed_recorder was used earlier in the process).

Common situations: Two metrics libraries both trying to install the global recorder; tests running in the same process where another test installed a recorder first; calling a different install path (prometheus::default_registry tricks) before this one.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/d1693383acc5ded7. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-gateway/src/metrics.rs:130

    static HANDLE: Mutex<Option<PrometheusHandle>> = Mutex::new(None);
    let mut guard = HANDLE
        .lock()
        .map_err(|e| anyhow::anyhow!("recorder lock poisoned: {e}"))?;
    if let Some(existing) = guard.as_ref() {
        return Ok(existing.clone());
    }
    let handle = PrometheusBuilder::new()
        // Set the per-route latency histogram's buckets explicitly.
        // The `Suffix` matcher applies to every series whose name
        // matches the metric base — same buckets for every
        // (method, route, status) combo.
        .set_buckets_for_metric(
            Matcher::Suffix(METRIC_REQUEST_DURATION_SECONDS.to_string()),
            DURATION_BUCKETS_SECONDS,
        )
        .map_err(|e| anyhow::anyhow!("configure histogram buckets: {e}"))?
        .install_recorder()
        .map_err(|e| anyhow::anyhow!("install Prometheus recorder: {e}"))?;

    // Describe every metric the gateway emits so `/metrics` carries
    // `# HELP` + `# TYPE` lines even before the series sees its
    // first observation. Touch each counter once at zero so it
    // appears in the scrape body — `metrics-exporter-prometheus`
    // only renders series after they've been recorded against.
    describe_counter!(
        METRIC_REQUESTS_TOTAL,
        Unit::Count,
        "Total HTTP requests by method+route+status."
    );
    describe_histogram!(
        METRIC_REQUEST_DURATION_SECONDS,
        Unit::Seconds,
        "Per-request handler latency by method+route+status."
    );
    describe_counter!(
        METRIC_AUTH_FAILURES_TOTAL,

View on GitHub (pinned to affd8760f4)