Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2025-03-20

### Added

- Initial release of the logging framework
- `LoggerContract` abstract base class defining the logger interface
- `LoguruLogger` implementation using Loguru library
- `LogLevel` enum for standardized log levels
- `LoggableMixin` for adding logging capabilities to classes
- Comprehensive test suite for all components
- Support for structured logging with context information
- Exception logging with type and message extraction
139 changes: 134 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@

## Overview

Python logger
This project provides a flexible and extensible logging framework for Python
applications. It offers a clean interface for consistent logging with rich
context information, built on top of the Loguru library while maintaining a
contract-based approach for easy adaptation to other logging backends.

Key features:

- πŸ“ Standardized logging interface through a contract
- πŸ”„ Easy integration into existing classes via a mixin
- 🌈 Beautiful, colorized console output via Loguru
- 🧩 Structured logging with context support
- πŸ“Š Granular log level control
- πŸ§ͺ Thoroughly tested implementation

## Setup and installation

Expand All @@ -24,18 +36,135 @@ Python logger

### Installation

1. Clone the repository

2. Install dependencies
```sh
make init
```

## Usage

### Basic Usage

```python
# Create a logger instance
logger = LoguruLogger(level=LogLevel.DEBUG)

# Basic logging
logger.info("Application started")

# Logging with context
logger.debug("Processing item",
context={"item_id": "12345", "status": "pending"})

# Error logging
try:
result = 1 / 0
except Exception as e:
logger.exception("Division error occurred", exc=e,
context={"operation": "division"})
```

### Using the LoggableMixin

```python
# Create a class with logging capabilities
class MyService(LoggableMixin):
def __init__(self, logger: LoggerContract):
super().__init__()
self.logger = logger

def process(self, data):
self.logger.info("Processing data", context={"data_size": len(data)})


# Usage
logger = LoguruLogger(level=LogLevel.INFO)
service = MyService(logger)
service.process([1, 2, 3])
```

### Log Levels

Available log levels (from lowest to highest priority):

- `LogLevel.DEBUG` - Detailed information for debugging
- `LogLevel.INFO` - General information about system operation
- `LogLevel.WARNING` - Indication of potential issues
- `LogLevel.ERROR` - Error conditions preventing a function from working
- `LogLevel.CRITICAL` - Critical conditions requiring immediate attention

Set the log level when creating the logger:

```python
# Only show warnings and above
logger = LoguruLogger(level=LogLevel.WARNING)
```

## API Reference

### LoggerContract

The abstract base class that defines the interface for all logger
implementations:

| Method | Description |
|-----------------------------------------|-------------------------------------------------|
| `debug(message, context=None)` | Log debug message with optional context |
| `info(message, context=None)` | Log info message with optional context |
| `warning(message, context=None)` | Log warning message with optional context |
| `error(message, context=None)` | Log error message with optional context |
| `critical(message, context=None)` | Log critical message with optional context |
| `exception(message, exc, context=None)` | Log exception with message and optional context |

### LoguruLogger

An implementation of `LoggerContract` using
the [Loguru](https://github.com/Delgan/loguru) library.

### LoggableMixin

A mixin class that adds logging capabilities to any class.

| Property | Description |
|----------|--------------------------------|
| `logger` | Get or set the logger instance |

### LogLevel

An enum representing log levels:

- `DEBUG`
- `INFO`
- `WARNING`
- `ERROR`
- `CRITICAL`

With a helper method:

- `from_str(value)` - Create LogLevel from a string (case-insensitive)

## Development

### Code Formatting and Linting

### Environment Variables
This project uses ruff for formatting and linting:

```sh
# Format code
make format

# Run linters
make lint
```

| Variable | Description | Required | Default Value | Possible Values |
|----------|-------------|----------|---------------|-----------------|
### Running Tests

### Architecture
```sh
# Run tests with coverage
make test
```

## Contributing

Expand Down
3 changes: 3 additions & 0 deletions docs/api/loggable_mixin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# LoggableMixin

::: src.logger.loggable_mixin.LoggableMixin
3 changes: 3 additions & 0 deletions docs/api/loglevel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# LogLevel

::: src.logger.loglevel.LogLevel
4 changes: 4 additions & 0 deletions docs/api/loguru_logger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# LoguruLogger

::: src.logger.loguru.LoguruLogger

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"mkdocstrings-python~=1.16.6",
"mkdocs-material~=9.6.9",
"griffe-inherited-docstrings~=1.1.1",
"loguru~=0.7.3",
]
readme = "docs/README.md"
requires-python = ">= 3.12"
Expand Down
2 changes: 2 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ jinja2==3.1.6
# via mkdocs
# via mkdocs-material
# via mkdocstrings
loguru==0.7.3
# via logger
markdown==3.7
# via mkdocs
# via mkdocs-autorefs
Expand Down
2 changes: 2 additions & 0 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jinja2==3.1.6
# via mkdocs
# via mkdocs-material
# via mkdocstrings
loguru==0.7.3
# via logger
markdown==3.7
# via mkdocs
# via mkdocs-autorefs
Expand Down
14 changes: 13 additions & 1 deletion src/logger/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
"""Package init file."""
"""Logger package for standardized logging across projects."""

