Skip to content

Commit 1a0566b

Browse files
committed
feat: add metrics-exporter-opentelemetry crate
1 parent 08c10f5 commit 1a0566b

File tree

9 files changed

+345
-2
lines changed

9 files changed

+345
-2
lines changed

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"metrics",
55
"metrics-benchmark",
66
"metrics-exporter-dogstatsd",
7+
"metrics-exporter-opentelemetry",
78
"metrics-exporter-prometheus",
89
"metrics-exporter-tcp",
910
"metrics-observer",
@@ -48,6 +49,10 @@ ndarray = { version = "0.16", default-features = false }
4849
ndarray-stats = { version = "0.6", default-features = false }
4950
noisy_float = { version = "0.2", default-features = false }
5051
once_cell = { version = "1", default-features = false, features = ["std"] }
52+
opentelemetry = { version = "0.30", default-features = false }
53+
opentelemetry_sdk = { version = "0.30", default-features = false }
54+
opentelemetry-otlp = { version = "0.30", default-features = false }
55+
opentelemetry-stdout = { version = "0.30", default-features = false }
5156
ordered-float = { version = "4.2", default-features = false }
5257
parking_lot = { version = "0.12", default-features = false }
5358
portable-atomic = { version = "1", default-features = false }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
- Initial release of OpenTelemetry metrics exporter for `metrics`
12+
- Support for counters, gauges, and histograms
13+
- Attribute/label support
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "metrics-exporter-opentelemetry"
3+
version = "0.1.0"
4+
edition = "2021"
5+
rust-version = "1.75.0"
6+
7+
description = "A metrics-compatible exporter for sending metrics to OpenTelemetry collectors."
8+
license = { workspace = true }
9+
authors = ["Metrics Contributors"]
10+
repository = { workspace = true }
11+
homepage = { workspace = true }
12+
documentation = "https://docs.rs/metrics-exporter-opentelemetry"
13+
readme = "README.md"
14+
15+
categories = ["development-tools::debugging"]
16+
keywords = ["metrics", "telemetry", "opentelemetry", "otel", "observability"]
17+
18+
[dependencies]
19+
metrics = { version = "^0.24", path = "../metrics" }
20+
metrics-util = { version = "^0.20", path = "../metrics-util", default-features = false, features = ["registry"] }
21+
opentelemetry = { workspace = true, features = ["metrics"] }
22+
portable-atomic = { workspace = true, features = ["float", "critical-section"] }
23+
24+
[package.metadata.docs.rs]
25+
all-features = true
26+
rustdoc-args = ["--cfg", "docsrs"]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# metrics-exporter-opentelemetry
2+
3+
[![Documentation](https://docs.rs/metrics-exporter-opentelemetry/badge.svg)](https://docs.rs/metrics-exporter-opentelemetry)
4+
5+
A [`metrics`][metrics] exporter for [OpenTelemetry].
6+
7+
## Features
8+
9+
- Export metrics to OpenTelemetry collectors using OTLP
10+
- Support for counters, gauges, and histograms
11+
- Integration with the OpenTelemetry SDK
12+
- Configurable export intervals and endpoints
13+
14+
## Usage
15+
16+
```rust
17+
use metrics::{counter, gauge, histogram};
18+
use metrics_exporter_opentelemetry::OpenTelemetryBuilder;
19+
use opentelemetry_otlp::WithExportConfig;
20+
use opentelemetry_sdk::{
21+
metrics::{PeriodicReader, SdkMeterProvider},
22+
runtime,
23+
};
24+
25+
// Configure OpenTelemetry exporter
26+
let exporter = opentelemetry_otlp::MetricExporter::builder()
27+
.with_tonic()
28+
.with_endpoint("http://localhost:4317")
29+
.build()?;
30+
31+
// Create a periodic reader
32+
let reader = PeriodicReader::builder(exporter, runtime::Tokio)
33+
.with_interval(Duration::from_secs(10))
34+
.build();
35+
36+
// Build the meter provider
37+
let provider = SdkMeterProvider::builder()
38+
.with_reader(reader)
39+
.build();
40+
41+
// Install the metrics exporter
42+
OpenTelemetryBuilder::new()
43+
.with_meter_provider(provider)
44+
.install()?;
45+
46+
// Now you can use the metrics macros
47+
counter!("requests_total").increment(1);
48+
gauge!("cpu_usage").set(0.75);
49+
histogram!("request_duration").record(0.234);
50+
```
51+
52+
## Examples
53+
54+
- [`opentelemetry_push`](examples/opentelemetry_push.rs): Demonstrates exporting metrics to an OpenTelemetry collector
55+
- [`opentelemetry_stdout`](examples/opentelemetry_stdout.rs): Shows metrics export to stdout for debugging
56+
57+
## License
58+
59+
This project is licensed under the MIT license.
60+
61+
[metrics]: https://github.com/metrics-rs/metrics
62+
[OpenTelemetry]: https://opentelemetry.io/
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! OpenTelemetry instrument wrappers for metrics traits.
2+
3+
use metrics::{CounterFn, GaugeFn, HistogramFn};
4+
use opentelemetry::metrics::{
5+
AsyncInstrumentBuilder, Histogram, ObservableCounter, ObservableGauge,
6+
};
7+
use opentelemetry::KeyValue;
8+
use portable_atomic::{AtomicF64, Ordering};
9+
use std::sync::atomic::AtomicU64;
10+
use std::sync::Arc;
11+
12+
pub struct OtelCounter {
13+
#[allow(dead_code)] // prevent from drop
14+
counter: ObservableCounter<u64>,
15+
value: Arc<AtomicU64>,
16+
}
17+
18+
impl OtelCounter {
19+
pub fn new(
20+
counter_builder: AsyncInstrumentBuilder<ObservableCounter<u64>, u64>,
21+
attributes: Vec<KeyValue>,
22+
) -> Self {
23+
let value = Arc::new(AtomicU64::new(0));
24+
let value_moved = Arc::clone(&value);
25+
let otel_counter = counter_builder
26+
.with_callback(move |observer| {
27+
observer.observe(value_moved.load(Ordering::Relaxed), &attributes);
28+
})
29+
.build();
30+
Self { counter: otel_counter, value }
31+
}
32+
}
33+
34+
impl CounterFn for OtelCounter {
35+
fn increment(&self, value: u64) {
36+
self.value.fetch_add(value, Ordering::Relaxed);
37+
}
38+
39+
fn absolute(&self, value: u64) {
40+
self.value.store(value, Ordering::Relaxed);
41+
}
42+
}
43+
44+
pub struct OtelGauge {
45+
#[allow(dead_code)] // prevent from drop
46+
gauge: ObservableGauge<f64>,
47+
value: Arc<AtomicF64>,
48+
}
49+
50+
impl OtelGauge {
51+
pub fn new(
52+
gauge_builder: AsyncInstrumentBuilder<ObservableGauge<f64>, f64>,
53+
attributes: Vec<KeyValue>,
54+
) -> Self {
55+
let value = Arc::new(AtomicF64::new(0.0));
56+
let value_moved = value.clone();
57+
let otel_gauge = gauge_builder
58+
.with_callback(move |observer| {
59+
observer.observe(value_moved.load(Ordering::Relaxed), &attributes);
60+
})
61+
.build();
62+
Self { gauge: otel_gauge, value }
63+
}
64+
}
65+
66+
impl GaugeFn for OtelGauge {
67+
fn increment(&self, value: f64) {
68+
self.value.fetch_add(value, Ordering::Relaxed);
69+
}
70+
71+
fn decrement(&self, value: f64) {
72+
self.value.fetch_sub(value, Ordering::Relaxed);
73+
}
74+
75+
fn set(&self, value: f64) {
76+
self.value.store(value, Ordering::Relaxed);
77+
}
78+
}
79+
80+
pub struct OtelHistogram {
81+
histogram: Histogram<f64>,
82+
attributes: Vec<KeyValue>,
83+
}
84+
85+
impl OtelHistogram {
86+
pub fn new(histogram: Histogram<f64>, attributes: Vec<KeyValue>) -> Self {
87+
Self { histogram, attributes }
88+
}
89+
}
90+
91+
impl HistogramFn for OtelHistogram {
92+
fn record(&self, value: f64) {
93+
self.histogram.record(value, &self.attributes);
94+
}
95+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//! An OpenTelemetry metrics exporter for `metrics`.
2+
mod instruments;
3+
mod storage;
4+
5+
use metrics::{Counter, Gauge, Histogram, Key, KeyName, Recorder, SharedString, Unit};
6+
use metrics_util::registry::{Registry, Storage};
7+
use opentelemetry::metrics::Meter;
8+
use crate::storage::OtelMetricStorage;
9+
10+
/// The OpenTelemetry recorder.
11+
pub struct OpenTelemetryRecorder {
12+
registry: Registry<Key, OtelMetricStorage>,
13+
}
14+
15+
impl OpenTelemetryRecorder {
16+
/// Creates a new OpenTelemetry recorder with the given meter.
17+
pub fn new(meter: Meter) -> Self {
18+
let storage = OtelMetricStorage::new(meter);
19+
Self {
20+
registry: Registry::new(storage),
21+
}
22+
}
23+
}
24+
25+
impl Recorder for OpenTelemetryRecorder {
26+
fn describe_counter(&self, _key_name: KeyName, _unit: Option<Unit>, _description: SharedString) {
27+
// Descriptions are handled when creating instruments
28+
}
29+
30+
fn describe_gauge(&self, _key_name: KeyName, _unit: Option<Unit>, _description: SharedString) {
31+
// Descriptions are handled when creating instruments
32+
}
33+
34+
fn describe_histogram(
35+
&self,
36+
_key_name: KeyName,
37+
_unit: Option<Unit>,
38+
_description: SharedString,
39+
) {
40+
// Descriptions are handled when creating instruments
41+
}
42+
43+
fn register_counter(&self, key: &Key, _metadata: &metrics::Metadata<'_>) -> Counter {
44+
self.registry.get_or_create_counter(key, |c| {
45+
Counter::from_arc(c.clone())
46+
})
47+
}
48+
49+
fn register_gauge(&self, key: &Key, _metadata: &metrics::Metadata<'_>) -> Gauge {
50+
self.registry.get_or_create_gauge(key, |g| {
51+
Gauge::from_arc(g.clone())
52+
})
53+
}
54+
55+
fn register_histogram(&self, key: &Key, _metadata: &metrics::Metadata<'_>) -> Histogram {
56+
self.registry.get_or_create_histogram(key, |h| {
57+
Histogram::from_arc(h.clone())
58+
})
59+
}
60+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::sync::Arc;
2+
use opentelemetry::KeyValue;
3+
use opentelemetry::metrics::Meter;
4+
use metrics::Key;
5+
use metrics_util::registry::Storage;
6+
use crate::instruments::{OtelCounter, OtelGauge, OtelHistogram};
7+
8+
pub struct OtelMetricStorage {
9+
meter: Meter,
10+
}
11+
12+
impl OtelMetricStorage {
13+
pub fn new(meter: Meter) -> Self {
14+
Self { meter }
15+
}
16+
17+
fn get_attributes(key: &Key) -> Vec<KeyValue> {
18+
key.labels()
19+
.map(|label| KeyValue::new(label.key().to_string(), label.value().to_string()))
20+
.collect()
21+
}
22+
}
23+
24+
impl Storage<Key> for OtelMetricStorage {
25+
type Counter = Arc<OtelCounter>;
26+
type Gauge = Arc<OtelGauge>;
27+
type Histogram = Arc<OtelHistogram>;
28+
29+
fn counter(&self, key: &Key) -> Self::Counter {
30+
let otel_counter_builder = self
31+
.meter
32+
.u64_observable_counter(key.name().to_string());
33+
let attributes = Self::get_attributes(key);
34+
Arc::new(OtelCounter::new(otel_counter_builder, attributes))
35+
}
36+
37+
fn gauge(&self, key: &Key) -> Self::Gauge {
38+
let builder = self
39+
.meter
40+
.f64_observable_gauge(key.name().to_string());
41+
let attributes = Self::get_attributes(key);
42+
Arc::new(OtelGauge::new(builder, attributes))
43+
}
44+
45+
fn histogram(&self, key: &Key) -> Self::Histogram {
46+
let histogram = self
47+
.meter
48+
.f64_histogram(key.name().to_string())
49+
.build();
50+
let attributes = Self::get_attributes(key);
51+
Arc::new(OtelHistogram::new(histogram, attributes))
52+
53+
}
54+
}

0 commit comments

Comments
 (0)