Metadata-Version: 2.4
Name: dynalog
Version: 0.1.0
Requires-Dist: lsb32>=0.2,<0.3
Requires-Dist: pytest>=8 ; extra == 'test'
Provides-Extra: test
License-File: LICENSE-LGPL-3.0
License-File: LICENSE-GPL-3.0
Summary: Structured logging for Python applications
Keywords: logging,structured-logging,logger,observability,logfmt,json,wasm
License-Expression: LGPL-3.0-only OR GPL-3.0-only
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# dynalog for Python

[![PyPI](https://img.shields.io/pypi/v/dynalog.svg)](https://pypi.org/project/dynalog/)
[![Python](https://img.shields.io/pypi/pyversions/dynalog.svg)](https://pypi.org/project/dynalog/)
[![License](https://img.shields.io/badge/license-LGPL--3.0%20OR%20GPL--3.0-blue.svg)](#license)

`dynalog` is a structured logging library for Python. It provides colored
console logs, named events, contextual fields, JSON and logfmt output,
filtering, redaction, enrichment, custom highlights, and test-friendly clocks
through one default `dynalog` instance.

```text
[D7MFXJ01RE3W4 info] Server started source=<unknown>:0 module=<unknown> port=3000 service=api
```

Console records use compact, sortable LSB32 timestamps. File and memory
records retain the original nanosecond timestamp and contain no ANSI color
sequences.

## Features

* **Pythonic**: Module-level logging functions and configurable `Dynalog` instances.
* **Structured**: Immutable events with named JSON-compatible payloads.
* **Contextual**: Shared fields with per-call overrides.
* **Readable**: Level colors, semantic indicators, and custom highlights.
* **Flexible**: Plain text, JSON, and compact logfmt output.
* **Composable**: Console, file, memory, fan-out, and custom destinations.
* **Controlled**: Minimum-level filtering, recursive redaction, and enrichment.
* **Precise**: Nanosecond event timestamps and 13-character LSB32 display IDs.
* **Testable**: Injectable clocks and in-memory capture.

## Installation

```console
pip install dynalog
```

Dynalog requires Python 3.10 or newer.

## Quick Start

```python
import dynalog
from dynalog import Level

dynalog.setup(
    level=Level.INFO,
    context={"service": "api"},
)

dynalog.info("Server started", context={"port": 3000})
dynalog.event("user.login", {"id": 42, "username": "Alice"})
```

The default destination is `Console`. Call `setup` once during application
startup to select a minimum level, output format, shared context, destinations,
or custom clock.

## Logging Levels

```python
dynalog.trace("Resolving configuration")
dynalog.debug("Cache connected")
dynalog.info("Server started")
dynalog.warn("Queue depth is elevated", context={"depth": 82})
dynalog.error("Request failed", context={"status": 503})
dynalog.fatal("Worker stopped")
```

Set a minimum level to discard lower-priority records:

```python
from dynalog import Level

dynalog.setup(level=Level.WARN)
```

Use `allows` before computing expensive diagnostic fields:

```python
if dynalog.allows(Level.DEBUG):
    dynalog.debug("Cache snapshot", context=collect_cache_details())
```

## Structured Events

Use named events for records intended for search, analytics, metrics, alerts,
or automated processing.

```python
dynalog.event(
    "payment.completed",
    {
        "order": "ORD-1042",
        "amount": 129.95,
        "currency": "USD",
    },
)
```

Messages and structured events use the same timestamp, filtering, context,
formatting, and destination configuration.

## Events and Sources

Construct an event directly when a caller needs explicit timestamps, context,
or source information:

```python
from dynalog import Event, Level, Source

event = Event.message(
    "Server started",
    timestamp=1_767_225_600_123_456_789,
    level=Level.INFO,
    context={"port": 3000},
    source=Source("app.py", 12, "app"),
)

dynalog.write(event)
```

`Event` values are immutable. Transforming an event returns a replacement
instead of changing the original value.

## Context

Shared context is merged into every new log record. Per-call context overrides
matching shared values without changing later events.

```python
dynalog.setup(
    context={"service": "checkout", "region": "west"},
)

dynalog.info(
    "Order accepted",
    context={"request": "req-42", "region": "central"},
)
```

Context values can be strings, integers, floats, booleans, or nested mappings.

## Output Formats

| Format | Use case |
| --- | --- |
| `Output.PLAIN` | Human-readable terminal output |
| `Output.JSON` | Files, collectors, and structured ingestion |
| `Output.COMPACT` | Dense single-line logfmt output |

```python
from dynalog import Output

dynalog.setup(output=Output.JSON)
```

Format an event without writing it to a destination:

```python
from dynalog import Event, Level

event = Event.message(
    "Cache connected",
    level=Level.INFO,
    context={"host": "cache.internal"},
)

plain = dynalog.text(event, "plain")
json = dynalog.text(event, "json")
compact = dynalog.text(event, "compact")
```

`format` returns `bytes`; `text` returns a decoded string.

## Destinations

Send the same record to multiple destinations by listing them in `sinks`.
Every destination is attempted, and failures are reported together after
dispatch.

```python
import dynalog
from dynalog import Console, File, Memory, Output

memory = Memory()

dynalog.setup(
    output=Output.JSON,
    sinks=[Console(), File("app.jsonl"), memory],
)

dynalog.info("Ready", context={"port": 3000})

assert len(memory.events) == 1
assert len(memory.records) == 1
```

`Memory.clear()` removes captured events and records.

### Custom Destinations

A custom destination implements `write(record, event)`:

```python
from dynalog import Event

class Queue:
    def __init__(self) -> None:
        self.records: list[tuple[bytes, Event]] = []

    def write(self, record: bytes, event: Event) -> None:
        self.records.append((bytes(record), event))

dynalog.setup(sinks=[Queue()])
```

## Redaction and Enrichment

Redaction returns a replacement event and leaves the original unchanged.
Matching keys are masked recursively.

```python
from dynalog import Event, Level

event = Event.message(
    "Authentication failed",
    timestamp=1_767_225_600_123_456_789,
    level=Level.ERROR,
    context={
        "user": "alice",
        "token": "secret",
        "credentials": {"password": "secret"},
    },
)

safe = dynalog.redact(event, ["password", "token"])
enriched = dynalog.enrich(safe, "api-01")
dynalog.write(enriched)
```

The default redaction keys are `password` and `token`. When no process ID is
provided, `enrich` uses the current Python process.

## Console Highlights

Built-in rules emphasize failures, warnings, HTTP status classes, booleans,
identifiers, timing values, paths, hosts, and ports. Add literal or compiled
regular expression rules for application-specific indicators.

```python
import re
import dynalog
from dynalog import Console, Tone

console = (
    Console(color=True)
    .highlight(re.compile(r"\bAUTH-\d+\b"), Tone.MAGENTA)
    .highlight("payment declined", Tone.RED)
)

dynalog.setup(sinks=[console])
dynalog.error("Payment AUTH-42 failed", context={"status": 503})
```

Available tones are `RED`, `YELLOW`, `GREEN`, `CYAN`, `MAGENTA`, `BLUE`,
`WHITE`, and `GRAY`. The first custom rule wins when matches overlap.

Color is selected automatically for a compatible terminal and disabled when
`NO_COLOR` is present. Pass `True` or `False` to `Console` to override automatic
detection.

## Testable Time

Provide a clock function that returns nanoseconds as an integer:

```python
import dynalog
from dynalog import Memory

memory = Memory()
now = 1_000_000_000

def clock() -> int:
    return now

dynalog.setup(sinks=[memory], clock=clock)
dynalog.info("first")
now += 1_000_000_000
dynalog.info("second")

delta = memory.events[1].timestamp - memory.events[0].timestamp
assert delta == 1_000_000_000
```

The default clock is `time.time_ns`.

## Public API

| Export | Purpose |
| --- | --- |
| `dynalog` | Module-level default logger functions |
| `Dynalog` | Independent logger instance |
| `Event` | Immutable message or structured event |
| `Source` | File, line, and module information |
| `Level` | Six ordered logging levels |
| `Output` | Plain, JSON, or compact formatting |
| `Tone` | Named console highlight color |
| `Console` | Colored terminal destination |
| `File` | Append-only file destination |
| `Memory` | In-memory events and records |
| `Sink` | Custom destination protocol |

Module-level functions include `setup`, `format`, `text`, `allows`, `redact`,
`enrich`, `write`, `event`, `trace`, `debug`, `info`, `warn`, `error`, `fatal`,
and `version`.

## Requirements

* Python 3.10 or newer.
* A wheel compatible with the target Python version and platform, or a Rust toolchain for source builds.
* A supported terminal for automatic ANSI colors.

## Publishing

Upload verified distributions to PyPI:

```console
python -m build
python -m twine check dist/*
python -m twine upload dist/*
```

PyPI authentication can use an API token through `TWINE_USERNAME=__token__`
and `TWINE_PASSWORD`.

The distribution includes the Python API, native extension, runtime artifact,
documentation, and both license texts.

## Limitations and Compatibility

* High-level log methods use unknown source fields unless a source is supplied explicitly.
* File output is append-only and does not currently rotate.
* Destination writes are synchronous; custom destinations should remain fast.
* Nanosecond timestamps do not guarantee uniqueness when events share an instant.
* LSB32 display IDs require every producer and consumer to use the same epoch.
* Building from source requires a supported Rust toolchain and native compiler.

## License

Dynalog is dual-licensed under either of the following, at your option:

* [GNU Lesser General Public License v3.0 only](LICENSE-LGPL-3.0)
* [GNU General Public License v3.0 only](LICENSE-GPL-3.0)

SPDX: `LGPL-3.0-only OR GPL-3.0-only`

