Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.
/ ddcLogs Public archive
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: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ from ddcLogs import SizeRotatingLog
logger = SizeRotatingLog(
level="debug",
name="app",
directory="/.logs",
directory="/app/logs",
filenames=["main.log", "app1.log"],
maxmbytes=5,
daystokeep=7,
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="America/Chicago",
streamhandler=True, # Add stream handler along with file handler
showlocation=False # This will show the filename and the line number where the message originated
streamhandler=True,
showlocation=False
).init()
logger.warning("This is a warning example")
```
Expand All @@ -94,16 +94,16 @@ from ddcLogs import TimedRotatingLog
logger = TimedRotatingLog(
level="debug",
name="app",
directory="./logs",
directory="/app/logs",
filenames=["main.log", "app2.log"],
when="midnight",
sufix="%Y%m%d",
daystokeep=7,
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="UTC",
streamhandler=True, # Add stream handler along with file handler
showlocation=False # This will show the filename and the line number where the message originated
streamhandler=True,
showlocation=False
).init()
logger.warning("This is a warning example")
```
Expand Down
4 changes: 2 additions & 2 deletions ddcLogs/basic_log.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
import logging
from typing import Optional
from ddcLogs.log_utils import get_format, get_level, get_timezone
from ddcLogs.log_utils import get_format, get_level, get_timezone_function
from ddcLogs.settings import LogSettings


