nui-python-shared-utils 1.3.0__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.
- nui_lambda_shared_utils/__init__.py +252 -0
- nui_lambda_shared_utils/base_client.py +323 -0
- nui_lambda_shared_utils/cli.py +225 -0
- nui_lambda_shared_utils/cloudwatch_metrics.py +367 -0
- nui_lambda_shared_utils/config.py +136 -0
- nui_lambda_shared_utils/db_client.py +623 -0
- nui_lambda_shared_utils/error_handler.py +372 -0
- nui_lambda_shared_utils/es_client.py +460 -0
- nui_lambda_shared_utils/es_query_builder.py +315 -0
- nui_lambda_shared_utils/jwt_auth.py +277 -0
- nui_lambda_shared_utils/lambda_helpers.py +84 -0
- nui_lambda_shared_utils/log_processors.py +172 -0
- nui_lambda_shared_utils/powertools_helpers.py +263 -0
- nui_lambda_shared_utils/secrets_helper.py +187 -0
- nui_lambda_shared_utils/slack_client.py +675 -0
- nui_lambda_shared_utils/slack_formatter.py +307 -0
- nui_lambda_shared_utils/slack_setup/__init__.py +14 -0
- nui_lambda_shared_utils/slack_setup/channel_creator.py +295 -0
- nui_lambda_shared_utils/slack_setup/channel_definitions.py +187 -0
- nui_lambda_shared_utils/slack_setup/setup_helpers.py +211 -0
- nui_lambda_shared_utils/timezone.py +117 -0
- nui_lambda_shared_utils/utils.py +291 -0
- nui_python_shared_utils-1.3.0.dist-info/METADATA +470 -0
- nui_python_shared_utils-1.3.0.dist-info/RECORD +28 -0
- nui_python_shared_utils-1.3.0.dist-info/WHEEL +5 -0
- nui_python_shared_utils-1.3.0.dist-info/entry_points.txt +2 -0
- nui_python_shared_utils-1.3.0.dist-info/licenses/LICENSE +21 -0
- nui_python_shared_utils-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enterprise-grade utilities for AWS Lambda functions with Slack, Elasticsearch, and monitoring integrations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Configuration system
|
|
6
|
+
from .config import (
|
|
7
|
+
Config,
|
|
8
|
+
get_config,
|
|
9
|
+
set_config,
|
|
10
|
+
configure,
|
|
11
|
+
get_es_host,
|
|
12
|
+
get_es_credentials_secret,
|
|
13
|
+
get_db_credentials_secret,
|
|
14
|
+
get_slack_credentials_secret,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Core utilities
|
|
18
|
+
from .secrets_helper import (
|
|
19
|
+
get_secret,
|
|
20
|
+
get_database_credentials,
|
|
21
|
+
get_elasticsearch_credentials,
|
|
22
|
+
get_slack_credentials,
|
|
23
|
+
get_api_key,
|
|
24
|
+
clear_cache,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Common utilities
|
|
28
|
+
from .utils import (
|
|
29
|
+
resolve_config_value,
|
|
30
|
+
create_aws_client,
|
|
31
|
+
handle_client_errors,
|
|
32
|
+
merge_dimensions,
|
|
33
|
+
validate_required_param,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Base client architecture
|
|
37
|
+
from .base_client import BaseClient, ServiceHealthMixin, RetryableOperationMixin
|
|
38
|
+
|
|
39
|
+
# Client implementations - only fail if actually used
|
|
40
|
+
try:
|
|
41
|
+
from .slack_client import SlackClient
|
|
42
|
+
except ImportError:
|
|
43
|
+
SlackClient = None # type: ignore
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from .es_client import ElasticsearchClient
|
|
47
|
+
except ImportError:
|
|
48
|
+
ElasticsearchClient = None # type: ignore
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from .db_client import DatabaseClient, PostgreSQLClient, get_pool_stats
|
|
52
|
+
except ImportError:
|
|
53
|
+
DatabaseClient = None # type: ignore
|
|
54
|
+
PostgreSQLClient = None # type: ignore
|
|
55
|
+
get_pool_stats = None # type: ignore
|
|
56
|
+
|
|
57
|
+
from .timezone import nz_time, format_nz_time
|
|
58
|
+
|
|
59
|
+
# Slack formatting utilities (no external dependencies)
|
|
60
|
+
from .slack_formatter import (
|
|
61
|
+
SlackBlockBuilder,
|
|
62
|
+
format_currency,
|
|
63
|
+
format_percentage,
|
|
64
|
+
format_number,
|
|
65
|
+
format_nz_time as format_nz_time_slack,
|
|
66
|
+
format_date_range,
|
|
67
|
+
format_daily_header,
|
|
68
|
+
format_weekly_header,
|
|
69
|
+
format_error_alert,
|
|
70
|
+
SEVERITY_EMOJI,
|
|
71
|
+
STATUS_EMOJI,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ES query builder - optional import
|
|
75
|
+
try:
|
|
76
|
+
from .es_query_builder import (
|
|
77
|
+
ESQueryBuilder,
|
|
78
|
+
build_error_rate_query,
|
|
79
|
+
build_top_errors_query,
|
|
80
|
+
build_response_time_query,
|
|
81
|
+
build_service_volume_query,
|
|
82
|
+
build_user_activity_query,
|
|
83
|
+
build_pattern_detection_query,
|
|
84
|
+
build_tender_participant_query,
|
|
85
|
+
)
|
|
86
|
+
except ImportError:
|
|
87
|
+
ESQueryBuilder = None # type: ignore
|
|
88
|
+
build_error_rate_query = None # type: ignore
|
|
89
|
+
build_top_errors_query = None # type: ignore
|
|
90
|
+
build_response_time_query = None # type: ignore
|
|
91
|
+
build_service_volume_query = None # type: ignore
|
|
92
|
+
build_user_activity_query = None # type: ignore
|
|
93
|
+
build_pattern_detection_query = None # type: ignore
|
|
94
|
+
build_tender_participant_query = None # type: ignore
|
|
95
|
+
from .error_handler import (
|
|
96
|
+
RetryableError,
|
|
97
|
+
NonRetryableError,
|
|
98
|
+
ErrorPatternMatcher,
|
|
99
|
+
ErrorAggregator,
|
|
100
|
+
with_retry,
|
|
101
|
+
retry_on_network_error,
|
|
102
|
+
retry_on_db_error,
|
|
103
|
+
retry_on_es_error,
|
|
104
|
+
handle_lambda_error,
|
|
105
|
+
categorize_retryable_error,
|
|
106
|
+
)
|
|
107
|
+
from .cloudwatch_metrics import (
|
|
108
|
+
MetricsPublisher,
|
|
109
|
+
MetricAggregator,
|
|
110
|
+
StandardMetrics,
|
|
111
|
+
TimedMetric,
|
|
112
|
+
track_lambda_performance,
|
|
113
|
+
create_service_dimensions,
|
|
114
|
+
publish_health_metric,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# AWS Powertools integration - optional import
|
|
118
|
+
try:
|
|
119
|
+
from .powertools_helpers import get_powertools_logger, powertools_handler
|
|
120
|
+
except ImportError:
|
|
121
|
+
get_powertools_logger = None # type: ignore
|
|
122
|
+
powertools_handler = None # type: ignore
|
|
123
|
+
|
|
124
|
+
# Log processing utilities (no external dependencies)
|
|
125
|
+
from .log_processors import (
|
|
126
|
+
CloudWatchLogEvent,
|
|
127
|
+
CloudWatchLogsData,
|
|
128
|
+
derive_index_name,
|
|
129
|
+
extract_cloudwatch_logs_from_kinesis,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Lambda context helpers (no external dependencies)
|
|
133
|
+
from .lambda_helpers import get_lambda_environment_info
|
|
134
|
+
|
|
135
|
+
# JWT authentication - optional import
|
|
136
|
+
try:
|
|
137
|
+
from .jwt_auth import (
|
|
138
|
+
validate_jwt,
|
|
139
|
+
require_auth,
|
|
140
|
+
check_auth,
|
|
141
|
+
get_jwt_public_key,
|
|
142
|
+
JWTValidationError,
|
|
143
|
+
AuthenticationError,
|
|
144
|
+
)
|
|
145
|
+
except ImportError:
|
|
146
|
+
validate_jwt = None # type: ignore
|
|
147
|
+
require_auth = None # type: ignore
|
|
148
|
+
check_auth = None # type: ignore
|
|
149
|
+
get_jwt_public_key = None # type: ignore
|
|
150
|
+
JWTValidationError = None # type: ignore
|
|
151
|
+
AuthenticationError = None # type: ignore
|
|
152
|
+
|
|
153
|
+
# Slack setup utilities (for CLI usage) - optional import
|
|
154
|
+
try:
|
|
155
|
+
from . import slack_setup
|
|
156
|
+
except ImportError:
|
|
157
|
+
slack_setup = None # type: ignore
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
# Configuration system
|
|
161
|
+
"Config",
|
|
162
|
+
"get_config",
|
|
163
|
+
"set_config",
|
|
164
|
+
"configure",
|
|
165
|
+
"get_es_host",
|
|
166
|
+
"get_es_credentials_secret",
|
|
167
|
+
"get_db_credentials_secret",
|
|
168
|
+
"get_slack_credentials_secret",
|
|
169
|
+
# Core utilities
|
|
170
|
+
"get_secret",
|
|
171
|
+
"get_database_credentials",
|
|
172
|
+
"get_elasticsearch_credentials",
|
|
173
|
+
"get_slack_credentials",
|
|
174
|
+
"get_api_key",
|
|
175
|
+
"clear_cache",
|
|
176
|
+
# Common utilities
|
|
177
|
+
"resolve_config_value",
|
|
178
|
+
"create_aws_client",
|
|
179
|
+
"handle_client_errors",
|
|
180
|
+
"merge_dimensions",
|
|
181
|
+
"validate_required_param",
|
|
182
|
+
# Base client architecture
|
|
183
|
+
"BaseClient",
|
|
184
|
+
"ServiceHealthMixin",
|
|
185
|
+
"RetryableOperationMixin",
|
|
186
|
+
# Client implementations
|
|
187
|
+
"SlackClient",
|
|
188
|
+
"ElasticsearchClient",
|
|
189
|
+
"DatabaseClient",
|
|
190
|
+
"PostgreSQLClient",
|
|
191
|
+
"get_pool_stats", # Legacy compatibility (None)
|
|
192
|
+
"nz_time",
|
|
193
|
+
"format_nz_time",
|
|
194
|
+
"slack_setup",
|
|
195
|
+
# Slack formatting
|
|
196
|
+
"SlackBlockBuilder",
|
|
197
|
+
"format_currency",
|
|
198
|
+
"format_percentage",
|
|
199
|
+
"format_number",
|
|
200
|
+
"format_nz_time_slack",
|
|
201
|
+
"format_date_range",
|
|
202
|
+
"format_daily_header",
|
|
203
|
+
"format_weekly_header",
|
|
204
|
+
"format_error_alert",
|
|
205
|
+
"SEVERITY_EMOJI",
|
|
206
|
+
"STATUS_EMOJI",
|
|
207
|
+
# ES query building
|
|
208
|
+
"ESQueryBuilder",
|
|
209
|
+
"build_error_rate_query",
|
|
210
|
+
"build_top_errors_query",
|
|
211
|
+
"build_response_time_query",
|
|
212
|
+
"build_service_volume_query",
|
|
213
|
+
"build_user_activity_query",
|
|
214
|
+
"build_pattern_detection_query",
|
|
215
|
+
"build_tender_participant_query",
|
|
216
|
+
# Error handling
|
|
217
|
+
"RetryableError",
|
|
218
|
+
"NonRetryableError",
|
|
219
|
+
"ErrorPatternMatcher",
|
|
220
|
+
"ErrorAggregator",
|
|
221
|
+
"with_retry",
|
|
222
|
+
"retry_on_network_error",
|
|
223
|
+
"retry_on_db_error",
|
|
224
|
+
"retry_on_es_error",
|
|
225
|
+
"handle_lambda_error",
|
|
226
|
+
"categorize_retryable_error",
|
|
227
|
+
# CloudWatch metrics
|
|
228
|
+
"MetricsPublisher",
|
|
229
|
+
"MetricAggregator",
|
|
230
|
+
"StandardMetrics",
|
|
231
|
+
"TimedMetric",
|
|
232
|
+
"track_lambda_performance",
|
|
233
|
+
"create_service_dimensions",
|
|
234
|
+
"publish_health_metric",
|
|
235
|
+
# AWS Powertools integration
|
|
236
|
+
"get_powertools_logger",
|
|
237
|
+
"powertools_handler",
|
|
238
|
+
# Log processing
|
|
239
|
+
"extract_cloudwatch_logs_from_kinesis",
|
|
240
|
+
"derive_index_name",
|
|
241
|
+
"CloudWatchLogEvent",
|
|
242
|
+
"CloudWatchLogsData",
|
|
243
|
+
# Lambda context helpers
|
|
244
|
+
"get_lambda_environment_info",
|
|
245
|
+
# JWT authentication
|
|
246
|
+
"validate_jwt",
|
|
247
|
+
"require_auth",
|
|
248
|
+
"check_auth",
|
|
249
|
+
"get_jwt_public_key",
|
|
250
|
+
"JWTValidationError",
|
|
251
|
+
"AuthenticationError",
|
|
252
|
+
]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base client class providing common functionality for AWS service clients.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
from .config import get_config
|
|
10
|
+
from .secrets_helper import get_secret
|
|
11
|
+
from .utils import resolve_config_value, validate_required_param, handle_client_errors
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseClient(ABC):
|
|
17
|
+
"""
|
|
18
|
+
Base class for AWS service clients providing standardized:
|
|
19
|
+
- Credential resolution and management
|
|
20
|
+
- Configuration integration
|
|
21
|
+
- Error handling patterns
|
|
22
|
+
- Logging context
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
secret_name: Optional[str] = None,
|
|
28
|
+
config_key_prefix: Optional[str] = None,
|
|
29
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
30
|
+
**kwargs
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize base client with standardized credential and config resolution.
|
|
34
|
+
|
|
35
|
+
Credential resolution precedence:
|
|
36
|
+
1. Explicit ``credentials`` dict — skip everything else
|
|
37
|
+
2. Environment variables (per-client patterns) — skip Secrets Manager
|
|
38
|
+
3. Secrets Manager (existing behavior) — unchanged
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
secret_name: Override secret name for credentials
|
|
42
|
+
config_key_prefix: Prefix for config keys (e.g., 'slack', 'es', 'db')
|
|
43
|
+
credentials: Direct credentials dict, bypasses Secrets Manager entirely
|
|
44
|
+
**kwargs: Additional client-specific parameters
|
|
45
|
+
"""
|
|
46
|
+
self.config = get_config()
|
|
47
|
+
self.config_key_prefix = config_key_prefix or self._get_default_config_prefix()
|
|
48
|
+
|
|
49
|
+
# Resolve and store credentials
|
|
50
|
+
self.credentials = self._resolve_credentials(secret_name, credentials)
|
|
51
|
+
|
|
52
|
+
# Store additional configuration
|
|
53
|
+
self.client_config = kwargs
|
|
54
|
+
|
|
55
|
+
# Initialize service-specific client
|
|
56
|
+
self._service_client = self._create_service_client()
|
|
57
|
+
|
|
58
|
+
log.info(
|
|
59
|
+
f"Initialized {self.__class__.__name__}",
|
|
60
|
+
extra={
|
|
61
|
+
"client_type": self.__class__.__name__,
|
|
62
|
+
"config_prefix": self.config_key_prefix,
|
|
63
|
+
"has_credentials": bool(self.credentials)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def _get_default_config_prefix(self) -> str:
|
|
69
|
+
"""Return the default configuration key prefix for this client type."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def _create_service_client(self) -> Any:
|
|
74
|
+
"""Create and return the underlying service client (e.g., WebClient, Elasticsearch)."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _get_default_secret_name(self) -> str:
|
|
79
|
+
"""Return the default secret name for this client type."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Resolve credentials from environment variables.
|
|
85
|
+
|
|
86
|
+
Subclasses override this to check client-specific env vars
|
|
87
|
+
(e.g. SLACK_BOT_TOKEN, ES_PASSWORD). Return a credentials dict
|
|
88
|
+
if the required env vars are present, or ``None`` to fall through
|
|
89
|
+
to Secrets Manager.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Credentials dict or None
|
|
93
|
+
"""
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _resolve_credentials(
|
|
97
|
+
self,
|
|
98
|
+
secret_name: Optional[str],
|
|
99
|
+
explicit_credentials: Optional[Dict[str, Any]] = None,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Resolve credentials using standardized precedence.
|
|
103
|
+
|
|
104
|
+
Precedence:
|
|
105
|
+
1. Explicit credentials dict (constructor param)
|
|
106
|
+
2. Environment variables (per-client ``_resolve_credentials_from_env``)
|
|
107
|
+
3. AWS Secrets Manager (via ``_fetch_credentials_from_sm``)
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
secret_name: Optional override for secret name
|
|
111
|
+
explicit_credentials: Direct credentials dict from constructor
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dictionary containing resolved credentials
|
|
115
|
+
"""
|
|
116
|
+
# 1. Explicit credentials dict — skip everything
|
|
117
|
+
if explicit_credentials is not None:
|
|
118
|
+
log.debug("Using explicitly provided credentials")
|
|
119
|
+
return explicit_credentials
|
|
120
|
+
|
|
121
|
+
# 2. Environment variables — skip Secrets Manager
|
|
122
|
+
env_credentials = self._resolve_credentials_from_env()
|
|
123
|
+
if env_credentials is not None:
|
|
124
|
+
log.debug("Using credentials from environment variables")
|
|
125
|
+
return env_credentials
|
|
126
|
+
|
|
127
|
+
# 3. Secrets Manager
|
|
128
|
+
return self._fetch_credentials_from_sm(secret_name)
|
|
129
|
+
|
|
130
|
+
def _resolve_secret_name(self, secret_name: Optional[str]) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Resolve the Secrets Manager secret name using standard precedence:
|
|
133
|
+
explicit param > env var > config default.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
secret_name: Optional explicit override
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Resolved secret name string
|
|
140
|
+
"""
|
|
141
|
+
resolved = resolve_config_value(
|
|
142
|
+
secret_name,
|
|
143
|
+
[
|
|
144
|
+
f"{self.config_key_prefix.upper()}_CREDENTIALS_SECRET",
|
|
145
|
+
f"{self.config_key_prefix.upper()}CREDENTIALS_SECRET" # Alternative format
|
|
146
|
+
],
|
|
147
|
+
getattr(self.config, f"{self.config_key_prefix}_credentials_secret", self._get_default_secret_name())
|
|
148
|
+
)
|
|
149
|
+
validate_required_param(resolved, "secret_name")
|
|
150
|
+
return resolved
|
|
151
|
+
|
|
152
|
+
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Fetch credentials from AWS Secrets Manager.
|
|
155
|
+
|
|
156
|
+
Subclasses override this to customize the SM fetch (e.g. DB clients
|
|
157
|
+
use ``get_database_credentials`` for field normalization).
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
secret_name: Optional override for secret name
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary containing resolved credentials
|
|
164
|
+
"""
|
|
165
|
+
resolved_secret_name = self._resolve_secret_name(secret_name)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
credentials = get_secret(resolved_secret_name)
|
|
169
|
+
log.debug(f"Retrieved credentials from secret: {resolved_secret_name}")
|
|
170
|
+
return credentials
|
|
171
|
+
except Exception as e:
|
|
172
|
+
log.error(
|
|
173
|
+
f"Failed to retrieve credentials from secret: {resolved_secret_name}",
|
|
174
|
+
extra={"secret_name": resolved_secret_name, "error": str(e)}
|
|
175
|
+
)
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
def _get_config_value(self, key: str, env_vars: Optional[list] = None, default: Any = None) -> Any:
|
|
179
|
+
"""
|
|
180
|
+
Get configuration value with standardized precedence.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
key: Configuration key (without prefix)
|
|
184
|
+
env_vars: Environment variable names to check
|
|
185
|
+
default: Default value
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Resolved configuration value
|
|
189
|
+
"""
|
|
190
|
+
# Get from client config (constructor kwargs) first
|
|
191
|
+
if key in self.client_config:
|
|
192
|
+
return self.client_config[key]
|
|
193
|
+
|
|
194
|
+
# Build full config key
|
|
195
|
+
config_key = f"{self.config_key_prefix}_{key}"
|
|
196
|
+
config_value = getattr(self.config, config_key, default)
|
|
197
|
+
|
|
198
|
+
# Use utility for full resolution
|
|
199
|
+
return resolve_config_value(
|
|
200
|
+
None, # No explicit parameter (already checked client_config)
|
|
201
|
+
env_vars or [],
|
|
202
|
+
config_value
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@handle_client_errors(reraise=True)
|
|
206
|
+
def _execute_with_error_handling(self, operation_name: str, operation_func, **log_context):
|
|
207
|
+
"""
|
|
208
|
+
Execute an operation with standardized error handling and logging.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
operation_name: Name of the operation for logging
|
|
212
|
+
operation_func: Function to execute
|
|
213
|
+
**log_context: Additional logging context
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Result of operation_func
|
|
217
|
+
"""
|
|
218
|
+
context = {
|
|
219
|
+
"client_type": self.__class__.__name__,
|
|
220
|
+
"operation": operation_name,
|
|
221
|
+
**log_context
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
log.debug(f"Executing {operation_name}", extra=context)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
result = operation_func()
|
|
228
|
+
log.debug(f"Successfully completed {operation_name}", extra=context)
|
|
229
|
+
return result
|
|
230
|
+
except Exception as e:
|
|
231
|
+
context["error_type"] = type(e).__name__
|
|
232
|
+
context["error_message"] = str(e)
|
|
233
|
+
log.error(f"Failed to execute {operation_name}: {e}", extra=context)
|
|
234
|
+
raise
|
|
235
|
+
|
|
236
|
+
def get_client_info(self) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Get information about this client instance.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dictionary with client information
|
|
242
|
+
"""
|
|
243
|
+
return {
|
|
244
|
+
"client_type": self.__class__.__name__,
|
|
245
|
+
"config_prefix": self.config_key_prefix,
|
|
246
|
+
"has_credentials": bool(self.credentials),
|
|
247
|
+
"client_config": {k: v for k, v in self.client_config.items() if not k.startswith('_')},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ServiceHealthMixin:
|
|
252
|
+
"""
|
|
253
|
+
Mixin providing common health check functionality for service clients.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def health_check(self) -> Dict[str, Any]:
|
|
257
|
+
"""
|
|
258
|
+
Perform a basic health check for the service.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Dictionary with health status information
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
self._perform_health_check()
|
|
265
|
+
return {
|
|
266
|
+
"status": "healthy",
|
|
267
|
+
"client_type": self.__class__.__name__,
|
|
268
|
+
"timestamp": log.time.time() if hasattr(log, 'time') else None
|
|
269
|
+
}
|
|
270
|
+
except Exception as e:
|
|
271
|
+
return {
|
|
272
|
+
"status": "unhealthy",
|
|
273
|
+
"client_type": self.__class__.__name__,
|
|
274
|
+
"error": str(e),
|
|
275
|
+
"error_type": type(e).__name__,
|
|
276
|
+
"timestamp": log.time.time() if hasattr(log, 'time') else None
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@abstractmethod
|
|
280
|
+
def _perform_health_check(self):
|
|
281
|
+
"""Perform service-specific health check. Should raise exception if unhealthy."""
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class RetryableOperationMixin:
|
|
286
|
+
"""
|
|
287
|
+
Mixin providing retry functionality for operations.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def execute_with_retry(
|
|
291
|
+
self,
|
|
292
|
+
operation_func,
|
|
293
|
+
operation_name: str,
|
|
294
|
+
max_attempts: int = 3,
|
|
295
|
+
**retry_kwargs
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Execute operation with retry logic.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
operation_func: Function to execute
|
|
302
|
+
operation_name: Name for logging
|
|
303
|
+
max_attempts: Maximum retry attempts
|
|
304
|
+
**retry_kwargs: Additional retry configuration
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Result of operation_func
|
|
308
|
+
"""
|
|
309
|
+
from .error_handler import with_retry
|
|
310
|
+
|
|
311
|
+
# Apply retry decorator dynamically
|
|
312
|
+
retried_operation = with_retry(
|
|
313
|
+
max_attempts=max_attempts,
|
|
314
|
+
**retry_kwargs
|
|
315
|
+
)(operation_func)
|
|
316
|
+
|
|
317
|
+
executor = getattr(self, "_execute_with_error_handling", None)
|
|
318
|
+
if executor is None:
|
|
319
|
+
raise AttributeError(
|
|
320
|
+
f"{self.__class__.__name__} must implement _execute_with_error_handling "
|
|
321
|
+
"to use RetryableOperationMixin"
|
|
322
|
+
)
|
|
323
|
+
return executor(operation_name, retried_operation)
|