Skip to content

UN-2123 [FEAT] Propagate request_id across services and workers - #2229

Open
Deepak-Kesavan wants to merge 1 commit into
mainfrom
UN-2123-propagate-request-id
Open

UN-2123 [FEAT] Propagate request_id across services and workers#2229
Deepak-Kesavan wants to merge 1 commit into
mainfrom
UN-2123-propagate-request-id

Conversation

@Deepak-Kesavan

@Deepak-Kesavan Deepak-Kesavan commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Completes end-to-end request_id (X-Request-ID) correlation so a single ID
can filter logs across the backend, Celery workers and services
in gcloud.

  • backend → workers: a before_task_publish signal
    (backend/backend/celery_signals.py) injects the request-scoped request_id
    (from StateStore) into every published Celery task's message headers — no
    per-send_task changes required.
  • workers: task_prerun now prefers an explicit request_id from the
    message headers over payload-derived ids (file_execution_id, etc.), and a
    new before_task_publish handler re-propagates it onto downstream
    worker→worker task chains.
  • workers → backend: the internal-API HTTP client now forwards
    X-Request-ID from the worker log context, so backend callbacks share the
    originating request's id.
  • x2text-service: reads/mints X-Request-ID and logs it via a
    self-contained logging module (no new dependency).
  • logging: all services unified onto one canonical log format so
    request_id / trace_id / span_id parse identically in gcloud.

Why

