Skip to content

Commit 9e6483a

Browse files
authored
support configuring metrics (#25)
* support configuring metrics * make tests stable
1 parent c3e738d commit 9e6483a

File tree

10 files changed

+2121
-1664
lines changed

10 files changed

+2121
-1664
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Add `span_exporter()` helper to build a span exporter directly in [#20](https://github.com/pydantic/logfire-rust/pull/20)
44
- Fix `set_resource` not being called on span processors added with `with_additional_span_processor()` in [#20](https://github.com/pydantic/logfire-rust/pull/20)
5+
- Add `MetricsOptions` for configuring additional metrics exporters in [#21](https://github.com/pydantic/logfire-rust/pull/21)
56

67
## [v0.1.0] (2025-03-13)
78

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ nu-ansi-term = "0.50.1"
3535
chrono = "0.4.39"
3636

3737
[dev-dependencies]
38+
async-trait = "0.1.88"
3839
insta = "1.42.1"
3940
opentelemetry_sdk = { version = "0.28", default-features = false, features = ["testing"] }
4041
regex = "1.11.1"
42+
tokio = {version = "1.44.1", features = ["test-util"] }
4143
ulid = "1.2.0"
4244

4345
[features]

src/bridges/tracing.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,15 @@ mod tests {
233233
use crate::{
234234
config::{AdvancedOptions, ConsoleOptions, Target},
235235
set_local_logfire,
236-
tests::{DeterministicExporter, DeterministicIdGenerator},
236+
test_utils::{DeterministicExporter, DeterministicIdGenerator},
237237
};
238238

239239
#[test]
240240
fn test_tracing_bridge() {
241241
let exporter = InMemorySpanExporterBuilder::new().build();
242242

243-
let config = crate::configure()
243+
let handler = crate::configure()
244+
.local()
244245
.send_to_logfire(false)
245246
.with_additional_span_processor(SimpleSpanProcessor::new(Box::new(
246247
DeterministicExporter::new(exporter.clone(), file!(), line!()),
@@ -249,11 +250,13 @@ mod tests {
249250
.with_default_level_filter(LevelFilter::TRACE)
250251
.with_advanced_options(
251252
AdvancedOptions::default().with_id_generator(DeterministicIdGenerator::new()),
252-
);
253+
)
254+
.finish()
255+
.unwrap();
253256

254-
let guard = set_local_logfire(config).unwrap();
257+
let guard = set_local_logfire(handler);
255258

256-
tracing::subscriber::with_default(guard.subscriber.clone(), || {
259+
tracing::subscriber::with_default(guard.subscriber().clone(), || {
257260
tracing::info!("root event"); // FIXME: this event is not emitted
258261
tracing::info!(name: "root event with value", field_value = 1); // FIXME: this event is not emitted
259262

@@ -283,7 +286,7 @@ mod tests {
283286
},
284287
parent_span_id: 0000000000000000,
285288
span_kind: Internal,
286-
name: "event src/bridges/tracing.rs:257",
289+
name: "event src/bridges/tracing.rs:260",
287290
start_time: SystemTime {
288291
tv_sec: 0,
289292
tv_nsec: 0,
@@ -336,7 +339,7 @@ mod tests {
336339
"code.lineno",
337340
),
338341
value: I64(
339-
11,
342+
13,
340343
),
341344
},
342345
KeyValue {
@@ -444,7 +447,7 @@ mod tests {
444447
"code.lineno",
445448
),
446449
value: I64(
447-
12,
450+
14,
448451
),
449452
},
450453
KeyValue {
@@ -524,7 +527,7 @@ mod tests {
524527
"code.lineno",
525528
),
526529
value: I64(
527-
14,
530+
16,
528531
),
529532
},
530533
KeyValue {
@@ -630,7 +633,7 @@ mod tests {
630633
"code.lineno",
631634
),
632635
value: I64(
633-
15,
636+
17,
634637
),
635638
},
636639
KeyValue {
@@ -746,7 +749,7 @@ mod tests {
746749
"code.lineno",
747750
),
748751
value: I64(
749-
15,
752+
17,
750753
),
751754
},
752755
KeyValue {
@@ -868,7 +871,7 @@ mod tests {
868871
"code.lineno",
869872
),
870873
value: I64(
871-
16,
874+
18,
872875
),
873876
},
874877
KeyValue {
@@ -984,7 +987,7 @@ mod tests {
984987
"code.lineno",
985988
),
986989
value: I64(
987-
16,
990+
18,
988991
),
989992
},
990993
KeyValue {
@@ -1106,7 +1109,7 @@ mod tests {
11061109
"code.lineno",
11071110
),
11081111
value: I64(
1109-
17,
1112+
19,
11101113
),
11111114
},
11121115
KeyValue {
@@ -1222,7 +1225,7 @@ mod tests {
12221225
"code.lineno",
12231226
),
12241227
value: I64(
1225-
17,
1228+
19,
12261229
),
12271230
},
12281231
KeyValue {
@@ -1344,7 +1347,7 @@ mod tests {
13441347
"code.lineno",
13451348
),
13461349
value: I64(
1347-
14,
1350+
16,
13481351
),
13491352
},
13501353
KeyValue {
@@ -1455,7 +1458,7 @@ mod tests {
14551458
"code.lineno",
14561459
),
14571460
value: I64(
1458-
265,
1461+
268,
14591462
),
14601463
},
14611464
],
@@ -1521,7 +1524,7 @@ mod tests {
15211524
"code.lineno",
15221525
),
15231526
value: I64(
1524-
266,
1527+
269,
15251528
),
15261529
},
15271530
],
@@ -1555,15 +1558,18 @@ mod tests {
15551558
..ConsoleOptions::default()
15561559
};
15571560

1558-
let config = crate::configure()
1561+
let handler = crate::configure()
1562+
.local()
15591563
.send_to_logfire(false)
15601564
.console_options(console_options.clone())
15611565
.install_panic_handler()
1562-
.with_default_level_filter(LevelFilter::TRACE);
1566+
.with_default_level_filter(LevelFilter::TRACE)
1567+
.finish()
1568+
.unwrap();
15631569

1564-
let guard = set_local_logfire(config).unwrap();
1570+
let guard = crate::set_local_logfire(handler);
15651571

1566-
tracing::subscriber::with_default(guard.subscriber.clone(), || {
1572+
tracing::subscriber::with_default(guard.subscriber().clone(), || {
15671573
tracing::info!("root event");
15681574
tracing::info!(name: "root event with value", field_value = 1);
15691575

src/config.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use std::{
88
sync::{Arc, Mutex},
99
};
1010

11-
use opentelemetry_sdk::trace::{IdGenerator, SpanProcessor};
11+
use opentelemetry_sdk::{
12+
metrics::reader::MetricReader,
13+
trace::{IdGenerator, SpanProcessor},
14+
};
1215
use tracing::Level;
1316

1417
use crate::ConfigureError;
@@ -160,6 +163,8 @@ pub struct AdvancedOptions {
160163
pub(crate) base_url: String,
161164
/// Generator for trace and span IDs.
162165
pub(crate) id_generator: Option<BoxedIdGenerator>,
166+
/// Resource to override default resource detection.
167+
pub(crate) resource: Option<opentelemetry_sdk::Resource>,
163168
//
164169
//
165170
// TODO: arguments below supported by Python
@@ -176,6 +181,7 @@ impl Default for AdvancedOptions {
176181
AdvancedOptions {
177182
base_url: "https://logfire-api.pydantic.dev".to_string(),
178183
id_generator: None,
184+
resource: None,
179185
}
180186
}
181187
}
@@ -197,6 +203,32 @@ impl AdvancedOptions {
197203
self.id_generator = Some(BoxedIdGenerator::new(Box::new(generator)));
198204
self
199205
}
206+
207+
/// Set the resource; overrides default resource detection.
208+
#[must_use]
209+
pub fn with_resource(mut self, resource: opentelemetry_sdk::Resource) -> Self {
210+
self.resource = Some(resource);
211+
self
212+
}
213+
}
214+
215+
/// Configuration of metrics.
216+
///
217+
/// This only has one option for now, but it's a place to add more related options in the future.
218+
#[derive(Default)]
219+
pub struct MetricsOptions {
220+
/// Sequence of metric readers to be used in addition to the default which exports metrics to Logfire's API.
221+
pub(crate) additional_readers: Vec<BoxedMetricReader>,
222+
}
223+
224+
impl MetricsOptions {
225+
/// Add a metric reader to the list of additional readers.
226+
#[must_use]
227+
pub fn with_additional_reader<T: MetricReader>(mut self, reader: T) -> Self {
228+
self.additional_readers
229+
.push(BoxedMetricReader::new(Box::new(reader)));
230+
self
231+
}
200232
}
201233

202234
/// Wrapper around a `SpanProcessor` to use in `additional_span_processors`.
@@ -251,6 +283,44 @@ impl IdGenerator for BoxedIdGenerator {
251283
}
252284
}
253285

286+
/// Wrapper around a `MetricReader` to use in `additional_readers`.
287+
#[derive(Debug)]
288+
pub(crate) struct BoxedMetricReader(Box<dyn MetricReader>);
289+
290+
impl BoxedMetricReader {
291+
pub fn new(reader: Box<dyn MetricReader>) -> Self {
292+
BoxedMetricReader(reader)
293+
}
294+
}
295+
296+
impl MetricReader for BoxedMetricReader {
297+
fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
298+
self.0.register_pipeline(pipeline);
299+
}
300+
301+
fn collect(
302+
&self,
303+
rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
304+
) -> opentelemetry_sdk::metrics::MetricResult<()> {
305+
self.0.collect(rm)
306+
}
307+
308+
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
309+
self.0.force_flush()
310+
}
311+
312+
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
313+
self.0.shutdown()
314+
}
315+
316+
fn temporality(
317+
&self,
318+
kind: opentelemetry_sdk::metrics::InstrumentKind,
319+
) -> opentelemetry_sdk::metrics::Temporality {
320+
self.0.temporality(kind)
321+
}
322+
}
323+
254324
#[cfg(test)]
255325
mod tests {
256326
use crate::config::SendToLogfire;

src/exporters.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ macro_rules! feature_required {
2727
}};
2828
}
2929

30-
/// Build a [`SpanExporter`][opentelemetry::trace::SpanExporter] for passing to
30+
/// Build a [`SpanExporter`][opentelemetry_sdk::trace::SpanExporter] for passing to
3131
/// [`with_additional_span_processor()`][crate::LogfireConfigBuilder::with_additional_span_processor].
3232
///
3333
/// This uses `OTEL_EXPORTER_OTLP_PROTOCOL` and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` environment
@@ -98,8 +98,21 @@ pub fn span_exporter(
9898
Ok(RemovePendingSpansExporter::new(span_exporter))
9999
}
100100

101-
// TODO: make this public?
102-
pub(crate) fn metric_exporter(
101+
/// Build a [`PushMetricExporter`][opentelemetry_sdk::metrics::exporter::PushMetricExporter] for passing to
102+
/// [`with_metrics_options()`][crate::LogfireConfigBuilder::with_metrics_options].
103+
///
104+
/// This uses `OTEL_EXPORTER_OTLP_PROTOCOL` and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` environment
105+
/// variables to determine the protocol to use (or otherwise defaults to [`Protocol::HttpBinary`]).
106+
///
107+
/// # Errors
108+
///
109+
/// Returns an error if the protocol specified by the env var is not supported or if the required feature is not enabled for
110+
/// the given protocol.
111+
///
112+
/// Returns an error if the endpoint is not a valid URI.
113+
///
114+
/// Returns an error if any headers are not valid HTTP headers.
115+
pub fn metric_exporter(
103116
endpoint: &str,
104117
headers: Option<HashMap<String, String>>,
105118
) -> Result<impl PushMetricExporter + use<>, ConfigureError> {

src/internal/exporters/console.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ mod tests {
267267
config::{ConsoleOptions, Target},
268268
internal::exporters::console::{ConsoleWriter, SimpleConsoleSpanExporter},
269269
set_local_logfire,
270-
tests::DeterministicExporter,
270+
test_utils::DeterministicExporter,
271271
};
272272

273273
#[test]
@@ -279,7 +279,8 @@ mod tests {
279279
..ConsoleOptions::default()
280280
};
281281

282-
let config = crate::configure()
282+
let handler = crate::configure()
283+
.local()
283284
.send_to_logfire(false)
284285
.with_additional_span_processor(SimpleSpanProcessor::new(Box::new(
285286
DeterministicExporter::new(
@@ -289,12 +290,14 @@ mod tests {
289290
),
290291
)))
291292
.install_panic_handler()
292-
.with_default_level_filter(LevelFilter::TRACE);
293+
.with_default_level_filter(LevelFilter::TRACE)
294+
.finish()
295+
.unwrap();
293296

294-
let guard = set_local_logfire(config).unwrap();
297+
let guard = set_local_logfire(handler);
295298

296299
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
297-
tracing::subscriber::with_default(guard.subscriber.clone(), || {
300+
tracing::subscriber::with_default(guard.subscriber().clone(), || {
298301
let root = crate::span!("root span").entered();
299302
let _ = crate::span!("hello world span").entered();
300303
let _ = crate::span!(level: Level::DEBUG, "debug span");
@@ -316,7 +319,7 @@ mod tests {
316319
1970-01-01T00:00:03.000000Z DEBUG logfire::internal::exporters::console::tests debug span
317320
1970-01-01T00:00:05.000000Z DEBUG logfire::internal::exporters::console::tests debug span with explicit parent
318321
1970-01-01T00:00:07.000000Z INFO logfire::internal::exporters::console::tests hello world log
319-
[2m1970-01-01T00:00:08.000000Z[0m[31m ERROR[0m [2;3mlogfire[0m [1mpanic: oh no![0m [3mlocation[0m=src/internal/exporters/console.rs:303:17, [3mbacktrace[0m=disabled backtrace
322+
[2m1970-01-01T00:00:08.000000Z[0m[31m ERROR[0m [2;3mlogfire[0m [1mpanic: oh no![0m [3mlocation[0m=src/internal/exporters/console.rs:306:17, [3mbacktrace[0m=disabled backtrace
320323
"#);
321324
}
322325
}

0 commit comments

Comments
 (0)