40 Commits

Author SHA1 Message Date
vitor-aignosi
a3b642dbef SIENTIAPDE-1199
Update tests in Gates and OpcRepository to enhance logging and include metadata

- Removed unnecessary debug assertion in the Gates activity test for format prediction.
- Updated the test for last timestamp with no data to include a metadata dictionary.
- Modified OpcRepository tests to utilize custom_error logging with metadata for improved error context.
2025-08-21 08:56:22 -03:00
vitor-aignosi
39ff7e2625 Merge branch 'main' into SIENTIAPDE-1199-revisar-e-testar-observabilidade 2025-08-21 08:51:41 -03:00
vitor-aignosi
7b5c519cde SIENTIAPDE-1199
Update replica count in values.yaml from 3 to 5 for increased scalability
2025-08-21 08:09:20 -03:00
Matheus Demoner
93f0e3b100 Merge pull request #15 from Aignosi/SIENTIAPDE-1169-pensar-e-projetar-testes-de-breakdown-e-performance
Sientiapde 1169 pensar e projetar testes de breakdown e performance
2025-08-20 12:50:07 -03:00
vitor-aignosi
935a9e9279 SIENTIAPDE-1199
Enhance logging in OpcRepository to include metadata for all log messages

- Introduced a metadata dictionary to the OpcRepository class for improved context in logging.
- Updated logging statements to utilize custom_info, custom_warning, and custom_error methods, ensuring all key operations and error messages include relevant metadata.
- Improved visibility of connection status, disconnection attempts, and error handling, facilitating better monitoring and debugging.
2025-08-20 11:35:42 -03:00
vitor-aignosi
40870b021a SIENTIAPDE-1199
Refactor logging in worker module to include metadata in custom info and error messages

- Updated logging statements in the worker module to utilize custom_info and custom_error methods, incorporating a metadata dictionary for enhanced context.
- Improved visibility of key operations such as starting the worker, notification handler, and activities, as well as error handling for unhandled exceptions.
2025-08-20 10:51:38 -03:00
vitor-aignosi
8b9bb8d720 SIENTIAPDE-1199
Refactor logging in Gates and MLFlow activities to use info level for key operations

- Updated logging statements in the Gates class to replace debug logs with info logs for input and output gate operations, enhancing visibility.
- Modified MLFlow class to use info logs for data transformation and prediction processes, improving clarity in the logging output.
- Adjusted OPC class to return the count of successfully written tags, providing better insight into data writing operations.
2025-08-20 10:38:08 -03:00
vitor-aignosi
89384d7783 SIENTIAPDE-1199
Update test for validate_connection method in OpcRepository to include detailed response structure

- Modified the test case to assert the response format when the connection is lost, ensuring it includes notification ID, message, block, and level attributes for better clarity in error handling.
2025-08-19 16:42:48 -03:00
vitor-aignosi
ef32167f57 SIENTIAPDE-1199
Update dependencies in requirements.txt and increment image tag in values.yaml

- Updated sientia-dataops-library dependency version from 1.4.0 to 1.4.1 in requirements.txt.
- Incremented image tag in values.yaml from 0.4.1 to 0.4.2.
2025-08-19 16:05:13 -03:00
vitor-aignosi
46e21c7538 SIENTIAPDE-1199
Enhance logging in MLFlow activity to include formatted JSON response data

- Added JSON import for better formatting of response data in debug logs.
- Updated logging statements to output transformed and prediction response data as pretty-printed JSON, improving readability and debugging context.
2025-08-19 12:12:15 -03:00
vitor-aignosi
8012ff778c SIENTIAPDE-1199
Refactor logging in OpcRepository to include full metadata in error and info messages

- Updated logging statements in the OpcRepository class to pass the complete metadata object instead of just the schedule name, enhancing the context available for debugging and monitoring.
2025-08-19 11:52:42 -03:00
vitor-aignosi
3a06f0ca39 SIENTIAPDE-1199
Update dependencies and refactor logging imports for observability

- Updated the sientia-dataops-library dependency version to 1.4.0 in requirements.txt.
- Changed image tag in values.yaml from 0.3.2 to 0.4.1.
- Refactored logging imports across multiple files to use the new observability module instead of the temporal.utils.logger.
- Updated retry policy imports in workflow files to reflect the new module structure.
2025-08-19 10:42:27 -03:00
vitor-aignosi
3ee076214c SIENTIAPDE-1169
Implement server validation and output tag management in OPC class

- Added a new method to validate the existence of OPC servers before writing data, improving error handling.
- Introduced a method to manage writing of prediction and confidence tags, streamlining the data writing process.
- Refactored the write_opc_data method to utilize the new validation and management methods for better code organization and clarity.
2025-08-19 09:07:05 -03:00
vitor-aignosi
fc6c95c264 SIENTIAPDE-1169
Refactor OPC data writing and repository initialization

- Simplified success tracking logic in the OPC class for writing prediction and confidence data.
- Removed unused pod_id attribute from OpcRepository initialization.
- Updated test cases to include pod_id for improved metrics tracking during data writing operations.
2025-08-19 08:31:58 -03:00
vitor-aignosi
cc7efba887 SIENTIAPDE-1169
Refactor OPC data writing to improve success tracking

- Updated the OPC class to store the success status of data writing operations for both prediction and confidence tags.
- Added logging for successful and failed writes to the OPC server, enhancing traceability of data operations.
2025-08-18 16:30:33 -03:00
vitor-aignosi
37a31a51a4 SIENTIAPDE-1169
Refactor OPC metrics to use unpacking for CORE_LABELS in metrics.py

- Updated the definition of prediction OPC writing metrics to utilize unpacking for CORE_LABELS, enhancing code clarity and maintainability.
2025-08-18 14:17:43 -03:00
vitor-aignosi
e567b8d0a7 SIENTIAPDE-1169
Update OPC metrics to include opc_server_id for enhanced tracking
2025-08-18 14:15:22 -03:00
vitor-aignosi
366d406689 SIENTIAPDE-1169
Enhance OpcRepository initialization to include pod_id for improved tracking
2025-08-18 14:08:00 -03:00
vitor-aignosi
f56755b69e SIENTIAPDE-1169
Refactor opc_repository.py to import metrics module

- Added import statement for the metrics module to enhance functionality in the OPC repository.
- This change supports the integration of metrics tracking for OPC operations.
2025-08-18 14:04:02 -03:00
vitor-aignosi
e35eb300cb SIENTIAPDE-1169
Implement OPC writing metrics and enhance OPC class initialization

- Added metrics for counting predictions written to the OPC server and monitoring their response times.
- Enhanced the OPC class initialization to include the pod ID for better tracking.
- Updated the write method to increment the prediction count and observe response times.
2025-08-18 13:58:43 -03:00
vitor-aignosi
6eb71739db SIENTIAPDE-1169
Update helm chart version in values.yaml to 0.5.0
2025-08-18 13:47:50 -03:00
vitor-aignosi
e82b25582c SIENTIAPDE-1169
Update helm chart version in values.yaml and modify logging in worker.py

- Updated helm upgrade command in values.yaml to version 0.5.0-uat.
- Changed log message in worker.py to indicate the start of the SDK Metrics Server, while retaining the original log for the Temporal Client.
2025-08-18 09:45:54 -03:00
vitor-aignosi
a2a9f95aa5 SIENTIAPDE-1169
SIENTIAPDE-1174 Add SDK metrics configuration and update worker for telemetry

- Introduced sdk-metrics service in values.yaml with ClusterIP configuration.
- Updated worker.py to integrate SDK metrics telemetry using the new HTTP_SDK_METRICS_PORT environment variable.
- Enhanced Prometheus configuration to bind SDK metrics to the specified port.
2025-08-18 09:41:17 -03:00
vitor-aignosi
7f3457e318 SIENTIAPDE-1169
Enhance BaseActivity initialization across multiple activities to include error counter

- Updated the initialization of the BaseActivity in Gates, MLFlow, and OPC classes to set the error counter to True, improving error tracking and handling capabilities.
2025-08-14 15:41:03 -03:00
vitor-aignosi
da10f900a8 SIENTIAPDE-1169
Update dependencies and configuration for improved functionality

- Updated sientia-dataops-library version from 1.3.7 to 1.3.8 in requirements.txt.
- Changed GITHUB_BRANCH in values.yaml to reflect new testing focus: SIENTIAPDE-1169.
- Modified notification level in gates.py from WARNING to ERROR for better error handling.
2025-08-14 15:25:04 -03:00
Bruno Domingues
f195b5116e Merge pull request #14 from Aignosi/SIENTIAPDE-1174-mapear-e-implementar-metricas-a-serem-criadas
Sientiapde 1174 mapear e implementar metricas a serem criadas
2025-08-13 16:35:04 -03:00
vitor-aignosi
dd513ddfaf SIENTIAPDE-1174
Add async test for write_metrics method in gates_activity

- Implemented a new test to validate the write_metrics functionality, ensuring metrics are correctly recorded for predictions, confidence, and response time.
- Utilized mocking to verify interactions with the metrics tracking system.
2025-08-05 14:39:02 -03:00
vitor-aignosi
fbdf9c3c6c SIENTIAPDE-1174
SIENTIAPDE-1174 Update tests to enhance MLFlow and workflow activity assertions

