SIENTIAPDE-1110

SIENTIAPDE-1110 Add MongoDB integration and enhance Redis activity with timestamp management functions.
This commit is contained in:
vitor-aignosi
2025-07-01 15:10:32 -03:00
parent e7b05ebf3a
commit 53337de74a
6 changed files with 227 additions and 6 deletions

View File

@@ -4,5 +4,6 @@ sqlalchemy
asyncua asyncua
redis redis
aiokafka aiokafka
pymongo
git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.2.0 git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.2.0
git+ssh://git@github.com/Aignosi/sientia-mlops-library.git@0.38.1 git+ssh://git@github.com/Aignosi/sientia-mlops-library.git@0.38.1

View File

@@ -7,16 +7,18 @@ with workflow.unsafe.imports_passed_through():
from scouter.activities.redis import Redis from scouter.activities.redis import Redis
from scouter.activities.kafka import Kafka from scouter.activities.kafka import Kafka
from scouter.activities.gates import Gates from scouter.activities.gates import Gates
from scouter.activities.mongodb import MongoDB
from typing import Any from typing import Any
class Activities(Postgres, Redis, Kafka, Gates): class Activities(Postgres, Redis, Kafka, Gates, MongoDB):
"""Activities class that combines multiple services with proper initialization.""" """Activities class that combines multiple services with proper initialization."""
def __init__(self, def __init__(self,
postgres_config: dict[str, Any], postgres_config: dict[str, Any],
redis_config: dict[str, Any], redis_config: dict[str, Any],
kafka_config: dict[str, Any], kafka_config: dict[str, Any],
mongodb_config: dict[str, Any],
logger: Logger, logger: Logger,
notification_handler: NotificationHandler): notification_handler: NotificationHandler):
@@ -62,6 +64,16 @@ class Activities(Postgres, Redis, Kafka, Gates):
notification_handler=notification_handler notification_handler=notification_handler
) )
# Initialize MongoDB
MongoDB.__init__(
self,
connection_string=mongodb_config['connection_string'],
database_name=mongodb_config['database_name'],
logger=logger,
notification_handler=notification_handler
)
def shutdown(self): def shutdown(self):
Postgres.close(self) Postgres.close(self)
Kafka.close(self) Kafka.close(self)
MongoDB.close(self)

View File

@@ -0,0 +1,121 @@
from temporalio import workflow, activity
with workflow.unsafe.imports_passed_through():
from typing import Any
import traceback
from logging import Logger
import datetime
from pymongo import MongoClient
from sientia_do.notifications.handlers import NotificationHandler
from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.activities.base import BaseActivity
def clear_mongo_id(docs: list) -> list:
"""
Remove the MongoDB internal `_id` field from the document.
Args:
docs (list): The document to clear.
Returns:
list: The documents without the `_id` field.
"""
for doc in docs:
if isinstance(doc, list):
clear_mongo_id(doc)
elif isinstance(doc, dict):
if "_id" in doc:
del doc["_id"]
for key, value in doc.items():
if isinstance(value, list):
clear_mongo_id(value)
elif isinstance(value, dict):
clear_mongo_id([value])
return docs
class MongoDB(BaseActivity):
def __init__(self, connection_string: str, database_name: str,
logger: Logger,
notification_handler: NotificationHandler):
self.connection_string = connection_string
self.database_name = database_name
self.client = MongoClient(
self.connection_string, serverSelectionTimeoutMS=5000)
self.client.server_info() # Trigger an exception if connection fails
self.database = self.client[self.database_name]
# Initialize MongoDB client here (omitted for brevity)
logger.info("MongoDB connection initialized")
BaseActivity.__init__(self,
logger=logger,
notification_handler=notification_handler)
def shutdown(self):
"""
Close the MongoDB client connection.
"""
try:
if self.client:
self.logger.info("Closing MongoDB connection...")
self.client.close()
self.logger.info("MongoDB connection closed successfully")
except Exception as e:
self.logger.error(f"Failed to close MongoDB connection: {e}")
def __del__(self):
"""
Destructor to ensure MongoDB client is closed when the object is deleted.
"""
self.shutdown()
@activity.defn(name="load_latest_data")
async def load_latest_data(self, input_data: dict[str, Any]) -> dict[str, Any]:
"""
Loads the latest data from MongoDB.
"""
metadata = input_data['metadata']
collection_name = input_data['collection_name']
last_data_timestamp = input_data['last_data_timestamp']
try:
data_filter = {
"inserted_at": {
"$gt": datetime.strptime(last_data_timestamp, "%Y-%m-%d %H:%M:%S.%f")
}
}
data = self.database[collection_name].find(data_filter, {"_id": 0})
data = clear_mongo_id(data)
self.info(
f"Loaded {len(data)} documents from MongoDB",
metadata=metadata
)
self.debug(
f"Loaded data: {data}",
metadata=metadata
)
return data
except Exception as e:
trace = traceback.format_exc()
self.send_notification(
metadata=metadata,
notification_id="MONGO_LOAD_ERROR",
message=f"Error loading data from MongoDB: {e}",
block="load_latest_data",
level=NotificationLevel.ERROR,
attachment_content=trace
)
raise e

