-
Notifications
You must be signed in to change notification settings - Fork 700
UN-2123 [FEAT] Propagate request_id across services and workers #2229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Deepak-Kesavan
wants to merge
1
commit into
main
Choose a base branch
from
UN-2123-propagate-request-id
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Celery signal handlers for the backend (producer side). | ||
|
|
||
| Propagates the HTTP ``request_id`` (correlation ID assigned by | ||
| ``CustomRequestIDMiddleware``) onto every published Celery task so that worker | ||
| logs can be correlated back to the originating request. | ||
|
|
||
| The value is placed in the task message headers under ``request_id``. Workers | ||
| read it from ``task.request`` in ``task_prerun`` and bind it onto their log | ||
| context -- see ``workers/shared/infrastructure/logging/logger.py``. Using the | ||
| ``before_task_publish`` signal means this works for *every* ``send_task`` / | ||
| ``.delay`` / ``.apply_async`` call with no per-call-site changes. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| from account_v2.constants import Common | ||
| from celery.signals import before_task_publish | ||
| from utils.local_context import StateStore | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @before_task_publish.connect | ||
| def propagate_request_id(headers=None, **kwargs): | ||
| """Inject the current request_id into the outgoing task's message headers. | ||
|
|
||
| Fires in the producer thread (the web request thread for API-triggered | ||
| tasks), where ``StateStore`` still holds the request_id set by | ||
| ``CustomRequestIDMiddleware``. No-ops when there is no request_id in scope | ||
| (e.g. beat-scheduled publishes), leaving the worker to fall back to its | ||
| own correlation id (execution_id / task_id). | ||
| """ | ||
| if headers is None: | ||
| return | ||
| try: | ||
| request_id = StateStore.get(Common.REQUEST_ID) | ||
| except Exception: | ||
| # StateStore can raise if CONCURRENCY_MODE is misconfigured; never let | ||
| # correlation plumbing break task publishing. | ||
| logger.debug("Unable to read request_id from StateStore", exc_info=True) | ||
| return | ||
| if request_id and not headers.get(Common.REQUEST_ID): | ||
| headers[Common.REQUEST_ID] = request_id |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """Request-id-aware logging for the x2text-service. | ||
|
|
||
| Self-contained mirror of the shared ``unstract.core.flask`` logging pattern so | ||
| the service participates in cross-service correlation (a single ``request_id`` | ||
| in every log line) without taking on the ``unstract-core`` dependency. | ||
|
|
||
| The format string is kept identical to the Django backend and the workers so a | ||
| single gcloud query parses ``request_id`` / ``trace_id`` / ``span_id`` uniformly | ||
| across every service. | ||
| """ | ||
|
|
||
| import logging | ||
| import uuid | ||
| from logging.config import dictConfig | ||
|
|
||
| from flask import Flask, g, request | ||
|
|
||
| # Canonical log format shared with the Django backend (``enriched``) and the | ||
| # workers (``WorkerLogger``). Keep these in sync. | ||
| LOG_FORMAT = ( | ||
| "%(levelname)s : [%(asctime)s]" | ||
| "{module:%(module)s process:%(process)d thread:%(thread)d " | ||
| "request_id:%(request_id)s trace_id:%(otelTraceID)s span_id:%(otelSpanID)s}" | ||
| " :- %(message)s" | ||
| ) | ||
|
|
||
|
|
||
| class RequestIDFilter(logging.Filter): | ||
| """Inject the current request's ``request_id`` into log records.""" | ||
|
|
||
| def filter(self, record: logging.LogRecord) -> bool: | ||
| record.request_id = getattr(g, "request_id", "-") if g else "-" | ||
| return True | ||
|
|
||
|
|
||
| class OTelFieldFilter(logging.Filter): | ||
| """Default OpenTelemetry id fields to ``"-"`` when not populated.""" | ||
|
|
||
| def filter(self, record: logging.LogRecord) -> bool: | ||
| for attr in ("otelTraceID", "otelSpanID"): | ||
| if not getattr(record, attr, None): | ||
| setattr(record, attr, "-") | ||
| return True | ||
|
|
||
|
|
||
| def setup_logging(log_level: int = logging.INFO) -> None: | ||
| """Configure root/werkzeug/gunicorn loggers with the standardized format.""" | ||
| dictConfig( | ||
| { | ||
| "version": 1, | ||
| "disable_existing_loggers": False, | ||
| "formatters": {"default": {"format": LOG_FORMAT}}, | ||
| "filters": { | ||
| "request_id": {"()": RequestIDFilter}, | ||
| "otel_ids": {"()": OTelFieldFilter}, | ||
| }, | ||
| "handlers": { | ||
| "wsgi": { | ||
| "class": "logging.StreamHandler", | ||
| "stream": "ext://flask.logging.wsgi_errors_stream", | ||
| "formatter": "default", | ||
| "filters": ["request_id", "otel_ids"], | ||
| }, | ||
| }, | ||
| "loggers": { | ||
| "werkzeug": { | ||
| "level": log_level, | ||
| "handlers": ["wsgi"], | ||
| "propagate": False, | ||
| }, | ||
| "gunicorn.access": { | ||
| "level": log_level, | ||
| "handlers": ["wsgi"], | ||
| "propagate": False, | ||
| }, | ||
| "gunicorn.error": { | ||
| "level": log_level, | ||
| "handlers": ["wsgi"], | ||
| "propagate": False, | ||
| }, | ||
| }, | ||
| "root": {"level": log_level, "handlers": ["wsgi"]}, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def register_request_id_middleware(app: Flask) -> None: | ||
| """Read ``X-Request-ID`` from each request (or mint one) onto Flask ``g``.""" | ||
|
|
||
| @app.before_request | ||
| def _assign_request_id() -> None: | ||
| g.request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
Repository: Zipstack/unstract
Length of output: 50373
🏁 Script executed:
Repository: Zipstack/unstract
Length of output: 305
🏁 Script executed:
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 raiseRuntimeErrorwhen logging runs outside an application/request context; usehas_request_context()before accessing it.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents