Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
**/target
/.idea
Cargo.lock
**/*.jsonl
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

## Unreleased
- Mark `MetricsHandle` as `Send` and `Sync` with an explicit safety contract: it is only safe if backends are either thread-safe (internally synchronized) or are used in a strictly single-threaded manner. This allows allocator counters to live in a `static LazyLock` and fixes the `*mut u8`-derived `Sync` error without changing runtime behavior.
- Clarify allocator instrumentation behavior: allocation counters cache the active handle on first use, so `set_metrics` must run before allocator instrumentation is enabled if you expect these counters to emit.
- Move the release configuration to `[workspace.metadata.release]` to eliminate Cargo's unused manifest key warning while preserving the same metadata for release tooling.
- Remove an extra blank line after the `Histogram` doc comment to satisfy `clippy::empty_line_after_doc_comments` (no behavior change).
- Skip span timing work when the no-op metrics handle is active, returning a no-op span that avoids `Instant::now()`/`rdtsc` and record calls. This reduces overhead when metrics are disabled while keeping behavior unchanged when a real backend is set.
- Avoid u128 nanosecond conversion on non-`rdtsc` span drops by computing nanos from seconds + subsecond nanos with wrapping arithmetic. This keeps the u64 return value behavior while trimming a small amount of conversion overhead.
- Cache the active metrics handle inside `Counter`/`Histogram` so hot-path operations avoid an atomic load. This tightens the initialization contract: `set_metrics` must run before any counters/histograms (including macro statics) are created.
- Replace mutable global metrics access with an immutable handle, moving metrics vtable calls to `&self` and using relaxed loads plus an explicit SeqCst store for the global handle in `set_metrics`. This removes `static mut` access and `get_mut` usage, reducing unsafe mutable aliasing while keeping hot-path reads fast when initialization happens before worker threads.
- Switch counters/histograms and proc-macro statics to `LazyCell` with `LazyCell::force`, dropping `UnsafeCell` and `LazyLock` implementations. This narrows the API surface to a single lazy init path and avoids raw mutable access inside statics while preserving lazy initialization semantics.
- Keep exporter publish signatures using the `Counters`/`Histograms` aliases (including top-level/UDP exporters) for consistency, even though they are just `HashMap<Id, _>` type aliases.
- Add a default-on `span` feature: when disabled, `Span` becomes a no-op type and timing code is compiled out; when enabled, span timing behaves as before.

This refactor makes `MetricsHandle` immutable because it is just a vtable pointer and backend synchronization lives behind it, so callers do not need mutable access to the handle. With an immutable global handle, we can remove `static mut` and `&mut` aliasing, and then use `LazyCell::force` to get shared references to counters and histograms without `UnsafeCell`, making the API cleaner and safer.

Breaking: `CounterOps`/`HistogramOps` are no longer implemented for `LazyLock<UnsafeCell<...>>`. Code using `LazyLock` should switch to `LazyCell` or use a plain `Counter`/`Histogram` value.
Behavioral note: `set_metrics` must run before counters/histograms are created (including macro statics) and before worker threads start; metrics cache the handle and relaxed loads may otherwise observe the old backend.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ members = [
]
resolver = "3"

[release]
[workspace.metadata.release]
workspace = true

[workspace.dependencies]
Expand Down
4 changes: 2 additions & 2 deletions metricus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ categories.workspace = true
rust-version.workspace = true

[features]
default = []
default = ["span"]
span = []
rdtsc = ["dep:quanta"]

[dependencies]
Expand All @@ -33,4 +34,3 @@ harness = false
name = "dispatch"
path = "benches/dispatch.rs"
harness = false

52 changes: 28 additions & 24 deletions metricus/src/counter.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
//! A `Counter` proxy struct for managing a metrics counter.

use crate::access::get_metrics_mut;
use crate::{Id, Tags};
use std::cell::{LazyCell, UnsafeCell};
use std::sync::LazyLock;
use crate::access::get_metrics;
use crate::{Id, MetricsHandle, Tags};
use std::cell::LazyCell;

/// Provides methods to create a new counter, increment it, and
/// increment it by a specified amount. It automatically deletes the counter
Expand Down Expand Up @@ -36,9 +35,15 @@ use std::sync::LazyLock;
///
/// my_function_with_tags();
/// ````
#[derive(Debug)]
pub struct Counter {
id: Id,
handle: &'static MetricsHandle,
}

impl std::fmt::Debug for Counter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Counter").field("id", &self.id).finish()
}
}

impl Counter {
Expand All @@ -61,8 +66,12 @@ impl Counter {
/// let counter = Counter::new("user_count", empty_tags());
/// ```
pub fn new(name: &str, tags: Tags) -> Self {
let counter_id = get_metrics_mut().new_counter(name, tags);
Self { id: counter_id }
let metrics = get_metrics();
let counter_id = metrics.new_counter(name, tags);
Self {
id: counter_id,
handle: metrics,
}
}

/// Create a counter object without registering it.
Expand All @@ -78,13 +87,14 @@ impl Counter {
/// let counter = Counter::new_with_id(1);
/// ```
pub fn new_with_id(id: Id) -> Self {
Self { id }
let metrics = get_metrics();
Self { id, handle: metrics }
}
}

impl Drop for Counter {
fn drop(&mut self) {
get_metrics_mut().delete_counter(self.id);
self.handle.delete_counter(self.id);
}
}

Expand Down Expand Up @@ -116,31 +126,25 @@ pub trait CounterOps {
}

impl CounterOps for Counter {
#[inline]
fn increment(&self) {
get_metrics_mut().increment_counter(self.id);
}

fn increment_by(&self, delta: u64) {
get_metrics_mut().increment_counter_by(self.id, delta);
}
}

impl CounterOps for LazyCell<UnsafeCell<Counter>> {
fn increment(&self) {
unsafe { &mut *self.get() }.increment()
self.handle.increment_counter(self.id);
}

#[inline]
fn increment_by(&self, delta: u64) {
unsafe { &mut *self.get() }.increment_by(delta)
self.handle.increment_counter_by(self.id, delta);
}
}

impl CounterOps for LazyLock<UnsafeCell<Counter>> {
impl<F: FnOnce() -> Counter> CounterOps for LazyCell<Counter, F> {
#[inline]
fn increment(&self) {
unsafe { &mut *self.get() }.increment()
LazyCell::force(self).increment()
}

#[inline]
fn increment_by(&self, delta: u64) {
unsafe { &mut *self.get() }.increment_by(delta)
LazyCell::force(self).increment_by(delta)
}
}
124 changes: 93 additions & 31 deletions metricus/src/histogram.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! A `Histogram` proxy struct for managing a metrics histogram.

use crate::access::get_metrics_mut;
use crate::{Id, Tags};
#[cfg(feature = "rdtsc")]
use crate::access::get_metrics;
use crate::{Id, MetricsHandle, Tags};
#[cfg(all(feature = "span", feature = "rdtsc"))]
use quanta::Clock;
use std::cell::{LazyCell, UnsafeCell};
#[cfg(not(feature = "rdtsc"))]
use std::cell::LazyCell;
#[cfg(not(feature = "span"))]
use std::marker::PhantomData;
#[cfg(all(feature = "span", not(feature = "rdtsc")))]
use std::time::Instant;

/// Facilitates the creation of a new histogram, recording of values, and
Expand Down Expand Up @@ -40,14 +42,25 @@ use std::time::Instant;
///
/// my_function_with_tags();
/// ````

#[derive(Debug)]
pub struct Histogram {
id: Id,
#[cfg(feature = "rdtsc")]
handle: &'static MetricsHandle,
#[cfg(all(feature = "span", feature = "rdtsc"))]
clock: Clock,
}

impl std::fmt::Debug for Histogram {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Histogram");
debug.field("id", &self.id);
#[cfg(all(feature = "span", feature = "rdtsc"))]
{
debug.field("clock", &self.clock);
}
debug.finish()
}
}

impl Histogram {
/// Creates a new histogram with the specified name and tags.
/// Units of measurement are not defined by the histogram itself but should be implied
Expand All @@ -70,10 +83,12 @@ impl Histogram {
/// let histogram = Histogram::new("login_duration", empty_tags());
/// ```
pub fn new(name: &str, tags: Tags) -> Self {
let histogram_id = get_metrics_mut().new_histogram(name, tags);
let metrics = get_metrics();
let histogram_id = metrics.new_histogram(name, tags);
Self {
id: histogram_id,
#[cfg(feature = "rdtsc")]
handle: metrics,
#[cfg(all(feature = "span", feature = "rdtsc"))]
clock: Clock::new(),
}
}
Expand Down Expand Up @@ -124,64 +139,111 @@ pub trait HistogramOps {
}

impl HistogramOps for Histogram {
#[inline]
fn record(&self, value: u64) {
get_metrics_mut().record(self.id, value);
self.handle.record(self.id, value);
}

#[inline]
#[cfg(feature = "span")]
fn span(&self) -> Span<'_> {
if std::ptr::eq(self.handle, &crate::NO_OP_METRICS_HANDLE) {
return Span { state: SpanState::NoOp };
}
Span {
histogram: self,
#[cfg(feature = "rdtsc")]
start_raw: self.clock.raw(),
#[cfg(not(feature = "rdtsc"))]
start_instant: Instant::now(),
state: SpanState::Active {
histogram: self,
#[cfg(feature = "rdtsc")]
start_raw: self.clock.raw(),
#[cfg(not(feature = "rdtsc"))]
start_instant: Instant::now(),
},
}
}

#[inline]
#[cfg(not(feature = "span"))]
fn span(&self) -> Span<'_> {
Span { _marker: PhantomData }
}

#[inline]
fn with_span<F: FnOnce() -> R, R>(&self, f: F) -> R {
let _span = self.span();
f()
}
}

impl HistogramOps for LazyCell<UnsafeCell<Histogram>> {
impl<F: FnOnce() -> Histogram> HistogramOps for LazyCell<Histogram, F> {
#[inline]
fn record(&self, value: u64) {
unsafe { &mut *self.get() }.record(value)
LazyCell::force(self).record(value)
}

#[inline]
fn span(&self) -> Span<'_> {
unsafe { &mut *self.get() }.span()
LazyCell::force(self).span()
}

fn with_span<F: FnOnce() -> R, R>(&self, f: F) -> R {
unsafe { &mut *self.get() }.with_span(f)
#[inline]
fn with_span<G: FnOnce() -> R, R>(&self, f: G) -> R {
LazyCell::force(self).with_span(f)
}
}

impl Drop for Histogram {
fn drop(&mut self) {
get_metrics_mut().delete_histogram(self.id);
self.handle.delete_histogram(self.id);
}
}

/// Used for measuring how long given operation takes. The duration is recorded in nanoseconds.
#[cfg(feature = "span")]
pub struct Span<'a> {
histogram: &'a Histogram,
#[cfg(feature = "rdtsc")]
start_raw: u64,
#[cfg(not(feature = "rdtsc"))]
start_instant: Instant,
state: SpanState<'a>,
}

/// No-op span used when the `span` feature is disabled.
#[cfg(not(feature = "span"))]
pub struct Span<'a> {
_marker: PhantomData<&'a ()>,
}

#[cfg(feature = "span")]
enum SpanState<'a> {
Active {
histogram: &'a Histogram,
#[cfg(feature = "rdtsc")]
start_raw: u64,
#[cfg(not(feature = "rdtsc"))]
start_instant: Instant,
},
NoOp,
}

#[cfg(feature = "span")]
impl Drop for Span<'_> {
fn drop(&mut self) {
#[cfg(feature = "rdtsc")]
{
let end_raw = self.histogram.clock.raw();
let elapsed = self.histogram.clock.delta_as_nanos(self.start_raw, end_raw);
self.histogram.record(elapsed);
if let SpanState::Active { histogram, start_raw } = &self.state {
let end_raw = histogram.clock.raw();
let elapsed = histogram.clock.delta_as_nanos(*start_raw, end_raw);
histogram.record(elapsed);
}
}
#[cfg(not(feature = "rdtsc"))]
self.histogram.record(self.start_instant.elapsed().as_nanos() as u64);
if let SpanState::Active {
histogram,
start_instant,
} = &self.state
{
let elapsed = start_instant.elapsed();
let nanos = elapsed
.as_secs()
.wrapping_mul(1_000_000_000)
.wrapping_add(u64::from(elapsed.subsec_nanos()));
histogram.record(nanos);
}
}
}
Loading
Loading