diff --git a/apps/devops/app/common/config/site_settings.py b/apps/devops/app/common/config/site_settings.py index 9a96914..c313636 100644 --- a/apps/devops/app/common/config/site_settings.py +++ b/apps/devops/app/common/config/site_settings.py @@ -23,6 +23,9 @@ class SiteSettings(BaseSettings): # TODO: confirm with Zhenyu BASE_RECONSILE_URL: str = "https://reconcile.freeleaps.mathmast.com" + # TODO: modify this with actual Loki URL + BASE_LOKI_URL: str = "http://localhost:3100" + class Config: env_file = ".devbase-webapi.env" env_file_encoding = "utf-8" diff --git a/apps/devops/app/common/models/deployment/deployment.py b/apps/devops/app/common/models/deployment/deployment.py index 2bcbdda..4d9a9e1 100644 --- a/apps/devops/app/common/models/deployment/deployment.py +++ b/apps/devops/app/common/models/deployment/deployment.py @@ -1,5 +1,5 @@ -from datetime import datetime -from typing import Literal +from datetime import datetime, timedelta +from typing import Literal, List from beanie import Document from bson import ObjectId @@ -50,6 +50,27 @@ class CheckDeploymentStatusRequest(BaseModel): target_env: str user_id: str +class CheckApplicationLogsRequest(BaseModel): + product_id: str + target_env: Literal["alpha", "prod"] = "alpha" + user_id: str = '' + log_level: List[Literal["info", "error", "debug"]] = Field(default_factory=lambda: ["info"]) + start_time: datetime = datetime.now() - timedelta(minutes=5) + end_time: datetime = datetime.now() + limit: int = 1000 + +class CheckApplicationLogsResponse(BaseModel): + product_id: str + target_env: Literal["alpha", "prod"] + user_id: str = '' + log_level: List[Literal["info", "error", "debug"]] + start_time: datetime + end_time: datetime + limit: int + logs: list[str] + + + diff --git a/apps/devops/app/routes/deployment/apis.py b/apps/devops/app/routes/deployment/apis.py index b1b098e..d514127 100644 --- a/apps/devops/app/routes/deployment/apis.py +++ b/apps/devops/app/routes/deployment/apis.py @@ -5,7 +5,8 @@ from fastapi import APIRouter, Depends from loguru import logger from app.common.models import CodeDepotDoc -from app.common.models.deployment.deployment import Deployment, InitDeploymentRequest +from app.common.models.deployment.deployment import Deployment, InitDeploymentRequest, CheckDeploymentStatusRequest, \ + CheckApplicationLogsRequest, CheckApplicationLogsResponse from app.routes.deployment.service import DeploymentService, get_deployment_service router = APIRouter(prefix="/deployment") @@ -57,3 +58,19 @@ async def create_dummy_code_depot( except Exception as e: logger.error(f"Failed to create dummy code depot: {e}") raise e + +@router.post("/checkApplicationLogs") +async def check_application_logs( + request: CheckApplicationLogsRequest, + service: DeploymentService = Depends(get_deployment_service) +) -> CheckApplicationLogsResponse: + """ + Check application logs for a given deployment. + """ + try: + res = await service.check_application_logs(request) + return res + except Exception as e: + logger.error(f"Failed to check application logs: {e}") + raise e + diff --git a/apps/devops/app/routes/deployment/service.py b/apps/devops/app/routes/deployment/service.py index 476718e..66e13f4 100644 --- a/apps/devops/app/routes/deployment/service.py +++ b/apps/devops/app/routes/deployment/service.py @@ -4,12 +4,14 @@ from datetime import datetime, timedelta from typing import List import httpx +import requests from fastapi import HTTPException, Depends from app.common.config.site_settings import site_settings from app.common.models import Deployment from app.common.models.code_depot.code_depot import CodeDepotDoc, DepotStatus -from app.common.models.deployment.deployment import InitDeploymentRequest +from app.common.models.deployment.deployment import InitDeploymentRequest, CheckApplicationLogsRequest, \ + CheckApplicationLogsResponse class DeploymentService: @@ -157,6 +159,53 @@ class DeploymentService: # raise HTTPException(status_code=response.status_code, detail=response.text) return True + async def check_application_logs( + self, + request: CheckApplicationLogsRequest, + loki_url: str = site_settings.BASE_LOKI_URL, + ) -> CheckApplicationLogsResponse: + # Convert to nanoseconds since epoch + start_ns = int(request.start_time.timestamp() * 1e9) + end_ns = int(request.end_time.timestamp() * 1e9) + + # TODO: convert product_id to application name if needed + base_query = f'{{application="{request.product_id}", environment="{request.target_env}"}}' + log_level = '|'.join(request.log_level) if request.log_level else '' + loki_query = f'{base_query} |~ "{log_level}"' + + params = { + "query": loki_query, + "limit": request.limit, + "start": start_ns, + "end": end_ns, + } + + url = f"{loki_url}/loki/api/v1/query_range" + response = requests.get(url, params=params) + + if response.status_code != 200: + raise Exception(f"Query failed: {response.status_code} - {response.text}") + + data = response.json() + streams = data.get("data", {}).get("result", []) + + logs = [] + for stream in streams: + for ts, log in stream.get("values", []): + timestamp = datetime.fromtimestamp(int(ts) / 1e9) + logs.append(f"[{timestamp}] {log.strip()}") + + return CheckApplicationLogsResponse( + product_id=request.product_id, + target_env=request.target_env, + user_id=request.user_id, + log_level=request.log_level, + start_time=request.start_time, + end_time=request.end_time, + limit=request.limit, + logs=logs + ) + # TODO: dummy test code, remove later async def create_dummy_code_depot( self, diff --git a/apps/devops/requirements.txt b/apps/devops/requirements.txt index 056543d..c593732 100644 --- a/apps/devops/requirements.txt +++ b/apps/devops/requirements.txt @@ -7,4 +7,8 @@ pydantic_settings==2.9.1 pytest==7.1.2 starlette==0.46.2 uvicorn==0.34.2 -httpx==0.24.0 \ No newline at end of file +httpx==0.24.0 +pydantic-settings~=2.9.1 +pymongo~=4.12.1 +pydantic~=2.11.4 +requests~=2.32.3 \ No newline at end of file