atomicshop 2.14.3__py3-none-any.whl → 2.14.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

@@ -6,8 +6,15 @@ from ctypes.wintypes import ULONG
6
6
  # Constants
7
7
  EVENT_TRACE_CONTROL_STOP = 1
8
8
  WNODE_FLAG_TRACED_GUID = 0x00020000
9
+ EVENT_TRACE_REAL_TIME_MODE = 0x00000100
10
+ EVENT_CONTROL_CODE_ENABLE_PROVIDER = 1
9
11
 
10
12
  MAXIMUM_LOGGERS = 64
13
+ ULONG64 = ctypes.c_uint64
14
+
15
+ INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
16
+ TRACEHANDLE = ULONG64
17
+
11
18
 
12
19
 
13
20
  """
@@ -43,7 +50,6 @@ class WNODE_HEADER(ctypes.Structure):
43
50
  ]
44
51
 
45
52
 
46
- # Define EVENT_TRACE_PROPERTIES structure
47
53
  class EVENT_TRACE_PROPERTIES(ctypes.Structure):
48
54
  _fields_ = [
49
55
  ("Wnode", WNODE_HEADER),
@@ -70,29 +76,32 @@ class EVENT_TRACE_PROPERTIES(ctypes.Structure):
70
76
  ]
71
77
 
72
78
 
73
- # Define the EVENT_TRACE_LOGFILE structure
74
- class EVENT_TRACE_LOGFILE(ctypes.Structure):
79
+ class TRACE_LOGFILE_HEADER(ctypes.Structure):
75
80
  _fields_ = [
76
- ("LogFileName", wintypes.LPWSTR),
77
- ("LoggerName", wintypes.LPWSTR),
78
- ("CurrentTime", wintypes.LARGE_INTEGER),
79
- ("BuffersRead", wintypes.ULONG),
80
- ("ProcessTraceMode", wintypes.ULONG),
81
- ("EventRecordCallback", wintypes.LPVOID),
82
81
  ("BufferSize", wintypes.ULONG),
83
- ("Filled", wintypes.ULONG),
84
- ("EventsLost", wintypes.ULONG),
85
- ("BuffersLost", wintypes.ULONG),
86
- ("RealTimeBuffersLost", wintypes.ULONG),
87
- ("LogBuffersLost", wintypes.ULONG),
88
- ("BuffersWritten", wintypes.ULONG),
82
+ ("Version", wintypes.ULONG),
83
+ ("ProviderVersion", wintypes.ULONG),
84
+ ("NumberOfProcessors", wintypes.ULONG),
85
+ ("EndTime", wintypes.LARGE_INTEGER),
86
+ ("TimerResolution", wintypes.ULONG),
87
+ ("MaximumFileSize", wintypes.ULONG),
89
88
  ("LogFileMode", wintypes.ULONG),
90
- ("IsKernelTrace", wintypes.ULONG),
91
- ("Context", wintypes.ULONG) # Placeholder for context pointer
89
+ ("BuffersWritten", wintypes.ULONG),
90
+ ("StartBuffers", wintypes.ULONG),
91
+ ("PointerSize", wintypes.ULONG),
92
+ ("EventsLost", wintypes.ULONG),
93
+ ("CpuSpeedInMHz", wintypes.ULONG),
94
+ ("LoggerName", wintypes.WCHAR * 256),
95
+ ("LogFileName", wintypes.WCHAR * 256),
96
+ ("TimeZone", wintypes.LPVOID),
97
+ ("BootTime", wintypes.LARGE_INTEGER),
98
+ ("PerfFreq", wintypes.LARGE_INTEGER),
99
+ ("StartTime", wintypes.LARGE_INTEGER),
100
+ ("ReservedFlags", wintypes.ULONG),
101
+ ("BuffersLost", wintypes.ULONG)
92
102
  ]
93
103
 
94
104
 
95
- # Define the EVENT_TRACE_HEADER structure
96
105
  class EVENT_TRACE_HEADER(ctypes.Structure):
97
106
  _fields_ = [
98
107
  ("Size", wintypes.USHORT),
@@ -116,10 +125,67 @@ class EVENT_TRACE_HEADER(ctypes.Structure):
116
125
  ]
117
126
 
118
127
 
119
- # Define the EVENT_RECORD structure
128
+ class EVENT_TRACE(ctypes.Structure):
129
+ _fields_ = [
130
+ ("Header", EVENT_TRACE_HEADER),
131
+ ("InstanceId", wintypes.DWORD),
132
+ ("ParentInstanceId", wintypes.DWORD),
133
+ ("ParentGuid", GUID),
134
+ ("MofData", ctypes.c_void_p),
135
+ ("MofLength", wintypes.ULONG),
136
+ ("ClientContext", wintypes.ULONG)
137
+ ]
138
+
139
+
140
+ class EVENT_TRACE_LOGFILEW(ctypes.Structure):
141
+ _fields_ = [
142
+ ("LogFileName", ctypes.c_wchar_p),
143
+ ("LoggerName", ctypes.c_wchar_p),
144
+ ("CurrentTime", wintypes.LARGE_INTEGER),
145
+ ("BuffersRead", wintypes.ULONG),
146
+ ("ProcessTraceMode", wintypes.ULONG),
147
+ ("CurrentEvent", EVENT_TRACE),
148
+ ("LogfileHeader", TRACE_LOGFILE_HEADER),
149
+ ("BufferCallback", ctypes.c_void_p), # Placeholder for buffer callback
150
+ ("BufferSize", wintypes.ULONG),
151
+ ("Filled", wintypes.ULONG),
152
+ ("EventsLost", wintypes.ULONG),
153
+ ("EventCallback", ctypes.CFUNCTYPE(None, ctypes.POINTER(EVENT_TRACE))),
154
+ ("Context", ULONG64)
155
+ ]
156
+
157
+
158
+ class EVENT_DESCRIPTOR(ctypes.Structure):
159
+ _fields_ = [
160
+ ("Id", wintypes.USHORT),
161
+ ("Version", wintypes.BYTE),
162
+ ("Channel", wintypes.BYTE),
163
+ ("Level", wintypes.BYTE),
164
+ ("Opcode", wintypes.BYTE),
165
+ ("Task", wintypes.USHORT),
166
+ ("Keyword", ULONG64),
167
+ ]
168
+
169
+
170
+ class EVENT_HEADER(ctypes.Structure):
171
+ _fields_ = [
172
+ ("Size", wintypes.USHORT),
173
+ ("HeaderType", wintypes.USHORT),
174
+ ("Flags", wintypes.USHORT),
175
+ ("EventProperty", wintypes.USHORT),
176
+ ("ThreadId", wintypes.ULONG),
177
+ ("ProcessId", wintypes.ULONG),
178
+ ("TimeStamp", wintypes.LARGE_INTEGER),
179
+ ("ProviderId", GUID),
180
+ ("EventDescriptor", EVENT_DESCRIPTOR),
181
+ ("ProcessorTime", ULONG64),
182
+ ("ActivityId", GUID),
183
+ ("RelatedActivityId", GUID),
184
+ ]
185
+
120
186
  class EVENT_RECORD(ctypes.Structure):
121
187
  _fields_ = [
122
- ("EventHeader", EVENT_TRACE_HEADER),
188
+ ("EventHeader", EVENT_HEADER),
123
189
  ("BufferContext", wintypes.ULONG),
124
190
  ("ExtendedDataCount", wintypes.USHORT),
125
191
  ("UserDataLength", wintypes.USHORT),
@@ -129,6 +195,50 @@ class EVENT_RECORD(ctypes.Structure):
129
195
  ]
130
196
 
131
197
 
198
+ # class EVENT_TRACE_LOGFILE(ctypes.Structure):
199
+ # _fields_ = [
200
+ # ("LogFileName", wintypes.LPWSTR),
201
+ # ("LoggerName", wintypes.LPWSTR),
202
+ # ("CurrentTime", wintypes.LARGE_INTEGER),
203
+ # ("BuffersRead", wintypes.ULONG),
204
+ # ("ProcessTraceMode", wintypes.ULONG),
205
+ # ("EventRecordCallback", wintypes.LPVOID),
206
+ # ("BufferSize", wintypes.ULONG),
207
+ # ("Filled", wintypes.ULONG),
208
+ # ("EventsLost", wintypes.ULONG),
209
+ # ("BuffersLost", wintypes.ULONG),
210
+ # ("RealTimeBuffersLost", wintypes.ULONG),
211
+ # ("LogBuffersLost", wintypes.ULONG),
212
+ # ("BuffersWritten", wintypes.ULONG),
213
+ # ("LogFileMode", wintypes.ULONG),
214
+ # ("IsKernelTrace", wintypes.ULONG),
215
+ # ("Context", wintypes.ULONG) # Placeholder for context pointer
216
+ # ]
217
+
218
+
219
+ class EVENT_TRACE_LOGFILE(ctypes.Structure):
220
+ _fields_ = [
221
+ ("LogFileName", wintypes.LPWSTR),
222
+ ("LoggerName", wintypes.LPWSTR),
223
+ ("CurrentTime", wintypes.LARGE_INTEGER),
224
+ ("BuffersRead", wintypes.ULONG),
225
+ ("ProcessTraceMode", wintypes.ULONG),
226
+ ("CurrentEvent", EVENT_RECORD),
227
+ ("LogfileHeader", TRACE_LOGFILE_HEADER),
228
+ ("BufferCallback", wintypes.LPVOID),
229
+ ("BufferSize", wintypes.ULONG),
230
+ ("Filled", wintypes.ULONG),
231
+ ("EventsLost", wintypes.ULONG),
232
+ ("EventCallback", ctypes.c_void_p),
233
+ ("IsKernelTrace", wintypes.ULONG),
234
+ ("Context", wintypes.LPVOID)
235
+ ]
236
+
237
+
238
+ # Define the callback type for processing events
239
+ EVENT_CALLBACK_TYPE = ctypes.WINFUNCTYPE(None, ctypes.POINTER(EVENT_RECORD))
240
+
241
+
132
242
  class PROVIDER_ENUMERATION_INFO(ctypes.Structure):
133
243
  _fields_ = [
134
244
  ("NumberOfProviders", ULONG),
@@ -1,12 +1,137 @@
1
1
  import ctypes
2
+ from ctypes import wintypes
2
3
  from ctypes.wintypes import ULONG
3
4
  import uuid
5
+ from typing import Literal
4
6
 
5
7
  from . import const
8
+ from ....etws import providers
9
+
10
+ class ETWSessionExists(Exception):
11
+ pass
12
+
13
+
14
+ # Convert the GUID string to a GUID structure
15
+ def _string_to_guid(guid_string):
16
+ guid_string = guid_string.strip('{}') # Remove curly braces
17
+ parts = guid_string.split('-')
18
+ return const.GUID(
19
+ Data1=int(parts[0], 16),
20
+ Data2=int(parts[1], 16),
21
+ Data3=int(parts[2], 16),
22
+ Data4=(ctypes.c_ubyte * 8)(*[
23
+ int(parts[3][i:i+2], 16) for i in range(0, 4, 2)
24
+ ] + [
25
+ int(parts[4][i:i+2], 16) for i in range(0, 12, 2)
26
+ ])
27
+ )
28
+
29
+
30
+ # Set up the ETW session
31
+ def start_etw_session(
32
+ session_name: str,
33
+ provider_guid_list: list = None,
34
+ provider_name_list: list = None,
35
+ verbosity_mode: int = 4,
36
+ maximum_buffers: int = 38
37
+ ):
38
+ """
39
+ Start an ETW session and enable the specified provider.
40
+
41
+ :param session_name: The name of the session to start.
42
+ :param provider_guid_list: The GUID list of the providers to enable.
43
+ :param provider_name_list: The name list of the providers to enable.
44
+ :param verbosity_mode: The verbosity level of the events to capture.
45
+ 0 - Always: Capture all events. This is typically used for critical events that should always be logged.
46
+ 1 - Critical: Capture critical events that indicate a severe problem.
47
+ 2 - Error: Capture error events that indicate a problem but are not critical.
48
+ 3 - Warning: Capture warning events that indicate a potential problem.
49
+ 4 - Information: Capture informational events that are not indicative of problems.
50
+ 5 - Verbose: Capture detailed trace events for diagnostic purposes.
51
+ :param maximum_buffers: The maximum number of buffers to use.
52
+ 0 or 16: The default value of ETW class. If you put 0, it will be converted to 16 by default by ETW itself.
53
+ 38: The maximum number of buffers that can be used.
54
+
55
+ Event Handling Capacity:
56
+ 16 Buffers: With fewer buffers, the session can handle a smaller volume of events before needing to flush
57
+ the buffers to the log file or before a real-time consumer needs to process them. If the buffers fill up
58
+ quickly and cannot be processed in time, events might be lost.
59
+ 38 Buffers: With more buffers, the session can handle a higher volume of events. This reduces the
60
+ likelihood of losing events in high-traffic scenarios because more events can be held in memory before
61
+ they need to be processed or written to a log file.
62
+ Performance Considerations:
63
+ 16 Buffers: Requires less memory, but may be prone to event loss under heavy load if the buffers fill up
64
+ faster than they can be processed.
65
+ 38 Buffers: Requires more memory, but can improve reliability in capturing all events under heavy load by
66
+ providing more buffer space. However, it can also increase the memory footprint of the application or
67
+ system running the ETW session.
68
+ """
69
+
70
+ if not provider_guid_list and not provider_name_list:
71
+ raise ValueError("Either 'provider_guid_list' or 'provider_name_list' must be provided")
72
+ elif provider_guid_list and provider_name_list:
73
+ raise ValueError("Only one of 'provider_guid_list' or 'provider_name_list' must be provided")
74
+
75
+ if provider_name_list:
76
+ provider_guid_list = []
77
+ for provider_name in provider_name_list:
78
+ provider_guid_list.append(providers.get_provider_guid_by_name(provider_name))
79
+
80
+ properties_size = ctypes.sizeof(const.EVENT_TRACE_PROPERTIES) + (2 * wintypes.MAX_PATH)
81
+ properties = (ctypes.c_byte * properties_size)()
82
+ properties_ptr = ctypes.cast(properties, ctypes.POINTER(const.EVENT_TRACE_PROPERTIES))
83
+
84
+ # Initialize the EVENT_TRACE_PROPERTIES structure
85
+ properties_ptr.contents.Wnode.BufferSize = properties_size
86
+ properties_ptr.contents.Wnode.Guid = const.GUID()
87
+ properties_ptr.contents.Wnode.Flags = const.WNODE_FLAG_TRACED_GUID
88
+ properties_ptr.contents.Wnode.ClientContext = 1 # QPC clock resolution
89
+ properties_ptr.contents.BufferSize = 1024
90
+ properties_ptr.contents.MinimumBuffers = 1
91
+ properties_ptr.contents.MaximumBuffers = maximum_buffers
92
+ properties_ptr.contents.MaximumFileSize = 0
93
+ properties_ptr.contents.LogFileMode = const.EVENT_TRACE_REAL_TIME_MODE
94
+ properties_ptr.contents.FlushTimer = 1
95
+ properties_ptr.contents.EnableFlags = 0
96
+
97
+ # Start the ETW session
98
+ session_handle = wintypes.HANDLE()
99
+ status = ctypes.windll.advapi32.StartTraceW(
100
+ ctypes.byref(session_handle),
101
+ ctypes.c_wchar_p(session_name),
102
+ properties_ptr
103
+ )
104
+
105
+ if status != 0:
106
+ if status == 183:
107
+ raise ETWSessionExists(f"ETW session [{session_name}] already exists")
108
+ else:
109
+ raise Exception(f"StartTraceW failed with error {status}")
110
+
111
+ # Enable each provider
112
+ for provider_guid in provider_guid_list:
113
+ provider_guid_struct = _string_to_guid(provider_guid)
114
+ status = ctypes.windll.advapi32.EnableTraceEx2(
115
+ session_handle,
116
+ ctypes.byref(provider_guid_struct),
117
+ const.EVENT_CONTROL_CODE_ENABLE_PROVIDER,
118
+ verbosity_mode,
119
+ 0,
120
+ 0,
121
+ 0,
122
+ None
123
+ )
124
+
125
+ if status != 0:
126
+ raise Exception(f"EnableTraceEx2 failed for provider {provider_guid} with error {status}")
127
+
128
+ print("ETW session started successfully")
129
+
130
+ return session_handle
6
131
 
7
132
 
8
133
  # Function to stop and delete ETW session
9
- def stop_and_delete_etw_session(session_name) -> tuple[bool, int]:
134
+ def stop_and_delete_etw_session(session_name: str) -> tuple[bool, int]:
10
135
  """
