Skip to content
Open
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
60 changes: 60 additions & 0 deletions src/bridges/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2335,6 +2335,36 @@ mod tests {
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.language",
),
value: String(
Static(
"rust",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.name",
),
value: String(
Static(
"opentelemetry",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.version",
),
value: String(
Static(
"0.0.0",
),
),
},
],
scope_metrics: [
DeterministicScopeMetrics {
Expand Down Expand Up @@ -2381,6 +2411,36 @@ mod tests {
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.language",
),
value: String(
Static(
"rust",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.name",
),
value: String(
Static(
"opentelemetry",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.version",
),
value: String(
Static(
"0.0.0",
),
),
},
],
scope_metrics: [
DeterministicScopeMetrics {
Expand Down
111 changes: 110 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
//! See [`LogfireConfigBuilder`] for documentation of all these options.

use std::{
collections::HashMap,
convert::Infallible,
fmt::Display,
marker::PhantomData,
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
Expand All @@ -17,7 +20,7 @@ use opentelemetry_sdk::{
use regex::Regex;
use tracing::{Level, level_filters::LevelFilter};

use crate::{ConfigureError, logfire::Logfire};
use crate::{ConfigureError, internal::env::get_optional_env, logfire::Logfire};

/// Builder for logfire configuration, returned from [`logfire::configure()`][crate::configure].
#[must_use = "call `.finish()` to complete logfire configuration."]
Expand Down Expand Up @@ -624,6 +627,112 @@ impl LogProcessor for BoxedLogProcessor {
}
}

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> {
/// Resolves a config value, using the provided value if present, otherwise falling back to the environment variable or the default.
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> {
/// Resolves an optional config value, using the provided value if present, otherwise falling back to the environment variable or `None`.
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;
Expand Down
48 changes: 25 additions & 23 deletions src/logfire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ use crate::{
__macros_impl::LogfireValue,
ConfigureError, LogfireConfigBuilder, ShutdownError,
bridges::tracing::LogfireTracingLayer,
config::{SendToLogfire, get_base_url_from_token},
config::{
LOGFIRE_ENVIRONMENT, LOGFIRE_SEND_TO_LOGFIRE, LOGFIRE_SERVICE_NAME,
LOGFIRE_SERVICE_VERSION, SendToLogfire, get_base_url_from_token,
},
internal::{
env::get_optional_env,
exporters::console::{ConsoleWriter, create_console_processors},
Expand Down Expand Up @@ -158,6 +161,13 @@ impl Logfire {
/// Called by `LogfireConfigBuilder::finish()`.
pub(crate) fn from_config_builder(
config: LogfireConfigBuilder,
) -> Result<Logfire, ConfigureError> {
Self::from_config_builder_and_env(config, None)
}

fn from_config_builder_and_env(
config: LogfireConfigBuilder,
env: Option<&HashMap<String, String>>,
) -> Result<Logfire, ConfigureError> {
let LogfireParts {
local,
Expand All @@ -170,7 +180,7 @@ impl Logfire {
enable_tracing_metrics,
shutdown_sender,
..
} = Self::build_parts(config, None)?;
} = Self::build_parts(config, env)?;

if !local {
// avoid otel logs firing as these messages are sent regarding "global meter provider"
Expand Down Expand Up @@ -276,13 +286,7 @@ impl Logfire {
}
}

let send_to_logfire = match config.send_to_logfire {
Some(send_to_logfire) => send_to_logfire,
None => match get_optional_env("LOGFIRE_SEND_TO_LOGFIRE", env)? {
Some(value) => value.parse()?,
None => SendToLogfire::Yes,
},
};
let send_to_logfire = LOGFIRE_SEND_TO_LOGFIRE.resolve(config.send_to_logfire, env)?;

let send_to_logfire = match send_to_logfire {
SendToLogfire::Yes => true,
Expand All @@ -302,34 +306,32 @@ impl Logfire {
}

// Add service-specific resources from config
let mut service_resource_builder = opentelemetry_sdk::Resource::builder_empty();
let mut has_service_attributes = false;
let mut service_resource_builder = opentelemetry_sdk::Resource::builder();

if let Some(service_name) = config.service_name {
if let Some(service_name) = LOGFIRE_SERVICE_NAME.resolve(config.service_name, env)? {
service_resource_builder = service_resource_builder.with_service_name(service_name);
has_service_attributes = true;
}

if let Some(service_version) = config.service_version {
if let Some(service_version) =
LOGFIRE_SERVICE_VERSION.resolve(config.service_version, env)?
{
service_resource_builder = service_resource_builder.with_attribute(
opentelemetry::KeyValue::new("service.version", service_version),
);
has_service_attributes = true;
}

if let Some(environment) = config.environment {
if let Some(environment) = LOGFIRE_ENVIRONMENT.resolve(config.environment, env)? {
service_resource_builder = service_resource_builder.with_attribute(
opentelemetry::KeyValue::new("deployment.environment.name", environment),
);
has_service_attributes = true;
}

if has_service_attributes {
let service_resource = service_resource_builder.build();
advanced_options.resources.push(service_resource);
}

for resource in advanced_options.resources {
// Use "default" resource first so that user-provided resources can override it
let service_resource = service_resource_builder.build();
for resource in [service_resource]
.into_iter()
.chain(advanced_options.resources)
{
tracer_provider_builder = tracer_provider_builder.with_resource(resource.clone());
logger_provider_builder = logger_provider_builder.with_resource(resource.clone());
meter_provider_builder = meter_provider_builder.with_resource(resource);
Expand Down
8 changes: 7 additions & 1 deletion src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,18 @@ pub fn remap_timestamps_in_console_output(output: &str) -> Cow<'_, str> {
}

/// `Resource` contains a hashmap, so deterministic tests need to convert to an ordered container.
fn make_deterministic_resource(resource: &Resource) -> Vec<KeyValue> {
pub fn make_deterministic_resource(resource: &Resource) -> Vec<KeyValue> {
let mut attrs: Vec<_> = resource
.iter()
.map(|(k, v)| KeyValue::new(k.clone(), v.clone()))
.collect();
attrs.sort_by_key(|kv| kv.key.clone());
for attr in &mut attrs {
// don't care about opentelemetry sdk version for tests
if attr.key.as_str() == "telemetry.sdk.version" {
attr.value = "0.0.0".into();
}
}
attrs
}

Expand Down
60 changes: 60 additions & 0 deletions tests/test_basic_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,36 @@ async fn test_basic_metrics() {
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.language",
),
value: String(
Static(
"rust",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.name",
),
value: String(
Static(
"opentelemetry",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.version",
),
value: String(
Static(
"0.0.0",
),
),
},
],
scope_metrics: [
DeterministicScopeMetrics {
Expand Down Expand Up @@ -1702,6 +1732,36 @@ async fn test_basic_metrics() {
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.language",
),
value: String(
Static(
"rust",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.name",
),
value: String(
Static(
"opentelemetry",
),
),
},
KeyValue {
key: Static(
"telemetry.sdk.version",
),
value: String(
Static(
"0.0.0",
),
),
},
],
scope_metrics: [
DeterministicScopeMetrics {
Expand Down
Loading
Loading