Expand All @@ -26,7 +26,7 @@ def __init__(
def init(self):
logger = logging.getLogger(self.appname)
logger.setLevel(self.level)
logging.Formatter.converter = get_timezone(self.timezone)
logging.Formatter.converter = get_timezone_function(self.timezone)
_format = get_format(self.showlocation, self.appname, self.timezone)
logging.basicConfig(datefmt=self.datefmt, encoding=self.encoding, format=_format)
return logger
68 changes: 34 additions & 34 deletions ddcLogs/log_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import logging.handlers
import os
import shutil
import sys
import time
from datetime import datetime, timedelta
from time import struct_time
Expand Down Expand Up @@ -37,7 +36,7 @@ def get_logger_and_formatter(

formatt = get_format(show_location, name, timezone)
formatter = logging.Formatter(formatt, datefmt=datefmt)
formatter.converter = get_timezone(timezone)
formatter.converter = get_timezone_function(timezone)
return logger, formatter


Expand All @@ -49,17 +48,18 @@ def check_filename_instance(filenames: list | tuple) -> None:


def check_directory_permissions(directory_path: str) -> None:
if os.path.isdir(directory_path) and not os.access(directory_path, os.R_OK | os.W_OK | os.X_OK):
write_stderr(f"Unable to access directory | {directory_path}")
raise OSError(errno.EACCES)
if os.path.isdir(directory_path) and not os.access(directory_path, os.W_OK | os.X_OK):
err_msg = f"Unable to access directory | {directory_path}"
write_stderr(err_msg)
raise PermissionError(err_msg)

try:
if not os.path.isdir(directory_path):
os.makedirs(directory_path, mode=0o755, exist_ok=True)
except OSError as e:
except PermissionError as e:
err_msg = f"Unable to create directory | {directory_path}"
write_stderr(f"{err_msg} | {repr(e)}")
raise e
raise PermissionError(err_msg)


def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
Expand All @@ -69,7 +69,7 @@ def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
if is_older_than_x_days(file, days_to_keep):
delete_file(file)
except Exception as e:
write_stderr(f"Unable to delete passed {days_to_keep} days logs | {file} | {repr(e)}")
write_stderr(f"Unable to delete {days_to_keep} days old logs | {file} | {repr(e)}")


def list_files(directory: str, ends_with: str) -> tuple:
Expand Down Expand Up @@ -146,19 +146,10 @@ def write_stderr(msg: str) -> None:
:return: None
"""

dt = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
sys.stderr.write(f"[{dt}]:[ERROR]:{msg}\n")
from .basic_log import BasicLog


def write_stdout(msg: str) -> None:
"""
Write msg to stdout
:param msg:
:return: None
"""

dt = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
sys.stdout.write(f"[{dt}]:[WARNING]:{msg}\n")
logger = BasicLog(level="INFO", name=__name__).init()
logger.error(msg)


def get_level(level: str) -> logging:
Expand Down Expand Up @@ -194,11 +185,18 @@ def get_log_path(directory: str, filename: str) -> str:
"""

log_file_path = str(os.path.join(directory, filename))
err_message = f"Unable to open log file for writing | {log_file_path}"

try:
open(log_file_path, "a+").close()
except IOError as e:
write_stderr(f"Unable to open log file for writing | {log_file_path} | {repr(e)}")
except PermissionError as e:
write_stderr(f"{err_message} | {repr(e)}")
raise PermissionError(err_message)
except FileNotFoundError as e:
write_stderr(f"{err_message} | {repr(e)}")
raise FileNotFoundError(err_message)
except OSError as e:
write_stderr(f"{err_message} | {repr(e)}")
raise e

return log_file_path
Expand All @@ -223,34 +221,36 @@ def get_format(show_location: bool, name: str, timezone: str) -> str:
return fmt


def gzip_file(source, output_partial_name) -> gzip:
def gzip_file_with_sufix(file_path, sufix) -> str | None:
"""
gzip file
:param source:
:param output_partial_name:
:return: gzip
:param file_path:
:param sufix:
:return: bool
"""

if os.path.isfile(source) and os.stat(source).st_size > 0:
sfname, sext = os.path.splitext(source)
renamed_dst = f"{sfname}_{output_partial_name}{sext}.gz"
if os.path.isfile(file_path):
sfname, sext = os.path.splitext(file_path)
renamed_dst = f"{sfname}_{sufix}{sext}.gz"

try:
with open(source, "rb") as fin:
with open(file_path, "rb") as fin:
with gzip.open(renamed_dst, "wb") as fout:
fout.writelines(fin)
except Exception as e:
write_stderr(f"Unable to zip log file | {source} | {repr(e)}")
write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
raise e

try:
delete_file(source)
delete_file(file_path)
except OSError as e:
write_stderr(f"Unable to delete_file old source log file | {source} | {repr(e)}")
write_stderr(f"Unable to delete source log file | {file_path} | {repr(e)}")
raise e

return renamed_dst


def get_timezone(
def get_timezone_function(
time_zone: str,
) -> Callable[[float | None, Any], struct_time] | Callable[[Any], struct_time]:

Expand Down
4 changes: 2 additions & 2 deletions ddcLogs/size_rotating.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
get_log_path,
get_logger_and_formatter,
get_stream_handler,
gzip_file,
gzip_file_with_sufix,
list_files,
remove_old_logs,
write_stderr,
Expand Down Expand Up @@ -87,7 +87,7 @@ def __call__(self, source: str, dest: str) -> None:
source_filename, _ = os.path.basename(source).split(".")
new_file_number = self._get_new_file_number(self.directory, source_filename)
if os.path.isfile(source):
gzip_file(source, new_file_number)
gzip_file_with_sufix(source, new_file_number)

@staticmethod
def _get_new_file_number(directory, source_filename):
Expand Down
6 changes: 3 additions & 3 deletions ddcLogs/timed_rotating.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
get_log_path,
get_logger_and_formatter,
get_stream_handler,
gzip_file,
gzip_file_with_sufix,
remove_old_logs,
)
from ddcLogs.settings import LogSettings
Expand Down Expand Up @@ -90,5 +90,5 @@ def __init__(self, dir_logs: str, days_to_keep: int):

def __call__(self, source: str, dest: str) -> None:
remove_old_logs(self.dir, self.days_to_keep)
output_dated_name = os.path.splitext(dest)[1].replace(".", "")
gzip_file(source, output_dated_name)
sufix = os.path.splitext(dest)[1].replace(".", "")
gzip_file_with_sufix(source, sufix)
4 changes: 2 additions & 2 deletions poetry.lock

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

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "ddcLogs"
version = "3.0.5"
version = "3.0.6"
description = "Easy logs with rotations"
license = "MIT"
readme = "README.md"
Expand Down Expand Up @@ -34,10 +34,10 @@ classifiers = [
optional = true

[tool.poetry.dependencies]
python = ">=3.10,<4.0"
python = ">=3.10,<=3.13"
pydantic-settings = ">=2.6.1"
python-dotenv = ">=1.0.1"
pytz = "^2024.2"
pytz = ">=2024.2"

[tool.poetry.group.test.dependencies]
coverage = ">=7.6.8"
Expand Down
84 changes: 64 additions & 20 deletions tests/test_log.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- encoding: utf-8 -*-
import gzip
import os
import tempfile
from datetime import datetime
Expand All @@ -10,7 +11,7 @@ class TestLogs:
@classmethod
def setup_class(cls):
cls.directory = tempfile.gettempdir()
cls.filenames = ("test1.log", "test2.log")
cls.filenames = ("testA.log", "testB.log", "testC.log")

@classmethod
def teardown_class(cls):
Expand All @@ -21,8 +22,15 @@ def teardown_class(cls):

def test_basic_log(self, caplog):
level = "INFO"
log = BasicLog(level=level).init()
log.info("test_basic_log")
logger = BasicLog(
level=level,
name="app",
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="UTC",
showlocation=False,
).init()
logger.info("test_basic_log")
assert level in caplog.text
assert "test_basic_log" in caplog.text

Expand All @@ -34,22 +42,40 @@ def test_size_rotating_log(self, caplog):
f.seek((2 * 1024 * 1024) - 1)
f.write(b"\0")

level = "INFO"
max_mbytes = 1
log = SizeRotatingLog(directory=self.directory,
level=level,
filenames=self.filenames,
maxmbytes=max_mbytes).init()
# creating an exisiting gz file to force rotation number
fname_no_ext = self.filenames[0].split(".")[0]
existing_gz_filename = f"{fname_no_ext}_1.log.gz"
existing_gz_file_path = str(os.path.join(self.directory, existing_gz_filename))
with gzip.open(existing_gz_file_path, "wb") as fout:
fout.write(b"")
new_gz_filename_rotated = f"{fname_no_ext}_2.log.gz"
new_gz_filepath_rotated = str(os.path.join(self.directory, new_gz_filename_rotated))

log.info("test_size_rotating_log")
level = "INFO"
logger = SizeRotatingLog(
level=level,
name="app",
directory=self.directory,
filenames=self.filenames,
maxmbytes=1,
daystokeep=7,
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="UTC",
streamhandler=True,
showlocation=False,
).init()
logger.info("test_size_rotating_log")
assert level in caplog.text
assert "test_size_rotating_log" in caplog.text

# delete test.gz files
# delete .gz files
assert os.path.isfile(new_gz_filepath_rotated) == True
delete_file(new_gz_filepath_rotated)
for filename in self.filenames:
gz_file_name = f"{os.path.splitext(filename)[0]}_1.log.gz"
gz_file_path = os.path.join(tempfile.gettempdir(), gz_file_name)
assert os.path.isfile(gz_file_path)
assert os.path.isfile(gz_file_path) == True
delete_file(gz_file_path)

def test_timed_rotating_log(self, caplog):
Expand All @@ -58,12 +84,21 @@ def test_timed_rotating_log(self, caplog):
month = 10
day = 10

log = TimedRotatingLog(
directory=self.directory,
logger = TimedRotatingLog(
level=level,
filenames=self.filenames
name="app",
directory=self.directory,
filenames=self.filenames,
when="midnight",
sufix="%Y%m%d",
daystokeep=7,
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="UTC",
streamhandler=True,
showlocation=False,
).init()
log.info("start_test_timed_rotating_log")
logger.info("start_test_timed_rotating_log")
assert level in caplog.text
assert "start_test_timed_rotating_log" in caplog.text

Expand All @@ -73,12 +108,21 @@ def test_timed_rotating_log(self, caplog):
file_path = str(os.path.join(self.directory, filename))
os.utime(file_path, (epoch_times, epoch_times))

log = TimedRotatingLog(
directory=self.directory,
logger = TimedRotatingLog(
level=level,
filenames=self.filenames
name="app",
directory=self.directory,
filenames=self.filenames,
when="midnight",
sufix="%Y%m%d",
daystokeep=7,
encoding="UTF-8",
datefmt="%Y-%m-%dT%H:%M:%S",
timezone="UTC",
streamhandler=True,
showlocation=False,
).init()
log.info("end_test_timed_rotating_log")
logger.info("end_test_timed_rotating_log")
assert level in caplog.text
assert "end_test_timed_rotating_log" in caplog.text

Expand Down
Loading