# sjl - Simple JSON Logger

📦 **[crates.io](https://crates.io/crates/sjl)** | 📚 **[docs.rs](https://docs.rs/sjl)**

## What
It's a Simple JSON Logger. It logs JSON to `stderr`.


## Why?
The most popular logging crate, [tracing](https://crates.io/crates/tracing), has [problems with nested JSON](https://www.reddit.com/r/rust/comments/1k75jvc/how_can_i_emit_a_tracing_event_with_an_unescaped/) unless you use the `valuable` crate with it which is [unstable and behind a feature flag for 3 years](https://github.com/tokio-rs/tracing/discussions/1906)... but that [still has issues with enums](https://github.com/tokio-rs/tracing/issues/3051) and doesn't feel natural to use with `.as_value()` everywhere.  The [slog](https://crates.io/crates/slog) crate has similar issues—I've written about both [here](https://josevalerio.com/rust-json-logging).

If you just want a Simple JSON Logger, you might find this useful.



 ## Installation

 ```bash
 cargo add sjl
 ```

## Usage
```rust
use sjl::Logger;

fn main() {
    let logger = Logger::new();

    logger.info("Saul Goodman", ()); // 2nd param is optional data
}
```

### Outputs
```json
{"timestamp":"2026-05-21T02:45:03.456Z","level":"info","message":"Saul Goodman"}
```

## Extended Usage / Raison d'être
```rust
use serde::Serialize;
use sjl::LoggerOptions;

#[derive(Serialize)] // <-- All you need!
struct User {
    name: String,
    cars: Vec<Car>,
}

#[derive(Serialize)]
struct Car {
    make: String,
    model: String,
    transmission: Transmission,
}

#[derive(Serialize)]
enum Transmission {
    Automatic,
    Manual,
}

fn main() {
    let logger = LoggerOptions::default().pretty(true).init();

    let user = User {
        name: "Jose".into(),
        cars: vec![
            Car {
                make: "Toyota".into(),
                model: "Rav4".into(),
                transmission: Transmission::Manual,
            },
            Car {
                make: "Tesla".into(),
                model: "Cybertruck".into(),
                transmission: Transmission::Automatic,
            },
        ],
    };

    logger.info("Saul Goodman!", &user);
}
```

### Outputs
```json
{
  "timestamp": "2026-05-21T03:39:36.780Z",
  "level": "info",
  "message": "Saul Goodman!",
  "data": {
    "name": "Jose",
    "cars": [
    // No escaped strings!
      {
        "make": "Toyota",
        "model": "Rav4",
        "transmission": "Manual" // Enums render normally
      },
      {
        "make": "Tesla",
        "model": "Cybertruck",
        "transmission": "Automatic"
      }
    ]
  }
}
```

## All Options
```rust
use std::time::Duration;
use sjl::{LoggerOptions, LogLevel};

fn main() {
    let logger = LoggerOptions::default()
        // Context are k/v pairs that are added to every log line
        // use these for identifiers like service, environment, version, etc.
        .context("service", "payments")
        .context("environment", "production")
        // Minimum severity that actually gets emitted.
        // For example, setting this to Info will not show Debug logs
        // Hierarchy: Debug < Info < Warn < Error
        .min_level(LogLevel::Warn)
        // Batching
        // Flush once the batch reaches this many bytes
        .flush_at_bytes(1_000)
        // ...or once we have this many messages
        .flush_at_messages(100)
        // ...or once this much time has passed since the last flush.
        // Whatever comes first wins.
        .flush_interval(Duration::from_millis(250))
        // Buffer pool
        // How many buffers to keep in the pool
        // Set this to around your expected concurrent in-flight log count
        .buffer_pool_size(20)
        // Starting capacity (in bytes) of each buffer. Tune this to your typical log size
        // so that hot path logging never has to grow the buffers
        .buffer_pool_initial_capacity(4_000)
        // Hard cap on how big the buffers can get. Any that exceed this size
        // will get shrunk back down before being returned to the pool
        // So that one giant log can't be a memory hog.
        .buffer_pool_max_capacity(100_000)
        // Rename the `timestamp` field in the output
        .timestamp_key("time")
        // Custom chrono strftime format. Default is RFC 3339 with milliseconds.
        // Build your own from here: https://docs.rs/chrono/latest/chrono/format/strftime/index.html
        .timestamp_format("%FT%I:%M:%S%p")
        // Pretty-print JSON using multiple lines. Default is compact, single line.
        .pretty(true)
        // Spawns a background worker thread and returns the logger
        .init();

    logger.error("Saul Goodman!", ());

}
```

### Outputs
```json
{
  "time": "2026-05-21T03:35:04AM",
  "level": "error",
  "message": "Saul Goodman!",
  "environment": "production",
  "service": "payments"
}
```




## Running Tests
```bash
cargo llvm-cov --html
```