feat(observability): backfill logging for upload, auth, and domain services
Why: - resolve_auth_context() runs on every authenticated request and logged nothing; four distinct rejection reasons (malformed/unknown/inactive/ expired key, inactive tenant) were all invisible. - The domain-allowlist rejection in upload_source_file() happens before any ingestion_jobs row exists, so it wasn't covered by the job-level ingestion.job.failed event either -- a rejected upload left zero trace. - Four of upload_source_file()'s five failure branches (parse_failed, chunk_limit_exceeded, embedding_failed, index_failed) called _mark_job_failed(), which wrote to Postgres but never logged; only storage_failed and timeout had an ad-hoc logger.warning duplicated at their own call sites. Changes: - auth/service.py: auth.succeeded / auth.failed (with a reason field per rejection type), matching ADR-0011's own event catalog. - domains/service.py: domain.rejected on the allowlist check; domain.created / domain.updated / domain.status_changed on the three mutations. - files/upload.py: centralized failure logging inside _mark_job_failed (every failure branch already calls it, so logging there once closes all five branches instead of duplicating a log call at each site) as ingestion.job.failed; added ingestion.job.started; renamed the ad-hoc files.upload.succeeded to ingestion.job.completed for catalog consistency. Impact: - None to request/response behavior -- log events only.
This commit is contained in:
@@ -8,6 +8,7 @@ must run with no Postgres session held open at all.
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.application.auth.context import AuthContext
|
||||
@@ -16,28 +17,66 @@ from src.application.auth.keys import parse_api_key, verify_secret
|
||||
from src.infrastructure.postgres.repositories import api_keys as api_keys_repo
|
||||
from src.infrastructure.postgres.repositories import tenants as tenants_repo
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def resolve_auth_context(
|
||||
sessionmaker: async_sessionmaker[AsyncSession], bearer_token: str
|
||||
) -> AuthContext:
|
||||
"""Resolve a bearer token, logging the outcome either way (ADR-0011).
|
||||
|
||||
This runs on every authenticated request, so `auth.failed` is the one
|
||||
event most likely to matter first when diagnosing a client integration
|
||||
issue -- and the reason string alone (never logged; it can echo back
|
||||
attacker-supplied key material) is not enough to tell a malformed token
|
||||
apart from a revoked one without this.
|
||||
"""
|
||||
parsed = parse_api_key(bearer_token)
|
||||
if parsed is None:
|
||||
logger.warning("auth.failed", reason="malformed_key")
|
||||
raise InvalidApiKeyError("malformed API key")
|
||||
key_prefix, secret = parsed
|
||||
|
||||
async with sessionmaker() as session:
|
||||
api_key = await api_keys_repo.get_by_prefix(session, key_prefix)
|
||||
if api_key is None or not verify_secret(secret, api_key.key_hash):
|
||||
logger.warning("auth.failed", reason="unknown_key", key_prefix=key_prefix)
|
||||
raise InvalidApiKeyError("unknown API key")
|
||||
if api_key.status != "active":
|
||||
logger.warning(
|
||||
"auth.failed",
|
||||
reason="key_inactive",
|
||||
key_prefix=key_prefix,
|
||||
api_key_id=str(api_key.id),
|
||||
key_status=api_key.status,
|
||||
)
|
||||
raise InvalidApiKeyError(f"API key is {api_key.status}")
|
||||
if api_key.expires_at is not None and api_key.expires_at <= datetime.now(UTC):
|
||||
logger.warning(
|
||||
"auth.failed",
|
||||
reason="key_expired",
|
||||
key_prefix=key_prefix,
|
||||
api_key_id=str(api_key.id),
|
||||
)
|
||||
raise InvalidApiKeyError("API key has expired")
|
||||
|
||||
tenant = await tenants_repo.get_by_id(session, api_key.tenant_id)
|
||||
if tenant is None or tenant.status != "active":
|
||||
logger.warning(
|
||||
"auth.failed",
|
||||
reason="tenant_inactive",
|
||||
key_prefix=key_prefix,
|
||||
api_key_id=str(api_key.id),
|
||||
tenant_id=str(api_key.tenant_id),
|
||||
)
|
||||
raise TenantInactiveError("tenant is not active")
|
||||
|
||||
logger.info(
|
||||
"auth.succeeded",
|
||||
tenant_id=str(tenant.id),
|
||||
api_key_id=str(api_key.id),
|
||||
actor_type=api_key.actor_type,
|
||||
)
|
||||
return AuthContext(
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
|
||||
Reference in New Issue
Block a user