use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
use tokio::time::MissedTickBehavior;
use tokio_util::sync::CancellationToken;
use tracing_core::field::{Field, Visit};
use tracing_core::{Event, Level, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
use crate::client::Client;
use crate::error::{Error, Result};
use crate::logs::{LogEntryInput, Logs, MAX_BATCH_ENTRIES};
use crate::types::LogLevel;
tokio::task_local! {
static IN_FLUSH: bool;
}
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
pub struct TracingLayer {
tx: mpsc::Sender<LogEntryInput>,
dropped: Arc<AtomicU64>,
}
impl TracingLayer {
#[must_use]
pub fn builder(client: &Client) -> TracingLayerBuilder {
TracingLayerBuilder {
client: client.clone(),
capacity: 1000,
flush_after: 50,
flush_interval: Duration::from_secs(5),
shutdown_timeout: Duration::from_secs(5),
}
}
}
impl<S: Subscriber> Layer<S> for TracingLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
if IN_FLUSH.try_with(|in_flush| *in_flush).unwrap_or(false) {
return;
}
let timestamp = format_rfc3339(SystemTime::now());
let mut visitor = FieldVisitor::default();
event.record(&mut visitor);
let entry = LogEntryInput {
level: map_level(event.metadata().level()),
message: visitor.message,
context: if visitor.fields.is_empty() {
None
} else {
Some(serde_json::Value::Object(visitor.fields))
},
timestamp: Some(timestamp),
};
if self.tx.try_send(entry).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
pub struct TracingLayerBuilder {
client: Client,
capacity: usize,
flush_after: usize,
flush_interval: Duration,
shutdown_timeout: Duration,
}
impl TracingLayerBuilder {
#[must_use]
pub fn capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
#[must_use]
pub fn flush_after(mut self, flush_after: usize) -> Self {
self.flush_after = flush_after;
self
}
#[must_use]
pub fn flush_interval(mut self, flush_interval: Duration) -> Self {
self.flush_interval = flush_interval;
self
}
#[must_use]
pub fn shutdown_timeout(mut self, shutdown_timeout: Duration) -> Self {
self.shutdown_timeout = shutdown_timeout;
self
}
pub fn build(self) -> Result<(TracingLayer, TracingGuard)> {
let handle = tokio::runtime::Handle::try_current().map_err(|_| Error::Api {
status: 0,
message: "the tracing layer must be built inside a tokio runtime \
(its background sender is spawned with tokio::spawn)"
.to_string(),
body: None,
})?;
let (tx, rx) = mpsc::channel(self.capacity.max(1));
let dropped = Arc::new(AtomicU64::new(0));
let cancel = CancellationToken::new();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let worker = Worker {
logs: self.client.logs(),
rx,
flush_after: self.flush_after.max(1),
flush_interval: self.flush_interval.max(Duration::from_millis(1)),
cancel: cancel.clone(),
dropped: dropped.clone(),
done_tx,
};
let join = handle.spawn(worker.run());
Ok((
TracingLayer {
tx,
dropped: dropped.clone(),
},
TracingGuard {
cancel,
done_rx: Some(done_rx),
worker: Some(join),
dropped,
shutdown_timeout: self.shutdown_timeout,
},
))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
pub struct TracingGuard {
cancel: CancellationToken,
done_rx: Option<std::sync::mpsc::Receiver<()>>,
worker: Option<tokio::task::JoinHandle<()>>,
dropped: Arc<AtomicU64>,
shutdown_timeout: Duration,
}
impl TracingGuard {
#[must_use]
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub async fn shutdown(mut self) {
self.cancel.cancel();
self.done_rx = None; if let Some(worker) = self.worker.take() {
let _ = worker.await;
}
}
}
impl Drop for TracingGuard {
fn drop(&mut self) {
self.cancel.cancel();
if let Some(done_rx) = self.done_rx.take() {
let _ = done_rx.recv_timeout(self.shutdown_timeout);
}
}
}
struct Worker {
logs: Logs,
rx: mpsc::Receiver<LogEntryInput>,
flush_after: usize,
flush_interval: Duration,
cancel: CancellationToken,
dropped: Arc<AtomicU64>,
done_tx: std::sync::mpsc::Sender<()>,
}
impl Worker {
async fn run(self) {
let Worker {
logs,
mut rx,
flush_after,
flush_interval,
cancel,
dropped,
done_tx,
} = self;
let mut buffer: Vec<LogEntryInput> = Vec::new();
let mut ticker = tokio::time::interval(flush_interval);
ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
ticker.tick().await;
loop {
tokio::select! {
biased;
_ = cancel.cancelled() => break,
received = rx.recv() => match received {
Some(entry) => {
buffer.push(entry);
if buffer.len() >= flush_after {
flush(&logs, &mut buffer, &dropped).await;
ticker.reset();
}
}
None => break,
},
_ = ticker.tick() => {
if !buffer.is_empty() {
flush(&logs, &mut buffer, &dropped).await;
}
}
}
}
while let Ok(entry) = rx.try_recv() {
buffer.push(entry);
}
flush(&logs, &mut buffer, &dropped).await;
let _ = done_tx.send(());
}
}
async fn flush(logs: &Logs, buffer: &mut Vec<LogEntryInput>, dropped: &AtomicU64) {
for chunk in buffer.chunks(MAX_BATCH_ENTRIES) {
let result = IN_FLUSH.scope(true, logs.write_batch(chunk)).await;
if result.is_err() {
dropped.fetch_add(chunk.len() as u64, Ordering::Relaxed);
}
}
buffer.clear();
}
fn map_level(level: &Level) -> LogLevel {
if *level == Level::ERROR {
LogLevel::Error
} else if *level == Level::WARN {
LogLevel::Warning
} else if *level == Level::INFO {
LogLevel::Info
} else {
LogLevel::Debug }
}
#[derive(Default)]
struct FieldVisitor {
message: String,
fields: serde_json::Map<String, serde_json::Value>,
}
impl FieldVisitor {
fn record_value(&mut self, field: &Field, value: serde_json::Value) {
if field.name() == "message" {
self.message = match value {
serde_json::Value::String(text) => text,
other => other.to_string(),
};
} else {
self.fields.insert(field.name().to_string(), value);
}
}
}
impl Visit for FieldVisitor {
fn record_i64(&mut self, field: &Field, value: i64) {
self.record_value(field, value.into());
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.record_value(field, value.into());
}
fn record_f64(&mut self, field: &Field, value: f64) {
let value = serde_json::Number::from_f64(value)
.map_or(serde_json::Value::Null, serde_json::Value::Number);
self.record_value(field, value);
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.record_value(field, value.into());
}
fn record_str(&mut self, field: &Field, value: &str) {
self.record_value(field, value.into());
}
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
self.record_value(field, value.to_string().into());
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.record_value(field, format!("{value:?}").into());
}
}
fn format_rfc3339(time: SystemTime) -> String {
let since_epoch = time.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = since_epoch.as_secs();
let millis = since_epoch.subsec_millis();
let (year, month, day) = civil_from_days(secs / 86_400);
let (hour, minute, second) = (secs / 3600 % 24, secs / 60 % 60, secs % 60);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
}
fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z % 146_097; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = yoe + era * 400 + u64::from(month <= 2);
(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
fn at(secs: u64, millis: u32) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(secs) + Duration::from_millis(u64::from(millis))
}
#[test]
fn formats_the_epoch() {
assert_eq!(format_rfc3339(UNIX_EPOCH), "1970-01-01T00:00:00.000Z");
}
#[test]
fn formats_known_timestamps() {
assert_eq!(
format_rfc3339(at(946_684_799, 0)),
"1999-12-31T23:59:59.000Z"
);
assert_eq!(
format_rfc3339(at(1_700_000_000, 123)),
"2023-11-14T22:13:20.123Z"
);
assert_eq!(
format_rfc3339(at(1_782_864_000, 7)),
"2026-07-01T00:00:00.007Z"
);
}
#[test]
fn formats_leap_days() {
assert_eq!(
format_rfc3339(at(1_709_164_800, 0)),
"2024-02-29T00:00:00.000Z"
);
assert_eq!(
format_rfc3339(at(951_782_400, 0)),
"2000-02-29T00:00:00.000Z"
);
}
#[test]
fn maps_all_five_tracing_levels() {
assert_eq!(map_level(&Level::TRACE), LogLevel::Debug);
assert_eq!(map_level(&Level::DEBUG), LogLevel::Debug);
assert_eq!(map_level(&Level::INFO), LogLevel::Info);
assert_eq!(map_level(&Level::WARN), LogLevel::Warning);
assert_eq!(map_level(&Level::ERROR), LogLevel::Error);
}
}