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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ chrono = { version = "0.4.19", features = ["serde"] }
atty = "0.2.14"
colored = "2.0.0"
itertools = "0.10.1"
regex = "1.5.4"

[dev-dependencies]
assert_cmd = "2.0.2"
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@

> _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" />
</div>
<hr/>

# Table of Contents

0. [How to install](#how-to-install)
1. [How to use](#how-to-use)
2. [Limitations](#limitations)
Expand All @@ -45,11 +46,13 @@ That's why I wrote `bunyan-rs`, a Rust port of (a subset of) the original [NodeJ
## How to install

Using `cargo`:

```bash
cargo install bunyan
```

You can verify your installation with

```bash
bunyan --help
```
Expand All @@ -61,11 +64,13 @@ Alternatively, you can download a pre-built binary for your operating system fro
`bunyan-rs` only supports stdin as input source.

You can pipe a log file into it:

```bash
cat tests/all/corpus/all.log | bunyan
```

Or you can pipe the output of a long-running job into it:

```bash
# Tail logs from a Docker container
docker logs -f my-app | bunyan
Expand All @@ -83,7 +88,7 @@ Compared to the original `bunyan` CLI, `bunyan-rs`:

- Only supports `stdin` as input source (no files);
- Does not support log snooping via DTrace (`-p` argument);
- Does not support the `-c/--condition` filtering mechanism;
- The `-c/--condition` filtering mechanism onlys supports a simple one-field comparison (`key==value` or `key < value`). It will find fields inside of nested JSON objects - but only compares based on the first one it finds. All standard binary comparison operators are supported.
- Does not support the `--pager/--no-pager` flags;
- Only supports the `long` output format;
- Only supports UTC format for time.
Expand All @@ -107,11 +112,14 @@ Speed has never been a burning problem while eyeballing logs from applications,
To benchmark `bunyan-rs` against the original NodeJS `bunyan` follow these steps:

- Build `bunyan-rs` using the `release` profile:

```bash
cargo build --release
```

- Install `bunyan` via `npm`. You will need `npx` as well;
- Benchmark!

```bash
# bunyan JS
time ./benchmark_js.sh benchmark_logs.txt
Expand Down
85 changes: 85 additions & 0 deletions src/compare/compare_operators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use crate::record::{LogRecord, LogRecordTypes};
use regex::Regex;
use std::str::FromStr;

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Comparitors {
Equal,
NotEqual,
GreaterThan,
LessThan,
GreaterOrEqualThan,
LessOrEqualThan,
}

impl FromStr for Comparitors {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"==" => Ok(Comparitors::Equal),
"!=" => Ok(Comparitors::NotEqual),
">" => Ok(Comparitors::GreaterThan),
"<" => Ok(Comparitors::LessThan),
">=" => Ok(Comparitors::GreaterOrEqualThan),
"<=" => Ok(Comparitors::LessOrEqualThan),
_ => Err(anyhow::anyhow!(format!("Invalid comparitor: '{}'", s))),
}
}
}

impl Comparitors {
pub fn get_regex() -> regex::Regex {
Regex::new(r"\s?(==|!=|>=|<=|<|>)\s?").unwrap()
}
pub fn compare<T: PartialEq + PartialOrd>(&self, operand1: T, operand2: T) -> bool {
match *self {
Comparitors::Equal => operand1 == operand2,
Comparitors::NotEqual => operand1 != operand2,
Comparitors::GreaterThan => operand1 > operand2,
Comparitors::LessThan => operand1 < operand2,
Comparitors::GreaterOrEqualThan => operand1 >= operand2,
Comparitors::LessOrEqualThan => operand1 <= operand2,
}
}
}

pub struct CompiledComparison {
pub key: String,
pub operator: Comparitors,
pub value: String,
}

impl CompiledComparison {
pub fn new(compare: &str) -> CompiledComparison {
let re = Comparitors::get_regex();
let mat = re
.find(compare)
.unwrap_or_else(|| panic!("Invalid comparison syntax of:{}", compare));
let key: String = compare[0..mat.start()].to_string();
let operator = Comparitors::from_str(compare[mat.start()..mat.end()].as_ref()).unwrap();
let value: String = compare[mat.end()..].to_string();
CompiledComparison {
key,
operator,
value,
}
}
}

pub fn do_compare(r: &LogRecord, cc: &CompiledComparison) -> bool {
match r.field_by_name(&cc.key) {
Some(x) => match x {
LogRecordTypes::U8(y) => {
let u8_value: u8 = cc.value.parse().expect("getting u8 from value");
cc.operator.compare(y, u8_value)
}
LogRecordTypes::Num(y) => {
let f64_value: f64 = cc.value.parse().expect("getting u32 from value");
cc.operator.compare(y, f64_value)
}
LogRecordTypes::Str(y) => cc.operator.compare(&y, &cc.value),
},
_ => false,
}
}
3 changes: 3 additions & 0 deletions src/compare/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) mod compare_operators;

