22
33import functools
44import inspect
5- import json
65import logging
76from contextvars import ContextVar
87from typing import TYPE_CHECKING , Any , NamedTuple
1514 from apify_client .clients .base .base_client import _BaseBaseClient
1615
1716
18- # Name of the logger used throughout the library
1917logger_name = __name__ .split ('.' )[0 ]
18+ """Name of the logger used throughout the library."""
2019
21- # Logger used throughout the library
2220logger = 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
2624class 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
4543class 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
9396class _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