11
136
  Stop and delete ETW session.
12
137
 
@@ -40,13 +165,19 @@ def stop_and_delete_etw_session(session_name) -> tuple[bool, int]:
40
165
  return True, status
41
166
 
42
167
 
43
- def get_all_providers() -> list[tuple[any, uuid.UUID]]:
168
+ def get_all_providers(key_as: Literal['name', 'guid'] = 'name') -> dict:
44
169
  """
45
170
  Get all ETW providers available on the system.
46
171
 
47
- :return: A list of tuples containing the provider name and GUID.
172
+ :param key_as: The key to use in the dictionary, either 'name' or 'guid'.
173
+ 'name': The provider name is used as the key, the guid as the value.
174
+ 'guid': The provider guid is used as the key, the name as the value.
175
+ :return: dict containing the provider name and GUID.
48
176
  """
49
177
 
178
+ if key_as not in ['name', 'guid']:
179
+ raise ValueError("key_as must be either 'name' or 'guid'")
180
+
50
181
  providers_info_size = ULONG(0)
51
182
  status = const.tdh.TdhEnumerateProviders(None, ctypes.byref(providers_info_size))
52
183
 
@@ -71,7 +202,7 @@ def get_all_providers() -> list[tuple[any, uuid.UUID]]:
71
202
  ctypes.addressof(providers_info.contents) + ctypes.sizeof(const.PROVIDER_ENUMERATION_INFO),
