diff --git a/pyproject.toml b/pyproject.toml index 8163787..21e453a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "fastapi[standard]>=0.116.0", "httpx>=0.28.1", "pydantic>=2.11.0", + "pydantic-settings>=2.6.1", "uplink>=0.9.7", ] diff --git a/src/comment_automation/celery_app.py b/src/comment_automation/celery_app.py index 0982c94..255fa76 100644 --- a/src/comment_automation/celery_app.py +++ b/src/comment_automation/celery_app.py @@ -1,11 +1,16 @@ -import os - from celery import Celery +from comment_automation.logging_config import configure_logging, get_celery_logger +from comment_automation.settings import get_settings + +configure_logging() +logger = get_celery_logger() +settings = get_settings() + celery_app = Celery( "comment_automation", - broker=os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0"), - backend=os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0"), + broker=settings.celery_broker_url, + backend=settings.celery_result_backend, include=["comment_automation.webhooks.tasks"], ) @@ -16,3 +21,5 @@ celery_app.conf.update( timezone="UTC", enable_utc=True, ) + +logger.info("Celery app configured") diff --git a/src/comment_automation/logging_config.py b/src/comment_automation/logging_config.py new file mode 100644 index 0000000..0e3b98c --- /dev/null +++ b/src/comment_automation/logging_config.py @@ -0,0 +1,46 @@ +import logging +from logging.config import dictConfig + +from comment_automation.settings import get_settings + + +def configure_logging() -> None: + settings = get_settings() + + dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "standard", + } + }, + "loggers": { + "comment_automation.http": { + "handlers": ["console"], + "level": settings.http_log_level, + "propagate": False, + }, + "comment_automation.celery": { + "handlers": ["console"], + "level": settings.celery_log_level, + "propagate": False, + }, + }, + } + ) + + +def get_http_logger() -> logging.Logger: + return logging.getLogger("comment_automation.http") + + +def get_celery_logger() -> logging.Logger: + return logging.getLogger("comment_automation.celery") diff --git a/src/comment_automation/main.py b/src/comment_automation/main.py index 0806b66..5e59168 100644 --- a/src/comment_automation/main.py +++ b/src/comment_automation/main.py @@ -1,8 +1,12 @@ from fastapi import FastAPI from pydantic import BaseModel +from comment_automation.logging_config import configure_logging, get_http_logger from comment_automation.webhooks.app import webhooks_app +configure_logging() +logger = get_http_logger() + app = FastAPI(title="CommentAutomation") app.mount("/webhooks", webhooks_app) @@ -13,4 +17,5 @@ class HealthResponse(BaseModel): @app.get("/health", response_model=HealthResponse) def health() -> HealthResponse: + logger.debug("Health check requested") return HealthResponse(status="ok") diff --git a/src/comment_automation/settings.py b/src/comment_automation/settings.py new file mode 100644 index 0000000..a3d115b --- /dev/null +++ b/src/comment_automation/settings.py @@ -0,0 +1,29 @@ +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class AppSettings(BaseSettings): + model_config = SettingsConfigDict(extra="ignore") + + celery_broker_url: str = Field( + default="redis://localhost:6379/0", alias="CELERY_BROKER_URL" + ) + celery_result_backend: str = Field( + default="redis://localhost:6379/0", + alias="CELERY_RESULT_BACKEND", + ) + + llm_endpoint_url: str | None = Field(default=None, alias="LLM_ENDPOINT_URL") + llm_endpoint_api_key: str | None = Field(default=None, alias="LLM_ENDPOINT_API_KEY") + llm_endpoint_timeout_seconds: float = Field( + default=10.0, + alias="LLM_ENDPOINT_TIMEOUT_SECONDS", + gt=0, + ) + + http_log_level: str = Field(default="INFO", alias="HTTP_LOG_LEVEL") + celery_log_level: str = Field(default="INFO", alias="CELERY_LOG_LEVEL") + + +def get_settings() -> AppSettings: + return AppSettings() diff --git a/src/comment_automation/webhooks/tasks.py b/src/comment_automation/webhooks/tasks.py index 823e236..54fb3a7 100644 --- a/src/comment_automation/webhooks/tasks.py +++ b/src/comment_automation/webhooks/tasks.py @@ -1,9 +1,11 @@ -import os from typing import Any import httpx from celery import shared_task +from comment_automation.logging_config import get_celery_logger +from comment_automation.settings import get_settings + class LLMEndpointTemporaryError(Exception): """Raised when the LLM endpoint fails with a transient/retryable error.""" @@ -13,6 +15,9 @@ class LLMEndpointConfigurationError(Exception): """Raised when required LLM endpoint configuration is missing.""" +logger = get_celery_logger() + + @shared_task( bind=True, name="comment_automation.tasks.send_instagram_comment_to_llm", @@ -33,18 +38,21 @@ def send_instagram_comment_to_llm(self, payload: dict[str, Any]) -> None: def _forward_payload_to_llm(payload: dict[str, Any]) -> None: """Send payload to LLM endpoint, mapping transient failures to retries.""" - endpoint_url = os.getenv("LLM_ENDPOINT_URL") + settings = get_settings() + + endpoint_url = settings.llm_endpoint_url if not endpoint_url: raise LLMEndpointConfigurationError("LLM_ENDPOINT_URL is not configured") - api_key = os.getenv("LLM_ENDPOINT_API_KEY") - timeout = float(os.getenv("LLM_ENDPOINT_TIMEOUT_SECONDS", "10")) + api_key = settings.llm_endpoint_api_key + timeout = settings.llm_endpoint_timeout_seconds headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" try: + logger.debug("Sending webhook payload to LLM endpoint") response = httpx.post( endpoint_url, json=payload, @@ -52,11 +60,13 @@ def _forward_payload_to_llm(payload: dict[str, Any]) -> None: timeout=timeout, ) except (httpx.TimeoutException, httpx.NetworkError) as exc: + logger.warning("Transient network failure while calling LLM endpoint") raise LLMEndpointTemporaryError( "Temporary network issue while calling LLM endpoint" ) from exc if response.status_code in {429, 500, 502, 503, 504}: + logger.warning("Retryable response from LLM endpoint: %s", response.status_code) raise LLMEndpointTemporaryError( f"Retryable LLM endpoint response status: {response.status_code}" ) diff --git a/uv.lock b/uv.lock index 1b1150c..c928d92 100644 --- a/uv.lock +++ b/uv.lock @@ -305,6 +305,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "uplink" }, ] @@ -326,6 +327,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "pydantic", specifier = ">=2.11.0" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "uplink", specifier = ">=0.9.7" }, ]