matrice-analytics 0.1.70__py3-none-any.whl → 0.1.89__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.
- matrice_analytics/post_processing/config.py +2 -2
- matrice_analytics/post_processing/core/base.py +1 -1
- matrice_analytics/post_processing/face_reg/face_recognition.py +871 -190
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +55 -25
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +908 -498
- matrice_analytics/post_processing/usecases/color_detection.py +18 -18
- matrice_analytics/post_processing/usecases/customer_service.py +356 -9
- matrice_analytics/post_processing/usecases/fire_detection.py +147 -9
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +549 -41
- matrice_analytics/post_processing/usecases/people_counting.py +11 -11
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +34 -34
- matrice_analytics/post_processing/utils/alert_instance_utils.py +950 -0
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1245 -0
- matrice_analytics/post_processing/utils/incident_manager_utils.py +1657 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.89.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.89.dist-info}/RECORD +19 -16
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.89.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.89.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.89.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
business_metrics_manager_utils.py
|
|
3
|
+
|
|
4
|
+
Manages business metrics aggregation and publishing to Redis/Kafka.
|
|
5
|
+
Aggregates metrics for 5 minutes (300 seconds) and pushes to output topic.
|
|
6
|
+
Supports aggregation types: mean (default), min, max, sum.
|
|
7
|
+
|
|
8
|
+
PRODUCTION-READY VERSION
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
import threading
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import urllib.request
|
|
17
|
+
import base64
|
|
18
|
+
import re
|
|
19
|
+
from typing import Dict, List, Optional, Any, Union
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Default aggregation interval in seconds (5 minutes)
|
|
26
|
+
DEFAULT_AGGREGATION_INTERVAL = 300
|
|
27
|
+
|
|
28
|
+
# Supported aggregation types
|
|
29
|
+
AGGREGATION_TYPES = ["mean", "min", "max", "sum"]
|
|
30
|
+
|
|
31
|
+
# Default metrics configuration with aggregation type
|
|
32
|
+
DEFAULT_METRICS_CONFIG = {
|
|
33
|
+
"customer_to_staff_ratio": "mean",
|
|
34
|
+
"service_coverage": "mean",
|
|
35
|
+
"interaction_rate": "mean",
|
|
36
|
+
"staff_utilization": "mean",
|
|
37
|
+
"area_utilization": "mean",
|
|
38
|
+
"service_quality_score": "mean",
|
|
39
|
+
"attention_score": "mean",
|
|
40
|
+
"overall_performance": "mean",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class MetricAggregator:
|
|
46
|
+
"""Stores aggregated values for a single metric."""
|
|
47
|
+
values: List[float] = field(default_factory=list)
|
|
48
|
+
agg_type: str = "mean"
|
|
49
|
+
|
|
50
|
+
def add_value(self, value: float):
|
|
51
|
+
"""Add a value to the aggregator."""
|
|
52
|
+
if value is not None and isinstance(value, (int, float)):
|
|
53
|
+
self.values.append(float(value))
|
|
54
|
+
|
|
55
|
+
def get_aggregated_value(self) -> Optional[float]:
|
|
56
|
+
"""Get the aggregated value based on aggregation type."""
|
|
57
|
+
if not self.values:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if self.agg_type == "mean":
|
|
61
|
+
return sum(self.values) / len(self.values)
|
|
62
|
+
elif self.agg_type == "min":
|
|
63
|
+
return min(self.values)
|
|
64
|
+
elif self.agg_type == "max":
|
|
65
|
+
return max(self.values)
|
|
66
|
+
elif self.agg_type == "sum":
|
|
67
|
+
return sum(self.values)
|
|
68
|
+
else:
|
|
69
|
+
# Default to mean if unknown type
|
|
70
|
+
return sum(self.values) / len(self.values)
|
|
71
|
+
|
|
72
|
+
def reset(self):
|
|
73
|
+
"""Reset the aggregator values."""
|
|
74
|
+
self.values = []
|
|
75
|
+
|
|
76
|
+
def has_values(self) -> bool:
|
|
77
|
+
"""Check if aggregator has any values."""
|
|
78
|
+
return len(self.values) > 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class CameraMetricsState:
|
|
83
|
+
"""Stores metrics state for a camera."""
|
|
84
|
+
camera_id: str
|
|
85
|
+
camera_name: str = ""
|
|
86
|
+
app_deployment_id: str = ""
|
|
87
|
+
application_id: str = ""
|
|
88
|
+
metrics: Dict[str, MetricAggregator] = field(default_factory=dict)
|
|
89
|
+
last_push_time: float = field(default_factory=time.time)
|
|
90
|
+
|
|
91
|
+
def add_metric_value(self, metric_name: str, value: float, agg_type: str = "mean"):
|
|
92
|
+
"""Add a value for a specific metric."""
|
|
93
|
+
if metric_name not in self.metrics:
|
|
94
|
+
self.metrics[metric_name] = MetricAggregator(agg_type=agg_type)
|
|
95
|
+
self.metrics[metric_name].add_value(value)
|
|
96
|
+
|
|
97
|
+
def get_aggregated_metrics(self) -> Dict[str, Dict[str, Any]]:
|
|
98
|
+
"""Get all aggregated metrics in output format."""
|
|
99
|
+
result = {}
|
|
100
|
+
for metric_name, aggregator in self.metrics.items():
|
|
101
|
+
if aggregator.has_values():
|
|
102
|
+
agg_value = aggregator.get_aggregated_value()
|
|
103
|
+
if agg_value is not None:
|
|
104
|
+
result[metric_name] = {
|
|
105
|
+
"data": round(agg_value, 4),
|
|
106
|
+
"agg_type": aggregator.agg_type
|
|
107
|
+
}
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def reset_metrics(self):
|
|
111
|
+
"""Reset all metric aggregators."""
|
|
112
|
+
for aggregator in self.metrics.values():
|
|
113
|
+
aggregator.reset()
|
|
114
|
+
self.last_push_time = time.time()
|
|
115
|
+
|
|
116
|
+
def has_metrics(self) -> bool:
|
|
117
|
+
"""Check if any metrics have values."""
|
|
118
|
+
return any(agg.has_values() for agg in self.metrics.values())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BUSINESS_METRICS_MANAGER:
|
|
122
|
+
"""
|
|
123
|
+
Manages business metrics aggregation and publishing.
|
|
124
|
+
|
|
125
|
+
Key behaviors:
|
|
126
|
+
- Aggregates business metrics for configurable interval (default 5 minutes)
|
|
127
|
+
- Publishes aggregated metrics to Redis/Kafka topic
|
|
128
|
+
- Supports multiple aggregation types (mean, min, max, sum)
|
|
129
|
+
- Resets all values after publishing
|
|
130
|
+
- Thread-safe operations
|
|
131
|
+
|
|
132
|
+
Usage:
|
|
133
|
+
manager = BUSINESS_METRICS_MANAGER(redis_client=..., kafka_client=...)
|
|
134
|
+
manager.start() # Start aggregation timer
|
|
135
|
+
manager.process_metrics(camera_id, metrics_data, stream_info)
|
|
136
|
+
manager.stop() # Stop on shutdown
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
OUTPUT_TOPIC = "business_metrics"
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
redis_client: Optional[Any] = None,
|
|
144
|
+
kafka_client: Optional[Any] = None,
|
|
145
|
+
output_topic: str = "business_metrics",
|
|
146
|
+
aggregation_interval: int = DEFAULT_AGGREGATION_INTERVAL,
|
|
147
|
+
metrics_config: Optional[Dict[str, str]] = None,
|
|
148
|
+
logger: Optional[logging.Logger] = None
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
Initialize BUSINESS_METRICS_MANAGER.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
redis_client: MatriceStream instance configured for Redis
|
|
155
|
+
kafka_client: MatriceStream instance configured for Kafka
|
|
156
|
+
output_topic: Topic/stream name for publishing metrics
|
|
157
|
+
aggregation_interval: Interval in seconds for aggregation (default 300 = 5 minutes)
|
|
158
|
+
metrics_config: Dict of metric_name -> aggregation_type
|
|
159
|
+
logger: Python logger instance
|
|
160
|
+
"""
|
|
161
|
+
self.redis_client = redis_client
|
|
162
|
+
self.kafka_client = kafka_client
|
|
163
|
+
self.output_topic = output_topic
|
|
164
|
+
self.aggregation_interval = aggregation_interval
|
|
165
|
+
self.metrics_config = metrics_config or DEFAULT_METRICS_CONFIG.copy()
|
|
166
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
167
|
+
|
|
168
|
+
# Per-camera metrics state tracking: {camera_id: CameraMetricsState}
|
|
169
|
+
self._camera_states: Dict[str, CameraMetricsState] = {}
|
|
170
|
+
self._states_lock = threading.Lock()
|
|
171
|
+
|
|
172
|
+
# Timer thread control
|
|
173
|
+
self._timer_thread: Optional[threading.Thread] = None
|
|
174
|
+
self._stop_event = threading.Event()
|
|
175
|
+
self._running = False
|
|
176
|
+
|
|
177
|
+
# Store factory reference for fetching camera info
|
|
178
|
+
self._factory_ref: Optional['BusinessMetricsManagerFactory'] = None
|
|
179
|
+
|
|
180
|
+
self.logger.info(
|
|
181
|
+
f"[BUSINESS_METRICS_MANAGER] Initialized with output_topic={output_topic}, "
|
|
182
|
+
f"aggregation_interval={aggregation_interval}s"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def set_factory_ref(self, factory: 'BusinessMetricsManagerFactory'):
|
|
186
|
+
"""Set reference to factory for accessing deployment info."""
|
|
187
|
+
self._factory_ref = factory
|
|
188
|
+
|
|
189
|
+
def start(self):
|
|
190
|
+
"""Start the background timer thread for periodic publishing."""
|
|
191
|
+
if self._running:
|
|
192
|
+
self.logger.warning("[BUSINESS_METRICS_MANAGER] Already running")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
self._running = True
|
|
196
|
+
self._stop_event.clear()
|
|
197
|
+
self._timer_thread = threading.Thread(
|
|
198
|
+
target=self._timer_loop,
|
|
199
|
+
daemon=True,
|
|
200
|
+
name="BusinessMetricsTimer"
|
|
201
|
+
)
|
|
202
|
+
self._timer_thread.start()
|
|
203
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER] ✓ Started timer thread")
|
|
204
|
+
|
|
205
|
+
def stop(self):
|
|
206
|
+
"""Stop the background timer thread gracefully."""
|
|
207
|
+
if not self._running:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER] Stopping...")
|
|
211
|
+
self._running = False
|
|
212
|
+
self._stop_event.set()
|
|
213
|
+
|
|
214
|
+
if self._timer_thread and self._timer_thread.is_alive():
|
|
215
|
+
self._timer_thread.join(timeout=5)
|
|
216
|
+
|
|
217
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER] ✓ Stopped")
|
|
218
|
+
|
|
219
|
+
def _timer_loop(self):
|
|
220
|
+
"""Background thread that checks and publishes metrics periodically."""
|
|
221
|
+
self.logger.info(
|
|
222
|
+
f"[BUSINESS_METRICS_MANAGER] Timer loop started "
|
|
223
|
+
f"(interval: {self.aggregation_interval}s, check_every: 10s)"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
loop_count = 0
|
|
227
|
+
while not self._stop_event.is_set():
|
|
228
|
+
loop_count += 1
|
|
229
|
+
try:
|
|
230
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Timer loop iteration #{loop_count}")
|
|
231
|
+
self._check_and_publish_all()
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.logger.error(
|
|
234
|
+
f"[BUSINESS_METRICS_MANAGER] Error in timer loop: {e}",
|
|
235
|
+
exc_info=True
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Sleep in small increments to allow quick shutdown
|
|
239
|
+
for _ in range(min(10, self.aggregation_interval)):
|
|
240
|
+
if self._stop_event.is_set():
|
|
241
|
+
break
|
|
242
|
+
time.sleep(1)
|
|
243
|
+
|
|
244
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER] Timer loop exited")
|
|
245
|
+
|
|
246
|
+
def _check_and_publish_all(self):
|
|
247
|
+
"""Check all cameras and publish metrics if interval has passed."""
|
|
248
|
+
current_time = time.time()
|
|
249
|
+
cameras_to_publish = []
|
|
250
|
+
|
|
251
|
+
with self._states_lock:
|
|
252
|
+
num_cameras = len(self._camera_states)
|
|
253
|
+
if num_cameras > 0:
|
|
254
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] _check_and_publish_all: checking {num_cameras} camera(s)")
|
|
255
|
+
|
|
256
|
+
for camera_id, state in self._camera_states.items():
|
|
257
|
+
elapsed = current_time - state.last_push_time
|
|
258
|
+
has_metrics = state.has_metrics()
|
|
259
|
+
metrics_count = sum(len(agg.values) for agg in state.metrics.values())
|
|
260
|
+
|
|
261
|
+
self.logger.debug(
|
|
262
|
+
f"[BUSINESS_METRICS_MANAGER] Camera {camera_id}: elapsed={elapsed:.1f}s, "
|
|
263
|
+
f"interval={self.aggregation_interval}s, has_metrics={has_metrics}, count={metrics_count}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if elapsed >= self.aggregation_interval and has_metrics:
|
|
267
|
+
cameras_to_publish.append(camera_id)
|
|
268
|
+
self.logger.info(
|
|
269
|
+
f"[BUSINESS_METRICS_MANAGER] ✓ Camera {camera_id} ready for publish "
|
|
270
|
+
f"(elapsed={elapsed:.1f}s >= {self.aggregation_interval}s)"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if cameras_to_publish:
|
|
274
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] Publishing metrics for {len(cameras_to_publish)} camera(s)")
|
|
275
|
+
|
|
276
|
+
for camera_id in cameras_to_publish:
|
|
277
|
+
try:
|
|
278
|
+
success = self._publish_camera_metrics(camera_id)
|
|
279
|
+
if success:
|
|
280
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] ✓ Successfully published metrics for camera: {camera_id}")
|
|
281
|
+
else:
|
|
282
|
+
self.logger.warning(f"[BUSINESS_METRICS_MANAGER] ❌ Failed to publish metrics for camera: {camera_id}")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.logger.error(
|
|
285
|
+
f"[BUSINESS_METRICS_MANAGER] Error publishing metrics for "
|
|
286
|
+
f"camera {camera_id}: {e}",
|
|
287
|
+
exc_info=True
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _extract_camera_info_from_stream(
|
|
291
|
+
self,
|
|
292
|
+
stream_info: Optional[Dict[str, Any]]
|
|
293
|
+
) -> Dict[str, str]:
|
|
294
|
+
"""
|
|
295
|
+
Extract camera info from stream_info.
|
|
296
|
+
|
|
297
|
+
Stream info structure example:
|
|
298
|
+
{
|
|
299
|
+
'broker': 'localhost:9092',
|
|
300
|
+
'topic': '692d7bde42582ffde3611908_input_topic', # camera_id is here!
|
|
301
|
+
'stream_time': '2025-12-02-05:09:53.914224 UTC',
|
|
302
|
+
'camera_info': {
|
|
303
|
+
'camera_name': 'cusstomer-cam-1',
|
|
304
|
+
'camera_group': 'staging-customer-1',
|
|
305
|
+
'location': '6908756db129880c34f2e09a'
|
|
306
|
+
},
|
|
307
|
+
'frame_id': '...'
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
stream_info: Stream metadata from usecase
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dict with camera_id, camera_name, app_deployment_id, application_id
|
|
315
|
+
"""
|
|
316
|
+
result = {
|
|
317
|
+
"camera_id": "",
|
|
318
|
+
"camera_name": "",
|
|
319
|
+
"app_deployment_id": "",
|
|
320
|
+
"application_id": ""
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if not stream_info:
|
|
324
|
+
self.logger.debug("[BUSINESS_METRICS_MANAGER] _extract_camera_info_from_stream: stream_info is None/empty")
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] _extract_camera_info_from_stream: stream_info keys = {list(stream_info.keys())}")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
# Try multiple paths to get camera info
|
|
331
|
+
# Path 1: Direct camera_info in stream_info (most common for streaming)
|
|
332
|
+
camera_info = stream_info.get("camera_info", {}) or {}
|
|
333
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Direct camera_info = {camera_info}")
|
|
334
|
+
|
|
335
|
+
# Path 2: From input_settings -> input_stream pattern
|
|
336
|
+
input_settings = stream_info.get("input_settings", {}) or {}
|
|
337
|
+
input_stream = input_settings.get("input_stream", {}) or {}
|
|
338
|
+
input_camera_info = input_stream.get("camera_info", {}) or {}
|
|
339
|
+
|
|
340
|
+
# Path 3: From input_streams array
|
|
341
|
+
input_streams = stream_info.get("input_streams", [])
|
|
342
|
+
if input_streams and len(input_streams) > 0:
|
|
343
|
+
input_data = input_streams[0] if isinstance(input_streams[0], dict) else {}
|
|
344
|
+
input_stream_inner = input_data.get("input_stream", input_data)
|
|
345
|
+
input_camera_info = input_stream_inner.get("camera_info", {}) or input_camera_info
|
|
346
|
+
|
|
347
|
+
# Path 4: Extract camera_id from topic field (e.g., "692d7bde42582ffde3611908_input_topic")
|
|
348
|
+
topic = stream_info.get("topic", "")
|
|
349
|
+
camera_id_from_topic = ""
|
|
350
|
+
if topic and "_input_topic" in topic:
|
|
351
|
+
camera_id_from_topic = topic.replace("_input_topic", "").strip()
|
|
352
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Extracted camera_id from topic: {camera_id_from_topic}")
|
|
353
|
+
|
|
354
|
+
# Merge all sources, preferring non-empty values
|
|
355
|
+
# camera_name - prefer camera_info.camera_name
|
|
356
|
+
result["camera_name"] = (
|
|
357
|
+
camera_info.get("camera_name", "") or
|
|
358
|
+
input_camera_info.get("camera_name", "") or
|
|
359
|
+
stream_info.get("camera_name", "") or
|
|
360
|
+
input_settings.get("camera_name", "") or
|
|
361
|
+
""
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# camera_id - try topic extraction first, then other sources
|
|
365
|
+
result["camera_id"] = (
|
|
366
|
+
camera_id_from_topic or
|
|
367
|
+
camera_info.get("camera_id", "") or
|
|
368
|
+
input_camera_info.get("camera_id", "") or
|
|
369
|
+
stream_info.get("camera_id", "") or
|
|
370
|
+
input_settings.get("camera_id", "") or
|
|
371
|
+
camera_info.get("cameraId", "") or
|
|
372
|
+
input_camera_info.get("cameraId", "") or
|
|
373
|
+
""
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# app_deployment_id
|
|
377
|
+
result["app_deployment_id"] = (
|
|
378
|
+
stream_info.get("app_deployment_id", "") or
|
|
379
|
+
stream_info.get("appDeploymentId", "") or
|
|
380
|
+
input_settings.get("app_deployment_id", "") or
|
|
381
|
+
input_settings.get("appDeploymentId", "") or
|
|
382
|
+
camera_info.get("app_deployment_id", "") or
|
|
383
|
+
""
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# application_id
|
|
387
|
+
result["application_id"] = (
|
|
388
|
+
stream_info.get("application_id", "") or
|
|
389
|
+
stream_info.get("applicationId", "") or
|
|
390
|
+
input_settings.get("application_id", "") or
|
|
391
|
+
input_settings.get("applicationId", "") or
|
|
392
|
+
camera_info.get("application_id", "") or
|
|
393
|
+
""
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Extracted camera info: {result}")
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
self.logger.error(f"[BUSINESS_METRICS_MANAGER] Error extracting camera info: {e}", exc_info=True)
|
|
400
|
+
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
def process_metrics(
|
|
404
|
+
self,
|
|
405
|
+
camera_id: str,
|
|
406
|
+
metrics_data: Dict[str, Any],
|
|
407
|
+
stream_info: Optional[Dict[str, Any]] = None
|
|
408
|
+
) -> bool:
|
|
409
|
+
"""
|
|
410
|
+
Process business metrics and add to aggregation.
|
|
411
|
+
|
|
412
|
+
This method:
|
|
413
|
+
1. Extracts camera info from stream_info
|
|
414
|
+
2. Adds each metric value to the appropriate aggregator
|
|
415
|
+
3. Checks if aggregation interval has passed and publishes if so
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
camera_id: Unique camera identifier
|
|
419
|
+
metrics_data: Business metrics dictionary from usecase
|
|
420
|
+
stream_info: Stream metadata
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
True if metrics were published, False otherwise
|
|
424
|
+
"""
|
|
425
|
+
try:
|
|
426
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] ===== process_metrics START =====")
|
|
427
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Input camera_id param: {camera_id}")
|
|
428
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] metrics_data keys: {list(metrics_data.keys()) if metrics_data else 'None'}")
|
|
429
|
+
|
|
430
|
+
if not metrics_data or not isinstance(metrics_data, dict):
|
|
431
|
+
self.logger.debug("[BUSINESS_METRICS_MANAGER] Empty or invalid metrics data, skipping")
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
# Extract camera info from stream_info
|
|
435
|
+
camera_info = self._extract_camera_info_from_stream(stream_info)
|
|
436
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Extracted camera_info: {camera_info}")
|
|
437
|
+
|
|
438
|
+
# Get factory app_deployment_id and application_id if available (from jobParams)
|
|
439
|
+
factory_app_deployment_id = ""
|
|
440
|
+
factory_application_id = ""
|
|
441
|
+
if self._factory_ref:
|
|
442
|
+
factory_app_deployment_id = self._factory_ref._app_deployment_id or ""
|
|
443
|
+
factory_application_id = self._factory_ref._application_id or ""
|
|
444
|
+
self.logger.debug(
|
|
445
|
+
f"[BUSINESS_METRICS_MANAGER] Factory values - "
|
|
446
|
+
f"app_deployment_id: {factory_app_deployment_id}, application_id: {factory_application_id}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Use extracted or fallback values
|
|
450
|
+
# Priority: stream_info > factory (from jobParams)
|
|
451
|
+
final_camera_id = camera_info.get("camera_id") or camera_id or ""
|
|
452
|
+
final_camera_name = camera_info.get("camera_name") or ""
|
|
453
|
+
final_app_deployment_id = camera_info.get("app_deployment_id") or factory_app_deployment_id or ""
|
|
454
|
+
final_application_id = camera_info.get("application_id") or factory_application_id or ""
|
|
455
|
+
|
|
456
|
+
self.logger.info(
|
|
457
|
+
f"[BUSINESS_METRICS_MANAGER] Final values - camera_id={final_camera_id}, "
|
|
458
|
+
f"camera_name={final_camera_name}, app_deployment_id={final_app_deployment_id}, "
|
|
459
|
+
f"application_id={final_application_id}"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
with self._states_lock:
|
|
463
|
+
# Get or create state for this camera
|
|
464
|
+
if final_camera_id not in self._camera_states:
|
|
465
|
+
self._camera_states[final_camera_id] = CameraMetricsState(
|
|
466
|
+
camera_id=final_camera_id,
|
|
467
|
+
camera_name=final_camera_name,
|
|
468
|
+
app_deployment_id=final_app_deployment_id,
|
|
469
|
+
application_id=final_application_id
|
|
470
|
+
)
|
|
471
|
+
self.logger.info(
|
|
472
|
+
f"[BUSINESS_METRICS_MANAGER] ✓ Created new state for camera: {final_camera_id}"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
state = self._camera_states[final_camera_id]
|
|
476
|
+
|
|
477
|
+
# Update camera info if we have better values
|
|
478
|
+
if final_camera_name and not state.camera_name:
|
|
479
|
+
state.camera_name = final_camera_name
|
|
480
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Updated camera_name to: {final_camera_name}")
|
|
481
|
+
if final_app_deployment_id and not state.app_deployment_id:
|
|
482
|
+
state.app_deployment_id = final_app_deployment_id
|
|
483
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Updated app_deployment_id to: {final_app_deployment_id}")
|
|
484
|
+
if final_application_id and not state.application_id:
|
|
485
|
+
state.application_id = final_application_id
|
|
486
|
+
|
|
487
|
+
# Add each metric value to aggregator
|
|
488
|
+
metrics_added = 0
|
|
489
|
+
for metric_name, value in metrics_data.items():
|
|
490
|
+
# Skip non-numeric fields and complex objects
|
|
491
|
+
if metric_name in ["peak_areas", "optimization_opportunities"]:
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
# Handle area_utilization which is a dict
|
|
495
|
+
if metric_name == "area_utilization" and isinstance(value, dict):
|
|
496
|
+
# Average all area utilization values
|
|
497
|
+
area_values = [v for v in value.values() if isinstance(v, (int, float))]
|
|
498
|
+
if area_values:
|
|
499
|
+
value = sum(area_values) / len(area_values)
|
|
500
|
+
else:
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
# Only process numeric values
|
|
504
|
+
if isinstance(value, (int, float)):
|
|
505
|
+
agg_type = self.metrics_config.get(metric_name, "mean")
|
|
506
|
+
with self._states_lock:
|
|
507
|
+
state.add_metric_value(metric_name, value, agg_type)
|
|
508
|
+
metrics_added += 1
|
|
509
|
+
|
|
510
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] Added {metrics_added} metric values to aggregator")
|
|
511
|
+
|
|
512
|
+
# Check if we should publish (interval elapsed)
|
|
513
|
+
current_time = time.time()
|
|
514
|
+
should_publish = False
|
|
515
|
+
elapsed = 0.0
|
|
516
|
+
metrics_count = 0
|
|
517
|
+
|
|
518
|
+
with self._states_lock:
|
|
519
|
+
elapsed = current_time - state.last_push_time
|
|
520
|
+
has_metrics = state.has_metrics()
|
|
521
|
+
metrics_count = sum(len(agg.values) for agg in state.metrics.values())
|
|
522
|
+
|
|
523
|
+
self.logger.debug(
|
|
524
|
+
f"[BUSINESS_METRICS_MANAGER] Publish check - elapsed={elapsed:.1f}s, "
|
|
525
|
+
f"interval={self.aggregation_interval}s, has_metrics={has_metrics}, "
|
|
526
|
+
f"total_values_count={metrics_count}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if elapsed >= self.aggregation_interval and has_metrics:
|
|
530
|
+
should_publish = True
|
|
531
|
+
self.logger.info(
|
|
532
|
+
f"[BUSINESS_METRICS_MANAGER] ✓ PUBLISH CONDITION MET! "
|
|
533
|
+
f"elapsed={elapsed:.1f}s >= interval={self.aggregation_interval}s"
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
remaining = self.aggregation_interval - elapsed
|
|
537
|
+
self.logger.debug(
|
|
538
|
+
f"[BUSINESS_METRICS_MANAGER] Not publishing yet. "
|
|
539
|
+
f"Remaining time: {remaining:.1f}s, metrics_count={metrics_count}"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if should_publish:
|
|
543
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] Triggering publish for camera: {final_camera_id}")
|
|
544
|
+
return self._publish_camera_metrics(final_camera_id)
|
|
545
|
+
|
|
546
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER] ===== process_metrics END (no publish) =====")
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
except Exception as e:
|
|
550
|
+
self.logger.error(
|
|
551
|
+
f"[BUSINESS_METRICS_MANAGER] Error processing metrics: {e}",
|
|
552
|
+
exc_info=True
|
|
553
|
+
)
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
def _publish_camera_metrics(self, camera_id: str) -> bool:
|
|
557
|
+
"""
|
|
558
|
+
Publish aggregated metrics for a specific camera.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
camera_id: Camera identifier
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
True if published successfully, False otherwise
|
|
565
|
+
"""
|
|
566
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] ========== PUBLISHING METRICS ==========")
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
with self._states_lock:
|
|
570
|
+
if camera_id not in self._camera_states:
|
|
571
|
+
self.logger.warning(
|
|
572
|
+
f"[BUSINESS_METRICS_MANAGER] No state found for camera: {camera_id}"
|
|
573
|
+
)
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
state = self._camera_states[camera_id]
|
|
577
|
+
|
|
578
|
+
if not state.has_metrics():
|
|
579
|
+
self.logger.debug(
|
|
580
|
+
f"[BUSINESS_METRICS_MANAGER] No metrics to publish for camera: {camera_id}"
|
|
581
|
+
)
|
|
582
|
+
return False
|
|
583
|
+
|
|
584
|
+
# Build the message
|
|
585
|
+
aggregated_metrics = state.get_aggregated_metrics()
|
|
586
|
+
|
|
587
|
+
message = {
|
|
588
|
+
"camera_id": state.camera_id,
|
|
589
|
+
"camera_name": state.camera_name,
|
|
590
|
+
"app_deployment_id": state.app_deployment_id,
|
|
591
|
+
"application_id": state.application_id,
|
|
592
|
+
"business_metrics": aggregated_metrics,
|
|
593
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
594
|
+
"aggregation_interval_seconds": self.aggregation_interval
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# Reset metrics after building message (inside lock)
|
|
598
|
+
state.reset_metrics()
|
|
599
|
+
|
|
600
|
+
self.logger.info(
|
|
601
|
+
f"[BUSINESS_METRICS_MANAGER] Built metrics message: "
|
|
602
|
+
f"{json.dumps(message, default=str)[:500]}..."
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
success = False
|
|
606
|
+
|
|
607
|
+
# Try Redis first (primary)
|
|
608
|
+
if self.redis_client:
|
|
609
|
+
try:
|
|
610
|
+
self.logger.debug(
|
|
611
|
+
f"[BUSINESS_METRICS_MANAGER] Publishing to Redis stream: {self.output_topic}"
|
|
612
|
+
)
|
|
613
|
+
self._publish_to_redis(self.output_topic, message)
|
|
614
|
+
self.logger.info(
|
|
615
|
+
f"[BUSINESS_METRICS_MANAGER] ✓ Metrics published to Redis"
|
|
616
|
+
)
|
|
617
|
+
success = True
|
|
618
|
+
except Exception as e:
|
|
619
|
+
self.logger.error(
|
|
620
|
+
f"[BUSINESS_METRICS_MANAGER] ❌ Redis publish failed: {e}",
|
|
621
|
+
exc_info=True
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Fallback to Kafka if Redis failed or no Redis client
|
|
625
|
+
if not success and self.kafka_client:
|
|
626
|
+
try:
|
|
627
|
+
self.logger.debug(
|
|
628
|
+
f"[BUSINESS_METRICS_MANAGER] Publishing to Kafka topic: {self.output_topic}"
|
|
629
|
+
)
|
|
630
|
+
self._publish_to_kafka(self.output_topic, message)
|
|
631
|
+
self.logger.info(
|
|
632
|
+
f"[BUSINESS_METRICS_MANAGER] ✓ Metrics published to Kafka"
|
|
633
|
+
)
|
|
634
|
+
success = True
|
|
635
|
+
except Exception as e:
|
|
636
|
+
self.logger.error(
|
|
637
|
+
f"[BUSINESS_METRICS_MANAGER] ❌ Kafka publish failed: {e}",
|
|
638
|
+
exc_info=True
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if success:
|
|
642
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] ========== METRICS PUBLISHED ==========")
|
|
643
|
+
else:
|
|
644
|
+
self.logger.error(
|
|
645
|
+
f"[BUSINESS_METRICS_MANAGER] ❌ METRICS NOT PUBLISHED (both transports failed)"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
return success
|
|
649
|
+
|
|
650
|
+
except Exception as e:
|
|
651
|
+
self.logger.error(
|
|
652
|
+
f"[BUSINESS_METRICS_MANAGER] Error publishing metrics: {e}",
|
|
653
|
+
exc_info=True
|
|
654
|
+
)
|
|
655
|
+
return False
|
|
656
|
+
|
|
657
|
+
def _publish_to_redis(self, topic: str, message: Dict[str, Any]):
|
|
658
|
+
"""Publish message to Redis stream."""
|
|
659
|
+
try:
|
|
660
|
+
self.redis_client.add_message(
|
|
661
|
+
topic_or_channel=topic,
|
|
662
|
+
message=json.dumps(message),
|
|
663
|
+
key=message.get("camera_id", "")
|
|
664
|
+
)
|
|
665
|
+
except Exception as e:
|
|
666
|
+
self.logger.error(f"[BUSINESS_METRICS_MANAGER] Redis publish error: {e}")
|
|
667
|
+
raise
|
|
668
|
+
|
|
669
|
+
def _publish_to_kafka(self, topic: str, message: Dict[str, Any]):
|
|
670
|
+
"""Publish message to Kafka topic."""
|
|
671
|
+
try:
|
|
672
|
+
self.kafka_client.add_message(
|
|
673
|
+
topic_or_channel=topic,
|
|
674
|
+
message=json.dumps(message),
|
|
675
|
+
key=message.get("camera_id", "")
|
|
676
|
+
)
|
|
677
|
+
except Exception as e:
|
|
678
|
+
self.logger.error(f"[BUSINESS_METRICS_MANAGER] Kafka publish error: {e}")
|
|
679
|
+
raise
|
|
680
|
+
|
|
681
|
+
def reset_camera_state(self, camera_id: str):
|
|
682
|
+
"""Reset metrics state for a specific camera."""
|
|
683
|
+
with self._states_lock:
|
|
684
|
+
if camera_id in self._camera_states:
|
|
685
|
+
self._camera_states[camera_id].reset_metrics()
|
|
686
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] Reset state for camera: {camera_id}")
|
|
687
|
+
|
|
688
|
+
def get_camera_state(self, camera_id: str) -> Optional[Dict[str, Any]]:
|
|
689
|
+
"""Get current metrics state for a camera (for debugging)."""
|
|
690
|
+
with self._states_lock:
|
|
691
|
+
state = self._camera_states.get(camera_id)
|
|
692
|
+
if state:
|
|
693
|
+
return {
|
|
694
|
+
"camera_id": state.camera_id,
|
|
695
|
+
"camera_name": state.camera_name,
|
|
696
|
+
"app_deployment_id": state.app_deployment_id,
|
|
697
|
+
"application_id": state.application_id,
|
|
698
|
+
"metrics_count": {
|
|
699
|
+
name: len(agg.values)
|
|
700
|
+
for name, agg in state.metrics.items()
|
|
701
|
+
},
|
|
702
|
+
"last_push_time": state.last_push_time,
|
|
703
|
+
"seconds_since_push": time.time() - state.last_push_time
|
|
704
|
+
}
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
def get_all_camera_states(self) -> Dict[str, Dict[str, Any]]:
|
|
708
|
+
"""Get all camera states for debugging/monitoring."""
|
|
709
|
+
with self._states_lock:
|
|
710
|
+
return {
|
|
711
|
+
cam_id: {
|
|
712
|
+
"camera_id": state.camera_id,
|
|
713
|
+
"camera_name": state.camera_name,
|
|
714
|
+
"metrics_count": {
|
|
715
|
+
name: len(agg.values)
|
|
716
|
+
for name, agg in state.metrics.items()
|
|
717
|
+
},
|
|
718
|
+
"last_push_time": state.last_push_time,
|
|
719
|
+
"seconds_since_push": time.time() - state.last_push_time
|
|
720
|
+
}
|
|
721
|
+
for cam_id, state in self._camera_states.items()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
def force_publish_all(self) -> int:
|
|
725
|
+
"""Force publish all cameras with pending metrics. Returns count published."""
|
|
726
|
+
published_count = 0
|
|
727
|
+
# Collect camera IDs with pending metrics without holding the lock during publish
|
|
728
|
+
with self._states_lock:
|
|
729
|
+
camera_ids = [cam_id for cam_id, state in self._camera_states.items() if state.has_metrics()]
|
|
730
|
+
for camera_id in camera_ids:
|
|
731
|
+
if self._publish_camera_metrics(camera_id):
|
|
732
|
+
published_count += 1
|
|
733
|
+
return published_count
|
|
734
|
+
|
|
735
|
+
def set_metrics_config(self, metrics_config: Dict[str, str]):
|
|
736
|
+
"""
|
|
737
|
+
Set aggregation type configuration for metrics.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
metrics_config: Dict of metric_name -> aggregation_type
|
|
741
|
+
"""
|
|
742
|
+
self.metrics_config = metrics_config
|
|
743
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER] Updated metrics config: {metrics_config}")
|
|
744
|
+
|
|
745
|
+
def set_aggregation_interval(self, interval_seconds: int):
|
|
746
|
+
"""
|
|
747
|
+
Set the aggregation interval.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
interval_seconds: New interval in seconds
|
|
751
|
+
"""
|
|
752
|
+
self.aggregation_interval = interval_seconds
|
|
753
|
+
self.logger.info(
|
|
754
|
+
f"[BUSINESS_METRICS_MANAGER] Updated aggregation interval to {interval_seconds}s"
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class BusinessMetricsManagerFactory:
|
|
759
|
+
"""
|
|
760
|
+
Factory class for creating BUSINESS_METRICS_MANAGER instances.
|
|
761
|
+
|
|
762
|
+
Handles session initialization and Redis/Kafka client creation
|
|
763
|
+
following the same pattern as IncidentManagerFactory.
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
ACTION_ID_PATTERN = re.compile(r"^[0-9a-f]{8,}$", re.IGNORECASE)
|
|
767
|
+
|
|
768
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
769
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
770
|
+
self._initialized = False
|
|
771
|
+
self._business_metrics_manager: Optional[BUSINESS_METRICS_MANAGER] = None
|
|
772
|
+
|
|
773
|
+
# Store these for later access
|
|
774
|
+
self._session = None
|
|
775
|
+
self._action_id: Optional[str] = None
|
|
776
|
+
self._instance_id: Optional[str] = None
|
|
777
|
+
self._deployment_id: Optional[str] = None
|
|
778
|
+
self._app_deployment_id: Optional[str] = None
|
|
779
|
+
self._application_id: Optional[str] = None # Store application_id from jobParams
|
|
780
|
+
self._external_ip: Optional[str] = None
|
|
781
|
+
|
|
782
|
+
def initialize(
|
|
783
|
+
self,
|
|
784
|
+
config: Any,
|
|
785
|
+
aggregation_interval: int = DEFAULT_AGGREGATION_INTERVAL,
|
|
786
|
+
metrics_config: Optional[Dict[str, str]] = None
|
|
787
|
+
) -> Optional[BUSINESS_METRICS_MANAGER]:
|
|
788
|
+
"""
|
|
789
|
+
Initialize and return BUSINESS_METRICS_MANAGER with Redis/Kafka clients.
|
|
790
|
+
|
|
791
|
+
This follows the same pattern as IncidentManagerFactory for
|
|
792
|
+
session initialization and Redis/Kafka client creation.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
config: Configuration object with session, server_id, etc.
|
|
796
|
+
aggregation_interval: Interval in seconds for aggregation (default 300)
|
|
797
|
+
metrics_config: Dict of metric_name -> aggregation_type
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
BUSINESS_METRICS_MANAGER instance or None if initialization failed
|
|
801
|
+
"""
|
|
802
|
+
if self._initialized and self._business_metrics_manager is not None:
|
|
803
|
+
self.logger.debug(
|
|
804
|
+
"[BUSINESS_METRICS_MANAGER_FACTORY] Already initialized, returning existing instance"
|
|
805
|
+
)
|
|
806
|
+
return self._business_metrics_manager
|
|
807
|
+
|
|
808
|
+
try:
|
|
809
|
+
# Import required modules
|
|
810
|
+
from matrice_common.stream.matrice_stream import MatriceStream, StreamType
|
|
811
|
+
from matrice_common.session import Session
|
|
812
|
+
|
|
813
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] ===== STARTING INITIALIZATION =====")
|
|
814
|
+
|
|
815
|
+
# Get or create session
|
|
816
|
+
self._session = getattr(config, 'session', None)
|
|
817
|
+
if not self._session:
|
|
818
|
+
self.logger.info(
|
|
819
|
+
"[BUSINESS_METRICS_MANAGER_FACTORY] No session in config, creating from environment..."
|
|
820
|
+
)
|
|
821
|
+
account_number = os.getenv("MATRICE_ACCOUNT_NUMBER", "")
|
|
822
|
+
access_key_id = os.getenv("MATRICE_ACCESS_KEY_ID", "")
|
|
823
|
+
secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY", "")
|
|
824
|
+
project_id = os.getenv("MATRICE_PROJECT_ID", "")
|
|
825
|
+
|
|
826
|
+
self.logger.debug(
|
|
827
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Env vars - "
|
|
828
|
+
f"account: {'SET' if account_number else 'NOT SET'}, "
|
|
829
|
+
f"access_key: {'SET' if access_key_id else 'NOT SET'}, "
|
|
830
|
+
f"secret: {'SET' if secret_key else 'NOT SET'}"
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
self._session = Session(
|
|
834
|
+
account_number=account_number,
|
|
835
|
+
access_key=access_key_id,
|
|
836
|
+
secret_key=secret_key,
|
|
837
|
+
project_id=project_id,
|
|
838
|
+
)
|
|
839
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Created session from environment")
|
|
840
|
+
else:
|
|
841
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Using session from config")
|
|
842
|
+
|
|
843
|
+
rpc = self._session.rpc
|
|
844
|
+
|
|
845
|
+
# Discover action_id
|
|
846
|
+
self._action_id = self._discover_action_id()
|
|
847
|
+
if not self._action_id:
|
|
848
|
+
self.logger.error("[BUSINESS_METRICS_MANAGER_FACTORY] ❌ Could not discover action_id")
|
|
849
|
+
print("----- BUSINESS METRICS MANAGER ACTION DISCOVERY -----")
|
|
850
|
+
print("action_id: NOT FOUND")
|
|
851
|
+
print("------------------------------------------------------")
|
|
852
|
+
self._initialized = True
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Discovered action_id: {self._action_id}")
|
|
856
|
+
|
|
857
|
+
# Fetch action details
|
|
858
|
+
action_details = {}
|
|
859
|
+
try:
|
|
860
|
+
action_url = f"/v1/actions/action/{self._action_id}/details"
|
|
861
|
+
action_resp = rpc.get(action_url)
|
|
862
|
+
if not (action_resp and action_resp.get("success", False)):
|
|
863
|
+
raise RuntimeError(
|
|
864
|
+
action_resp.get("message", "Unknown error")
|
|
865
|
+
if isinstance(action_resp, dict) else "Unknown error"
|
|
866
|
+
)
|
|
867
|
+
action_doc = action_resp.get("data", {}) if isinstance(action_resp, dict) else {}
|
|
868
|
+
action_details = action_doc.get("actionDetails", {}) if isinstance(action_doc, dict) else {}
|
|
869
|
+
|
|
870
|
+
# IMPORTANT: jobParams contains application_id
|
|
871
|
+
# Structure: response['data']['jobParams']['application_id']
|
|
872
|
+
job_params = action_doc.get("jobParams", {}) if isinstance(action_doc, dict) else {}
|
|
873
|
+
|
|
874
|
+
# Extract server details
|
|
875
|
+
server_id = (
|
|
876
|
+
action_details.get("serverId")
|
|
877
|
+
or action_details.get("server_id")
|
|
878
|
+
or action_details.get("serverID")
|
|
879
|
+
or action_details.get("redis_server_id")
|
|
880
|
+
or action_details.get("kafka_server_id")
|
|
881
|
+
)
|
|
882
|
+
server_type = (
|
|
883
|
+
action_details.get("serverType")
|
|
884
|
+
or action_details.get("server_type")
|
|
885
|
+
or action_details.get("type")
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
# Store identifiers
|
|
889
|
+
self._deployment_id = action_details.get("_idDeployment") or action_details.get("deployment_id")
|
|
890
|
+
|
|
891
|
+
# app_deployment_id: check actionDetails first, then jobParams
|
|
892
|
+
self._app_deployment_id = (
|
|
893
|
+
action_details.get("app_deployment_id") or
|
|
894
|
+
action_details.get("appDeploymentId") or
|
|
895
|
+
action_details.get("app_deploymentId") or
|
|
896
|
+
job_params.get("app_deployment_id") or
|
|
897
|
+
job_params.get("appDeploymentId") or
|
|
898
|
+
job_params.get("app_deploymentId") or
|
|
899
|
+
""
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# application_id: PRIMARILY from jobParams (this is where it lives!)
|
|
903
|
+
# response['data']['jobParams'].get('application_id', '')
|
|
904
|
+
self._application_id = (
|
|
905
|
+
job_params.get("application_id") or
|
|
906
|
+
job_params.get("applicationId") or
|
|
907
|
+
job_params.get("app_id") or
|
|
908
|
+
job_params.get("appId") or
|
|
909
|
+
action_details.get("application_id") or
|
|
910
|
+
action_details.get("applicationId") or
|
|
911
|
+
""
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
self._instance_id = action_details.get("instanceID") or action_details.get("instanceId")
|
|
915
|
+
self._external_ip = action_details.get("externalIP") or action_details.get("externalIp")
|
|
916
|
+
|
|
917
|
+
print("----- BUSINESS METRICS MANAGER ACTION DETAILS -----")
|
|
918
|
+
print(f"action_id: {self._action_id}")
|
|
919
|
+
print(f"server_type: {server_type}")
|
|
920
|
+
print(f"server_id: {server_id}")
|
|
921
|
+
print(f"deployment_id: {self._deployment_id}")
|
|
922
|
+
print(f"app_deployment_id: {self._app_deployment_id}")
|
|
923
|
+
print(f"application_id: {self._application_id}")
|
|
924
|
+
print(f"instance_id: {self._instance_id}")
|
|
925
|
+
print(f"external_ip: {self._external_ip}")
|
|
926
|
+
print(f"jobParams keys: {list(job_params.keys()) if job_params else []}")
|
|
927
|
+
print("----------------------------------------------------")
|
|
928
|
+
|
|
929
|
+
self.logger.info(
|
|
930
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Action details - server_type={server_type}, "
|
|
931
|
+
f"instance_id={self._instance_id}, "
|
|
932
|
+
f"app_deployment_id={self._app_deployment_id}, application_id={self._application_id}"
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Log all available keys for debugging
|
|
936
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER_FACTORY] actionDetails keys: {list(action_details.keys())}")
|
|
937
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER_FACTORY] jobParams keys: {list(job_params.keys()) if job_params else []}")
|
|
938
|
+
|
|
939
|
+
except Exception as e:
|
|
940
|
+
self.logger.error(
|
|
941
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] ❌ Failed to fetch action details: {e}",
|
|
942
|
+
exc_info=True
|
|
943
|
+
)
|
|
944
|
+
print("----- BUSINESS METRICS MANAGER ACTION DETAILS ERROR -----")
|
|
945
|
+
print(f"action_id: {self._action_id}")
|
|
946
|
+
print(f"error: {e}")
|
|
947
|
+
print("---------------------------------------------------------")
|
|
948
|
+
self._initialized = True
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
# Determine localhost vs cloud using externalIP from action_details
|
|
952
|
+
is_localhost = False
|
|
953
|
+
public_ip = self._get_public_ip()
|
|
954
|
+
|
|
955
|
+
# Get server host from action_details
|
|
956
|
+
server_host = (
|
|
957
|
+
action_details.get("externalIP")
|
|
958
|
+
or action_details.get("external_IP")
|
|
959
|
+
or action_details.get("externalip")
|
|
960
|
+
or action_details.get("external_ip")
|
|
961
|
+
or action_details.get("externalIp")
|
|
962
|
+
or action_details.get("external_Ip")
|
|
963
|
+
)
|
|
964
|
+
print(f"server_host: {server_host}")
|
|
965
|
+
self.logger.info(f"[BUSINESS_METRICS_MANAGER_FACTORY] DEBUG - server_host: {server_host}")
|
|
966
|
+
|
|
967
|
+
localhost_indicators = ["localhost", "127.0.0.1", "0.0.0.0"]
|
|
968
|
+
if server_host in localhost_indicators:
|
|
969
|
+
is_localhost = True
|
|
970
|
+
self.logger.info(
|
|
971
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Detected Localhost environment "
|
|
972
|
+
f"(Public IP={public_ip}, Server IP={server_host})"
|
|
973
|
+
)
|
|
974
|
+
else:
|
|
975
|
+
is_localhost = False
|
|
976
|
+
self.logger.info(
|
|
977
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Detected Cloud environment "
|
|
978
|
+
f"(Public IP={public_ip}, Server IP={server_host})"
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
redis_client = None
|
|
982
|
+
kafka_client = None
|
|
983
|
+
|
|
984
|
+
# STRICT SWITCH: Only Redis if localhost, Only Kafka if cloud
|
|
985
|
+
if is_localhost:
|
|
986
|
+
# Initialize Redis client (ONLY) using instance_id
|
|
987
|
+
if not self._instance_id:
|
|
988
|
+
self.logger.error(
|
|
989
|
+
"[BUSINESS_METRICS_MANAGER_FACTORY] ❌ Localhost mode but instance_id missing"
|
|
990
|
+
)
|
|
991
|
+
else:
|
|
992
|
+
try:
|
|
993
|
+
url = f"/v1/actions/get_redis_server_by_instance_id/{self._instance_id}"
|
|
994
|
+
self.logger.info(
|
|
995
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Fetching Redis server info "
|
|
996
|
+
f"for instance: {self._instance_id}"
|
|
997
|
+
)
|
|
998
|
+
response = rpc.get(url)
|
|
999
|
+
|
|
1000
|
+
if isinstance(response, dict) and response.get("success", False):
|
|
1001
|
+
data = response.get("data", {})
|
|
1002
|
+
host = data.get("host")
|
|
1003
|
+
port = data.get("port")
|
|
1004
|
+
username = data.get("username")
|
|
1005
|
+
password = data.get("password", "")
|
|
1006
|
+
db_index = data.get("db", 0)
|
|
1007
|
+
conn_timeout = data.get("connection_timeout", 120)
|
|
1008
|
+
|
|
1009
|
+
print("----- BUSINESS METRICS MANAGER REDIS SERVER PARAMS -----")
|
|
1010
|
+
print(f"instance_id: {self._instance_id}")
|
|
1011
|
+
print(f"host: {host}")
|
|
1012
|
+
print(f"port: {port}")
|
|
1013
|
+
print(f"username: {username}")
|
|
1014
|
+
print(f"password: {'*' * len(password) if password else ''}")
|
|
1015
|
+
print(f"db: {db_index}")
|
|
1016
|
+
print(f"connection_timeout: {conn_timeout}")
|
|
1017
|
+
print("--------------------------------------------------------")
|
|
1018
|
+
|
|
1019
|
+
self.logger.info(
|
|
1020
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Redis params - "
|
|
1021
|
+
f"host={host}, port={port}, user={username}"
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
redis_client = MatriceStream(
|
|
1025
|
+
StreamType.REDIS,
|
|
1026
|
+
host=host,
|
|
1027
|
+
port=int(port),
|
|
1028
|
+
password=password,
|
|
1029
|
+
username=username,
|
|
1030
|
+
db=db_index,
|
|
1031
|
+
connection_timeout=conn_timeout
|
|
1032
|
+
)
|
|
1033
|
+
# Setup for metrics publishing
|
|
1034
|
+
redis_client.setup("business_metrics")
|
|
1035
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Redis client initialized")
|
|
1036
|
+
else:
|
|
1037
|
+
self.logger.warning(
|
|
1038
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Failed to fetch Redis server info: "
|
|
1039
|
+
f"{response.get('message', 'Unknown error') if isinstance(response, dict) else 'Unknown error'}"
|
|
1040
|
+
)
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
self.logger.warning(
|
|
1043
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Redis initialization failed: {e}"
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
else:
|
|
1047
|
+
# Initialize Kafka client (ONLY) using global info endpoint
|
|
1048
|
+
try:
|
|
1049
|
+
url = f"/v1/actions/get_kafka_info"
|
|
1050
|
+
self.logger.info(
|
|
1051
|
+
"[BUSINESS_METRICS_MANAGER_FACTORY] Fetching Kafka server info for Cloud mode"
|
|
1052
|
+
)
|
|
1053
|
+
response = rpc.get(url)
|
|
1054
|
+
|
|
1055
|
+
if isinstance(response, dict) and response.get("success", False):
|
|
1056
|
+
data = response.get("data", {})
|
|
1057
|
+
enc_ip = data.get("ip")
|
|
1058
|
+
enc_port = data.get("port")
|
|
1059
|
+
|
|
1060
|
+
# Decode base64 encoded values
|
|
1061
|
+
ip_addr = None
|
|
1062
|
+
port = None
|
|
1063
|
+
try:
|
|
1064
|
+
ip_addr = base64.b64decode(str(enc_ip)).decode("utf-8")
|
|
1065
|
+
except Exception:
|
|
1066
|
+
ip_addr = enc_ip
|
|
1067
|
+
try:
|
|
1068
|
+
port = base64.b64decode(str(enc_port)).decode("utf-8")
|
|
1069
|
+
except Exception:
|
|
1070
|
+
port = enc_port
|
|
1071
|
+
|
|
1072
|
+
print("----- BUSINESS METRICS MANAGER KAFKA SERVER PARAMS -----")
|
|
1073
|
+
print(f"ipAddress: {ip_addr}")
|
|
1074
|
+
print(f"port: {port}")
|
|
1075
|
+
print("--------------------------------------------------------")
|
|
1076
|
+
|
|
1077
|
+
self.logger.info(
|
|
1078
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Kafka params - ip={ip_addr}, port={port}"
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
bootstrap_servers = f"{ip_addr}:{port}"
|
|
1082
|
+
kafka_client = MatriceStream(
|
|
1083
|
+
StreamType.KAFKA,
|
|
1084
|
+
bootstrap_servers=bootstrap_servers,
|
|
1085
|
+
sasl_mechanism="SCRAM-SHA-256",
|
|
1086
|
+
sasl_username="matrice-sdk-user",
|
|
1087
|
+
sasl_password="matrice-sdk-password",
|
|
1088
|
+
security_protocol="SASL_PLAINTEXT"
|
|
1089
|
+
)
|
|
1090
|
+
# Setup for metrics publishing (producer-only; no consumer group needed)
|
|
1091
|
+
kafka_client.setup("business_metrics")
|
|
1092
|
+
self.logger.info(
|
|
1093
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Kafka client initialized "
|
|
1094
|
+
f"(servers={bootstrap_servers})"
|
|
1095
|
+
)
|
|
1096
|
+
else:
|
|
1097
|
+
self.logger.warning(
|
|
1098
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Failed to fetch Kafka server info: "
|
|
1099
|
+
f"{response.get('message', 'Unknown error') if isinstance(response, dict) else 'Unknown error'}"
|
|
1100
|
+
)
|
|
1101
|
+
except Exception as e:
|
|
1102
|
+
self.logger.warning(
|
|
1103
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Kafka initialization failed: {e}"
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
# Create business metrics manager if we have at least one transport
|
|
1107
|
+
if redis_client or kafka_client:
|
|
1108
|
+
self._business_metrics_manager = BUSINESS_METRICS_MANAGER(
|
|
1109
|
+
redis_client=redis_client,
|
|
1110
|
+
kafka_client=kafka_client,
|
|
1111
|
+
output_topic="business_metrics",
|
|
1112
|
+
aggregation_interval=aggregation_interval,
|
|
1113
|
+
metrics_config=metrics_config or DEFAULT_METRICS_CONFIG.copy(),
|
|
1114
|
+
logger=self.logger
|
|
1115
|
+
)
|
|
1116
|
+
# Set factory reference for accessing deployment info
|
|
1117
|
+
self._business_metrics_manager.set_factory_ref(self)
|
|
1118
|
+
# Start the timer thread
|
|
1119
|
+
self._business_metrics_manager.start()
|
|
1120
|
+
|
|
1121
|
+
transport = "Redis" if redis_client else "Kafka"
|
|
1122
|
+
self.logger.info(
|
|
1123
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] ✓ Business metrics manager created with {transport}"
|
|
1124
|
+
)
|
|
1125
|
+
print(f"----- BUSINESS METRICS MANAGER INITIALIZED ({transport}) -----")
|
|
1126
|
+
else:
|
|
1127
|
+
self.logger.warning(
|
|
1128
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] No {'Redis' if is_localhost else 'Kafka'} client available, "
|
|
1129
|
+
f"business metrics manager not created"
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
self._initialized = True
|
|
1133
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] ===== INITIALIZATION COMPLETE =====")
|
|
1134
|
+
return self._business_metrics_manager
|
|
1135
|
+
|
|
1136
|
+
except ImportError as e:
|
|
1137
|
+
self.logger.error(f"[BUSINESS_METRICS_MANAGER_FACTORY] Import error: {e}")
|
|
1138
|
+
self._initialized = True
|
|
1139
|
+
return None
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
self.logger.error(
|
|
1142
|
+
f"[BUSINESS_METRICS_MANAGER_FACTORY] Initialization failed: {e}",
|
|
1143
|
+
exc_info=True
|
|
1144
|
+
)
|
|
1145
|
+
self._initialized = True
|
|
1146
|
+
return None
|
|
1147
|
+
|
|
1148
|
+
def _discover_action_id(self) -> Optional[str]:
|
|
1149
|
+
"""Discover action_id from current working directory name (and parents)."""
|
|
1150
|
+
try:
|
|
1151
|
+
candidates: List[str] = []
|
|
1152
|
+
|
|
1153
|
+
try:
|
|
1154
|
+
cwd = Path.cwd()
|
|
1155
|
+
candidates.append(cwd.name)
|
|
1156
|
+
for parent in cwd.parents:
|
|
1157
|
+
candidates.append(parent.name)
|
|
1158
|
+
except Exception:
|
|
1159
|
+
pass
|
|
1160
|
+
|
|
1161
|
+
try:
|
|
1162
|
+
usr_src = Path("/usr/src")
|
|
1163
|
+
if usr_src.exists():
|
|
1164
|
+
for child in usr_src.iterdir():
|
|
1165
|
+
if child.is_dir():
|
|
1166
|
+
candidates.append(child.name)
|
|
1167
|
+
except Exception:
|
|
1168
|
+
pass
|
|
1169
|
+
|
|
1170
|
+
for candidate in candidates:
|
|
1171
|
+
if candidate and len(candidate) >= 8 and self.ACTION_ID_PATTERN.match(candidate):
|
|
1172
|
+
return candidate
|
|
1173
|
+
except Exception:
|
|
1174
|
+
pass
|
|
1175
|
+
return None
|
|
1176
|
+
|
|
1177
|
+
def _get_public_ip(self) -> str:
|
|
1178
|
+
"""Get the public IP address of this machine."""
|
|
1179
|
+
self.logger.info("[BUSINESS_METRICS_MANAGER_FACTORY] Fetching public IP address...")
|
|
1180
|
+
try:
|
|
1181
|
+
public_ip = urllib.request.urlopen(
|
|
1182
|
+
"https://v4.ident.me", timeout=120
|
|
1183
|
+
).read().decode("utf8").strip()
|
|
1184
|
+
self.logger.debug(f"[BUSINESS_METRICS_MANAGER_FACTORY] Public IP: {public_ip}")
|
|
1185
|
+
return public_ip
|
|
1186
|
+
except Exception as e:
|
|
1187
|
+
self.logger.warning(f"[BUSINESS_METRICS_MANAGER_FACTORY] Error fetching public IP: {e}")
|
|
1188
|
+
return "localhost"
|
|
1189
|
+
|
|
1190
|
+
def _get_backend_base_url(self) -> str:
|
|
1191
|
+
"""Resolve backend base URL based on ENV variable."""
|
|
1192
|
+
env = os.getenv("ENV", "prod").strip().lower()
|
|
1193
|
+
if env in ("prod", "production"):
|
|
1194
|
+
host = "prod.backend.app.matrice.ai"
|
|
1195
|
+
elif env in ("dev", "development"):
|
|
1196
|
+
host = "dev.backend.app.matrice.ai"
|
|
1197
|
+
else:
|
|
1198
|
+
host = "staging.backend.app.matrice.ai"
|
|
1199
|
+
return f"https://{host}"
|
|
1200
|
+
|
|
1201
|
+
@property
|
|
1202
|
+
def is_initialized(self) -> bool:
|
|
1203
|
+
return self._initialized
|
|
1204
|
+
|
|
1205
|
+
@property
|
|
1206
|
+
def business_metrics_manager(self) -> Optional[BUSINESS_METRICS_MANAGER]:
|
|
1207
|
+
return self._business_metrics_manager
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
# Module-level factory instance for convenience
|
|
1211
|
+
_default_factory: Optional[BusinessMetricsManagerFactory] = None
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def get_business_metrics_manager(
|
|
1215
|
+
config: Any,
|
|
1216
|
+
logger: Optional[logging.Logger] = None,
|
|
1217
|
+
aggregation_interval: int = DEFAULT_AGGREGATION_INTERVAL,
|
|
1218
|
+
metrics_config: Optional[Dict[str, str]] = None
|
|
1219
|
+
) -> Optional[BUSINESS_METRICS_MANAGER]:
|
|
1220
|
+
"""
|
|
1221
|
+
Get or create BUSINESS_METRICS_MANAGER instance.
|
|
1222
|
+
|
|
1223
|
+
This is a convenience function that uses a module-level factory.
|
|
1224
|
+
For more control, use BusinessMetricsManagerFactory directly.
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
config: Configuration object with session, server_id, etc.
|
|
1228
|
+
logger: Logger instance
|
|
1229
|
+
aggregation_interval: Interval in seconds for aggregation (default 300)
|
|
1230
|
+
metrics_config: Dict of metric_name -> aggregation_type
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
BUSINESS_METRICS_MANAGER instance or None
|
|
1234
|
+
"""
|
|
1235
|
+
global _default_factory
|
|
1236
|
+
|
|
1237
|
+
if _default_factory is None:
|
|
1238
|
+
_default_factory = BusinessMetricsManagerFactory(logger=logger)
|
|
1239
|
+
|
|
1240
|
+
return _default_factory.initialize(
|
|
1241
|
+
config,
|
|
1242
|
+
aggregation_interval=aggregation_interval,
|
|
1243
|
+
metrics_config=metrics_config
|
|
1244
|
+
)
|
|
1245
|
+
|