from .contract import LoggerContract
from .loggable_mixin import LoggableMixin
from .loglevel import LogLevel
from .loguru import LoguruLogger

__all__ = [
"LogLevel",
"LoggableMixin",
"LoggerContract",
"LoguruLogger",
]
1 change: 0 additions & 1 deletion src/logger/__main__.py

This file was deleted.

73 changes: 73 additions & 0 deletions src/logger/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from abc import ABC, abstractmethod
from collections.abc import Mapping
from typing import Any


class LoggerContract(ABC):
"""Abstract base class for logger implementations at various severity levels, with optional context information.""" # noqa: E501

@abstractmethod
def debug(self, message: str, context: Mapping[str, Any] | None = None) -> None:
"""Log a debug message.

Args:
message: The message to log
context: Additional contextual information to include with the log.
"""
raise NotImplementedError

@abstractmethod
def info(self, message: str, context: Mapping[str, Any] | None = None) -> None:
"""Log an info message.

Args:
message: The message to log
context: Additional contextual information to include with the log.
"""
raise NotImplementedError

@abstractmethod
def warning(self, message: str, context: Mapping[str, Any] | None = None) -> None:
"""Log a warning message.

Args:
message: The message to log
context: Additional contextual information to include with the log.
"""
raise NotImplementedError

@abstractmethod
def error(self, message: str, context: Mapping[str, Any] | None = None) -> None:
"""Log an error message.

Args:
message: The message to log
context: Additional contextual information to include with the log.
"""
raise NotImplementedError

@abstractmethod
def critical(self, message: str, context: Mapping[str, Any] | None = None) -> None:
"""Log a critical message.

Args:
message: The message to log
context: Additional contextual information to include with the log.
"""
raise NotImplementedError

@abstractmethod
def exception(
self,
message: str,
exc: Exception,
context: Mapping[str, Any] | None = None,
) -> None:
"""Log an exception.

Args:
message: A descriptive message about the exception
exc: The exception object
context: Additional contextual information to include with the log.
"""
raise NotImplementedError
32 changes: 32 additions & 0 deletions src/logger/loggable_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from .contract import LoggerContract


class LoggableMixin:
"""Mixin to add logging capabilities to a class."""

def __init__(self) -> None:
"""Initialize the mixin."""
self._logger: LoggerContract | None = None

@property
def logger(self) -> LoggerContract:
"""Get the logger instance.

Returns:
The configured logger instance.

Raises:
RuntimeError: If logger has not been set.
"""
if self._logger is None:
raise RuntimeError("Logger not set")
return self._logger

@logger.setter
def logger(self, logger: LoggerContract) -> None:
"""Set a logger.

Args:
logger: Logger instance to use.
"""
self._logger = logger
34 changes: 34 additions & 0 deletions src/logger/loglevel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
from enum import IntEnum
from typing import Self


class LogLevel(IntEnum):
"""Represents the different log levels."""

DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL

@classmethod
def from_str(cls, value: str) -> Self:
"""Create a LogLevel from a string, case-insensitive.

Args:
value: String representation of the log level.

Returns:
LogLevel enum value.

Raises:
ValueError: If the string doesn't match any log level.
"""
try:
return cls[value.upper()]
except KeyError as e:
valid_values = [e.name for e in cls]
raise ValueError(
f"Invalid log level '{value}'. Must be one of: {valid_values}",
) from e
Loading