Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
594 changes: 259 additions & 335 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ path = "src/lib.rs"
name = "bunyan"

[dependencies]
clap = { version = "4.0.27", features = ["derive"] }
anyhow = "1.0.66"
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.89"
chrono = { version = "0.4.23", default-features = false, features = ["serde", "clock"] }
clap = { version = "4.5.27", features = ["derive"] }
anyhow = "1.0.95"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = { version = "1.0.137", features = ["preserve_order"] }
chrono = { version = "0.4.39", default-features = false, features = ["serde", "clock"] }
atty = "0.2.14"
colored = "2.0.0"
itertools = "0.10.5"
colored = "3.0.0"
itertools = "0.14.0"

[dev-dependencies]
assert_cmd = "2.0.6"
predicates = "2.1.3"
assert_cmd = "2.0.16"
predicates = "3.1.3"
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@

> _Structured logs are the greatest thing since sliced bread._

Are you annoyed from having to install `npm` just to get a copy of the amazing [NodeJS bunyan CLI](https://github.com/trentm/node-bunyan) to pretty-print your logs?
Are you annoyed from having to install `npm` just to get a copy of the amazing [NodeJS bunyan CLI](https://github.com/trentm/node-bunyan) to pretty-print your logs?

I feel you!

That's why I wrote `bunyan-rs`, a Rust port of (a subset of) the original [NodeJS bunyan CLI](https://github.com/trentm/node-bunyan).
That's why I wrote `bunyan-rs`, a Rust port of (a subset of) the original [NodeJS bunyan CLI](https://github.com/trentm/node-bunyan).

<div>
<img src="https://raw.githubusercontent.com/LukeMathWalker/bunyan/main/images/ConsoleBunyanOutput.png" />
Expand Down Expand Up @@ -85,10 +85,9 @@ Compared to the original `bunyan` CLI, `bunyan-rs`:
- Does not support log snooping via DTrace (`-p` argument);
- Does not support the `-c/--condition` filtering mechanism;
- Does not support the `--pager/--no-pager` flags;
- Only supports the `long` output format;
- Only supports UTC format for time.

Some of the above might or might not be added in the future.
Some of the above might or might not be added in the future.
If you are interested in contributing, please open an issue.

## Bunyan ecosystem in Rust
Expand Down Expand Up @@ -119,7 +118,7 @@ time ./benchmark_js.sh benchmark_logs.txt
time ./benchmark_rs.sh benchmark_logs.txt
```

On my system `bunyan-rs` is roughly 5x faster on this very non-scientific and highly inaccurate benchmark - your mileage may vary.
On my system `bunyan-rs` is roughly 5x faster on this very non-scientific and highly inaccurate benchmark - your mileage may vary.
The Rust code is highly non-optimised (we are allocating freely and wastefully!) - streamlining it could be a fun exercise.

## License
Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ struct Cli {
level: NumericalLogLevel,
/// Specify an output format.
///
/// - long: prettified JSON;
/// long: Default output, long form, colorful and "pretty".
///
/// short: Like the default output, but more concise.
///
/// json: JSON output, 2-space indentation.
///
/// json-N: JSON output, N-space indentation, e.g. "json-4".
///
/// bunyan: Alias for "json-0", the Bunyan "native" format.
#[arg(short, long, default_value = "long")]
output: Format,
/// Colorize output.
Expand Down
60 changes: 42 additions & 18 deletions src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,71 @@ use serde_json::Serializer;
use std::borrow::Cow;
use std::convert::TryFrom;

#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct LogRecord<'a> {
/// This is the bunyan log format version. The log version is a single integer.
/// It is meant to be 0 until version "1.0.0" of `node-bunyan` is released.
/// Thereafter, starting with 1, this will be incremented if there is any backward incompatible
/// change to the log record format.
#[serde(rename = "v")]
#[allow(dead_code)]
pub version: u8,
/// See `LogLevel`
pub level: u8,
/// Name of the service/application emitting logs in bunyan format.
pub name: &'a str,
/// Log message.
#[serde(rename = "msg")]
pub message: Cow<'a, str>,
/// See `LogLevel`
pub level: u8,
/// Name of the operating system host.
pub hostname: &'a str,
/// Process identifier.
#[serde(rename = "pid")]
pub process_identifier: u32,
/// The time of the event captured by the log in [ISO 8601 extended format](http://en.wikipedia.org/wiki/ISO_8601).
pub time: DateTime<Utc>,
/// Log message.
#[serde(rename = "msg")]
pub message: Cow<'a, str>,
/// Any extra contextual piece of information in the log record.
#[serde(flatten)]
pub extras: serde_json::Map<String, serde_json::Value>,
}

impl<'a> LogRecord<'a> {
pub fn format(&self, _format: Format) -> String {
pub fn format(&self, format: Format) -> String {
let level = format_level(self.level);
let formatted = format!(
"[{}] {}: {}/{} on {}: {}{}",
self.time.to_rfc3339_opts(SecondsFormat::Millis, true),
level,
self.name,
self.process_identifier,
self.hostname,
self.message.cyan(),
format_extras(&self.extras)
);
formatted
match format {
Format::Long => {
format!(
"[{}] {}: {}/{} on {}: {}{}",
self.time.to_rfc3339_opts(SecondsFormat::Millis, true),
level,
self.name,
self.process_identifier,
self.hostname,
self.message.cyan(),
format_extras(&self.extras)
)
}
Format::Short => {
format!(
"{} {} {}: {}{}",
self.time.format("%H:%M:%S%.3fZ"),
level,
self.name,
self.message.cyan(),
format_extras(&self.extras)
)
}
Format::Json => serde_json::to_string_pretty(&self).expect("This should not happen"),
Format::JsonN(l) => {
let indent = " ".repeat(l.into());
let value = serde_json::to_value(&self).expect("This should not happen");
json_to_indented_string(&value, &indent)
}
Format::Bunyan => format!(
"{}\n",
serde_json::to_string(&self).expect("This should not happen")
),
}
}
}

Expand Down
27 changes: 26 additions & 1 deletion src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ use std::str::FromStr;
pub enum Format {
/// Prettified JSON.
Long,
Short,
Json,
JsonN(u8),
Bunyan,
}

impl FromStr for Format {
Expand All @@ -13,7 +17,28 @@ impl FromStr for Format {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"long" => Ok(Format::Long),
_ => Err(anyhow::anyhow!(format!("Invalid format value: '{}'", s))),
"short" => Ok(Format::Short),
"json" => Ok(Format::Json),
"bunyan" => Ok(Format::Bunyan),
s => {
if s.is_ascii() {
if let Some((prefix, len_str)) = s.split_at_checked(5) {
if prefix == "json-" {
if let Ok(len) = len_str.parse::<u8>() {
if len < 1 {
return Ok(Format::Bunyan);
} else if len <= 10 {
return Ok(Format::JsonN(len));
} else {
return Ok(Format::JsonN(10));
}
}
}
};
}

Err(anyhow::anyhow!(format!("Invalid format value: '{}'", s)))
}
}
}
}
1 change: 1 addition & 0 deletions tests/all/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ mod crashers;
mod formatting;
pub mod helpers;
mod levels;
mod output;
105 changes: 105 additions & 0 deletions tests/all/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::helpers::{command, get_corpus_path};

#[test]
fn extra_field_long() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("long").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(
"[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message (extra=field)\n",
));
}

#[test]
fn extra_field_short() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("short").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(
"22:56:52.856Z INFO myservice: My message (extra=field)\n",
));
}

#[test]
fn extra_field_json() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("json").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(
r#"{
"v": 0,
"name": "myservice",
"msg": "My message",
"level": 30,
"hostname": "example.com",
"pid": 123,
"time": "2012-02-08T22:56:52.856Z",
"extra": "field"
}"#,
));
}

#[test]
fn extra_field_json4() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("json-4").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(
r#"{
"v": 0,
"name": "myservice",
"msg": "My message",
"level": 30,
"hostname": "example.com",
"pid": 123,
"time": "2012-02-08T22:56:52.856Z",
"extra": "field"
}"#,
));
}

#[test]
fn extra_field_json_more_than_10_still_10() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("json-25").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(
r#"{
"v": 0,
"name": "myservice",
"msg": "My message",
"level": 30,
"hostname": "example.com",
"pid": 123,
"time": "2012-02-08T22:56:52.856Z",
"extra": "field"
}"#,
));
}

#[test]
fn extra_field_json_0() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("json-0").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(r#"{"v":0,"name":"myservice","msg":"My message","level":30,"hostname":"example.com","pid":123,"time":"2012-02-08T22:56:52.856Z","extra":"field"}
"#
));
}

#[test]
fn extra_field_bunyan() {
let input_path = get_corpus_path().join("extrafield.log");

let mut cmd = command();
cmd.arg("-o").arg("bunyan").pipe_stdin(input_path).unwrap();
cmd.assert().success().stdout(predicates::str::diff(r#"{"v":0,"name":"myservice","msg":"My message","level":30,"hostname":"example.com","pid":123,"time":"2012-02-08T22:56:52.856Z","extra":"field"}
"#
));
}