Merge pull request #14 from Aignosi/SIENTIAPDE-1174-mapear-e-implementar-metricas-a-serem-criadas

Sientiapde 1174 mapear e implementar metricas a serem criadas
This commit is contained in:
Bruno Domingues
2025-08-13 16:34:49 -03:00
committed by GitHub
12 changed files with 173 additions and 12 deletions

View File

@@ -5,6 +5,7 @@ asyncua
redis redis
aiokafka aiokafka
pymongo pymongo
git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.3.5 git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.3.7
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
pydruid[pandas] pydruid[pandas]
prometheus-client

View File

@@ -8,6 +8,7 @@ with workflow.unsafe.imports_passed_through():
from scouter.activities.gates import Gates from scouter.activities.gates import Gates
from scouter.activities.mongodb import MongoDB from scouter.activities.mongodb import MongoDB
from typing import Any from typing import Any
from os import getenv
class Activities(Postgres, Redis, Gates, MongoDB,): class Activities(Postgres, Redis, Gates, MongoDB,):
@@ -61,6 +62,8 @@ class Activities(Postgres, Redis, Gates, MongoDB,):
notification_handler=notification_handler notification_handler=notification_handler
) )
self.pod_id = getenv("HOSTNAME", "localhost")
def shutdown(self): def shutdown(self):
Postgres.close(self) Postgres.close(self)
MongoDB.shutdown(self) MongoDB.shutdown(self)

View File