72
203
  ctypes.POINTER(const.PROVIDER_INFORMATION * provider_count))
73
204
 
74
- providers = []
205
+ providers: dict = {}
75
206
  for i in range(provider_count):
76
207
  provider = provider_array.contents[i]
77
208
  provider_name_offset = provider.ProviderNameOffset
@@ -79,7 +210,12 @@ def get_all_providers() -> list[tuple[any, uuid.UUID]]:
79
210
  ctypes.addressof(providers_info.contents) + provider_name_offset, ctypes.c_wchar_p)
80
211
  provider_name = provider_name_ptr.value
81
212
  provider_guid = uuid.UUID(bytes_le=bytes(provider.ProviderId))
82
- providers.append((provider_name, provider_guid))
213
+ provider_guid_string = str(provider_guid)
214
+
215
+ if key_as == 'name':
216
+ providers[provider_name] = provider_guid_string
217
+ elif key_as == 'guid':
218
+ providers[provider_guid_string] = provider_name
83
219
 
84
220
  return providers
85
221
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import time
2
3
 
3
4
 
4
5
  # Log formatter, means how the log will look inside the file
@@ -8,32 +9,9 @@ import logging
8
9
 
9
10
  # ".40" truncating the string to only 40 characters. Example: %(message).250s
10
11
 
