#client/cliente.py
#archivo donde se hace el post

#tambien es donde se levanta docker_compose
import httpx, time
import json
import logging

logger = logging.getLogger(__name__)

#en este archivo debe ir un try que apague docker 
#si se hace cancelled error y otro exceptionq que
#maneje errores impervistos y reinicies el sistema

def enviar_datos():
    PATH = "http://localhost:8000/runs"

    try:
        respuesta = httpx.post(PATH,timeout=30.0)

        respuesta.raise_for_status()

        resultado = respuesta.json()

        run_id = resultado.get("run_id")

        if run_id is not None:
            s = httpx.get(f"http://localhost:8000/runs/{run_id}").json()
            logger.info("ID: %s", s)
            if s["status"] in ("COMPLETED", "FAILED"):
                break
            time.sleep(2)
            
    except httpx.HTTPStatusError as error:
        print("Error HTTP:", error.response.status_code)
        print("Detalle:", error.response.text)

    except httpx.RequestError as error:
        print("Error de conexión:", error)

    except (json.JSONDecodeError, OSError) as error:
        print("Error leyendo inputs.json:", error)
    


if __name__ == "__main__":
    enviar_datos()

#_______________________
#app/life_span.py
import logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
from data_base.sql_create_tables.Engine_tables import create_tables
from app.shutdown_completo import shutdown
from contextlib import asynccontextmanager
from fastapi import FastAPI
from routes import temporal_id, work
from temporal.cliente_temporal_connet import TemporalClientFactory

logger = logging.getLogger(__name__)

@asynccontextmanager
async def lifespan(app: FastAPI):
    try:
        try:
            await create_tables()
        except Exception:
            self.logger.exception("error en crear tablas")
            raise
            
        try:
            app.state.temporal = await TemporalClientFactory.client_conn()
        except Exception as e:
            self.logger.exception("error al conectar temporal", e)
            raise
            
        yield
    except Exception:
        await shutdown()
    

app = FastAPI(lifespan=lifespan,docs_url=None, redoc_url=None, openapi_url=None)  # deshabilita la documentación pública


app.include_router(temporal_id.router)
app.include_router(work.router)

#_______________________
#temporal/cliente_temporal_connet.py
from temporalio.client import Client
from temporalio.contrib.pydantic import pydantic_data_converter
import logging
from core.config import TEMPORAL_HOST, TEMPORAL_NAMESPACE

class TemporalClientFactory:
    def __init__(self):
        self.logger = logging.getLogger(__name__)

    async def client_conn():
        try:
            client = await Client.connect(TEMPORAL_HOST, data_converter=pydantic_data_converter, namespace=TEMPORAL_NAMESPACE)
            self.logger.info("Cliente conectado a Temporal en %s", TEMPORAL_HOST)
            return client
    
        except RPCError as e:
            raise("Temporal rechazó la conexión: %s", e)
        
        except ValueError as e:
            raise("TEMPORAL_HOST mal configurado: %s", e)
              
        except (ConnectionRefusedError, OSError) as e:
            raise("No se pudo alcanzar el host de Temporal: %s", e)        
        
        except TimeoutError as e:
            raise("Timeout al conectar con Temporal: %s", e)         
        
        except Exception:
           raise("error inesperado al conectar con el cliente")
           
            
    async def close(self, client: Client | None):
        if client is None:
            return
        try:
            if hasattr(client, "close"):
                await client.close()
        except Exception as e:
            raise(f"error al cerrar cliente temporal: {e}")
            
        
    
#_______________________
#app/shutdown_completo.py
from app.cliente_temporal_connet import close
from data_base.engine_sq.Creation_engine import close_engine
import logging

logger = logging.getLogger(__name__)

def shutdown():
    try:
        await close()
    except Exception:
        logger.warning("error la cerrar temporal")
        
    try:
        await close_engine()
    except Exception:
        logger.warning("error al cerrar motor de SQL")
        
    
    #aqui va todas las desconecciones




#_______________________
#routes/work.py
from fastapi import APIRouter, Request
from temporal.workflows import MiWorkflow
from core.config import TASK_QUEUE

router = APIRouter()


#dentro de la funcion debe de ir un
#bucle while para que siempre pida metricas
#tambien falta un try y dene haber un raise de 
#canceled error que suba
#y un try que reinicie temporal 
@router.post("/runs", status_code=202)
async def create_run(request: Request):
    client = request.app.state.temporal
    handle = await client.start_workflow(
        MiWorkflow.run,
        id=f"rates-{uuid4()}",
        task_queue=TASK_QUEUE
    )

    #result = handle.result

    #manejar los raise de las activitys
    #aqui se valida pydantic para prometheus
    
    #from metricas.metrica_entrada import RateParams
    
    #result debe de pasar los datos al post
    #aqui de debe de llamar a la ruta de
    #prometheus para que guarde las metricas
    #investigar PromQL y Alertmanager
    





#_______________________
#routes/temporal_id.py
#archivo donde va la ruta que hace get 
#al id del temporal
@app.get("/runs/{run_id}")
#este get se puede hacerce al prometheus y
#sacar los datos guardados 

#_______________________
#temporal/register_worker.py
from temporal.cliente_temporal_connet import TemporalClientFactory
from temporalio.worker import Worker
from temporal.workflows import MiWorkflow
from activitys.activity_psutil import _psutil
from activitys.activity_comparations import comparation
from activitys.guardar_metricas_sql import upsert_metrics
from activitys.activity_gemini import analisis_gemini
from activitys.guardar_analisis_sql import upsert_analisis

from core.config import TASK_QUEUE, WORKFLOW_CONCURRENCY

class Registry_worker:
    def __init__(self):
        self.client = TemporalClientFactory.client_conn()

    async def create_worker(self):
        try:
            worker = Worker(
                self.client,
                task_queue=TASK_QUEUE,
                workflows=[MiWorkflow],
                activities=[_psutil, comparation, upsert_metrics_data, analisis_gemini, upsert_analisis],
                max_concurrent_activities=WORKFLOW_CONCURRENCY
                )
                
                await worker.run()
            
        except ValueError as e:
            self.logger.error("Configuración inválida del worker (falta decorador @activity.defn o @workflow.defn?): %s", e)
            self.logger.info("worker.run dejo de correr")
            raise
            
        except RPCError as e:
            self.logger.error("Worker perdió conexión con el servidor de Temporal: %s", e)
            self.logger.info("worker.run dejo de correr, se reintentara")
            raise

        except Exception as e:
            self.logger.exception("error inesperado: %s", e)
            self.logger.info("worker.run dejo de correr, se reintentara")
            raise
            #verificar este archivo y verificar
            #quien lo llama
        





#_______________________
#temporal/workflows.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
from temporalio import activity
            
@workflow.defn
class MiWorkflow:
    @workflow.run
    async def run(self) -> dict:
        retry = RetryPolicy(
            maximum_attempts=3,
            non_retryable_error_types=["ValueError", "exception"],
        )

        #def de metricas
        metrics_system = await workflow.execute_activity(
            "_psutil",
            start_to_close_timeout=timedelta(minutes=2),
            retry_policy=retry
        )

        #def de comparacion de metricas
        comparation_metrics = await workflow.execute_activity(
            "comparation",
            metrics_system,
            start_to_close_timeout=timedelta(minutes=1),
            retry_policy=retry
        )

        #def de tabla SQL
        try:
            await workflow.execute_activity(
                "upsert_metrics_data",
                {
                    "metrics": metrics_system
                },
                start_to_close_timeout=timedelta(minutes=1),
                retry_policy=retry
            )

        except ValueError:
            raise
            
        except Exception:
            raise
        
        if comparation_metrics["alerta"]:
            #def de analisi con gemini IA
            try:
                analysis = await workflow.execute_activity(
                    "analisis_gemini",
                    {
                        "metrics": metrics_system,
                        "alertas": comparation_metrics["alertas"],
                    },
                    start_to_close_timeout=timedelta(minutes=2),
                    retry_policy=retry
                )

            except ValueError:
                raise
                
            except Exception:
                raise

            #def de metricas y analisi en tabla SQL
            try:
                await workflow.execute_activity(
                    "upsert_analisis",
                    {
                        "metrics": metrics_system,
                        "analisis_alerta": analysis
                    },
                    start_to_close_timeout=timedelta(minutes=1),
                    retry_policy=retry
                )

            except ValueError:
                raise
                
            except Exception:
                raise
                
        else:
            analysis = None

        return {
            "metrics": metrics_system,
            "analysis": analysis,
        }

