From a5c525ed74c677b791f75c73d65e1d2ec5e21007 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Wed, 26 Aug 2026 10:41:52 +0530 Subject: [PATCH 1/5] feat: tracing(in DB) and logger --- .../versions/010_add_traceability_columns.py | 84 +++++++++ backend/app/api/routes/guardrails.py | 161 ++++++++++++++++-- backend/app/core/enum.py | 23 +++ backend/app/core/exception_handlers.py | 16 ++ .../config/base_validator_config.py | 4 +- backend/app/crud/request_log.py | 1 + backend/app/models/logging/request_log.py | 14 +- backend/app/models/logging/validator_log.py | 41 +++++ backend/app/schemas/guardrail_config.py | 8 +- ...est_guardrails_traceability_integration.py | 98 +++++++++++ 10 files changed, 428 insertions(+), 22 deletions(-) create mode 100644 backend/app/alembic/versions/010_add_traceability_columns.py create mode 100644 backend/app/tests/test_guardrails_traceability_integration.py diff --git a/backend/app/alembic/versions/010_add_traceability_columns.py b/backend/app/alembic/versions/010_add_traceability_columns.py new file mode 100644 index 00000000..6774160d --- /dev/null +++ b/backend/app/alembic/versions/010_add_traceability_columns.py @@ -0,0 +1,84 @@ +"""Add traceability columns to request_log and validator_log + +Revision ID: 010 +Revises: 009 +Create Date: 2026-08-25 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision: str = "010" +down_revision = "009" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "request_log", + sa.Column( + "metadata", + JSONB, + nullable=True, + comment="Full run_guardrails request payload", + ), + ) + op.add_column( + "validator_log", + sa.Column( + "order", + sa.Integer(), + nullable=True, + comment="1-based execution order of the validator within the request", + ), + ) + op.add_column( + "validator_log", + sa.Column( + "stage", + sa.String(), + nullable=True, + comment="Stage the validator checked (input or output)", + ), + ) + op.add_column( + "validator_log", + sa.Column( + "type", + sa.String(), + nullable=True, + comment="Validator type (ValidatorType enum value)", + ), + ) + op.add_column( + "validator_log", + sa.Column( + "family", + sa.String(), + nullable=True, + comment="Validator family (lexical, classifier or semantic)", + ), + ) + op.add_column( + "validator_log", + sa.Column( + "metadata", + JSONB, + nullable=True, + comment="Full resolved validator config used for this run", + ), + ) + + +def downgrade() -> None: + op.drop_column("validator_log", "metadata") + op.drop_column("validator_log", "family") + op.drop_column("validator_log", "type") + op.drop_column("validator_log", "stage") + op.drop_column("validator_log", "order") + op.drop_column("request_log", "metadata") diff --git a/backend/app/api/routes/guardrails.py b/backend/app/api/routes/guardrails.py index 5779c93f..56a0cc1f 100644 --- a/backend/app/api/routes/guardrails.py +++ b/backend/app/api/routes/guardrails.py @@ -1,3 +1,4 @@ +import logging import uuid from uuid import UUID @@ -13,8 +14,13 @@ LLM_CRITIC_REPHRASE_MESSAGE, REPHRASE_ON_FAIL_PREFIX, ) -from app.core.enum import LLMValidatorName, ValidatorType -from app.core.exception_handlers import _safe_error_message +from app.core.enum import ( + VALIDATOR_FAMILY, + LLMValidatorName, + Stage, + ValidatorType, +) +from app.core.exception_handlers import _normalize_error_detail, _safe_error_message from app.core.guardrail_controller import build_guard, get_validator_config_models from app.core.validators.config.answer_relevance_custom_llm_safety_validator_config import ( AnswerRelevanceCustomLLMSafetyValidatorConfig, @@ -37,6 +43,8 @@ from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse from app.utils import APIResponse, load_description +logger = logging.getLogger(__name__) + router = APIRouter(prefix="/guardrails", tags=["guardrails"]) @@ -50,7 +58,7 @@ def run_guardrails( payload: GuardrailRequest, session: SessionDep, auth: AuthDep, - suppress_pass_logs: bool = True, + suppress_pass_logs: bool = False, ): """ Resolves any config-backed validator references (ban list words, topic relevance scope), @@ -64,9 +72,35 @@ def run_guardrails( payload, auth.organization_id, auth.project_id ) except ValueError: + logger.warning( + "run_guardrails: invalid request_id %r (org=%s project=%s), no request log written", + payload.request_id, + auth.organization_id, + auth.project_id, + ) return APIResponse.failure_response(error="Invalid request_id") - _resolve_validator_configs(payload, session, auth) + try: + _resolve_validator_configs(payload, session, auth) + except Exception as exc: + # Config resolution failed (missing/mismatched stored config, DB error). + # Finalize the request log so it never sits at PROCESSING forever. + error_message = ( + _normalize_error_detail(exc.detail) + if isinstance(exc, HTTPException) + else _safe_error_message(exc) + ) + logger.error( + "run_guardrails: config resolution failed for request_log %s: %s", + request_log.id, + exc, + exc_info=not isinstance(exc, HTTPException), + ) + _mark_request_failed(request_log_crud, request_log.id, str(error_message)) + if isinstance(exc, HTTPException): + raise + return APIResponse.failure_response(error=str(error_message)) + has_output_validator = any( isinstance(v, AnswerRelevanceCustomLLMSafetyValidatorConfig) for v in payload.validators @@ -113,6 +147,25 @@ def list_validators(auth: AuthDep): return {"validators": validators} +def _mark_request_failed( + request_log_crud: RequestLogCrud, request_log_id: UUID, error_message: str +) -> None: + """Best-effort finalization of a request log on an early-exit error path.""" + try: + request_log_crud.update( + request_log_id=request_log_id, + request_status=RequestStatus.ERROR, + request_log_update=RequestLogUpdate( + response_text=error_message, + response_id=uuid.uuid4(), + ), + ) + except Exception: + logger.exception( + "Failed to finalize request log %s after an error", request_log_id + ) + + def _resolve_validator_configs( payload: GuardrailRequest, session: Session, auth: TenantContext ) -> None: @@ -221,19 +274,34 @@ def _finalize( if response_text is None: response_text = "Validation failed" - request_log_crud.update( - request_log_id=request_log_id, - request_status=status, - request_log_update=RequestLogUpdate( - response_text=response_text, - response_id=response_id, - ), - ) + # Log-persistence failures are logged but never break the user response. + try: + request_log_crud.update( + request_log_id=request_log_id, + request_status=status, + request_log_update=RequestLogUpdate( + response_text=response_text, + response_id=response_id, + ), + ) + except Exception: + logger.exception("Failed to update request log %s", request_log_id) if guard is not None: - add_validator_logs( - guard, request_log_id, validator_log_crud, auth, suppress_pass_logs - ) + try: + add_validator_logs( + guard, + request_log_id, + validator_log_crud, + auth, + suppress_pass_logs, + validator_configs=validators, + ) + except Exception: + logger.exception( + "Failed to write validator logs for request log %s", + request_log_id, + ) rephrase_needed = validated_output is not None and ( validated_output == LLM_CRITIC_REPHRASE_MESSAGE @@ -277,6 +345,12 @@ def _finalize( ) except Exception as exc: + logger.error( + "Guardrails execution failed for request log %s: %s", + request_log_id, + exc, + exc_info=True, + ) # Case 3: unexpected system / runtime failure # First try to extract structured fail results from guard history. # This handles on_fail="exception" where guardrails raises instead of returning. @@ -322,12 +396,31 @@ def _redact_input(error_message: str, input_text: str) -> str: return error_message.replace(input_text, "") +def _map_validator_configs(guard: Guard, validator_configs) -> dict: + """ + Maps guard-history validator names (rail_alias) back to the request's + validator configs, using the built validators the guard actually ran. + """ + built = getattr(guard, "_validators", None) + if not built or not validator_configs: + return {} + # ponytail: first config wins per alias; two same-type validators in one + # request share trace fields. Split by position if that ever matters. + mapping = {} + for validator, config in zip(built, validator_configs): + alias = getattr(validator, "rail_alias", None) + if alias: + mapping.setdefault(alias, config) + return mapping + + def add_validator_logs( guard: Guard, request_log_id: UUID, validator_log_crud: ValidatorLogCrud, auth: TenantContext, suppress_pass_logs: bool = False, + validator_configs=None, ) -> None: """ Writes a ValidatorLog entry for each validator outcome in the guard's last iteration. @@ -346,7 +439,9 @@ def add_validator_logs( if not outputs or not getattr(outputs, "validator_logs", None): return - for log in iteration.outputs.validator_logs: + config_by_alias = _map_validator_configs(guard, validator_configs) + + for order, log in enumerate(iteration.outputs.validator_logs, start=1): result = log.validation_result if result is None: @@ -359,18 +454,50 @@ def add_validator_logs( if isinstance(result, FailResult): error_message = result.error_message + # registered_name is the rail alias ("guardrails/ban_list"); + # validator_name is only the display/class name. + config = config_by_alias.get( + getattr(log, "registered_name", None) or log.validator_name + ) + stage = type_ = family = meta = None + if config is not None: + type_ = config.type + family = VALIDATOR_FAMILY.get(config.type) + stage = ( + config.stage.value + if config.stage + else ( + Stage.Output.value + if config.type == ValidatorType.AnswerRelevanceCustomLLM.value + else Stage.Input.value + ) + ) + meta = config.model_dump(mode="json") + validator_log = ValidatorLog( request_id=request_log_id, organization_id=auth.organization_id, project_id=auth.project_id, name=log.validator_name, + order=order, + stage=stage, + type=type_, + family=family, + meta=meta, input=str(log.value_before_validation), output=log.value_after_validation, error=error_message, outcome=ValidatorOutcome(result.outcome.upper()), ) - validator_log_crud.create(log=validator_log) + try: + validator_log_crud.create(log=validator_log) + except Exception: + logger.exception( + "Failed to write validator log (validator=%s, request_log=%s)", + log.validator_name, + request_log_id, + ) def _normalize_llm_critic_error(message: str) -> str: diff --git a/backend/app/core/enum.py b/backend/app/core/enum.py index fee1a644..0cf1b8c3 100644 --- a/backend/app/core/enum.py +++ b/backend/app/core/enum.py @@ -43,3 +43,26 @@ class ValidatorType(Enum): ProfanityFree = "profanity_free" NSFWText = "nsfw_text" AnswerRelevanceCustomLLM = "answer_relevance_custom_llm" + + +class ValidatorFamily(str, Enum): + Lexical = "lexical" + Classifier = "classifier" + Semantic = "semantic" + + +# Family of each validator type: lexical = word/pattern matching, +# classifier = local ML model, semantic = makes a real LLM call. +VALIDATOR_FAMILY: dict[str, ValidatorFamily] = { + ValidatorType.LexicalSlur.value: ValidatorFamily.Lexical, + ValidatorType.BanList.value: ValidatorFamily.Lexical, + ValidatorType.ProfanityFree.value: ValidatorFamily.Lexical, + ValidatorType.PIIRemover.value: ValidatorFamily.Classifier, + ValidatorType.GenderAssumptionBias.value: ValidatorFamily.Classifier, + ValidatorType.LlamaGuard7B.value: ValidatorFamily.Classifier, + ValidatorType.NSFWText.value: ValidatorFamily.Classifier, + ValidatorType.TopicRelevance.value: ValidatorFamily.Semantic, + ValidatorType.TopicRelevanceLLM.value: ValidatorFamily.Semantic, + ValidatorType.LLMCritic.value: ValidatorFamily.Semantic, + ValidatorType.AnswerRelevanceCustomLLM.value: ValidatorFamily.Semantic, +} diff --git a/backend/app/core/exception_handlers.py b/backend/app/core/exception_handlers.py index dd5739c9..be3fa9f1 100644 --- a/backend/app/core/exception_handlers.py +++ b/backend/app/core/exception_handlers.py @@ -1,3 +1,5 @@ +import logging + from fastapi import FastAPI, Request, HTTPException from fastapi.exceptions import RequestValidationError, ResponseValidationError from fastapi.responses import JSONResponse @@ -10,6 +12,8 @@ from app.core.config import settings from app.utils import APIResponse +logger = logging.getLogger(__name__) + def _format_validation_errors(errors: list[dict]) -> str: missing_fields: list[str] = [] @@ -102,6 +106,12 @@ async def starlette_http_exception_handler( async def response_validation_error_handler( request: Request, exc: ResponseValidationError ): + logger.error( + "Response validation failed for %s %s", + request.method, + request.url.path, + exc_info=exc, + ) return JSONResponse( status_code=HTTP_500_INTERNAL_SERVER_ERROR, content=APIResponse.failure_response(_safe_error_message(exc)).model_dump(), @@ -109,6 +119,12 @@ async def response_validation_error_handler( @app.exception_handler(Exception) async def generic_error_handler(request: Request, exc: Exception): + logger.error( + "Unhandled exception for %s %s", + request.method, + request.url.path, + exc_info=exc, + ) return JSONResponse( status_code=HTTP_500_INTERNAL_SERVER_ERROR, content=APIResponse.failure_response(_safe_error_message(exc)).model_dump(), diff --git a/backend/app/core/validators/config/base_validator_config.py b/backend/app/core/validators/config/base_validator_config.py index 1fda7629..d336a52e 100644 --- a/backend/app/core/validators/config/base_validator_config.py +++ b/backend/app/core/validators/config/base_validator_config.py @@ -5,7 +5,7 @@ from pydantic import ConfigDict, PrivateAttr from sqlmodel import SQLModel -from app.core.enum import GuardrailOnFail, ValidatorType +from app.core.enum import GuardrailOnFail, Stage, ValidatorType from app.core.on_fail_actions import rephrase_query_on_fail @@ -13,6 +13,8 @@ class BaseValidatorConfig(SQLModel): model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) on_fail: GuardrailOnFail = GuardrailOnFail.Fix + # Which text this validator is meant to check; recorded in ValidatorLog. + stage: Optional[Stage] = None _validator_metadata: Optional[Dict[str, Any]] = PrivateAttr(default=None) def _on_fix(self, value: str, fail_result: FailResult): diff --git a/backend/app/crud/request_log.py b/backend/app/crud/request_log.py index 17731c6b..c0cec3f2 100644 --- a/backend/app/crud/request_log.py +++ b/backend/app/crud/request_log.py @@ -24,6 +24,7 @@ def create( output_text=payload.output, organization_id=organization_id, project_id=project_id, + meta=payload.model_dump(mode="json"), ) self.session.add(create_request_log) self.session.commit() diff --git a/backend/app/models/logging/request_log.py b/backend/app/models/logging/request_log.py index 648e44d2..4d7e29c4 100644 --- a/backend/app/models/logging/request_log.py +++ b/backend/app/models/logging/request_log.py @@ -1,8 +1,10 @@ from datetime import datetime from enum import Enum -from typing import Optional +from typing import Any, Optional from uuid import UUID, uuid4 +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import JSONB from sqlmodel import SQLModel, Field from app.utils import now @@ -61,6 +63,16 @@ class RequestLog(SQLModel, table=True): sa_column_kwargs={"comment": "LLM output text passed for output guardrails"}, ) + meta: Optional[dict[str, Any]] = Field( + default=None, + sa_column=Column( + "metadata", + JSONB, + nullable=True, + comment="Full run_guardrails request payload", + ), + ) + response_text: Optional[str] = Field( default=None, nullable=True, diff --git a/backend/app/models/logging/validator_log.py b/backend/app/models/logging/validator_log.py index c04a4a11..5eed4f0c 100644 --- a/backend/app/models/logging/validator_log.py +++ b/backend/app/models/logging/validator_log.py @@ -1,7 +1,10 @@ from datetime import datetime from enum import Enum +from typing import Any from uuid import UUID, uuid4 +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import JSONB from sqlmodel import SQLModel, Field from app.utils import now @@ -42,6 +45,44 @@ class ValidatorLog(SQLModel, table=True): sa_column_kwargs={"comment": "Name of the validator used"}, ) + order: int | None = Field( + default=None, + nullable=True, + sa_column_kwargs={ + "comment": "1-based execution order of the validator within the request" + }, + ) + + stage: str | None = Field( + default=None, + nullable=True, + sa_column_kwargs={"comment": "Stage the validator checked (input or output)"}, + ) + + type: str | None = Field( + default=None, + nullable=True, + sa_column_kwargs={"comment": "Validator type (ValidatorType enum value)"}, + ) + + family: str | None = Field( + default=None, + nullable=True, + sa_column_kwargs={ + "comment": "Validator family (lexical, classifier or semantic)" + }, + ) + + meta: dict[str, Any] | None = Field( + default=None, + sa_column=Column( + "metadata", + JSONB, + nullable=True, + comment="Full resolved validator config used for this run", + ), + ) + input: str = Field( nullable=False, sa_column_kwargs={"comment": "Input message for the validator to check"}, diff --git a/backend/app/schemas/guardrail_config.py b/backend/app/schemas/guardrail_config.py index 24fafcfe..9dd57be9 100644 --- a/backend/app/schemas/guardrail_config.py +++ b/backend/app/schemas/guardrail_config.py @@ -86,9 +86,11 @@ def normalize_validators_from_config_api(cls, data): # Strip persistence/system fields before handing a stored validator # config to Guardrails. Reuse the shared system-field set, but keep - # `type` (the discriminator) and `on_fail_action` (remapped below), - # and add the DB-only columns. - drop_fields = (VALIDATOR_CONFIG_SYSTEM_FIELDS - {"type", "on_fail_action"}) | { + # `type` (the discriminator), `stage` (recorded in validator logs), + # and `on_fail_action` (remapped below), and add the DB-only columns. + drop_fields = ( + VALIDATOR_CONFIG_SYSTEM_FIELDS - {"type", "on_fail_action", "stage"} + ) | { "id", "created_at", "updated_at", diff --git a/backend/app/tests/test_guardrails_traceability_integration.py b/backend/app/tests/test_guardrails_traceability_integration.py new file mode 100644 index 00000000..e6ce819d --- /dev/null +++ b/backend/app/tests/test_guardrails_traceability_integration.py @@ -0,0 +1,98 @@ +import pytest +from sqlmodel import Session, select + +from app.models.logging.request_log import RequestLog, RequestStatus +from app.models.logging.validator_log import ValidatorLog +from app.tests.conftest import test_engine +from app.tests.seed_data import ( + VALIDATOR_INTEGRATION_ORGANIZATION_ID, + VALIDATOR_INTEGRATION_PROJECT_ID, +) +from app.tests.utils.constants import VALIDATE_API_PATH + +pytestmark = pytest.mark.integration + +request_id = "123e4567-e89b-12d3-a456-426614174000" +TENANT_HEADERS = { + "X-ORGANIZATION-ID": str(VALIDATOR_INTEGRATION_ORGANIZATION_ID), + "X-PROJECT-ID": str(VALIDATOR_INTEGRATION_PROJECT_ID), +} + + +def test_validator_logs_capture_order_stage_type_family_and_metadata( + integration_client, +): + payload = { + "request_id": request_id, + "input": "this contains badword", + "validators": [ + {"type": "ban_list", "banned_words": ["badword"], "stage": "input"}, + {"type": "uli_slur_match", "severity": "all"}, + ], + } + response = integration_client.post( + VALIDATE_API_PATH, headers=TENANT_HEADERS, json=payload + ) + assert response.status_code == 200 + assert response.json()["success"] is True + + with Session(test_engine) as session: + request_log = session.exec(select(RequestLog)).one() + assert request_log.status == RequestStatus.SUCCESS + # Metadata is the parsed payload with validator defaults filled in. + assert request_log.meta["request_id"] == request_id + assert request_log.meta["input"] == payload["input"] + assert [v["type"] for v in request_log.meta["validators"]] == [ + "ban_list", + "uli_slur_match", + ] + + logs = session.exec(select(ValidatorLog).order_by(ValidatorLog.order)).all() + # Passing validators are logged by default (suppress_pass_logs=False). + assert [log.order for log in logs] == [1, 2] + assert [log.type for log in logs] == ["ban_list", "uli_slur_match"] + assert [log.family for log in logs] == ["lexical", "lexical"] + assert [log.stage for log in logs] == ["input", "input"] + assert logs[0].meta["banned_words"] == ["badword"] + assert logs[0].meta["stage"] == "input" + assert logs[1].meta["type"] == "uli_slur_match" + + +def test_config_resolution_failure_finalizes_request_log(integration_client): + response = integration_client.post( + VALIDATE_API_PATH, + headers=TENANT_HEADERS, + json={ + "request_id": request_id, + "input": "hello", + "validators": [ + { + "type": "ban_list", + "ban_list_id": "00000000-0000-0000-0000-000000000000", + } + ], + }, + ) + assert response.status_code == 404 + + with Session(test_engine) as session: + request_log = session.exec(select(RequestLog)).one() + # The row must not be left at PROCESSING forever. + assert request_log.status == RequestStatus.ERROR + assert request_log.response_text + + +def test_invalid_request_id_returns_failure_without_crash(integration_client): + response = integration_client.post( + VALIDATE_API_PATH, + headers=TENANT_HEADERS, + json={ + "request_id": "not-a-uuid", + "input": "hello", + "validators": [{"type": "ban_list", "banned_words": ["x"]}], + }, + ) + assert response.status_code == 200 + body = response.json() + assert body["success"] is False + assert body["error"] == "Invalid request_id" From 01cec99be3d69c53b22f58772d12a6bfb7bfb8ff Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Tue, 1 Sep 2026 09:18:45 +0530 Subject: [PATCH 2/5] PR fixes and nits --- backend/app/api/API_USAGE.md | 4 +- .../app/api/docs/guardrails/run_guardrails.md | 6 +- backend/app/api/routes/guardrails.py | 58 +++++++++++-------- backend/app/core/exception_handlers.py | 4 +- ...ance_custom_llm_safety_validator_config.py | 3 + ...est_guardrails_traceability_integration.py | 28 +++++++++ 6 files changed, 73 insertions(+), 30 deletions(-) diff --git a/backend/app/api/API_USAGE.md b/backend/app/api/API_USAGE.md index 2c6f263a..e04d0034 100644 --- a/backend/app/api/API_USAGE.md +++ b/backend/app/api/API_USAGE.md @@ -186,7 +186,7 @@ Endpoint: - `POST /api/v1/guardrails/` Query params: -- `suppress_pass_logs=true|false` (default `true`) +- `suppress_pass_logs=true|false` (default `false`: pass-case validator logs are persisted too; set `true` to store only fail-case logs) Request fields: - `request_id` (UUID string) @@ -204,7 +204,7 @@ Important: Example: ```bash -curl -X POST "http://localhost:8001/api/v1/guardrails/?suppress_pass_logs=true" \ +curl -X POST "http://localhost:8001/api/v1/guardrails/" \ -H "Authorization: Bearer " \ -H "X-ORGANIZATION-ID: 1" \ -H "X-PROJECT-ID: 101" \ diff --git a/backend/app/api/docs/guardrails/run_guardrails.md b/backend/app/api/docs/guardrails/run_guardrails.md index 7a02220d..c8c5f9b9 100644 --- a/backend/app/api/docs/guardrails/run_guardrails.md +++ b/backend/app/api/docs/guardrails/run_guardrails.md @@ -2,9 +2,9 @@ Runs guardrails on input text with a selected list of validators. Behavior notes: - Runtime validator format uses `on_fail`; config-style payloads with `on_fail_action` are accepted and normalized. -- `suppress_pass_logs=true` skips persisting pass-case validator logs. -- The endpoint always saves a `request_log` entry for the run. -- Validator logs are also saved; with `suppress_pass_logs=true`, only fail-case validator logs are persisted. Otherwise, all validator logs are added. +- By default (`suppress_pass_logs=false`), a validator log is persisted for every executed validator, pass or fail. Pass `suppress_pass_logs=true` to persist only fail-case validator logs. +- The endpoint always saves a `request_log` entry for the run, including the full request payload in its `metadata`. +- Each validator log records the validator's execution `order`, `stage` (input/output), `type`, `family`, and the resolved config in `metadata`. - For `ban_list`, `ban_list_id` can be resolved to `banned_words` from tenant ban list configs. - For `topic_relevance`, `topic_relevance_config_id` is required and is resolved to `llm_prompt` + `prompt_schema_version` from tenant LLM prompt configs. Requires `OPENAI_API_KEY` to be configured; returns a validation failure with an explicit error if missing. - For `llm_critic`, `OPENAI_API_KEY` must be configured; returns `success=false` with an explicit error if missing. diff --git a/backend/app/api/routes/guardrails.py b/backend/app/api/routes/guardrails.py index 56a0cc1f..1141bc0c 100644 --- a/backend/app/api/routes/guardrails.py +++ b/backend/app/api/routes/guardrails.py @@ -28,6 +28,7 @@ from app.core.validators.config.ban_list_safety_validator_config import ( BanListSafetyValidatorConfig, ) +from app.core.validators.config.base_validator_config import BaseValidatorConfig from app.core.validators.config.topic_relevance_llm_safety_validator_config import ( TopicRelevanceLLMSafetyValidatorConfig, ) @@ -73,7 +74,7 @@ def run_guardrails( ) except ValueError: logger.warning( - "run_guardrails: invalid request_id %r (org=%s project=%s), no request log written", + "[run_guardrails] invalid request_id %r (org=%s project=%s), no request log written", payload.request_id, auth.organization_id, auth.project_id, @@ -90,16 +91,18 @@ def run_guardrails( if isinstance(exc, HTTPException) else _safe_error_message(exc) ) + if isinstance(error_message, list): + error_message = "; ".join(str(item) for item in error_message) logger.error( - "run_guardrails: config resolution failed for request_log %s: %s", + "[run_guardrails] config resolution failed for request_log %s: %s", request_log.id, exc, exc_info=not isinstance(exc, HTTPException), ) - _mark_request_failed(request_log_crud, request_log.id, str(error_message)) + _mark_request_failed(request_log_crud, request_log.id, error_message) if isinstance(exc, HTTPException): raise - return APIResponse.failure_response(error=str(error_message)) + return APIResponse.failure_response(error=error_message) has_output_validator = any( isinstance(v, AnswerRelevanceCustomLLMSafetyValidatorConfig) @@ -118,7 +121,7 @@ def run_guardrails( @router.get("/", description=load_description("guardrails/list_validators.md")) -def list_validators(auth: AuthDep): +def list_validators(_: AuthDep): """ Lists all validators and their parameters directly. """ @@ -152,6 +155,9 @@ def _mark_request_failed( ) -> None: """Best-effort finalization of a request log on an early-exit error path.""" try: + # The error that got us here may have left the shared session in a + # failed-transaction state; clear it or this update raises too. + request_log_crud.session.rollback() request_log_crud.update( request_log_id=request_log_id, request_status=RequestStatus.ERROR, @@ -162,7 +168,8 @@ def _mark_request_failed( ) except Exception: logger.exception( - "Failed to finalize request log %s after an error", request_log_id + "[_mark_request_failed] failed to finalize request log %s after an error", + request_log_id, ) @@ -285,7 +292,12 @@ def _finalize( ), ) except Exception: - logger.exception("Failed to update request log %s", request_log_id) + logger.exception( + "[_finalize] failed to update request log %s", request_log_id + ) + # Clear the failed transaction so the validator-log writes + # below still have a usable session. + request_log_crud.session.rollback() if guard is not None: try: @@ -299,7 +311,7 @@ def _finalize( ) except Exception: logger.exception( - "Failed to write validator logs for request log %s", + "[_finalize] failed to write validator logs for request log %s", request_log_id, ) @@ -346,7 +358,7 @@ def _finalize( except Exception as exc: logger.error( - "Guardrails execution failed for request log %s: %s", + "[_validate_with_guard] guardrails execution failed for request log %s: %s", request_log_id, exc, exc_info=True, @@ -396,7 +408,9 @@ def _redact_input(error_message: str, input_text: str) -> str: return error_message.replace(input_text, "") -def _map_validator_configs(guard: Guard, validator_configs) -> dict: +def _map_validator_configs( + guard: Guard, validator_configs: list[BaseValidatorConfig] | None +) -> dict[str, BaseValidatorConfig]: """ Maps guard-history validator names (rail_alias) back to the request's validator configs, using the built validators the guard actually ran. @@ -406,7 +420,7 @@ def _map_validator_configs(guard: Guard, validator_configs) -> dict: return {} # ponytail: first config wins per alias; two same-type validators in one # request share trace fields. Split by position if that ever matters. - mapping = {} + mapping: dict[str, BaseValidatorConfig] = {} for validator, config in zip(built, validator_configs): alias = getattr(validator, "rail_alias", None) if alias: @@ -420,11 +434,12 @@ def add_validator_logs( validator_log_crud: ValidatorLogCrud, auth: TenantContext, suppress_pass_logs: bool = False, - validator_configs=None, + validator_configs: list[BaseValidatorConfig] | None = None, ) -> None: """ Writes a ValidatorLog entry for each validator outcome in the guard's last iteration. - Pass results are skipped when suppress_pass_logs is True. + Pass results are skipped when suppress_pass_logs is True; `order` keeps each + row's true execution position, so persisted orders may have gaps — intentional. """ history = getattr(guard, "history", None) if not history: @@ -463,15 +478,9 @@ def add_validator_logs( if config is not None: type_ = config.type family = VALIDATOR_FAMILY.get(config.type) - stage = ( - config.stage.value - if config.stage - else ( - Stage.Output.value - if config.type == ValidatorType.AnswerRelevanceCustomLLM.value - else Stage.Input.value - ) - ) + # Per-config stage defaults live on the config classes + # (answer_relevance defaults to output). + stage = config.stage.value if config.stage else Stage.Input.value meta = config.model_dump(mode="json") validator_log = ValidatorLog( @@ -494,10 +503,13 @@ def add_validator_logs( validator_log_crud.create(log=validator_log) except Exception: logger.exception( - "Failed to write validator log (validator=%s, request_log=%s)", + "[add_validator_logs] failed to write validator log (validator=%s, request_log=%s)", log.validator_name, request_log_id, ) + # Clear the failed transaction so one bad row doesn't poison + # the inserts for the remaining validators. + validator_log_crud.session.rollback() def _normalize_llm_critic_error(message: str) -> str: diff --git a/backend/app/core/exception_handlers.py b/backend/app/core/exception_handlers.py index be3fa9f1..aa5b62a9 100644 --- a/backend/app/core/exception_handlers.py +++ b/backend/app/core/exception_handlers.py @@ -107,7 +107,7 @@ async def response_validation_error_handler( request: Request, exc: ResponseValidationError ): logger.error( - "Response validation failed for %s %s", + "[response_validation_error_handler] response validation failed for %s %s", request.method, request.url.path, exc_info=exc, @@ -120,7 +120,7 @@ async def response_validation_error_handler( @app.exception_handler(Exception) async def generic_error_handler(request: Request, exc: Exception): logger.error( - "Unhandled exception for %s %s", + "[generic_error_handler] unhandled exception for %s %s", request.method, request.url.path, exc_info=exc, diff --git a/backend/app/core/validators/config/answer_relevance_custom_llm_safety_validator_config.py b/backend/app/core/validators/config/answer_relevance_custom_llm_safety_validator_config.py index d3ae0eda..8cc25630 100644 --- a/backend/app/core/validators/config/answer_relevance_custom_llm_safety_validator_config.py +++ b/backend/app/core/validators/config/answer_relevance_custom_llm_safety_validator_config.py @@ -2,12 +2,15 @@ from uuid import UUID from app.core.config import settings +from app.core.enum import Stage from app.core.validators.answer_relevance_custom_llm import AnswerRelevanceCustomLLM from app.core.validators.config.base_validator_config import BaseValidatorConfig class AnswerRelevanceCustomLLMSafetyValidatorConfig(BaseValidatorConfig): type: Literal["answer_relevance_custom_llm"] + # This validator judges the LLM's answer, so it checks the output stage. + stage: Stage | None = Stage.Output llm_callable: str = settings.ANSWER_RELEVANCE_LLM_MODEL # Inline prompt template with {query} and {answer} placeholders. # If None, the validator uses its built-in default. diff --git a/backend/app/tests/test_guardrails_traceability_integration.py b/backend/app/tests/test_guardrails_traceability_integration.py index e6ce819d..c414933e 100644 --- a/backend/app/tests/test_guardrails_traceability_integration.py +++ b/backend/app/tests/test_guardrails_traceability_integration.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock, patch + import pytest from sqlmodel import Session, select @@ -82,6 +84,32 @@ def test_config_resolution_failure_finalizes_request_log(integration_client): assert request_log.response_text +def test_guard_execution_failure_finalizes_request_log(integration_client): + failing_guard = MagicMock() + failing_guard.validate.side_effect = RuntimeError("boom") + failing_guard.history = None # no structured fail results to extract + + with patch("app.api.routes.guardrails.build_guard", return_value=failing_guard): + response = integration_client.post( + VALIDATE_API_PATH, + headers=TENANT_HEADERS, + json={ + "request_id": request_id, + "input": "hello", + "validators": [{"type": "ban_list", "banned_words": ["x"]}], + }, + ) + + # The failure is a first-class outcome, not an exception to the caller. + assert response.status_code == 200 + assert response.json()["success"] is False + + with Session(test_engine) as session: + request_log = session.exec(select(RequestLog)).one() + assert request_log.status == RequestStatus.ERROR + assert request_log.response_text + + def test_invalid_request_id_returns_failure_without_crash(integration_client): response = integration_client.post( VALIDATE_API_PATH, From a2172cd0b67b4122fbf149fc52034152e7605956 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Tue, 1 Sep 2026 09:45:08 +0530 Subject: [PATCH 3/5] fix: remove family refs and added hardening of logs --- .../versions/010_add_traceability_columns.py | 16 +++---- .../app/api/docs/guardrails/run_guardrails.md | 2 +- backend/app/api/routes/guardrails.py | 47 ++++++++++++------- backend/app/core/enum.py | 23 --------- backend/app/crud/request_log.py | 7 ++- backend/app/models/logging/validator_log.py | 16 +++---- ...est_guardrails_traceability_integration.py | 7 ++- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/backend/app/alembic/versions/010_add_traceability_columns.py b/backend/app/alembic/versions/010_add_traceability_columns.py index 6774160d..a023b097 100644 --- a/backend/app/alembic/versions/010_add_traceability_columns.py +++ b/backend/app/alembic/versions/010_add_traceability_columns.py @@ -40,28 +40,28 @@ def upgrade() -> None: op.add_column( "validator_log", sa.Column( - "stage", - sa.String(), + "duration_ms", + sa.Integer(), nullable=True, - comment="Stage the validator checked (input or output)", + comment="Wall-clock execution time of the validator in milliseconds", ), ) op.add_column( "validator_log", sa.Column( - "type", + "stage", sa.String(), nullable=True, - comment="Validator type (ValidatorType enum value)", + comment="Stage the validator checked (input or output)", ), ) op.add_column( "validator_log", sa.Column( - "family", + "type", sa.String(), nullable=True, - comment="Validator family (lexical, classifier or semantic)", + comment="Validator type (ValidatorType enum value)", ), ) op.add_column( @@ -77,8 +77,8 @@ def upgrade() -> None: def downgrade() -> None: op.drop_column("validator_log", "metadata") - op.drop_column("validator_log", "family") op.drop_column("validator_log", "type") op.drop_column("validator_log", "stage") + op.drop_column("validator_log", "duration_ms") op.drop_column("validator_log", "order") op.drop_column("request_log", "metadata") diff --git a/backend/app/api/docs/guardrails/run_guardrails.md b/backend/app/api/docs/guardrails/run_guardrails.md index c8c5f9b9..b48f6b2c 100644 --- a/backend/app/api/docs/guardrails/run_guardrails.md +++ b/backend/app/api/docs/guardrails/run_guardrails.md @@ -4,7 +4,7 @@ Behavior notes: - Runtime validator format uses `on_fail`; config-style payloads with `on_fail_action` are accepted and normalized. - By default (`suppress_pass_logs=false`), a validator log is persisted for every executed validator, pass or fail. Pass `suppress_pass_logs=true` to persist only fail-case validator logs. - The endpoint always saves a `request_log` entry for the run, including the full request payload in its `metadata`. -- Each validator log records the validator's execution `order`, `stage` (input/output), `type`, `family`, and the resolved config in `metadata`. +- Each validator log records the validator's execution `order`, `duration_ms`, `stage` (input/output), `type`, and the resolved config (plus any verdict detail under `result_metadata`) in `metadata`. - For `ban_list`, `ban_list_id` can be resolved to `banned_words` from tenant ban list configs. - For `topic_relevance`, `topic_relevance_config_id` is required and is resolved to `llm_prompt` + `prompt_schema_version` from tenant LLM prompt configs. Requires `OPENAI_API_KEY` to be configured; returns a validation failure with an explicit error if missing. - For `llm_critic`, `OPENAI_API_KEY` must be configured; returns `success=false` with an explicit error if missing. diff --git a/backend/app/api/routes/guardrails.py b/backend/app/api/routes/guardrails.py index 1141bc0c..d6158334 100644 --- a/backend/app/api/routes/guardrails.py +++ b/backend/app/api/routes/guardrails.py @@ -1,3 +1,4 @@ +import json import logging import uuid from uuid import UUID @@ -14,12 +15,7 @@ LLM_CRITIC_REPHRASE_MESSAGE, REPHRASE_ON_FAIL_PREFIX, ) -from app.core.enum import ( - VALIDATOR_FAMILY, - LLMValidatorName, - Stage, - ValidatorType, -) +from app.core.enum import LLMValidatorName, Stage, ValidatorType from app.core.exception_handlers import _normalize_error_detail, _safe_error_message from app.core.guardrail_controller import build_guard, get_validator_config_models from app.core.validators.config.answer_relevance_custom_llm_safety_validator_config import ( @@ -28,7 +24,6 @@ from app.core.validators.config.ban_list_safety_validator_config import ( BanListSafetyValidatorConfig, ) -from app.core.validators.config.base_validator_config import BaseValidatorConfig from app.core.validators.config.topic_relevance_llm_safety_validator_config import ( TopicRelevanceLLMSafetyValidatorConfig, ) @@ -41,7 +36,11 @@ from app.crud.validator_log import ValidatorLogCrud from app.models.logging.request_log import RequestLogUpdate, RequestStatus from app.models.logging.validator_log import ValidatorLog, ValidatorOutcome -from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse +from app.schemas.guardrail_config import ( + GuardrailRequest, + GuardrailResponse, + ValidatorConfigItem, +) from app.utils import APIResponse, load_description logger = logging.getLogger(__name__) @@ -70,7 +69,7 @@ def run_guardrails( try: request_log = request_log_crud.create( - payload, auth.organization_id, auth.project_id + payload, auth.organization_id, auth.project_id, suppress_pass_logs ) except ValueError: logger.warning( @@ -409,8 +408,8 @@ def _redact_input(error_message: str, input_text: str) -> str: def _map_validator_configs( - guard: Guard, validator_configs: list[BaseValidatorConfig] | None -) -> dict[str, BaseValidatorConfig]: + guard: Guard, validator_configs: list[ValidatorConfigItem] | None +) -> dict[str, ValidatorConfigItem]: """ Maps guard-history validator names (rail_alias) back to the request's validator configs, using the built validators the guard actually ran. @@ -420,7 +419,7 @@ def _map_validator_configs( return {} # ponytail: first config wins per alias; two same-type validators in one # request share trace fields. Split by position if that ever matters. - mapping: dict[str, BaseValidatorConfig] = {} + mapping: dict[str, ValidatorConfigItem] = {} for validator, config in zip(built, validator_configs): alias = getattr(validator, "rail_alias", None) if alias: @@ -434,7 +433,7 @@ def add_validator_logs( validator_log_crud: ValidatorLogCrud, auth: TenantContext, suppress_pass_logs: bool = False, - validator_configs: list[BaseValidatorConfig] | None = None, + validator_configs: list[ValidatorConfigItem] | None = None, ) -> None: """ Writes a ValidatorLog entry for each validator outcome in the guard's last iteration. @@ -474,24 +473,40 @@ def add_validator_logs( config = config_by_alias.get( getattr(log, "registered_name", None) or log.validator_name ) - stage = type_ = family = meta = None + stage = type_ = meta = None if config is not None: type_ = config.type - family = VALIDATOR_FAMILY.get(config.type) # Per-config stage defaults live on the config classes # (answer_relevance defaults to output). stage = config.stage.value if config.stage else Stage.Input.value meta = config.model_dump(mode="json") + # Verdict detail the validator attached to its result (e.g. topic + # relevance scope_score/reasoning); stored beside the config dump. + result_metadata = getattr(result, "metadata", None) + if result_metadata: + meta = meta or {} + # Round-trip through json so a non-serializable value degrades to + # its str() instead of failing the row insert. + meta["result_metadata"] = json.loads( + json.dumps(result_metadata, default=str) + ) + + duration_ms = None + start_time = getattr(log, "start_time", None) + end_time = getattr(log, "end_time", None) + if start_time and end_time: + duration_ms = int((end_time - start_time).total_seconds() * 1000) + validator_log = ValidatorLog( request_id=request_log_id, organization_id=auth.organization_id, project_id=auth.project_id, name=log.validator_name, order=order, + duration_ms=duration_ms, stage=stage, type=type_, - family=family, meta=meta, input=str(log.value_before_validation), output=log.value_after_validation, diff --git a/backend/app/core/enum.py b/backend/app/core/enum.py index 0cf1b8c3..fee1a644 100644 --- a/backend/app/core/enum.py +++ b/backend/app/core/enum.py @@ -43,26 +43,3 @@ class ValidatorType(Enum): ProfanityFree = "profanity_free" NSFWText = "nsfw_text" AnswerRelevanceCustomLLM = "answer_relevance_custom_llm" - - -class ValidatorFamily(str, Enum): - Lexical = "lexical" - Classifier = "classifier" - Semantic = "semantic" - - -# Family of each validator type: lexical = word/pattern matching, -# classifier = local ML model, semantic = makes a real LLM call. -VALIDATOR_FAMILY: dict[str, ValidatorFamily] = { - ValidatorType.LexicalSlur.value: ValidatorFamily.Lexical, - ValidatorType.BanList.value: ValidatorFamily.Lexical, - ValidatorType.ProfanityFree.value: ValidatorFamily.Lexical, - ValidatorType.PIIRemover.value: ValidatorFamily.Classifier, - ValidatorType.GenderAssumptionBias.value: ValidatorFamily.Classifier, - ValidatorType.LlamaGuard7B.value: ValidatorFamily.Classifier, - ValidatorType.NSFWText.value: ValidatorFamily.Classifier, - ValidatorType.TopicRelevance.value: ValidatorFamily.Semantic, - ValidatorType.TopicRelevanceLLM.value: ValidatorFamily.Semantic, - ValidatorType.LLMCritic.value: ValidatorFamily.Semantic, - ValidatorType.AnswerRelevanceCustomLLM.value: ValidatorFamily.Semantic, -} diff --git a/backend/app/crud/request_log.py b/backend/app/crud/request_log.py index c0cec3f2..c7e901ca 100644 --- a/backend/app/crud/request_log.py +++ b/backend/app/crud/request_log.py @@ -16,15 +16,20 @@ def create( payload: GuardrailRequest, organization_id: int, project_id: int, + suppress_pass_logs: bool = False, ) -> RequestLog: request_id = UUID(payload.request_id) + # suppress_pass_logs is a query param (not part of the payload), but a + # trace reader needs it to tell "passes suppressed" from "nothing ran". + meta = payload.model_dump(mode="json") + meta["suppress_pass_logs"] = suppress_pass_logs create_request_log = RequestLog( request_id=request_id, request_text=payload.input, output_text=payload.output, organization_id=organization_id, project_id=project_id, - meta=payload.model_dump(mode="json"), + meta=meta, ) self.session.add(create_request_log) self.session.commit() diff --git a/backend/app/models/logging/validator_log.py b/backend/app/models/logging/validator_log.py index 5eed4f0c..6ede6b10 100644 --- a/backend/app/models/logging/validator_log.py +++ b/backend/app/models/logging/validator_log.py @@ -53,24 +53,24 @@ class ValidatorLog(SQLModel, table=True): }, ) - stage: str | None = Field( + duration_ms: int | None = Field( default=None, nullable=True, - sa_column_kwargs={"comment": "Stage the validator checked (input or output)"}, + sa_column_kwargs={ + "comment": "Wall-clock execution time of the validator in milliseconds" + }, ) - type: str | None = Field( + stage: str | None = Field( default=None, nullable=True, - sa_column_kwargs={"comment": "Validator type (ValidatorType enum value)"}, + sa_column_kwargs={"comment": "Stage the validator checked (input or output)"}, ) - family: str | None = Field( + type: str | None = Field( default=None, nullable=True, - sa_column_kwargs={ - "comment": "Validator family (lexical, classifier or semantic)" - }, + sa_column_kwargs={"comment": "Validator type (ValidatorType enum value)"}, ) meta: dict[str, Any] | None = Field( diff --git a/backend/app/tests/test_guardrails_traceability_integration.py b/backend/app/tests/test_guardrails_traceability_integration.py index c414933e..d0494793 100644 --- a/backend/app/tests/test_guardrails_traceability_integration.py +++ b/backend/app/tests/test_guardrails_traceability_integration.py @@ -21,7 +21,7 @@ } -def test_validator_logs_capture_order_stage_type_family_and_metadata( +def test_validator_logs_capture_order_stage_type_and_metadata( integration_client, ): payload = { @@ -44,6 +44,9 @@ def test_validator_logs_capture_order_stage_type_family_and_metadata( # Metadata is the parsed payload with validator defaults filled in. assert request_log.meta["request_id"] == request_id assert request_log.meta["input"] == payload["input"] + # Query params aren't in the payload; recorded so the trace shows + # whether pass logs were suppressed. + assert request_log.meta["suppress_pass_logs"] is False assert [v["type"] for v in request_log.meta["validators"]] == [ "ban_list", "uli_slur_match", @@ -52,8 +55,8 @@ def test_validator_logs_capture_order_stage_type_family_and_metadata( logs = session.exec(select(ValidatorLog).order_by(ValidatorLog.order)).all() # Passing validators are logged by default (suppress_pass_logs=False). assert [log.order for log in logs] == [1, 2] + assert all(log.duration_ms is not None and log.duration_ms >= 0 for log in logs) assert [log.type for log in logs] == ["ban_list", "uli_slur_match"] - assert [log.family for log in logs] == ["lexical", "lexical"] assert [log.stage for log in logs] == ["input", "input"] assert logs[0].meta["banned_words"] == ["badword"] assert logs[0].meta["stage"] == "input" From ad4a75d7d8e7756a6b72ecd8694fe68f2fa7e0a1 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Fri, 11 Sep 2026 15:01:47 +0530 Subject: [PATCH 4/5] gitignored logs --- .gitignore | 4 +++- backend/logs/app.log | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 70a7a62c..02306ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ backend/app/evaluation/outputs/**/*.json # marimo related artefacts backend/notebooks/*.csv -**/__marimo__/ \ No newline at end of file +**/__marimo__/ + +backend/logs/* \ No newline at end of file diff --git a/backend/logs/app.log b/backend/logs/app.log index d4cbda7e..7846f310 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -21288,3 +21288,17 @@ LiteLLM completion() model= gpt-4o-mini; provider = openai 2026-09-09 16:58:26,562 INFO watchfiles.main 1 change detected 2026-09-09 16:58:26,950 INFO watchfiles.main 30 changes detected 2026-09-09 16:58:29,728 INFO watchfiles.main 6 changes detected +2026-09-11 14:36:04,068 INFO presidio-analyzer Using device of type: cpu +2026-09-11 14:36:05,006 INFO app.core.telemetry Telemetry initialized (environment=production) +2026-09-11 14:36:05,006 INFO uvicorn.error Started server process [45507] +2026-09-11 14:36:05,007 INFO uvicorn.error Waiting for application startup. +2026-09-11 14:36:05,007 INFO uvicorn.error Application startup complete. +2026-09-11 14:36:05,008 INFO uvicorn.error Shutting down +2026-09-11 14:36:05,109 INFO uvicorn.error Waiting for application shutdown. +2026-09-11 14:36:05,110 INFO uvicorn.error Application shutdown complete. +2026-09-11 14:36:05,110 INFO uvicorn.error Finished server process [45507] +2026-09-11 14:36:09,841 INFO presidio-analyzer Using device of type: cpu +2026-09-11 14:36:10,619 INFO app.core.telemetry Telemetry initialized (environment=production) +2026-09-11 14:36:10,619 INFO uvicorn.error Started server process [45642] +2026-09-11 14:36:10,620 INFO uvicorn.error Waiting for application startup. +2026-09-11 14:36:10,620 INFO uvicorn.error Application startup complete. From ff1d78fb61f5c4ff8665d1870f0f76dc9ac7dc29 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Fri, 11 Sep 2026 15:19:48 +0530 Subject: [PATCH 5/5] chore: update docker-compose to add cloudwatch streaming --- .env.example | 2 ++ docker-compose.yml | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.env.example b/.env.example index ccaf0103..41b9c359 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,5 @@ ALLOWED_IPS="127.0.0.1" # URL for the guardrails API — required for the multiple_validators evaluation script GUARDRAILS_API_URL="http://localhost:8001/api/v1/guardrails/" +AWSLOGS_GROUP=/ec2/kaapi-staging-ui-guardrails + diff --git a/docker-compose.yml b/docker-compose.yml index 6c11e7a1..a15e0770 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,3 +57,10 @@ services: path: ./backend/pyproject.toml - action: rebuild path: ./backend/uv.lock + logging: + driver: awslogs + options: + awslogs-region: ap-south-1 + awslogs-group: ${AWSLOGS_GROUP:-/ec2/kaapi-staging-ui-guardrails} + awslogs-stream: backend + awslogs-create-group: "true"