11
- # Adding '%(asctime)s.%(msecs)06f' will print milliseconds as well as nanoseconds:
12
- # 2022-02-17 15:15:51,913.335562
13
- # If you don't use custom 'datefmt' in your 'setFormatter' function,
14
- # it will print duplicate milliseconds:
15
- # 2022-02-17 15:15:51,913.913.335562
16
- # The setting should be like:
17
- # file_handler.setFormatter(logging.Formatter(log_formatter_file, datefmt='%Y-%m-%d,%H:%M:%S'))
18
- # 's' stands for string. 'd' stands for digits, a.k.a. 'int'. 'f' stands for float.
19
-
20
- # Old tryouts:
21
- # log_formatter_file: str = f"%(asctime)s.%(msecs)06f | " \
22
- # log_formatter_file: str = f"%(asctime)s.%(msecs) | " \
23
- # f"%(levelname)-{len(log_header_level)}s | " \
24
- # f"%(name)-{len(log_header_logger)}s | " \
25
- # f"%(filename)-{len(log_header_script)}s : " \
26
- # f"%(lineno)-{len(log_header_line)}d | " \
27
- # "%(threadName)s: %(message)s"
28
- # log_formatter_file: str = "{asctime}.{msecs:0<3.0f} | " \
29
- # log_formatter_file: str = "{asctime}.{msecs:0>3.0f}.{msecs:0>.6f} | " \
30
-
31
- # Old tryouts for reference:
32
- # file_formatter = logging.Formatter(log_formatter_file, style='{')
33
- # file_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
34
- # file_formatter.default_msec_format = '%s,%03d'
35
- # file_formatter.default_msec_format = '%s,%03f'
36
12
 
