Metadata-Version: 2.4
Name: pyqt-logging-manager
Version: 1.0.0
Summary: Reusable logging utilities for PyQt6 applications
Author-email: RogerGdot <rogergdot@gmail.com>
Project-URL: Repository, https://gitlab.dlr.de/fm/fks/thermoelektrik/python-lib/pyqt-logging-manager
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: pyqt6

# pyqt-logging-manager

Reusable logging utilities for PyQt6 applications, developed at DLR.

This package wraps the complete logging setup - file handlers, GUI handler, and history dialogs - in one `LoggingManager` class that can be reused in PyQt6 projects without project-specific changes.

---

## Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [LoggingManager API](#loggingmanager-api)
- [LogColumn](#logcolumn)
- [Additional Classes And Functions](#additional-classes-and-functions)
- [Examples](#examples)

---

## Features

- **`LoggingManager`**: initialize once and let the manager wire everything up.
- **RotatingFileHandler** for all file handlers, with configurable maximum file size and backup count.
- **Optional handlers** for main logs, error logs, and debug logs.
- **GUI integration** through `QTextEdit` with level-based color coding.
- **Log history dialogs** with filter buttons (All / Warnings + Errors / Errors Only).
- **Structured formatter configuration** through the `LogColumn` enum instead of raw format strings.
- **Thread-safe GUI logging**: `GuiLogger` forwards records from worker threads to the GUI thread through a Qt signal.
- **No project-specific code** and no external runtime dependency besides PyQt6.

---

## Requirements

- Python >= 3.13
- PyQt6

---

## Installation

### Editable Install From A Local Checkout

Use this during development so package changes are immediately visible in consuming projects.

```bash
# Clone the package from GitLab
git clone git@gitlab.dlr.de:fm/fks/thermoelektrik/python-lib/pyqt-logging-manager.git

# Install it into the target project after activating its virtual environment
pip install -e /path/to/pyqt-logging-manager
```

### Install Directly From GitLab

```bash
pip install git+ssh://git@gitlab.dlr.de/fm/fks/thermoelektrik/python-lib/pyqt-logging-manager.git
```

### Install A Specific Version Or Tag

```bash
pip install git+ssh://git@gitlab.dlr.de/fm/fks/thermoelektrik/python-lib/pyqt-logging-manager.git@v0.1.0
```

---

## Quick Start

### Minimal GUI-Only Usage

```python
from pyqt_logging_manager import LoggingManager

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        uic.loadUi("my_app.ui", self)

        self.log_manager = LoggingManager(system_name="MyTool")
        self.log_manager.connect_widget(self.plainLog)  # QTextEdit from the UI

        logger = self.log_manager.get_logger("MAIN")
        logger.info("Application started")
```

### Standard File Configuration

```python
from pyqt_logging_manager import LoggingManager

self.log_manager = LoggingManager(
    system_name="MyApplication",
    system_version="1.2.0",
    log_dir="logs/",
)
self.log_manager.connect_widget(self.plainLog)
```

Creates: `logs/MyApplication.log`

### Full Configuration

```python
from pyqt_logging_manager import LoggingManager, LogColumn
import logging

self.log_manager = LoggingManager(
    system_name="MyApplication",
    system_version="1.2.0",
    log_dir="logs/",
    level=logging.INFO,
    delete_old_logs=True,
    error_log=True,          # -> logs/MyApplication_Error.log
    debug=True,              # -> logs/MyApplication_Debug.log
    max_bytes=20_000_000,    # 20 MB per file
    backup_count=5,          # max. 5 backup files
    columns=[
        (LogColumn.TIMESTAMP, 32),
        (LogColumn.LEVEL,     10),
        (LogColumn.LOGGER,    16),
        (LogColumn.MESSAGE,    0),
    ],
    separator="\t",
)
self.log_manager.connect_widget(self.plainLog)

# Run custom logic for warnings, for example opening a dialog
self.log_manager.connect_extra_slot(self._on_warning)

logger = self.log_manager.get_logger("MAIN")
logger.info("Ready")
```

---

## LoggingManager API

### Constructor

```python
LoggingManager(
    system_name: str,
    system_version: str = "",
    log_dir: str | None = None,
    level: int = logging.INFO,
    delete_old_logs: bool = True,
    error_log: bool = False,
    debug: bool = False,
    max_bytes: int = 20_000_000,
    backup_count: int = 5,
    formatter_string: str | None = None,
    formatter_width: int | None = None,
    columns: list[tuple[LogColumn, int]] | None = None,
    separator: str = "\t",
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `system_name` | `str` | - | Application name used for file names and log headers. |
| `system_version` | `str` | `""` | Optional version shown in the log header. |
| `log_dir` | `str \| None` | `None` | Directory for log files. `None` means GUI-only logging. |
| `level` | `int` | `logging.INFO` | Minimum root logger level. |
| `delete_old_logs` | `bool` | `True` | Delete log files and backups at startup. |
| `error_log` | `bool` | `False` | Create a separate error FileHandler for `WARNING` and above. |
| `debug` | `bool` | `False` | Create a debug RotatingFileHandler for `DEBUG` and above. |
| `max_bytes` | `int` | `20_000_000` | Maximum file size in bytes for all file handlers. |
| `backup_count` | `int` | `5` | Maximum number of backup files per handler. |
| `formatter_string` | `str \| None` | `None` | Raw Python logging format string. Overrides `columns`. |
| `formatter_width` | `int \| None` | `128` | Automatic line wrap width. |
| `columns` | `list \| None` | `None` | Structured column definition; see `LogColumn`. |
| `separator` | `str` | `"\t"` | Separator between columns. |

**Formatter priority:** `formatter_string` > `columns` > default format.

### Methods

#### `connect_widget(widget: QTextEdit) -> None`

Connects a `QTextEdit` as GUI log output. In PyQt6 projects, call this after `loadUi()`.

```python
self.log_manager.connect_widget(self.plainLog)
```

#### `get_logger(name: str) -> logging.Logger`

Returns a named logger. All loggers automatically propagate to all registered handlers.

```python
logger = self.log_manager.get_logger("CALC_ENGINE")
logger.warning("Temperature outside the expected range")
```

#### `connect_extra_slot(slot) -> None`

Connects an additional slot to the `GuiLogger` signal. Call this after `connect_widget()`.

The slot must have the signature `(message: str, level: int, levelname: str)`.

```python
def _on_warning(self, _msg: str, level: int, _name: str) -> None:
    if level >= logging.WARNING:
        self.log_manager.show_errors(parent=self)

self.log_manager.connect_extra_slot(self._on_warning)
```

#### `show_history(parent: QWidget | None = None) -> None`

Opens the log history dialog with all records and filter buttons. If it is already open, it is brought to the foreground.

```python
self.log_manager.show_history(parent=self)
```

#### `show_errors(parent: QWidget | None = None) -> None`

Opens the error log dialog for warnings and errors only. If it is already open, it is brought to the foreground.

```python
self.log_manager.show_errors(parent=self)
```

### Attributes

| Attribute | Type | Description |
|---|---|---|
| `log_records` | `list[tuple[str, int, str]]` | All log records as `(message, level, levelname)`. |

---

## LogColumn

Instead of a raw format string, the formatter can be configured with the `LogColumn` enum and column widths.

```python
from pyqt_logging_manager import LogColumn

columns=[
    (LogColumn.TIMESTAMP, 32),   # Timestamp, 32 characters
    (LogColumn.LEVEL,     10),   # Log level, 10 characters
    (LogColumn.LOGGER,    16),   # Logger name, 16 characters
    (LogColumn.MESSAGE,    0),   # Message, no fixed width
]
```

**Width `0`** means no padding; the field is as wide as needed.

### Available Column Types

| `LogColumn.X` | Content | Example Value |
|---|---|---|
| `TIMESTAMP` | Date and time | `2026-05-05 14:32:01` |
| `LEVEL` | Log level | `INFO`, `WARNING`, `ERROR` |
| `LOGGER` | Logger name | `MAIN`, `CALC_ENGINE` |
| `MESSAGE` | Message | `Calculation finished` |
| `FILE` | Source file without path | `calc_engine.py` |
| `LINE` | Line number | `217` |
| `FUNCTION` | Function name | `run_segment` |

---

## Additional Classes And Functions

### `GuiLogger`

Qt-based `logging.Handler` that forwards records as `pyqtSignal(str, int, str)`. It is used internally by `LoggingManager`, but can also be used directly.

### `WrappingFormatter`

`logging.Formatter` with automatic line wrapping. Continuation lines are indented below the first message line.

```python
formatter = WrappingFormatter(
    fmt="%(asctime)s\t%(levelname)s\t%(message)s",
    width=100,
)
```

### `LogHistoryDialog`

`QDialog` for displaying and filtering log records. It supports two modes:

- `mode="all"`: all records with filter buttons (All / Warnings + Errors / Errors Only).
- `mode="errors"`: warnings and errors only, with a checkbox to hide warnings.

```python
dialog = LogHistoryDialog(self.log_manager.log_records, mode="all", parent=self)
dialog.show()
```

### `setup_exception_hook`

Installs a global handler for uncaught exceptions.

```python
from pyqt_logging_manager import setup_exception_hook

setup_exception_hook(
    logger=self.log_manager.get_logger("MAIN"),
    show_dialog=True,
    parent=self,
)
```

### `add_measurement_log_handler` / `remove_measurement_log_handler`

Adds a temporary FileHandler for a measurement and removes it afterwards.

```python
from pyqt_logging_manager import add_measurement_log_handler, remove_measurement_log_handler

# Start measurement
handler = add_measurement_log_handler(
    log_dir="measurements/2026-05-05/",
    measurement_name="Run_01",
)

# ... measurement is running ...

# End measurement
remove_measurement_log_handler(handler)
```

---

## Examples

### Small GUI Tool Without Files

```python
from pyqt_logging_manager import LoggingManager

self.log_manager = LoggingManager("MyTool")
self.log_manager.connect_widget(self.logWidget)
self.log_manager.get_logger("MAIN").info("Started")
```

### Application With All Handlers

```python
import logging
from pyqt_logging_manager import LoggingManager, LogColumn, setup_exception_hook

self.log_manager = LoggingManager(
    system_name="MySystem",
    system_version="2.0.0",
    log_dir="logs/",
    error_log=True,
    debug=True,
    columns=[
        (LogColumn.TIMESTAMP, 24),
        (LogColumn.LEVEL,      8),
        (LogColumn.LOGGER,    20),
        (LogColumn.MESSAGE,    0),
    ],
)
self.log_manager.connect_widget(self.plainLog)
self.log_manager.connect_extra_slot(self._auto_open_errors)

setup_exception_hook(
    logger=self.log_manager.get_logger("MAIN"),
    show_dialog=True,
    parent=self,
)
```

### Loggers In Other Modules

```python
# In calc_engine.py; no LoggingManager import required
import logging

class CalcEngine:
    def __init__(self):
        self.logger = logging.getLogger("CALC_ENGINE")

    def run(self):
        self.logger.info("Calculation starts")
        # ...
        self.logger.debug("Segment 5 finished")
```

All records automatically propagate to the root logger and therefore to all handlers managed by `LoggingManager`.

---

## License

Internal - DLR FM-KP
