1. API reference

Metrics

Every metric Carbon core and each datasource emit, the metrics primitives, and the LogMetrics and PrometheusMetrics exporters.

On this page

This page lists every built-in metric in Carbon 2.0.0, the primitives in carbon_core::metrics, and the two exporter crates. For how metrics fit into a pipeline, see Metrics; to scrape them, see Prometheus.

How metrics are collected#

  • Every metric is a static Counter, Gauge or Histogram registered in one process-wide MetricsRegistry.
  • Metrics have no labels. Each crate prefixes its own names instead (carbon_, yellowstone_grpc_, transaction_crawler_ and so on).
  • Pipeline metrics are registered by PipelineBuilder::build. With the carbon-core postgres feature enabled, build also registers the Postgres metrics, whether or not you use a Postgres processor.
  • Datasource metrics are registered when the datasource's consume starts, so they appear in snapshots only after Pipeline::run has started that datasource.
  • Pipeline::run itself exports a snapshot only once, at shutdown. Periodic output comes from the exporter: LogMetrics runs its own timer, and PrometheusMetrics renders the registry on each scrape.

Built-in metrics#

Histogram buckets are listed as their upper bounds. Each histogram also has an implicit overflow bucket (+Inf).

carbon-core pipeline#

Defined in pipeline.rs. Updated by Pipeline::run once per update it receives.

NameTypeUnitMeaning
carbon_updates_received_totalcounterupdatesUpdates taken off the datasource channel.
carbon_updates_processed_totalcounterupdatesUpdates the pipeline finished handling, successfully or not.
carbon_updates_successful_totalcounterupdatesUpdates for which every matching pipe returned Ok.
carbon_updates_failed_totalcounterupdatesUpdates for which a pipe, decoder conversion or processor returned Err. The error is logged and the pipeline continues. Pipes after the failing one do not run for that update.
carbon_updates_queuedgaugeupdatesChannel length after the last processed update: how many updates are waiting. Compare with channel_buffer_size.
carbon_account_updates_processed_totalcounterupdatesUpdate::Account updates that went through all account pipes without error.
carbon_transaction_updates_processed_totalcounterupdatesUpdate::Transaction updates that went through all instruction and transaction pipes without error.
carbon_account_deletions_processed_totalcounterupdatesUpdate::AccountDeletion updates that went through all deletion pipes without error.
carbon_block_details_processed_totalcounterupdatesUpdate::BlockDetails updates that went through all block-details pipes without error.
carbon_updates_process_time_nanosecondshistogramnsTime to run one update through every pipe. Buckets: 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9.
carbon_updates_process_time_millisecondshistogrammsSame measurement in whole milliseconds (truncated, so sub-millisecond updates record 0). Buckets: 1, 5, 10, 25, 50, 100, 250, 500, 1000.

carbon-core postgres feature#

Defined in postgres/processors.rs. Updated by PostgresAccountProcessor, PostgresJsonAccountProcessor, PostgresInstructionProcessor and PostgresJsonInstructionProcessor (see the Postgres reference). The counter names do not end in _total.

NameTypeUnitMeaning
postgres_accounts_upsert_upsertedcounterrowsAccount upserts that succeeded.
postgres_accounts_upsert_failedcounterrowsAccount upserts that returned an error.
postgres_accounts_upsert_duration_millisecondshistogrammsDuration of successful account upserts. Buckets: 1, 5, 10, 25, 50, 100, 250, 500, 1000.
postgres_instructions_upsert_upsertedcounterrowsInstruction upserts that succeeded.
postgres_instructions_upsert_failedcounterrowsInstruction upserts that returned an error.
postgres_instructions_upsert_duration_millisecondshistogrammsDuration of successful instruction upserts. Buckets: 1, 5, 10, 25, 50, 100, 250, 500, 1000.

Datasource histogram buckets#

Most datasource histograms use one of three bucket sets:

SetBuckets
ns1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9 (nanoseconds)
ms-short1, 5, 10, 25, 50, 100, 250, 500, 1000 (milliseconds)
ms-long1, 10, 50, 100, 500, 1000, 5000 (milliseconds)

carbon-yellowstone-grpc-datasource#

See Yellowstone gRPC. Updates are queued with a non-blocking try_send. When the pipeline channel is full, the update is dropped and an error is logged.

NameTypeUnitMeaning
yellowstone_grpc_account_updates_received_totalcounterupdatesAccount notifications converted to Update::Account. Counted even when try_send failed and the update was dropped.
yellowstone_grpc_account_deletions_received_totalcounterupdatesZero-lamport account notifications converted to Update::AccountDeletion. Counted even when the send failed.
yellowstone_grpc_transaction_updates_received_totalcounterupdatesTransactions queued to the pipeline. Not counted when the send failed.
yellowstone_grpc_account_process_time_nanosecondshistogramns (ns set)Time to convert one account notification and attempt the send.
yellowstone_grpc_account_deletion_process_time_nanosecondshistogramns (ns set)Same, for deletions.
yellowstone_grpc_transaction_process_time_nanosecondshistogramns (ns set)Time to convert one protobuf transaction and queue it.

carbon-helius-laserstream-datasource#

See Helius LaserStream. Same behaviour as Yellowstone, including try_send.

NameTypeUnitMeaning
laserstream_account_updates_received_totalcounterupdatesAccount notifications converted to Update::Account, counted even when the send failed.
laserstream_account_deletions_received_totalcounterupdatesZero-lamport notifications converted to Update::AccountDeletion, counted even when the send failed.
laserstream_transaction_updates_received_totalcounterupdatesTransactions queued to the pipeline.
laserstream_account_process_time_nanosecondshistogramns (ns set)Time to convert one account notification and attempt the send.
laserstream_account_deletion_process_time_nanosecondshistogramns (ns set)Same, for deletions.
laserstream_transaction_process_time_nanosecondshistogramns (ns set)Time to convert one transaction and queue it.

carbon-stream-message-datasource#

See Stream message. Names use the agave_grpc_ prefix. Sends wait for channel capacity, so updates are not dropped when the pipeline is slow, and the process-time histograms include that wait.

NameTypeUnitMeaning
agave_grpc_account_updates_received_totalcounterupdatesAccount messages forwarded as Update::Account.
agave_grpc_account_deletions_received_totalcounterupdatesZero-lamport account messages forwarded as Update::AccountDeletion.
agave_grpc_transaction_updates_received_totalcounterupdatesTransactions forwarded to the pipeline.
agave_grpc_account_process_time_nanosecondshistogramns (ns set)Time to forward one account message, including the wait for channel capacity.
agave_grpc_account_deletion_process_time_nanosecondshistogramns (ns set)Same, for deletions.
agave_grpc_transaction_process_time_nanosecondshistogramns (ns set)Same, for transactions.

carbon-helius-atlas-ws-datasource#

See Helius Atlas WebSocket. Counters are incremented before the non-blocking try_send, so they include dropped updates.

NameTypeUnitMeaning
helius_atlas_ws_account_updates_received_totalcounterupdatesAccount notifications converted to Update::Account.
helius_atlas_ws_account_deletions_received_totalcounterupdatesZero-lamport notifications converted to Update::AccountDeletion.
helius_atlas_ws_transaction_updates_received_totalcounterupdatesTransaction notifications converted to Update::Transaction.
helius_atlas_ws_account_process_time_nanosecondshistogramns (ns set)Time to convert one account notification.
helius_atlas_ws_account_deletion_process_time_nanosecondshistogramns (ns set)Same, for deletions.
helius_atlas_ws_transaction_process_time_nanosecondshistogramns (ns set)Time to convert one transaction notification.
helius_atlas_ws_clock_process_time_nanosecondshistogramns (ns set)Time to deserialize one Clock sysvar notification. The datasource watches the Clock sysvar to detect stalls and slot gaps and to trigger reconnects.

carbon-rpc-block-subscribe-datasource#

See RPC block subscribe. Failed transactions (non-Ok status) are skipped and not counted. If try_send fails, the remaining transactions of that block are dropped.

NameTypeUnitMeaning
block_subscribe_transactions_processed_totalcountertransactionsTransactions converted to Update::Transaction, counted before the send.
block_subscribe_blocks_received_totalcounterblocksBlock notifications fully handled.
block_subscribe_transaction_process_time_nanosecondshistogramns (ns set)Time to decode and convert one transaction.
block_subscribe_block_process_time_nanosecondshistogramns (ns set)Time to handle one block notification, all transactions included.

carbon-rpc-block-crawler-datasource#

See RPC block crawler. Failed transactions are skipped and not counted. If try_send fails, the remaining transactions of that block are dropped.

NameTypeUnitMeaning
block_crawler_blocks_fetched_totalcounterblocksSuccessful getBlock calls.
block_crawler_blocks_skipped_totalcounterblocksgetBlock calls that failed with RPC error -32004, -32007 or -32009 (skipped slot or block not available). Other errors are logged, not counted.
block_crawler_blocks_received_totalcounterblocksFetched blocks picked up by the processing task.
block_crawler_blocks_processed_totalcounterblocksBlocks whose transactions were all handled.
block_crawler_transactions_processed_totalcountertransactionsTransactions converted to Update::Transaction, counted before the send.
block_crawler_blocks_fetch_times_millisecondshistogramms (ms-long set)Duration of each successful getBlock call.
block_crawler_transaction_process_time_nanosecondshistogramns (ns set)Time to decode and convert one transaction.
block_crawler_block_process_time_nanosecondshistogramns (ns set)Time to handle one block, all transactions included.

carbon-rpc-transaction-crawler-datasource#

See RPC transaction crawler.

NameTypeUnitMeaning
transaction_crawler_signatures_fetched_totalcountersignaturesSignatures returned by getSignaturesForAddress, summed over pages.
transaction_crawler_transactions_fetched_totalcountertransactionsgetTransaction requests that completed, including those that still failed after all retries.
transaction_crawler_signatures_fetch_times_millisecondshistogramms (ms-long set)Time per getSignaturesForAddress page, including handing the signatures to the fetch stage.
transaction_crawler_transaction_fetch_times_millisecondshistogramms (ms-long set)Time per successful getTransaction, measured from the first attempt, so retries and backoff are included.
transaction_crawler_transaction_process_time_millisecondshistogramms (ms-long set)Time to convert one fetched transaction into an Update::Transaction. Failed transactions are skipped and not recorded.

carbon-rpc-gpa-datasource#

See RPC getProgramAccounts.

NameTypeUnitMeaning
rpc_gpa_fetch_duration_millisecondshistogramms (ms-short set)Duration of the single getProgramAccounts call.
rpc_gpa_accounts_processed_totalcounteraccountsAccounts successfully sent to the pipeline.

carbon-rpc-program-subscribe-datasource#

See RPC program subscribe. If try_send fails, the subscription loop exits.

NameTypeUnitMeaning
program_subscribe_accounts_processed_totalcounteraccountsprogramSubscribe notifications converted to an account update or deletion, counted before the send.
program_subscribe_account_process_time_nanosecondshistogramns (ns set)Time to decode and convert one notification.

carbon-helius-gpa-v2-datasource#

See Helius getProgramAccountsV2.

NameTypeUnitMeaning
helius_gpa_v2_pages_fetched_totalcounterpagesPages fetched from the paginated API.
helius_gpa_v2_accounts_processed_totalcounteraccountsAccounts successfully sent to the pipeline.
helius_gpa_v2_fetch_duration_millisecondshistogramms (ms-short set)Time to fetch one page.

carbon-helius-gtfa-datasource#

See Helius getTransactionsForAddress.

NameTypeUnitMeaning
helius_gtfa_pages_fetched_totalcounterpagesPages fetched from the API.
helius_gtfa_transactions_processed_totalcountertransactionsTransactions successfully sent to the pipeline.
helius_gtfa_fetch_duration_millisecondshistogramms (ms-short set)Time to fetch one page.

carbon-validator-snapshot-datasource#

See Validator snapshot. Histogram buckets: 1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000, 10000 ms. A snapshot load usually takes longer than 10 s, so expect it in the +Inf bucket.

NameTypeUnitMeaning
validator_snapshot_load_duration_millisecondshistogrammsTime to load the snapshot from a local path or download it.
validator_snapshot_scan_duration_millisecondshistogrammsTime to scan every account after loading.
validator_snapshot_accounts_processed_totalcounteraccountsAccounts successfully sent to the pipeline.

carbon-jetstreamer-datasource (Carbon 1.x)#

carbon-jetstreamer-datasource is published at 1.0.0 on Carbon 1.x and is not compatible with carbon-core 2.0.0. Listed for completeness.

NameTypeUnitMeaning
jetstreamer_blocks_sent_totalcounterblocksBlock details sent to the pipeline.
jetstreamer_transactions_sent_totalcountertransactionsTransactions sent to the pipeline.
jetstreamer_transactions_filtered_in_totalcountertransactionsTransactions that matched the datasource filters.
jetstreamer_transactions_filtered_out_totalcountertransactionsTransactions that did not match the filters.
jetstreamer_internal_slots_processedgaugeslotsSlots processed by the firehose, from its internal stats.
jetstreamer_internal_blocks_processedgaugeblocksBlocks processed by the firehose.
jetstreamer_internal_transactions_processedgaugetransactionsTransactions processed by the firehose.

carbon-jito-shredstream-grpc-datasource (Carbon 1.x)#

Also published at 1.0.0 on Carbon 1.x only.

NameTypeUnitMeaning
jito_shredstream_grpc_entry_updates_received_totalcounterentriesEntries received, summed per message.
jito_shredstream_grpc_duplicate_entries_totalcounterentriesDuplicate entries skipped.
jito_shredstream_grpc_entry_process_time_nanosecondshistogramns (ns set)Time to process one entries message.

Metric#

Name and help text shared by Counter, Gauge and Histogram.

Rust
pub trait Metric: Send + Sync {
    fn name(&self) -> &'static str;
    fn help(&self) -> &'static str { "" }
}

Details. Implemented by the three primitives. The registry stores the concrete types, not dyn Metric, so implementing Metric on your own type does not make it exportable.

Example.

Rust
use carbon_core::metrics::{Counter, Metric};

static SEEN: Counter = Counter::new("my_seen_total", "Things seen");
assert_eq!(SEEN.name(), "my_seen_total");

See also. Counter, docs.rs.

Counter#

A monotonically increasing u64.

Rust
pub struct Counter { /* private */ }

impl Counter {
    pub const fn new(name: &'static str, help: &'static str) -> Self;
    pub fn inc(&self);
    pub fn inc_by(&self, v: u64);
    pub fn get(&self) -> u64;
}

Details. new is const, so you can declare a counter as a plain static. inc and inc_by are a single relaxed atomic add, cheap enough for the hot path. A counter is exported only after you pass it to MetricsRegistry::register_counter.

MethodDescription
new(name, help)Creates a counter at 0. Use Prometheus-style snake_case names, ending in _total by convention.
inc()Adds 1.
inc_by(v)Adds v.
get()Returns the current value.

Example. See Register custom metrics.

See also. Gauge, docs.rs.

Gauge#

An f64 value that can go up and down.

Rust
pub struct Gauge { /* private */ }

impl Gauge {
    pub const fn new(name: &'static str, help: &'static str) -> Self;
    pub fn set(&self, value: f64);
    pub fn add(&self, delta: f64);
    pub fn get(&self) -> f64;
}

Details. Stored as the bit pattern of an f64 in an AtomicU64. Starts at 0.0. add accepts negative deltas.

MethodDescription
new(name, help)Creates a gauge at 0. const, so usable in a static.
set(value)Replaces the value.
add(delta)Adds delta atomically (compare-and-swap loop).
get()Returns the current value.

Example. See Register custom metrics.

See also. Counter, docs.rs.

Histogram#

A bucketed distribution of f64 observations with fixed upper bounds.

Rust
pub struct Histogram { /* private */ }

impl Histogram {
    pub fn new(name: &'static str, help: &'static str, boundaries: Vec<f64>) -> Self;
    pub fn record(&self, value: f64);
    pub fn get(&self) -> HistogramSnapshot;
}

Details. boundaries are inclusive upper bounds and must be sorted ascending. The histogram keeps boundaries.len() + 1 buckets: a value goes into the first bucket whose bound is >= the value, and values above the last bound go into the final overflow bucket. record also adds the value to a running sum. Bucket counts are per bucket, not cumulative; PrometheusMetrics makes them cumulative when it renders them.

new allocates, so it is not const. Wrap a static histogram in std::sync::LazyLock.

MethodDescription
new(name, help, boundaries)Creates an empty histogram.
record(value)Counts value in its bucket and adds it to the sum.
get()Returns a HistogramSnapshot.

Example. See Register custom metrics.

See also. HistogramSnapshot, docs.rs.

HistogramSnapshot#

Point-in-time copy of a histogram.

Rust
pub struct HistogramSnapshot {
    pub counts: Vec<u64>,
    pub boundaries: Vec<f64>,
    pub sum: f64,
}

Details.

FieldTypeMeaning
countsVec<u64>Observations per bucket, not cumulative. Has boundaries.len() + 1 entries; the last is the overflow bucket.
boundariesVec<f64>Bucket upper bounds, as passed to Histogram::new.
sumf64Sum of all recorded values.

The total count is counts.iter().sum::<u64>().

Example.

Rust
let h = SWAP_SIZE.get();
let total: u64 = h.counts.iter().sum();
println!("{} observations, mean {}", total, h.sum / total.max(1) as f64);

See also. MetricsSnapshot, docs.rs.

MetricsSnapshot#

Point-in-time values of every registered metric, passed to MetricsExporter::export.

Rust
pub struct MetricsSnapshot {
    pub counters: Vec<(&'static str, &'static str, u64)>,
    pub gauges: Vec<(&'static str, &'static str, f64)>,
    pub histograms: Vec<(&'static str, &'static str, HistogramSnapshot)>,
}

Details. Each tuple is (name, help, value). Entries are in registration order. A metric registered twice appears twice.

Example.

Rust
let snapshot = carbon_core::metrics::MetricsRegistry::global().snapshot();
let received = snapshot
    .counters
    .iter()
    .find(|(name, _, _)| *name == "carbon_updates_received_total")
    .map(|(_, _, v)| *v)
    .unwrap_or(0);

See also. MetricsRegistry, docs.rs.

MetricsRegistry#

The process-wide registry that exporters read from.

Rust
pub struct MetricsRegistry { /* private */ }

impl MetricsRegistry {
    pub fn global() -> &'static MetricsRegistry;
    pub fn register_counter(&self, counter: &'static Counter);
    pub fn register_gauge(&self, gauge: &'static Gauge);
    pub fn register_histogram(&self, histogram: &'static Histogram);
    pub fn snapshot(&self) -> MetricsSnapshot;
}

Details. There is one registry per process, returned by global(); you cannot create another. It holds &'static references, so metrics must live in statics. Registration appends without deduplication (see the warning above). There is no way to unregister a metric. The internal locks panic with metrics counters lock poisoned (or gauges, histograms) if a thread panicked while holding them.

Example. See Register custom metrics.

See also. MetricsSnapshot, docs.rs.

Register custom metrics#

Declare metrics as statics, register them once at startup (before Pipeline::run), and update them from your processors. Every exporter picks them up with the built-in metrics.

src/main.rs
use {
    carbon_core::metrics::{Counter, Gauge, Histogram, MetricsRegistry},
    std::sync::LazyLock,
};

static SWAPS_SEEN: Counter = Counter::new("my_swaps_seen_total", "Swaps decoded by my processor");
static LAST_SLOT: Gauge = Gauge::new("my_last_slot", "Slot of the last processed swap");
static SWAP_SIZE: LazyLock<Histogram> = LazyLock::new(|| {
    Histogram::new(
        "my_swap_amount_in",
        "Swap input amount in lamports",
        vec![1_000.0, 1_000_000.0, 1_000_000_000.0],
    )
});

fn main() {
    let registry = MetricsRegistry::global();
    registry.register_counter(&SWAPS_SEEN);
    registry.register_gauge(&LAST_SLOT);
    registry.register_histogram(&SWAP_SIZE);

    SWAPS_SEEN.inc();
    SWAPS_SEEN.inc_by(2);
    LAST_SLOT.set(312_000_000.0);
    SWAP_SIZE.record(500.0);
    SWAP_SIZE.record(2_500_000.0);

    let snapshot = registry.snapshot();
    for (name, _help, value) in &snapshot.counters {
        println!("counter {name} = {value}");
    }
    for (name, _help, value) in &snapshot.gauges {
        println!("gauge {name} = {value}");
    }
    for (name, _help, h) in &snapshot.histograms {
        println!("histogram {name}: counts={:?} boundaries={:?} sum={}", h.counts, h.boundaries, h.sum);
    }
}
Output
counter my_swaps_seen_total = 3
gauge my_last_slot = 312000000
histogram my_swap_amount_in: counts=[1, 0, 1, 0] boundaries=[1000.0, 1000000.0, 1000000000.0] sum=2500500

This program needs only carbon-core = "2.0.0".

MetricsExporter#

A sink for metrics snapshots, registered with PipelineBuilder::metrics.

Rust
pub trait MetricsExporter: Send + Sync {
    fn initialize(self: Arc<Self>) -> CarbonResult<()> { Ok(()) }
    fn export(&self, snapshot: &MetricsSnapshot) -> CarbonResult<()>;
    fn shutdown(&self) -> CarbonResult<()> { Ok(()) }
}

Details. Pipeline::run calls these methods at fixed points:

MethodWhen the pipeline calls itNotes
initializeOnce per exporter, at the start of run, inside the Tokio runtime, before any datasource starts.Spawn periodic work here. Returning Err makes run return that error.
exportOnce per exporter, at shutdown, with a final snapshot.The pipeline does not call export per update or on a timer. The carbon-core doc comment saying it runs "after each processed update" is inaccurate.
shutdownOnce per exporter, after the final export.Stop background tasks here.

The final export and shutdown run on every shutdown path: Ctrl-C, cancellation through datasource_cancellation_token, and all datasources finishing. An Err from either makes run return early with that error.

Example. An exporter that prints counters every 10 seconds and once more at shutdown:

src/exporter.rs
use {
    carbon_core::{
        error::CarbonResult,
        metrics::{MetricsExporter, MetricsRegistry, MetricsSnapshot},
    },
    std::{sync::Arc, time::Duration},
};

/// Prints every counter to stdout every 10 seconds and once more at shutdown.
struct StdoutExporter;

impl MetricsExporter for StdoutExporter {
    fn initialize(self: Arc<Self>) -> CarbonResult<()> {
        // The pipeline only calls `export` at shutdown, so run your own loop
        // for periodic exports.
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(10));
            loop {
                interval.tick().await;
                let _ = self.export(&MetricsRegistry::global().snapshot());
            }
        });
        Ok(())
    }

    fn export(&self, snapshot: &MetricsSnapshot) -> CarbonResult<()> {
        for (name, _help, value) in &snapshot.counters {
            println!("{name} {value}");
        }
        Ok(())
    }
}

Register it with .metrics(Arc::new(StdoutExporter)).

See also. LogMetrics, PrometheusMetrics, Metrics, docs.rs.

LogMetrics#

An exporter that writes a summary and every metric to the log crate at info level. Crate: carbon-log-metrics = "2.0.0".

Rust
pub struct LogMetrics { /* private */ }

impl LogMetrics {
    pub fn new() -> Self;
    pub fn new_with_flush_interval(interval_secs: u64) -> Self;
}

impl Default for LogMetrics { /* same as new() */ }
impl MetricsExporter for LogMetrics { /* … */ }

Details.

ConstructorFlush interval
LogMetrics::new() / LogMetrics::default()5 seconds
LogMetrics::new_with_flush_interval(interval_secs)interval_secs seconds. Must be at least 1: a value of 0 makes the background task panic, and periodic logging stops.
  • initialize spawns a Tokio task that logs a snapshot every interval. The first tick fires immediately, so you see one block at startup.
  • The pipeline's shutdown export logs one last block, then shutdown cancels the task.
  • Output goes through log::info! with target carbon_log_metrics. Nothing is printed unless you install a logger (for example env_logger) with info enabled for that target.
  • Each block starts with a summary line: HH:MM:SS (+time since last flush) | N processed (P%), N successful, N failed (F%), Q in queue. The processed percentage is carbon_updates_processed_total divided by carbon_updates_received_total plus carbon_updates_queued; the failed percentage is relative to processed.
  • Then one line per counter and gauge ( name: value) and one per histogram ( name: count=N; buckets and sum are not logged).

Example. This program runs a pipeline with a datasource that emits three empty blocks and then returns, which closes the channel and ends the pipeline.

src/main.rs
use {
    async_trait::async_trait,
    carbon_core::{
        datasource::{BlockDetails, Datasource, DatasourceId, Update, UpdateType},
        error::CarbonResult,
        pipeline::Pipeline,
        processor::Processor,
    },
    carbon_log_metrics::LogMetrics,
    std::sync::Arc,
    tokio::sync::mpsc::Sender,
    tokio_util::sync::CancellationToken,
};

/// Emits three empty blocks, then returns, which ends the pipeline.
struct ThreeBlocks;

#[async_trait]
impl Datasource for ThreeBlocks {
    async fn consume(
        &self,
        id: DatasourceId,
        sender: Sender<(Update, DatasourceId)>,
        _cancellation_token: CancellationToken,
    ) -> CarbonResult<()> {
        for slot in 1..=3 {
            let block = BlockDetails {
                slot,
                block_hash: None,
                previous_block_hash: None,
                rewards: None,
                num_reward_partitions: None,
                block_time: None,
                block_height: None,
            };
            let _ = sender.send((Update::BlockDetails(block), id.clone())).await;
        }
        Ok(())
    }

    fn update_types(&self) -> Vec<UpdateType> {
        vec![UpdateType::BlockDetails]
    }
}

struct Noop;

impl Processor<BlockDetails> for Noop {
    async fn process(&mut self, _data: &BlockDetails) -> CarbonResult<()> {
        Ok(())
    }
}

#[tokio::main]
async fn main() -> CarbonResult<()> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

    Pipeline::builder()
        .datasource(ThreeBlocks)
        .block_details(Noop)
        .metrics(Arc::new(LogMetrics::new()))
        .build()?
        .run()
        .await
}
Cargo.toml
[dependencies]
async-trait = "0.1.89"
carbon-core = "2.0.0"
carbon-log-metrics = "2.0.0"
env_logger = "0.11.8"
tokio = { version = "1.48.0", features = ["full"] }
tokio-util = "0.7.17"
Terminal
cargo run
Output
[2026-11-08T23:53:11Z INFO  carbon_core::pipeline] starting pipeline. num_datasources: 1, num_exporters: 1, num_account_pipes: 0, num_account_deletion_pipes: 0, num_instruction_pipes: 0, num_transaction_pipes: 0
[2026-11-08T23:53:11Z INFO  carbon_core::pipeline] update_receiver closed, shutting down.
[2026-11-08T23:53:11Z INFO  carbon_log_metrics] 00:00:00 (+4.145248ms) | 3 processed (100%), 3 successful, 0 failed (0%), 0 in queue
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_received_total: 3
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_processed_total: 3
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_successful_total: 3
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_failed_total: 0
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_account_updates_processed_total: 0
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_transaction_updates_processed_total: 0
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_account_deletions_processed_total: 0
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_block_details_processed_total: 3
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_queued: 0
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_process_time_nanoseconds: count=3
[2026-11-08T23:53:11Z INFO  carbon_log_metrics]   carbon_updates_process_time_milliseconds: count=3
[2026-11-08T23:53:11Z INFO  carbon_core::pipeline] pipeline shutdown complete.

The pipeline ended before the first periodic tick ran, so only the shutdown export appears. With the carbon-core postgres feature enabled, the six postgres_* metrics are listed too.

See also. MetricsExporter, docs.rs.

PrometheusMetrics#

An exporter that serves the registry in the Prometheus text format over HTTP. Crate: carbon-prometheus-metrics = "2.0.0".

Rust
pub struct PrometheusMetrics { /* private */ }

impl PrometheusMetrics {
    pub fn new() -> Self;
    pub fn with_server(config: PrometheusServerConfig) -> Self;              // feature "http-server"
    pub fn start_server(
        config: PrometheusServerConfig,
    ) -> tokio::task::JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>>; // feature "http-server"
    pub fn prometheus_text() -> String;
    pub fn format_snapshot(snapshot: &MetricsSnapshot) -> String;
}

impl Default for PrometheusMetrics { /* same as new() */ }
impl MetricsExporter for PrometheusMetrics { /* … */ }

Details.

ItemBehaviour
new() / default()An exporter that does nothing: no server is started and export is a no-op. Use it only if you serve prometheus_text() yourself or call start_server.
with_server(config)When the pipeline calls initialize, spawns an HTTP server on config.listen_addr. Bind errors are logged (Prometheus metrics server error: …) and do not stop the pipeline.
start_server(config)Spawns the same server immediately on the current Tokio runtime and returns its JoinHandle. Independent of any pipeline.
prometheus_text()Renders a fresh snapshot of the global registry.
format_snapshot(snapshot)Renders a given snapshot.
exportNo-op. Values are read live on each scrape, so the shutdown export changes nothing.
shutdownDefault no-op. The server stops on its own when the process receives Ctrl-C; it is not stopped by a cancellation token.

The server:

  • Serves GET /metrics only, with content-type: text/plain; charset=utf-8; version=0.0.4 and cache-control: no-cache, no-store, must-revalidate.
  • Applies a request timeout (408 Request Timeout after request_timeout_secs) and, when enable_cors is set, a permissive CORS layer.
  • Logs Prometheus metrics server starting on http://<addr>/metrics at startup.

Rendering:

  • Characters other than ASCII letters, digits, _ and : in names are replaced with _. All built-in names are already valid.
  • # HELP is written when the help text is non-empty, then # TYPE (counter, gauge or histogram).
  • Histograms are written as cumulative <name>_bucket{le="…"} lines, a le="+Inf" bucket, <name>_count and <name>_sum. There are no other labels.

Example. Serve metrics on 127.0.0.1:9464 for the lifetime of the pipeline. The datasource emits three blocks and then waits for Ctrl-C.

src/main.rs
use {
    async_trait::async_trait,
    carbon_core::{
        datasource::{BlockDetails, Datasource, DatasourceId, Update, UpdateType},
        error::CarbonResult,
        pipeline::Pipeline,
        processor::Processor,
    },
    carbon_prometheus_metrics::{PrometheusMetrics, PrometheusServerConfig},
    std::sync::Arc,
    tokio::sync::mpsc::Sender,
    tokio_util::sync::CancellationToken,
};

/// Emits three empty blocks, then waits for shutdown.
struct ThreeBlocks;

#[async_trait]
impl Datasource for ThreeBlocks {
    async fn consume(
        &self,
        id: DatasourceId,
        sender: Sender<(Update, DatasourceId)>,
        cancellation_token: CancellationToken,
    ) -> CarbonResult<()> {
        for slot in 1..=3 {
            let block = BlockDetails {
                slot,
                block_hash: None,
                previous_block_hash: None,
                rewards: None,
                num_reward_partitions: None,
                block_time: None,
                block_height: None,
            };
            let _ = sender.send((Update::BlockDetails(block), id.clone())).await;
        }
        cancellation_token.cancelled().await;
        Ok(())
    }

    fn update_types(&self) -> Vec<UpdateType> {
        vec![UpdateType::BlockDetails]
    }
}

struct Noop;

impl Processor<BlockDetails> for Noop {
    async fn process(&mut self, _data: &BlockDetails) -> CarbonResult<()> {
        Ok(())
    }
}

#[tokio::main]
async fn main() -> CarbonResult<()> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

    let config = PrometheusServerConfig::new().listen_addr("127.0.0.1:9464".parse().unwrap());

    Pipeline::builder()
        .datasource(ThreeBlocks)
        .block_details(Noop)
        .metrics(Arc::new(PrometheusMetrics::with_server(config)))
        .build()?
        .run()
        .await
}

Add carbon-prometheus-metrics = "2.0.0" to the dependencies from the LogMetrics example. Start it with cargo run, then scrape it from another terminal:

Terminal
curl -s http://127.0.0.1:9464/metrics
Output
# HELP carbon_updates_received_total Total updates pulled from datasources
# TYPE carbon_updates_received_total counter
carbon_updates_received_total 3
# HELP carbon_updates_processed_total Total updates processed by the pipeline
# TYPE carbon_updates_processed_total counter
carbon_updates_processed_total 3
…
# HELP carbon_updates_queued Current number of updates waiting in queue
# TYPE carbon_updates_queued gauge
carbon_updates_queued 0
# HELP carbon_updates_process_time_nanoseconds Time taken to process updates in nanoseconds
# TYPE carbon_updates_process_time_nanoseconds histogram
carbon_updates_process_time_nanoseconds_bucket{le="1000"} 2
carbon_updates_process_time_nanoseconds_bucket{le="10000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="100000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="1000000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="10000000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="100000000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="1000000000"} 3
carbon_updates_process_time_nanoseconds_bucket{le="+Inf"} 3
carbon_updates_process_time_nanoseconds_count 3
carbon_updates_process_time_nanoseconds_sum 7030
…

Press Ctrl-C to stop both the pipeline and the server.

See also. PrometheusServerConfig, Prometheus, docs.rs.

PrometheusServerConfig#

Configuration for the /metrics HTTP server. Requires the http-server feature (on by default).

Rust
#[derive(Debug, Clone)]
pub struct PrometheusServerConfig {
    pub listen_addr: std::net::SocketAddr,
    pub enable_cors: bool,
    pub request_timeout_secs: u64,
}

impl PrometheusServerConfig {
    pub fn new() -> Self;
    pub fn listen_addr(self, addr: std::net::SocketAddr) -> Self;
    pub fn enable_cors(self, enable: bool) -> Self;
    pub fn request_timeout_secs(self, timeout: u64) -> Self;
}

Details. new() and Default give these values; each builder method replaces one.

FieldTypeDefaultDescription
listen_addrSocketAddr0.0.0.0:9464Address and port to bind. The default listens on all interfaces.
enable_corsbooltrueAdds a permissive CORS layer (access-control-allow-origin: *).
request_timeout_secsu6430Requests running longer get 408 Request Timeout.

Example. Serve metrics yourself, without a pipeline exporter:

src/main.rs
use carbon_prometheus_metrics::{PrometheusMetrics, PrometheusServerConfig};

#[tokio::main]
async fn main() {
    // Serve /metrics yourself instead of through the exporter.
    let server = PrometheusMetrics::start_server(
        PrometheusServerConfig::new()
            .listen_addr("0.0.0.0:9100".parse().unwrap())
            .enable_cors(false)
            .request_timeout_secs(10),
    );

    // Or render the exposition text directly, e.g. from your own HTTP handler.
    let text: String = PrometheusMetrics::prometheus_text();
    println!("{text}");

    server.abort();
}

See also. run_metrics_server, docs.rs.

run_metrics_server#

Free functions that run the /metrics server on the current task until Ctrl-C. Require the http-server feature.

Rust
pub async fn run_metrics_server(
    listen_addr: std::net::SocketAddr,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;

pub async fn run_default_metrics_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>>;

pub async fn run_metrics_server_with_config(
    config: PrometheusServerConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;

Details. run_metrics_server uses listen_addr with the other defaults; run_default_metrics_server uses PrometheusServerConfig::default() (0.0.0.0:9464). They return Err only if binding fails. A server error after startup is logged and the function returns Ok(()). Each function also returns Ok(()) after Ctrl-C. PrometheusMetrics::with_server and start_server call run_metrics_server_with_config inside tokio::spawn.

Example.

Rust
tokio::spawn(carbon_prometheus_metrics::run_metrics_server(
    "127.0.0.1:9464".parse().unwrap(),
));

See also. PrometheusMetrics, docs.rs.

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.