Skip to content

Commit 0f5e002

Browse files
committed
update logging module
1 parent 37f85e5 commit 0f5e002

File tree

1 file changed

+64
-90
lines changed

1 file changed

+64
-90
lines changed

src/apify_client/_logging.py

Lines changed: 64 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import functools
44
import inspect
5-
import json
65
import logging
76
from contextvars import ContextVar
87
from typing import TYPE_CHECKING, Any, NamedTuple
@@ -15,15 +14,16 @@
1514
from apify_client.clients.base.base_client import _BaseBaseClient
1615

1716

18-
# Name of the logger used throughout the library
1917
logger_name = __name__.split('.')[0]
18+
"""Name of the logger used throughout the library."""
2019

21-
# Logger used throughout the library
2220
logger = logging.getLogger(logger_name)
21+
"""Logger used throughout the library."""
2322

2423

25-
# Context containing the details of the request and the resource client making the request
2624
class LogContext(NamedTuple):
25+
"""Request context details for logging (attempt, client method, HTTP method, resource ID, URL)."""
26+
2727
attempt: ContextVar[int | None]
2828
client_method: ContextVar[str | None]
2929
method: ContextVar[str | None]
@@ -40,58 +40,64 @@ class LogContext(NamedTuple):
4040
)
4141

4242

43-
# Metaclass for resource clients which wraps all their public methods
44-
# With injection of their details to the log context vars
4543
class WithLogDetailsClient(type):
44+
"""Metaclass that wraps public methods to inject client details into log context."""
45+
4646
def __new__(cls, name: str, bases: tuple, attrs: dict) -> WithLogDetailsClient:
47+
"""Wrap all public methods in the class with logging context injection."""
4748
for attr_name, attr_value in attrs.items():
4849
if not attr_name.startswith('_') and inspect.isfunction(attr_value):
4950
attrs[attr_name] = _injects_client_details_to_log_context(attr_value)
5051

5152
return type.__new__(cls, name, bases, attrs)
5253

5354

54-
# Wraps an unbound method so that its call will inject the details
55-
# of the resource client (which is the `self` argument of the method)
56-
# to the log context vars
57-
def _injects_client_details_to_log_context(fun: Callable) -> Callable:
58-
if inspect.iscoroutinefunction(fun):
55+
class RedirectLogFormatter(logging.Formatter):
56+
"""Log formatter that prepends colored logger name to messages."""
5957

60-
@functools.wraps(fun)
61-
async def async_wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
62-
log_context.client_method.set(fun.__qualname__)
63-
log_context.resource_id.set(resource_client.resource_id)
58+
def format(self, record: logging.LogRecord) -> str:
59+
"""Format log by prepending colored logger name.
6460
65-
return await fun(resource_client, *args, **kwargs)
61+
Args:
62+
record: The log record to format.
6663
67-
return async_wrapper
68-
elif inspect.isasyncgenfunction(fun): # noqa: RET505
64+
Returns:
65+
Formatted log message with colored logger name prefix.
66+
"""
67+
formatted_logger_name = f'{Fore.CYAN}[{record.name}]{Style.RESET_ALL}'
68+
return f'{formatted_logger_name} -> {record.msg}'
6969

70-
@functools.wraps(fun)
71-
async def async_generator_wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
72-
log_context.client_method.set(fun.__qualname__)
73-
log_context.resource_id.set(resource_client.resource_id)
7470

75-
async for item in fun(resource_client, *args, **kwargs):
76-
yield item
71+
def create_redirect_logger(name: str) -> logging.Logger:
72+
"""Create a logger for redirecting logs from another Actor.
7773
78-
return async_generator_wrapper
79-
else:
74+
Args:
75+
name: Logger name. Use dot notation for hierarchy (e.g., "apify.xyz" creates "xyz" under "apify").
8076
81-
@functools.wraps(fun)
82-
def wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
83-
log_context.client_method.set(fun.__qualname__)
84-
log_context.resource_id.set(resource_client.resource_id)
77+
Returns:
78+
Configured logger with RedirectLogFormatter.
79+
"""
80+
to_logger = logging.getLogger(name)
81+
to_logger.propagate = False
8582

86-
return fun(resource_client, *args, **kwargs)
83+
# Remove filters and handlers in case this logger already exists and was set up in some way.
84+
for handler in to_logger.handlers:
85+
to_logger.removeHandler(handler)
86+
for log_filter in to_logger.filters:
87+
to_logger.removeFilter(log_filter)
8788

