3
3
import os
4
4
import time
5
5
from abc import ABCMeta , abstractmethod
6
+ from datetime import datetime , timezone
6
7
from functools import partial
7
8
from typing import Any , Callable , Dict , Iterable , List , Optional , Tuple , Union
8
9
@@ -61,9 +62,10 @@ def __init__(
61
62
json_deserializer : Optional [Callable [[Union [Dict , str , bool , int , float ]], str ]] = None ,
62
63
json_default : Optional [Callable [[Any ], Any ]] = None ,
63
64
datefmt : Optional [str ] = None ,
65
+ datetime_fmt : Optional [str ] = None ,
64
66
log_record_order : Optional [List [str ]] = None ,
65
67
utc : bool = False ,
66
- ** kwargs
68
+ ** kwargs ,
67
69
):
68
70
"""Return a LambdaPowertoolsFormatter instance.
69
71
@@ -86,20 +88,33 @@ def __init__(
86
88
Only used when no custom JSON encoder is set
87
89
88
90
datefmt : str, optional
89
- String directives (strftime) to format log timestamp
91
+ String directives (strftime) to format log timestamp using `time`. Only one of `datefmt`
92
+ and `datetime_fmt` should be specified.
90
93
91
94
See https://docs.python.org/3/library/time.html#time.strftime
95
+ datetime_fmt : str, optional
96
+ String directives (strftime) to format log timestamp using `datetime`. Only one of
97
+ `datefmt` and `datetime_fmt` should be specified.
98
+
99
+ See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior . This
100
+ also supports a custom %F directive for milliseconds.
92
101
utc : bool, optional
93
102
set logging timestamp to UTC, by default False to continue to use local time as per stdlib
94
103
log_record_order : list, optional
95
104
set order of log keys when logging, by default ["level", "location", "message", "timestamp"]
96
105
kwargs
97
106
Key-value to be included in log messages
107
+
98
108
"""
99
109
self .json_deserializer = json_deserializer or json .loads
100
110
self .json_default = json_default or str
101
111
self .json_serializer = json_serializer or partial (json .dumps , default = self .json_default , separators = ("," , ":" ))
112
+
113
+ if datefmt and datetime_fmt :
114
+ raise ValueError (f"at most one of datefmt { datefmt !r} and datetime_fmt { datetime_fmt !r} can be specified" )
102
115
self .datefmt = datefmt
116
+ self .datetime_fmt = datetime_fmt
117
+
103
118
self .utc = utc
104
119
self .log_record_order = log_record_order or ["level" , "location" , "message" , "timestamp" ]
105
120
self .log_format = dict .fromkeys (self .log_record_order ) # Set the insertion order for the log messages
@@ -129,13 +144,26 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003
129
144
130
145
def formatTime (self , record : logging .LogRecord , datefmt : Optional [str ] = None ) -> str :
131
146
record_ts = self .converter (record .created ) # type: ignore
132
- if datefmt :
133
- return time .strftime (datefmt , record_ts )
134
147
135
148
# NOTE: Python `time.strftime` doesn't provide msec directives
136
149
# so we create a custom one (%F) and replace logging record ts
137
150
# Reason 2 is that std logging doesn't support msec after TZ
138
151
msecs = "%03d" % record .msecs
152
+
153
+ if datefmt or self .datefmt :
154
+ return time .strftime (datefmt or self .datefmt , record_ts )
155
+
156
+ elif self .datetime_fmt :
157
+ timestamp = record .created + record .msecs / 1000
158
+
159
+ dt = datetime .fromtimestamp (timestamp , tz = timezone .utc )
160
+ if not self .utc :
161
+ # convert back to local time
162
+ dt = dt .astimezone ()
163
+
164
+ custom_fmt = self .datetime_fmt .replace (self .custom_ms_time_directive , msecs )
165
+ return dt .strftime (custom_fmt )
166
+
139
167
custom_fmt = self .default_time_format .replace (self .custom_ms_time_directive , msecs )
140
168
return time .strftime (custom_fmt , record_ts )
141
169
@@ -219,7 +247,7 @@ def _extract_log_keys(self, log_record: logging.LogRecord) -> Dict[str, Any]:
219
247
Structured log as dictionary
220
248
"""
221
249
record_dict = log_record .__dict__ .copy ()
222
- record_dict ["asctime" ] = self .formatTime (record = log_record , datefmt = self . datefmt )
250
+ record_dict ["asctime" ] = self .formatTime (record = log_record )
223
251
extras = {k : v for k , v in record_dict .items () if k not in RESERVED_LOG_ATTRS }
224
252
225
253
formatted_log = {}
0 commit comments