Metadata-Version: 2.4
Name: nnlogging2
Version: 0.2.0
Summary: `nnlogging2` is a non-invasive extension of the standard Python logging library. `nnlogging2` is designed to track numerical metrics by time and execution context.
Keywords: experiment,logging,machine learning
Author: amas127
Author-email: amas127 <amadeusxu127@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Dist: numpy>=2
Requires-Dist: typing-extensions>=4.15.0
Requires-Dist: pyrefly>=1.1.1 ; extra == 'dev'
Requires-Dist: pytest>=9.0 ; extra == 'dev'
Requires-Dist: pytest-cov>=7.0 ; extra == 'dev'
Requires-Dist: line-profiler>=5.0 ; extra == 'perf'
Requires-Dist: py-spy>=0.4 ; extra == 'perf'
Requires-Python: >=3.12, <=3.15
Project-URL: Issues, https://codeberg.org/amas127/nnlogging2/issues
Project-URL: Repository, https://codeberg.org/amas127/nnlogging2.git
Provides-Extra: dev
Provides-Extra: perf
Description-Content-Type: text/markdown

# nnlogging2

[![status-badge](https://ci.codeberg.org/api/badges/16633/status.svg)](https://ci.codeberg.org/repos/16633)
[![PyPI - Version](https://img.shields.io/pypi/v/nnlogging2)](https://pypi.org/project/nnlogging2/)
![PyPI - Downloads](https://img.shields.io/pypi/dm/nnlogging2)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/nnlogging2)

`nnlogging2` is a non-invasive extension of the standard Python logging library.
`nnlogging2` is designed to track numerical metrics by time and execution context.

- **Metric Tracking:** Fast automatic metric statistics tracking.
- **Extensibility:** Fork metric, logger, handler or formatter at any time.
- **Compatibility:** Compatible with standard python logging library and third-party
  modules.

## Installation

```sh
pip install nnlogging2
```

---

## Usage

For open-box usage, `nnlogging2` provides:

- `MetricLogger` (subclass of `logging.Logger`);
- `ContextedMetricLogger` (subclass of `logging.LoggerAdapter`);
- `AttyStreamHandler` (subclass of `logging.StreamHandler`);
- `ContextedMetricFormatter` (subclass of `logging.Formatter`).

A quick preview script can be found in the `Preview Script` subsection.

### `FallbackProtectorMeta`

Despite designed classes mostly inherited from standard logging classes, they also
chained with a custom metaclass `FallbackProtectorMeta`. `FallbackProtectorMeta` is
designed to turn the default class attribute overwriting behavior into modify the
default value (fallback).

An escaping window has been left for future usage:

```python
FPM = FallbackProtectorMeta


class Cls(metaclass=FPM):
    attr = Fallback(1)


assert Cls.attr == 1
assert isinstance(Cls.__dict__["attr"], Fallback)

Cls.attr = 2

assert Cls.attr == 2
assert isinstance(Cls.__dict__["attr"], Fallback)  # `FPM` prevents overwriting

del Cls.attr  # delete class attribute to fully remove `Fallback` instance
Cls.attr = 3

assert Cls.attr == 3
assert isinstance(Cls.__dict__["attr"], int)
```

`FallbackProtectorMeta` allows different instances shares attribute with the same name
but not necessarily same value. This is typical when one want different formatters
responding to different record keys, or different loggers can be manipulated in
different modules without affecting each other.

### `MetricLogger`

Attributes:

- `extra_key` (fallbackable): Key to be injected into logging record.
- `metric_factory` (fallbackable): Factory used when tracer created.
- `metric_attrs` (fallbackable): Attributes this logger tracing.
- `tracer`: Tracer to trace metrics.

Note:

- Although `metric_attrs` is default as `("val",)`, `nnlogging2` provides default metric
  class with the following attributes: `median`, `mean`, `max`, `lastval`, and `val`.
  For further extension, `val` is the only attribute you need to reimplement.
- `metric_attrs` is encouraged to record necessary attributes cause the access
  implementation is currenly running in loops and the time cost will grow linearily.
- `tracer` should be replaced manually, and the data recorded will not automatically
  preserved.

### `ContextedMetricLogger`

`ContextedMetricLogger` is designed to inherit `logging.LoggerAdapter` since contexts
are not necessary when tracing metrics and can be replaced by custom formatters. Despite
many reasons, we still provide this class to make open-box usage happy.

Attributes:

- `extra_key` (fallbackable): Key to be injected into logging record.
- `context`: the contexts to be injected into logging record.
- `cover`: whether to cover contexts existed or not.

### `AttyStreamHandler`

Because ANSI escaped code print nothing but hold length size when formatting, please
increase 9 character length, e.g. '%(levelname)-8s' -> '%(levelname)-17s', when you know
ANSI escaped codes will work.

Attributes:

- `colored_level`: Whether to show a colored level or not.

### `ContextedMetricFormatter`

This formatter extracts context and metric data from the `LogRecord`, formats them
according to the specified format strings, and temporarily injects them into the record
(default as `context_ext` and `metrics_ext` attributes) so they can be used in the main
log message format string.

Attributes:

- `context_fmtstr` (fallbackable): Context formatting string.
- `context_delim` (fallbackable): Context delimiter.
- `context_key` (fallbackable): Formatted context string entry.
- `metrics_fmtstr` (fallbackable): Metrics formatting string.
- `metrics_delim` (fallbackable): Metrics delimiter.
- `metrics_key` (fallbackable): Formatted metrics string entry.

---

## Preview Script

```python
from nnlogging2 import get_metric_logger, ContextedMetricFormatter, AttyStreamHandler
from nnlogging2 import helpers as hp

# 1. Initialize the promoted logger
logger = get_metric_logger("train")
logger.metric_attrs = ("lastval", "mean", "median")
logger.setLevel(hp.level_to_int("INFO"))

# 2. Setup formatting for context and metrics
# Inject "%(context_ext)s" and "%(metrics_ext)s" to formatter
# (8 showing characters, 9 ANSI escaped codes)
formatter = ContextedMetricFormatter(
    fmt="%(levelname)-17s %(context_ext)s %(message)s%(metrics_ext)s"
)
handler = AttyStreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)

# 3. Attach context to logger (optional)
ctx_logger = logger.contexted({"epoch": "E1/1"})

# 4. Standard logging
ctx_logger.debug("debug ...")
ctx_logger.info("info ...")
ctx_logger.warning("warning ...")
ctx_logger.error("error ...")
ctx_logger.critical("critical ...")

# 5. Update & Logging
for i in range(3):
    # Update numerical values (automatically tracked in a moving window)
    ctx_logger.update(loss=0.5 / (i + 1), accuracy=0.8 + (i * 0.05))
    ctx_logger.log_metric(
        hp.level_to_int("INFO"),
        metrics=[
            ("loss", "[%(metricname)s: %(lastval).2f (%(mean).2f)]"),
            ("accuracy", "[%(metricname)s: %(median).2f]"),
        ],
    )
```

## Preview

![assets/preview.png](assets/preview.png)

---

## License

This project is licensed under the MIT License [LICENSE](LICENSE).