#_______________________
#metricas/metrica_entrada.py
from pydantic import BaseModel, Field

class RateParams(BaseModel):
    base: str = Field(min_length=3, max_length=3)   # "USD"
    symbols: list[str] = Field(min_length=1)        # ["EUR", "MXN"]
#cambiar esto
    
#_______________________
#activitys/activity_psutil.py
import psutil
from temporalio import activity

@activity.defn
def _psutil() -> dict:

    cpu = psutil.cpu_percent(interval=1)

    memoria = psutil.virtual_memory()

    disco = psutil.disk_usage("/")

    return {
        "cpu_percent": round(cpu),
        "memory_percent": round(memoria.percent),
        "memory_available": round(memoria.available),
        "disk_percent": round(disco.percent),
        "disk_used": round(disco.used)
        }

#_______________________
#activitys/activity_comparations.py
from temporalio import activity
from core.config import MAX_PERCENT
from promps.promp import promp_analisis

@activity.defn
def comparation(data_system: dict) -> dict:

    recursos = (
        "cpu_percent",
        "memory_percent",
        "disk_percent",
    )

    if any(data_system[recurso] >= MAX_PERCENT for recurso in recursos):
        data_system["alerta"] = promp_analisis()
        
        return data_system

    return data_system


#_______________________
#promps/promp.py
def promp_analisis() -> str:
    return """
        Actúa como un ingeniero especializado en monitorización de sistemas, análisis de rendimiento y Python.
        Tu tarea es analizar las métricas de rendimiento de una máquina que recibirás mediante un diccionario de Python en formato JSON.

        Las métricas pueden incluir:

        * `cpu_percent`: porcentaje de utilización de CPU.
        * `memory_percent`: porcentaje de utilización de RAM.
        * `memory_available`: memoria RAM disponible en bytes.
        * `disk_percent`: porcentaje de espacio ocupado en disco.
        * `disk_used`: espacio utilizado en disco en bytes.
        
        **Reglas de análisis:**
        
        1. Evalúa individualmente CPU, RAM y disco.
        2. Considera que un recurso presenta un nivel de alerta cuando su porcentaje es igual o superior al 90 %.
        3. Clasifica cada recurso como:

        * `normal`: menos del 70 %.
        * `advertencia`: desde el 70 % hasta menos del 90 %.
        * `critico`: igual o superior al 90 %.
        4. Identifica qué recurso presenta mayor utilización.
        5. Explica brevemente los posibles efectos de los valores observados sobre el rendimiento del sistema.
        6. No declares que la máquina está saturada basándote únicamente en un promedio general. Analiza cada recurso por separado.
        7. Si faltan métricas, hay valores inválidos o los datos son insuficientes, indícalo sin inventar valores.
        8. No inventes información sobre procesos, temperatura, carga de CPU, swap o latencia de disco si no se proporcionan esos datos.
        9. Devuelve exclusivamente un objeto JSON válido, sin Markdown ni texto fuera del JSON.

        **Formato de respuesta obligatorio:**
        {
        "estado_general": "normal | advertencia | critico",
        "recurso_mas_utilizado": "cpu | ram | disco | desconocido",
        "metricas": {
        "cpu": {
        "porcentaje": 0,
        "estado": "normal | advertencia | critico",
        "analisis": ""
        },
        "ram": {
        "porcentaje": 0,
        "estado": "normal | advertencia | critico",
        "analisis": ""
        },
        "disco": {
        "porcentaje": 0,
        "estado": "normal | advertencia | critico",
        "analisis": ""
        }
        },
        "recomendacion": "",
        "datos_insuficientes": false
        }
        
        El campo `estado_general` debe reflejar el estado más grave entre los recursos evaluados. Si alguno está en estado crítico, el estado general será crítico. Si ninguno está crítico, pero alguno está en advertencia, será advertencia. De lo contrario, será normal.
        
        Analiza las siguientes métricas:
        """

#_______________________
#activitys/guardar_metricas_sql.py
from temporalio import activity
from data_base.sql_create_tables import insert_metrics_data

@activity.defn
async def upsert_metrics_data(data: dict):
    try:
        await insert_metrics_data(data)

    except ValueError:
        raise
        
    except Exception:
        raise

#_______________________
#activitys/activity_gemini.py
from temporalio import activity

