1
- import copy
2
1
import functools
2
+ import inspect
3
3
import logging
4
4
import os
5
5
import random
@@ -34,7 +34,7 @@ def _is_cold_start() -> bool:
34
34
return cold_start
35
35
36
36
37
- class Logger ( logging . Logger ) :
37
+ class Logger :
38
38
"""Creates and setups a logger to format statements in JSON.
39
39
40
40
Includes service name and any additional key=value into logs
@@ -55,6 +55,8 @@ class Logger(logging.Logger):
55
55
service name to be appended in logs, by default "service_undefined"
56
56
level : str, optional
57
57
logging.level, by default "INFO"
58
+ child: bool, optional
59
+ create a child Logger named <service>.<caller_file_name>, False by default
58
60
sample_rate: float, optional
59
61
sample rate for debug calls within execution context defaults to 0.0
60
62
stream: sys.stdout, optional
@@ -80,7 +82,7 @@ class Logger(logging.Logger):
80
82
>>> def handler(event, context):
81
83
logger.info("Hello")
82
84
83
- **Append payment_id to previously setup structured log logger**
85
+ **Append payment_id to previously setup logger**
84
86
85
87
>>> from aws_lambda_powertools import Logger
86
88
>>> logger = Logger(service="payment")
@@ -89,18 +91,16 @@ class Logger(logging.Logger):
89
91
logger.structure_logs(append=True, payment_id=event["payment_id"])
90
92
logger.info("Hello")
91
93
92
- Parameters
93
- ----------
94
- logging : logging.Logger
95
- Inherits Logger
96
- service: str
97
- name of the service to create the logger for, "service_undefined" by default
98
- level: str, int
99
- log level, INFO by default
100
- sampling_rate: float
101
- debug log sampling rate, 0.0 by default
102
- stream: sys.stdout
103
- log stream, stdout by default
94
+ **Create child Logger using logging inheritance via child param**
95
+
96
+ >>> # app.py
97
+ >>> import another_file
98
+ >>> from aws_lambda_powertools import Logger
99
+ >>> logger = Logger(service="payment")
100
+ >>>
101
+ >>> # another_file.py
102
+ >>> from aws_lambda_powertools import Logger
103
+ >>> logger = Logger(service="payment", child=True)
104
104
105
105
Raises
106
106
------
@@ -112,19 +112,72 @@ def __init__(
112
112
self ,
113
113
service : str = None ,
114
114
level : Union [str , int ] = None ,
115
+ child : bool = False ,
115
116
sampling_rate : float = None ,
116
117
stream : sys .stdout = None ,
117
118
** kwargs ,
118
119
):
119
120
self .service = service or os .getenv ("POWERTOOLS_SERVICE_NAME" ) or "service_undefined"
120
121
self .sampling_rate = sampling_rate or os .getenv ("POWERTOOLS_LOGGER_SAMPLE_RATE" ) or 0.0
121
- self .log_level = level or os .getenv ("LOG_LEVEL" ) or logging .INFO
122
- self .handler = logging .StreamHandler (stream ) if stream is not None else logging .StreamHandler (sys .stdout )
122
+ self .log_level = self ._get_log_level (level )
123
+ self .child = child
124
+ self ._handler = logging .StreamHandler (stream ) if stream is not None else logging .StreamHandler (sys .stdout )
123
125
self ._default_log_keys = {"service" : self .service , "sampling_rate" : self .sampling_rate }
124
- self .log_keys = copy .copy (self ._default_log_keys )
125
-
126
- super ().__init__ (name = self .service , level = self .log_level )
127
-
126
+ self ._logger = self ._get_logger ()
127
+
128
+ self ._init_logger (** kwargs )
129
+
130
+ def __getattr__ (self , name ):
131
+ # Proxy attributes not found to actual logger to support backward compatibility
132
+ # https://github.com/awslabs/aws-lambda-powertools-python/issues/97
133
+ return getattr (self ._logger , name )
134
+
135
+ def _get_log_level (self , level : str ):
136
+ """ Returns preferred log level set by the customer in upper case """
137
+ log_level : str = level or os .getenv ("LOG_LEVEL" )
138
+ log_level = log_level .upper () if log_level is not None else logging .INFO
139
+
140
+ return log_level
141
+
142
+ def _get_logger (self ):
143
+ """ Returns a Logger named {self.service}, or {self.service.filename} for child loggers"""
144
+ logger_name = self .service
145
+ if self .child :
146
+ logger_name = f"{ self .service } .{ self ._get_caller_filename ()} "
147
+
148
+ return logging .getLogger (logger_name )
149
+
150
+ def _get_caller_filename (self ):
151
+ """ Return caller filename by finding the caller frame """
152
+ # Current frame => _get_logger()
153
+ # Previous frame => logger.py
154
+ # Before previous frame => Caller
155
+ frame = inspect .currentframe ()
156
+ caller_frame = frame .f_back .f_back .f_back
157
+ filename = caller_frame .f_globals ["__name__" ]
158
+
159
+ return filename
160
+
161
+ def _init_logger (self , ** kwargs ):
162
+ """Configures new logger"""
163
+
164
+ # Skip configuration if it's a child logger to prevent
165
+ # multiple handlers being attached as well as different sampling mechanisms
166
+ # and multiple messages from being logged as handlers can be duplicated
167
+ if not self .child :
168
+ self ._configure_sampling ()
169
+ self ._logger .setLevel (self .log_level )
170
+ self ._logger .addHandler (self ._handler )
171
+ self .structure_logs (** kwargs )
172
+
173
+ def _configure_sampling (self ):
174
+ """Dynamically set log level based on sampling rate
175
+
176
+ Raises
177
+ ------
178
+ InvalidLoggerSamplingRateError
179
+ When sampling rate provided is not a float
180
+ """
128
181
try :
129
182
if self .sampling_rate and random .random () <= float (self .sampling_rate ):
130
183
logger .debug ("Setting log level to Debug due to sampling rate" )
@@ -134,12 +187,8 @@ def __init__(
134
187
f"Expected a float value ranging 0 to 1, but received { self .sampling_rate } instead. Please review POWERTOOLS_LOGGER_SAMPLE_RATE environment variable." # noqa E501
135
188
)
136
189
137
- self .setLevel (self .log_level )
138
- self .structure_logs (** kwargs )
139
- self .addHandler (self .handler )
140
-
141
190
def inject_lambda_context (self , lambda_handler : Callable [[Dict , Any ], Any ] = None , log_event : bool = False ):
142
- """Decorator to capture Lambda contextual info and inject into struct logging
191
+ """Decorator to capture Lambda contextual info and inject into logger
143
192
144
193
Parameters
145
194
----------
@@ -216,21 +265,24 @@ def structure_logs(self, append: bool = False, **kwargs):
216
265
append : bool, optional
217
266
[description], by default False
218
267
"""
219
- self .handler .setFormatter (JsonFormatter (** self ._default_log_keys , ** kwargs ))
220
-
221
- if append :
222
- new_keys = {** self .log_keys , ** kwargs }
223
- self .handler .setFormatter (JsonFormatter (** new_keys ))
224
268
225
- self .log_keys .update (** kwargs )
269
+ # Child loggers don't have handlers attached, use its parent handlers
270
+ handlers = self ._logger .parent .handlers if self .child else self ._logger .handlers
271
+ for handler in handlers :
272
+ if append :
273
+ # Update existing formatter in an existing logger handler
274
+ handler .formatter .update_formatter (** kwargs )
275
+ else :
276
+ # Set a new formatter for a logger handler
277
+ handler .setFormatter (JsonFormatter (** self ._default_log_keys , ** kwargs ))
226
278
227
279
228
280
def set_package_logger (
229
281
level : Union [str , int ] = logging .DEBUG , stream : sys .stdout = None , formatter : logging .Formatter = None
230
282
):
231
283
"""Set an additional stream handler, formatter, and log level for aws_lambda_powertools package logger.
232
284
233
- **Package log by default is supressed (NullHandler), this should only used for debugging.
285
+ **Package log by default is suppressed (NullHandler), this should only used for debugging.
234
286
This is separate from application Logger class utility**
235
287
236
288
Example
0 commit comments