Skip to content

Logging Contract

Feature: 006-observability | Date: 2026-05-29

Structured Log Format

All log entries emitted by the system MUST conform to this JSON structure:

json
{
  "event": "<event_name>",
  "level": "<severity>",
  "timestamp": "<ISO_8601>",
  "correlation_id": "<UUID4_or_empty>",
  "<key>": "<value>"
}

Required Fields

FieldTypeSourceDescription
eventstrCallerEvent name passed to logger.info(event, ...)
levelstrstructlog processorAuto-added by add_log_level processor
timestampstrstructlog processorISO 8601 format, added by TimeStamper(fmt="iso")
correlation_idstrstructlog processorFrom ContextVar, added by _add_correlation_id processor

Request Log Additional Fields

Backend HTTP request logs include these additional fields:

FieldTypeSourceRequired
methodstrHTTP requestYes
pathstrHTTP requestYes
status_codeintHTTP responseYes
duration_msfloatMeasuredYes
user_idstrJWT or "anonymous"Yes
user_emailstrJWTIf authenticated
user_rolestrJWTIf authenticated
ipstrrequest.client.hostYes
user_agentstrrequest headerYes
geo_countrystrGeoIP lookupIf available
geo_citystrGeoIP lookupIf available

Frontend Proxy Log Fields

FieldTypeSourceRequired
eventstr"proxy_request" or "proxy_error"Yes
methodstrHTTP requestYes
pathstrURL pathYes
status_codeintBackend responseYes
duration_msfloatMeasuredYes
user_idstrServer sessionIf authenticated
user_emailstrServer sessionIf authenticated
user_rolestrServer sessionIf authenticated
ipstrx-forwarded-forIf present
user_agentstrrequest headerIf present
request_bodyobjectRequest body (redacted)For non-GET

Sensitive Field Redaction

The following keys MUST be redacted to [REDACTED] in any logged request body:

  • password
  • hashed_password
  • token
  • access_token
  • refresh_token
  • secret
  • api_key
  • authorization
  • private_key
  • credentials

Redaction is case-insensitive and recursive (nested objects are traversed).

Loki Transport

  • Backend: python-logging-loki LokiHandler attached to root Python logger
  • Frontend: Direct HTTP POST to Loki push API (fire-and-forget)
  • Labels: {app: "scraper", env: "production"} (backend), {app: "frontend", env: NODE_ENV, level: <level>} (frontend)
  • Auth: HTTP Basic with GRAFANA_LOKI_USER:GRAFANA_API_KEY

Metric Instruments

Instrument NameTypeAttributesDescription
scraper_runs_totalCounterTotal scraper run invocations
scraper_run_duration_secondsHistogramDuration of each run
scraper_articles_found_totalCountersourceArticles discovered per source
scraper_articles_new_totalCountersourceNew articles per source
scraper_articles_duplicate_totalCountersourceDuplicate articles per source
scraper_errors_totalCountersourceErrors per source

Environment Variables

VariableUsed ByRequired For
GRAFANA_OTLP_USEROTel metrics + tracingGrafana Cloud OTLP
GRAFANA_API_KEYOTel, Loki (backend + frontend)Grafana Cloud auth
GRAFANA_OTLP_ENDPOINTOTel metrics + tracingGrafana Cloud OTLP
GRAFANA_LOKI_URLLoki (backend + frontend)Loki push
GRAFANA_LOKI_USERLoki (backend + frontend)Loki auth
SENTRY_DSNSentry (CLI entrypoints)Error tracking
NEXTAUTH_SECRETRequestLoggingMiddlewareJWT decode
GEOIP_DB_PATHGeoIPCustom DB path (default: /app/data/GeoLite2-City.mmdb)
TELEGRAM_BOT_TOKENTelegramNotifierPipeline notifications
TELEGRAM_CHAT_IDTelegramNotifierPipeline notifications
GRAFANA_URLGrafana embed proxyDashboard embedding
GRAFANA_SA_TOKENGrafana embed proxyGrafana auth
BACKEND_URLFrontend proxyBackend URL (default: http://localhost:8000)