Skip to content

Commit b889565

Browse files
committed
New logging metaclass
1 parent 28a183f commit b889565

File tree

3 files changed

+50
-14
lines changed

3 files changed

+50
-14
lines changed

lib/logging.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
22

33

4+
# ---- FORMATTING ----
5+
46
class ColoredFormatter(logging.Formatter):
57
"""Custom formatter with colored output and specific formatting for transfers."""
68

@@ -59,12 +61,13 @@ def format(self, record: logging.LogRecord) -> str:
5961

6062
return result + exc_text
6163

62-
6364
class HealthCheckFilter(logging.Filter):
6465
def filter(self, record):
6566
return '"GET /health HTTP/1.1" 200' not in record.getMessage()
6667

6768

69+
# ---- LOGGING SETUP ----
70+
6871
def setup_logging() -> logging.Logger:
6972
"""Configure all loggers to use our custom ColoredFormatter."""
7073
formatter = ColoredFormatter(
@@ -96,7 +99,6 @@ def setup_logging() -> logging.Logger:
9699

97100
return root_logger
98101

99-
100102
def get_logger(logger_name: str) -> logging.Logger:
101103
console_handler = logging.StreamHandler()
102104
console_handler.setLevel(logging.DEBUG)
@@ -114,3 +116,38 @@ def get_logger(logger_name: str) -> logging.Logger:
114116
logger.propagate = False
115117

116118
return logger
119+
120+
121+
# ---- PATCHING ----
122+
123+
class HasLogging(type):
124+
"""Metaclass that automatically adds logging methods and a logger property."""
125+
126+
def __new__(mcs, name, bases, namespace, **kwargs):
127+
name_from = kwargs.get('name_from', 'name')
128+
129+
@property
130+
def logger(self):
131+
if not hasattr(self, '_logger'):
132+
class_name = self.__class__.__name__
133+
fallback_name = class_name + str(id(self))[-4:]
134+
logger_name = getattr(self, name_from, fallback_name)
135+
self._logger = get_logger(logger_name)
136+
if not hasattr(self, name_from):
137+
self._logger.warning(
138+
f"Object {class_name} does not have attribute '{name_from}', "
139+
f"using default name: {logger_name}"
140+
)
141+
return self._logger
142+
143+
namespace['logger'] = logger
144+
145+
def make_log_method(level):
146+
def log_method(self, msg, *args, **kwargs):
147+
getattr(self.logger, level)(msg, *args, **kwargs)
148+
return log_method
149+
150+
for level in {'debug', 'info', 'warning', 'error', 'exception', 'critical'}:
151+
namespace[level] = make_log_method(level)
152+
153+
return super().__new__(mcs, name, bases, namespace)

lib/store.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import redis.asyncio as redis
44
from typing import Optional, Annotated
55

6-
from lib.logging import get_logger
6+
from lib.logging import HasLogging, get_logger
77

88

9-
class Store:
9+
class Store(metaclass=HasLogging, name_from='transfer_id'):
1010
"""
1111
Redis-based store for file transfer queues and events.
1212
Handles data queuing and event signaling for transfer coordination.
@@ -17,7 +17,6 @@ class Store:
1717
def __init__(self, transfer_id: str):
1818
self.transfer_id = transfer_id
1919
self.redis = self.get_redis()
20-
self.log = get_logger(transfer_id)
2120

2221
self._k_queue = self.key('queue')
2322
self._k_meta = self.key('metadata')
@@ -77,12 +76,12 @@ async def wait_for_event(self, event_name: str, timeout: float = 300.0) -> None:
7776
async def _poll_marker():
7877
while not await self.redis.exists(event_marker_key):
7978
await asyncio.sleep(1)
80-
self.log.debug(f">> POLL: Event '{event_name}' fired.")
79+
self.debug(f">> POLL: Event '{event_name}' fired.")
8180

8281
async def _listen_for_message():
8382
async for message in pubsub.listen():
8483
if message and message['type'] == 'message':
85-
self.log.debug(f">> SUB : Received message for event '{event_name}'.")
84+
self.debug(f">> SUB : Received message for event '{event_name}'.")
8685
return
8786

8887
poll_marker = asyncio.wait_for(_poll_marker(), timeout=timeout)
@@ -98,7 +97,7 @@ async def _listen_for_message():
9897
task.cancel()
9998

10099
except asyncio.TimeoutError:
101-
self.log.error(f"Timeout waiting for event '{event_name}' after {timeout} seconds.")
100+
self.error(f"Timeout waiting for event '{event_name}' after {timeout} seconds.")
102101
for task in tasks:
103102
task.cancel()
104103
raise
@@ -182,6 +181,6 @@ async def cleanup(self) -> int:
182181
break
183182

184183
if keys_to_delete:
185-
self.log.debug(f"- Cleaning up {len(keys_to_delete)} keys")
184+
self.debug(f"- Cleaning up {len(keys_to_delete)} keys")
186185
return await self.redis.delete(*keys_to_delete)
187186
return 0

lib/transfer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from typing import AsyncIterator, Callable, Awaitable, Optional
55

66
from lib.store import Store
7-
from lib.logging import get_logger
87
from lib.metadata import FileMetadata
8+
from lib.logging import HasLogging, get_logger
9+
10+
logger = get_logger('transfer')
911

1012

1113
class FileTransferError(Exception):
@@ -26,7 +28,8 @@ def __repr__(self):
2628
return self.__class__.__name__ + f"({', '.join(map(repr, self.args))}, {self.kwargs})"
2729

2830

29-
class FileTransfer:
31+
class FileTransfer(metaclass=HasLogging, name_from='uid'):
32+
"""Handles file transfers, including metadata queries and data streaming."""
3033

3134
DONE_FLAG = b'\x00\xFF'
3235
DEAD_FLAG = b'\xDE\xAD'
@@ -38,9 +41,6 @@ def __init__(self, uid: str, file: FileMetadata):
3841
self.bytes_uploaded = 0
3942
self.bytes_downloaded = 0
4043

41-
log = get_logger(self.uid)
42-
self.debug, self.info, self.warning, self.error = log.debug, log.info, log.warning, log.error
43-
4444
@classmethod
4545
async def create(cls, uid: str, file: FileMetadata):
4646
transfer = cls(uid, file)

0 commit comments

Comments
 (0)