@@ -1,8 +1,12 @@
from temporalio import workflow, activity from temporalio import workflow, activity
from scouter import metrics
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
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.notifications.handlers import NotificationHandler
from sientia_do.temporal.utils.logger import Logger
from scouter.utils.quality.filters import null_values_filter, out_of_bounds_filter from scouter.utils.quality.filters import null_values_filter, out_of_bounds_filter
from typing import Any from typing import Any
import traceback import traceback
@@ -16,6 +20,10 @@ quality_gate_filters = {
class Gates(BaseActivity): class Gates(BaseActivity):
def __init__(self, logger: Logger, notification_handler: NotificationHandler):
BaseActivity.__init__(
self, logger, notification_handler, set_error_counter=True)
def apply_aggregation(self, group: DataFrame, aggr_function: str, def apply_aggregation(self, group: DataFrame, aggr_function: str,
metadata: dict[str, Any]) -> float | None | str: metadata: dict[str, Any]) -> float | None | str:
""" """
@@ -247,3 +255,18 @@ class Gates(BaseActivity):
) )
return data.to_dict() return data.to_dict()
@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]
"""
metadata = input_data['metadata']
metrics.LABORIOUS_DATA_WRITTEN_COUNT.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name']
).inc()

View File

@@ -57,7 +57,8 @@ class MongoDB(BaseActivity):
BaseActivity.__init__(self, BaseActivity.__init__(self,
logger=logger, logger=logger,
notification_handler=notification_handler) notification_handler=notification_handler,
set_error_counter=True)
def shutdown(self): def shutdown(self):
""" """

View File

@@ -1,8 +1,8 @@
import traceback
from temporalio import workflow, activity from temporalio import workflow, activity
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
from logging import Logger from logging import Logger
import traceback
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.redis_base import Redis as RedisBase from sientia_do.temporal.activities.redis_base import Redis as RedisBase
@@ -10,6 +10,7 @@ with workflow.unsafe.imports_passed_through():
from typing import Any from typing import Any
from pandas import DataFrame from pandas import DataFrame
from datetime import datetime from datetime import datetime
from scouter import metrics
class Redis(RedisBase): class Redis(RedisBase):
@@ -136,16 +137,29 @@ class Redis(RedisBase):
return data_hold return data_hold
try: try:
to_register_metrics = []
for _, row in data.iterrows(): for _, row in data.iterrows():
value = row['value'] value = row['value']
data_hold[row['name']] = value data_hold[row['name']] = value
to_register_metrics.append(
(row['name'], value))
data_hold['timestamp'] = data['timestamp'].max() if not data.empty else \ data_hold['timestamp'] = data['timestamp'].max() if not data.empty else \
datetime.now().strftime("%Y-%m-%d %H:%M:%S") datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.set(key, data_hold, ttl=retention_time) self.set(key, data_hold, ttl=retention_time)
# Register metrics
for metric in to_register_metrics:
metrics.TAG_CHANGES_MONITOR.labels(
pod_id=self.pod_id,
model_name=metadata['model_name'],
pipeline_name=metadata['workflow_name'],
tag_name=metric[0]
).set(metric[1])
data_hold_df = DataFrame(data_hold, index=[0]) data_hold_df = DataFrame(data_hold, index=[0])
data_hold_melted = data_hold_df.melt( data_hold_melted = data_hold_df.melt(
id_vars='timestamp', var_name='variable', value_name='value') id_vars='timestamp', var_name='variable', value_name='value')

21
scouter/metrics.py Normal file
View File

@@ -0,0 +1,21 @@
from prometheus_client import Gauge, Counter
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"]
LABORIOUS_DATA_WRITTEN_COUNT = Counter(
"scouter_laborious_data_written_count",
"Number of writings to the database table laborious_data",
CORE_LABELS,
)
TAG_CHANGES_MONITOR = Gauge(
"scouter_tag_changes_monitor",
"Current value change of each tag",
[*CORE_LABELS, "tag_name"],
)

View File

@@ -12,19 +12,26 @@ with workflow.unsafe.imports_passed_through():
from scouter.workflow.fake_data import FakeData from scouter.workflow.fake_data import FakeData
from scouter.activities.faker import Faker from scouter.activities.faker import Faker
import asyncio import asyncio
from prometheus_client import start_http_server
from scouter import metrics
from scouter.utils.connectors_config import ( from scouter.utils.connectors_config import (
build_postgres_config, build_postgres_config,
build_redis_config, build_redis_config,
build_mongodb_config build_mongodb_config
) )
POD_ID = os.getenv("HOSTNAME", "localhost")
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...') logger.info(f"Starting Worker with pod_id: {POD_ID}")
logger.info("Starting prometheus client...")
start_prometheus_server()
logger.info('Starting Notification Handler...') logger.info('Starting Notification Handler...')
@@ -77,7 +84,8 @@ async def main():
activities.aggregate_data, activities.aggregate_data,
activities.group_and_hold_data, activities.group_and_hold_data,
activities.export_data_to_postgres, activities.export_data_to_postgres,
activities.store_data_package activities.write_metrics,
activities.store_data_package,
], ],
max_concurrent_workflow_tasks=100, max_concurrent_workflow_tasks=100,
max_concurrent_activities=100, max_concurrent_activities=100,
@@ -112,7 +120,20 @@ async def main():
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

@@ -86,6 +86,15 @@ class CoreScouter:
start_to_close_timeout=timedelta(seconds=60) start_to_close_timeout=timedelta(seconds=60)
) )
await workflow.execute_activity_method(
Activities.write_metrics,
{
**metadata,
},
retry_policy=retry_policy,
start_to_close_timeout=timedelta(seconds=60)
)
if input_data.get('debug_data_package', False): if input_data.get('debug_data_package', False):
await workflow.execute_activity_method( await workflow.execute_activity_method(
Activities.store_data_package, Activities.store_data_package,

View File

@@ -402,3 +402,18 @@ async def test_aggregate_data_raise_exception(gates_fixture):
) )
else: else:
assert False assert False
@pytest.mark.asyncio
@patch('scouter.activities.gates.metrics')
async def test_write_metrics(mock_metrics, gates_fixture):
"""Test write_metrics method."""
input_data = {
'metadata': metadata['metadata']
}
await gates_fixture.write_metrics(input_data)
mock_metrics.LABORIOUS_DATA_WRITTEN_COUNT.labels.assert_called_once_with(
pod_id=gates_fixture.pod_id,
model_name=metadata['metadata']['model_name'],
pipeline_name=metadata['metadata']['workflow_name']
)

View File

@@ -20,6 +20,7 @@ def redis_activity(_mock_redis_init):
activity.redis_client = MagicMock() activity.redis_client = MagicMock()
activity.logger = logger activity.logger = logger
activity.notification_handler = notification_handler activity.notification_handler = notification_handler
activity.pod_id = 'test_pod_id'
return activity return activity

25
tests/test_metrics.py Normal file
View File

@@ -0,0 +1,25 @@
# tests/unit/test_metrics.py
import pytest
from prometheus_client import Counter, Gauge, Histogram
import scouter.metrics as metrics
# --- Test Functions for Each Metric (Corrected for v0.22.0 _name behavior) ---
def test_scouter_laborious_data_written_count():
"""Verify the definition of LABORIOUS_DATA_WRITTEN_COUNT."""
assert metrics.LABORIOUS_DATA_WRITTEN_COUNT is not None
assert isinstance(metrics.LABORIOUS_DATA_WRITTEN_COUNT, Counter)
assert metrics.LABORIOUS_DATA_WRITTEN_COUNT._name == "scouter_laborious_data_written_count"
assert set(metrics.LABORIOUS_DATA_WRITTEN_COUNT._labelnames) == {
"pod_id", "model_name", "pipeline_name"}
def test_scouter_tag_changes_monitor():
"""Verify the definition of TAG_CHANGES_MONITOR."""
assert metrics.TAG_CHANGES_MONITOR is not None
assert isinstance(metrics.TAG_CHANGES_MONITOR, Gauge)
assert metrics.TAG_CHANGES_MONITOR._name == "scouter_tag_changes_monitor"
assert set(metrics.TAG_CHANGES_MONITOR._labelnames) == {
"pod_id", "model_name", "pipeline_name", "tag_name"}

View File

@@ -3,7 +3,7 @@
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 5 replicaCount: 3
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image: image:
@@ -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.3.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,14 +111,39 @@ tolerations: []
affinity: {} affinity: {}
service: {} services:
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).
interval: 30s
# O path do endpoint de métricas na sua aplicação.
path: /metrics
# Labels adicionais para o recurso ServiceMonitor.
# Essencial para que o Prometheus Operator o descubra. Se você usa o helm chart kube-prometheus-stack,
# ele procura por ServiceMonitors com o label "release: kube-prometheus-stack".
additionalLabels:
release: kube-prometheus-stack
# Configurações de relabeling adicionais, se necessário.
# ref: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
relabelings: []
port: metrics
env: env:
# Entrypoint variables # Entrypoint variables
- name: GITHUB_REPO_URL - name: GITHUB_REPO_URL
value: "git@github.com:Aignosi/sientia-dataops-scouter_temporal.git" value: "git@github.com:Aignosi/sientia-dataops-scouter_temporal.git"
- name: GITHUB_BRANCH - name: GITHUB_BRANCH
value: "SIENTIAPDE-1172-criar-pipeline-de-alertas-orquestrador" value: "SIENTIAPDE-1174-mapear-e-implementar-metricas-a-serem-criadas"
- name: PYTHON_APP - name: PYTHON_APP
value: "scouter.worker.worker" value: "scouter.worker.worker"
@@ -160,6 +185,8 @@ env:
- name: LOG_LEVEL - name: LOG_LEVEL
value: "DEBUG" value: "DEBUG"
- name: HTTP_METRICS_PORT
value: "9090"
- name: PROJECT_NAME - name: PROJECT_NAME
value: "sientia-scouter" value: "sientia-scouter"
@@ -190,7 +217,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-scouter-worker sientia/sientia-module -n sientia --create-namespace -f ./values.yaml --version 0.4.0-uat # helm upgrade --install sientia-scouter-worker sientia/sientia-module -n sientia --create-namespace -f ./values.yaml --version 0.4.0
# kubectl create secret generic git-ssh-key-sientia-scouter-worker \ # kubectl create secret generic git-ssh-key-sientia-scouter-worker \
# --namespace sientia \ # --namespace sientia \