Skip to content

Add JSON logging and Prometheus metrics from the service template - #110

Closed
lewisjared wants to merge 2 commits into
mainfrom
feat/observability
Closed

Add JSON logging and Prometheus metrics from the service template#110
lewisjared wants to merge 2 commits into
mainfrom
feat/observability

Conversation

@lewisjared

@lewisjared lewisjared commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Add some additional observability tooling

  • Every request emits one JSON wide event carrying the method, path, status, duration and a request_id, which is also stamped on the response as x-request-id. Set LOG_FORMAT=text for readable logs during local development.
  • Prometheus metrics are served from /metrics.
  • /livez and /readyz answer orchestrator probes. /readyz runs the checks on app.state.readiness_checks, and build_app registers one proving the REF database still answers a query.
  • /deploy/info reports the build stamps baked into the image, the same values the wide events carry.

Removals

GET /api/v1/utils/health-check/ is gone. Nothing called it

scripts/generate-client-sdk.sh used to pipe the app's stdout into openapi.json, which broke once the app started logging to stdout. It now writes the file from Python instead.

Summary by CodeRabbit

  • New Features

    • Added liveness and readiness endpoints for service health monitoring.
    • Added deployment information reporting through /deploy/info.
    • Added Prometheus metrics at /metrics.
    • Added request correlation IDs, processing-time headers, and structured access logs.
    • Added configurable JSON or text log output.
  • Breaking Changes

    • Removed the /api/v1/utils/health-check/ endpoint. Use /livez and /readyz instead.
  • Documentation

    • Added observability documentation covering logs, metrics, health checks, deployment details, and configuration.
  • Tests

    • Added coverage for health, deployment, metrics, logging, middleware, and operational routes.

Adopts the observability setup from copier-python-service, so this app lines
up with the other Climate Resource services.

- Every request emits one JSON wide event carrying the method, path, status,
  duration and a request_id, which is also stamped on the response as
  x-request-id. Set LOG_FORMAT=text for readable logs during local development.
- Prometheus metrics are served from /metrics.
- /livez and /readyz answer orchestrator probes. /readyz runs the checks on
  app.state.readiness_checks, and build_app registers one proving the REF
  database still answers a query.
- /deploy/info reports the build stamps baked into the image.

The template logs through the standard library, so the nine loguru call sites
move across. loguru stays as a transitive dependency of climate-ref, and
configure_logging re-points its sink at the standard library so those records
land in the same stream rather than going out unstructured.

Removes GET /api/v1/utils/health-check/, which nothing called.
@netlify

netlify Bot commented Sep 7, 2026

Copy link
Copy Markdown

Deploy Preview for climate-ref canceled.

Name Link
🔨 Latest commit b42c202
🔍 Latest deploy log https://app.netlify.com/projects/climate-ref/deploys/6a9e8f24a9b96b0008ad951c

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The backend adds structured logging, request correlation, health probes, deployment information, and Prometheus metrics. It removes the legacy health-check client API and updates OpenAPI generation, documentation, tests, and configuration.

Changes

Observability and API operations

