mod decode;
mod display;
mod recognition;
mod severity;
mod timestamp;
#[cfg(feature = "otlp")]
pub mod otlp;
use std::borrow::Cow;
use serde_json::{Map, Value};
pub use severity::Severity;
pub use timestamp::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseOutcome {
Structured,
Text,
InvalidJson,
DuplicateKeys,
InputLimit,
}
#[derive(Debug)]
pub struct ParsedLog<'a> {
original: &'a str,
fields: Map<String, Value>,
message_path: Option<&'static [&'static str]>,
clean_message: Option<String>,
severity: Option<Severity>,
timestamp: Option<Timestamp>,
outcome: ParseOutcome,
}
impl ParsedLog<'_> {
pub fn message(&self) -> &str {
self.clean_message
.as_deref()
.unwrap_or_else(|| self.source_message())
}
fn source_message(&self) -> &str {
self.message_path
.and_then(|path| recognition::at(&self.fields, path))
.and_then(Value::as_str)
.unwrap_or(self.original)
}
pub fn severity(&self) -> Option<Severity> {
self.severity
}
pub fn timestamp(&self) -> Option<Timestamp> {
self.timestamp
}
pub fn fields(&self) -> &Map<String, Value> {
&self.fields
}
pub fn original(&self) -> &str {
self.original
}
pub fn outcome(&self) -> ParseOutcome {
self.outcome
}
}
#[derive(Debug, Clone)]
pub struct Parser {
max_bytes: usize,
}
impl Default for Parser {
fn default() -> Self {
Self::with_max_bytes(1024 * 1024)
}
}
impl Parser {
pub const fn with_max_bytes(max_bytes: usize) -> Self {
Self { max_bytes }
}
pub fn parse<'a>(&self, input: &'a str) -> ParsedLog<'a> {
let (fields, view, outcome) = if input.len() > self.max_bytes {
(Map::new(), None, ParseOutcome::InputLimit)
} else if !input.trim().starts_with('{') {
(Map::new(), None, ParseOutcome::Text)
} else {
match decode::object(input) {
Ok(decoded) => (
decoded.fields,
decoded.recognition,
ParseOutcome::Structured,
),
Err(outcome) => (Map::new(), None, outcome),
}
};
let recognized = view.as_ref().unwrap_or(&fields);
let recognize = !recognized.is_empty();
let mut log = ParsedLog {
original: input,
message_path: recognize
.then(|| recognition::message_path(recognized))
.flatten(),
clean_message: None,
severity: recognize
.then(|| recognition::severity(recognized))
.flatten(),
timestamp: recognize
.then(|| recognition::timestamp(recognized))
.flatten(),
fields,
outcome,
};
if outcome != ParseOutcome::InputLimit {
if let Cow::Owned(message) = display::clean(log.source_message()) {
log.clean_message = Some(message);
}
}
log
}
}
pub fn parse(input: &str) -> ParsedLog<'_> {
Parser::default().parse(input)
}