block/buzz · critical

metrics exporter must install exactly once: {error}

Error message

metrics exporter must install exactly once: {error}

What it means

install() is the legacy panic-on-failure entry point that installs the global metrics recorder and Prometheus exporter exactly once. It delegates to try_install() and panics with 'metrics exporter must install exactly once: {error}' on any failure — typically because a recorder/exporter was already installed, or the port could not be bound.

Source

Thrown at crates/buzz-relay/src/metrics.rs:219

        .with_http_listener(([0, 0, 0, 0], port))
        .build()
        .map_err(MetricsInstallError::Build)?;

    metrics::set_global_recorder(recorder)
        .map_err(|_error| MetricsInstallError::RecorderConflict)?;
    describe_readiness_metrics();
    describe_db_pool_metrics();
    tokio::spawn(exporter);
    Ok(())
}

/// Install the global metrics recorder and spawn the Prometheus HTTP exporter.
///
/// This compatibility entry point preserves the original panic-on-failure API.
/// New startup code should use [`try_install`] to report typed failures.
pub fn install(port: u16, gauge_idle_timeout_secs: u64) {
    try_install(port, gauge_idle_timeout_secs)
        .unwrap_or_else(|error| panic!("metrics exporter must install exactly once: {error}"));
}

/// Register the frozen readiness metric descriptions with the active recorder.
pub(crate) fn describe_readiness_metrics() {
    metrics::describe_counter!(
        "buzz_readiness_checks_total",
        "Kubernetes health-listener readiness probes by terminal bounded reason"
    );
    metrics::describe_counter!(
        "buzz_readiness_dependency_checks_total",
        "Completed readiness dependency attempts by dependency and bounded outcome"
    );
    metrics::describe_histogram!(
        "buzz_readiness_check_duration_seconds",
        metrics::Unit::Seconds,
        "Completed readiness check duration without outcome label multiplication"
    );
    metrics::describe_gauge!(

View on GitHub (pinned to dad5a33865)

Solutions

  1. Call try_install(port, secs) instead and handle the typed error — it is the recommended API for startup code
  2. Ensure install()/try_install() is invoked exactly once per process (guard in main/init)
  3. If running tests, install metrics once in a shared setup or use a once-guard
  4. Change the metrics port if another process holds it

Example fix

// before
metrics::install(port, gauge_idle_timeout_secs);
// after
if let Err(error) = metrics::try_install(port, gauge_idle_timeout_secs) {
    tracing::error!(%error, "metrics exporter failed to install");
    return Err(error.into());
}
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure single install with a process-wide guard
static INIT: OnceLock<()> = OnceLock::new();
if INIT.set(()).is_err() { return; } // already installed

Type guard

null

Try / catch

match metrics::try_install(port, secs) {
    Ok(()) => tracing::info!("metrics installed"),
    Err(e) => tracing::error!(%e, "metrics install failed"),
}

Prevention

When it happens

Trigger: Calling install() (or relay startup) twice in the same process — e.g. tests installing the recorder then calling main(); or try_install failing because the Prometheus HTTP exporter port is already in use.

Common situations: Test binaries where each test calls setup that installs metrics; double invocation of relay main; another process (or a leftover relay) holds the metrics port.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05). Data as JSON: /api/errors/320d3f867e390ccc. Report an issue: GitHub.