13
+ DEFAULT_STREAM_FORMATTER: str = "%(levelname)s | %(threadName)s | %(name)s | %(message)s"
14
+ DEFAULT_MESSAGE_FORMATTER: str = "%(message)s"
37
15
 
38
16
  FORMAT_ELEMENT_TO_HEADER: dict = {
39
17
  'asctime': 'Event Time [Y-M-D H:M:S]',
@@ -56,7 +34,7 @@ FORMAT_ELEMENT_TO_HEADER: dict = {
56
34
  }
57
35
 
58
36
  DEFAULT_FORMATTER_TXT_FILE: str = \
59
- "{asctime},{msecs:013.9f} | " \
37
+ "{asctime} | " \
60
38
  "{levelname:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['levelname'])}" + "s} | " \
61
39
  "{name:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['name'])}" + "s} | " \
62
40
  "{filename:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['filename'])}" + "s} : " \
@@ -64,7 +42,53 @@ DEFAULT_FORMATTER_TXT_FILE: str = \
64
42
  "{threadName} | {message}"
65
43
 
66
44
  DEFAULT_FORMATTER_CSV_FILE: str = \
67
- '\"{asctime}.{msecs:010.6f}\",{levelname},{name},{filename},{lineno},{threadName},\"{message}\"'
45
+ '\"{asctime}\",{levelname},{name},{filename},{lineno},{threadName},\"{message}\"'
46
+
47
+
48
+ class NanosecondsFormatter(logging.Formatter):
49
+ def __init__(self, fmt=None, datefmt=None, style='%', use_nanoseconds=False):
50
+ super().__init__(fmt, datefmt, style)
51
+ self.use_nanoseconds = use_nanoseconds
52
+
53
+ def formatTime(self, record, datefmt=None):
54
+ ct = self.converter(record.created)
55
+
56
+ if datefmt:
57
+ # Remove unsupported %f from datefmt if present
58
+ if '%f' in datefmt:
59
+ datefmt = datefmt.replace('%f', '')
60
+ self.use_nanoseconds = True
61
+ else:
62
+ # Default time format if datefmt is not provided
63
+ datefmt = '%Y-%m-%d %H:%M:%S'
64
+
65
+ s = time.strftime(datefmt, ct)
66
+
67
+ if self.use_nanoseconds:
68
+ # Calculate nanoseconds from the fractional part of the timestamp
69
+ nanoseconds = f'{record.created:.9f}'.split('.')[1]
70
+ # Return the formatted string with nanoseconds appended
71
+ return f'{s}.{nanoseconds}'
72
+ else:
73
+ return s
74
+
75
+
76
+
77
+
78
+
79
+ # if datefmt is None:
80
+ # # Use the default behavior if no datefmt is provided
81
+ # return super().formatTime(record, datefmt)
82
+ # elif '%f' in datefmt:
83
+ # # Format the time up to seconds
84
+ # base_time = time.strftime(datefmt.replace('%f', ''), ct)
85
+ # # Calculate nanoseconds from the fractional part of the timestamp
86
+ # nanoseconds = f'{record.created:.9f}'.split('.')[1]
87
+ # # Return the formatted string with nanoseconds appended
88
+ # return base_time + nanoseconds
89
+ # else:
90
+ # # Use the provided datefmt if it doesn't include %f
91
+ # return time.strftime(datefmt, ct)
68
92
 
69
93
 
70
94
  class FormatterProcessor:
@@ -150,7 +174,11 @@ class FormatterProcessor:
150
174
 
151
175
 
152
176
  def get_logging_formatter_from_string(
153
- formatter: str, style=None, datefmt=None, disable_duplicate_ms: bool = False) -> logging.Formatter:
177
+ formatter: str,
178
+ style=None,
179
+ datefmt=None,
180
+ use_nanoseconds: bool = False
181
+ ) -> logging.Formatter:
154
182
  """
155
183
  Function to get the logging formatter from the string.
156
184
 
@@ -160,12 +188,12 @@ def get_logging_formatter_from_string(
160
188
  '%': will use the '%' style.
161
189
  '{': will use the '{' style.
162
190
  :param datefmt: string, date format of 'asctime' element. Default is None.
163
- :param disable_duplicate_ms: bool, if True, will disable the duplicate milliseconds in the 'asctime' element.
164
- Example: If we're using '%(asctime)s.%(msecs)06f' msecs value in our time stamp, we need to use custom
165
- 'datefmt' to get rid of the additional duplicate milliseconds:
166
- Instead of '2022-02-17 15:15:51,913.913.335562' print '2022-02-17 15:15:51,913.335562'
167
- The problem with this method is that milliseconds aren't adjusted to 3 digits with zeroes (like 1 = 001).
168
- We can use the regular strftime format: datefmt='%Y-%m-%d,%H:%M:%S:%f'
191
+ We use custom formatter that can process the date format with nanoseconds:
192
+ '%Y-%m-%d %H:%M:%S.%f' -> '2021-01-01 00:00:00.000000000'
193
+ :param use_nanoseconds: bool, if set to True, the formatter will use nanoseconds instead of milliseconds.
194
+ This will print 'asctime' in the following format: '2021-01-01 00:00:00.000000000', instead of
195
+ '2021-01-01 00:00:00.000'.
196
+
169
197
  :return: logging.Formatter, formatter.
170
198
  """