Layer / File(s) Summary
Standard logging configuration
.env.sample, backend/src/ref_backend/logging_config.py, backend/src/ref_backend/main.py, backend/src/ref_backend/core/*, backend/src/ref_backend/api/routes/*, backend/src/ref_backend/models/diagnostics.py, backend/src/ref_backend/analytics.py, backend/tests/test_logging_config.py
The backend uses standard-library logging with JSON output by default and text output as an option. Loguru records are routed through the configured formatter.
Operational endpoints and request instrumentation
backend/src/ref_backend/builder.py, backend/src/ref_backend/health.py, backend/src/ref_backend/deploy.py, backend/src/ref_backend/metrics.py, backend/src/ref_backend/middleware.py, backend/pyproject.toml, backend/tests/test_health.py, backend/tests/test_deploy.py, backend/tests/test_metrics.py, backend/tests/test_middleware.py, backend/tests/test_spa_fallback.py
The application registers /livez, /readyz, /deploy/info, and /metrics. Requests receive correlation headers and structured access logs. Readiness checks include database connectivity.
Legacy health-check API removal
frontend/src/client/@tanstack/react-query.gen.ts, frontend/src/client/index.ts, frontend/src/client/sdk.gen.ts, frontend/src/client/types.gen.ts, changelog/110.breaking.md
The generated client removes the legacy health-check function, query helpers, response types, and public exports.
Schema generation and observability documentation
scripts/generate-client-sdk.sh, docs/observability.md, changelog/110.feature.md
OpenAPI JSON is written directly to the frontend file. Documentation records logging, metrics, health, deployment, and observability configuration.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WideEventMiddleware
  participant HealthRouter
  participant Database
  participant Metrics
  Client->>WideEventMiddleware: Send request
  WideEventMiddleware->>HealthRouter: Route request
  HealthRouter->>Database: Run readiness query
  Database-->>HealthRouter: Return readiness result
  HealthRouter-->>WideEventMiddleware: Return response
  WideEventMiddleware->>Metrics: Record request and latency
  WideEventMiddleware-->>Client: Return response with correlation headers
Loading

Merge Risk: 🟡 Moderate · up to b42c2

Sensitive query values can enter access logs and readiness failures can expose internal details. These security issues should be fixed before merge; logging-context omissions also reduce correlation quality.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 24 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the primary observability changes: JSON logging and Prometheus metrics. It is concise and specific.
Full details: Docstring Coverage

Explanation

Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 24 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 9a1441bf-58f1-4abb-b1c5-327792cab2b6

📥 Commits

Reviewing files that changed from the base of the PR and between ea6e50a and b42c202.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • .env.sample
  • backend/pyproject.toml
  • backend/src/ref_backend/analytics.py
  • backend/src/ref_backend/api/routes/aft.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/api/routes/utils.py
  • backend/src/ref_backend/builder.py
  • backend/src/ref_backend/core/config.py
  • backend/src/ref_backend/core/diagnostic_metadata.py
  • backend/src/ref_backend/core/ref.py
  • backend/src/ref_backend/deploy.py
  • backend/src/ref_backend/health.py
  • backend/src/ref_backend/log.py
  • backend/src/ref_backend/logging_config.py
  • backend/src/ref_backend/main.py
  • backend/src/ref_backend/metrics.py
  • backend/src/ref_backend/middleware.py
  • backend/src/ref_backend/models/diagnostics.py
  • backend/tests/test_api/test_routes/test_utils.py
  • backend/tests/test_deploy.py
  • backend/tests/test_health.py
  • backend/tests/test_logging_config.py
  • backend/tests/test_metrics.py
  • backend/tests/test_middleware.py
  • backend/tests/test_spa_fallback.py
  • changelog/110.breaking.md
  • changelog/110.feature.md
  • docs/observability.md
  • frontend/src/client/@tanstack/react-query.gen.ts
  • frontend/src/client/index.ts
  • frontend/src/client/sdk.gen.ts
  • frontend/src/client/types.gen.ts
  • scripts/generate-client-sdk.sh
💤 Files with no reviewable changes (4)
  • backend/tests/test_api/test_routes/test_utils.py
  • frontend/src/client/types.gen.ts
  • backend/src/ref_backend/api/routes/utils.py
  • backend/src/ref_backend/log.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

ok = await ok
except Exception as exc:
logger.warning(f"Readiness check {name} failed", exc_info=True)
failures[name] = str(exc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,140p' backend/src/ref_backend/health.py
printf '\n--- health references ---\n'
rg -n "health_router|readyz|readiness_checks|HTTPException|TrustedHost|AuthenticationMiddleware|middleware" backend/src/ref_backend -g '*.py'

Repository: Climate-REF/ref-app

Length of output: 8383


🏁 Script executed:

sed -n '45,80p' backend/src/ref_backend/main.py
sed -n '1,120p' backend/src/ref_backend/middleware.py

Repository: Climate-REF/ref-app

Length of output: 5214


Information Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information

Reachability: External · Exploitability: Moderate

Do not return readiness-check exception text.

When a check raises, str(exc) is included in the /readyz 503 response. Keep the detailed exception in logs and return a stable generic 503 response body.

Comment on lines +118 to +125
base = super().format(record)
extras = []
for key, value in record.__dict__.items():
if key in _RESERVED_LOG_RECORD_KEYS or key.startswith("_"):
continue
extras.append(f"{key}={value!r}")
if extras:
base = f"{base} | {' '.join(extras)}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include environment context in text logs.

TextFormatter.format only appends fields from record.__dict__. With LOG_FORMAT=text, it omits _ENV_CONTEXT fields, which violates the documented logging contract. Merge _ENV_CONTEXT before adding record-specific extras.

Proposed fix
-        extras = []
+        fields = dict(_ENV_CONTEXT)
         for key, value in record.__dict__.items():
             if key in _RESERVED_LOG_RECORD_KEYS or key.startswith("_"):
                 continue
-            extras.append(f"{key}={value!r}")
+            fields[key] = value
+        extras = [f"{key}={value!r}" for key, value in fields.items()]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
base = super().format(record)
extras = []
for key, value in record.__dict__.items():
if key in _RESERVED_LOG_RECORD_KEYS or key.startswith("_"):
continue
extras.append(f"{key}={value!r}")
if extras:
base = f"{base} | {' '.join(extras)}"
base = super().format(record)
fields = dict(_ENV_CONTEXT)
for key, value in record.__dict__.items():
if key in _RESERVED_LOG_RECORD_KEYS or key.startswith("_"):
continue
fields[key] = value
extras = [f"{key}={value!r}" for key, value in fields.items()]
if extras:
base = f"{base} | {' '.join(extras)}"

Comment on lines +146 to +150
logging.getLogger(record["name"] or "loguru").log(
record["level"].no,
record["message"],
exc_info=record["exception"],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/sh
set -eu

# Inspect remaining bound Loguru calls that require context preservation.
rg -n -C 3 --type py 'from loguru import logger|\.bind\(' backend/src backend/tests

Repository: Climate-REF/ref-app

Length of output: 636


🏁 Script executed:

#!/bin/sh
set -eu

sed -n '1,180p' backend/src/ref_backend/logging_config.py
printf '\n--- Loguru usage and formatter tests ---\n'
rg -n -C 3 --type py 'loguru_logger|logger\.bind|extra=|JsonFormatter|TextFormatter|_loguru_sink' backend/src backend/tests

Repository: Climate-REF/ref-app

Length of output: 12373


🏁 Script executed:

#!/bin/sh
set -eu

python3 - <<'PY'
from pathlib import Path

path = Path("backend/src/ref_backend/logging_config.py")
lines = path.read_text().splitlines()
for start, end in ((70, 155),):
    for number in range(start, min(end, len(lines)) + 1):
        print(f"{number:4}: {lines[number - 1]}")
PY

Repository: Climate-REF/ref-app

Length of output: 3475


🌐 Web query:

official Loguru documentation record extra logging.Logger.log extra Python documentation

💡 Result:

In Loguru, you can record extra information into the log record's extra dictionary using several methods, depending on whether the context is temporary, persistent, or per-log entry [1][2]. To record extra data in Loguru, you can use the following approaches: 1. Keyword arguments in logging calls: When you pass keyword arguments to any logging function (e.g., logger.info("Message", key="value")), Loguru automatically adds those keyword arguments to the extra dictionary of the log record [3][4][5]. These values are used for both message formatting and as part of the extra record attribute [3][5]. 2. The bind() method: Use logger.bind(kwargs) to create a new logger instance with persistent context [2]. Every message logged by this new instance will automatically include the bound keyword arguments in its extra dictionary [3][6]. This is ideal for adding consistent context like user IDs or server information across multiple log entries [2][7]. 3. The contextualize() method: Use logger.contextualize(kwargs) as a context manager (with the with statement) to temporarily add context to the extra dictionary for the duration of a specific block of code [8][2]. This is useful for request-specific data, such as a transaction ID, that should only appear in logs within a certain scope [2]. 4. The patch() method: Use logger.patch() to dynamically update the extra dictionary for every new message [1]. This is useful for adding values that need to be calculated at the time of logging, such as timestamps or other dynamic properties [1][6]. To display this information in your logs, you must ensure your format string includes the extra field (e.g., {extra[key]}) or simply {extra} to show the entire dictionary [1][2][7]. Example: from loguru import logger import sys # Configure a sink that displays extra data logger.remove logger.add(sys.stderr, format="{message} {extra}") # 1. Per-log entry via keyword arguments logger.info("Processing data", user_id=123) # 2. Persistent binding user_logger = logger.bind(session="abc") user_logger.info("User action") # 3. Temporary context with logger.contextualize(request_id="xyz"): logger.info("Inside request")

Citations:


Forward bound Loguru context.

_loguru_sink drops record["extra"] before it calls logging.Logger.log. Bound fields such as request_id do not reach JsonFormatter or TextFormatter. Forward non-reserved fields through extra.

Proposed fix
     record = message.record
+    extra = {
+        key: value
+        for key, value in record["extra"].items()
+        if key not in _RESERVED_LOG_RECORD_KEYS
+    }
     logging.getLogger(record["name"] or "loguru").log(
         record["level"].no,
         record["message"],
         exc_info=record["exception"],
+        extra=extra,
     )

"request_id": request_id,
"method": request.method,
"path": request.url.path,
"query": dict(request.query_params),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External · Exploitability: Moderate

Do not log raw query parameters.

External callers can send sensitive values such as access_token. This middleware copies every query parameter into the access-log event without redaction. Remove query, or allowlist and redact values before logging.

@lewisjared lewisjared closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant