0x0001 |When Monitoring Becomes as Simple as Ordering Takeout
Have you ever had this experience?
At three in the morning, the server suddenly alarms, CPU spikes to 99%, memory is full, and the logs are filled with <span>panic!</span> cries. While you chew on cold instant noodles, you frantically search through the code, only to find that a counter no one remembers was never initialized—it has been incrementing wildly until it brought the entire service down.
At this moment, you think: if only there were a “God’s eye view” that could show in real-time how many times each function was called, how long each transaction took, whether user balances were negative… wouldn’t that be great?
Don’t worry, Prometheus is that “God”.
It is one of the most popular open-source monitoring systems in the cloud-native era, thanks to its pull-based architecture, powerful query language PromQL, and perfect partnership with Grafana, it has become a beloved tool for countless engineers.
But here comes the problem: writing monitoring code is too tedious!
The traditional way requires manually registering metrics, concatenating labels, handling type conversions… After a whole set of operations, the code is more complex than the business logic. After finally finishing, you run it—”Huh? Why is there no data for this counter?” After searching for a long time, you realize you forgot to call <span>register()</span>.
So we start to fantasize: Can we automate monitoring a bit? For example, like writing a database model, just declare the fields and let the framework handle the rest?
The good news is—now it really can!
Today we are going to talk about a fresh library from the Rust community (just released in 2025) with huge potential:👉 <span>prometric</span> 👈
Its slogan is quite bold:
“Define monitoring metrics just like defining a struct.”
Doesn’t that sound a bit mystical? Don’t worry, we will unveil its mystery step by step.
0x0002 |What is <span>prometric</span>? A Monitoring Macro That Works on Its Own
Let’s formally introduce this new friend:
- Project Address:https://github.com/chainbound/prometric[1]
- Main Language:Rust 🦀
- Core Functionality:Automatically generate embedded Prometheus metrics through attribute macros
- Inspired By:
<span>metrics-derive</span><span>, but it goes further by directly integrating with the native </span><code><span>prometheus</span>library - Key Highlights:Supports dynamic labels + zero boilerplate code + type safety + automatic registration
In other words, you only need to add <span>#[metrics]</span> on the struct, and then annotate each field with <span>#[metric(...)]</span>, and the rest—creation, registration, naming, labeling, exposing interfaces—will all be handled by it.
This feels like ordering takeout, not only delivered to your door, but they also help you wash the dishes.
Let’s look at an example to ease the tension
use prometric_derive::metrics;
use prometric::{Counter, Gauge, Histogram};
#[metrics(scope = "app")]
struct AppMetrics {
/// The total number of HTTP requests.
#[metric(rename = "http_requests_total", labels = ["method", "path"])]
http_requests: Counter,
#[metric(labels = ["method", "path"], buckets = [0.005, 0.01, 0.025, 0.05, 0.1])]
http_requests_duration: Histogram,
#[metric(help = "The current number of active users.")]
current_users: Gauge,
}
With just a few lines, you have three monitorable metrics:
- Total Requests (Counter)
- Request Latency Distribution (Histogram)
- Current Active Users (Gauge)
And each comes with documentation (HELP), type declaration (TYPE), prefix naming, label management…
What’s next? How to use it?
let metrics = AppMetrics::builder()
.with_label("host", "localhost")
.with_label("port", "8080")
.build();
// Start recording!
metrics.http_requests("GET", "/").inc(); // +1 request
metrics.http_requests_duration("GET", "/").observe(0.03); // took 30ms
metrics.current_users().set(42); // currently 42 users online
Isn’t it refreshing enough to make you want to cry?
Compared to traditional writing, you need to:
- Manually create
<span>IntCounterVec</span> - Call
<span>register()</span>to register to the global registry - Ensure names do not conflict
- Each call must be
<span>.get_metric_with_label_values(&["GET", "/"])</span> - If you forget any step, the program will silently fail or panic
And now? One line of <span>inc()</span><span> solves the battle.</span>
This is the power of abstraction.
0x0003 |Delving into the Source Code: How <span>prometric</span> Works Behind the Scenes
You think this is just a simple macro? Wrong. It hides a sophisticated design philosophy behind it.
Let’s break down how it achieves the trinity of “zero configuration, high flexibility, strong typing”.
Step 1: Struct Annotation — <span>#[metrics(scope = "...")]</span>
When you write:
#[metrics(scope = "app")]
struct AppMetrics { ... }
The compiler will trigger the <span>prometric-derive</span> procedural macro, starting to scan all fields of this struct.
<span>scope</span> parameter determines the naming prefix for all metrics. For example, in the above case, the final metric names will become:
<span>app_http_requests_total</span><span>app_http_requests_duration</span><span>app_current_users</span>
This is one of Prometheus’s best practices: avoiding naming conflicts. After all, no one wants their <span>requests_total</span> to collide with someone else’s.
Step 2: Field Annotations — <span>#[metric(...)]</span>
This is where the real magic happens.
For each field with a <span>#[metric]</span> annotation, the macro generates the corresponding getter method and automatically completes the following tasks:
| Action | Description |
|---|---|
| ✅ Create Metric Instance | Calls the corresponding constructor based on the field type (Counter/Gauge/Histogram) |
| ✅ Register to Registry | Defaults to using the global registry, but can be customized |
| ✅ Set HELP Text | Uses doc comments or <span>help</span> attributes |
| ✅ Handle Label Dimensions | Supports static labels (added in builder) and dynamic labels (passed as method parameters) |
| ✅ Generate Accessor Methods | Such as <span>http_requests(&self, method: &str, path: &str)</span> |
For example:
#[metric(labels = ["method", "path"])]
http_requests: Counter,
Will be expanded into code similar to this (simplified version):
impl AppMetrics {
pub fn http_requests(&self, method: &str, path: &str) -> CounterGuard<'_> {
let vec = IntCounterVec::new(
Opts::new("app_http_requests_total", "The total number of HTTP requests."),
&["method", "path"],
).unwrap();
register(vec).unwrap(); // Actually only registers once
CounterGuard(vec.with_label_values(&[method, path]))
}
}
Of course, the actual implementation is more complex, considering thread safety, duplicate registration, etc., but we don’t have to worry about that—because the macro has taken care of it for you.
Step 3: Builder Pattern — Enter the Builder Pattern
You might ask: “What about shared labels? For example, global information like <span>host</span>, <span>region</span>?”
The answer is: use <span>builder()</span><span>!</span>
let metrics = AppMetrics::builder()
.with_label("host", "localhost")
.with_label("region", "us-west-1")
.build();
These labels will be automatically attached to all metrics, without needing to pass them manually each time.
This is equivalent to adding “job” or “instance” level labels in Prometheus, which belong to operational metadata.
Dynamic labels (like <span>method</span>, <span>path</span>) represent business dimensions, and combining the two can create a complete monitoring map.
0x0004 |Three Core Metrics in Action: How to Use Counter, Gauge, Histogram?
Although Prometheus supports four basic metric types (Counter, Gauge, Histogram, Summary), <span>prometric</span><span> currently focuses on the first three. Let’s take a look at each.</span>
🟢 Counter: A “Step Counter” That Only Increases
As the name suggests, a Counter is a monotonically increasing counter, suitable for counting:
- Number of requests
- Number of errors
- Number of successful tasks
- Data written
Using <span>prometric</span><span> is very intuitive:</span>
#[metric(help = "Total number of login attempts")]
login_attempts: Counter,
#[metric(rename = "failed_logins", labels = ["reason"])]
failed_login_counter: Counter,
Usage:
metrics.login_attempts().inc(); // +1 login attempt
metrics.failed_login_counter("invalid_password").inc(); // +1 failure, reason tagged
Prometheus query suggestions:
# Login failure rate per second
rate(failed_logins[5m])
# Compare success and failure ratios
sum(rate(login_attempts[5m])) by (job)
/
sum(rate(failed_logins[5m])) by (job)
⚠️ Notes:
- Do not use for scenarios that may decrease (like inventory reduction), that’s the job of Gauge.
- Don’t use it for “current values” because it will only keep increasing.
🔵 Gauge: A “Thermometer” That Fluctuates Freely
Gauge can increase or decrease, suitable for representing instantaneous states:
- Memory usage
- Current number of connections
- Game player levels
- Account balance (floating point is also possible!)
Example:
#[metric(help = "Current memory usage in bytes")]
memory_usage: Gauge<u64>,
#[metric(labels = ["user_id"], help = "User's account balance in USD")]
account_balance: Gauge<f64>,
Usage:
metrics.memory_usage().set(get_current_memory());
metrics.account_balance("u_12345").set(-50.0); // Oops, in debt
Prometheus query suggestions:
# Current average balance
avg(account_balance)
# Memory usage trend
increase(memory_usage[1h]) // Note: increase has limited meaning for gauge, use with caution
💡 Tip: If you need to know the “rate of change”, you can use <span>deriv()</span><span> or </span><code><span>idelta()</span> functions to process Gauge.
🟠 Histogram: A “Microscope” for Performance Analysis
If Counter is macro statistics, and Gauge is a real-time snapshot, then Histogram is a powerful tool for performance optimization.
It can tell you:
- Is 99% of the request response time < 100ms?
- Are there any slow queries dragging down overall performance?
- Is the request latency distribution normal?
Definition:
#[metric(buckets = [0.01, 0.05, 0.1, 0.5, 1.0], help = "HTTP request duration in seconds")]
http_duration: Histogram,
Here, <span>buckets</span> is key—it is a set of preset “interval thresholds”. For example, <span>[0.01, 0.05, ...]</span> indicates:
- How many requests ≤ 10ms?
- How many requests ≤ 50ms?
- ……
- How many requests ≤ 1s?
Observing data:
let start = Instant::now();
// do something...
let duration = start.elapsed().as_secs_f64();
metrics.http_duration("GET", "/api/user").observe(duration);
Prometheus query suggestions:
# 99th percentile latency
histogram_quantile(0.99, sum(rate(app_http_duration_bucket[5m])) by (le))
# Average latency (not precise)
sum(rate(app_http_duration_sum[5m])) / sum(rate(app_http_duration_count[5m]))
# View distribution curve
rate(app_http_duration_bucket[5m])
🎯 Best Practices:
- Customizing
<span>buckets</span><span> is more effective than using default values. For example, if your API SLA is 200ms, you must include </span><code><span>0.2</span>. - Avoid too many buckets (generally 8-15 is sufficient), otherwise high cardinality can affect performance.
0x0005 |Advanced Features: Dynamic Labels, Generics, Custom Registry
You think <span>prometric</span><span> is just "syntactic sugar"? Too young.</span>
Its real strength lies in: both simplicity and flexibility.
🧩 Dynamic Labels vs Static Labels
We mentioned two types of labels:
- Static Labels: Set in the builder using
<span>.with_label()</span><span>, suitable for deployment environment-related information (host, region, version)</span> - Dynamic Labels: Passed as parameters to getter methods, suitable for business context (method, path, status_code)
They can coexist, and the order does not affect the output.
For example:
let metrics = AppMetrics::builder()
.with_label("env", "prod")
.build();
metrics.http_requests("POST", "/pay").inc();
The final metric looks like this:
app_http_requests_total{env="prod",method="POST",path="/pay"} 1
Perfectly balancing operational and business perspectives.
🔄 Generic Support: Making Metrics “Generic” Too
You can even make the entire metrics struct generic!
#[metrics(scope = "generic")]
struct GenericMetrics<T: Clone> {
#[metric(help = "Processed items count")]
processed: Counter,
#[metric(help = "Average processing time")]
latency: Histogram,
#[metric(help = "Current buffer size")]
buffer_size: Gauge<T>,
}
As long as <span>T</span> implements <span>Into<f64></span><span> (because Prometheus internally uses f64 for storage), it can be used normally.</span>
However, note: generic fields cannot have labels, otherwise the number of parameters cannot be determined.
🏦 Custom Registry: Say Goodbye to Global Pollution
By default, <span>prometric</span><span> uses Prometheus's global registry (</span><code><span>default_registry()</span><span>). This is convenient, but can also lead to naming conflicts or testing interference.</span>
The solution: custom registry!
use prometheus::Registry;
let registry = Registry::new();
let metrics = AppMetrics::builder()
.with_registry(registry.clone())
.build();
// You can expose this registry for use by the HTTP server later
This way you can:
- Isolate metrics in unit tests
- Use different registries for different modules
- Make integration testing validation easier
It’s simply a blessing for clean code programmers.
0x0006 |Application in Real Projects: Building a Monitoring Web Service from Scratch
Talking on paper feels shallow, let’s do something real.
Suppose you want to create a minimalist URL Shortener service, with the following requirements:
- Provide
<span>/shorten</span><span> and </span><code><span>/go/:key</span>interfaces - Record request counts, error counts, response times
- Expose
<span>/metrics</span><span> for Prometheus to scrape</span>
Technology stack selection:
- Web Framework:
<span>axum</span> - Metrics:
<span>prometric</span> - Expose Metrics:
<span>prometheus::Encoder</span>
Step 1: Define Metrics
use prometric_derive::metrics;
use prometric::{Counter, Histogram};
#[metrics(scope = "shortener")]
struct ShortenerMetrics {
#[metric(labels = ["method", "status"], help = "Total HTTP requests")]
http_requests: Counter,
#[metric(labels = ["error_type"], help = "Number of errors occurred")]
errors: Counter,
#[metric(buckets = [0.005, 0.01, 0.025, 0.05, 0.1], help = "Request processing duration")]
request_duration: Histogram,
}
Step 2: Initialize and Inject AppState
use axum::{Router, routing::get, extract::Extension};
use std::sync::Arc;
use tokio::net::TcpListener;
#[derive(Clone)]
struct AppState {
metrics: Arc<ShortenerMetrics>,
}
async fn run_server() {
let registry = prometheus::Registry::new();
let metrics = ShortenerMetrics::builder()
.with_registry(registry.clone())
.with_label("service", "url-shortener")
.build();
let state = AppState {
metrics: Arc::new(metrics),
};
let app = Router::new()
.route("/shorten", get(shorten_handler))
.route("/go/:key", get(redirect_handler))
.route("/metrics", get(metrics_handler))
.layer(Extension(state));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Step 3: Record Metrics in Handlers
use axum::extract::{Path, Extension};
use std::time::Instant;
async fn shorten_handler(Extension(state): Extension<AppState>) -> Result<String, StatusCode> {
let timer = Instant::now();
// Simulate business logic
if rand::random::<f32>() < 0.1 {
state.metrics.errors("db_failure").inc();
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
state.metrics.http_requests("GET", "200").inc();
state.metrics.request_duration("shorten").observe(timer.elapsed().as_secs_f64());
Ok("OK".to_string())
}
async fn redirect_handler(
Path(key): Path<String>,
Extension(state): Extension<AppState>,
) -> Result<Redirect, StatusCode> {
let timer = Instant::now();
// Look up short link...
if key == "bad" {
state.metrics.http_requests("GET", "404").inc();
state.metrics.errors("not_found").inc();
return Err(StatusCode::NOT_FOUND);
}
state.metrics.http_requests("GET", "302").inc();
state.metrics.request_duration("redirect").observe(timer.elapsed().as_secs_f64());
Ok(Redirect::permanent(&format!("https://example.com")))
}
Step 4: Expose the <span>/metrics</span><span> Interface</span>
use axum::response::IntoResponse;
use prometheus::{Encoder, TextEncoder};
async fn metrics_handler() -> impl IntoResponse {
let encoder = TextEncoder::new();
let mut buf = Vec::new();
let metric_families = prometheus::gather();
encoder.encode(&metric_families, &mut buf).unwrap();
([(axum::http::header::CONTENT_TYPE, encoder.format_type())], buf)
}
After starting, visit <span>http://localhost:3000/metrics</span><span>, and you will see output like this:</span>
# HELP shortener_http_requests Total HTTP requests
# TYPE shortener_http_requests counter
shortener_http_requests{method="GET",service="url-shortener",status="200"} 7
shortener_http_requests{method="GET",service="url-shortener",status="404"} 2
# HELP shortener_request_duration Request processing duration
# TYPE shortener_request_duration histogram
shortener_request_duration_bucket{...,le="0.01"} 5
shortener_request_duration_bucket{...,le="0.025"} 8
...
Then you can scrape it with Prometheus and create beautiful dashboards in Grafana!
0x0007 |Comparison with Other Solutions: Where Does <span>prometric</span> Win?
There are actually quite a few Rust metrics solutions on the market, let’s compare them horizontally.
| Solution | Advantages | Disadvantages | Applicable Scenarios |
|---|---|---|---|
Native <span>prometheus</span> crate |
Complete control, mature and stable | Lots of boilerplate code, prone to errors | High performance requirements, need fine control |
<span>metrics</span> + <span>metrics-exporter-prometheus</span> |
Cross-backend compatibility, rich ecosystem | Multiple abstraction layers, slightly lower performance | Multiple monitoring systems coexist |
<span>tokio-metrics</span> |
Designed for Tokio, lightweight | Single functionality | Asynchronous task monitoring |
<span>prometric</span> |
Zero boilerplate, type-safe, supports dynamic labels | New project, small community | Rapid development, focus on simplicity |
It can be seen that <span>prometric</span><span> has a very clear positioning:</span>
Not the strongest, but definitely the most worry-free.
It is particularly suitable for:
- Quickly integrating monitoring during the MVP stage
- Low entry barrier for new team members
- Focusing on business rather than infrastructure plumbing
Moreover, it directly depends on the <span>prometheus</span> crate, with no intermediate abstraction layer, making performance loss almost negligible.
0x0008 |Pitfall Guide: Common Traps You Might Encounter
No tool is perfect, here are some common pitfalls:
❌ Incorrectly Using Counter to Record Negative Values
metrics.user_count().inc_by(-1); // ❌ Dangerous! Counter should only increase
Although <span>inc_by(-1)</span> syntax is valid, it violates Prometheus best practices. It should be replaced with Gauge.
⚠️ Label Combination Explosion (Cardinality Explosion)
#[metric(labels = ["user_id"])] // If user_id is UUID, it will lead to millions of time series!
per_user_counter: Counter,
Consequences: Memory spikes, Prometheus OOM, slow queries.
✅ Correct Approach: Only label low cardinality dimensions (like status, method), use exemplars or log associations for high cardinality.
🔁 Multiple Builds Leading to Duplicate Registrations
let m1 = AppMetrics::builder().build();
let m2 = AppMetrics::builder().build(); // panic: duplicate metric
Because the same metric name cannot be registered twice.
✅ Solution: Global singleton, or use custom registry for isolation.
🐞 Forgetting to Clean Registry During Testing
Frequent registrations in unit tests can lead to conflicts.
✅ Recommended Practice:
#[cfg(test)]
mod tests {
use prometheus::register;
fn fresh_registry() -> Registry {
let r = Registry::new();
// Manually register to avoid global pollution
r
}
}
0x0009 |Future Prospects: How Can <span>prometric</span> Evolve?
Although <span>prometric</span><span> is still very young (only released a few months ago, with 17 stars), I see great potential in it.</span>
Here are a few promising directions for improvement:
✅ Support for Summary Type
Currently, Summary is not supported, but in certain scenarios (like TCP RTT statistics), Summary is more suitable than Histogram.
Hope to add in the future:
#[metric(quantiles = [(0.5, 0.05), (0.9, 0.01), (0.99, 0.001)])]
tcp_rtt: Summary,
🧩 More Flexible Label Binding Mechanism
For example, allowing binding some labels in the builder, leaving the rest for runtime:
let metrics = AppMetrics::builder()
.bind_labels(["method"]) // Fix method in advance
.build();
metrics.http_requests("GET").inc(); // Only need to pass path
📦 Integrate with tracing-subscriber
Combine with the <span>tracing</span> ecosystem to achieve automatic metrics from spans, truly achieving “invisible monitoring”.
Imagine:
#[tracing::instrument]
async fn process_payment() {
// Automatically record duration, success/failure
}
No need to manually call <span>observe()</span><span>, the framework does it all.</span>
0x0010 |Conclusion: Let Monitoring Return to Its Essence, Not a Burden
Finally, I want to say:
Monitoring should not be the enemy of developers, but the most loyal partner.
We write code to create value, not to scratch our heads over logs every day.
<span>prometric</span><span> and tools like it are designed to make observability more natural and painless.</span>
It doesn’t show off, doesn’t over-design, it just quietly automates repetitive tasks, allowing you to focus on what truly matters—user experience, system stability, business growth.
So, the next time you have to write a bunch of <span>IntCounterVec::new(...)</span><span>, stop and ask yourself:</span>
“Am I reinventing the wheel?”
Perhaps there are already tools like <span>prometric</span><span> willing to take on this burden for you.</span>
📌 Recommended Further Reading:
- Prometheus Official Documentation[2]
- `prometheus` crate docs[3]
- `metrics-derive`[4] — Inspiration for
<span>prometric</span> - Rust ❤️ Prometheus: Monitoring Made Elegant[5] — Lorenzo’s classic article
🚀 Give it a try!
# Cargo.toml
[dependencies]
prometric = "0.1"
prometric-derive = "0.1"
Then follow the examples in this article to add your first set of metrics to your project.
You will find: monitoring can be this enjoyable.
References
[1]
https://github.com/chainbound/prometric: https://github.com/chainbound/prometric
[2]
Prometheus Official Documentation: https://prometheus.io/docs/
[3]
<span>prometheus</span> crate docs: https://docs.rs/prometheus/latest/prometheus/
[4]
<span>metrics-derive</span><span>: </span><span>https://github.com/ithacaxyz/metrics-derive</span>
[5]
Rust ❤️ Prometheus: Monitoring Made Elegant: https://www.lpalmieri.com/posts/2022/06/monitoring-rust-applications-with-prometheus/