- Added logger initialization in MLFlowRepository for improved logging.
- Updated prediction test assertions to reflect changes in output structure.
- Increased activity method call count assertions in workflow tests for accuracy.
2025-08-05 14:19:56 -03:00
vitor-aignosi
3d7de2ef5e Merge branch 'main' into SIENTIAPDE-1174-mapear-e-implementar-metricas-a-serem-criadas 2025-08-05 14:05:09 -03:00
vitor-aignosi
c92e2f41c3 SIENTIAPDE-1174
pdate metrics tracking to include response time histogram

- Changed PREDICTION_RESPONSE_TIME_MONITOR from Gauge to Histogram for better response time analysis.
- Updated response time observation method in gates.py to utilize the new Histogram functionality.
2025-08-04 16:09:26 -03:00
vitor-aignosi
3cf7b4ec16 SIENTIAPDE-1174
SIENTIAPDE-1174 Update replica count and image tag in values.yaml

- Increased replicaCount from 1 to 3 for improved availability.
- Updated image tag from 0.3.1 to 0.3.2 for the latest features and fixes.
2025-08-04 15:24:20 -03:00
vitor-aignosi
663b286ad7 SIENTIAPDE-1174
SIENTIAPDE-1174 Update tests.ipynb to normalize counter and rollout values, and fix MLFlowRepository initialization error

- Changed execution_count for a cell to null for consistency.
- Updated normalization logic for counter and rollout values to scale between -100 and 100.
- Fixed TypeError in MLFlowRepository initialization by ensuring the logger argument is provided.
2025-08-04 10:03:15 -03:00
Bruno Domingues
28743eb720 Merge pull request #13 from Aignosi/SIENTIAPDE-1172-criar-pipeline-de-alertas-orquestrador
Sientiapde 1172 criar pipeline de alertas orquestrador
2025-08-04 10:02:56 -03:00
vitor-aignosi
47df5ebc60 SIENTIAPDE-1174
SIENTIAPDE-1174 Update MLFlowRepository initialization to include logger for enhanced logging capabilities
2025-08-01 17:15:49 -03:00
vitor-aignosi
669494c90b SIENTIAPDE-1174
Mark application as DOWN in metrics on shutdown to improve failure reporting
2025-08-01 12:01:29 -03:00
vitor-aignosi
963f089c7b SIENTIAPDE-1174
SIENTIAPDE-1174 Update replica count and enhance logging in MLFlow

- Changed replicaCount in values.yaml from 3 to 1 for reduced resource usage.
- Added debug logging for prediction response data in MLFlow to improve traceability.
2025-08-01 10:32:06 -03:00
vitor-aignosi
4f6f4d1fa9 SIENTIAPDE-1174
Enable data index reset in MLFlow by uncommenting data.reset_index, improving data processing consistency.
2025-08-01 10:20:42 -03:00
vitor-aignosi
4fb89b3862 SIENTIAPDE-1174
Comment out data.reset_index in MLFlow to prevent index reset during data processing, improving data handling without altering existing functionality.
2025-08-01 10:16:41 -03:00
vitor-aignosi
d2952654b4 SIENTIAPDE-1174
SIENTIAPDE-1174 Add write_metrics activity to main workflow for enhanced metrics tracking

- Included the write_metrics activity in the main workflow to support Prometheus metrics tracking.
2025-08-01 10:03:19 -03:00
vitor-aignosi
70f1abe681 SIENTIAPDE-1174
Update dependencies, modify replica count, and implement metrics tracking

- Updated sientia-dataops-library version from 1.3.5 to 1.3.7 in requirements.txt.
- Changed replicaCount in values.yaml from 5 to 3 and incremented image tag from 0.2.7 to 0.3.1.
- Added Prometheus metrics tracking in gates.py and worker.py, including a new write_metrics method.
- Configured Prometheus service and ServiceMonitor in values.yaml for metrics collection.
2025-08-01 10:00:47 -03:00
21 changed files with 682 additions and 131 deletions

View File

@@ -3,7 +3,7 @@ from temporalio import activity, workflow
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
from sientia_do.temporal.activities.postgres import Postgres from sientia_do.temporal.activities.postgres import Postgres
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.temporal.utils.logger import Logger from sientia_do.observability.logger import Logger
from laborious.activities.mlflow import MLFlow from laborious.activities.mlflow import MLFlow
from laborious.activities.gates import Gates from laborious.activities.gates import Gates
from laborious.activities.opc import OPC from laborious.activities.opc import OPC

View File

@@ -6,7 +6,7 @@ with workflow.unsafe.imports_passed_through():
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.activities.base import BaseActivity from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.temporal.utils.logger import Logger from sientia_do.observability.logger import Logger
from laborious.utils.filters.mlflow_filters import nan_values_filter, api_error_filter from laborious.utils.filters.mlflow_filters import nan_values_filter, api_error_filter
from typing import Any from typing import Any
from laborious.utils.filters.conditional_filters import ( from laborious.utils.filters.conditional_filters import (
@@ -15,6 +15,7 @@ with workflow.unsafe.imports_passed_through():
) )
from pandas import DataFrame from pandas import DataFrame
from datetime import datetime from datetime import datetime
from laborious import metrics
input_filter_functions = { input_filter_functions = {
'SPECIFIC_VARIABLES_NULL_VALUES': filter_specific_variables_null_values, 'SPECIFIC_VARIABLES_NULL_VALUES': filter_specific_variables_null_values,
@@ -47,7 +48,8 @@ mlflow_content_filter_functions = {
class Gates(BaseActivity): class Gates(BaseActivity):
def __init__(self, logger: Logger, notification_handler: NotificationHandler): def __init__(self, logger: Logger, notification_handler: NotificationHandler):
BaseActivity.__init__(self, logger, notification_handler) BaseActivity.__init__(
self, logger, notification_handler, set_error_counter=True)
@activity.defn(name="input_gate") @activity.defn(name="input_gate")
async def input_gate(self, input_data: dict[str, Any]) -> tuple[str | None, int, str]: async def input_gate(self, input_data: dict[str, Any]) -> tuple[str | None, int, str]:
@@ -66,7 +68,7 @@ class Gates(BaseActivity):
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Performing input gate...", metadata) self.info("Performing input gate...", metadata)
self.debug(f"Input data: {input_data}", metadata) self.debug(f"Input data: {input_data}", metadata)
@@ -101,11 +103,11 @@ class Gates(BaseActivity):
for path_flag in path_priority: for path_flag in path_priority:
if path_flag in filter_output: if path_flag in filter_output:
self.debug(f"Input gate result: {path_flag}", metadata) self.info(f"Input gate result: {path_flag}", metadata)
return path_flag, input_filter_functions['path_confidence'][path_flag], \ return path_flag, input_filter_functions['path_confidence'][path_flag], \
"Input data with bad quality" "Input data with bad quality"
self.debug("Nothing was filtered by the input gate", metadata) self.info("Nothing was filtered by the input gate", metadata)
return None, 0, "" return None, 0, ""
@activity.defn(name="mlflow_response_gate") @activity.defn(name="mlflow_response_gate")
@@ -126,7 +128,7 @@ class Gates(BaseActivity):
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Performing mlflow response gate...", metadata) self.info("Performing mlflow response gate...", metadata)
filters = input_data['filters'] filters = input_data['filters']
data = input_data['data'] data = input_data['data']
@@ -151,7 +153,7 @@ class Gates(BaseActivity):
notification_id=f"{gate_type.upper()}_GATE_RESPONSE_FILTER__{fil}", notification_id=f"{gate_type.upper()}_GATE_RESPONSE_FILTER__{fil}",
message=data['content']['message'], message=data['content']['message'],
block="mlflow_gate", block="mlflow_gate",
level=NotificationLevel.WARNING, level=NotificationLevel.ERROR,
attachment_content=data['content']['traceback'] attachment_content=data['content']['traceback']
) )
except Exception as e: except Exception as e:
@@ -167,12 +169,12 @@ class Gates(BaseActivity):
for path_flag in path_priority: for path_flag in path_priority:
if path_flag in filter_output: if path_flag in filter_output:
self.debug( self.info(
f"Mlflow response gate result: {path_flag}", metadata) f"Mlflow response gate result: {path_flag}", metadata)
return path_flag, mlflow_response_filter_functions['path_confidence'][path_flag], \ return path_flag, mlflow_response_filter_functions['path_confidence'][path_flag], \
", ".join(comments) ", ".join(comments)
self.debug("Nothing was filtered by the mlflow response gate", metadata) self.info("Nothing was filtered by the mlflow response gate", metadata)
return None, 0, "" return None, 0, ""
@activity.defn(name="mlflow_content_gate") @activity.defn(name="mlflow_content_gate")
@@ -193,7 +195,7 @@ class Gates(BaseActivity):
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Performing mlflow content gate...", metadata) self.info("Performing mlflow content gate...", metadata)
filters = input_data['filters'] filters = input_data['filters']
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
@@ -232,12 +234,12 @@ class Gates(BaseActivity):
for path_flag in path_priority: for path_flag in path_priority:
if path_flag in filter_output: if path_flag in filter_output:
self.debug( self.info(
f"Mlflow content gate result: {path_flag}", metadata) f"Mlflow content gate result: {path_flag}", metadata)
return path_flag, mlflow_content_filter_functions['path_confidence'][path_flag], \ return path_flag, mlflow_content_filter_functions['path_confidence'][path_flag], \
"Transformed data not passed the content filter" "Transformed data not passed the content filter"
self.debug("Nothing was filtered by the mlflow content gate", metadata) self.info("Nothing was filtered by the mlflow content gate", metadata)
return None, 0, "" return None, 0, ""
@activity.defn(name="format_prediction") @activity.defn(name="format_prediction")
@@ -254,7 +256,7 @@ class Gates(BaseActivity):
dict: The formatted data. dict: The formatted data.
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Formatting prediction...", metadata) self.info("Formatting prediction...", metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
data['timestamp'] = input_data['timestamp'] data['timestamp'] = input_data['timestamp']
@@ -264,6 +266,8 @@ class Gates(BaseActivity):
data['comments'] = "" data['comments'] = ""
data = data.sort_values(by='timestamp') data = data.sort_values(by='timestamp')
self.info(f"Prediction formatted: {data.size} rows", metadata)
return data.to_dict() return data.to_dict()
@activity.defn(name="format_default_prediction") @activity.defn(name="format_default_prediction")
@@ -285,7 +289,7 @@ class Gates(BaseActivity):
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Formatting default prediction...", metadata) self.debug("Formatting default prediction...", metadata)
return DataFrame({ data = DataFrame({
'prediction': [0], 'prediction': [0],
'response_time': [0], 'response_time': [0],
'timestamp': [input_data['timestamp']], 'timestamp': [input_data['timestamp']],
@@ -293,7 +297,10 @@ class Gates(BaseActivity):
'prediction_confidence': [input_data['prediction_confidence']], 'prediction_confidence': [input_data['prediction_confidence']],
'prediction_status': ['Bad'], 'prediction_status': ['Bad'],
'comments': [input_data['comment']] 'comments': [input_data['comment']]
}).to_dict() })
self.info(f"Default prediction formatted: {data.size} rows", metadata)
return data.to_dict()
@activity.defn(name="get_last_timestamp") @activity.defn(name="get_last_timestamp")
async def get_last_timestamp(self, input_data: dict[str, Any]) -> str: async def get_last_timestamp(self, input_data: dict[str, Any]) -> str:
@@ -305,7 +312,53 @@ class Gates(BaseActivity):
Returns: Returns:
str: The last timestamp of the data. str: The last timestamp of the data.
""" """
metadata = input_data['metadata']
self.info("Getting last timestamp...", metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
if data.empty: if data.empty:
return datetime.now().strftime('%Y-%m-%d %H:%M:%S') return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.info(
f"Last timestamp: {max(data['timestamp'].values.tolist())}", metadata)
return max(data['timestamp'].values.tolist()) return max(data['timestamp'].values.tolist())
@activity.defn(name="write_metrics")
async def write_metrics(self, input_data: dict[str, Any]):
"""
Write metrics to the database.
input_data:
metadata: dict[str, Any]
prediction: dict[str, Any]
"""
metadata = input_data['metadata']
prediction = DataFrame(input_data['prediction'])
prediction_confidence = prediction['prediction_confidence'].values[0]
response_time = prediction['response_time'].values[0]
self.info(
f"Writing metrics for model {metadata['model_name']}", metadata)
metrics.PREDICTIONS_WRITTEN_COUNT.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name']
).inc()
metrics.PREDICTION_CONFIDENCE_MONITOR.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name']
).set(prediction_confidence)
metrics.PREDICTION_RESPONSE_TIME_MONITOR.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name']
).observe(response_time)
self.info(
f"Metrics written for model {metadata['model_name']}", metadata)

