use opentelemetry_proto::tonic::{
common::v1::{any_value::Value as OtlpValue, AnyValue, ArrayValue, KeyValue, KeyValueList},
logs::v1::LogRecord,
};
use serde_json::{Map, Value};
use crate::{ParsedLog, Severity};
pub fn log_record(log: &ParsedLog<'_>) -> LogRecord {
let time_unix_nano = log
.timestamp()
.and_then(|time| {
u64::try_from(time.unix_seconds())
.ok()?
.checked_mul(1_000_000_000)?
.checked_add(u64::from(time.nanoseconds()))
})
.unwrap_or(0);
let severity_number = match log.severity() {
None => 0,
Some(Severity::Trace) => 1,
Some(Severity::Debug) => 5,
Some(Severity::Info) => 9,
Some(Severity::Warn) => 13,
Some(Severity::Error) => 17,
Some(Severity::Fatal) => 21,
};
LogRecord {
time_unix_nano,
severity_number,
severity_text: log
.severity()
.map(Severity::as_str)
.unwrap_or("")
.to_owned(),
body: Some(AnyValue {
value: Some(OtlpValue::StringValue(log.message().to_owned())),
}),
attributes: if log.fields().is_empty() {
Vec::new()
} else {
vec![KeyValue {
key: "app".to_owned(),
value: Some(object(log.fields())),
}]
},
..LogRecord::default()
}
}
fn object(fields: &Map<String, Value>) -> AnyValue {
AnyValue {
value: Some(OtlpValue::KvlistValue(KeyValueList {
values: fields
.iter()
.map(|(key, value)| KeyValue {
key: key.clone(),
value: Some(value_to_otlp(value)),
})
.collect(),
})),
}
}
fn value_to_otlp(value: &Value) -> AnyValue {
let value = match value {
Value::Null => None,
Value::Bool(value) => Some(OtlpValue::BoolValue(*value)),
Value::String(value) => Some(OtlpValue::StringValue(value.clone())),
Value::Array(values) => Some(OtlpValue::ArrayValue(ArrayValue {
values: values.iter().map(value_to_otlp).collect(),
})),
Value::Object(fields) => return object(fields),
Value::Number(number) => {
if let Some(integer) = number.as_i64() {
Some(OtlpValue::IntValue(integer))
} else {
let text = number.to_string();
let integer_literal = !text.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E'));
let double = if integer_literal {
None
} else {
number.as_f64().filter(|float| {
serde_json::Number::from_f64(*float).is_some_and(|candidate| {
let candidate = candidate.to_string();
text == candidate
|| decimal_parts(&text)
.is_some_and(|parts| Some(parts) == decimal_parts(&candidate))
})
})
};
Some(
double
.map(OtlpValue::DoubleValue)
.unwrap_or_else(|| OtlpValue::StringValue(text)),
)
}
}
};
AnyValue { value }
}
fn decimal_parts(text: &str) -> Option<(bool, String, i64)> {
let negative = text.starts_with('-');
let unsigned = text.strip_prefix('-').unwrap_or(text);
let (mantissa, exponent) = unsigned.split_once(['e', 'E']).unwrap_or((unsigned, "0"));
let fraction_len = mantissa
.split_once('.')
.map_or(0, |(_, fraction)| fraction.len());
let mut digits: String = mantissa
.chars()
.filter(|ch| *ch != '.')
.skip_while(|ch| *ch == '0')
.collect();
if digits.is_empty() {
return Some((negative, digits, 0));
}
let significant_len = digits.trim_end_matches('0').len();
let trailing_zeros = digits.len() - significant_len;
digits.truncate(significant_len);
let exponent = exponent
.parse::<i64>()
.ok()?
.checked_sub(i64::try_from(fraction_len).ok()?)?
.checked_add(i64::try_from(trailing_zeros).ok()?)?;
Some((negative, digits, exponent))
}