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
39 changes: 34 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ name: Run Tests

on:
push:
branches:
- "**" # including all branches before excluding master
- "!master"
- "!main"
branches: ["**"]


jobs:
tests:
test:
name: Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -41,3 +39,34 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: ddc/pythonLogs

build:
name: Build Test Package
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install build dependencies only
run: poetry install --only main --no-interaction --no-ansi

- name: Build package
run: poetry build

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: python-packages
path: dist/
retention-days: 7
67 changes: 0 additions & 67 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,6 @@ on:


jobs:
test:
name: Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/')"
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install dependencies
run: poetry install --with test --no-interaction --no-ansi

- name: Run tests with coverage
run: poetry run poe tests

- name: Upload coverage reports to Codecov
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: ddc/pythonLogs

build:
name: Build Test Package
runs-on: ubuntu-latest
needs: test
if: "!startsWith(github.ref, 'refs/tags/')"
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install build dependencies only
run: poetry install --only main --no-interaction --no-ansi

- name: Build package
run: poetry build

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: python-packages
path: dist/
retention-days: 7

release:
name: Build and Release
runs-on: ubuntu-latest
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

[![Support me on GitHub](https://img.shields.io/badge/Support_me_on_GitHub-154c79?style=for-the-badge&logo=github)](https://github.com/sponsors/ddc)

A modern, high-performance Python logging library with automatic file rotation, context manager support, and memory optimization.
High-performance Python logging library with file rotation and optimized caching for better performance


## Table of Contents
Expand Down Expand Up @@ -384,25 +384,36 @@ error_logger.error("Database connection failed")
audit_logger.info("User admin logged in")
```

## Env Variables (Optional)
## Env Variables (Optional | Production)
.env variables can be used by leaving all options blank when calling the function
If not specified inside the .env file, it will use the dafault value
This is a good approach for production environments, since options can be changed easily
```python
from pythonLogs import timed_rotating_logger
log = timed_rotating_logger()
```

```
LOG_LEVEL=DEBUG
LOG_TIMEZONE=America/Chicago
LOG_TIMEZONE=UTC
LOG_ENCODING=UTF-8
LOG_APPNAME=app
LOG_FILENAME=app.log
LOG_DIRECTORY=/app/logs
LOG_DAYS_TO_KEEP=30
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
LOG_STREAM_HANDLER=True
LOG_SHOW_LOCATION=False
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
LOG_MAX_LOGGERS=50
LOG_LOGGER_TTL_SECONDS=1800

# SizeRotatingLog
LOG_MAX_FILE_SIZE_MB=10

# TimedRotatingLog
LOG_ROTATE_WHEN=midnight
LOG_ROTATE_AT_UTC=True
LOG_ROTATE_FILE_SUFIX="%Y%m%d"
```


Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pythonLogs"
version = "4.0.1"
description = "A modern, high-performance Python logging library with automatic file rotation, factory pattern for easy logger creation, and optimized caching for better performance."
version = "4.0.2"
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
license = "MIT"
readme = "README.md"
authors = ["Daniel Costa <danieldcsta@gmail.com>"]
Expand Down Expand Up @@ -34,6 +34,7 @@ classifiers = [

[tool.poetry.dependencies]
python = "^3.10"
pydantic = "^2.11.7"
pydantic-settings = "^2.10.1"
python-dotenv = "^1.1.1"

Expand All @@ -48,6 +49,12 @@ pytest = "^8.4.1"
[tool.poetry.group.test]
optional = true


[tool.black]
line-length = 120
skip-string-normalization = true


[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
Expand All @@ -60,6 +67,12 @@ omit = [
]


[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
]


[tool.poe.tasks]
_test = "coverage run -m pytest -v"
_coverage_report = "coverage report"
Expand Down
5 changes: 4 additions & 1 deletion pythonLogs/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ LOG_APPNAME=app
LOG_FILENAME=app.log
LOG_DIRECTORY=/app/logs
LOG_DAYS_TO_KEEP=30
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
LOG_STREAM_HANDLER=True
LOG_SHOW_LOCATION=False
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
LOG_MAX_LOGGERS=50
LOG_LOGGER_TTL_SECONDS=1800

# SizeRotatingLog
LOG_MAX_FILE_SIZE_MB=10

# TimedRotatingLog
LOG_ROTATE_WHEN=midnight
LOG_ROTATE_AT_UTC=True
LOG_ROTATE_FILE_SUFIX="%Y%m%d"
33 changes: 32 additions & 1 deletion pythonLogs/factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- encoding: utf-8 -*-
import atexit
import logging
import threading
import time
Expand Down Expand Up @@ -28,6 +29,23 @@ class LoggerFactory:
# Memory optimization settings
_max_loggers = 100 # Maximum number of cached loggers
_logger_ttl = 3600 # Logger TTL in seconds (1 hour)
_initialized = False # Flag to track if memory limits have been initialized
_atexit_registered = False # Flag to track if atexit cleanup is registered

@classmethod
def _ensure_initialized(cls) -> None:
"""Ensure memory limits are initialized from settings on first use."""
if not cls._initialized:
from pythonLogs.settings import get_log_settings
settings = get_log_settings()
cls._max_loggers = settings.max_loggers
cls._logger_ttl = settings.logger_ttl_seconds
cls._initialized = True

# Register atexit cleanup on first use
if not cls._atexit_registered:
atexit.register(cls._atexit_cleanup)
cls._atexit_registered = True

@classmethod
def get_or_create_logger(
Expand All @@ -54,6 +72,9 @@ def get_or_create_logger(

# Thread-safe check-and-create operation
with cls._registry_lock:
# Initialize memory limits from settings on first use
cls._ensure_initialized()

# Clean up expired loggers first
cls._cleanup_expired_loggers()

Expand Down Expand Up @@ -114,7 +135,7 @@ def _enforce_size_limit(cls) -> None:

@classmethod
def set_memory_limits(cls, max_loggers: int = 100, ttl_seconds: int = 3600) -> None:
"""Configure memory management limits for the logger registry.
"""Configure memory management limits for the logger registry at runtime.

Args:
max_loggers: Maximum number of cached loggers
Expand All @@ -123,10 +144,20 @@ def set_memory_limits(cls, max_loggers: int = 100, ttl_seconds: int = 3600) -> N
with cls._registry_lock:
cls._max_loggers = max_loggers
cls._logger_ttl = ttl_seconds
cls._initialized = True # Mark as manually configured
# Clean up immediately with new settings
cls._cleanup_expired_loggers()
cls._enforce_size_limit()

@classmethod
def _atexit_cleanup(cls) -> None:
"""Cleanup function registered with atexit to ensure proper resource cleanup."""
try:
cls.clear_registry()
except Exception:
# Silently ignore exceptions during shutdown cleanup
pass

@staticmethod
def _cleanup_logger(logger: logging.Logger) -> None:
"""Clean up logger resources by closing all handlers."""
Expand Down
11 changes: 7 additions & 4 deletions pythonLogs/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ class LogSettings(BaseSettings):
"""If any ENV variable is omitted, it falls back to default values here"""

level: Optional[LogLevel] = Field(default=LogLevel.INFO)
timezone: Optional[str] = Field(default=DEFAULT_TIMEZONE)
encoding: Optional[str] = Field(default=DEFAULT_ENCODING)
appname: Optional[str] = Field(default="app")
directory: Optional[str] = Field(default="/app/logs")
filename: Optional[str] = Field(default="app.log")
encoding: Optional[str] = Field(default=DEFAULT_ENCODING)
date_format: Optional[str] = Field(default=DEFAULT_DATE_FORMAT)
directory: Optional[str] = Field(default="/app/logs")
days_to_keep: Optional[int] = Field(default=DEFAULT_BACKUP_COUNT)
timezone: Optional[str] = Field(default=DEFAULT_TIMEZONE)
date_format: Optional[str] = Field(default=DEFAULT_DATE_FORMAT)
stream_handler: Optional[bool] = Field(default=True)
show_location: Optional[bool] = Field(default=False)
# Memory management
max_loggers: Optional[int] = Field(default=100)
logger_ttl_seconds: Optional[int] = Field(default=3600)

# SizeRotatingLog
max_file_size_mb: Optional[int] = Field(default=10)
Expand Down
Loading