View File

@@ -1,3 +1,4 @@
import json
from temporalio import activity, workflow from temporalio import activity, workflow
@@ -5,7 +6,7 @@ with workflow.unsafe.imports_passed_through():
from sientia_do.temporal.activities.base import BaseActivity from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.utils.logger import Logger from sientia_do.observability.logger import Logger
from laborious.utils.repository.model_repository import MLFlowRepository from laborious.utils.repository.model_repository import MLFlowRepository
from typing import Any from typing import Any
import numpy as np import numpy as np
@@ -16,14 +17,15 @@ with workflow.unsafe.imports_passed_through():
class MLFlow(BaseActivity): class MLFlow(BaseActivity):
def __init__(self, mlflow_host: str, mlflow_port: int, mlflow_username: str, def __init__(self, mlflow_host: str, mlflow_port: int, mlflow_username: str,
mlflow_password: str, logger: Logger, notification_handler: NotificationHandler): mlflow_password: str, logger: Logger, notification_handler: NotificationHandler):
BaseActivity.__init__(self, logger, notification_handler) BaseActivity.__init__(
self, logger, notification_handler, set_error_counter=True)
self.mlflow_host = mlflow_host self.mlflow_host = mlflow_host
self.mlflow_port = mlflow_port self.mlflow_port = mlflow_port
self.mlflow_username = mlflow_username self.mlflow_username = mlflow_username
self.mlflow_password = mlflow_password self.mlflow_password = mlflow_password
self.model_monitoring_repository = MLFlowRepository( self.model_monitoring_repository = MLFlowRepository(
f"{mlflow_host}:{mlflow_port}", mlflow_username, mlflow_password f"{mlflow_host}:{mlflow_port}", mlflow_username, mlflow_password, logger.base_logger
) )
@activity.defn(name="request_transform") @activity.defn(name="request_transform")
@@ -39,7 +41,7 @@ class MLFlow(BaseActivity):
dict[str, Any]: The transformed data. dict[str, Any]: The transformed data.
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug('Transforming data...', metadata) self.info('Transforming data...', metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
model_name = input_data['model_name'] model_name = input_data['model_name']
model_retention = input_data['model_retention'] model_retention = input_data['model_retention']
@@ -65,8 +67,10 @@ class MLFlow(BaseActivity):
response_data = self.model_monitoring_repository.transform( response_data = self.model_monitoring_repository.transform(
model_name, data, model_retention) model_name, data, model_retention)
self.debug("Response data:", metadata) self.debug("Transform response data:", metadata)
self.debug(response_data, metadata) self.debug(json.dumps(response_data, indent=4), metadata)
self.info("Data transformed successfully", metadata)
return response_data return response_data
@@ -83,7 +87,7 @@ class MLFlow(BaseActivity):
dict[str, Any]: The predicted data. dict[str, Any]: The predicted data.
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug('Predicting data...', metadata) self.info('Predicting data...', metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
model_name = input_data['model_name'] model_name = input_data['model_name']
model_retention = input_data['model_retention'] model_retention = input_data['model_retention']
@@ -95,7 +99,10 @@ class MLFlow(BaseActivity):
response_data = self.model_monitoring_repository.predict( response_data = self.model_monitoring_repository.predict(
model_name, data, model_retention) model_name, data, model_retention)
self.debug(response_data, metadata) self.debug("Prediction response data:", metadata)
self.debug(json.dumps(response_data, indent=4), metadata)
self.info("Prediction completed successfully", metadata)
return response_data return response_data

View File

@@ -5,7 +5,7 @@ with workflow.unsafe.imports_passed_through():
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.activities.base import BaseActivity from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.temporal.utils.logger import Logger from sientia_do.observability.logger import Logger
from laborious.utils.repository.opc_repository import OpcRepository from laborious.utils.repository.opc_repository import OpcRepository
from typing import Any from typing import Any
import traceback import traceback
@@ -22,6 +22,9 @@ class OPC(BaseActivity):
self.notification_handler = notification_handler self.notification_handler = notification_handler
self.opc_servers = opc_servers self.opc_servers = opc_servers
BaseActivity.__init__(
self, logger, notification_handler, set_error_counter=True)
self.opc_repository: dict[str, OpcRepository] = {} self.opc_repository: dict[str, OpcRepository] = {}
for id, server in opc_servers.items(): for id, server in opc_servers.items():
self.opc_repository[id] = OpcRepository( self.opc_repository[id] = OpcRepository(
@@ -34,6 +37,7 @@ class OPC(BaseActivity):
server_cert_path=server['server_cert_path'], server_cert_path=server['server_cert_path'],
notification_handler=self.notification_handler, notification_handler=self.notification_handler,
reconnection_interval=server['reconnection_interval'], reconnection_interval=server['reconnection_interval'],
pod_id=self.pod_id
) )
is_connected, error_data = self.opc_repository[id].connect() is_connected, error_data = self.opc_repository[id].connect()
if not is_connected: if not is_connected:
@@ -51,7 +55,6 @@ class OPC(BaseActivity):
attachment_content=error_data.get( attachment_content=error_data.get(
'attachment_content', None) 'attachment_content', None)
) )
BaseActivity.__init__(self, logger, notification_handler)
def write_data(self, server_id: str, tag: str, data: Any, def write_data(self, server_id: str, tag: str, data: Any,
data_type: str, tag_type: str, metadata: dict[str, Any]) -> bool: data_type: str, tag_type: str, metadata: dict[str, Any]) -> bool:
@@ -96,6 +99,59 @@ class OPC(BaseActivity):
) )
raise e raise e
def validate_server(self, server_id: str, metadata: dict[str, Any]) -> bool:
if self.opc_repository.get(server_id) is None:
message = f"OPC server {server_id} not found to perform write operation."
self.send_notification(
metadata=metadata,
notification_id="OPC_SERVER_NOT_FOUND",
message=message,
block="write_opc_data",
level=NotificationLevel.ERROR,
attachment_content=f"OPC servers: {list(self.opc_repository.keys())}"
)
return False
return True
def manage_output_tags(
self, server_id: str, config: dict[str, Any], data: DataFrame,
metadata: dict[str, Any], success: bool) -> tuple[bool, int]:
count = 0
if 'prediction_tags' in config:
for tag, tag_config in config['prediction_tags'].items():
local_success = self.write_data(
server_id=server_id,
tag=tag,
data=data.head(1)['prediction'].values[0],
data_type=tag_config['data_type'],
tag_type='prediction',
metadata=metadata
)
if local_success:
self.info(
f"Prediction data written to OPC server {server_id} for tag {tag}.", metadata)
count += 1
success = success and local_success
if 'confidence_tags' in config:
for tag, tag_config in config['confidence_tags'].items():
local_success = self.write_data(
server_id=server_id,
tag=tag,
data=data.head(1)['prediction_confidence'].values[0],
data_type=tag_config['data_type'],
tag_type='confidence',
metadata=metadata
)
if local_success:
self.info(
f"Confidence data written to OPC server {server_id} for tag {tag}.", metadata)
count += 1
success = success and local_success
return success, count
@activity.defn(name='write_opc_data') @activity.defn(name='write_opc_data')
async def write_opc_data(self, input_data: dict[str, Any]) -> dict[Any, Any]: async def write_opc_data(self, input_data: dict[str, Any]) -> dict[Any, Any]:
""" """
@@ -116,47 +172,28 @@ class OPC(BaseActivity):
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
self.debug("Writing data to OPC servers...", metadata) self.info("Writing data to OPC servers...", metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
opc_output_config = input_data['opc_output_config'] opc_output_config = input_data['opc_output_config']
self.debug(data, metadata) self.info(f"Data to write: {data.size} rows", metadata)
success = True success = True
success_count = 0
for server_id, config in opc_output_config.items(): for server_id, config in opc_output_config.items():
if self.opc_repository.get(server_id) is None:
message = f"OPC server {server_id} not found to perform write operation." if not self.validate_server(server_id, metadata):
self.send_notification(
metadata=metadata,
notification_id="OPC_SERVER_NOT_FOUND",
message=message,
block="write_opc_data",
level=NotificationLevel.ERROR,
attachment_content=f"OPC servers: {list(self.opc_repository.keys())}"
)
success = False success = False
continue
if 'prediction_tags' in config: local_success, local_count = self.manage_output_tags(
for tag, tag_config in config['prediction_tags'].items(): server_id, config, data, metadata, success)
success = success and self.write_data( success = success and local_success
server_id=server_id, success_count += local_count
tag=tag,
data=data.head(1)['prediction'].values[0],
data_type=tag_config['data_type'],
tag_type='prediction',
metadata=metadata
)
if 'confidence_tags' in config: self.info(
for tag, tag_config in config['confidence_tags'].items(): f"Data written to OPC server {server_id}: {local_count} of {len(config['prediction_tags'])} prediction tags and {len(config['confidence_tags'])} confidence tags", metadata)
self.write_data(
server_id=server_id,
tag=tag,
data=data.head(1)['prediction_confidence'].values[0],
data_type=tag_config['data_type'],
tag_type='confidence',
metadata=metadata
)
return self.process_confidence(data, success, metadata) return self.process_confidence(data, success, metadata)

41
laborious/metrics.py Normal file
View File

@@ -0,0 +1,41 @@
from prometheus_client import Gauge, Counter, Histogram
APP_UP = Gauge(
"app_up",
"Indicates if the application is running (1) or shutting down (0)",
["pod_id"],
)
CORE_LABELS = ["pod_id", "model_name", "pipeline_name"]
PREDICTIONS_WRITTEN_COUNT = Counter(
"laborious_predictions_written_count",
"Number of predictions written to the database table predictions",
CORE_LABELS,
)
PREDICTION_CONFIDENCE_MONITOR = Gauge(
"laborious_prediction_confidence_monitor",
"Current confidence of each prediction",
CORE_LABELS,
)
PREDICTION_RESPONSE_TIME_MONITOR = Histogram(
"laborious_prediction_response_time_monitor",
"Current response time of each prediction",
CORE_LABELS,
buckets=[0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]
)
PREDICTION_OPC_WRITING_COUNT = Counter(
"laborious_prediction_opc_writing_count",
"Number of predictions written to the OPC server",
[*CORE_LABELS, "opc_server_id"],
)
PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR = Histogram(
"laborious_prediction_opc_writing_response_time_monitor",
"Current response time of each prediction written to the OPC server",
[*CORE_LABELS, "opc_server_id"],
buckets=[0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]
)

View File

@@ -19,10 +19,11 @@ from sientia.ModelServing import ModelServing
class MLFlowRepository(): class MLFlowRepository():
def __init__(self, host, username, password): def __init__(self, host, username, password, logger):
self.model_serving = ModelServing(tracking_uri=host, self.model_serving = ModelServing(tracking_uri=host,
username=username, password=password) username=username, password=password,
logger=logger)
def transform(self, model_name: str, data: pd.DataFrame, model_retention: int): def transform(self, model_name: str, data: pd.DataFrame, model_retention: int):
""" """
@@ -68,7 +69,7 @@ class MLFlowRepository():
try: try:
start_time = datetime.now() start_time = datetime.now()
data = self.model_serving.get_cached_predict( data = self.model_serving.get_cached_predict(
model_name, data, model_retention)[-1:] model_name, data, model_retention)
end_time = datetime.now() end_time = datetime.now()
data = pd.DataFrame(data, columns=['prediction']) data = pd.DataFrame(data, columns=['prediction'])

View File

@@ -1,4 +1,5 @@
import traceback import traceback
import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -8,7 +9,8 @@ from asyncua.ua import DataValue, Variant, VariantType, DateTime
from regex import F from regex import F
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.utils.logger import Logger from sientia_do.observability.logger import Logger
from laborious import metrics
data_type_map = { data_type_map = {
'float': { 'float': {
@@ -38,7 +40,7 @@ class OpcRepository():
def __init__(self, id: str, url: str, logger: Logger, def __init__(self, id: str, url: str, logger: Logger,
notification_handler: NotificationHandler, notification_handler: NotificationHandler,
reconnection_interval: int = 60, server_uri: str = None, cert_path: str = None, reconnection_interval: int = 60, server_uri: str = None, cert_path: str = None,
private_key_path: str = None, server_cert_path: str = None): private_key_path: str = None, server_cert_path: str = None, pod_id: str = None):
self.url = url self.url = url
self.id = id self.id = id
self.server_uri = server_uri self.server_uri = server_uri
@@ -51,6 +53,14 @@ class OpcRepository():
self.last_reconnection_time = None self.last_reconnection_time = None
self.notification_handler = notification_handler self.notification_handler = notification_handler
self.client = None self.client = None
self.pod_id = pod_id
self.metadata = {
'model_name': '-',
'model_id': '-',
'workflow_name': 'opc_repository',
'schedule_name': '-'
}
def set_security(self): def set_security(self):
""" """
@@ -81,7 +91,7 @@ class OpcRepository():
self.server_cert_path) if self.server_cert_path else None self.server_cert_path) if self.server_cert_path else None
self.client.application_uri = self.server_uri self.client.application_uri = self.server_uri
self.logger.info('Setting security...') self.logger.custom_info('Setting security...', self.metadata)
self.client.set_security( self.client.set_security(
SecurityPolicyBasic256, SecurityPolicyBasic256,
certificate=str(cert), certificate=str(cert),
@@ -104,7 +114,8 @@ class OpcRepository():
self.client = Client(self.url) self.client = Client(self.url)
if self.cert_path: if self.cert_path:
self.set_security() self.set_security()
self.logger.info(f'Starting connection to OPC server {self.id}...') self.logger.custom_info(
f'Starting connection to OPC server {self.id}...', self.metadata)
return self.try_connect() return self.try_connect()
def try_connect(self) -> tuple[bool, dict[str, Any]]: def try_connect(self) -> tuple[bool, dict[str, Any]]:
@@ -121,7 +132,7 @@ class OpcRepository():
return True, {} return True, {}
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
self.logger.error(trace) self.logger.custom_error(trace, self.metadata)
return False, { return False, {
"notification_id": f"OPC_CONNECTION_ERROR_{self.id}", "notification_id": f"OPC_CONNECTION_ERROR_{self.id}",
@@ -139,9 +150,11 @@ class OpcRepository():
return return
try: try:
self.client.disconnect() self.client.disconnect()
self.logger.info('Disconnected from OPC server') self.logger.custom_info(
'Disconnected from OPC server', self.metadata)
except Exception as e: except Exception as e:
self.logger.error(f"Failed to disconnect from OPC server: {e}") self.logger.custom_error(
f"Failed to disconnect from OPC server: {e}", self.metadata)
self.client = None self.client = None
def __del__(self): def __del__(self):
@@ -151,7 +164,8 @@ class OpcRepository():
try: try:
self.disconnect() self.disconnect()
except Exception as e: except Exception as e:
self.logger.error(f"Error in destructor: {e}") self.logger.custom_error(
f"Error in destructor: {e}", self.metadata)
def validate_connection(self) -> tuple[bool, dict[str, Any]]: def validate_connection(self) -> tuple[bool, dict[str, Any]]:
""" """
@@ -168,32 +182,39 @@ class OpcRepository():
return self.connect() return self.connect()
if self.error_count > 5: if self.error_count > 5:
self.logger.warning( self.logger.custom_warning(
f"OPC server {self.id} will be disconnected due to multiple errors") f"OPC server {self.id} will be disconnected due to multiple errors", self.metadata)
try: try:
self.disconnect() self.disconnect()
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
self.logger.error(f"Failed to disconnect from OPC server: {e}") self.logger.custom_error(
self.logger.error(trace) f"Failed to disconnect from OPC server: {e}", self.metadata)
self.logger.info( self.logger.custom_error(trace, self.metadata)
f"Attempting to reconnect to OPC server {self.id}...") self.logger.custom_info(
f"Attempting to reconnect to OPC server {self.id}...", self.metadata)
return self.connect() return self.connect()
if hasattr(self.client, 'aio_obj') and self.client.aio_obj.uaclient.protocol is None or \ if hasattr(self.client, 'aio_obj') and self.client.aio_obj.uaclient.protocol is None or \
(hasattr(self.client.aio_obj.uaclient, 'protocol') and (hasattr(self.client.aio_obj.uaclient, 'protocol') and
self.client.aio_obj.uaclient.protocol.state == "closed"): self.client.aio_obj.uaclient.protocol.state == "closed"):
self.logger.error( self.logger.custom_error(
f"OPC server {self.id} is not connected") f"OPC server {self.id} is not connected", self.metadata)
if self.last_reconnection_time is None or (datetime.now() - self.last_reconnection_time).total_seconds( if self.last_reconnection_time is None or (datetime.now() - self.last_reconnection_time).total_seconds(
) > self.reconnection_interval: ) > self.reconnection_interval:
self.disconnect() self.disconnect()
self.logger.error( self.logger.custom_info(
f"Trying to reconnect to OPC server {self.id}...") f"Trying to reconnect to OPC server {self.id}...", self.metadata)
return self.connect() return self.connect()
return False, {} return False, {
"notification_id": f"OPC_CONNECTION_AWAITING_RECONNECTION_WINDOW_{self.id}",
"message": f"OPC server {self.id} is not connected, waiting for next reconnection window...",
"block": "opc_repository",
"level": NotificationLevel.WARNING
}
return True, {} return True, {}
@@ -215,6 +236,8 @@ class OpcRepository():
if not is_connected: if not is_connected:
return False, error return False, error
start_time = time.time()
try: try:
node = self.client.get_node(node) node = self.client.get_node(node)
except Exception as e: except Exception as e:
@@ -239,7 +262,7 @@ class OpcRepository():
data = data_type_map[data_type]['converter'](value) data = data_type_map[data_type]['converter'](value)
logger.custom_info( logger.custom_info(
f'Writing {data} - {type(data)} to {node}', metadata.get('schedule_name', 'N/A')) f'Writing {data} - {type(data)} to {node}', metadata)
now = datetime.now() now = datetime.now()
ua_data = DataValue( ua_data = DataValue(
Variant(data, data_type_map[data_type]['opc_type']), Variant(data, data_type_map[data_type]['opc_type']),
@@ -256,9 +279,26 @@ class OpcRepository():
try: try:
node.write_value(ua_data) node.write_value(ua_data)
metrics.PREDICTION_OPC_WRITING_COUNT.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name'],
opc_server_id=self.id
).inc()
end_time = time.time()
response_time = end_time - start_time
metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name'],
opc_server_id=self.id
).observe(response_time)
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
logger.custom_error(trace, metadata.get('schedule_name', 'N/A')) logger.custom_error(trace, metadata)
self.error_count += 1 self.error_count += 1
return False, { return False, {
"notification_id": f"OPC_WRITE_DATA_ERROR_{self.id}", "notification_id": f"OPC_WRITE_DATA_ERROR_{self.id}",

View File

@@ -1,6 +1,6 @@
from temporalio import workflow, client from temporalio import workflow, client
from temporalio.worker import Worker from temporalio.worker import Worker, PollerBehaviorAutoscaling
from temporalio.runtime import Runtime, TelemetryConfig, PrometheusConfig
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
import os import os
@@ -19,16 +19,32 @@ with workflow.unsafe.imports_passed_through():
build_mongodb_config build_mongodb_config
) )
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.temporal.utils.logger import get_logger from sientia_do.observability.logger import get_logger
from laborious import metrics
from prometheus_client import start_http_server
POD_ID = os.getenv('POD_ID')
SDK_METRICS_PORT = int(os.getenv('HTTP_SDK_METRICS_PORT', "9091"))
async def main(): async def main():
host = os.getenv('TEMPORAL_HOST', 'localhost:7233') host = os.getenv('TEMPORAL_HOST', 'localhost:7233')
logger = get_logger(__name__) logger = get_logger(__name__)
logger.info('Starting Worker...') metadata = {
'pod_id': POD_ID,
'model_name': '-',
'model_id': '-',
'workflow_name': '-',
'schedule_name': '-',
}
logger.info('Starting Notification Handler...') logger.custom_info(f'Starting Worker with POD_ID: {POD_ID}', metadata)
logger.custom_info("Starting prometheus client...", metadata)
start_prometheus_server()
logger.custom_info('Starting Notification Handler...', metadata)
mongo_config = build_mongodb_config() mongo_config = build_mongodb_config()
notification_handler = NotificationHandler( notification_handler = NotificationHandler(
@@ -38,7 +54,7 @@ async def main():
project_name=os.getenv('PROJECT_NAME', 'laborious') project_name=os.getenv('PROJECT_NAME', 'laborious')
) )
logger.info('Starting Activities...') logger.custom_info('Starting Activities...', metadata)
activities = Activities( activities = Activities(
postgres_config=build_postgres_config(), postgres_config=build_postgres_config(),
@@ -48,14 +64,25 @@ async def main():
notification_handler=notification_handler notification_handler=notification_handler
) )
logger.info('Starting Temporal Client...') logger.custom_info(
f'Starting SDK Metrics Server on port {SDK_METRICS_PORT}...', metadata)
new_runtime = Runtime(
telemetry=TelemetryConfig(
metrics=PrometheusConfig(
bind_address=f"0.0.0.0:{SDK_METRICS_PORT}")
)
)
logger.custom_info('Starting Temporal Client...', metadata)
temporal_client = await client.Client.connect( temporal_client = await client.Client.connect(
target_host=host, target_host=host,
namespace=os.getenv('TEMPORAL_NAMESPACE', 'laborious') namespace=os.getenv('TEMPORAL_NAMESPACE', 'laborious'),
runtime=new_runtime
) )
logger.info('Starting Workers...') logger.custom_info('Starting Workers...', metadata)
workers = [ workers = [
Worker( Worker(
@@ -68,11 +95,12 @@ async def main():
activities.update_production_model, activities.update_production_model,
activities.export_data_to_postgres activities.export_data_to_postgres
], ],
max_concurrent_workflow_tasks=100, max_concurrent_workflow_tasks=50,
max_concurrent_activities=100, max_concurrent_activities=50,
max_concurrent_local_activities=100, max_concurrent_local_activities=50,
max_concurrent_workflow_task_polls=100, max_cached_workflows=200,
max_cached_workflows=50, workflow_task_poller_behavior=PollerBehaviorAutoscaling(),
activity_task_poller_behavior=PollerBehaviorAutoscaling()
), ),
Worker( Worker(
temporal_client, temporal_client,
@@ -95,13 +123,15 @@ async def main():
# Postgres # Postgres
activities.load_custom_query, activities.load_custom_query,
activities.repeat_last_prediction, activities.repeat_last_prediction,
activities.export_data_to_postgres activities.export_data_to_postgres,
activities.write_metrics
], ],
max_concurrent_workflow_tasks=100, max_concurrent_workflow_tasks=50,
max_concurrent_activities=100, max_concurrent_activities=50,
max_concurrent_local_activities=100, max_concurrent_local_activities=50,
max_concurrent_workflow_task_polls=100, max_cached_workflows=200,
max_cached_workflows=50, workflow_task_poller_behavior=PollerBehaviorAutoscaling(),
activity_task_poller_behavior=PollerBehaviorAutoscaling()
) )
] ]
@@ -109,21 +139,34 @@ async def main():
for w in workers: for w in workers:
handlers.append(w.run()) handlers.append(w.run())
logger.info('Workers started successfully') logger.custom_info('Workers started successfully', metadata)
try: try:
# This will run the workers and wait for them to complete. # This will run the workers and wait for them to complete.
# If an exception occurs in any of the worker handlers, it will be propagated here. # If an exception occurs in any of the worker handlers, it will be propagated here.
await asyncio.gather(*handlers) await asyncio.gather(*handlers)
except BaseException as e: # NOSONAR except BaseException as e: # NOSONAR
logger.error(f"An unhandled exception occurred: {e}") logger.custom_error(f"An unhandled exception occurred: {e}", metadata)
finally: finally:
if notification_handler: if notification_handler:
notification_handler.shutdown() notification_handler.shutdown()
if activities: if activities:
activities.shutdown() activities.shutdown()
# Exit with a non-zero status code to indicate failure to Kubernetes # Exit with a non-zero status code to indicate failure to Kubernetes
metrics.APP_UP.labels(pod_id=POD_ID).set(0) # Mark app as DOWN
sys.exit(1) sys.exit(1)
def start_prometheus_server():
try:
port = int(os.getenv("HTTP_METRICS_PORT", 9090))
start_http_server(port)
print(f"Prometheus server started on port {port}.")
metrics.APP_UP.labels(pod_id=POD_ID).set(1) # Mark app as UP
except Exception as e:
print(f"Failed to start Prometheus server: {e}")
os._exit(1)
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main()) asyncio.run(main())

View File

@@ -3,7 +3,7 @@ from temporalio import workflow
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
from laborious.activities.activities import Activities from laborious.activities.activities import Activities
from typing import Any from typing import Any
from sientia_do.temporal.utils.policies import retry_policy from sientia_do.temporal.policies import retry_policy
from datetime import timedelta from datetime import timedelta

View File

@@ -3,7 +3,7 @@ from temporalio import workflow
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
from laborious.activities.activities import Activities from laborious.activities.activities import Activities
from typing import Any from typing import Any
from sientia_do.temporal.utils.policies import retry_policy from sientia_do.temporal.policies import retry_policy
from datetime import timedelta from datetime import timedelta

View File

@@ -4,7 +4,7 @@ with workflow.unsafe.imports_passed_through():
from laborious.activities.activities import Activities from laborious.activities.activities import Activities
from typing import Any from typing import Any
from datetime import timedelta from datetime import timedelta
from sientia_do.temporal.utils.policies import retry_policy from sientia_do.temporal.policies import retry_policy
@workflow.defn(name="format_and_export_prediction") @workflow.defn(name="format_and_export_prediction")
@@ -95,3 +95,13 @@ class FormatAndExportPrediction():
retry_policy=retry_policy, retry_policy=retry_policy,
start_to_close_timeout=timedelta(seconds=60) start_to_close_timeout=timedelta(seconds=60)
) )
await workflow.execute_activity_method(
Activities.write_metrics,
{
**metadata,
'prediction': prediction
},
retry_policy=retry_policy,
start_to_close_timeout=timedelta(seconds=60)
)

