pub(crate) mod metrics;
pub(crate) mod tracing;
mod tracing_json;
use std::{
collections::{HashMap, HashSet},
fmt,
future::{Future, ready},
str::FromStr,
sync::{Arc, Mutex, MutexGuard, PoisonError},
time::Duration,
};
use logfire::{
ConfigureError, Logfire,
config::{AdvancedOptions, MetricsOptions},
};
pub use metrics::{Measurement, MetricKind, MetricValue, Metrics};
use opentelemetry::{
Context, InstrumentationScope,
trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState},
};
use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest;
use opentelemetry_sdk::{
error::OTelSdkResult,
logs::{LogProcessor, SdkLogRecord},
metrics::{PeriodicReader, Temporality, data::ResourceMetrics, exporter::PushMetricExporter},
trace::{Span, SpanData, SpanProcessor},
};
use prost::Message;
pub const TELEMETRY_ADAPTER_VERSION: u8 = 1;
pub struct TelemetryAdapterHandle {
logfire: Logfire,
metrics: Metrics,
}
#[derive(Clone)]
pub struct TelemetryContext {
parent: Option<SpanContext>,
logfire: Logfire,
}
impl TelemetryAdapterHandle {
pub fn context(
&self,
trace_id: &str,
span_id: &str,
trace_flags: u8,
trace_state: &str,
) -> Result<TelemetryContext, String> {
self.context_with_remote(trace_id, span_id, trace_flags, trace_state, true)
}
pub fn context_with_remote(
&self,
trace_id: &str,
span_id: &str,
trace_flags: u8,
trace_state: &str,
is_remote: bool,
) -> Result<TelemetryContext, String> {
let trace_id = TraceId::from_hex(trace_id).map_err(|err| format!("invalid trace ID: {err}"))?;
let span_id = SpanId::from_hex(span_id).map_err(|err| format!("invalid span ID: {err}"))?;
self.context_from_ids(trace_id, span_id, trace_flags, trace_state, is_remote)
}
pub fn context_from_ids(
&self,
trace_id: TraceId,
span_id: SpanId,
trace_flags: u8,
trace_state: &str,
is_remote: bool,
) -> Result<TelemetryContext, String> {
let trace_state = TraceState::from_str(trace_state).map_err(|err| format!("invalid trace state: {err}"))?;
if trace_id == TraceId::INVALID || span_id == SpanId::INVALID {
return Err("trace and span IDs must be non-zero".to_owned());
}
Ok(TelemetryContext {
parent: Some(SpanContext::new(
trace_id,
span_id,
TraceFlags::new(trace_flags),
is_remote,
trace_state,
)),
logfire: self.logfire.clone(),
})
}
#[must_use]
pub fn unparented_context(&self) -> TelemetryContext {
TelemetryContext {
parent: None,
logfire: self.logfire.clone(),
}
}
#[must_use]
pub fn metrics(&self) -> Metrics {
self.metrics.clone()
}
pub fn force_flush(&self) -> OTelSdkResult {
self.logfire.force_flush()
}
}
impl TelemetryContext {
#[must_use]
pub fn for_logfire(logfire: Logfire) -> Self {
Self { parent: None, logfire }
}
}
impl TelemetryContext {
pub(crate) fn logfire(&self) -> Logfire {
self.logfire.clone()
}
pub(crate) fn propagation_headers(&self) -> Vec<(String, String)> {
let Some(parent) = &self.parent else {
return Vec::new();
};
let traceparent = format!(
"00-{}-{}-{:02x}",
parent.trace_id(),
parent.span_id(),
parent.trace_flags().to_u8()
);
let mut headers = vec![("traceparent".to_owned(), traceparent)];
let tracestate = parent.trace_state().header();
if !tracestate.is_empty() {
headers.push(("tracestate".to_owned(), tracestate));
}
headers
}
pub(crate) fn into_parent(self) -> Option<Context> {
self.parent
.map(|parent| Context::new().with_remote_span_context(parent))
}
}
pub trait TelemetryAdapter: Send + Sync + 'static {
fn start_span(&self, span: &SpanData) -> bool;
fn start_span_with_parent(&self, span: &SpanData, parent: &Context) -> bool {
let _ = parent;
self.start_span(span)
}
fn end_span(&self, span: &SpanData) -> bool;
fn emit_log(&self, parent_span_id: SpanId, record: &SdkLogRecord) -> bool;
fn disable_root(&self, trace_id: TraceId, root_span_id: SpanId);
fn record_metric(&self, measurement: &Measurement<'_>) {
let _ = measurement;
}
fn export_metrics(&self, payload: &[u8]) {
let _ = payload;
}
}
pub fn configure_telemetry_adapter(
adapter: Arc<dyn TelemetryAdapter>,
) -> Result<TelemetryAdapterHandle, ConfigureError> {
configure_adapter(adapter, false)
}
pub fn configure_telemetry_adapter_with_host_metrics(
adapter: Arc<dyn TelemetryAdapter>,
) -> Result<TelemetryAdapterHandle, ConfigureError> {
configure_adapter(adapter, true)
}
fn configure_adapter(
adapter: Arc<dyn TelemetryAdapter>,
host_owns_metrics: bool,
) -> Result<TelemetryAdapterHandle, ConfigureError> {
let processor = AdapterProcessor::new(Arc::clone(&adapter));
let config = logfire::configure()
.send_to_logfire(false)
.with_console(None)
.with_install_panic_handler(false)
.with_additional_span_processor(processor.clone())
.with_advanced_options(AdvancedOptions::default().with_log_processor(processor));
let config = if host_owns_metrics {
config.with_metrics(None)
} else {
let exporter = AdapterMetricExporter::new(Arc::clone(&adapter));
let reader = PeriodicReader::builder(exporter).build();
config.with_metrics(Some(MetricsOptions::default().with_additional_reader(reader)))
};
let logfire = config.finish()?;
let metrics = if host_owns_metrics {
Metrics::for_adapter(adapter)
} else {
Metrics::for_logfire(logfire.clone())
};
Ok(TelemetryAdapterHandle { logfire, metrics })
}
struct AdapterMetricExporter {
adapter: Arc<dyn TelemetryAdapter>,
}
impl AdapterMetricExporter {
fn new(adapter: Arc<dyn TelemetryAdapter>) -> Self {
Self { adapter }
}
}
impl PushMetricExporter for AdapterMetricExporter {
fn export(&self, metrics: &ResourceMetrics) -> impl Future<Output = OTelSdkResult> + Send {
let request = ExportMetricsServiceRequest::from(metrics);
self.adapter.export_metrics(&request.encode_to_vec());
ready(Ok(()))
}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _: Duration) -> OTelSdkResult {
Ok(())
}
fn temporality(&self) -> Temporality {
Temporality::Delta
}
}
#[derive(Clone)]
struct AdapterProcessor(Arc<ProcessorState>);
struct ProcessorState {
adapter: Arc<dyn TelemetryAdapter>,
routing: Mutex<RoutingState>,
}
struct RoutingState {
spans: HashMap<SpanKey, SpanKey>,
open_spans: HashMap<SpanKey, usize>,
disabled: HashSet<SpanKey>,
active_deliveries: HashMap<SpanKey, usize>,
cleanup_sent: HashSet<SpanKey>,
finished_roots: HashSet<SpanKey>,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct SpanKey {
trace_id: TraceId,
span_id: SpanId,
}
impl AdapterProcessor {
fn new(adapter: Arc<dyn TelemetryAdapter>) -> Self {
Self(Arc::new(ProcessorState {
adapter,
routing: Mutex::new(RoutingState {
spans: HashMap::new(),
open_spans: HashMap::new(),
disabled: HashSet::new(),
active_deliveries: HashMap::new(),
cleanup_sent: HashSet::new(),
finished_roots: HashSet::new(),
}),
}))
}
fn begin_delivery(routing: &mut RoutingState, root: SpanKey) -> bool {
if routing.disabled.contains(&root) {
false
} else {
*routing.active_deliveries.entry(root).or_default() += 1;
true
}
}
fn finish_delivery(&self, root: SpanKey, delivered: bool) {
let cleanup = {
let mut routing = lock(&self.0.routing);
let active = routing
.active_deliveries
.get_mut(&root)
.expect("delivery registered before callback");
*active -= 1;
if *active == 0 {
routing.active_deliveries.remove(&root);
}
if !delivered {
routing.disabled.insert(root);
}
let cleanup = routing.disabled.contains(&root)
&& !routing.active_deliveries.contains_key(&root)
&& routing.cleanup_sent.insert(root);
Self::forget_finished_root(&mut routing, root);
cleanup
};
if cleanup {
self.0.adapter.disable_root(root.trace_id, root.span_id);
}
}
fn forget_finished_root(routing: &mut RoutingState, root: SpanKey) {
if routing.finished_roots.contains(&root)
&& !routing.active_deliveries.contains_key(&root)
&& !routing.open_spans.contains_key(&root)
{
routing.disabled.remove(&root);
routing.cleanup_sent.remove(&root);
routing.finished_roots.remove(&root);
}
}
}
impl fmt::Debug for AdapterProcessor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("AdapterProcessor")
}
}
impl SpanProcessor for AdapterProcessor {
fn on_start(&self, span: &mut Span, parent: &Context) {
let Some(data) = span.exported_data() else { return };
let span_key = SpanKey {
trace_id: data.span_context.trace_id(),
span_id: data.span_context.span_id(),
};
let parent_key = SpanKey {
trace_id: span_key.trace_id,
span_id: parent.span().span_context().span_id(),
};
let (root, deliver) = {
let mut routing = lock(&self.0.routing);
let root = routing.spans.get(&parent_key).copied().unwrap_or(span_key);
routing.spans.insert(span_key, root);
*routing.open_spans.entry(root).or_default() += 1;
let deliver = Self::begin_delivery(&mut routing, root);
(root, deliver)
};
if deliver {
let delivered = self.0.adapter.start_span_with_parent(&data, parent);
self.finish_delivery(root, delivered);
}
}
fn on_end(&self, span: SpanData) {
let span_key = SpanKey {
trace_id: span.span_context.trace_id(),
span_id: span.span_context.span_id(),
};
let (root, deliver) = {
let mut routing = lock(&self.0.routing);
let Some(root) = routing.spans.remove(&span_key) else {
return;
};
let open_spans = routing.open_spans.get_mut(&root).expect("span registered on start");
*open_spans -= 1;
if *open_spans == 0 {
routing.open_spans.remove(&root);
}
let deliver = Self::begin_delivery(&mut routing, root);
(root, deliver)
};
if deliver {
let delivered = self.0.adapter.end_span(&span);
self.finish_delivery(root, delivered);
}
let mut routing = lock(&self.0.routing);
if root == span_key {
routing.finished_roots.insert(root);
}
Self::forget_finished_root(&mut routing, root);
}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
Ok(())
}
}
impl LogProcessor for AdapterProcessor {
fn emit(&self, record: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) {
let Some(context) = record.trace_context() else { return };
let parent = SpanKey {
trace_id: context.trace_id,
span_id: context.span_id,
};
let (root, deliver) = {
let mut routing = lock(&self.0.routing);
let Some(root) = routing.spans.get(&parent).copied() else {
return;
};
let deliver = Self::begin_delivery(&mut routing, root);
(root, deliver)
};
if deliver {
let delivered = self.0.adapter.emit_log(context.span_id, record);
self.finish_delivery(root, delivered);
}
}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
Ok(())
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}