Skip to content

Commit 8c092cd

Browse files
authored
support setting service resource attributes from environment variable (#109)
1 parent 55858d2 commit 8c092cd

File tree

6 files changed

+660
-25
lines changed

6 files changed

+660
-25
lines changed

src/bridges/tracing.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,6 +2335,36 @@ mod tests {
23352335
),
23362336
),
23372337
},
2338+
KeyValue {
2339+
key: Static(
2340+
"telemetry.sdk.language",
2341+
),
2342+
value: String(
2343+
Static(
2344+
"rust",
2345+
),
2346+
),
2347+
},
2348+
KeyValue {
2349+
key: Static(
2350+
"telemetry.sdk.name",
2351+
),
2352+
value: String(
2353+
Static(
2354+
"opentelemetry",
2355+
),
2356+
),
2357+
},
2358+
KeyValue {
2359+
key: Static(
2360+
"telemetry.sdk.version",
2361+
),
2362+
value: String(
2363+
Static(
2364+
"0.0.0",
2365+
),
2366+
),
2367+
},
23382368
],
23392369
scope_metrics: [
23402370
DeterministicScopeMetrics {
@@ -2381,6 +2411,36 @@ mod tests {
23812411
),
23822412
),
23832413
},
2414+
KeyValue {
2415+
key: Static(
2416+
"telemetry.sdk.language",
2417+
),
2418+
value: String(
2419+
Static(
2420+
"rust",
2421+
),
2422+
),
2423+
},
2424+
KeyValue {
2425+
key: Static(
2426+
"telemetry.sdk.name",
2427+
),
2428+
value: String(
2429+
Static(
2430+
"opentelemetry",
2431+
),
2432+
),
2433+
},
2434+
KeyValue {
2435+
key: Static(
2436+
"telemetry.sdk.version",
2437+
),
2438+
value: String(
2439+
Static(
2440+
"0.0.0",
2441+
),
2442+
),
2443+
},
23842444
],
23852445
scope_metrics: [
23862446
DeterministicScopeMetrics {

src/config.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
//! See [`LogfireConfigBuilder`] for documentation of all these options.
44
55
use std::{
6+
collections::HashMap,
7+
convert::Infallible,
68
fmt::Display,
9+
marker::PhantomData,
710
path::PathBuf,
811
str::FromStr,
912
sync::{Arc, Mutex},
@@ -17,7 +20,7 @@ use opentelemetry_sdk::{
1720
use regex::Regex;
1821
use tracing::{Level, level_filters::LevelFilter};
1922

20-
use crate::{ConfigureError, logfire::Logfire};
23+
use crate::{ConfigureError, internal::env::get_optional_env, logfire::Logfire};
2124

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

630+
pub(crate) trait ParseConfigValue: Sized {
631+
fn parse_config_value(s: &str) -> Result<Self, ConfigureError>;
632+
}
633+
634+
impl<T> ParseConfigValue for T
635+
where
636+
T: FromStr,
637+
ConfigureError: From<T::Err>,
638+
{
639+
fn parse_config_value(s: &str) -> Result<Self, ConfigureError> {
640+
Ok(s.parse()?)
641+
}
642+
}
643+
644+
pub(crate) struct ConfigValue<T> {
645+
env_vars: &'static [&'static str],
646+
default_value: fn() -> T,
647+
}
648+
649+
impl<T> ConfigValue<T> {
650+
const fn new(env_vars: &'static [&'static str], default_value: fn() -> T) -> Self {
651+
Self {
652+
env_vars,
653+
default_value,
654+
}
655+
}
656+
}
657+
impl<T: ParseConfigValue> ConfigValue<T> {
658+
/// Resolves a config value, using the provided value if present, otherwise falling back to the environment variable or the default.
659+
pub(crate) fn resolve(
660+
&self,
661+
value: Option<T>,
662+
env: Option<&HashMap<String, String>>,
663+
) -> Result<T, ConfigureError> {
664+
if let Some(v) = try_resolve_from_env(value, self.env_vars, env)? {
665+
return Ok(v);
666+
}
667+
668+
Ok((self.default_value)())
669+
}
670+
}
671+
672+
pub(crate) struct OptionalConfigValue<T> {
673+
env_vars: &'static [&'static str],
674+
default_value: PhantomData<Option<T>>,
675+
}
676+
677+
impl<T> OptionalConfigValue<T> {
678+
const fn new(env_vars: &'static [&'static str]) -> Self {
679+
Self {
680+
env_vars,
681+
default_value: PhantomData,
682+
}
683+
}
684+
}
685+
686+
impl<T: ParseConfigValue> OptionalConfigValue<T> {
687+
/// Resolves an optional config value, using the provided value if present, otherwise falling back to the environment variable or `None`.
688+
pub(crate) fn resolve(
689+
&self,
690+
value: Option<T>,
691+
env: Option<&HashMap<String, String>>,
692+
) -> Result<Option<T>, ConfigureError> {
693+
try_resolve_from_env(value, self.env_vars, env)
694+
}
695+
}
696+
697+
fn try_resolve_from_env<T>(
698+
value: Option<T>,
699+
env_vars: &[&str],
700+
env: Option<&HashMap<String, String>>,
701+
) -> Result<Option<T>, ConfigureError>
702+
where
703+
T: ParseConfigValue,
704+
{
705+
if let Some(v) = value {
706+
return Ok(Some(v));
707+
}
708+
709+
for var in env_vars {
710+
if let Some(s) = get_optional_env(var, env)? {
711+
return T::parse_config_value(&s).map(Some);
712+
}
713+
}
714+
715+
Ok(None)
716+
}
717+
718+
impl From<Infallible> for ConfigureError {
719+
fn from(_: Infallible) -> Self {
720+
unreachable!("Infallible cannot be constructed")
721+
}
722+
}
723+
724+
pub(crate) static LOGFIRE_SEND_TO_LOGFIRE: ConfigValue<SendToLogfire> =
725+
ConfigValue::new(&["LOGFIRE_SEND_TO_LOGFIRE"], || SendToLogfire::Yes);
726+
727+
pub(crate) static LOGFIRE_SERVICE_NAME: OptionalConfigValue<String> =
728+
OptionalConfigValue::new(&["LOGFIRE_SERVICE_NAME", "OTEL_SERVICE_NAME"]);
729+
730+
pub(crate) static LOGFIRE_SERVICE_VERSION: OptionalConfigValue<String> =
731+
OptionalConfigValue::new(&["LOGFIRE_SERVICE_VERSION", "OTEL_SERVICE_VERSION"]);
732+
733+
pub(crate) static LOGFIRE_ENVIRONMENT: OptionalConfigValue<String> =
734+
OptionalConfigValue::new(&["LOGFIRE_ENVIRONMENT"]);
735+
627736
#[cfg(test)]
628737
mod tests {
629738
use crate::config::SendToLogfire;

src/logfire.rs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ use crate::{
3636
__macros_impl::LogfireValue,
3737
ConfigureError, LogfireConfigBuilder, ShutdownError,
3838
bridges::tracing::LogfireTracingLayer,
39-
config::{SendToLogfire, get_base_url_from_token},
39+
config::{
40+
LOGFIRE_ENVIRONMENT, LOGFIRE_SEND_TO_LOGFIRE, LOGFIRE_SERVICE_NAME,
41+
LOGFIRE_SERVICE_VERSION, SendToLogfire, get_base_url_from_token,
42+
},
4043
internal::{
4144
env::get_optional_env,
4245
exporters::console::{ConsoleWriter, create_console_processors},
@@ -158,6 +161,13 @@ impl Logfire {
158161
/// Called by `LogfireConfigBuilder::finish()`.
159162
pub(crate) fn from_config_builder(
160163
config: LogfireConfigBuilder,
164+
) -> Result<Logfire, ConfigureError> {
165+
Self::from_config_builder_and_env(config, None)
166+
}
167+
168+
fn from_config_builder_and_env(
169+
config: LogfireConfigBuilder,
170+
env: Option<&HashMap<String, String>>,
161171
) -> Result<Logfire, ConfigureError> {
162172
let LogfireParts {
163173
local,
@@ -170,7 +180,7 @@ impl Logfire {
170180
enable_tracing_metrics,
171181
shutdown_sender,
172182
..
173-
} = Self::build_parts(config, None)?;
183+
} = Self::build_parts(config, env)?;
174184

175185
if !local {
176186
// avoid otel logs firing as these messages are sent regarding "global meter provider"
@@ -276,13 +286,7 @@ impl Logfire {
276286
}
277287
}
278288

279-
let send_to_logfire = match config.send_to_logfire {
280-
Some(send_to_logfire) => send_to_logfire,
281-
None => match get_optional_env("LOGFIRE_SEND_TO_LOGFIRE", env)? {
282-
Some(value) => value.parse()?,
283-
None => SendToLogfire::Yes,
284-
},
285-
};
289+
let send_to_logfire = LOGFIRE_SEND_TO_LOGFIRE.resolve(config.send_to_logfire, env)?;
286290

287291
let send_to_logfire = match send_to_logfire {
288292
SendToLogfire::Yes => true,
@@ -302,34 +306,32 @@ impl Logfire {
302306
}
303307

304308
// Add service-specific resources from config
305-
let mut service_resource_builder = opentelemetry_sdk::Resource::builder_empty();
306-
let mut has_service_attributes = false;
309+
let mut service_resource_builder = opentelemetry_sdk::Resource::builder();
307310

308-
if let Some(service_name) = config.service_name {
311+
if let Some(service_name) = LOGFIRE_SERVICE_NAME.resolve(config.service_name, env)? {
309312
service_resource_builder = service_resource_builder.with_service_name(service_name);
310-
has_service_attributes = true;
311313
}
312314

313-
if let Some(service_version) = config.service_version {
315+
if let Some(service_version) =
316+
LOGFIRE_SERVICE_VERSION.resolve(config.service_version, env)?
317+
{
314318
service_resource_builder = service_resource_builder.with_attribute(
315319
opentelemetry::KeyValue::new("service.version", service_version),
316320
);
317-
has_service_attributes = true;
318321
}
319322

320-
if let Some(environment) = config.environment {
323+
if let Some(environment) = LOGFIRE_ENVIRONMENT.resolve(config.environment, env)? {
321324
service_resource_builder = service_resource_builder.with_attribute(
322325
opentelemetry::KeyValue::new("deployment.environment.name", environment),
323326
);
324-
has_service_attributes = true;
325327
}
326328

327-
if has_service_attributes {
328-
let service_resource = service_resource_builder.build();
329-
advanced_options.resources.push(service_resource);
330-
}
331-
332-
for resource in advanced_options.resources {
329+
// Use "default" resource first so that user-provided resources can override it
330+
let service_resource = service_resource_builder.build();
331+
for resource in [service_resource]
332+
.into_iter()
333+
.chain(advanced_options.resources)
334+
{
333335
tracer_provider_builder = tracer_provider_builder.with_resource(resource.clone());
334336
logger_provider_builder = logger_provider_builder.with_resource(resource.clone());
335337
meter_provider_builder = meter_provider_builder.with_resource(resource);

src/test_utils.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,18 @@ pub fn remap_timestamps_in_console_output(output: &str) -> Cow<'_, str> {
191191
}
192192

193193
/// `Resource` contains a hashmap, so deterministic tests need to convert to an ordered container.
194-
fn make_deterministic_resource(resource: &Resource) -> Vec<KeyValue> {
194+
pub fn make_deterministic_resource(resource: &Resource) -> Vec<KeyValue> {
195195
let mut attrs: Vec<_> = resource
196196
.iter()
197197
.map(|(k, v)| KeyValue::new(k.clone(), v.clone()))
198198
.collect();
199199
attrs.sort_by_key(|kv| kv.key.clone());
200+
for attr in &mut attrs {
201+
// don't care about opentelemetry sdk version for tests
202+
if attr.key.as_str() == "telemetry.sdk.version" {
203+
attr.value = "0.0.0".into();
204+
}
205+
}
200206
attrs
201207
}
202208

tests/test_basic_exports.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,36 @@ async fn test_basic_metrics() {
16501650
),
16511651
),
16521652
},
1653+
KeyValue {
1654+
key: Static(
1655+
"telemetry.sdk.language",
1656+
),
1657+
value: String(
1658+
Static(
1659+
"rust",
1660+
),
1661+
),
1662+
},
1663+
KeyValue {
1664+
key: Static(
1665+
"telemetry.sdk.name",
1666+
),
1667+
value: String(
1668+
Static(
1669+
"opentelemetry",
1670+
),
1671+
),
1672+
},
1673+
KeyValue {
1674+
key: Static(
1675+
"telemetry.sdk.version",
1676+
),
1677+
value: String(
1678+
Static(
1679+
"0.0.0",
1680+
),
1681+
),
1682+
},
16531683
],
16541684
scope_metrics: [
16551685
DeterministicScopeMetrics {
@@ -1702,6 +1732,36 @@ async fn test_basic_metrics() {
17021732
),
17031733
),
17041734
},
1735+
KeyValue {
1736+
key: Static(
1737+
"telemetry.sdk.language",
1738+
),
1739+
value: String(
1740+
Static(
1741+
"rust",
1742+
),
1743+
),
1744+
},
1745+
KeyValue {
1746+
key: Static(
1747+
"telemetry.sdk.name",
1748+
),
1749+
value: String(
1750+
Static(
1751+
"opentelemetry",
1752+
),
1753+
),
1754+
},
1755+
KeyValue {
1756+
key: Static(
1757+
"telemetry.sdk.version",
1758+
),
1759+
value: String(
1760+
Static(
1761+
"0.0.0",
1762+
),
1763+
),
1764+
},
17051765
],
17061766
scope_metrics: [
17071767
DeterministicScopeMetrics {

0 commit comments

Comments
 (0)