@activity.defn
def analisis_gemini(data: dict) -> dict:
    pass

#_______________________
#activitys/guardar_analisis_sql.py
from temporalio import activity

@activity.defn
def upsert_analisis


#_______________________
#data_base/sql_create_tables.py
from sqlalchemy import create_engine, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from data_base.engine_sql import Creation_engine
from sqlalchemy.exc import SQLAlchemyError, OperationalError


class Base(declarative_base):
    pass

class Table_data_metrics(Base):
    __tablename__ = "metrics"
    
    ID: Mapped[int] = mapped_column(primary_key=True)
    
    cpu_percent: Mapped[int]= mapped_column(Integer, default=0)
    
    memory_percent: Mapped[int]= mapped_column(Integer, default=0)

    memory_available: Mapped[int]= mapped_column(Integer, default=0)
    
    disk_percent: Mapped[int]= mapped_column(Integer, default=0)

    disk_used: Mapped[int]= mapped_column(Integer, default=0)
    

class Table_data_analisis(Base):
    __tablename__ = "metrics_analisis"

    #aqui va la columna str del analisis texto

    #aqui la json del dict


class Engine_tables:
    def __init__():
        self.Engine = Creation_engine.engine()
        
        self._session_factory = async_sessionmaker(
            self.Engine,
            expire_on_commit=False
        )

    #esto es para que cuando se necesite la sesion solo se llame la funcion por si se modifica async_sessionmaker
    def get_session(self) -> AsyncSession:
        """Obtiene una sesión asíncrona."""
        return self._session_factory()

    #crear tablas
    async def create_tables(self):
        try:
            async with self.Engine.begin() as conn:
                await conn.run_sync(
                    Base.metadata.create_all
                )
        except OperationalError:
            raise("Error operativo al conectar con PostgreSQL ""o crear las tablas.")       
                
        except SQLAlchemyError:
            raise("Error de SQLAlchemy al inicializar las tablas.")
            

        except exception:
            raise("error desconocido")
        

    #insertar metricas
    async def insert_metrics_data(self, data: dict):
        try:
            async with self.get_session() as session:
                async with session.begin():
                    metricas = Table_data_metrics(**data)
                    session.add(metricas)
        
        except ValueError as e:
            raise ValueError("Valores incorrectos al insertar datos") from e

        except Exception as e:
            raise RuntimeError("Error al insertar datos de métricas") from e


    #insertar analisis
    async def upsert_analisis(self):
        try:
            pass
        except ValueError as e:
            raise ValueError("Valores incorrectos al insertar datos") from e

        except Exception as e:
            raise RuntimeError("Error al insertar datos de métricas") from e
            
            
            
            
        
        

#_______________________
#data_base/engine_sql.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from core.config import DB_URL, SQL_ECHO, MAX_WORKERS, MAX_OVERFLOWS
import asyncio
import logging


class Creation_engine:
    def __init__(self):
        self.logger = logging.getLogger(__name__)
        
        self.ENGINE = create_async_engine(
            DB_URL,
            echo=SQL_ECHO,
            pool_size=MAX_WORKERS,
            max_overflow=MAX_OVERFLOWS,
            pool_pre_ping=True
        )
        
    async def engine(self):
        return self.ENGINE

    async def close_engine(self):
        try:
            await self.ENGINE.dispose()
            
        except asyncio.CancelledError:
            self.logger.warning("El cierre del motor fue cancelado.")
            raise
                
        except Exception:
            self.logger.exception("Error al cerrar el motor de la base de datos.")
            raise
            
        
        

#URL = "postgresql+asyncpg://usuario:password@localhost:5432/mi_base_datos"

#postgresql+psycopg = Motor de base de datos y controlador de Python.

#usuario:password = Credenciales de acceso.

#localhost:5432 = Dirección del servidor y puerto de PostgreSQL.

#mi_base_datos = Nombre de la base de datos a la que accederás.
#_______________________
#dockerfile

#_______________________
#docker_compose
#aqui se levanta 
#temporal, sql, API , worker, Prometheus
#_______________________

#_______________________
#core/config.py
DB_URL

SQL_ECHO

MAX_WORKERS

MAX_OVERFLOWS

TASK_QUEUE

WORKFLOW_CONCURRENCY

TEMPORAL_HOST

TEMPORAL_NAMESPACE

MAX_PERCENT






Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: