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.
- atomicshop/__init__.py +1 -1
- atomicshop/datetimes.py +3 -2
- atomicshop/etws/providers.py +18 -2
- atomicshop/etws/sessions.py +1 -1
- atomicshop/etws/trace.py +0 -1
- atomicshop/file_io/csvs.py +1 -1
- atomicshop/mitm/initialize_engines.py +8 -2
- atomicshop/mitm/initialize_mitm_server.py +24 -7
- atomicshop/mitm/statistic_analyzer.py +376 -3
- atomicshop/wrappers/ctyping/etw_winapi/const.py +130 -20
- atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py +141 -5
- atomicshop/wrappers/loggingw/formatters.py +66 -40
- atomicshop/wrappers/loggingw/handlers.py +51 -11
- atomicshop/wrappers/loggingw/loggingw.py +180 -167
- atomicshop/wrappers/loggingw/reading.py +20 -19
- {atomicshop-2.14.3.dist-info → atomicshop-2.14.5.dist-info}/METADATA +1 -1
- {atomicshop-2.14.3.dist-info → atomicshop-2.14.5.dist-info}/RECORD +20 -20
- {atomicshop-2.14.3.dist-info → atomicshop-2.14.5.dist-info}/LICENSE.txt +0 -0
- {atomicshop-2.14.3.dist-info → atomicshop-2.14.5.dist-info}/WHEEL +0 -0
- {atomicshop-2.14.3.dist-info → atomicshop-2.14.5.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
("
|
|
84
|
-
("
|
|
85
|
-
("
|
|
86
|
-
("
|
|
87
|
-
("
|
|
88
|
-
("
|
|
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
|
-
("
|
|
91
|
-
("
|
|
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
|
-
|
|
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",
|
|
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(
|
|
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
|
-
:
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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,
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
220
|
+
# Update the handler's suffix to include the date format
|
|
183
221
|
file_handler.suffix = logfile_suffix
|
|
184
|
-
|
|
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
|
|