View File

@@ -18,6 +18,47 @@ class Redis(RedisBase):
RedisBase.__init__(self, host, port, username, RedisBase.__init__(self, host, port, username,
password, logger, notification_handler) password, logger, notification_handler)
@activity.defn(name="get_last_data_timestamp")
async def get_last_data_timestamp(self, input_data: dict[str, Any]) -> dict[str, Any]:
"""
Gets the last data timestamp from redis.
"""
metadata = input_data['metadata']
key = f"{input_data['workflow_name']}_{input_data['schedule_name']}"
data_hold = self.get(key)
self.debug(
f"Last collected timestamp: {data_hold}",
metadata=metadata
)
if not data_hold:
return None
return data_hold
@activity.defn(name="put_last_data_timestamp")
async def put_last_data_timestamp(self, input_data: dict[str, Any]):
"""
Puts the last data timestamp into redis.
"""
metadata = input_data['metadata']
key = f"{input_data['workflow_name']}_{input_data['schedule_name']}"
data = DataFrame(input_data['data'])
last_data_timestamp = data['inserted_at'].max()
self.debug(
f"Last collected timestamp to insert: {last_data_timestamp}",
metadata=metadata
)
self.set(key, last_data_timestamp, ttl=input_data['retention_time'])
return last_data_timestamp
@activity.defn(name="group_and_hold_data") @activity.defn(name="group_and_hold_data")
async def group_and_hold_data(self, input_data: dict[str, Any]): async def group_and_hold_data(self, input_data: dict[str, Any]):
""" """

View File

@@ -28,3 +28,15 @@ def build_redis_config():
'username': getenv('REDIS_USERNAME', None), 'username': getenv('REDIS_USERNAME', None),
'password': getenv('REDIS_PASSWORD', None) 'password': getenv('REDIS_PASSWORD', None)
} }
def build_mongodb_config():
username = getenv('MONGODB_USERNAME', 'sientia')
password = getenv('MONGODB_PASSWORD', 'sientia')
uri = getenv('MONGODB_URL', 'localhost:27017')
connection_string = f'mongodb://{username}:{password}@{uri}'
return {
'connection_string': connection_string,
'database_name': getenv('MONGODB_DATABASE_NAME', 'sientia')
}

View File

@@ -41,14 +41,48 @@ class Scouter:
} }
} }
data = await workflow.execute_activity_method( # data = await workflow.execute_activity_method(
Activities.load_from_kafka, # Activities.load_from_kafka,
# {
# **metadata,
# 'topic': input_data['topic']
# },
# retry_policy=retry_policy,
# start_to_close_timeout=timedelta(seconds=60)
# )
last_data_timestamp = await workflow.execute_local_activity_method(
Activities.get_last_data_timestamp,
{ {
**metadata, **metadata,
'topic': input_data['topic'] 'workflow_name': input_data['workflow_name'],
'schedule_name': input_data['schedule_name']
}, },
retry_policy=retry_policy, start_to_close_timeout=timedelta(seconds=60),
start_to_close_timeout=timedelta(seconds=60) retry_policy=retry_policy
)
data = await workflow.execute_local_activity_method(
Activities.load_latest_data,
{
**metadata,
'collection_name': f"raw_{input_data['schedule_name']}",
'last_data_timestamp': last_data_timestamp
},
start_to_close_timeout=timedelta(seconds=60),
retry_policy=retry_policy
)
await workflow.execute_activity_method(
Activities.put_last_data_timestamp,
{
**metadata,
'data': data,
'workflow_name': input_data['workflow_name'],
'schedule_name': input_data['schedule_name']
},
start_to_close_timeout=timedelta(seconds=60),
retry_policy=retry_policy
) )
if data == {}: if data == {}: