use std::time::{SystemTime, UNIX_EPOCH};
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TelemetryEvent {
pub name: String,
pub severity: Severity,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub body: Option<String>,
pub attributes: Vec<Attribute>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub timestamp_ns: Option<u64>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub span_id: Option<u64>,
}
impl TelemetryEvent {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), ..Self::default() }
}
pub fn in_span(mut self, span: u64) -> Self {
self.span_id = Some(span);
self
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn with_attribute(
mut self,
key: impl Into<String>,
value: impl Into<AttributeValue>,
) -> Self {
self.attributes.push(Attribute::new(key, value));
self
}
pub fn custom(name: &str, attributes: Vec<Attribute>) -> Self {
let name = format!("custom.{}", name.trim_start_matches("custom."));
Self { attributes, ..Self::new(name) }
}
pub fn size_hint(&self) -> usize {
RECORD_OVERHEAD_BYTES
+ 2 * self.name.len()
+ self.body.as_ref().map_or(self.name.len(), String::len)
+ attributes_size_hint(&self.attributes)
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Severity {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl Severity {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Trace => "TRACE",
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogSource {
Sdk,
Ffi,
WebRtc,
}
impl LogSource {
fn as_str(self) -> &'static str {
match self {
Self::Sdk => "sdk",
Self::Ffi => "ffi",
Self::WebRtc => "webrtc",
}
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[derive(Debug, Clone, PartialEq)]
pub struct LogRecord {
pub severity: Severity,
pub source: LogSource,
pub body: String,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub logger: Option<String>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub function: Option<String>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub file: Option<String>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub line: Option<u32>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub timestamp_ns: Option<u64>,
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub span_id: Option<u64>,
}
impl From<LogRecord> for TelemetryEvent {
fn from(record: LogRecord) -> Self {
let mut event = TelemetryEvent::default()
.with_severity(record.severity)
.with_body(record.body)
.with_attribute("lk.log.source", record.source.as_str());
if let Some(logger) = record.logger.filter(|s| !s.is_empty()) {
event = event.with_attribute("lk.log.logger", logger);
}
if let Some(function) = record.function.filter(|s| !s.is_empty()) {
event = event.with_attribute("code.function.name", function);
}
if let Some(file) = record.file.filter(|s| !s.is_empty()) {
event = event.with_attribute("code.file.path", file);
}
if let Some(line) = record.line.filter(|l| *l > 0) {
event = event.with_attribute("code.line.number", i64::from(line));
}
event.timestamp_ns = record.timestamp_ns;
event.span_id = record.span_id;
event
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[derive(Debug, Clone, PartialEq)]
pub struct Attribute {
pub key: String,
pub value: AttributeValue,
}
impl Attribute {
pub fn new(key: impl Into<String>, value: impl Into<AttributeValue>) -> Self {
Self { key: key.into(), value: value.into() }
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[derive(Debug, Clone, PartialEq)]
pub enum AttributeValue {
Str(String),
Int(i64),
Double(f64),
Bool(bool),
}
impl From<&str> for AttributeValue {
fn from(value: &str) -> Self {
Self::Str(value.to_owned())
}
}
impl From<String> for AttributeValue {
fn from(value: String) -> Self {
Self::Str(value)
}
}
impl From<i64> for AttributeValue {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<f64> for AttributeValue {
fn from(value: f64) -> Self {
Self::Double(value)
}
}
impl From<bool> for AttributeValue {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
pub(crate) fn now_unix_nanos() -> u64 {
let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| {
u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
});
#[cfg(test)]
let now = now.saturating_add_signed(CLOCK_JUMP_NS.with(|jump| jump.get()));
now
}
#[cfg(test)]
thread_local! {
pub(crate) static CLOCK_JUMP_NS: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
}
#[expect(dead_code, reason = "enforced by the pipeline, not in place yet")]
pub(crate) const MAX_NAME_BYTES: usize = 128;
pub(crate) const MAX_KEY_BYTES: usize = 128;
pub(crate) const MAX_VALUE_BYTES: usize = 1024;
pub(crate) const MAX_CUSTOM_ATTRIBUTES: usize = 64;
pub(crate) fn is_reserved(key: &str) -> bool {
key.starts_with("lk.")
|| key.starts_with("otel.")
|| key.starts_with("code.")
|| key == "session.id"
|| key == "error.type"
|| key == "network.connection.type"
}
pub(crate) fn valid_custom(key: &str, value: Option<&AttributeValue>) -> bool {
let value_ok = match value {
Some(AttributeValue::Str(s)) => s.len() <= MAX_VALUE_BYTES,
_ => true,
};
!key.is_empty() && key.len() <= MAX_KEY_BYTES && !is_reserved(key) && value_ok
}
const RECORD_OVERHEAD_BYTES: usize = 78;
const ATTRIBUTE_OVERHEAD_BYTES: usize = 8;
const NUMBER_VALUE_BYTES: usize = 8;
pub(crate) fn attributes_size_hint(attributes: &[Attribute]) -> usize {
attributes.iter().map(|a| ATTRIBUTE_OVERHEAD_BYTES + a.key.len() + a.value.size_hint()).sum()
}
impl AttributeValue {
fn size_hint(&self) -> usize {
match self {
AttributeValue::Str(s) => s.len(),
_ => NUMBER_VALUE_BYTES,
}
}
}