use std::{
collections::HashMap,
convert::Infallible,
fmt::Display,
marker::PhantomData,
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
use opentelemetry_sdk::{
logs::LogProcessor,
metrics::reader::MetricReader,
trace::{IdGenerator, SpanProcessor},
};
use regex::Regex;
use tracing::{Level, level_filters::LevelFilter};
use crate::{ConfigureError, internal::env::get_optional_env, logfire::Logfire};
#[must_use = "call `.finish()` to complete logfire configuration."]
pub struct LogfireConfigBuilder {
pub(crate) local: bool,
pub(crate) send_to_logfire: Option<SendToLogfire>,
pub(crate) token: Option<String>,
pub(crate) service_name: Option<String>,
pub(crate) service_version: Option<String>,
pub(crate) environment: Option<String>,
pub(crate) console_options: Option<ConsoleOptions>,
pub(crate) data_dir: Option<PathBuf>,
pub(crate) additional_span_processors: Vec<BoxedSpanProcessor>,
pub(crate) advanced: Option<AdvancedOptions>,
pub(crate) metrics: Option<MetricsOptions>,
pub(crate) install_panic_handler: bool,
pub(crate) default_level_filter: Option<LevelFilter>,
}
impl Default for LogfireConfigBuilder {
fn default() -> Self {
Self {
local: false,
send_to_logfire: None,
token: None,
service_name: None,
service_version: None,
environment: None,
console_options: None,
data_dir: None,
additional_span_processors: Vec::new(),
advanced: None,
metrics: None,
install_panic_handler: true,
default_level_filter: None,
}
}
}
impl LogfireConfigBuilder {
#[doc(hidden)] pub fn local(mut self) -> Self {
self.local = true;
self
}
pub fn with_install_panic_handler(mut self, install: bool) -> Self {
self.install_panic_handler = install;
self
}
#[deprecated(since = "0.8.0", note = "noop; now installed by default")]
pub fn install_panic_handler(self) -> Self {
self
}
pub fn send_to_logfire<T: Into<SendToLogfire>>(mut self, send_to_logfire: T) -> Self {
self.send_to_logfire = Some(send_to_logfire.into());
self
}
pub fn with_token<T: Into<String>>(mut self, token: T) -> Self {
self.token = Some(token.into());
self
}
pub fn with_service_name<T: Into<String>>(mut self, service_name: T) -> Self {
self.service_name = Some(service_name.into());
self
}
pub fn with_service_version<T: Into<String>>(mut self, service_version: T) -> Self {
self.service_version = Some(service_version.into());
self
}
pub fn with_environment<T: Into<String>>(mut self, environment: T) -> Self {
self.environment = Some(environment.into());
self
}
pub fn with_console(mut self, console_options: Option<ConsoleOptions>) -> Self {
self.console_options = console_options;
self
}
pub fn with_data_dir<T: Into<PathBuf>>(mut self, data_dir: T) -> Self {
self.data_dir = Some(data_dir.into());
self
}
pub fn with_default_level_filter(mut self, default_level_filter: LevelFilter) -> Self {
self.default_level_filter = Some(default_level_filter);
self
}
pub fn with_additional_span_processor<T: SpanProcessor + 'static>(
mut self,
span_processor: T,
) -> Self {
self.additional_span_processors
.push(BoxedSpanProcessor::new(Box::new(span_processor)));
self
}
pub fn with_advanced_options(mut self, advanced: AdvancedOptions) -> Self {
self.advanced = Some(advanced);
self
}
pub fn with_metrics(mut self, metrics: Option<MetricsOptions>) -> Self {
self.metrics = metrics;
self
}
pub fn finish(self) -> Result<Logfire, ConfigureError> {
Logfire::from_config_builder(self)
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum SendToLogfire {
#[default]
Yes,
No,
IfTokenPresent,
}
impl Display for SendToLogfire {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SendToLogfire::Yes => write!(f, "yes"),
SendToLogfire::No => write!(f, "no"),
SendToLogfire::IfTokenPresent => write!(f, "if-token-present"),
}
}
}
impl FromStr for SendToLogfire {
type Err = ConfigureError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"yes" => Ok(SendToLogfire::Yes),
"no" => Ok(SendToLogfire::No),
"if-token-present" => Ok(SendToLogfire::IfTokenPresent),
_ => Err(ConfigureError::InvalidConfigurationValue {
parameter: "LOGFIRE_SEND_TO_LOGFIRE",
value: s.to_owned(),
}),
}
}
}
impl From<bool> for SendToLogfire {
fn from(b: bool) -> Self {
if b {
SendToLogfire::Yes
} else {
SendToLogfire::No
}
}
}
#[derive(Debug, Clone)]
pub struct ConsoleOptions {
pub(crate) colors: ConsoleColors,
pub(crate) target: Target,
#[deprecated(
since = "0.9.0",
note = "field access will be removed; use builder methods"
)]
pub include_timestamps: bool,
#[deprecated(
since = "0.9.0",
note = "field access will be removed; use builder methods"
)]
pub min_log_level: Level,
}
impl Default for ConsoleOptions {
fn default() -> Self {
ConsoleOptions {
colors: ConsoleColors::default(),
#[expect(deprecated)]
min_log_level: Level::INFO,
target: Target::default(),
#[expect(deprecated)]
include_timestamps: true,
}
}
}
impl ConsoleOptions {
#[must_use]
pub fn with_target(mut self, target: Target) -> Self {
self.target = target;
self
}
}
impl ConsoleOptions {
#[must_use]
pub fn with_colors(mut self, colors: ConsoleColors) -> Self {
self.colors = colors;
self
}
#[must_use]
#[expect(deprecated, reason = "this builder method replaces field access")]
pub fn with_include_timestamps(mut self, include: bool) -> Self {
self.include_timestamps = include;
self
}
#[must_use]
#[expect(deprecated, reason = "this builder method replaces field access")]
pub fn with_min_log_level(mut self, min_log_level: Level) -> Self {
self.min_log_level = min_log_level;
self
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum ConsoleColors {
#[default]
Auto,
Always,
Never,
}
#[derive(Default, Debug, Clone, Copy)]
pub enum SpanStyle {
Simple,
Indented,
#[default]
ShowParents,
}
#[derive(Default, Clone)]
pub enum Target {
Stdout,
#[default]
Stderr,
Pipe(Arc<Mutex<dyn std::io::Write + Send + 'static>>),
}
impl std::fmt::Debug for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Target::Stdout => write!(f, "stdout"),
Target::Stderr => write!(f, "stderr"),
Target::Pipe(_) => write!(f, "pipe"),
}
}
}
#[derive(Default)]
pub struct AdvancedOptions {
pub(crate) base_url: Option<String>,
pub(crate) id_generator: Option<BoxedIdGenerator>,
pub(crate) resources: Vec<opentelemetry_sdk::Resource>,
pub(crate) log_record_processors: Vec<BoxedLogProcessor>,
pub(crate) enable_tracing_metrics: bool,
}
impl AdvancedOptions {
#[must_use]
pub fn with_base_url<T: AsRef<str>>(mut self, base_url: T) -> Self {
self.base_url = Some(base_url.as_ref().into());
self
}
#[must_use]
pub fn with_id_generator<T: IdGenerator + Send + Sync + 'static>(
mut self,
generator: T,
) -> Self {
self.id_generator = Some(BoxedIdGenerator::new(Box::new(generator)));
self
}
#[must_use]
pub fn with_resource(mut self, resource: opentelemetry_sdk::Resource) -> Self {
self.resources.push(resource);
self
}
#[must_use]
pub fn with_log_processor<T: LogProcessor + Send + Sync + 'static>(
mut self,
processor: T,
) -> Self {
self.log_record_processors
.push(BoxedLogProcessor::new(Box::new(processor)));
self
}
#[must_use]
pub fn with_tracing_metrics(mut self, enable: bool) -> Self {
self.enable_tracing_metrics = enable;
self
}
}
struct RegionData {
base_url: &'static str,
#[expect(dead_code)] gcp_region: &'static str,
}
const US_REGION: RegionData = RegionData {
base_url: "https://logfire-us.pydantic.dev",
gcp_region: "us-east4",
};
const EU_REGION: RegionData = RegionData {
base_url: "https://logfire-eu.pydantic.dev",
gcp_region: "europe-west4",
};
pub(crate) fn get_base_url_from_token(token: &str) -> &'static str {
let pydantic_logfire_token_pattern = Regex::new(
r"^(?P<safe_part>pylf_v(?P<version>[0-9]+)_(?P<region>[a-z]+)_)(?P<token>[a-zA-Z0-9]+)$",
)
.expect("token regex is known to be valid");
#[expect(clippy::wildcard_in_or_patterns, reason = "being explicit about us")]
match pydantic_logfire_token_pattern
.captures(token)
.and_then(|captures| captures.name("region"))
.map(|region| region.as_str())
{
Some("eu") => EU_REGION.base_url,
Some("us") | _ => US_REGION.base_url,
}
}
#[derive(Default)]
pub struct MetricsOptions {
pub(crate) additional_readers: Vec<BoxedMetricReader>,
}
impl MetricsOptions {
#[must_use]
pub fn with_additional_reader<T: MetricReader>(mut self, reader: T) -> Self {
self.additional_readers
.push(BoxedMetricReader::new(Box::new(reader)));
self
}
}
#[derive(Debug)]
pub(crate) struct BoxedSpanProcessor(Box<dyn SpanProcessor>);
impl BoxedSpanProcessor {
pub fn new(processor: Box<dyn SpanProcessor + Send + Sync>) -> Self {
BoxedSpanProcessor(processor)
}
}
impl SpanProcessor for BoxedSpanProcessor {
fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) {
self.0.on_start(span, cx);
}
fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {
self.0.on_end(span);
}
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.force_flush()
}
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown()
}
fn shutdown_with_timeout(
&self,
timeout: std::time::Duration,
) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown_with_timeout(timeout)
}
fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
self.0.set_resource(resource);
}
}
#[derive(Debug)]
pub(crate) struct BoxedIdGenerator(Box<dyn IdGenerator>);
impl BoxedIdGenerator {
pub fn new(generator: Box<dyn IdGenerator>) -> Self {
BoxedIdGenerator(generator)
}
}
impl IdGenerator for BoxedIdGenerator {
fn new_trace_id(&self) -> opentelemetry::trace::TraceId {
self.0.new_trace_id()
}
fn new_span_id(&self) -> opentelemetry::trace::SpanId {
self.0.new_span_id()
}
}
#[derive(Debug)]
pub(crate) struct BoxedMetricReader(Box<dyn MetricReader>);
impl BoxedMetricReader {
pub fn new(reader: Box<dyn MetricReader>) -> Self {
BoxedMetricReader(reader)
}
}
impl MetricReader for BoxedMetricReader {
fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
self.0.register_pipeline(pipeline);
}
fn collect(
&self,
rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.collect(rm)
}
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.force_flush()
}
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown()
}
fn shutdown_with_timeout(
&self,
timeout: std::time::Duration,
) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown_with_timeout(timeout)
}
fn temporality(
&self,
kind: opentelemetry_sdk::metrics::InstrumentKind,
) -> opentelemetry_sdk::metrics::Temporality {
self.0.temporality(kind)
}
}
#[derive(Debug)]
pub(crate) struct BoxedLogProcessor(Box<dyn LogProcessor + Send + Sync>);
impl BoxedLogProcessor {
pub fn new(processor: Box<dyn LogProcessor + Send + Sync>) -> Self {
Self(processor)
}
}
impl LogProcessor for BoxedLogProcessor {
fn emit(
&self,
log_record: &mut opentelemetry_sdk::logs::SdkLogRecord,
instrumentation_scope: &opentelemetry::InstrumentationScope,
) {
self.0.emit(log_record, instrumentation_scope);
}
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.force_flush()
}
fn shutdown_with_timeout(
&self,
timeout: std::time::Duration,
) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown_with_timeout(timeout)
}
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
self.0.shutdown()
}
fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
self.0.set_resource(resource);
}
}
pub(crate) trait ParseConfigValue: Sized {
fn parse_config_value(s: &str) -> Result<Self, ConfigureError>;
}
impl<T> ParseConfigValue for T
where
T: FromStr,
ConfigureError: From<T::Err>,
{
fn parse_config_value(s: &str) -> Result<Self, ConfigureError> {
Ok(s.parse()?)
}
}
pub(crate) struct ConfigValue<T> {
env_vars: &'static [&'static str],
default_value: fn() -> T,
}
impl<T> ConfigValue<T> {
const fn new(env_vars: &'static [&'static str], default_value: fn() -> T) -> Self {
Self {
env_vars,
default_value,
}
}
}
impl<T: ParseConfigValue> ConfigValue<T> {
pub(crate) fn resolve(
&self,
value: Option<T>,
env: Option<&HashMap<String, String>>,
) -> Result<T, ConfigureError> {
if let Some(v) = try_resolve_from_env(value, self.env_vars, env)? {
return Ok(v);
}
Ok((self.default_value)())
}
}
pub(crate) struct OptionalConfigValue<T> {
env_vars: &'static [&'static str],
default_value: PhantomData<Option<T>>,
}
impl<T> OptionalConfigValue<T> {
const fn new(env_vars: &'static [&'static str]) -> Self {
Self {
env_vars,
default_value: PhantomData,
}
}
}
impl<T: ParseConfigValue> OptionalConfigValue<T> {
pub(crate) fn resolve(
&self,
value: Option<T>,
env: Option<&HashMap<String, String>>,
) -> Result<Option<T>, ConfigureError> {
try_resolve_from_env(value, self.env_vars, env)
}
}
fn try_resolve_from_env<T>(
value: Option<T>,
env_vars: &[&str],
env: Option<&HashMap<String, String>>,
) -> Result<Option<T>, ConfigureError>
where
T: ParseConfigValue,
{
if let Some(v) = value {
return Ok(Some(v));
}
for var in env_vars {
if let Some(s) = get_optional_env(var, env)? {
return T::parse_config_value(&s).map(Some);
}
}
Ok(None)
}
impl From<Infallible> for ConfigureError {
fn from(_: Infallible) -> Self {
unreachable!("Infallible cannot be constructed")
}
}
pub(crate) static LOGFIRE_SEND_TO_LOGFIRE: ConfigValue<SendToLogfire> =
ConfigValue::new(&["LOGFIRE_SEND_TO_LOGFIRE"], || SendToLogfire::Yes);
pub(crate) static LOGFIRE_SERVICE_NAME: OptionalConfigValue<String> =
OptionalConfigValue::new(&["LOGFIRE_SERVICE_NAME", "OTEL_SERVICE_NAME"]);
pub(crate) static LOGFIRE_SERVICE_VERSION: OptionalConfigValue<String> =
OptionalConfigValue::new(&["LOGFIRE_SERVICE_VERSION", "OTEL_SERVICE_VERSION"]);
pub(crate) static LOGFIRE_ENVIRONMENT: OptionalConfigValue<String> =
OptionalConfigValue::new(&["LOGFIRE_ENVIRONMENT"]);
#[cfg(test)]
mod tests {
use crate::config::SendToLogfire;
#[test]
fn test_send_to_logfire_from_bool() {
assert_eq!(SendToLogfire::from(true), SendToLogfire::Yes);
assert_eq!(SendToLogfire::from(false), SendToLogfire::No);
}
#[test]
#[expect(deprecated)]
fn test_console_options_with_timestamps() {
let options = super::ConsoleOptions::default().with_include_timestamps(false);
assert!(!options.include_timestamps);
let options = super::ConsoleOptions::default().with_include_timestamps(true);
assert!(options.include_timestamps);
}
#[test]
#[expect(deprecated)]
fn test_console_option_with_min_log_level() {
let console_options = super::ConsoleOptions::default();
assert_eq!(console_options.min_log_level, tracing::Level::INFO);
let console_options =
super::ConsoleOptions::default().with_min_log_level(tracing::Level::DEBUG);
assert_eq!(console_options.min_log_level, tracing::Level::DEBUG);
}
}