logged-stream
Transparent logging wrapper for any synchronous or asynchronous Rust IO stream — inspect every byte, error, and lifecycle event that passes through.
- Description
- Usage
- Quick start
- Example
- Record kinds
- Architecture
- Provided implementations
- Use cases
- License
- Contribution
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::ioRead/Writestreams andtokioAsyncRead/AsyncWritestreams 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
unsafecode.
Each logged event flows through four pluggable parts — event -> Formatter -> Filter -> Logger — described under Architecture.
Usage
Add logged-stream to your Cargo.toml:
[]
= "0.7"
or run:
$ 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 (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:
use LevelFilter;
use ConsoleLogger;
use DefaultFilter;
use LoggedStream;
use LowercaseHexadecimalFormatter;
use Cursor;
use Read;
Running it logs the read, then the drop when stream goes out of scope:
[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_loggerandlogin yourCargo.toml:ConsoleLoggeronly forwards records to thelogfacade, and any backend works.
See Provided implementations for the full set of formatters, filters and loggers you can swap in.
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.
use LevelFilter;
use ConsoleLogger;
use DefaultFilter;
use LoggedStream;
use LowercaseHexadecimalFormatter;
use Read;
use Write;
use TcpStream;
Output to console:
[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/ directory:
tcp-stream-console-logger.rs— the synchronous example above, together with the echo server.tokio-tcp-stream-console-logger.rs— the same overtokio's asynchronous API.file-logger.rs— writing records to a file instead of the console.composite-filters.rs— combining filters withAllFilter/AnyFilter.
Full API documentation is on docs.rs.
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:
event -> Formatter -> Filter -> Logger
- The inner IO object (
S). The stream you are wrapping.LoggedStreamimplements the same IO traitSdoes, so it slots in whereverSwas 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, includingShutdownandDrop. - 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.
LoggedStreamcaptures 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_proxydoes exactly this: it wraps eachTcpStream, 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 or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (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.