View File

@@ -3,7 +3,7 @@ from temporalio import workflow
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
from laborious.activities.activities import Activities from laborious.activities.activities import Activities
from typing import Any from typing import Any
from sientia_do.temporal.utils.policies import retry_policy from sientia_do.temporal.policies import retry_policy
from datetime import timedelta from datetime import timedelta

View File

@@ -3,5 +3,6 @@ psycopg2-binary
sqlalchemy sqlalchemy
asyncua asyncua
redis redis
git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.3.5 git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.4.1
git+ssh://git@github.com/Aignosi/sientia-mlops-library.git@0.38.5 git+ssh://git@github.com/Aignosi/sientia-mlops-library.git@0.38.5
prometheus-client

218
tests.ipynb Normal file
View File

@@ -0,0 +1,218 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "b10e5c25",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[ 0.5479121 -0.12224312 0.71719584 0.39473606 -0.8116453 0.9512447\n",
" 0.5222794 0.57212861 -0.74377273 -0.09922812 -0.25840395 0.85352998\n",
" 0.28773024 0.64552323 -0.1131716 -0.54552256 0.10916957 -0.87236549\n",
" 0.65526234 0.2633288 0.51617548 -0.29094806 0.94139605 0.78624224\n",
" 0.55676699 -0.61072258 -0.06655799 -0.91239247 -0.69142102 0.36609791\n",
" 0.48952431 0.93501946 -0.34834928 -0.25908059 -0.06088838 -0.62105728\n",
" -0.74015699 -0.04859015 -0.5461813 0.33962799 -0.12569616 0.66535639\n",
" 0.4005302 -0.37526672 0.6645196 0.60952871 -0.22504324 -0.42334379\n",
" 0.36499101 -0.72049503 -0.6001836 -0.98527546 0.57384876 0.32970171\n",
" 0.41033076 0.56145806 -0.08216845 0.13748239 -0.720406 -0.77093985\n",
" 0.33680592 -0.05780759 0.13047221 0.52999771 0.26943664 0.1071588\n",
" 0.11841432 -0.3920998 -0.93836433 -0.12656522 -0.57083065 -0.18294271\n",
" 0.70680615 -0.53212103 -0.88339452 -0.43723222 -0.41281248 0.32383303\n",
" 0.1140643 0.56779642 0.32862708 -0.18722628 0.62804077 -0.66605416\n",
" -0.95457585 -0.81990428 0.4447187 -0.07624554 -0.67745644 0.00208955\n",
" -0.69537579 0.39264075 -0.10768745 -0.23795755 -0.39697582 0.26056519\n",
" -0.27637478 -0.82470016 -0.7639882 0.92379533 0.81716138 0.39941427\n",
" -0.46826008 0.93835275 0.55750181 0.43378038 -0.101277 -0.45551688\n",
" -0.80721808 0.80520479 -0.08844742 -0.59527327 -0.38808675 0.15843914\n",
" -0.64645443 0.71322857 0.51703906 0.43892591 -0.13581392 0.25461768\n",
" 0.16819594 0.2996932 -0.83111136 -0.1683852 -0.91677165 -0.01201836\n",
" -0.34027758 -0.71095162 -0.79319406 0.17528914 -0.65881406 0.85024024\n",
" 0.16212228 -0.30626039 0.18183098 -0.95439226 0.91711843 -0.03539313\n",
" 0.56547045 -0.83454 -0.02668334 -0.01858601 0.87565291 0.1434561\n",
" -0.0530212 -0.46604867 -0.33686201 0.0413448 -0.12217708 -0.95677584\n",
" 0.65258385 0.79232154 -0.71950182 0.10807229 -0.78284852 0.34448019\n",
" -0.43753243 0.31884527 0.45398923 0.53729498 -0.78451811 0.83202369\n",
" -0.53957202 -0.92517489 0.10970494 -0.25815543 0.65957949 0.61650294\n",
" -0.36572221 0.90579879 -0.41816432 0.03011426 -0.48806982 0.87208714\n",
" -0.67078436 -0.91017876 -0.12980588 0.98475113 0.78335453 0.49721604\n",
" 0.78158498 0.78689328 0.03771672 -0.3681419 0.54402486 0.32332253\n",
" -0.25268454 -0.81106666 0.49357922 -0.47507897 0.8736263 -0.51805885\n",
" -0.75448414 0.66222534 -0.69343137 -0.64146338 0.19876558 0.74912408\n",
" -0.60713067 -0.37935265 0.55480968 0.94365285 0.00148237 -0.71220499\n",
" -0.97212742 -0.54068794 -0.73635556 0.35531735 -0.75633499 0.01265986\n",
" 0.38852487 0.16223322 -0.6004487 0.60824905 0.43081426 0.47796801\n",
" -0.7378845 -0.75249239 0.8551251 -0.20484361 -0.39810262 -0.02283191\n",
" 0.32572843 0.91124651 -0.42710755 0.84961686 -0.95028102 0.11039608\n",
" 0.26795022 -0.78820519 -0.71932081 -0.16177136 0.93246382 0.19208511\n",
" 0.86604644 0.60872183 -0.0652368 0.5695269 -0.96432643 -0.78171201\n",
" 0.65885723 0.59363418 -0.53471852 0.06153918 0.21203164 0.73547791\n",
" 0.20621431 -0.17485686 -0.25163191 -0.14823583 0.30386205 0.73498126\n",
" -0.09220624 -0.50432087 -0.52667527 0.49202856 0.63313753 -0.78944384\n",
" -0.86688229 0.18886733 -0.70765351 0.64932838 -0.37933065 -0.71225613\n",
" 0.84194094 -0.66893655 -0.43055984 -0.69277321 -0.76901987 -0.95770397\n",
" -0.88920918 -0.65071706 -0.89323613 0.18228763 0.36142905 -0.21273909\n",
" -0.36401781 0.00905247 0.75000988 0.70226325 -0.91304988 -0.63700318\n",
" -0.52651026 -0.50122485 0.1424653 -0.16747515 -0.90149176 -0.25277172\n",
" 0.0475059 -0.79665619 0.66691711 -0.89607627 0.84968374 -0.80177372\n",
" 0.6871499 0.80530629 0.95914136 0.60405176 0.55895508]\n",
"[ 0.28496655 0.55799271 -0.73089558 0.07213607 0.02844574 0.71514429\n",
" -0.07440127 -0.22982101 0.27912654 -0.46707336 -0.72046318 -0.04424545\n",
" -0.16622126 -0.53486012 -0.26497638 -0.2672151 -0.34500887 -0.24107184\n",
" 0.37148669 -0.40624705 0.89771585 0.83269604 -0.03817914 -0.34327759\n",
" 0.07086958 0.69712098 0.30517468 0.60878366 0.06544455 0.26583526\n",
" -0.42368877 0.46978632 -0.59519081 0.38959626 0.72143814 -0.73579433\n",
" 0.22875948 -0.8098085 0.45143126 -0.83101356 0.87187965 -0.72518414\n",
" 0.91776049 0.60176835 0.18736401 0.56524821 0.59022968 0.89205413\n",
" -0.49323329 0.18015179 -0.8099016 0.2323314 -0.65741739 0.12990122\n",
" 0.14486103 -0.06802969 0.04526355 0.52784678 0.59848943 -0.01569357\n",
" 0.19918688 0.86247247 -0.76053282 -0.76579287 -0.82458198 0.31572657\n",
" -0.1627834 0.54864283 0.34246283 -0.33272448 0.79673309 0.52506429\n",
" -0.45893012 -0.27161596 -0.37112004 -0.6847767 -0.70443325 0.87225493\n",
" -0.12419193 -0.23336035 0.45937142 0.10598613 0.87227997 0.56060299\n",
" -0.04126087 -0.24728105 0.97326309 0.43552047 0.90238932 -0.76304285\n",
" 0.70106736 0.27414777 -0.75615664 0.176516 0.37219273 -0.97539463\n",
" -0.09136408 0.65079902 -0.40928195 -0.08290384 -0.11537175 -0.39614522\n",
" 0.83688379 0.56258807 -0.77882318 0.99406932 0.75840005 -0.43218312\n",
" 0.67379316 -0.78716094 0.99820946 0.33136947 0.30025003 -0.81911855\n",
" 0.7940668 -0.94200099 -0.51834388 -0.71395625 0.55353588 -0.60359155\n",
" 0.82127645 0.31253808 -0.92767458 -0.98914033 -0.89668417 0.21185036\n",
" 0.60296362 -0.52289436 0.69881769 -0.88553612 0.60192771 0.85559086\n",
" 0.5442168 0.39624157 0.67596044 -0.9196974 -0.59643578 -0.75015264\n",
" 0.00906198 0.49037626 0.26002369 0.7022622 -0.68957402 0.46924218\n",
" -0.61391702 -0.4584825 0.41980939 0.96040957 0.22308721 -0.89099937\n",
" 0.23261794 -0.9152989 0.76829142 0.41915657 -0.65374431 -0.81655799\n",
" -0.63293354 0.96005436 -0.08287872 0.5681619 0.27281668 0.1448263\n",
" -0.70973949 0.89204891 -0.39731473 0.15603443 0.39955189 0.29846631\n",
" 0.88118882 -0.70312202 0.01670548 -0.19193122 -0.05166254 -0.76156495\n",
" -0.73181078 -0.44384891 -0.39059079 -0.14419357 0.22197509 0.26925823\n",
" -0.17637821 -0.18243378 -0.56474295 0.1766125 -0.36591818 -0.92788033\n",
" -0.16319991 -0.05173465 -0.54881426 0.14491587 0.1315438 0.40400436\n",
" 0.29589696 0.30486611 -0.3675717 0.57486444 0.09828877 -0.13716361\n",
" 0.25202496 -0.27868533 0.02547849 0.47341138 0.77280577 0.84211439\n",
" 0.00726585 0.04055023 0.59974082 -0.37109862 0.67476472 -0.01171671\n",
" -0.76828655 -0.85588171 0.68398642 -0.88886417 -0.43877713 -0.33173992\n",
" -0.65401111 -0.37221326 0.48538513 -0.97063431 0.65434685 0.71309605\n",
" -0.25547685 -0.6927742 0.20168082 -0.76065489 -0.27016128 0.91685836\n",
" 0.99092895 0.54420978 -0.37807698 0.3753301 0.41081273 -0.22431661\n",
" 0.28177727 -0.97854471 -0.58188468 0.05017661 -0.67249739 -0.66818626\n",
" 0.67260858 0.97826601 0.11193886 0.67813946 0.98064333 -0.71680822\n",
" -0.10350877 -0.21485457 -0.83990143 0.51066035 -0.13244195 -0.06134613\n",
" -0.69865405 -0.6381467 0.81420724 -0.91070182 -0.53429543 -0.41588134\n",
" -0.01960492 0.17289035 -0.01342005 -0.83176933 -0.51266509 0.68717677\n",
" 0.2751774 0.2982981 0.34040651 0.52580604 -0.88378304 -0.26678323\n",
" 0.07905487 -0.32308703 0.68895775 -0.03485498 0.53725518 0.70403103\n",
" 0.00958297 0.81910449 0.17424788 0.7005486 -0.31881841 -0.00236608\n",
" 0.06282208 -0.79004057 -0.20289499 0.83467535 0.26166448 -0.64498684\n",
" -0.32228873 -0.61679398 -0.95035374 0.85492092 -0.10358534 -0.38492986\n",
" 0.19695438 -0.98537109 -0.44395579 0.40606693 0.26753955]\n",
" Counter Rollout CounterPlusRollout Timestamp\n",
"0 0.000000 0.000000 0.000000 2025-01-01 00:00:00\n",
"1 0.547912 0.284967 0.832879 2025-01-01 00:00:01\n",
"2 0.425669 0.842959 1.268628 2025-01-01 00:00:02\n",
"3 1.142865 0.112064 1.254928 2025-01-01 00:00:03\n",
"4 1.537601 0.184200 1.721801 2025-01-01 00:00:04\n"
]
}
],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"# Set random seed for reproducibility\n",
"rng = np.random.default_rng(42)\n",
"\n",
"# Generate random walks starting at 0\n",
"counter = np.zeros(300)\n",
"rollout = np.zeros(300)\n",
"\n",
"# Generate random steps between -1 and 1\n",
"counter_steps = rng.uniform(-1, 1, 299)\n",
"rollout_steps = rng.uniform(-1, 1, 299)\n",
"\n",
"print(counter_steps)\n",
"print(rollout_steps)\n",
"\n",
"# Calculate cumulative sum and scale to -100 to 100 range\n",
"for i in range(1, 300):\n",
" counter[i] = counter[i-1] + counter_steps[i-1]\n",
" rollout[i] = rollout[i-1] + rollout_steps[i-1]\n",
"\n",
"# normalize values between -100 and 100, lowest value is -100, highest value is 100\n",
"counter = (counter - min(counter)) / (max(counter) - min(counter)) * 200 - 100\n",
"rollout = (rollout - min(rollout)) / (max(rollout) - min(rollout)) * 200 - 100\n",
"\n",
"# Create DataFrame\n",
"df = pd.DataFrame({\n",
" 'Counter': counter,\n",
" 'Rollout': rollout,\n",
" 'CounterPlusRollout': counter + rollout\n",
"})\n",
"\n",
"# add a timestamp column\n",
"df['Timestamp'] = pd.date_range(start='2025-01-01', periods=300, freq='1s')\n",
"\n",
"# Save to CSV\n",
"df.to_csv('random_walks.csv', index=False)\n",
"\n",
"# Display first few rows\n",
"print(df.head())\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "e7c8eeb1",
"metadata": {},
"outputs": [
{
"ename": "TypeError",
"evalue": "MLFlowRepository.__init__() missing 1 required positional argument: 'logger'",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mTypeError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mlaborious\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mrepository\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmodel_repository\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m MLFlowRepository\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m mlflow_repository = \u001b[43mMLFlowRepository\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 4\u001b[39m \u001b[43m \u001b[49m\u001b[43mhost\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mhttp://localhost:5080/\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 5\u001b[39m \u001b[43m \u001b[49m\u001b[43musername\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43maignosi\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 6\u001b[39m \u001b[43m \u001b[49m\u001b[43mpassword\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43maignosi\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 7\u001b[39m \u001b[43m)\u001b[49m\n\u001b[32m 9\u001b[39m mlflow_repository.get_experiment_by_run_id(\u001b[33m\"\u001b[39m\u001b[33m1\u001b[39m\u001b[33m\"\u001b[39m)\n",
"\u001b[31mTypeError\u001b[39m: MLFlowRepository.__init__() missing 1 required positional argument: 'logger'"
]
}
],
"source": [
"from laborious.utils.repository.model_repository import MLFlowRepository\n",
"\n",
"mlflow_repository = MLFlowRepository(\n",
" host=\"http://localhost:5080/\",\n",
" username=\"aignosi\",\n",
" password=\"aignosi\"\n",
")\n",
"\n",
"mlflow_repository.get_experiment_by_run_id(\"1\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -344,7 +344,6 @@ async def test_format_prediction(gates_activity):
assert result['prediction_confidence'] == {0: 0.9} assert result['prediction_confidence'] == {0: 0.9}
assert result['prediction_status'] == {0: 'Good'} assert result['prediction_status'] == {0: 'Good'}
assert result['comments'] == {0: ""} assert result['comments'] == {0: ""}
gates_activity.debug.assert_called()
@mark.asyncio @mark.asyncio
@@ -393,7 +392,8 @@ async def test_get_last_timestamp_with_data(gates_activity):
async def test_get_last_timestamp_no_data(gates_activity): async def test_get_last_timestamp_no_data(gates_activity):
# Arrange # Arrange
input_data = { input_data = {
'data': {} 'data': {},
**metadata
} }
# Act # Act
@@ -402,3 +402,42 @@ async def test_get_last_timestamp_no_data(gates_activity):
# Assert # Assert
assert isinstance(result, str) # Should be a timestamp string assert isinstance(result, str) # Should be a timestamp string
assert len(result) > 0 assert len(result) > 0
@mark.asyncio
@patch('laborious.activities.gates.metrics')
async def test_write_metrics(mock_metrics, gates_activity):
"""Test write_metrics method."""
input_data = {
**metadata,
'prediction': {
'prediction': [1, 2, 3],
'prediction_confidence': [0.9, 0.8, 0.7],
'response_time': [0.1, 0.2, 0.3]
}
}
await gates_activity.write_metrics(input_data)
mock_metrics.PREDICTIONS_WRITTEN_COUNT.labels.assert_called_once_with(
pod_id=gates_activity.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name']
)
mock_metrics.PREDICTIONS_WRITTEN_COUNT.labels.return_value.inc.assert_called_once_with()
mock_metrics.PREDICTION_CONFIDENCE_MONITOR.labels.assert_called_once_with(
pod_id=gates_activity.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name']
)
mock_metrics.PREDICTION_CONFIDENCE_MONITOR.labels.return_value.set.assert_called_once_with(
0.9
)
mock_metrics.PREDICTION_RESPONSE_TIME_MONITOR.labels.assert_called_once_with(
pod_id=gates_activity.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name']
)
mock_metrics.PREDICTION_RESPONSE_TIME_MONITOR.labels.return_value.observe.assert_called_once_with(
0.1
)

View File

@@ -24,7 +24,7 @@ def test___init__(mock_mlflow_repository):
assert mlflow.mlflow_password == "admin" assert mlflow.mlflow_password == "admin"
mock_mlflow_repository.assert_called_once_with( mock_mlflow_repository.assert_called_once_with(
"http://localhost:5000", "admin", "admin" "http://localhost:5000", "admin", "admin", ANY
) )

View File

@@ -91,6 +91,7 @@ def test___init__(mock_send_notification, mock_opc_repository):
server_cert_path="", server_cert_path="",
notification_handler=mock_notification_handler, notification_handler=mock_notification_handler,
reconnection_interval=60, reconnection_interval=60,
pod_id='localhost'
), ),
]) ])
mock_opc_repository.assert_has_calls([ mock_opc_repository.assert_has_calls([
@@ -104,6 +105,7 @@ def test___init__(mock_send_notification, mock_opc_repository):
server_cert_path="", server_cert_path="",
notification_handler=mock_notification_handler, notification_handler=mock_notification_handler,
reconnection_interval=60, reconnection_interval=60,
pod_id='localhost'
) )
]) ])

