409 lines
11 KiB
Rust
409 lines
11 KiB
Rust
#![deny(rustdoc::broken_intra_doc_links, rustdoc::bare_urls, rust_2018_idioms)]
|
|
|
|
use observability_deps::tracing::{
|
|
self,
|
|
field::{Field, Visit},
|
|
subscriber::Interest,
|
|
Id, Level, Subscriber,
|
|
};
|
|
use std::borrow::Cow;
|
|
use std::{io::Write, time::SystemTime};
|
|
use tracing_subscriber::{fmt::MakeWriter, layer::Context, registry::LookupSpan, Layer};
|
|
|
|
/// Implements a `tracing_subscriber::Layer` which generates
|
|
/// [logfmt] formatted log entries, suitable for log ingestion
|
|
///
|
|
/// At time of writing, I could find no good existing crate
|
|
///
|
|
/// <https://github.com/mcountryman/logfmt_logger> from @mcountryman
|
|
/// looked very small and did not (obviously) work with the tracing subscriber
|
|
///
|
|
/// [logfmt]: https://brandur.org/logfmt
|
|
pub struct LogFmtLayer<W>
|
|
where
|
|
W: for<'writer> MakeWriter<'writer>,
|
|
{
|
|
writer: W,
|
|
display_target: bool,
|
|
}
|
|
|
|
impl<W> LogFmtLayer<W>
|
|
where
|
|
W: for<'writer> MakeWriter<'writer>,
|
|
{
|
|
/// Create a new logfmt Layer to pass into tracing_subscriber
|
|
///
|
|
/// Note this layer simply formats and writes to the specified writer. It
|
|
/// does not do any filtering for levels itself. Filtering can be done
|
|
/// using a EnvFilter
|
|
///
|
|
/// For example:
|
|
/// ```
|
|
/// use logfmt::LogFmtLayer;
|
|
/// use tracing_subscriber::{EnvFilter, prelude::*, self};
|
|
///
|
|
/// // setup debug logging level
|
|
/// std::env::set_var("RUST_LOG", "debug");
|
|
///
|
|
/// // setup formatter to write to stderr
|
|
/// let formatter =
|
|
/// LogFmtLayer::new(std::io::stderr);
|
|
///
|
|
/// tracing_subscriber::registry()
|
|
/// .with(EnvFilter::from_default_env())
|
|
/// .with(formatter)
|
|
/// .init();
|
|
/// ```
|
|
pub fn new(writer: W) -> Self {
|
|
Self {
|
|
writer,
|
|
display_target: true,
|
|
}
|
|
}
|
|
|
|
/// Control whether target and location attributes are displayed (on by default).
|
|
///
|
|
/// Note: this API mimics that of other fmt layers in tracing-subscriber crate.
|
|
pub fn with_target(self, display_target: bool) -> Self {
|
|
Self {
|
|
display_target,
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S, W> Layer<S> for LogFmtLayer<W>
|
|
where
|
|
W: for<'writer> MakeWriter<'writer> + 'static,
|
|
S: Subscriber + for<'a> LookupSpan<'a>,
|
|
{
|
|
fn register_callsite(
|
|
&self,
|
|
_metadata: &'static tracing::Metadata<'static>,
|
|
) -> tracing::subscriber::Interest {
|
|
Interest::always()
|
|
}
|
|
|
|
fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
|
|
let writer = self.writer.make_writer();
|
|
let metadata = ctx.metadata(id).expect("span should have metadata");
|
|
let mut p = FieldPrinter::new(writer, metadata.level(), self.display_target);
|
|
p.write_span_name(metadata.name());
|
|
attrs.record(&mut p);
|
|
p.write_span_id(id);
|
|
p.write_timestamp();
|
|
}
|
|
|
|
fn max_level_hint(&self) -> Option<tracing::metadata::LevelFilter> {
|
|
None
|
|
}
|
|
|
|
fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
|
|
let writer = self.writer.make_writer();
|
|
let mut p = FieldPrinter::new(writer, event.metadata().level(), self.display_target);
|
|
// record fields
|
|
event.record(&mut p);
|
|
if let Some(span) = ctx.lookup_current() {
|
|
p.write_span_id(&span.id())
|
|
}
|
|
// record source information
|
|
p.write_source_info(event);
|
|
p.write_timestamp();
|
|
}
|
|
}
|
|
|
|
/// This thing is responsible for actually printing log information to
|
|
/// a writer
|
|
struct FieldPrinter<W: Write> {
|
|
writer: W,
|
|
display_target: bool,
|
|
}
|
|
|
|
impl<W: Write> FieldPrinter<W> {
|
|
fn new(mut writer: W, level: &Level, display_target: bool) -> Self {
|
|
let level_str = match *level {
|
|
Level::TRACE => "trace",
|
|
Level::DEBUG => "debug",
|
|
Level::INFO => "info",
|
|
Level::WARN => "warn",
|
|
Level::ERROR => "error",
|
|
};
|
|
|
|
write!(writer, r#"level={}"#, level_str).ok();
|
|
|
|
Self {
|
|
writer,
|
|
display_target,
|
|
}
|
|
}
|
|
|
|
fn write_span_name(&mut self, value: &str) {
|
|
write!(self.writer, " span_name=\"{}\"", quote_and_escape(value)).ok();
|
|
}
|
|
|
|
fn write_source_info(&mut self, event: &tracing::Event<'_>) {
|
|
if !self.display_target {
|
|
return;
|
|
}
|
|
|
|
let metadata = event.metadata();
|
|
write!(
|
|
self.writer,
|
|
" target=\"{}\"",
|
|
quote_and_escape(metadata.target())
|
|
)
|
|
.ok();
|
|
|
|
if let Some(module_path) = metadata.module_path() {
|
|
if metadata.target() != module_path {
|
|
write!(self.writer, " module_path=\"{}\"", module_path).ok();
|
|
}
|
|
}
|
|
if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) {
|
|
write!(self.writer, " location=\"{}:{}\"", file, line).ok();
|
|
}
|
|
}
|
|
|
|
fn write_span_id(&mut self, id: &Id) {
|
|
write!(self.writer, " span={}", id.into_u64()).ok();
|
|
}
|
|
|
|
fn write_timestamp(&mut self) {
|
|
let ns_since_epoch = SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.expect("System time should have been after the epoch")
|
|
.as_nanos();
|
|
|
|
write!(self.writer, " time={:?}", ns_since_epoch).ok();
|
|
}
|
|
}
|
|
|
|
impl<W: Write> Drop for FieldPrinter<W> {
|
|
fn drop(&mut self) {
|
|
// finish the log line
|
|
writeln!(self.writer).ok();
|
|
}
|
|
}
|
|
|
|
impl<W: Write> Visit for FieldPrinter<W> {
|
|
fn record_i64(&mut self, field: &Field, value: i64) {
|
|
write!(
|
|
self.writer,
|
|
" {}={}",
|
|
translate_field_name(field.name()),
|
|
value
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn record_u64(&mut self, field: &Field, value: u64) {
|
|
write!(
|
|
self.writer,
|
|
" {}={}",
|
|
translate_field_name(field.name()),
|
|
value
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn record_bool(&mut self, field: &Field, value: bool) {
|
|
write!(
|
|
self.writer,
|
|
" {}={}",
|
|
translate_field_name(field.name()),
|
|
value
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn record_str(&mut self, field: &Field, value: &str) {
|
|
write!(
|
|
self.writer,
|
|
" {}={}",
|
|
translate_field_name(field.name()),
|
|
quote_and_escape(value)
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
|
|
let field_name = translate_field_name(field.name());
|
|
|
|
let debug_formatted = format!("{:?}", value);
|
|
write!(
|
|
self.writer,
|
|
" {}={:?}",
|
|
field_name,
|
|
quote_and_escape(&debug_formatted)
|
|
)
|
|
.ok();
|
|
|
|
let display_formatted = format!("{}", value);
|
|
write!(
|
|
self.writer,
|
|
" {}.display={}",
|
|
field_name,
|
|
quote_and_escape(&display_formatted)
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
|
// Note this appears to be invoked via `debug!` and `info! macros
|
|
let formatted_value = format!("{:?}", value);
|
|
write!(
|
|
self.writer,
|
|
" {}={}",
|
|
translate_field_name(field.name()),
|
|
quote_and_escape(&formatted_value)
|
|
)
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
/// return true if the string value already starts/ends with quotes and is
|
|
/// already properly escaped (all spaces escaped)
|
|
fn needs_quotes_and_escaping(value: &str) -> bool {
|
|
// mismatches beginning / end quotes
|
|
if value.starts_with('"') != value.ends_with('"') {
|
|
return true;
|
|
}
|
|
|
|
// ignore beginning/ending quotes, if any
|
|
let pre_quoted = value.len() >= 2 && value.starts_with('"') && value.ends_with('"');
|
|
|
|
let value = if pre_quoted {
|
|
&value[1..value.len() - 1]
|
|
} else {
|
|
value
|
|
};
|
|
|
|
// unescaped quotes
|
|
let c0 = value.chars();
|
|
let c1 = value.chars().skip(1);
|
|
if c0.zip(c1).any(|(c0, c1)| c0 != '\\' && c1 == '"') {
|
|
return true;
|
|
}
|
|
|
|
// Quote any strings that contain a literal '=' which the logfmt parser
|
|
// interprets as a key/value separator.
|
|
if value.chars().any(|c| c == '=') && !pre_quoted {
|
|
return true;
|
|
}
|
|
|
|
if value.bytes().any(|b| b <= b' ') && !pre_quoted {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// escape any characters in name as needed, otherwise return string as is
|
|
fn quote_and_escape(value: &'_ str) -> Cow<'_, str> {
|
|
if needs_quotes_and_escaping(value) {
|
|
Cow::Owned(format!("{:?}", value))
|
|
} else {
|
|
Cow::Borrowed(value)
|
|
}
|
|
}
|
|
|
|
// Translate the field name from tracing into the logfmt style
|
|
fn translate_field_name(name: &str) -> &str {
|
|
if name == "message" {
|
|
"msg"
|
|
} else {
|
|
name
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn quote_and_escape_len0() {
|
|
assert_eq!(quote_and_escape(""), "");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len1() {
|
|
assert_eq!(quote_and_escape("f"), "f");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len2() {
|
|
assert_eq!(quote_and_escape("fo"), "fo");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len3() {
|
|
assert_eq!(quote_and_escape("foo"), "foo");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len3_1quote_start() {
|
|
assert_eq!(quote_and_escape("\"foo"), "\"\\\"foo\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len3_1quote_end() {
|
|
assert_eq!(quote_and_escape("foo\""), "\"foo\\\"\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_len3_2quote() {
|
|
assert_eq!(quote_and_escape("\"foo\""), "\"foo\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_space() {
|
|
assert_eq!(quote_and_escape("foo bar"), "\"foo bar\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_space_prequoted() {
|
|
assert_eq!(quote_and_escape("\"foo bar\""), "\"foo bar\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_space_prequoted_but_not_escaped() {
|
|
assert_eq!(quote_and_escape("\"foo \"bar\""), "\"\\\"foo \\\"bar\\\"\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_quoted_quotes() {
|
|
assert_eq!(quote_and_escape("foo:\"bar\""), "\"foo:\\\"bar\\\"\"");
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_nested_1() {
|
|
assert_eq!(quote_and_escape(r#"a "b" c"#), r#""a \"b\" c""#);
|
|
}
|
|
|
|
#[test]
|
|
fn quote_and_escape_nested_2() {
|
|
assert_eq!(
|
|
quote_and_escape(r#"a "0 \"1\" 2" c"#),
|
|
r#""a \"0 \\\"1\\\" 2\" c""#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quote_not_printable() {
|
|
assert_eq!(quote_and_escape("foo\nbar"), r#""foo\nbar""#);
|
|
assert_eq!(quote_and_escape("foo\r\nbar"), r#""foo\r\nbar""#);
|
|
assert_eq!(quote_and_escape("foo\0bar"), r#""foo\0bar""#);
|
|
}
|
|
|
|
#[test]
|
|
fn not_quote_unicode_unnecessarily() {
|
|
assert_eq!(quote_and_escape("mikuličić"), "mikuličić");
|
|
}
|
|
|
|
#[test]
|
|
// https://github.com/influxdata/influxdb_iox/issues/4352
|
|
fn test_uri_quoted() {
|
|
assert_eq!(quote_and_escape("/api/v2/write?bucket=06fddb4f912a0d7f&org=9df0256628d1f506&orgID=9df0256628d1f506&precision=ns"), r#""/api/v2/write?bucket=06fddb4f912a0d7f&org=9df0256628d1f506&orgID=9df0256628d1f506&precision=ns""#);
|
|
}
|
|
}
|