5 releases

new 0.2.0 Jan 13, 2025
0.1.3 Oct 16, 2024
0.1.2 Sep 2, 2024
0.1.1 Aug 24, 2024
0.1.0 Aug 23, 2024

#341 in Debugging

Download history 36/week @ 2024-09-24 23/week @ 2024-10-01 143/week @ 2024-10-15 7/week @ 2024-10-22 7/week @ 2024-12-10 60/week @ 2025-01-07

60 downloads per month

MIT license

52KB
779 lines

winston

Crates.io Rust

A Winston.js-inspired logging library for Rust.

Installation

Add to your Cargo.toml:

[dependencies]
winston = "0.2"

Alternatively, run:

cargo add winston

Quick Start

Using the Global Logger

use winston::{flush, configure, log, transports::stdout, Logger, LoggerOptions};

fn main() {
    let new_options = LoggerOptions::new()
        .level("debug")
        .add_transport(stdout());

    configure(Some(new_options));

    log!(info, "Hello, world!");
    log!(warn, "Something might be wrong.");

    flush();
}

The global logger is an application-wide static reference that provides centralized logging access, requiring configuration only once to add a transport, as it starts without a default one. It eliminates the need to pass logger instances around, with functions like log(), configure() close() and flush() operating directly on this global logger. Macros like log!() implicitly use it. Since static references don’t automatically call drop, flush() is necessary to ensure all logs are processed, particularly before the application exits.

Creating Your Own Logger

use winston::{
    format::{combine, json, timestamp},
    log,
    transports::{stdout, File},
    Logger,
};

fn main() {
    let logger = Logger::builder()
        .level("debug")
        .add_transport(stdout())
        .add_transport(
            File::builder()
                .filename("app.log")
                .build()
        )
        .format(combine(vec![timestamp(), json()]))
        .build();

    log!(logger, info, "Logging with multiple transports");
}

Configuration Options

Option Description Default Value
level Minimum severity of log messages to be logged. Anything equal to or higher in severity will be logged. info
levels Severity levels for log entries. {error: 0, warn: 1, info: 2, debug: 3, trace: 4}
transports Logging destinations (stdout, stderr, File, Custom). None
format Log message formatting (e.g., json, timestamp). json
channel_capacity Maximum size of the log message buffer. 1024
backpressure_strategy Action when buffer is full (Block, DropOldest, DropCurrent). Block

Logging Basics

The log! Macro

The simplest way to log messages is using the log! macro:

// Using the global logger
log!(info, "System started");
log!(warn, "Disk space low", usage = 92);

// Using a specific logger
log!(logger, error, "Connection failed", retries = 3, timeout = 120);

It takes the following parameters:

  • level: Log level (info, warn, error, etc.).
  • Message: A string message.
  • Optional key-value pairs: Metadata to add context.

Level-specific Methods and Macros

Winston provides macros to create level-specific logging methods and macros:

use std::collections::HashMap;
use winston::{meta, Logger};

// Define custom logging methods and macros
winston::create_log_methods!(foo, bar, baz, foobar);  // Creates methods like logger.foo(), logger.bar(), etc.
winston::create_level_macros!(foo, bar, baz, foobar);  // Creates macros like foo!(), bar!(), etc.

let logger = Logger::builder()
    .add_transport(stdout())  // Use stdout as the logging transport
    .levels(HashMap::from([  // Define custom log levels and their severity values
        ("foo", 0),  // Most severe
        ("bar", 1),
        ("baz", 2),
        ("foobar", 3)   // Least severe
    ]))
    .level("bar")    // Set the minimum log level to "bar" (log bar and more severe levels)
    .build();  // Build the logger instance

// Usage of the logger methods with various levels and metadata:

// Log a message at the "foo" level with no metadata
logger.foo("Foo-level message", None);

// Log a message at the "foobar" level with metadata (key-value pairs)
logger.foobar(
    "Foobar-level message with metadata",
    Some(meta!(key = "value", timestamp = 1234567890))
);

// Use the "foobar" macro to log a message at the "foobar" level
foobar!(logger, "Foobar-level macro logging");

// with metadata
foobar!(logger, "Foobar-level macro logging with metadata", meta!(key3 = true, key4 = 42.5));

// Log a message at the "foo" level globally (no logger instance needed)
foo!("Global log test");

// Log a message at the "foo" level globally with metadata
foo!(@global, "Another global test", meta!(key3 = true, key4 = 42.5));

Key Concepts

Transports

Transports define the destinations where log messages are written. Winston includes core transports that leverage Rust's standard I/O capabilities, with additional custom transports possible through community contributions. Each transport implements the Transport trait from winston_transport:

pub trait Transport: Send + Sync {
    // Required: Handles writing log messages
    fn log(&self, info: LogInfo);

    // Optional: Flushes buffered logs
    fn flush(&self) -> Result<(), String> { Ok(()) }

    // Optional: Gets minimum log level
    fn get_level(&self) -> Option<&String> { None }

    // Optional: Gets format configuration
    fn get_format(&self) -> Option<&Format> { None }

    // Optional: Retrieves matching log entries
    fn query(&self, _options: &LogQuery) -> Result<Vec<LogInfo>, String> { Ok(Vec::new()) }
}

Built-in Transports

Winston provides two core transports:

WriterTransport

A generic transport that writes to any destination implementing Rust's Write trait:

use std::io::{self, Write};
use winston::transports::WriterTransport;

// Write to stdout
let stdout_transport = WriterTransport::new(io::stdout())
    .with_level("info");

// Write to a file
let file = std::fs::File::create("app.log").unwrap();
let file_transport = WriterTransport::new(file)
    .with_format(json());

// Write to a network socket
let stream = std::net::TcpStream::connect("127.0.0.1:8080").unwrap();
let network_transport = WriterTransport::new(stream);

There are quick WriterTransport creation for common use cases:

use winston::transports::{stdout, stderr};

// Quick stdout/stderr transports
let logger = Logger::builder()
    .add_transport(stdout())    // Same as WriterTransport::new(io::stdout())
    .add_transport(stderr())    // Same as WriterTransport::new(io::stderr())
    .build();
File Transport

Specialized file transport with querying capabilities for log retrieval.

Creating Custom Transports

To define a custom transport, implement the Transport trait and define the log method:

use winston::{log, LogInfo, Transport};

struct MyCustomTransport;

impl Transport for MyCustomTransport {
    fn log(&self, info: LogInfo) {
        println!("Custom transport: {}", info.message);
    }
}

fn main() {
    let custom_transport = MyCustomTransport;

    let logger = Logger::builder()
        .add_transport(custom_transport)
        .build();

    log!(info, "This uses a custom transport!");
}

Multiple Transports

You can use multiple transports simultaneously, even of the same type. Each transport can have its own configuration:

use winston::{log, Logger, format::{json, simple}, transports::{stdout, WriterTransport}};
use std::fs::File;

let logger = Logger::builder()
    // Log all info and above to stdout with simple formatting
    .add_transport(
        stdout()
            .with_level("info")
            .with_format(simple())
    )
    // Log all error to file with JSON formatting
    .add_transport(
        WriterTransport::new(File::create("app.log").unwrap())
            .with_level("error")
            .with_format(json())
    )
    .build();

// Usage
log!(error, "Appears in file only");
log!(info, "Appears in both stdout and file");

Logging Levels

Winston's logging levels conform to the severity ordering specified by RFC 5424, ranked in ascending order of importance. Lower numeric values indicate more critical (important) events.

levels: {
    error: 0,   // Critical errors - issues causing system failure
    warn:  1,   // Warnings - potential problems or recoverable issues
    info:  2,   // Informational - standard operations tracking
    debug: 3,   // Debugging - diagnostic details for troubleshooting
    trace: 4    // Tracing - the most verbose, fine-grained logs
}

Custom Logging Levels

In addition to the predefined rust, syslog, and cli levels available in winston via winston::format::config::, you can also choose to define your own:

use std::collections::HashMap;
use winston::Logger;

let custom_levels = HashMap::from([
    ("critical", 0),  // Highest severity
    ("high",     1),
    ("medium",   2),
    ("low",      3)   // Lowest severity
]);

let logger = Logger::builder()
    .levels(custom_levels)
    .build();

Log Level

The level configuration represents the minimum severity of messages to be logged. For instance, if the level is set to "info", the logger will process only info, warn, and error messages while ignoring less critical levels like debug and trace.

let logger = Logger::builder()
    .level("info")  // Logs only info, warn, and error levels
    .build();

Per-Transport Log Level

Each transport can define its own log level, overriding the logger level. This allows for targeted logging based on the output medium.

let logger = Logger::builder()
    .level("info")  // Logger default level
    .add_transport(
        stderr()
            .with_level("error")  // stderr only logs error messages
    )
    .add_transport(
        File::builder()
            .filename("app.log")
            .level("debug")  // File logs debug and above
            .build()
    )
    .build();

In this example:

  • The logger level is set to info.
  • The stderr transport logs only error messages.
  • The file transport logs debug and higher (i.e., debug, info, warn, and error).

Formats

For advanced formatting, Winston leverages logform, which is re-exported as winston::format for convenience:

use winston::{Logger, format::{combine, timestamp, json}};

let logger = Logger::builder()
    .format(combine(vec![timestamp(), json()]))
    .build();

Each transport can have its own format, which takes precedence over the logger format:

let logger = Logger::builder()
    .format(json())  // Logger default format
    .add_transport(
        stdout()
            .with_format(combine(vec![timestamp(), colored()]))  // Colorized console output
    )
    .add_transport(
        File::builder()
            .filename("app.log")
            .format(json())  // Structured JSON for file logs
            .build()
    )
    .build();

Advanced Features

Backpressure Handling

Winston provides three backpressure strategies when the logging channel is full:

  • Block: Wait until space is available
  • DropOldest: Remove the oldest log message
  • DropCurrent: Discard the current log message
use winston::{Logger, BackpressureStrategy};

let logger = Logger::builder()
    .channel_capacity(100)
    .backpressure_strategy(BackpressureStrategy::DropOldest)
    .build();

Log Querying

Winston supports retrieving log entries from transports.

To enable querying for a custom transport, override the query method in your Transport implementation.

use winston::{Logger, LogQuery};

let query = LogQuery::new()
    .from("2 hours ago")
    .until("now")
    .levels(vec!["error"])
    .limit(10)
    .order("desc")
    .search_term("critical")

let results = logger.query(query);

LogQuery Configuration Options

Option Description Default Value
from Start time for the query (supports string formats compatible with parse_datetime) Utc::now() - Duration::days(1)
until End time for the query(supports string formats compatible with parse_datetime) Utc::now()
limit Maximum number of log entries to retrieve. 50
start Offset for query results, used for pagination. 0
order Order of results, either asc, ascending, descending or desc. Descending
levels List of log levels to include in the query (e.g., ["error", "info"]). [] (no filter, includes all levels)
fields List of fields to filter by. [] (no specific fields required)
search_term Text to search for in log messages. None (no search term applied)

Logging Timestamps

Timestamps are essential for effective log querying. Log entries must include a timestamp field in their metadata (LogInfo.meta) for Winston’s querying capabilities to function as expected. The timestamp field should be a string compatible with dateparser.

Winston's built-in timestamp format simplifies this requirement:

use winston::{Logger, timestamp};

let logger = Logger::new()
    .format(timestamp()) // Adds a timestamp to each log entry
    .build();

Runtime Reconfiguration

Change logging configuration dynamically at runtime:

use winston::{transports::stdout, Logger, LoggerOptions};

let logger = Logger::default();
logger.configure(
    LoggerOptions::new()
        .level("debug")
        .add_transport(stdout())
);

Performance

  • Configurable Buffering: Adjust channel capacity to match your application's needs

Contributing

Contributions are welcome! Please submit issues and pull requests on our GitHub repository.

License

MIT License

Dependencies

~6–15MB
~182K SLoC