View File

@@ -16,7 +16,8 @@ def mlflow_repository():
repo = MLFlowRepository( repo = MLFlowRepository(
host='http://localhost:5000', host='http://localhost:5000',
username='admin', username='admin',
password='admin' password='admin',
logger=MagicMock()
) )
return repo return repo
@@ -71,7 +72,9 @@ def test_predict_success(mlflow_repository):
assert output['success'] is True assert output['success'] is True
assert output['content'] == {'prediction': { assert output['content'] == {'prediction': {
0: 3}, 'response_time': ANY} 0: 2,
1: 3
}, 'response_time': ANY}
def test_predict_error(mlflow_repository): def test_predict_error(mlflow_repository):

View File

@@ -139,8 +139,9 @@ def test_disconnect_error(opc_repository, mock_client):
mock_client.disconnect.side_effect = Exception("Test error") mock_client.disconnect.side_effect = Exception("Test error")
opc_repository.disconnect() opc_repository.disconnect()
opc_repository.logger.error.assert_called_once_with( opc_repository.logger.custom_error.assert_called_once_with(
"Failed to disconnect from OPC server: Test error" "Failed to disconnect from OPC server: Test error",
ANY
) )
assert opc_repository.client is None assert opc_repository.client is None
@@ -163,9 +164,9 @@ def test_validate_connection_error_count_disconnect_error(opc_repository):
assert response == opc_repository.connect.return_value assert response == opc_repository.connect.return_value
opc_repository.disconnect.assert_called_once() opc_repository.disconnect.assert_called_once()
opc_repository.connect.assert_called_once() opc_repository.connect.assert_called_once()
opc_repository.logger.error.assert_has_calls( opc_repository.logger.custom_error.assert_has_calls(
[ [
call("Failed to disconnect from OPC server: Test error"), call("Failed to disconnect from OPC server: Test error", ANY),
] ]
) )
@@ -182,7 +183,12 @@ def test_validate_connection_lost_not_time_to_reconect(_mock_datetime, opc_repos
response = opc_repository.validate_connection() response = opc_repository.validate_connection()
opc_repository.try_connect.assert_not_called() opc_repository.try_connect.assert_not_called()
assert response == (False, {}) assert response == (False, {
"notification_id": f"OPC_CONNECTION_AWAITING_RECONNECTION_WINDOW_{opc_repository.id}",
"message": f"OPC server {opc_repository.id} is not connected, waiting for next reconnection window...",
"block": "opc_repository",
"level": NotificationLevel.WARNING
})
@patch('laborious.utils.repository.opc_repository.hasattr', return_value=True) @patch('laborious.utils.repository.opc_repository.hasattr', return_value=True)
@@ -263,18 +269,36 @@ def test_write_data_invalid_data_type(opc_repository, mock_client):
assert error_data.get('attachment_content') is None assert error_data.get('attachment_content') is None
def test_write_data(opc_repository, mock_client): @patch('laborious.utils.repository.opc_repository.metrics')
def test_write_data(mock_metrics, opc_repository, mock_client):
opc_repository.validate_connection = MagicMock(return_value=(True, {})) opc_repository.validate_connection = MagicMock(return_value=(True, {}))
opc_repository.client = mock_client opc_repository.client = mock_client
mock_node = MagicMock() mock_node = MagicMock()
mock_client.get_node.return_value = mock_node mock_client.get_node.return_value = mock_node
opc_repository.write_data("ns=2;s=TestNode", 42.0, opc_repository.write_data("ns=2;s=TestNode", 42.0,
"float", opc_repository.logger, metadata) "float", opc_repository.logger, metadata['metadata'])
mock_client.get_node.assert_called_once_with("ns=2;s=TestNode") mock_client.get_node.assert_called_once_with("ns=2;s=TestNode")
mock_node.write_value.assert_called_once() mock_node.write_value.assert_called_once()
mock_metrics.PREDICTION_OPC_WRITING_COUNT.labels.assert_called_once_with(
pod_id=opc_repository.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name'],
opc_server_id=opc_repository.id
)
mock_metrics.PREDICTION_OPC_WRITING_COUNT.labels.return_value.inc.assert_called_once_with()
mock_metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR.labels.assert_called_once_with(
pod_id=opc_repository.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name'],
opc_server_id=opc_repository.id
)
mock_metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR.labels.return_value.observe.assert_called_once_with(
ANY)
def test_write_data_write_value_failed(opc_repository, mock_client): def test_write_data_write_value_failed(opc_repository, mock_client):
opc_repository.validate_connection = MagicMock(return_value=(True, {})) opc_repository.validate_connection = MagicMock(return_value=(True, {}))

View File

@@ -79,7 +79,7 @@ async def test_run_none_path_flag(workflow_mock, format_and_export_prediction):
start_to_close_timeout=ANY start_to_close_timeout=ANY
)]) )])
assert workflow_mock.execute_activity_method.call_count == 2 assert workflow_mock.execute_activity_method.call_count == 3
assert workflow_mock.execute_local_activity_method.call_count == 1 assert workflow_mock.execute_local_activity_method.call_count == 1
@@ -145,5 +145,5 @@ async def test_run_default_path_flag(workflow_mock, format_and_export_prediction
) )
]) ])
assert workflow_mock.execute_activity_method.call_count == 2 assert workflow_mock.execute_activity_method.call_count == 3
assert workflow_mock.execute_local_activity_method.call_count == 1 assert workflow_mock.execute_local_activity_method.call_count == 1

