everysk-lib 1.10.2__cp312-cp312-win_amd64.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.
- everysk/__init__.py +30 -0
- everysk/_version.py +683 -0
- everysk/api/__init__.py +61 -0
- everysk/api/api_requestor.py +167 -0
- everysk/api/api_resources/__init__.py +23 -0
- everysk/api/api_resources/api_resource.py +371 -0
- everysk/api/api_resources/calculation.py +779 -0
- everysk/api/api_resources/custom_index.py +42 -0
- everysk/api/api_resources/datastore.py +81 -0
- everysk/api/api_resources/file.py +42 -0
- everysk/api/api_resources/market_data.py +223 -0
- everysk/api/api_resources/parser.py +66 -0
- everysk/api/api_resources/portfolio.py +43 -0
- everysk/api/api_resources/private_security.py +42 -0
- everysk/api/api_resources/report.py +65 -0
- everysk/api/api_resources/report_template.py +39 -0
- everysk/api/api_resources/tests.py +115 -0
- everysk/api/api_resources/worker_execution.py +64 -0
- everysk/api/api_resources/workflow.py +65 -0
- everysk/api/api_resources/workflow_execution.py +93 -0
- everysk/api/api_resources/workspace.py +42 -0
- everysk/api/http_client.py +63 -0
- everysk/api/tests.py +32 -0
- everysk/api/utils.py +262 -0
- everysk/config.py +451 -0
- everysk/core/_tests/serialize/test_json.py +336 -0
- everysk/core/_tests/serialize/test_orjson.py +295 -0
- everysk/core/_tests/serialize/test_pickle.py +48 -0
- everysk/core/cloud_function/main.py +78 -0
- everysk/core/cloud_function/tests.py +86 -0
- everysk/core/compress.py +245 -0
- everysk/core/datetime/__init__.py +12 -0
- everysk/core/datetime/calendar.py +144 -0
- everysk/core/datetime/date.py +424 -0
- everysk/core/datetime/date_expression.py +299 -0
- everysk/core/datetime/date_mixin.py +1475 -0
- everysk/core/datetime/date_settings.py +30 -0
- everysk/core/datetime/datetime.py +713 -0
- everysk/core/exceptions.py +435 -0
- everysk/core/fields.py +1176 -0
- everysk/core/firestore.py +555 -0
- everysk/core/fixtures/_settings.py +29 -0
- everysk/core/fixtures/other/_settings.py +18 -0
- everysk/core/fixtures/user_agents.json +88 -0
- everysk/core/http.py +691 -0
- everysk/core/lists.py +92 -0
- everysk/core/log.py +709 -0
- everysk/core/number.py +37 -0
- everysk/core/object.py +1469 -0
- everysk/core/redis.py +1021 -0
- everysk/core/retry.py +51 -0
- everysk/core/serialize.py +674 -0
- everysk/core/sftp.py +414 -0
- everysk/core/signing.py +53 -0
- everysk/core/slack.py +127 -0
- everysk/core/string.py +199 -0
- everysk/core/tests.py +240 -0
- everysk/core/threads.py +199 -0
- everysk/core/undefined.py +70 -0
- everysk/core/unittests.py +73 -0
- everysk/core/workers.py +241 -0
- everysk/sdk/__init__.py +23 -0
- everysk/sdk/base.py +98 -0
- everysk/sdk/brutils/cnpj.py +391 -0
- everysk/sdk/brutils/cnpj_pd.py +129 -0
- everysk/sdk/engines/__init__.py +26 -0
- everysk/sdk/engines/cache.py +185 -0
- everysk/sdk/engines/compliance.py +37 -0
- everysk/sdk/engines/cryptography.py +69 -0
- everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/expression.pyi +55 -0
- everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/helpers.pyi +26 -0
- everysk/sdk/engines/lock.py +120 -0
- everysk/sdk/engines/market_data.py +244 -0
- everysk/sdk/engines/settings.py +19 -0
- everysk/sdk/entities/__init__.py +23 -0
- everysk/sdk/entities/base.py +784 -0
- everysk/sdk/entities/base_list.py +131 -0
- everysk/sdk/entities/custom_index/base.py +209 -0
- everysk/sdk/entities/custom_index/settings.py +29 -0
- everysk/sdk/entities/datastore/base.py +160 -0
- everysk/sdk/entities/datastore/settings.py +17 -0
- everysk/sdk/entities/fields.py +375 -0
- everysk/sdk/entities/file/base.py +215 -0
- everysk/sdk/entities/file/settings.py +63 -0
- everysk/sdk/entities/portfolio/base.py +248 -0
- everysk/sdk/entities/portfolio/securities.py +241 -0
- everysk/sdk/entities/portfolio/security.py +580 -0
- everysk/sdk/entities/portfolio/settings.py +97 -0
- everysk/sdk/entities/private_security/base.py +226 -0
- everysk/sdk/entities/private_security/settings.py +17 -0
- everysk/sdk/entities/query.py +603 -0
- everysk/sdk/entities/report/base.py +214 -0
- everysk/sdk/entities/report/settings.py +23 -0
- everysk/sdk/entities/script.py +310 -0
- everysk/sdk/entities/secrets/base.py +128 -0
- everysk/sdk/entities/secrets/script.py +119 -0
- everysk/sdk/entities/secrets/settings.py +17 -0
- everysk/sdk/entities/settings.py +48 -0
- everysk/sdk/entities/tags.py +174 -0
- everysk/sdk/entities/worker_execution/base.py +307 -0
- everysk/sdk/entities/worker_execution/settings.py +63 -0
- everysk/sdk/entities/workflow_execution/base.py +113 -0
- everysk/sdk/entities/workflow_execution/settings.py +32 -0
- everysk/sdk/entities/workspace/base.py +99 -0
- everysk/sdk/entities/workspace/settings.py +27 -0
- everysk/sdk/settings.py +67 -0
- everysk/sdk/tests.py +105 -0
- everysk/sdk/worker_base.py +47 -0
- everysk/server/__init__.py +9 -0
- everysk/server/applications.py +63 -0
- everysk/server/endpoints.py +516 -0
- everysk/server/example_api.py +69 -0
- everysk/server/middlewares.py +80 -0
- everysk/server/requests.py +62 -0
- everysk/server/responses.py +119 -0
- everysk/server/routing.py +64 -0
- everysk/server/settings.py +36 -0
- everysk/server/tests.py +36 -0
- everysk/settings.py +98 -0
- everysk/sql/__init__.py +9 -0
- everysk/sql/connection.py +232 -0
- everysk/sql/model.py +376 -0
- everysk/sql/query.py +417 -0
- everysk/sql/row_factory.py +63 -0
- everysk/sql/settings.py +49 -0
- everysk/sql/utils.py +129 -0
- everysk/tests.py +23 -0
- everysk/utils.py +81 -0
- everysk/version.py +15 -0
- everysk_lib-1.10.2.dist-info/.gitignore +5 -0
- everysk_lib-1.10.2.dist-info/METADATA +326 -0
- everysk_lib-1.10.2.dist-info/RECORD +137 -0
- everysk_lib-1.10.2.dist-info/WHEEL +5 -0
- everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
- everysk_lib-1.10.2.dist-info/top_level.txt +2 -0
everysk/core/log.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
|
|
4
|
+
#
|
|
5
|
+
# This is an unpublished work containing confidential and proprietary
|
|
6
|
+
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
|
|
7
|
+
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
|
|
8
|
+
#
|
|
9
|
+
###############################################################################
|
|
10
|
+
__all__ = ['Logger', 'LoggerManager']
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import sys
|
|
15
|
+
import traceback as python_traceback
|
|
16
|
+
from contextlib import AbstractContextManager
|
|
17
|
+
from contextvars import ContextVar
|
|
18
|
+
from datetime import datetime as DateTime # noqa: N812
|
|
19
|
+
from threading import Thread
|
|
20
|
+
from types import TracebackType
|
|
21
|
+
from typing import Any
|
|
22
|
+
from zoneinfo import ZoneInfo
|
|
23
|
+
|
|
24
|
+
from everysk.config import settings
|
|
25
|
+
|
|
26
|
+
DEFAULT_APP_SERVER = settings.LOGGING_APP_SERVER
|
|
27
|
+
DEFAULT_TRACE_PARTS = {'version': '', 'trace_id': '', 'span_id': '', 'trace_sample': False}
|
|
28
|
+
HEADER_TRACEPARENT = 'traceparent'
|
|
29
|
+
HEADER_X_CLOUD_TRACE_CONTEXT = 'x-cloud-trace-context'
|
|
30
|
+
LOGGER_MANAGER_CONTEXT_VAR_NAME = 'everysk-lib-log-extra-context-var'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
###############################################################################
|
|
34
|
+
# Private functions Implementation
|
|
35
|
+
###############################################################################
|
|
36
|
+
def _default(obj: Any) -> str | None:
|
|
37
|
+
"""
|
|
38
|
+
Function is used to convert the object to a string inside the json.dumps.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
obj (Any): The object to be converted to a string.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(obj, bytes):
|
|
44
|
+
return obj.decode(json.detect_encoding(obj))
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_gcp_headers(headers: dict | None = None) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Get the headers to be added to the log.
|
|
52
|
+
The order is if the headers are sent in the log function, set in context or get from the default function.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
headers (dict, optional): The headers generated outside the log. Defaults to None.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
dict: Only the headers that are in the list HEADER_TRACEPARENT and HEADER_X_CLOUD_TRACE_CONTEXT.
|
|
59
|
+
"""
|
|
60
|
+
if not headers:
|
|
61
|
+
headers = LoggerManager._extra.get().get('http_headers', {}) # noqa: SLF001
|
|
62
|
+
|
|
63
|
+
return {key: value for key, value in headers.items() if key in (HEADER_TRACEPARENT, HEADER_X_CLOUD_TRACE_CONTEXT)}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_trace_data(headers: dict) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
Get the trace_id, span_id and trace_sample from the headers.
|
|
69
|
+
https://cloud.google.com/trace/docs/trace-context
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
headers (dict): The headers dictionary.
|
|
73
|
+
"""
|
|
74
|
+
trace_parts = DEFAULT_TRACE_PARTS.copy()
|
|
75
|
+
trace = headers.get(HEADER_TRACEPARENT, '')
|
|
76
|
+
if trace:
|
|
77
|
+
_parse_traceparent(trace, trace_parts)
|
|
78
|
+
else:
|
|
79
|
+
trace = headers.get(HEADER_X_CLOUD_TRACE_CONTEXT, '')
|
|
80
|
+
if trace:
|
|
81
|
+
_parse_x_cloud_trace_context(trace, trace_parts)
|
|
82
|
+
|
|
83
|
+
if trace_parts['trace_id']:
|
|
84
|
+
trace_parts['trace_id'] = f'{settings.LOGGING_GOOGLE_CLOUD_TRACE_ID}/{trace_parts["trace_id"]}'
|
|
85
|
+
|
|
86
|
+
return trace_parts
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_traceback() -> str:
|
|
90
|
+
"""Get the traceback of the current exception."""
|
|
91
|
+
result = python_traceback.format_exc()
|
|
92
|
+
# When there is no traceback the result is 'NoneType: None\n'
|
|
93
|
+
# Like this is the most common case we check for equality and return an empty string
|
|
94
|
+
if result == 'NoneType: None\n':
|
|
95
|
+
return ''
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_traceparent(traceparent: str, trace_parts: dict) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Parse the traceparent header and return a dictionary with the version, trace_id, span_id and trace_flags.
|
|
103
|
+
Header example -> traceparent: "00-4bfa9e049143840bef864a7859f2e5df-1c6c592f9e46e3fb-01"
|
|
104
|
+
https://www.w3.org/TR/trace-context/
|
|
105
|
+
From W3C documentation: traceparent: "<version>-<trace-id>-<parent-id>-<trace-flags>"
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
traceparent (str): The traceparent header value.
|
|
109
|
+
trace_parts (dict): The dictionary where the parsed values will be stored.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
trace_parts['version'], trace_parts['trace_id'], trace_parts['span_id'], trace_parts['trace_sample'] = (
|
|
113
|
+
traceparent.split('-')
|
|
114
|
+
)
|
|
115
|
+
trace_parts['trace_sample'] = bool(int(trace_parts['trace_sample']))
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_x_cloud_trace_context(trace_context: str, trace_parts: dict) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Parse the x-cloud-trace-context header and return a dictionary with the trace_id and span_id.
|
|
123
|
+
Header example -> x-cloud-trace-context: "4bfa9e049143840bef864a7859f2e5df/2048109991600514043;o=1"
|
|
124
|
+
From Google documentation: X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=OPTIONS
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
trace_context (str): The x-cloud-trace-context header value.
|
|
128
|
+
trace_parts (dict): The dictionary where the parsed values will be stored.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
trace_parts['trace_id'], trace_parts['span_id'] = trace_context.split('/')
|
|
132
|
+
trace_parts['span_id'], trace_parts['trace_sample'] = trace_parts['span_id'].split(';')
|
|
133
|
+
trace_parts['trace_sample'] = trace_parts['trace_sample'].endswith('1') # pylint: disable=no-member
|
|
134
|
+
except ValueError:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
###############################################################################
|
|
139
|
+
# Formatter Class Implementation
|
|
140
|
+
###############################################################################
|
|
141
|
+
class Formatter(logging.Formatter):
|
|
142
|
+
def _get_default_dict(self, message: str, severity: str) -> dict:
|
|
143
|
+
"""
|
|
144
|
+
Python logging default values.
|
|
145
|
+
Severity levels: CRITICAL , ERROR , WARNING , INFO , DEBUG
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
message (str): The message to be logged.
|
|
149
|
+
severity (str): The severity of the message.
|
|
150
|
+
"""
|
|
151
|
+
return {'message': message, 'severity': severity}
|
|
152
|
+
|
|
153
|
+
def _get_default_extra_dict(
|
|
154
|
+
self, name: str, headers: dict, payload: dict, response: dict, traceback: str, labels: dict
|
|
155
|
+
) -> dict:
|
|
156
|
+
"""
|
|
157
|
+
Get the default extra data dictionary to be added to the log.
|
|
158
|
+
Until now we only have the logName, traceback, http headers and http payload.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
name (str): The name used to create the log.
|
|
162
|
+
headers (dict): A dictionary with the HTTP headers.
|
|
163
|
+
payload (dict): A dictionary with the HTTP payload.
|
|
164
|
+
response (dict): A dictionary with the HTTP response.
|
|
165
|
+
traceback (str): The traceback of the exception.
|
|
166
|
+
labels (dict): A dictionary with the labels to be added to the log.
|
|
167
|
+
"""
|
|
168
|
+
return {
|
|
169
|
+
'logName': name,
|
|
170
|
+
'labels': labels,
|
|
171
|
+
'traceback': traceback,
|
|
172
|
+
'http': {'headers': headers, 'payload': payload, 'response': response},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def _get_default_gcp_dict(self, headers: dict, filename: str, line: int, func_name: str) -> dict:
|
|
176
|
+
"""
|
|
177
|
+
Default Google Cloud Platform dictionary to be added to the log.
|
|
178
|
+
This dictionary has the trace_id, span_id, trace_sample used to chain the logs in the Google Cloud Logging.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
headers (dict): A dictionary with the HTTP headers.
|
|
182
|
+
filename (str): Filename where the log was placed.
|
|
183
|
+
line (int): The line where the log was placed.
|
|
184
|
+
func_name (str): The function name where the log was placed.
|
|
185
|
+
"""
|
|
186
|
+
trace = _get_trace_data(headers)
|
|
187
|
+
return {
|
|
188
|
+
'logging.googleapis.com/trace': trace['trace_id'],
|
|
189
|
+
'logging.googleapis.com/spanId': trace['span_id'],
|
|
190
|
+
'logging.googleapis.com/trace_sampled': trace['trace_sample'],
|
|
191
|
+
'logging.googleapis.com/sourceLocation': {'file': filename, 'line': line, 'function': func_name},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def _get_result_dict(self, record: logging.LogRecord) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Convert the log record to a dictionary.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
record (logging.LogRecord): Record object with all the information about the log.
|
|
200
|
+
"""
|
|
201
|
+
result = self._get_default_dict(message=record.getMessage(), severity=record.levelname)
|
|
202
|
+
result.update(
|
|
203
|
+
self._get_default_extra_dict(
|
|
204
|
+
name=record.name,
|
|
205
|
+
headers=getattr(record, 'http_headers', {}),
|
|
206
|
+
payload=getattr(record, 'http_payload', {}),
|
|
207
|
+
response=getattr(record, 'http_response', {}),
|
|
208
|
+
traceback=getattr(record, 'traceback', ''),
|
|
209
|
+
labels=getattr(record, 'labels', {}),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
result.update(
|
|
213
|
+
self._get_default_gcp_dict(
|
|
214
|
+
headers=result['http']['headers'],
|
|
215
|
+
filename=record.pathname,
|
|
216
|
+
line=record.lineno,
|
|
217
|
+
func_name=record.funcName,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802
|
|
223
|
+
"""
|
|
224
|
+
Format the message to be displayed in the terminal or Google Log Explorer.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
record (logging.LogRecord): Record object with all the information about the log.
|
|
228
|
+
"""
|
|
229
|
+
result = self._get_result_dict(record)
|
|
230
|
+
return json.dumps(result, default=_default)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
###############################################################################
|
|
234
|
+
# LoggerManager Class Implementation
|
|
235
|
+
###############################################################################
|
|
236
|
+
class LoggerManager(AbstractContextManager):
|
|
237
|
+
## Private attributes
|
|
238
|
+
_extra: ContextVar = ContextVar(LOGGER_MANAGER_CONTEXT_VAR_NAME, default={}) # noqa: B039
|
|
239
|
+
_old_value: dict = None
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
http_headers: dict | None = None,
|
|
244
|
+
http_payload: dict | None = None,
|
|
245
|
+
http_response: dict | None = None,
|
|
246
|
+
labels: dict | None = None,
|
|
247
|
+
stacklevel: int | None = None,
|
|
248
|
+
traceback: str | None = None,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Context class to create a context manager for the Logger object.
|
|
252
|
+
This class is used to add extra information to the log.
|
|
253
|
+
This manager together with everysk.core.threads will keep the context inside the thread.
|
|
254
|
+
If you use python threads you need to pass the context inside the thread.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
http_headers (dict, optional): The HTTP headers to be added to the log. Defaults to None.
|
|
258
|
+
http_payload (dict, optional): The HTTP payload to be added to the log. Defaults to None.
|
|
259
|
+
http_response (dict, optional): The HTTP response to be added to the log. Defaults to None.
|
|
260
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to None.
|
|
261
|
+
traceback (str, optional): The traceback to be added to the log. Defaults to None.
|
|
262
|
+
labels (dict, optional): A dictionary with the labels to be added to the log. Defaults to None.
|
|
263
|
+
"""
|
|
264
|
+
self.http_headers = http_headers
|
|
265
|
+
self.http_payload = http_payload
|
|
266
|
+
self.http_response = http_response
|
|
267
|
+
self.labels = labels
|
|
268
|
+
self.stacklevel = stacklevel
|
|
269
|
+
self.traceback = traceback
|
|
270
|
+
|
|
271
|
+
# Save this value to be used in the __exit__ method to restore the context
|
|
272
|
+
self._old_value = self._extra.get().copy()
|
|
273
|
+
|
|
274
|
+
def __enter__(self) -> 'LoggerManager':
|
|
275
|
+
"""
|
|
276
|
+
Method to be executed when the context manager is created and always return self.
|
|
277
|
+
This method is always executed even if "LoggerManager as some_var:" is not used.
|
|
278
|
+
"""
|
|
279
|
+
# First we get the original context
|
|
280
|
+
context: dict = self._extra.get()
|
|
281
|
+
|
|
282
|
+
if self.http_headers is not None:
|
|
283
|
+
context['http_headers'] = self.http_headers
|
|
284
|
+
|
|
285
|
+
if self.http_payload is not None:
|
|
286
|
+
context['http_payload'] = self.http_payload
|
|
287
|
+
|
|
288
|
+
if self.http_response is not None:
|
|
289
|
+
context['http_response'] = self.http_response
|
|
290
|
+
|
|
291
|
+
if self.labels is not None:
|
|
292
|
+
context['labels'] = self.labels
|
|
293
|
+
|
|
294
|
+
if self.stacklevel is not None:
|
|
295
|
+
context['stacklevel'] = self.stacklevel
|
|
296
|
+
|
|
297
|
+
if self.traceback is not None:
|
|
298
|
+
context['traceback'] = self.traceback
|
|
299
|
+
|
|
300
|
+
# Finally we store the new context
|
|
301
|
+
self._extra.set(context)
|
|
302
|
+
|
|
303
|
+
return self
|
|
304
|
+
|
|
305
|
+
def __exit__(
|
|
306
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
307
|
+
) -> bool | None:
|
|
308
|
+
"""
|
|
309
|
+
https://docs.python.org/3/library/stdtypes.html#contextmanager.__exit__
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
bool | None: If return is False any exception will be raised.
|
|
313
|
+
"""
|
|
314
|
+
# Restore the context to the original value
|
|
315
|
+
self._extra.set(self._old_value)
|
|
316
|
+
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def reset(cls) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Reset the context to the default value.
|
|
323
|
+
This method is used to avoid shared values between requests in server.endpoints module.
|
|
324
|
+
"""
|
|
325
|
+
cls._extra = ContextVar(LOGGER_MANAGER_CONTEXT_VAR_NAME, default={}) # noqa: B039
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
###############################################################################
|
|
329
|
+
# Logger Class Implementation
|
|
330
|
+
###############################################################################
|
|
331
|
+
class Logger:
|
|
332
|
+
## Private attributes
|
|
333
|
+
# This needs to be initialized with an empty set because it will be a global list
|
|
334
|
+
_deprecated_hash: set[str] = set() # noqa: RUF012
|
|
335
|
+
_default_stacklevel: int = 3 # 3 -> The place where the log.method was placed; 2 -> This file; 1 -> Python logger
|
|
336
|
+
_log: logging.Logger = None
|
|
337
|
+
_slack_timer: DateTime = None
|
|
338
|
+
|
|
339
|
+
## Public attributes
|
|
340
|
+
name: str = None
|
|
341
|
+
stacklevel: int = None
|
|
342
|
+
|
|
343
|
+
## Private methods
|
|
344
|
+
def __init__(self, name: str, stacklevel: int | None = None) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Logger class used to send messages to STDOUT or Google CLoud Logging.
|
|
347
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
348
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
349
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
name (str, optional): The name of the log. Defaults to "root".
|
|
353
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
>>> from everysk.core.log import Logger
|
|
357
|
+
>>> log = Logger(name='log-test')
|
|
358
|
+
>>> log.debug('Test')
|
|
359
|
+
2024-02-07 12:49:10,640 - DEBUG - {} - Test
|
|
360
|
+
"""
|
|
361
|
+
if name == 'root':
|
|
362
|
+
raise ValueError('The name of the log could not be "root".')
|
|
363
|
+
self.name = name
|
|
364
|
+
if stacklevel is not None and stacklevel > 0:
|
|
365
|
+
self.stacklevel = stacklevel
|
|
366
|
+
|
|
367
|
+
self._log = self._get_python_logger()
|
|
368
|
+
|
|
369
|
+
def _get_extra_data(self, extra: dict, level: int | None = None) -> dict:
|
|
370
|
+
"""
|
|
371
|
+
Get the extra data to be added to the log.
|
|
372
|
+
We check if the extra was sent in the function, if not we check if it
|
|
373
|
+
was set in the LoggerManager context, if not we check if we have a
|
|
374
|
+
default function to get the data.
|
|
375
|
+
We only set the payload
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
extra (dict): The extra data sent in the function.
|
|
379
|
+
level (int): The level of the log.
|
|
380
|
+
"""
|
|
381
|
+
http_headers = self._get_http_headers(extra.pop('http_headers', {}))
|
|
382
|
+
if http_headers:
|
|
383
|
+
extra['http_headers'] = http_headers
|
|
384
|
+
|
|
385
|
+
http_payload = self._get_http_payload(extra.pop('http_payload', {}), level)
|
|
386
|
+
if http_payload:
|
|
387
|
+
extra['http_payload'] = http_payload
|
|
388
|
+
|
|
389
|
+
http_response = self._get_http_response(extra.pop('http_response', {}))
|
|
390
|
+
if http_response:
|
|
391
|
+
extra['http_response'] = http_response
|
|
392
|
+
|
|
393
|
+
# https://everysk.atlassian.net/browse/COD-6151
|
|
394
|
+
# Because we use labels in the format for the normal log, this key always will be present
|
|
395
|
+
labels = extra.pop('labels', {})
|
|
396
|
+
if not labels:
|
|
397
|
+
# If we don't receive labels as param in log functions we get from the context
|
|
398
|
+
labels = LoggerManager._extra.get().get('labels', {}) # noqa: SLF001
|
|
399
|
+
|
|
400
|
+
# Labels are always present in the log even if it is an empty dictionary
|
|
401
|
+
extra['labels'] = labels
|
|
402
|
+
|
|
403
|
+
traceback = extra.pop('traceback', '')
|
|
404
|
+
if not traceback:
|
|
405
|
+
# If we don't receive traceback as param in log functions we get from the context
|
|
406
|
+
# or from the result of traceback module
|
|
407
|
+
traceback = LoggerManager._extra.get().get('traceback', '') or _get_traceback() # noqa: SLF001
|
|
408
|
+
if traceback:
|
|
409
|
+
extra['traceback'] = traceback
|
|
410
|
+
|
|
411
|
+
return extra
|
|
412
|
+
|
|
413
|
+
def _get_http_headers(self, http_headers: dict) -> dict:
|
|
414
|
+
"""
|
|
415
|
+
Get the http headers to be added to the log.
|
|
416
|
+
The order is if the headers are sent in the log function, set in context or get from the default function.
|
|
417
|
+
We only search for the payload if the level is ERROR or CRITICAL.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
http_headers (dict): The HTTP headers sent in the log function.
|
|
421
|
+
level (int): The level of the log.
|
|
422
|
+
"""
|
|
423
|
+
return _get_gcp_headers(http_headers)
|
|
424
|
+
|
|
425
|
+
def _get_http_payload(self, http_payload: dict, level: int | None = None) -> dict:
|
|
426
|
+
"""
|
|
427
|
+
Get the http payload to be added to the log.
|
|
428
|
+
The order is if the payload are sent in the log function, set in context or get from the default function.
|
|
429
|
+
We only search for the payload if the level is ERROR or CRITICAL.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
http_payload (dict): The HTTP payload sent in the log function.
|
|
433
|
+
level (int): The level of the log.
|
|
434
|
+
"""
|
|
435
|
+
if not http_payload and level in (logging.ERROR, logging.CRITICAL):
|
|
436
|
+
http_payload = LoggerManager._extra.get().get('http_payload', {}) # noqa: SLF001
|
|
437
|
+
|
|
438
|
+
return http_payload
|
|
439
|
+
|
|
440
|
+
def _get_http_response(self, http_response: dict) -> dict:
|
|
441
|
+
"""
|
|
442
|
+
Get the http response to be added to the log.
|
|
443
|
+
The order is if the response are sent in the log function, set in context or get from the default function.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
http_response (dict): The HTTP response sent in the log function.
|
|
447
|
+
"""
|
|
448
|
+
if not http_response:
|
|
449
|
+
http_response = LoggerManager._extra.get().get('http_response', {}) # noqa: SLF001
|
|
450
|
+
|
|
451
|
+
return http_response
|
|
452
|
+
|
|
453
|
+
def _get_python_logger(self) -> logging.Logger:
|
|
454
|
+
"""
|
|
455
|
+
Method that creates/get the Python Logger object and attach the correct handler.
|
|
456
|
+
The default handler will be the stdout.
|
|
457
|
+
"""
|
|
458
|
+
if self._log is not None:
|
|
459
|
+
return self._log
|
|
460
|
+
|
|
461
|
+
# Create the log
|
|
462
|
+
log = logging.getLogger(self.name)
|
|
463
|
+
log.setLevel(logging.DEBUG)
|
|
464
|
+
log.propagate = False # Don't pass message to others loggers
|
|
465
|
+
|
|
466
|
+
# We should only have one handler per log name
|
|
467
|
+
if not hasattr(log, 'handler'):
|
|
468
|
+
log.handler = logging.StreamHandler(stream=sys.stdout)
|
|
469
|
+
|
|
470
|
+
# Set the format that the message is displayed
|
|
471
|
+
if settings.LOGGING_JSON:
|
|
472
|
+
log.handler.setFormatter(Formatter())
|
|
473
|
+
else:
|
|
474
|
+
log.handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(labels)s - %(message)s'))
|
|
475
|
+
|
|
476
|
+
# Add the handler inside the log
|
|
477
|
+
log.addHandler(log.handler)
|
|
478
|
+
|
|
479
|
+
# Set the level that the handler will be using.
|
|
480
|
+
log.handler.setLevel(logging.DEBUG)
|
|
481
|
+
|
|
482
|
+
return log
|
|
483
|
+
|
|
484
|
+
def _get_stacklevel(self, stacklevel: int) -> int:
|
|
485
|
+
"""
|
|
486
|
+
Get the stacklevel that was set in the function
|
|
487
|
+
or in the LoggerManager context
|
|
488
|
+
or set in the Logger object
|
|
489
|
+
or use the default stacklevel.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
stacklevel (int): The stacklevel to be used on the log.
|
|
493
|
+
"""
|
|
494
|
+
if not stacklevel:
|
|
495
|
+
stacklevel = LoggerManager._extra.get().get('stacklevel', {}) # noqa: SLF001
|
|
496
|
+
if not stacklevel:
|
|
497
|
+
stacklevel = self.stacklevel or self._default_stacklevel
|
|
498
|
+
|
|
499
|
+
return stacklevel
|
|
500
|
+
|
|
501
|
+
def _send_to_log(
|
|
502
|
+
self, level: int, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None
|
|
503
|
+
) -> None:
|
|
504
|
+
"""
|
|
505
|
+
Send the message to the python logger using the correct level.
|
|
506
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
507
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
508
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
level (int): The level of the message.
|
|
512
|
+
msg (str): The message to log.
|
|
513
|
+
*args (tuple): The arguments to be used on the message.
|
|
514
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
515
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
516
|
+
"""
|
|
517
|
+
stacklevel = self._get_stacklevel(stacklevel)
|
|
518
|
+
extra = self._get_extra_data(extra or {}, level)
|
|
519
|
+
self._log.log(level, msg, *args, extra=extra, stacklevel=stacklevel)
|
|
520
|
+
|
|
521
|
+
def _show_deprecated(self, _id: str, *, show_once: bool) -> bool:
|
|
522
|
+
"""
|
|
523
|
+
If show_once is False this always return True, otherwise
|
|
524
|
+
checks if this _id is in the self._deprecated_hash set.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
_id (str): String that will be stored to be checked later
|
|
528
|
+
show_once (bool): Flag to store or not the id.
|
|
529
|
+
"""
|
|
530
|
+
if show_once:
|
|
531
|
+
if _id in self._deprecated_hash:
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
self._deprecated_hash.add(_id)
|
|
535
|
+
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
## Public methods
|
|
539
|
+
def critical(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
540
|
+
"""
|
|
541
|
+
Log a message with severity CRITICAL on this logger.
|
|
542
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
543
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
544
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
msg (str): The message to log.
|
|
548
|
+
*args (tuple): The arguments to be used on the message.
|
|
549
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
550
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
551
|
+
"""
|
|
552
|
+
self._send_to_log(logging.CRITICAL, msg, *args, extra=extra, stacklevel=stacklevel)
|
|
553
|
+
|
|
554
|
+
def debug(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
555
|
+
"""
|
|
556
|
+
Log a message with severity DEBUG on this logger.
|
|
557
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
558
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
559
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
msg (str): The message to log.
|
|
563
|
+
*args (tuple): The arguments to be used on the message.
|
|
564
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
565
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
566
|
+
"""
|
|
567
|
+
self._send_to_log(logging.DEBUG, msg, *args, extra=extra, stacklevel=stacklevel)
|
|
568
|
+
|
|
569
|
+
def deprecated( # noqa: D417
|
|
570
|
+
self, msg: str, *args, show_once: bool = True, extra: dict | None = None, stacklevel: int | None = None
|
|
571
|
+
) -> None:
|
|
572
|
+
"""
|
|
573
|
+
Shows a DeprecationWarning message with severity 'WARNING'.
|
|
574
|
+
If show_once is True, then the message will be showed only once.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
msg (str): The message that must be showed.
|
|
578
|
+
show_once (bool, optional): If the message must be showed only once. Defaults to True.
|
|
579
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
580
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
581
|
+
"""
|
|
582
|
+
_id = hash(f'{msg}, {args}')
|
|
583
|
+
if self._show_deprecated(_id=_id, show_once=show_once):
|
|
584
|
+
msg = f'DeprecationWarning: {msg}'
|
|
585
|
+
self.warning(msg, *args, extra=extra, stacklevel=stacklevel)
|
|
586
|
+
|
|
587
|
+
def error(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
588
|
+
"""
|
|
589
|
+
Log a message with severity ERROR on this logger.
|
|
590
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
591
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
592
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
msg (str): The message to log.
|
|
596
|
+
*args (tuple): The arguments to be used on the message.
|
|
597
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
598
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
599
|
+
"""
|
|
600
|
+
self._send_to_log(logging.ERROR, msg, *args, extra=extra, stacklevel=stacklevel)
|
|
601
|
+
|
|
602
|
+
def exception(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
603
|
+
"""
|
|
604
|
+
Log a message with severity ERROR on this logger.
|
|
605
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
606
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
607
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
msg (str): The message to log.
|
|
611
|
+
*args (tuple): The arguments to be used on the message.
|
|
612
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
613
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
614
|
+
"""
|
|
615
|
+
self.error(msg, *args, extra=extra, stacklevel=stacklevel)
|
|
616
|
+
|
|
617
|
+
def info(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
618
|
+
"""
|
|
619
|
+
Log a message with severity INFO on this logger.
|
|
620
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
621
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
622
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
msg (str): The message to log.
|
|
626
|
+
*args (tuple): The arguments to be used on the message.
|
|
627
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
628
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
629
|
+
"""
|
|
630
|
+
self._send_to_log(logging.INFO, msg, *args, extra=extra, stacklevel=stacklevel)
|
|
631
|
+
|
|
632
|
+
def _can_send_slack(self) -> bool:
|
|
633
|
+
"""
|
|
634
|
+
Check if the slack message can be sent.
|
|
635
|
+
We keep a track of the last message sent to avoid sending too many messages in less than 3 seconds.
|
|
636
|
+
"""
|
|
637
|
+
# We only send the message if we are in PROD and not in unittest
|
|
638
|
+
if settings.PROFILE != 'PROD' or 'unittest' in sys.modules:
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
# We check if the last message was sent in less than 3 seconds to avoid sending too many messages
|
|
642
|
+
now = DateTime.now(ZoneInfo('UTC')) # pylint: disable=no-member
|
|
643
|
+
if self._slack_timer and (now - self._slack_timer).seconds < 3: # noqa: PLR2004
|
|
644
|
+
return False
|
|
645
|
+
|
|
646
|
+
self._slack_timer = now
|
|
647
|
+
return True
|
|
648
|
+
|
|
649
|
+
def slack(
|
|
650
|
+
self,
|
|
651
|
+
title: str,
|
|
652
|
+
message: str,
|
|
653
|
+
color: str,
|
|
654
|
+
url: str | None = None,
|
|
655
|
+
extra: dict | None = None,
|
|
656
|
+
stacklevel: int | None = None,
|
|
657
|
+
) -> None:
|
|
658
|
+
"""
|
|
659
|
+
Send a message to a Slack channel using Slack WebHooks.
|
|
660
|
+
https://api.slack.com/messaging/webhooks
|
|
661
|
+
The same message will be sent to the default log too:
|
|
662
|
+
danger -> error
|
|
663
|
+
success -> info
|
|
664
|
+
warning -> warning
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
title (str): The title of the message.
|
|
668
|
+
message (str): The body of the message.
|
|
669
|
+
color (str): 'danger' | 'success' | 'warning'
|
|
670
|
+
url (str, optional): The slack webhook url. Defaults to settings.SLACK_URL.
|
|
671
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
672
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
673
|
+
"""
|
|
674
|
+
if url is None:
|
|
675
|
+
url = settings.SLACK_URL
|
|
676
|
+
|
|
677
|
+
# We send the message only if url is set and we are in PROD and not in unittest
|
|
678
|
+
if url and self._can_send_slack():
|
|
679
|
+
# The import must be here to avoid circular import inside http module
|
|
680
|
+
from everysk.core.slack import Slack # noqa: PLC0415
|
|
681
|
+
|
|
682
|
+
client = Slack(title=title, message=message, color=color, url=url)
|
|
683
|
+
# This will send the message to Slack without block the request
|
|
684
|
+
Thread(target=client.send).start()
|
|
685
|
+
|
|
686
|
+
log_message = f'Slack message: {title} -> {message}'
|
|
687
|
+
if color == 'danger':
|
|
688
|
+
self.error(log_message, extra=extra, stacklevel=stacklevel)
|
|
689
|
+
|
|
690
|
+
elif color == 'success':
|
|
691
|
+
self.info(log_message, extra=extra, stacklevel=stacklevel)
|
|
692
|
+
|
|
693
|
+
elif color == 'warning':
|
|
694
|
+
self.warning(log_message, extra=extra, stacklevel=stacklevel)
|
|
695
|
+
|
|
696
|
+
def warning(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
|
|
697
|
+
"""
|
|
698
|
+
Log a message with severity WARNING on this logger.
|
|
699
|
+
Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
|
|
700
|
+
logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
|
|
701
|
+
another file for the log object maybe you need to change to lvl 3.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
msg (str): The message to log.
|
|
705
|
+
*args (tuple): The arguments to be used on the message.
|
|
706
|
+
extra (dict, optional): Extra information to be added to the log. Defaults to None.
|
|
707
|
+
stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
|
|
708
|
+
"""
|
|
709
|
+
self._send_to_log(logging.WARNING, msg, *args, extra=extra, stacklevel=stacklevel)
|