ipulse-shared-core-ftredge 2.54__tar.gz → 2.56__tar.gz
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 ipulse-shared-core-ftredge might be problematic. Click here for more details.
- {ipulse_shared_core_ftredge-2.54/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-2.56}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/setup.py +1 -1
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/enums/__init__.py +1 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/enums/enums_common_utils.py +42 -17
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/utils_common.py +206 -59
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/utils_gcp.py +6 -9
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/README.md +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/enums/enums_data_eng.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/enums/enums_module_fincore.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/enums/enums_modules.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/audit_log_firestore.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/organisation.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/pulse_enums.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/tests/__init__.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/tests/test.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge/utils_templates_and_schemas.py +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
- {ipulse_shared_core_ftredge-2.54 → ipulse_shared_core_ftredge-2.56}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.56
|
|
4
4
|
Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
|
|
5
5
|
Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
|
|
6
6
|
Author: Russlan Ramdowar
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name='ipulse_shared_core_ftredge',
|
|
6
|
-
version='2.
|
|
6
|
+
version='2.56',
|
|
7
7
|
package_dir={'': 'src'}, # Specify the source directory
|
|
8
8
|
packages=find_packages(where='src'), # Look for packages in 'src'
|
|
9
9
|
install_requires=[
|
|
@@ -5,7 +5,7 @@ from .utils_gcp import (setup_gcp_logger_and_error_report,
|
|
|
5
5
|
write_csv_to_gcs, write_json_to_gcs)
|
|
6
6
|
from .utils_templates_and_schemas import (create_bigquery_schema_from_json,
|
|
7
7
|
check_format_against_schema_template)
|
|
8
|
-
from .utils_common import (ContextLog,
|
|
8
|
+
from .utils_common import (ContextLog, Pipelinemon)
|
|
9
9
|
|
|
10
10
|
from .enums import (TargetLogs, LogLevel, Unit, Frequency,
|
|
11
11
|
Module, SubModule, BaseDataCategory,
|
|
@@ -2,10 +2,28 @@
|
|
|
2
2
|
# pylint: disable=missing-module-docstring
|
|
3
3
|
# pylint: disable=missing-function-docstring
|
|
4
4
|
# pylint: disable=missing-class-docstring
|
|
5
|
+
# pylint: disable=line-too-long
|
|
5
6
|
|
|
6
7
|
from enum import Enum
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class SystemsImpacted(Enum):
|
|
11
|
+
NO = "__no"
|
|
12
|
+
YES = "__yes"
|
|
13
|
+
INVESTIGATE = "__investigate"
|
|
14
|
+
MULTIPLE = "__multiple"
|
|
15
|
+
DB = "db"
|
|
16
|
+
BQ_TABLE= "bq_table"
|
|
17
|
+
BQ_TABLES = "bq_tables"
|
|
18
|
+
GCS_BUCKET = "gcs_bucket"
|
|
19
|
+
GCS_BUCKETS = "gcs_buckets"
|
|
20
|
+
GCS_BUCKET_FILE = "gcs_bucket_file"
|
|
21
|
+
GCS_BUCKET_FILES = "gcs_bucket_files"
|
|
22
|
+
API = "api"
|
|
23
|
+
APIS = "apis"
|
|
24
|
+
LOCAL_FILE = "local_file"
|
|
25
|
+
LOCAL_FILES = "local_files"
|
|
26
|
+
|
|
9
27
|
class TargetLogs(Enum):
|
|
10
28
|
MIXED="mixed_logs"
|
|
11
29
|
SUCCESSES = "success_logs"
|
|
@@ -15,22 +33,27 @@ class TargetLogs(Enum):
|
|
|
15
33
|
WARNINGS_AND_ERRORS = "warn_n_err_logs"
|
|
16
34
|
ERRORS = "error_logs"
|
|
17
35
|
|
|
18
|
-
|
|
19
36
|
class LogLevel(Enum):
|
|
20
37
|
"""
|
|
21
38
|
Standardized notice levels for data engineering pipelines,
|
|
22
39
|
designed for easy analysis and identification of manual
|
|
23
40
|
intervention needs.
|
|
24
41
|
"""
|
|
25
|
-
DEBUG =
|
|
42
|
+
DEBUG = 10 # Detailed debug information (for development/troubleshooting)
|
|
43
|
+
|
|
44
|
+
INFO = 100
|
|
45
|
+
INFO_PERSISTNACE_COMPLETE= 101
|
|
46
|
+
INFO_UPDATE_COMPLETE = 102
|
|
47
|
+
INFO_DELETE_COMPLETE = 103
|
|
26
48
|
|
|
27
|
-
INFO = 200
|
|
28
49
|
SUCCESS = 201
|
|
50
|
+
SUCCESS_WITH_NOTICES = 211
|
|
51
|
+
SUCCESS_WITH_WARNINGS = 212
|
|
29
52
|
|
|
30
53
|
NOTICE = 300 # Maybe same file or data already fully or partially exists
|
|
31
54
|
NOTICE_ALREADY_EXISTS = 301 # Data already exists, no action required
|
|
32
55
|
NOTICE_PARTIAL_EXISTS = 302 # Partial data exists, no action required
|
|
33
|
-
|
|
56
|
+
NOTICE_ACTION_CANCELLED = 303 # Data processing cancelled, no action required
|
|
34
57
|
|
|
35
58
|
# Warnings indicate potential issues that might require attention:
|
|
36
59
|
WARNING = 400 # General warning, no immediate action required
|
|
@@ -40,18 +63,22 @@ class LogLevel(Enum):
|
|
|
40
63
|
WARNING_FIX_REQUIRED = 404 # Action required, pipeline can likely continue
|
|
41
64
|
|
|
42
65
|
ERROR = 500 # General error, no immediate action required
|
|
43
|
-
# Errors indicate a problem that disrupts normal pipeline execution:
|
|
44
|
-
ERROR_EXCEPTION_REDO = 501
|
|
45
|
-
ERROR_CUSTOM_REDO = 502 # Temporary error, automatic retry likely to succeed
|
|
46
|
-
|
|
47
|
-
ERROR_EXCEPTION_INVESTIGATE = 601 # Exception occured after some data was likely persisted (e.g., to GCS or BQ)
|
|
48
|
-
ERROR_CUSTOM_INVESTIGATE= 602
|
|
49
|
-
ERROR_EXCEPTION_PERSTISTANCE = 603 # Exception occured after data was persisted (e.g., to GCS or BQ)
|
|
50
|
-
ERROR_CUSTOM_PERSTISTANCE = 604
|
|
51
66
|
|
|
67
|
+
ERROR_EXCEPTION = 501
|
|
68
|
+
ERROR_CUSTOM = 502 # Temporary error, automatic retry likely to succeed
|
|
69
|
+
ERROR_OPERATION_PARTIALLY_FAILED = 511 # Partial or full failure, manual intervention required
|
|
70
|
+
ERROR_OPERATION_FAILED = 512 # Operation failed, manual intervention required
|
|
71
|
+
ERORR_OPERATION_WITH_WARNINGS = 513 # Partial or full failure, manual intervention required
|
|
72
|
+
ERORR_OPERATION_WITH_ERRORS = 514 # Partial or full failure, manual intervention required
|
|
73
|
+
ERORR_OPERATION_WITH_WARNINGS_OR_ERRORS = 515 # Partial or full failure, manual intervention required
|
|
74
|
+
|
|
75
|
+
ERROR_THRESHOLD_REACHED = 551
|
|
76
|
+
ERROR_PIPELINE_THRESHOLD_REACHED = 552 # Error due to threshold reached, no immediate action required
|
|
77
|
+
ERROR_SUBTHRESHOLD_REACHED = 553 # Error due to threshold reached, no immediate action required
|
|
78
|
+
ERROR_DATA_QUALITY_THRESHOLD_REACHED = 554 # Error due to threshold reached, no immediate action required
|
|
52
79
|
# Critical errors indicate severe failures requiring immediate attention:
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
CRITICAL=600 # General critical error, requires immediate action
|
|
81
|
+
CRITICAL_SYSTEM_FAILURE = 601 # System-level failure (e.g., infrastructure, stackoverflow ), requires immediate action
|
|
55
82
|
|
|
56
83
|
UNKNOWN=1001 # Unknown error, should not be used in normal operation
|
|
57
84
|
|
|
@@ -63,8 +90,6 @@ class LogStatus(Enum):
|
|
|
63
90
|
RESOLVED = "resolved"
|
|
64
91
|
IGNORED = "ignored"
|
|
65
92
|
CANCELLED = "cancelled"
|
|
66
|
-
|
|
67
|
-
|
|
68
93
|
|
|
69
94
|
### Exception during full exection, partially saved
|
|
70
95
|
# Exception during ensemble pipeline; modifications collected in local object , nothing persisted
|
|
@@ -143,4 +168,4 @@ class Frequency(Enum):
|
|
|
143
168
|
THREE_M="3m"
|
|
144
169
|
SIX_M="6m"
|
|
145
170
|
ONE_Y="1y"
|
|
146
|
-
THREE_Y="3y"
|
|
171
|
+
THREE_Y="3y"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# pylint: disable=logging-fstring-interpolation
|
|
4
4
|
# pylint: disable=line-too-long
|
|
5
5
|
# pylint: disable=missing-class-docstring
|
|
6
|
+
# pylint: disable=broad-exception-caught
|
|
6
7
|
import traceback
|
|
7
8
|
import json
|
|
8
9
|
import uuid
|
|
@@ -17,11 +18,14 @@ from ipulse_shared_core_ftredge.utils_gcp import write_json_to_gcs
|
|
|
17
18
|
# ["data_import","data_quality", "data_processing","data_general","data_persistance","metadata_quality", "metadata_processing", "metadata_persistance","metadata_general"]
|
|
18
19
|
|
|
19
20
|
class ContextLog:
|
|
20
|
-
|
|
21
|
+
MAX_FIELD_LINES = 26 # Define the maximum number of traceback lines to include
|
|
22
|
+
MAX_FIELD_LENGTH=10000
|
|
23
|
+
|
|
21
24
|
def __init__(self, level: LogLevel, base_context: str = None, collector_id: str = None,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
context: str = None, description: str = None,
|
|
26
|
+
e: Exception = None, e_type: str = None, e_message: str = None, e_traceback: str = None,
|
|
27
|
+
log_status: LogStatus = LogStatus.OPEN, subject: str = None, systems_impacted: List[str] = None
|
|
28
|
+
):
|
|
25
29
|
if e is not None:
|
|
26
30
|
e_type = type(e).__name__ if e_type is None else e_type
|
|
27
31
|
e_message = str(e) if e_message is None else e_message
|
|
@@ -34,19 +38,60 @@ class ContextLog:
|
|
|
34
38
|
self.description = description
|
|
35
39
|
self._base_context = base_context
|
|
36
40
|
self._context = context
|
|
41
|
+
self._systems_impacted = systems_impacted if systems_impacted else []
|
|
37
42
|
self.collector_id = collector_id
|
|
38
43
|
self.exception_type = e_type
|
|
39
44
|
self.exception_message = e_message
|
|
40
|
-
self.exception_traceback =
|
|
45
|
+
self.exception_traceback = e_traceback
|
|
41
46
|
self.log_status = log_status
|
|
42
47
|
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
43
48
|
|
|
49
|
+
@property
|
|
50
|
+
def base_context(self):
|
|
51
|
+
return self._base_context
|
|
52
|
+
|
|
53
|
+
@base_context.setter
|
|
54
|
+
def base_context(self, value):
|
|
55
|
+
self._base_context = value
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def context(self):
|
|
59
|
+
return self._context
|
|
60
|
+
|
|
61
|
+
@context.setter
|
|
62
|
+
def context(self, value):
|
|
63
|
+
self._context = value
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def systems_impacted(self):
|
|
67
|
+
return self._systems_impacted
|
|
68
|
+
|
|
69
|
+
@systems_impacted.setter
|
|
70
|
+
def systems_impacted(self, list_of_si: List[str]):
|
|
71
|
+
self._systems_impacted = list_of_si
|
|
72
|
+
|
|
73
|
+
def add_system_impacted(self, system_impacted: str):
|
|
74
|
+
if self._systems_impacted is None:
|
|
75
|
+
self._systems_impacted = []
|
|
76
|
+
self._systems_impacted.append(system_impacted)
|
|
77
|
+
|
|
78
|
+
def remove_system_impacted(self, system_impacted: str):
|
|
79
|
+
if self._systems_impacted is not None:
|
|
80
|
+
self._systems_impacted.remove(system_impacted)
|
|
81
|
+
|
|
82
|
+
def clear_systems_impacted(self):
|
|
83
|
+
self._systems_impacted = []
|
|
84
|
+
|
|
44
85
|
def _format_traceback(self, e_traceback, e_message):
|
|
45
86
|
if not e_traceback or e_traceback == 'None\n':
|
|
46
87
|
return None
|
|
47
88
|
|
|
48
89
|
traceback_lines = e_traceback.splitlines()
|
|
49
|
-
|
|
90
|
+
|
|
91
|
+
# Check if the traceback is within the limits
|
|
92
|
+
if len(traceback_lines) <= self.MAX_FIELD_LINES and len(e_traceback) <= self.MAX_FIELD_LENGTH:
|
|
93
|
+
return e_traceback
|
|
94
|
+
|
|
50
95
|
# Remove lines that are part of the exception message if they are present in traceback
|
|
51
96
|
message_lines = e_message.splitlines() if e_message else []
|
|
52
97
|
if message_lines:
|
|
@@ -56,7 +101,7 @@ class ContextLog:
|
|
|
56
101
|
|
|
57
102
|
# Filter out lines from third-party libraries (like site-packages)
|
|
58
103
|
filtered_lines = [line for line in traceback_lines if "site-packages" not in line]
|
|
59
|
-
|
|
104
|
+
|
|
60
105
|
# If filtering results in too few lines, revert to original traceback
|
|
61
106
|
if len(filtered_lines) < 2:
|
|
62
107
|
filtered_lines = traceback_lines
|
|
@@ -69,61 +114,106 @@ class ContextLog:
|
|
|
69
114
|
else:
|
|
70
115
|
combined_lines.append(line)
|
|
71
116
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
combined_lines[:keep_lines_start] +
|
|
80
|
-
['... (truncated) ...'] +
|
|
117
|
+
# Ensure the number of lines doesn't exceed MAX_TRACEBACK_LINES
|
|
118
|
+
if len(combined_lines) > self.MAX_FIELD_LINES:
|
|
119
|
+
keep_lines_start = min(self.MAX_FIELD_LINES // 2, len(combined_lines))
|
|
120
|
+
keep_lines_end = min(self.MAX_FIELD_LINES // 2, len(combined_lines) - keep_lines_start)
|
|
121
|
+
combined_lines = (
|
|
122
|
+
combined_lines[:keep_lines_start] +
|
|
123
|
+
['... (truncated) ...'] +
|
|
81
124
|
combined_lines[-keep_lines_end:]
|
|
82
125
|
)
|
|
83
|
-
else:
|
|
84
|
-
formatted_traceback = '\n'.join(combined_lines)
|
|
85
126
|
|
|
127
|
+
formatted_traceback = '\n'.join(combined_lines)
|
|
128
|
+
|
|
129
|
+
# Ensure the total length doesn't exceed MAX_TRACEBACK_LENGTH
|
|
130
|
+
if len(formatted_traceback) > self.MAX_FIELD_LENGTH:
|
|
131
|
+
truncated_length = self.MAX_FIELD_LENGTH - len('... (truncated) ...')
|
|
132
|
+
half_truncated_length = truncated_length // 2
|
|
133
|
+
formatted_traceback = (
|
|
134
|
+
formatted_traceback[:half_truncated_length] +
|
|
135
|
+
'\n... (truncated) ...\n' +
|
|
136
|
+
formatted_traceback[-half_truncated_length:]
|
|
137
|
+
)
|
|
86
138
|
return formatted_traceback
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
def to_dict(self, max_field_len:int =10000, size_limit:float=256 * 1024 * 0.80):
|
|
141
|
+
size_limit = int(size_limit) # Ensure size_limit is an integer
|
|
142
|
+
|
|
143
|
+
# Unified list of all fields
|
|
144
|
+
systems_impacted_str = f"{len(self.systems_impacted)} system(s): " + " ,,, ".join(self.systems_impacted) if self.systems_impacted else None
|
|
145
|
+
fields = [
|
|
146
|
+
("log_status", str(self.log_status.name)),
|
|
147
|
+
("level_code", self.level.value),
|
|
148
|
+
("level_name", str(self.level.name)),
|
|
149
|
+
("base_context", str(self.base_context)),
|
|
150
|
+
("timestamp", str(self.timestamp)),
|
|
151
|
+
("collector_id", str(self.collector_id)),
|
|
152
|
+
("systems_impacted", systems_impacted_str),
|
|
153
|
+
("context", str(self.context)), # special sizing rules apply to it
|
|
154
|
+
("subject", str(self.subject)),
|
|
155
|
+
("description", str(self.description)),
|
|
156
|
+
("exception_type", str(self.exception_type)),
|
|
157
|
+
("exception_message", str(self.exception_message)),
|
|
158
|
+
("exception_traceback", str(self._format_traceback(self.exception_traceback,self.exception_message)))
|
|
159
|
+
]
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
161
|
+
# Function to calculate the byte size of a JSON-encoded field
|
|
162
|
+
def field_size(key, value):
|
|
163
|
+
return len(json.dumps({key: value}).encode('utf-8'))
|
|
164
|
+
|
|
165
|
+
# Function to truncate a value based on its type
|
|
166
|
+
# Function to truncate a value based on its type
|
|
167
|
+
def truncate_value(value, max_size):
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
half_size = max_size // 2
|
|
170
|
+
return value[:half_size] + '...' + value[-(max_size - half_size - 3):]
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
# Ensure no field exceeds max_field_len
|
|
174
|
+
for i, (key, value) in enumerate(fields):
|
|
175
|
+
if isinstance(value, str) and len(value) > max_field_len:
|
|
176
|
+
fields[i] = (key, truncate_value(value, max_field_len))
|
|
177
|
+
|
|
178
|
+
# Ensure total size of the dict doesn't exceed size_limit
|
|
179
|
+
total_size = sum(field_size(key, value) for key, value in fields)
|
|
180
|
+
log_dict = {}
|
|
181
|
+
truncated = False
|
|
182
|
+
|
|
183
|
+
if total_size > size_limit:
|
|
184
|
+
truncated = True
|
|
185
|
+
remaining_size = size_limit
|
|
186
|
+
remaining_fields = len(fields)
|
|
187
|
+
|
|
188
|
+
for key, value in fields:
|
|
189
|
+
if remaining_fields > 0:
|
|
190
|
+
max_size_per_field = remaining_size // remaining_fields
|
|
191
|
+
else:
|
|
192
|
+
max_size_per_field = 0
|
|
193
|
+
|
|
194
|
+
field_sz = field_size(key, value)
|
|
195
|
+
if field_sz > max_size_per_field:
|
|
196
|
+
value = truncate_value(value, max_size_per_field)
|
|
197
|
+
field_sz = field_size(key, value)
|
|
198
|
+
|
|
199
|
+
log_dict[key] = value
|
|
200
|
+
remaining_size -= field_sz
|
|
201
|
+
remaining_fields -= 1
|
|
202
|
+
else:
|
|
203
|
+
log_dict = dict(fields)
|
|
95
204
|
|
|
96
|
-
|
|
97
|
-
def context(self):
|
|
98
|
-
return self._context
|
|
205
|
+
log_dict['trunc'] = truncated
|
|
99
206
|
|
|
100
|
-
|
|
101
|
-
def context(self, value):
|
|
102
|
-
self._context = value
|
|
207
|
+
return log_dict
|
|
103
208
|
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
"base_context": self.base_context,
|
|
107
|
-
"context": self.context,
|
|
108
|
-
"level_code": self.level.value,
|
|
109
|
-
"level_name": self.level.name,
|
|
110
|
-
"subject": self.subject,
|
|
111
|
-
"description": self.description,
|
|
112
|
-
"exception_type": self.exception_type,
|
|
113
|
-
"exception_message": self.exception_message,
|
|
114
|
-
"exception_traceback": self.exception_traceback,
|
|
115
|
-
"log_status": self.log_status.value,
|
|
116
|
-
"collector_id": self.collector_id,
|
|
117
|
-
"timestamp": self.timestamp
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class PipelineWatcher:
|
|
209
|
+
class Pipelinemon:
|
|
121
210
|
ERROR_START_CODE = LogLevel.ERROR.value
|
|
122
211
|
WARNING_START_CODE = LogLevel.WARNING.value
|
|
123
212
|
NOTICE_START_CODE = LogLevel.NOTICE.value
|
|
124
213
|
SUCCESS_START_CODE = LogLevel.SUCCESS.value
|
|
214
|
+
INFO_START_CODE = LogLevel.INFO.value
|
|
125
215
|
|
|
126
|
-
def __init__(self, base_context: str, target_logs: TargetLogs = TargetLogs.MIXED, logger_name=None):
|
|
216
|
+
def __init__(self, base_context: str, target_logs: TargetLogs = TargetLogs.MIXED, logger_name=None, max_log_field_size:int =10000, max_log_dict_size:float=256 * 1024 * 0.80):
|
|
127
217
|
self._id = str(uuid.uuid4())
|
|
128
218
|
self._logs = []
|
|
129
219
|
self._early_stop = False
|
|
@@ -131,11 +221,15 @@ class PipelineWatcher:
|
|
|
131
221
|
self._warnings_count = 0
|
|
132
222
|
self._notices_count = 0
|
|
133
223
|
self._successes_count = 0
|
|
224
|
+
self._infos_count = 0
|
|
225
|
+
self._systems_impacted = []
|
|
134
226
|
self._level_counts = {level.name: 0 for level in LogLevel}
|
|
135
227
|
self._base_context = base_context
|
|
136
228
|
self._context_stack = []
|
|
137
229
|
self._target_logs = target_logs.value
|
|
138
230
|
self._logger = self._initialize_logger(logger_name)
|
|
231
|
+
self._max_log_field_size = max_log_field_size
|
|
232
|
+
self._max_log_dict_size = max_log_dict_size
|
|
139
233
|
|
|
140
234
|
def _initialize_logger(self, logger_name):
|
|
141
235
|
if logger_name:
|
|
@@ -170,6 +264,38 @@ class PipelineWatcher:
|
|
|
170
264
|
def id(self):
|
|
171
265
|
return self._id
|
|
172
266
|
|
|
267
|
+
@property
|
|
268
|
+
def systems_impacted(self):
|
|
269
|
+
return self._systems_impacted
|
|
270
|
+
|
|
271
|
+
@systems_impacted.setter
|
|
272
|
+
def systems_impacted(self, list_of_si: List[str]):
|
|
273
|
+
self._systems_impacted = list_of_si
|
|
274
|
+
|
|
275
|
+
def add_system_impacted(self, system_impacted: str):
|
|
276
|
+
if self._systems_impacted is None:
|
|
277
|
+
self._systems_impacted = []
|
|
278
|
+
self._systems_impacted.append(system_impacted)
|
|
279
|
+
|
|
280
|
+
def clear_systems_impacted(self):
|
|
281
|
+
self._systems_impacted = []
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def max_log_field_size(self):
|
|
285
|
+
return self._max_log_field_size
|
|
286
|
+
|
|
287
|
+
@max_log_field_size.setter
|
|
288
|
+
def max_log_field_size(self, value):
|
|
289
|
+
self._max_log_field_size = value
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def max_log_dict_size(self):
|
|
293
|
+
return self._max_log_dict_size
|
|
294
|
+
|
|
295
|
+
@max_log_dict_size.setter
|
|
296
|
+
def max_log_dict_size(self, value):
|
|
297
|
+
self._max_log_dict_size = value
|
|
298
|
+
|
|
173
299
|
@property
|
|
174
300
|
def early_stop(self):
|
|
175
301
|
return self._early_stop
|
|
@@ -179,29 +305,28 @@ class PipelineWatcher:
|
|
|
179
305
|
if create_error_log:
|
|
180
306
|
if pop_context:
|
|
181
307
|
self.pop_context()
|
|
182
|
-
self.add_log(ContextLog(level=LogLevel.
|
|
308
|
+
self.add_log(ContextLog(level=LogLevel.ERROR_PIPELINE_THRESHOLD_REACHED,
|
|
183
309
|
subject="EARLY_STOP",
|
|
184
310
|
description=f"Total MAX_ERRORS_TOLERANCE of {max_errors_tolerance} has been reached."))
|
|
185
311
|
|
|
186
312
|
def reset_early_stop(self):
|
|
187
313
|
self._early_stop = False
|
|
188
314
|
|
|
189
|
-
def get_early_stop(self):
|
|
190
|
-
return self._early_stop
|
|
191
315
|
|
|
192
|
-
def add_log(self, log: ContextLog):
|
|
316
|
+
def add_log(self, log: ContextLog, ):
|
|
193
317
|
if (self._target_logs == TargetLogs.SUCCESSES and log.level >=self.NOTICE_START_CODE) or \
|
|
194
318
|
(self._target_logs == TargetLogs.WARNINGS_AND_ERRORS and log.level.value < self.WARNING_START_CODE):
|
|
195
|
-
raise ValueError(f"Invalid log level {log.level.name} for
|
|
319
|
+
raise ValueError(f"Invalid log level {log.level.name} for Pipelinemon target logs setup: {self._target_logs}")
|
|
196
320
|
log.base_context = self.base_context
|
|
197
321
|
log.context = self.current_context
|
|
198
322
|
log.collector_id = self.id
|
|
199
|
-
|
|
323
|
+
log.systems_impacted = self.systems_impacted
|
|
324
|
+
log_dict = log.to_dict(max_field_len=self.max_log_field_size, size_limit=self.max_log_dict_size)
|
|
200
325
|
self._logs.append(log_dict)
|
|
201
326
|
self._update_counts(log_dict)
|
|
202
327
|
|
|
203
328
|
if self._logger:
|
|
204
|
-
# We specifically want to avoid having an ERROR log level for this structured
|
|
329
|
+
# We specifically want to avoid having an ERROR log level for this structured Pipelinemon reporting, to ensure Errors are alerting on Critical Application Services.
|
|
205
330
|
# A single ERROR log level can be used for the entire pipeline, which shall be used at the end of the pipeline
|
|
206
331
|
if log.level.value >= self.WARNING_START_CODE:
|
|
207
332
|
self._logger.log_struct(log_dict, severity="WARNING")
|
|
@@ -220,6 +345,7 @@ class PipelineWatcher:
|
|
|
220
345
|
self._warnings_count = 0
|
|
221
346
|
self._notices_count = 0
|
|
222
347
|
self._successes_count = 0
|
|
348
|
+
self._infos_count = 0
|
|
223
349
|
self._level_counts = {level.name: 0 for level in LogLevel}
|
|
224
350
|
|
|
225
351
|
def clear_logs(self):
|
|
@@ -258,6 +384,15 @@ class PipelineWatcher:
|
|
|
258
384
|
def count_successes(self):
|
|
259
385
|
return self._successes_count
|
|
260
386
|
|
|
387
|
+
def count_successes_with_notice(self):
|
|
388
|
+
return self.count_logs_by_level(LogLevel.SUCCESS_WITH_NOTICES)
|
|
389
|
+
|
|
390
|
+
def count_successes_no_notice(self):
|
|
391
|
+
return self.count_logs_by_level(LogLevel.SUCCESS)
|
|
392
|
+
|
|
393
|
+
def count_infos(self):
|
|
394
|
+
return self._infos_count
|
|
395
|
+
|
|
261
396
|
def count_all_logs(self):
|
|
262
397
|
return len(self._logs)
|
|
263
398
|
|
|
@@ -314,7 +449,13 @@ class PipelineWatcher:
|
|
|
314
449
|
def count_successes_for_current_and_nested_contexts(self):
|
|
315
450
|
return self._count_logs(self.current_context, level_code_min=self.SUCCESS_START_CODE, level_code_max=self.NOTICE_START_CODE-1)
|
|
316
451
|
|
|
317
|
-
def
|
|
452
|
+
def count_infos_for_current_context(self):
|
|
453
|
+
return self._count_logs(self.current_context, exact_match=True, level_code_min=self.INFO_START_CODE, level_code_max=self.SUCCESS_START_CODE-1)
|
|
454
|
+
|
|
455
|
+
def count_infos_for_current_and_nested_contexts(self):
|
|
456
|
+
return self._count_logs(self.current_context, level_code_min=self.INFO_START_CODE, level_code_max=self.SUCCESS_START_CODE-1)
|
|
457
|
+
|
|
458
|
+
def export_logs_to_gcs_file(self, bucket_name, storage_client, file_prefix=None, file_name=None, top_level_context=None, save_locally=False, overwrite_if_exists=False, increment_if_exists=True, local_path=None, logger=None, max_retries=2):
|
|
318
459
|
def log_message(message):
|
|
319
460
|
if logger:
|
|
320
461
|
logger.info(message)
|
|
@@ -343,9 +484,11 @@ class PipelineWatcher:
|
|
|
343
484
|
local_path=local_path,
|
|
344
485
|
logger=logger,
|
|
345
486
|
max_retries=max_retries,
|
|
346
|
-
overwrite_if_exists=
|
|
487
|
+
overwrite_if_exists=overwrite_if_exists,
|
|
488
|
+
increment_if_exists=increment_if_exists
|
|
489
|
+
|
|
347
490
|
)
|
|
348
|
-
log_message(f"{file_prefix} successfully saved (overwritten={result.get('gcs_file_overwritten')}) to GCS at {result.get('gcs_path')} and locally at {result.get('local_path')}.")
|
|
491
|
+
log_message(f"{file_prefix} successfully saved (overwritten={result.get('gcs_file_overwritten')}, incremented={result.get('gcs_file_saved_with_increment')}) to GCS at {result.get('gcs_path')} and locally at {result.get('local_path')}.")
|
|
349
492
|
except Exception as e:
|
|
350
493
|
log_error(f"Failed at export_logs_to_gcs_file for {file_prefix} for file {file_name} to bucket {bucket_name}: {type(e).__name__} - {str(e)}")
|
|
351
494
|
|
|
@@ -383,6 +526,8 @@ class PipelineWatcher:
|
|
|
383
526
|
self._notices_count -= 1
|
|
384
527
|
elif self.SUCCESS_START_CODE <= level_code < self.NOTICE_START_CODE:
|
|
385
528
|
self._successes_count -= 1
|
|
529
|
+
elif self.INFO_START_CODE <= level_code < self.SUCCESS_START_CODE:
|
|
530
|
+
self._infos_count -= 1
|
|
386
531
|
self._level_counts[level_name] -= 1
|
|
387
532
|
else:
|
|
388
533
|
if level_code >= self.ERROR_START_CODE:
|
|
@@ -393,4 +538,6 @@ class PipelineWatcher:
|
|
|
393
538
|
self._notices_count += 1
|
|
394
539
|
elif self.SUCCESS_START_CODE <= level_code < self.NOTICE_START_CODE:
|
|
395
540
|
self._successes_count += 1
|
|
396
|
-
self.
|
|
541
|
+
elif self.INFO_START_CODE <= level_code < self.SUCCESS_START_CODE:
|
|
542
|
+
self._infos_count += 1
|
|
543
|
+
self._level_counts[level_name] += 1
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
|
2
2
|
# pylint: disable=missing-function-docstring
|
|
3
3
|
# pylint: disable=missing-class-docstring
|
|
4
|
+
# pylint: disable=broad-exception-caught
|
|
5
|
+
# pylint: disable=line-too-long
|
|
6
|
+
# pylint: disable=unused-variable
|
|
4
7
|
import json
|
|
5
8
|
import csv
|
|
6
9
|
from io import StringIO
|
|
@@ -23,7 +26,6 @@ from google.api_core.exceptions import NotFound
|
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
##### THIS APPROACH IS USED NOW ########
|
|
26
|
-
## TODO Fix the issue with POST 0B Nan.... printed in Cloud Logging , which is referring to posting to Cloud Logging probably.
|
|
27
29
|
ENV = os.getenv('ENV', 'LOCAL').strip("'")
|
|
28
30
|
|
|
29
31
|
def setup_gcp_logger_and_error_report(logger_name,level=logging.INFO, use_cloud_logging=True):
|
|
@@ -130,11 +132,7 @@ def write_json_to_gcs(bucket_name, storage_client, data, file_name,
|
|
|
130
132
|
This function attempts to upload data to GCS. If the upload fails after
|
|
131
133
|
retries and `save_locally` is True or `local_path` is provided, it attempts
|
|
132
134
|
to save the data locally.
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
dict: A dictionary containing the GCS path (or None if upload failed),
|
|
136
|
-
the local path (or None if not saved locally), a boolean indicating if the file was overwritten,
|
|
137
|
-
a boolean indicating if the file already existed, and a boolean indicating if the file was saved with an incremented name.
|
|
135
|
+
It also tries to handle file name conflicts by overwriting or incrementing. If both are provided as Ture, an exception will be raised.
|
|
138
136
|
"""
|
|
139
137
|
|
|
140
138
|
def log_message(message):
|
|
@@ -214,14 +212,13 @@ def write_json_to_gcs(bucket_name, storage_client, data, file_name,
|
|
|
214
212
|
local_path_final = os.path.join("/tmp", file_name)
|
|
215
213
|
else:
|
|
216
214
|
local_path_final = os.path.join(local_path, file_name)
|
|
217
|
-
|
|
215
|
+
|
|
218
216
|
if os.path.exists(local_path_final):
|
|
219
217
|
if increment_if_exists:
|
|
220
218
|
increment = 0
|
|
221
219
|
while os.path.exists(local_path_final):
|
|
222
220
|
increment += 1
|
|
223
221
|
local_path_final = os.path.join(local_path, f"{base_file_name}_{increment}{ext}")
|
|
224
|
-
gcs_file_saved_with_increment = True
|
|
225
222
|
elif not overwrite_if_exists:
|
|
226
223
|
log_message(f"File {file_name} already exists locally at {local_path_final} and overwrite is set to False. Skipping save.")
|
|
227
224
|
success = True
|
|
@@ -267,4 +264,4 @@ def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_in
|
|
|
267
264
|
except ValueError as e:
|
|
268
265
|
logger.error(f"ValueError: {e}")
|
|
269
266
|
except Exception as e:
|
|
270
|
-
|
|
267
|
+
logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.56
|
|
4
4
|
Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
|
|
5
5
|
Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
|
|
6
6
|
Author: Russlan Ramdowar
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|