Skip to content

Logging & Error Handling

mijanullah12 edited this page Oct 20, 2026 · 1 revision

Libraries

Area Library Config
Backend structlog on top of the standard library logging backend/app/core/logging.py
Backend errors FastAPI exception handlers backend/app/core/errors.py
Frontend No library: a small wrapper over console frontend/src/services/logger.ts
Frontend errors ApiError, ErrorMessage, ErrorBoundary frontend/src/services/api.ts, frontend/src/components/

The standard library's logging is routed through the same structlog renderer, so logging.getLogger(__name__) and uvicorn's logs come out in the same JSON format.

How to log (backend)

from app.core.logging import get_logger, logged_run

logger = get_logger(__name__)
logger.info("organization_matched", organization_id=org.id, rule="domain")

with logged_run("sync", source="omerlo") as counts:
    counts["fetched"] = len(records)
  • Event names are short snake_case verbs in the past tense; data goes in keyword arguments, not in the message.
  • logged_run counts are masked like any other key, so count names must not contain a sensitive word (use contacts_imported, not emails_imported), and must not be named event, level, timestamp, module, duration_ms, run_id, run_kind or source.
  • print is rejected by Ruff (T20); console.* is rejected by ESLint outside the logger.

Levels

Level Use for
DEBUG Detail useful while developing (each page fetched, each rule tried)
INFO Lifecycle events: startup, shutdown, each request, each run started and completed with counts
WARNING Something recoverable: a client error, a skipped record, a retry
ERROR A failure, always with the stack trace (logger.exception or exc_info=)

Set with LOG_LEVEL (backend, default INFO) and VITE_LOG_LEVEL (frontend, default debug in development and warn in builds).

Log format

One JSON object per line on stdout:

{"event": "request_completed", "method": "GET", "path": "/api/health", "status_code": 200, "duration_ms": 1.4, "request_id": "5a52f95c30a64f5093310ef5bc03285e", "level": "info", "module": "app.core.request_context", "timestamp": "2026-10-20T03:28:59.875084Z"}
  • request_id is taken from a safe incoming X-Request-ID header or generated, and returned in the response header and in error bodies, so a user can quote it.
  • run_id is bound for the whole of a sync or import run.

Personal data and secrets

  • Values under keys containing password, secret, token, api_key, apikey, authorization, credential, cookie, email or phone become [REDACTED]. Keys are matched lowercased with - read as _, so the X-Api-Key header is covered.
  • Emails and North American phone numbers inside any text, including stack traces, become [EMAIL] and [PHONE] (a number that is part of a decimal, such as 1234567890.5, is not masked).
  • Only the request path is logged, never the query string (ADR D8). httpx logs are kept at WARNING because they include full URLs.
  • Request validation errors are returned and logged without the rejected input.

Errors

Exception HTTP status code
DataValidationError 422 validation_error
NotFoundError 404 not_found
ConnectorError(source=...) 502 connector_error
PulseError (base) 500 internal_error
Any other exception 500 internal_error, generic message, stack trace only in the logs

Every error response has the body {"error": {"code", "message", "details", "request_id"}}. Messages of PulseError subclasses are shown to users, so write them for a user and keep personal data out of them. In the frontend, internal_error, service_unavailable, network_error (the server could not be reached) and any 5xx http_error (a server error without a standard body) show a friendly generic text; other error messages are shown as written.

Clone this wiki locally