Metadata-Version: 2.4
Name: fluentlog
Version: 0.1.1
Summary: Opinionated structured logging library for Python with a fluent interface
License: Apache-2.0
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: orjson>=3.11.7
Dynamic: license-file

# Fluentlog

Opinionated structured logging for Python with a fluent API.

- API inspired by zerolog
- JSON output format
- OpenTelemetry naming conventions when relevant
- Near zero-cost for disabled log levels

## Installation

```bash
pip install fluentlog
```

## Getting Started

### Simple example

```python
import fluentlog

log = fluentlog.Logger().bind().int("request_id", 1).logger()

log.info().str("user", "jmcs").int("uid", 42).msg("user logged in")
# {"level":"INFO","request_id":1,"user":"jmcs","uid":42,"message":"user logged in"}

# Disabled levels have near-zero overhead
log.debug().func( expensive_func).msg("debug info")  # expensive_func is never called
```

### Log Levels

fluentlog supports the following log levels, from more to less critical:

- `FATAL`: Errors the application can't recover from
- `ERROR`: Errors that make the current context fail, but not the entire application
- `WARNING`: Recoverable errors
- `INFO`: Expected lifecycle events and relevant business signals
- `DEBUG`: Internal details useful while diagnosing behavior during development
- `TRACE`: Very fine-grained execution details, usually only useful for deep debugging

You can set the log level for your logger either in the constructor or using a fluent method:

```python
import fluentlog

log = fluentlog.Logger(level=fluentlog.Level.DEBUG)

# or 

log = fluentlog.Logger().set_level(fluentlog.Level.DEBUG)
```

### Logging context

```python
import fluentlog

def some_func():
    log = fluentlog.context()
    log.info().msg("From func")

def main():
    log = fluentlog.context().bind().str("context", "example").logger()
    some_func()
    # {"level":"INFO", "message": "From func"}
    with fluentlog.context_logger(log):
        some_func()
        # {"level":"INFO", "context": "example", "message": "From func"}

main()
```

## Performance

Benchmarks show ~2-3x faster than stdlib logging with formatted output, with greater advantages
when log levels are filtered.

## Design decisions

### Why use different methods for different types?
Using different methods for different types allows for optimising serialization strategies for
mutable and immutable types. For example, `dict()` and `list()` deep-copy their arguments to prevent
mutations after the event is logged from affecting the output, while `int()` and `str()` can safely
reference immutable values directly without copying.

### Why dummy events for disabled log levels?
Having dummy events achieves near-zero overhead, as we can avoid unnecessary processing without
having to check the log level everywhere.

### Why OpenTelemetry naming conventions?
I use OpenTelemetry for distributed tracing, and like consistent and precise naming, even when it
comes at the cost of verbosity.

### Why no formatted messages?
Formatted messages are familiar because that's how traditional logging usually works. But for
structured logs they are a trap, as important data gets buried in strings instead of proper fields,
which makes filtering and querying harder.

### Why context-based logger passing?
Preserving logging context across boundaries is essential in complex applications, but having
bound context inside a function is useful too. Context-based logger passing allows for both
options and keeps things purposeful while avoiding cluttering application APIs.