View File

@@ -11,7 +11,7 @@ image:
# This sets the pull policy for images. # This sets the pull policy for images.
pullPolicy: Always pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion. # Overrides the image tag whose default is the chart appVersion.
tag: "0.2.7" tag: "0.4.2"
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: imagePullSecrets:
@@ -111,11 +111,39 @@ tolerations: []
affinity: {} affinity: {}
service: services:
enabled: false sdk-metrics:
type: ClusterIP enabled: true
port: 4840 type: ClusterIP
targetPort: 4840 port: 9091
targetPort: 9091
name: sdk-metrics
metrics:
enabled: true
type: ClusterIP
port: 9090
targetPort: 9090
name: metrics
# Configuração do ServiceMonitor para o Prometheus Operator
# ref: https://github.com/prometheus-operator/prometheus-operator
serviceMonitor:
# Se true, um recurso ServiceMonitor será criado.
enabled: true
# O intervalo no qual as métricas devem ser coletadas (ex: 30s, 1m).
endpoints:
- port: metrics
path: /metrics
interval: 30s
relabelings: []
- port: sdk-metrics
path: /metrics
interval: 30s
relabelings: []
additionalLabels:
release: kube-prometheus-stack
env: env:
@@ -123,7 +151,7 @@ env:
- name: GITHUB_REPO_URL - name: GITHUB_REPO_URL
value: "git@github.com:Aignosi/sientia-dataops-laborious_temporal.git" value: "git@github.com:Aignosi/sientia-dataops-laborious_temporal.git"
- name: GITHUB_BRANCH - name: GITHUB_BRANCH
value: "SIENTIAPDE-1172-criar-pipeline-de-alertas-orquestrador" value: "SIENTIAPDE-1199-revisar-e-testar-observabilidade"
- name: PYTHON_APP - name: PYTHON_APP
value: "laborious.worker.worker" value: "laborious.worker.worker"
@@ -162,6 +190,10 @@ env:
- name: LOG_LEVEL - name: LOG_LEVEL
value: "DEBUG" value: "DEBUG"
- name: HTTP_METRICS_PORT
value: "9090"
- name: HTTP_SDK_METRICS_PORT
value: "9091"
- name: PROJECT_NAME - name: PROJECT_NAME
value: "sientia-laborious" value: "sientia-laborious"
@@ -189,7 +221,7 @@ ssh:
# kubectl create secret docker-registry docker-hub-secret --namespace sientia --docker-server=http://aignosi.azurecr.io --docker-username=aignosi --docker-password=5I5zpQ6sRaHqX1hD3dr+2mo647yO3FRc359/wu6gsP+ACRDRz5mp # kubectl create secret docker-registry docker-hub-secret --namespace sientia --docker-server=http://aignosi.azurecr.io --docker-username=aignosi --docker-password=5I5zpQ6sRaHqX1hD3dr+2mo647yO3FRc359/wu6gsP+ACRDRz5mp
# helm upgrade --install sientia-laborious-worker sientia/sientia-module -n sientia --create-namespace -f ./values.yaml --version 0.4.0-uat # helm upgrade --install sientia-laborious-worker sientia/sientia-module -n sientia --create-namespace -f ./values.yaml --version 0.5.0
# kubectl create secret generic git-ssh-key-sientia-laborious-worker \ # kubectl create secret generic git-ssh-key-sientia-laborious-worker \
# --namespace sientia \ # --namespace sientia \