171
199
 
@@ -173,10 +201,8 @@ def get_logging_formatter_from_string(
173
201
  if not style:
174
202
  style = FormatterProcessor(formatter).get_style()['style']
175
203
 
176
- # The regular 'datefmt' is '%Y-%m-%d,%H:%M:%S:%f'. If we want to use it with milliseconds 'msecs' element,
177
- # we need to disable the duplicate milliseconds.
178
- if disable_duplicate_ms:
179
- datefmt = '%Y-%m-%d,%H:%M:%S'
180
-
181
204
  # Create the logging formatter.
182
- return logging.Formatter(formatter, style=style, datefmt=datefmt)
205
+ if use_nanoseconds or '%f' in datefmt:
206
+ return NanosecondsFormatter(formatter, style=style, datefmt=datefmt, use_nanoseconds=use_nanoseconds)
207
+ else:
208
+ return logging.Formatter(formatter, style=style, datefmt=datefmt)
@@ -2,6 +2,11 @@ import logging
2
2
  from logging.handlers import TimedRotatingFileHandler, QueueListener, QueueHandler
3
3
  import re
4
4
  import os
5
+ from pathlib import Path
6
+
7
+
8
+ DEFAULT_DATE_STRING_FORMAT: str = "%Y_%m_%d"
9
+ DEFAULT_DATE_REGEX_PATTERN: str = r"^\d{4}_\d{2}_\d{2}$"
5
10
 
6
11
 
7
12
  class TimedRotatingFileHandlerWithHeader(TimedRotatingFileHandler):
@@ -150,7 +155,20 @@ def get_handler_name(handler: logging.Handler) -> str:
150
155
  return handler.get_name()
151
156
 
152
157
 
153
- def change_rotated_filename(file_handler: logging.Handler, file_extension: str):
158
+ def change_rotated_filename(
159
+ file_handler: logging.Handler,
160
+ date_format_string: str = None,
161
+ date_regex_pattern: str = None
162
+ ):
163
+ """
164
+ Function to change the way TimedRotatingFileHandler managing the rotating filename.
165
+
166
+ :param file_handler: FileHandler to change the rotating filename for.
167
+ :param date_format_string: Date format string to use for the rotated log filename.
168
+ If None, the default 'DEFAULT_DATE_STRING_FORMAT' will be used.
169
+ :param date_regex_pattern: Regex pattern to match the rotated log filenames.
170
+ If None, the default 'DEFAULT_DATE_REGEX_PATTERN' will be used.
171
+ """
154
172
  # Changing the way TimedRotatingFileHandler managing the rotating filename
155
173
  # Default file suffix is only "Year_Month_Day" with addition of the dot (".") character to the
156
174
  # "file name + extension" that you provide it. Example: log file name:
@@ -170,18 +188,40 @@ def change_rotated_filename(file_handler: logging.Handler, file_extension: str):
170
188
  # file_handler.extMatch = re.compile(r"^\d{4}_\d{2}_\d{2}" + re.escape(log_file_extension) + r"$")
171
189
  # file_handler.extMatch = re.compile(r"^\d{4}_\d{2}_\d{2}.txt$")
172
190
 
173
- # Set variables that are responsible for setting TimedRotatingFileHandler filename on rotation.
174
- # Log files time format, need only date
175
- format_date_log_filename: str = "%Y_%m_%d"
176
- # Log file suffix.
177
- logfile_suffix: str = "_" + format_date_log_filename + file_extension
178
- # Regex object to match the TimedRotatingFileHandler file name suffix.
179
- # "re.escape" is used to "escape" strings in regex and use them as is.
180
- logfile_regex_suffix = re.compile(r"^\d{4}_\d{2}_\d{2}" + re.escape(file_extension) + r"$")
191
+ # Update the namer function to format the rotated filename correctly
192
+ def namer(name):
193
+ # Currently the 'name' is full file path + '.' + logfile_suffix.
194
+ # Example: 'C:\\path\\to\\file.log._2021_12_24'
195
+ # Get the parent directory of the file: C:\path\to
196
+ parent_dir: str = str(Path(name).parent)
197
+ # Get the base filename without the extension: file.log
198
+ filename: str = Path(name).stem
199
+ # Get the date part of the filename: _2021_12_24
200
+ date_part: str = str(Path(name).suffix).replace(".", "")
201
+ # Get the file extension: log
202
+ file_extension: str = Path(filename).suffix
203
+ # Get the file name without the extension: file
204
+ file_stem: str = Path(filename).stem
205
+
206
+ return f"{parent_dir}{os.sep}{file_stem}{date_part}{file_extension}"
207
+
208
+ # Construct the new suffix without the file extension
209
+ if date_format_string is None:
210
+ logfile_suffix = f"_{DEFAULT_DATE_STRING_FORMAT}"
211
+ else:
212
+ logfile_suffix = f"_{date_format_string}"
213
+
214
+ # Regex pattern to match the rotated log filenames
215
+ if date_regex_pattern is None:
216
+ logfile_regex_suffix = re.compile(DEFAULT_DATE_REGEX_PATTERN)
217
+ else:
218
+ logfile_regex_suffix = re.compile(date_regex_pattern)
181
219
 
182
- # Changing the setting that we set above
220
+ # Update the handler's suffix to include the date format
183
221
  file_handler.suffix = logfile_suffix
184
- file_handler.namer = lambda name: name.replace(file_extension + ".", "") + file_extension
222
+
223
+ file_handler.namer = namer
224
+ # Update the handler's extMatch regex to match the new filename format
185
225
  file_handler.extMatch = logfile_regex_suffix
186
226
 
187
227