pub use compare_operators::Comparitors;
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod compare;
mod level;
mod record;
mod sources;
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ struct Cli {
/// numeric value.
#[clap(short, long, default_value = "trace")]
level: NumericalLogLevel,
/// Set a filter based on comparing a field to a provided value
///
/// form of <field> <comparison> <value>, ie: -c 'hostname == my.host.name'
#[clap(short, long)]
compare: Option<String>,
/// Specify an output format.
///
/// - long: prettified JSON;
Expand Down Expand Up @@ -43,5 +48,5 @@ fn main() {
colored::control::set_override(true);
}

process_stdin(cli.output, cli.level.0, cli.strict);
process_stdin(cli.output, cli.level.0, cli.strict, cli.compare);
}
64 changes: 62 additions & 2 deletions src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use itertools::Itertools;
use serde::Serialize;
use serde_json::ser::PrettyFormatter;
use serde_json::Serializer;
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::convert::TryFrom;

Expand Down Expand Up @@ -32,7 +33,15 @@ pub struct LogRecord<'a> {
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>,
pub extras: Map<String, Value>,
}

//pub enum LogRecordTypes<'a> {
pub enum LogRecordTypes {
U8(u8),
Str(String),
//Using largest possible number type for flexibility - so we only have to have one type
Num(f64),
}

impl<'a> LogRecord<'a> {
Expand All @@ -50,8 +59,59 @@ impl<'a> LogRecord<'a> {
);
formatted
}
pub fn field_by_name(&self, field_name: &str) -> Option<LogRecordTypes> {
match field_name.to_lowercase().as_str() {
"version" => Some(LogRecordTypes::U8(self.version)),
"level" => Some(LogRecordTypes::U8(self.level)),
"name" => Some(LogRecordTypes::Str(self.name.to_owned())),
"hostname" => Some(LogRecordTypes::Str(self.hostname.to_owned())),
"pid" => Some(LogRecordTypes::Num(self.process_identifier as f64)),
"time" => Some(LogRecordTypes::Str(
self.time.to_rfc3339_opts(SecondsFormat::Millis, true),
)),
"message" => Some(LogRecordTypes::Str(self.message.to_string())),
// if we can't find our field, see if it is buried inside the extras
_ => get_field_from_extras(field_name, &self.extras),
}
}
}
fn get_field_from_extras(
input_key: &str,
extra: &serde_json::value::Map<String, serde_json::Value>,
) -> Option<LogRecordTypes> {
// Walking through the map instead of using extra.get() to deal with nested objects
for (key, value) in extra.iter() {
if key == input_key {
match value {
Value::Null => return None,
Value::Bool(b) => {
return Some(LogRecordTypes::Str(format!("{}", b)));
}
Value::Number(n) => {
return Some(LogRecordTypes::Num(n.as_f64().unwrap()));
}
Value::String(s) => {
return Some(LogRecordTypes::Str(s.to_string()));
}
Value::Object(_) => return None,
Value::Array(_) => return None,
};
} else {
match value {
Value::Object(o) => {
// We have a nested object, if we can find our field inside it, return it
if let Some(nested_value) = get_field_from_extras(input_key, o) {
return Some(nested_value);
}
}
_ => {
continue;
}
}
}
}
None
}

pub fn format_level(level: u8) -> String {
if let Ok(level) = NamedLogLevel::try_from(level) {
match level {
Expand Down
26 changes: 24 additions & 2 deletions src/sources/stdin.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
use crate::compare::compare_operators::do_compare;
use crate::compare::compare_operators::CompiledComparison;
use crate::record::LogRecord;
use crate::Format;
use std::io::BufRead;

pub fn process_stdin(format: Format, level_filter: u8, strict: bool) {
pub fn process_stdin(format: Format, level_filter: u8, strict: bool, compare: Option<String>) {
let stdin = std::io::stdin();
let compare_set;
let cc;

match compare {
Some(c_string) => {
compare_set = true;
let compare_string = c_string;
cc = Some(CompiledComparison::new(&compare_string));
}
None => {
compare_set = false;
cc = None;
}
}
for line in stdin.lock().lines() {
let line = line.unwrap();
match serde_json::from_str::<LogRecord>(&line) {
Ok(r) => {
if r.level >= level_filter {
print!("{}", r.format(format))
if compare_set {
if do_compare(&r, cc.as_ref().unwrap()) {
print!("{}", r.format(format));
}
} else {
print!("{}", r.format(format))
}
}
}
Err(_) => {
Expand Down
Loading