88-
return wrapper
89+
handler = logging.StreamHandler()
90+
handler.setFormatter(RedirectLogFormatter())
91+
to_logger.addHandler(handler)
92+
to_logger.setLevel(logging.DEBUG)
93+
return to_logger
8994

9095

91-
# A filter which lets every log record through,
92-
# but adds the current logging context to the record
9396
class _ContextInjectingFilter(logging.Filter):
97+
"""Filter that injects current log context into all log records."""
98+
9499
def filter(self, record: logging.LogRecord) -> bool:
100+
"""Add log context variables to the record."""
95101
record.client_method = log_context.client_method.get()
96102
record.resource_id = log_context.resource_id.get()
97103
record.method = log_context.method.get()
@@ -100,71 +106,39 @@ def filter(self, record: logging.LogRecord) -> bool:
100106
return True
101107

102108

103-
logger.addFilter(_ContextInjectingFilter())
104-
105-
106-
# Log formatter useful for debugging of the client
107-
# Will print out all the extra fields added to the log record
108-
class _DebugLogFormatter(logging.Formatter):
109-
empty_record = logging.LogRecord('dummy', 0, 'dummy', 0, 'dummy', None, None)
110-
111-
# Gets the extra fields from the log record which are not present on an empty record
112-
def _get_extra_fields(self, record: logging.LogRecord) -> dict:
113-
extra_fields: dict = {}
114-
for key, value in record.__dict__.items():
115-
if key not in self.empty_record.__dict__:
116-
extra_fields[key] = value # noqa: PERF403
117-
118-
return extra_fields
119-
120-
def format(self, record: logging.LogRecord) -> str:
121-
extra = self._get_extra_fields(record)
109+
def _injects_client_details_to_log_context(fun: Callable) -> Callable:
110+
"""Wrap a method to inject resource client details into log context before execution."""
111+
if inspect.iscoroutinefunction(fun):
122112

123-
log_string = super().format(record)
124-
if extra:
125-
log_string = f'{log_string} ({json.dumps(extra)})'
126-
return log_string
113+
@functools.wraps(fun)
114+
async def async_wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
115+
log_context.client_method.set(fun.__qualname__)
116+
log_context.resource_id.set(resource_client.resource_id)
127117

118+
return await fun(resource_client, *args, **kwargs)
128119

129-
def create_redirect_logger(
130-
name: str,
131-
) -> logging.Logger:
132-
"""Create a logger for redirecting logs from another Actor.
120+
return async_wrapper
133121

134-
Args:
135-
name: The name of the logger. It can be used to inherit from other loggers. Example: `apify.xyz` will use logger
136-
named `xyz` and make it a children of `apify` logger.
122+
if inspect.isasyncgenfunction(fun):
137123

138-
Returns:
139-
The created logger.
140-
"""
141-
to_logger = logging.getLogger(name)
142-
to_logger.propagate = False
124+
@functools.wraps(fun)
125+
async def async_generator_wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
126+
log_context.client_method.set(fun.__qualname__)
127+
log_context.resource_id.set(resource_client.resource_id)
143128

144-
# Remove filters and handlers in case this logger already exists and was set up in some way.
145-
for handler in to_logger.handlers:
146-
to_logger.removeHandler(handler)
147-
for log_filter in to_logger.filters:
148-
to_logger.removeFilter(log_filter)
129+
async for item in fun(resource_client, *args, **kwargs):
130+
yield item
149131

150-
handler = logging.StreamHandler()
151-
handler.setFormatter(RedirectLogFormatter())
152-
to_logger.addHandler(handler)
153-
to_logger.setLevel(logging.DEBUG)
154-
return to_logger
132+
return async_generator_wrapper
155133

134+
@functools.wraps(fun)
135+
def wrapper(resource_client: _BaseBaseClient, *args: Any, **kwargs: Any) -> Any:
136+
log_context.client_method.set(fun.__qualname__)
137+
log_context.resource_id.set(resource_client.resource_id)
156138

157-
class RedirectLogFormatter(logging.Formatter):
158-
"""Formatter applied to default redirect logger."""
139+
return fun(resource_client, *args, **kwargs)
159140

160-
def format(self, record: logging.LogRecord) -> str:
161-
"""Format the log by prepending logger name to the original message.
141+
return wrapper
162142

163-
Args:
164-
record: Log record to be formatted.
165143

166-
Returns:
167-
Formatted log message.
168-
"""
169-
formatted_logger_name = f'{Fore.CYAN}[{record.name}]{Style.RESET_ALL}'
170-
return f'{formatted_logger_name} -> {record.msg}'
144+
logger.addFilter(_ContextInjectingFilter())

0 commit comments

Comments
 (0)