use super::error::Error;
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
use opentelemetry_sdk::Resource;
#[derive(Debug, Clone)]
pub struct AppInsightsConfig {
pub connection_string: String,
pub service_name: String,
pub sample_rate: f64,
}
pub const CONNECTION_STRING_VAR: &str = "APPLICATIONINSIGHTS_CONNECTION_STRING";
impl AppInsightsConfig {
pub fn new(connection_string: impl Into<String>, service_name: impl Into<String>) -> Self {
Self {
connection_string: connection_string.into(),
service_name: service_name.into(),
sample_rate: 1.0,
}
}
#[must_use]
pub fn with_sample_rate(mut self, rate: f64) -> Self {
self.sample_rate = rate;
self
}
pub fn from_env(service_name: impl Into<String>) -> Result<Self, Error> {
Self::from_lookup(service_name, |name| std::env::var(name).ok())
}
pub fn from_lookup(
service_name: impl Into<String>,
lookup: impl FnOnce(&str) -> Option<String>,
) -> Result<Self, Error> {
let connection_string = lookup(CONNECTION_STRING_VAR)
.ok_or_else(|| Error::AppInsights(format!("{CONNECTION_STRING_VAR} is not set")))?;
Ok(Self::new(connection_string, service_name))
}
}
#[derive(Debug, Clone)]
pub struct Providers {
pub logger: SdkLoggerProvider,
pub tracer: SdkTracerProvider,
}
impl Providers {
pub fn force_flush(&self) {
let _ = self.logger.force_flush();
let _ = self.tracer.force_flush();
}
pub fn shutdown(&self) {
let _ = self.logger.shutdown();
let _ = self.tracer.shutdown();
}
}
pub fn provider(config: &AppInsightsConfig) -> Result<SdkLoggerProvider, Error> {
let client = std::thread::spawn(reqwest::blocking::Client::new)
.join()
.map_err(|_| Error::AppInsights("could not build the HTTP client".to_string()))?;
let exporter = opentelemetry_application_insights::Exporter::new_from_connection_string(
config.connection_string.clone(),
client,
)
.map_err(|e| Error::AppInsights(format!("unusable connection string: {e}")))?;
let resource = Resource::builder_empty()
.with_attributes(vec![opentelemetry::KeyValue::new(
"service.name",
config.service_name.clone(),
)])
.build();
Ok(SdkLoggerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build())
}
pub fn providers(config: &AppInsightsConfig) -> Result<Providers, Error> {
let client = std::thread::spawn(reqwest::blocking::Client::new)
.join()
.map_err(|_| Error::AppInsights("could not build the HTTP client".to_string()))?;
let exporter = opentelemetry_application_insights::Exporter::new_from_connection_string(
config.connection_string.clone(),
client,
)
.map_err(|e| Error::AppInsights(format!("unusable connection string: {e}")))?;
let resource = Resource::builder_empty()
.with_attributes(vec![opentelemetry::KeyValue::new(
"service.name",
config.service_name.clone(),
)])
.build();
Ok(Providers {
logger: SdkLoggerProvider::builder()
.with_batch_exporter(exporter.clone())
.with_resource(resource.clone())
.build(),
tracer: SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(
if config.sample_rate.is_finite() {
config.sample_rate.clamp(0.0, 1.0)
} else {
1.0
},
))))
.build(),
})
}
pub fn trace_layer<S>(
providers: &Providers,
) -> tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
use opentelemetry::trace::TracerProvider as _;
tracing_opentelemetry::layer().with_tracer(providers.tracer.tracer("stratify"))
}
pub fn layer(
provider: &SdkLoggerProvider,
) -> OpenTelemetryTracingBridge<SdkLoggerProvider, opentelemetry_sdk::logs::SdkLogger> {
OpenTelemetryTracingBridge::new(provider)
}
pub(super) struct Export<L, T> {
pub(super) providers: Option<Providers>,
pub(super) logs: Option<super::sinks::ErasedLayer<L>>,
pub(super) traces: Option<super::sinks::ErasedLayer<T>>,
}
pub(super) fn export<L, T>(
config: Option<AppInsightsConfig>,
filter: &Option<tracing_subscriber::EnvFilter>,
) -> Result<Export<L, T>, Error>
where
L: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
T: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync,
{
use tracing_subscriber::Layer as _;
let Some(config) = config else {
return Ok(Export {
providers: None,
logs: None,
traces: None,
});
};
let providers = providers(&config)?;
let logs = layer(&providers.logger).with_filter(super::sinks::clone_filter(filter));
let traces = trace_layer::<T>(&providers);
Ok(Export {
providers: Some(providers),
logs: Some(Box::new(logs)),
traces: Some(Box::new(traces)),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_malformed_connection_string_is_an_error_not_a_panic() {
let config = AppInsightsConfig::new("not-a-connection-string", "svc");
let result = provider(&config);
assert!(matches!(result, Err(Error::AppInsights(_))));
}
#[test]
fn a_well_formed_connection_string_builds_a_provider() {
let config = AppInsightsConfig::new(
"InstrumentationKey=00000000-1111-2222-3333-444444444444;\
IngestionEndpoint=https://example.invalid/",
"svc",
);
let built = provider(&config);
assert!(
built.is_ok(),
"a valid connection string must yield a provider"
);
if let Ok(p) = built {
let _ = p.shutdown();
}
}
#[test]
fn from_lookup_reports_an_absent_variable() {
let empty = |_: &str| None;
let result = AppInsightsConfig::from_lookup("svc", empty);
assert!(matches!(result, Err(Error::AppInsights(_))));
}
#[test]
fn from_lookup_names_the_variable_in_the_error() {
let empty = |_: &str| None;
let result = AppInsightsConfig::from_lookup("svc", empty);
match result {
Err(Error::AppInsights(message)) => {
assert!(message.contains(CONNECTION_STRING_VAR), "got: {message}")
}
other => panic!("expected an app insights error, got {other:?}"),
}
}
#[test]
fn from_lookup_reads_the_variable_azure_defines() {
let seen = std::cell::Cell::new(None);
let lookup = |name: &str| {
seen.set(Some(name.to_string()));
Some("InstrumentationKey=abc".to_string())
};
let config = AppInsightsConfig::from_lookup("svc", lookup).expect("a value is present");
assert_eq!(seen.take().as_deref(), Some(CONNECTION_STRING_VAR));
assert_eq!(config.connection_string, "InstrumentationKey=abc");
assert_eq!(config.service_name, "svc");
}
#[test]
fn the_sample_rate_defaults_to_everything() {
let config = AppInsightsConfig::new("cs", "svc");
assert_eq!(config.sample_rate, 1.0);
}
#[test]
fn an_out_of_range_sample_rate_still_builds_providers() {
let config = AppInsightsConfig::new(
"InstrumentationKey=00000000-0000-0000-0000-000000000000;\
IngestionEndpoint=https://unroutable.invalid/",
"svc",
)
.with_sample_rate(3.0);
let built = providers(&config);
assert!(built.is_ok());
if let Ok(p) = built {
p.shutdown();
}
}
#[test]
fn the_service_name_is_carried_on_the_config() {
let config = AppInsightsConfig::new("cs", "nse-api");
assert_eq!(config.service_name, "nse-api");
}
}