logged-stream 0.7.0

Logging of all read/write operations, errors and drop of underlying IO object.
Documentation
# logged-stream <!-- omit in toc -->

_Transparent logging wrapper for any synchronous or asynchronous Rust IO stream — inspect every byte, error, and lifecycle event that passes through._

[![Crates.io version][crates-version-badge]][crates-url]
[![Crates.io downloads][crates-downloads-badge]][crates-url]
[![Released API docs][docs-badge]][docs-url]
[![Master API docs][master-docs-badge]][master-docs-url]
[![Rust version][rust-version]][rust-url]
[![License][license-badge]][license-url]
[![Workflow Status][workflow-badge]][actions-url]
[![Lines count][sloc-badge]][scc-repo-url]
[![Cocomo][cocomo-badge]][scc-repo-url]

[crates-version-badge]: https://img.shields.io/crates/v/logged-stream.svg
[crates-downloads-badge]: https://img.shields.io/crates/d/logged-stream.svg
[crates-url]: https://crates.io/crates/logged-stream
[docs-badge]: https://docs.rs/logged-stream/badge.svg
[docs-url]: https://docs.rs/logged-stream
[license-badge]: https://img.shields.io/crates/l/logged-stream.svg
[license-url]: https://github.com/qwerty541/logged-stream/blob/master/LICENSE-MIT
[master-docs-badge]: https://img.shields.io/badge/docs-master-green.svg
[master-docs-url]: https://qwerty541.github.io/logged-stream/master/
[workflow-badge]: https://github.com/qwerty541/logged-stream/workflows/check/badge.svg
[actions-url]: https://github.com/qwerty541/logged-stream/actions
[rust-version]: https://img.shields.io/badge/rust-1.85.1%2B-lightgrey.svg?logo=rust
[rust-url]: https://blog.rust-lang.org/
[sloc-badge]: https://sloc.xyz/github/qwerty541/logged-stream/?badge-bg-color=2081C2
[cocomo-badge]: https://sloc.xyz/github/qwerty541/logged-stream/?badge-bg-color=2081C2&category=cocomo
[scc-repo-url]: https://github.com/boyter/scc

<details>
<summary>Table of contents</summary>

- [Description]#description
- [Usage]#usage
- [Quick start]#quick-start
- [Example]#example
- [Record kinds]#record-kinds
- [Architecture]#architecture
- [Provided implementations]#provided-implementations
  - [Formatters (`BufferFormatter`)]#formatters-bufferformatter
  - [Filters (`RecordFilter`)]#filters-recordfilter
  - [Loggers (`Logger`)]#loggers-logger
- [Use cases]#use-cases
- [License]#license
- [Contribution]#contribution
</details>

## Description

`logged-stream` provides a single wrapper type, `LoggedStream`, that wraps any underlying IO object and logs every read, write, error, shutdown and drop that passes through it — without changing the code on either side. Because it re-implements the same IO trait it wraps, it drops straight into existing synchronous or asynchronous code.

-   **Drop-in.** Same trait in, same trait out — wrapping a stream leaves the surrounding code unchanged.
-   **Sync and async.** Works with `std::io` `Read`/`Write` streams and `tokio` `AsyncRead`/`AsyncWrite` streams alike.
-   **Composable.** Choose how bytes are formatted, which records are kept, and where they go — or plug in your own.
-   **Small and safe.** A small dependency set and no `unsafe` code.

Each logged event flows through four pluggable parts — `event -> Formatter -> Filter -> Logger` — described under [Architecture](#architecture).

## Usage

Add `logged-stream` to your `Cargo.toml`:

```toml
[dependencies]
logged-stream = "0.7"
```

or run:

```console
$ cargo add logged-stream@0.7
```

It requires **Rust 1.85.1 or newer** (Rust 2024 edition).

## Quick start

`LoggedStream::new` takes the stream to wrap plus the three pluggable parts — a formatter, a filter, and a logger. The snippet below wraps an in-memory [`Cursor`][cursor] (standing in for a socket or file, so it runs with no external setup), renders bytes as lowercase hexadecimal, keeps every record with `DefaultFilter`, and prints them through `ConsoleLogger`:

```rust
use log::LevelFilter;
use logged_stream::ConsoleLogger;
use logged_stream::DefaultFilter;
use logged_stream::LoggedStream;
use logged_stream::LowercaseHexadecimalFormatter;
use std::io::Cursor;
use std::io::Read;

fn main() {
    // ConsoleLogger forwards records to the `log` facade; env_logger prints them.
    env_logger::builder()
        .filter_level(LevelFilter::Debug)
        .format_timestamp_millis()
        .init();

    let mut stream = LoggedStream::new(
        Cursor::new(vec![0xde, 0xad, 0xbe, 0xef]),
        LowercaseHexadecimalFormatter::new_default(),
        DefaultFilter,
        ConsoleLogger::new_unchecked("debug"),
    );

    let mut buf = [0u8; 4];
    stream.read_exact(&mut buf).unwrap();
}
```

Running it logs the read, then the drop when `stream` goes out of scope:

```log
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] < de:ad:be:ef
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] x Deallocated.
```

> To see any output you also need `env_logger` and `log` in your `Cargo.toml`: `ConsoleLogger` only forwards records to the [`log`][log] facade, and any backend works.

See [Provided implementations](#provided-implementations) for the full set of formatters, filters and loggers you can swap in.

[cursor]: https://doc.rust-lang.org/std/io/struct.Cursor.html
[log]: https://crates.io/crates/log

## Example

A more realistic case: wrapping a `std::net::TcpStream` connected to an echo server. Each `write_all` logs a `>` record and each `read_exact` logs a `<` record, so both directions of the exchange are visible.

```rust
use log::LevelFilter;
use logged_stream::ConsoleLogger;
use logged_stream::DefaultFilter;
use logged_stream::LoggedStream;
use logged_stream::LowercaseHexadecimalFormatter;
use std::io::Read;
use std::io::Write;
use std::net::TcpStream;

fn main() {
    env_logger::builder()
        .filter_level(LevelFilter::Debug)
        .format_timestamp_millis()
        .init();

    // Assumes an echo server is listening on 127.0.0.1:8080 (the full example starts one).
    let mut client = LoggedStream::new(
        TcpStream::connect("127.0.0.1:8080").unwrap(),
        LowercaseHexadecimalFormatter::new_default(),
        DefaultFilter,
        ConsoleLogger::new_unchecked("debug"),
    );

    let send = [0x01, 0x02, 0x03, 0x04];
    client.write_all(&send).unwrap();
    let mut response = [0u8; 4];
    client.read_exact(&mut response).unwrap();

    let send = [0x05, 0x06, 0x07, 0x08];
    client.write_all(&send).unwrap();
    let mut response = [0u8; 4];
    client.read_exact(&mut response).unwrap();
}
```

Output to console:

```log
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] > 01:02:03:04
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] < 01:02:03:04
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] > 05:06:07:08
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] < 05:06:07:08
[2026-01-01T12:00:00.000Z DEBUG logged_stream::logger] x Deallocated.
```

The snippets above are condensed. Full runnable versions live in the [`examples/`](./examples) directory:

-   [`tcp-stream-console-logger.rs`]./examples/tcp-stream-console-logger.rs — the synchronous example above, together with the echo server.
-   [`tokio-tcp-stream-console-logger.rs`]./examples/tokio-tcp-stream-console-logger.rs — the same over `tokio`'s asynchronous API.
-   [`file-logger.rs`]./examples/file-logger.rs — writing records to a file instead of the console.
-   [`composite-filters.rs`]./examples/composite-filters.rs — combining filters with `AllFilter` / `AnyFilter`.

Full API documentation is on [docs.rs](https://docs.rs/logged-stream).

## Record kinds

Every log line begins with a single character identifying the kind of event:

| Char | Kind | Emitted when |
| --- | --- | --- |
| `<` | Read | bytes were read from the wrapped stream |
| `>` | Write | bytes were written to the wrapped stream |
| `!` | Error | a real IO error occurred (transient `WouldBlock` / `WriteZero` are skipped) |
| `-` | Shutdown | an asynchronous stream was shut down (`poll_shutdown`) |
| `x` | Drop | the wrapper was dropped (message `Deallocated.`) |
| `+` | Open | a manual marker you emit yourself with `log_open` (e.g. connection start); never produced automatically |

Every kind except `Open` is produced automatically. `Open` is a marker you record yourself with `LoggedStream::log_open` — for example `stream.log_open(format!("Established connection with {peer}"))` right after a connection is established. Like any record, it passes through the filter, so a `RecordKindFilter` that omits `Open` will suppress it.

## Architecture

`LoggedStream<S, Formatter, Filter, L>` is generic over four independent, pluggable parts. Each logged event flows through them in order:

```text
event  ->  Formatter  ->  Filter  ->  Logger
```

-   **The inner IO object (`S`).** The stream you are wrapping. `LoggedStream` implements the same IO trait `S` does, so it slots in wherever `S` was used.
-   **Formatter (`BufferFormatter`).** Turns the read and written byte buffers into the display strings you see in the log.
-   **Filter (`RecordFilter`).** Decides which records are logged. It runs on _every_ record kind, including `Shutdown` and `Drop`.
-   **Logger (`Logger`).** The sink that consumes accepted records.

All three of `BufferFormatter`, `RecordFilter` and `Logger` are public, `Send + 'static` and object-safe, with blanket implementations for `Box<...>` (and `Arc<T>` where `T: Sync` for `BufferFormatter`). You are free to supply your own implementation of any part and select one at runtime as a trait object.

## Provided implementations

### Formatters (`BufferFormatter`)

Control how byte buffers are rendered. Each formatter stores a separator (default `:`) and exposes parallel constructors: `new`, `new_static`, `new_owned` and `new_default`.

| Formatter | Renders each byte as |
| --- | --- |
| `LowercaseHexadecimalFormatter` | lowercase hexadecimal — `0a:ff` |
| `UppercaseHexadecimalFormatter` | uppercase hexadecimal — `0A:FF` |
| `DecimalFormatter` | decimal — `10:255` |
| `OctalFormatter` | octal — `012:377` |
| `BinaryFormatter` | binary — `00001010:11111111` |

### Filters (`RecordFilter`)

Decide which records reach the logger.

| Filter | Behavior |
| --- | --- |
| `DefaultFilter` | Accepts every record. |
| `RecordKindFilter` | Accepts only the record kinds in an allow-list given at construction. |
| `AllFilter` | AND — a record passes only if every child filter accepts it (an empty list accepts everything). |
| `AnyFilter` | OR — a record passes if any child filter accepts it (an empty list rejects everything). |

### Loggers (`Logger`)

Consume each accepted record.

| Logger | Destination |
| --- | --- |
| `ConsoleLogger` | Emits records through the `log` facade. |
| `FileLogger` | Writes records to a file. |
| `MemoryStorageLogger` | Retains recent records in a bounded in-memory buffer. |
| `ChannelLogger` | Sends records over an `mpsc` channel for handling elsewhere. |

## Use cases

`LoggedStream` hands you a formatted, filterable, timestamped record of every read, write, error, shutdown, and drop that crosses a stream — without changing the code on either side of it. That makes it a building block for a range of tools and diagnostics. A few examples of what you can build:

-   **Network traffic monitoring** — wrap a `TcpStream` (synchronous or asynchronous) to record every byte a client and server exchange, for debugging protocols, tracking data flow, or auditing what actually crossed the wire.
-   **I/O debugging** — see the exact bytes moving through a file, socket, or any custom stream, alongside the errors, shutdowns, and drops around them, to pin down data corruption or where a connection unexpectedly closed.
-   **Protocol and application-traffic capture** — log the raw wire traffic of a protocol you are implementing or reverse-engineering, such as a database driver, an RPC channel, or a custom binary format. `LoggedStream` captures the bytes; decode or parse them downstream if you need higher-level detail like SQL statements.
-   **Timing and throughput diagnostics** — every record is timestamped, so you can layer your own cadence, gap, and throughput analysis on top: when reads and writes happened and how much data moved. (The timestamps are yours to correlate — the library does not measure per-call latency itself.)
-   **Transparent proxies and audit trails** — build a man-in-the-middle proxy or an audit log that relays traffic unchanged while recording it. The author's [`logged_tcp_proxy`]https://github.com/qwerty541/logged-tcp-proxy does exactly this: it wraps each `TcpStream`, splits it into read/write halves, and prints every connection's payload. Route the records to a file, memory, or a channel through the pluggable loggers to keep a durable trail.

## License

Licensed under either of

-   Apache License, Version 2.0, ([LICENSE-APACHE]LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
-   MIT license ([LICENSE-MIT]LICENSE-MIT or https://opensource.org/licenses/MIT)

at your option.

## Contribution

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.