Debugging across services is painful when logs can't be correlated. The backend
already assigned a request_id per request (and the frontend forwards one), but
it died at the backend boundary — it was never propagated to the Celery workers
or echoed on worker→backend calls, so worker logs showed request_id:- (or an
execution_id that didn't match the originating request). This threads the same
id through the whole chain.

Jira: UN-2123

How

The worker-side receiving/logging machinery already existed (UN-3435) and was
explicitly designed to accept a real request_id from the producer — this PR
supplies the missing producer side and closes the remaining hops:

  1. before_task_publish (backend) reads StateStore.get(Common.REQUEST_ID) and
    sets headers["request_id"]. Guarded so it can never break task publishing.
  2. Worker _bind_task_context (task_prerun) reads task.request.request_id
    (Celery 5.6.2 exposes custom message headers on the task Context; a raw
    request.headers mapping is used as a version-safe fallback). Resolution
    order: message-header request_id → payload-derived id → Celery task_id.
  3. Worker _propagate_request_id_on_publish (before_task_publish) forwards the
    bound request_id onto tasks the worker itself publishes.
  4. base_client._make_request adds X-Request-ID from the worker log context.
  5. x2text-service create_app() wires setup_logging +
    register_request_id_middleware from a new self-contained app/logging_util.py.
  6. The shared Flask formatter (unstract/core/flask/logging.py) is aligned to
    the canonical backend/worker format.

Out of scope (recommended as separate tickets):

  • LLM Whisperer (LLMW) adoption of this standard — separate repo/product.
  • Full OpenTelemetry trace activation. The OTel scaffolding (trace_id
    fields in every formatter, meta-deps) is currently inert — no service runs
    under opentelemetry-instrument, exporters are hardcoded to none, and there
    is no OTLP endpoint — so trace_id/span_id always render as -. Activating
    real distributed tracing is a larger DevOps/observability effort. This PR uses
    request_id (which the frontend/backend already surface via the
    X-Request-ID response header) as the pragmatic correlation key.
  • SDK1 x2text-adapter → x2text-service header forwarding — needs the SDK
    execution request_id threaded through several tool-side layers; belongs with
    the tool-execution/OTel correlation follow-up. (x2text still logs any
    X-Request-ID it receives after this PR.)

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

Low risk — the change is additive and defensively guarded:

  • Both before_task_publish handlers no-op safely (null-guarded, StateStore
    read wrapped in try/except) so they cannot break task publishing even if
    no request_id is in scope (e.g. beat-scheduled tasks fall back to the
    existing execution_id/task_id behaviour).
  • Reading task.request.request_id uses getattr(..., None) and only overrides
    the previously-working payload-derived id when a header is actually present —
    so existing worker correlation is preserved when the header is absent.
  • One intentional log-format change: the shared Flask formatter now matches
    the canonical backend/worker format, which means Flask-service log lines
    (platform-service, runner) show module:<source-module> instead of the
    logger name. Log content is otherwise unchanged; any dashboard keying on
    the logger name string would need updating (grep-by-request_id/trace_id is
    unaffected and is the point of the change).
  • No API contract, DB, or schema changes.

Database Migrations

None.

Env Config

None. (x2text-service reads the existing LOG_LEVEL env if set; defaults to INFO.)

Relevant Docs

Related Issues or PRs

Dependencies Versions

None added. (x2text-service deliberately avoids taking on unstract-core; it
uses a small self-contained logging module instead.)

Notes on Testing

  • Static: all touched files py_compile-clean; pre-commit (ruff, ruff-format,
    pycln, pyupgrade, secret-scan, test-selection) green.
  • Verified against Celery 5.6.2 that a custom key in the message headers is
    exposed on the task Context via both attribute and .get() access.
  • One runtime smoke test still recommended on a live cluster (not yet run):
    trigger a workflow/API execution and confirm a single X-Request-ID from the
    originating HTTP request appears in the worker log lines (request_id:<id>)
    and on the worker→backend internal-API calls. If Celery header→Context
    attribute promotion ever regressed, the request.headers fallback covers it,
    but the smoke test is the real guarantee.

Screenshots

N/A (no UI surface).

Checklist

I have read and understood the Contribution Guidelines.

Complete end-to-end request_id (X-Request-ID) correlation so a single ID can
be used to filter logs across the backend, Celery workers and services in
gcloud.

- backend: before_task_publish signal injects the request-scoped request_id
  (from StateStore) into every published Celery task's message headers, with
  no per-call-site changes.
- workers: task_prerun now prefers an explicit request_id from the message
  headers over payload-derived ids; a before_task_publish handler re-propagates
  it onto downstream worker->worker task chains.
- workers: internal-API HTTP client now forwards X-Request-ID from the worker
  log context so backend callbacks share the originating request's id.
- x2text-service: reads/mints X-Request-ID and logs it (self-contained, no new
  dependency).
- logging: unified all services onto one canonical log format so request_id/
  trace_id/span_id parse identically in gcloud.

LLMW adoption and full OpenTelemetry trace activation are intentionally out of
scope (separate tickets).
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added end-to-end request ID tracking across web requests, background tasks, and service-to-service calls.
    • Request IDs are now propagated automatically through asynchronous task processing and outbound requests.
    • Requests without an ID receive a generated identifier for reliable correlation.
  • Improvements
    • Standardized log fields and formatting, including request, trace, process, and thread information.
    • Improved task-level request ID prioritization and handling when identifiers are unavailable.

Walkthrough

The changes add request ID middleware and canonical logging fields, propagate request IDs through Celery task headers, bind them in workers, and forward them in outbound worker HTTP requests.

Changes

Request ID observability

Layer / File(s) Summary
Service request logging
x2text-service/app/config.py, x2text-service/app/logging_util.py
The service configures logging from LOG_LEVEL. Middleware records X-Request-ID or generates a UUID. Logging filters add request and OpenTelemetry fields.
Canonical logging fields
unstract/core/src/unstract/core/flask/logging.py
Flask logs now use canonical module, process, thread, request, trace, and span fields.
Celery request propagation
backend/backend/celery_signals.py, backend/backend/celery_service.py, workers/shared/infrastructure/logging/logger.py
Producer signals add request IDs to task headers. Worker context binding prioritizes header IDs, then payload IDs, then task IDs. Signal installation includes before_task_publish.
Worker HTTP propagation
workers/shared/clients/base_client.py
Workers include the bound request ID in the outbound X-Request-ID header when it is available.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FlaskApp
  participant StateStore
  participant Celery
  participant WorkerLogger
  participant BaseClient

  FlaskApp->>FlaskApp: capture or generate request_id
  FlaskApp->>StateStore: store request_id
  Celery->>StateStore: read request_id before publish
  StateStore-->>Celery: return request_id
  Celery->>Celery: add request_id to task headers
  Celery->>WorkerLogger: deliver task message
  WorkerLogger->>WorkerLogger: bind request_id from headers
  WorkerLogger->>BaseClient: provide current request_id
  BaseClient->>BaseClient: send X-Request-ID
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main request_id propagation change across services and workers.
Description check ✅ Passed The description follows the required template and provides clear scope, rationale, implementation details, risks, testing, and related information.
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.
✨ 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 UN-2123-propagate-request-id

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.

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@x2text-service/app/logging_util.py`:
- Around line 31-33: Update RequestIDFilter.filter to call has_request_context()
before accessing Flask’s context-local g; assign the request ID from g only when
a request context exists, otherwise retain "-".
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6da06d9c-e764-45ab-a92a-7479f521926e

📥 Commits

Reviewing files that changed from the base of the PR and between aac5ede and e217f4d.

📒 Files selected for processing (7)
  • backend/backend/celery_service.py
  • backend/backend/celery_signals.py
  • unstract/core/src/unstract/core/flask/logging.py
  • workers/shared/clients/base_client.py
  • workers/shared/infrastructure/logging/logger.py
  • x2text-service/app/config.py
  • x2text-service/app/logging_util.py

Comment on lines +31 to +33
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = getattr(g, "request_id", "-") if g else "-"
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)logging_util\.py$|requirements|pyproject|setup|Pipfile|poetry|gunicorn|main|wsgi|app'

echo "== logging_util.py =="
if [ -f x2text-service/app/logging_util.py ]; then
  cat -n x2text-service/app/logging_util.py
fi

echo "== flask imports/usages of RequestIDFilter/Logging =="
rg -n "RequestIDFilter|LoggingMiddleware|logging_util|log_config|handlers|gunicorn|request_id|from flask import|import flask" -S .

Repository: Zipstack/unstract

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect Flask proxy g semantics from installed package if available =="
python3 - <<'PY'
try:
    from flask import g, has_request_context
    print("flask_available=True")
    print(has_request_context())
    try:
        _ = bool(g)
        print("bool_g_outside_request_context=True")
    except Exception as e:
        print(f"bool_g_outside_request_context_error={type(e).__name__}: {e}")
    try:
        _ = getattr(g, "request_id", "-") if g else "-"
        print("getattr_if_bool_g_outside_request_context=True")
    except Exception as e:
        print(f"getattr_if_bool_g_outside_request_context_error={type(e).__name__}: {e}")
except Exception as e:
    print(f"flask_available_or_probe_error={type(e).__name__}: {e}")
PY

Repository: Zipstack/unstract

Length of output: 305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== similar core logging filter =="
cat -n unstract/core/src/unstract/core/flask/logging.py

echo "== exact matching RequestIDFilter definitions using Flask g =="
python3 - <<'PY'
from pathlib import Path
import re

patterns = [
    re.compile(r"class RequestIDFilter\(logging\.Filter\):"),
    re.compile(r"getattr\(\bg\b, .request_id"),
    re.compile(r"\n\s+if \bg\b else \"\-\""),
]
for path in Path(".").rglob("*.py"):
    text = path.read_text(errors="ignore")
    if "RequestIDFilter" in text and "g, \"request_id\"" in text:
        print("\n---", path, "---")
        for i, m in enumerate(re.finditer(r"class RequestIDFilter\(logging\.Filter\):", text)):
            print(f"definition instance {i+1} at line 1..")
            line = text[:m.start()].count("\n") + 1
            print(line)
            start = m.start()
            end = text.find("\nclass ", start + 1)
            snippet = text[start:end if end != -1 else None]
            print(snippet[:2000])
PY

Repository: Zipstack/unstract

Length of output: 4964


🌐 Web query:

Flask g request_context proxy RuntimeError outside application context has_request_context documentation

💡 Result:

The RuntimeError: Working outside of application context or Working outside of request context in Flask occurs when you attempt to access proxy objects (like g, current_app, request, or session) without an active corresponding context [1][2][3]. The g object is a proxy to a namespace used to store data during a single request or application context [4]. It is bound to the application context, meaning it is only available when an application context is active [1][4]. To resolve these errors, you must ensure your code runs within the appropriate context: 1. Application Context: If you need to access current_app or g outside of a view function (e.g., in a CLI command, initialization script, or background task), wrap your code in a with app.app_context: block [1][2][5]. 2. Request Context: If you need to access request or session objects, you generally need an active HTTP request [3]. When testing, you can use with app.test_request_context: to simulate an environment [3][6]. 3. has_request_context: Flask provides a utility function, flask.has_request_context, which returns a boolean indicating whether a request context is currently active [4]. Similarly, flask.has_app_context can be used to check for an active application context [7]. These errors often arise when code is placed outside of a view function or is being executed in a context where Flask has not been initialized [1][2][3]. If you are attempting to interact with a Flask application from an external script, do not use Flask's request object; instead, use standard HTTP libraries like requests to send requests to the running application [8].

Citations:


Guard Flask context access in RequestIDFilter.

Line 32 dereferences Flask’s context-local g, which can raise RuntimeError when logging runs outside an application/request context; use has_request_context() before accessing it.

Proposed fix
-from flask import Flask, g, request
+from flask import Flask, g, has_request_context, request

 class RequestIDFilter(logging.Filter):
     def filter(self, record: logging.LogRecord) -> bool:
-        record.request_id = getattr(g, "request_id", "-") if g else "-"
+        record.request_id = (
+            getattr(g, "request_id", "-") if has_request_context() else "-"
+        )
         return True
📝 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
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = getattr(g, "request_id", "-") if g else "-"
return True
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = (
getattr(g, "request_id", "-") if has_request_context() else "-"
)
return True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@x2text-service/app/logging_util.py` around lines 31 - 33, Update
RequestIDFilter.filter to call has_request_context() before accessing Flask’s
context-local g; assign the request ID from g only when a request context
exists, otherwise retain "-".

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR propagates request IDs through backend-to-worker Celery messages, worker task chains, worker callbacks, and x2text requests while standardizing service log formats.

  • Registers backend and worker Celery signal handlers for request-ID propagation and task-context binding.
  • Adds X-Request-ID to worker-to-backend internal API calls.
  • Adds request-aware logging middleware and canonical formatting to x2text and shared Flask logging.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure identified.

Request IDs are propagated defensively across Celery and HTTP boundaries, explicit values are preserved, absent context retains established fallback behavior, and task-scoped worker context is cleared after execution.

Important Files Changed

Filename Overview
backend/backend/celery_service.py Imports the new signal module during Celery bootstrap so producer-side propagation is registered.
backend/backend/celery_signals.py Safely injects the request-scoped ID into outgoing Celery message headers without overriding an explicit header.
workers/shared/infrastructure/logging/logger.py Prefers propagated IDs during task context binding, forwards them to downstream tasks, and retains existing fallbacks and cleanup.
workers/shared/clients/base_client.py Adds the current worker correlation ID to internal backend HTTP requests.
x2text-service/app/logging_util.py Introduces standardized request-aware logging and middleware for x2text.
x2text-service/app/config.py Initializes standardized logging and request-ID middleware during application creation.
unstract/core/src/unstract/core/flask/logging.py Aligns shared Flask log output with the backend and worker correlation-field format.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Backend
    participant Broker
    participant WorkerA as Worker
    participant WorkerB as Downstream Worker
    participant X2Text

    Client->>Backend: HTTP request with X-Request-ID
    Backend->>Broker: Publish task with request_id header
    Broker->>WorkerA: Deliver task
    WorkerA->>WorkerA: Bind request_id to log context
    WorkerA->>Broker: Publish downstream task with request_id
    Broker->>WorkerB: Deliver downstream task
    WorkerA->>Backend: Internal callback with X-Request-ID
    Client->>X2Text: Request with optional X-Request-ID
    X2Text->>X2Text: Reuse or mint request_id for logs
Loading

Reviews (1): Last reviewed commit: "UN-2123 [FEAT] Propagate request_id acro..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 20.7
e2e-coowners e2e 1 0 0 0 1.4
e2e-etl e2e 1 0 0 0 8.7
e2e-login e2e 2 0 0 0 1.3
e2e-prompt-studio e2e 1 0 0 0 4.8
e2e-smoke e2e 2 0 0 0 1.5
e2e-workflow e2e 1 0 0 0 16.3
integration-backend integration 267 0 0 26 46.9
integration-connectors integration 1 0 0 7 8.3
integration-workers integration 140 0 0 1 52.4
unit-backend unit 998 0 0 1 31.3
unit-connectors unit 63 0 0 0 8.1
unit-core unit 33 0 0 0 1.0
unit-platform-service unit 15 0 0 0 2.0
unit-rig unit 109 0 0 0 4.1
unit-sdk1 unit 480 0 0 0 18.9
unit-workers unit 1335 0 0 1 88.6
TOTAL 3452 0 0 36 316.5

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

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