Repository: langgenius/dify
Pull request #19783: feat: Add optional request and response JSON logging extension with configuration and tests
Author: login: NeatGuyCoding
Labels: name: 📚 documentation
description: Improvements or additions to documentation
name: 💪 enhancement
description: New feature or request
name: size:L
description: This PR changes 100-499 lines, ignoring generated files.
name: lgtm
description: This PR has been approved by a maintainer
Created: 2025-05-15T13:13:45Z | Merged: 2025-05-17T09:31:09Z

# Summary

This change adds debug logging for HTTP requests and responses in the application. It enhances the debugging capabilities by logging request and response details when the DEBUG log level is enabled. The feature specifically logs JSON request and response bodies in a formatted manner, making it easier to troubleshoot API interactions.

The implementation adds two Flask decorators:
1. `@app.before_request`: Logs incoming request details including method, path, and JSON body (if present)
2. `@app.after_request`: Logs outgoing response details including status, content type, and JSON body (if present)

This change improves developer experience by providing more visibility into the API traffic during development and debugging sessions.

Resolves #19781

# Screenshots

| Before | After |
|--------|-------|
| No request/response logging | Detailed request/response logs with formatted JSON bodies |

# Checklist

> [!IMPORTANT]  
> Please review the checklist below before submitting your pull request.

- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
- [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
- [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
- [x] I've updated the documentation accordingly.
- [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods

Linked issue #19781: log for request and response
### Self Checks

- [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
- [x] I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
- [x] [FOR CHINESE USERS] 请务必使用英文提交 Issue，否则会被关闭。谢谢！:)
- [x] Please do not modify this template :) and fill in all the required fields.

### 1. Is this request related to a challenge you're experiencing? Tell me about your story.

Yes, this request is related to a challenge I'm currently experiencing. I'm building an AI application using Dify, but during the debugging process, I find it difficult to track the details of API calls. When issues occur in my application, I cannot clearly see the specific content of requests and responses, which makes troubleshooting extremely challenging.

I need to be able to view complete HTTP request and response logs, including request headers, request bodies, response status codes, response bodies, and other information. These logs are crucial for understanding API call behavior, diagnosing issues, and optimizing application performance.

Currently, I have to use external tools like Postman or browser developer tools to capture this information, but this approach is inefficient and not convenient for long-term monitoring. If Dify could incorporate built-in HTTP request and response logging functionality, it would greatly improve my development efficiency and application quality.

I hope this feature can be added to make it easier for us to develop and maintain Dify-based applications.

### 2. Additional context or comments

The log is only enabled in DEBUG, and contains request method, path, body(if application/json), response status, content-type, body(if application/json)

### 3. Can you help us with this feature?

- [x] I am interested in contributing to this feature.
Linked issue comments: user: login: laipz8200
created_at: 2025-05-15T15:09:46Z
body: Maybe duplicate with #18952
author_association: MEMBER

Diff:
diff --git a/api/.env.example b/api/.env.example
index 3230e3d828afc2..9dec564c28ecec 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -348,6 +348,7 @@ SENTRY_DSN=
 
 # DEBUG
 DEBUG=false
+ENABLE_REQUEST_LOGGING=False
 SQLALCHEMY_ECHO=false
 
 # Notion import configuration, support public and internal
diff --git a/api/app_factory.py b/api/app_factory.py
index 1c886ac5c727b9..3a258be28fb839 100644
--- a/api/app_factory.py
+++ b/api/app_factory.py
@@ -54,6 +54,7 @@ def initialize_extensions(app: DifyApp):
         ext_otel,
         ext_proxy_fix,
         ext_redis,
+        ext_request_logging,
         ext_sentry,
         ext_set_secretkey,
         ext_storage,
@@ -83,6 +84,7 @@ def initialize_extensions(app: DifyApp):
         ext_blueprints,
         ext_commands,
         ext_otel,
+        ext_request_logging,
     ]
     for ext in extensions:
         short_name = ext.__name__.split(".")[-1]
diff --git a/api/configs/deploy/__init__.py b/api/configs/deploy/__init__.py
index 950936d3c65462..63f4dfba637848 100644
--- a/api/configs/deploy/__init__.py
+++ b/api/configs/deploy/__init__.py
@@ -17,6 +17,12 @@ class DeploymentConfig(BaseSettings):
         default=False,
     )
 
+    # Request logging configuration
+    ENABLE_REQUEST_LOGGING: bool = Field(
+        description="Enable request and response body logging",
+        default=False,
+    )
+
     EDITION: str = Field(
         description="Deployment edition of the application (e.g., 'SELF_HOSTED', 'CLOUD')",
         default="SELF_HOSTED",
diff --git a/api/extensions/ext_request_logging.py b/api/extensions/ext_request_logging.py
new file mode 100644
index 00000000000000..7c69483e0f6c24
--- /dev/null
+++ b/api/extensions/ext_request_logging.py
@@ -0,0 +1,73 @@
+import json
+import logging
+
+import flask
+import werkzeug.http
+from flask import Flask
+from flask.signals import request_finished, request_started
+
+from configs import dify_config
+
+_logger = logging.getLogger(__name__)
+
+
+def _is_content_type_json(content_type: str) -> bool:
+    if not content_type:
+        return False
+    content_type_no_option, _ = werkzeug.http.parse_options_header(content_type)
+    return content_type_no_option.lower() == "application/json"
+
+
+def _log_request_started(_sender, **_extra):
+    """Log the start of a request."""
+    if not _logger.isEnabledFor(logging.DEBUG):
+        return
+
+    request = flask.request
+    if not (_is_content_type_json(request.content_type) and request.data):
+        _logger.debug("Received Request %s -> %s", request.method, request.path)
+        return
+    try:
+        json_data = json.loads(request.data)
+    except (TypeError, ValueError):
+        _logger.exception("Failed to parse JSON request")
+        return
+    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+    _logger.debug(
+        "Received Request %s -> %s, Request Body:\n%s",
+        request.method,
+        request.path,
+        formatted_json,
+    )
+
+
+def _log_request_finished(_sender, response, **_extra):
+    """Log the end of a request."""
+    if not _logger.isEnabledFor(logging.DEBUG) or response is None:
+        return
+
+    if not _is_content_type_json(response.content_type):
+        _logger.debug("Response %s %s", response.status, response.content_type)
+        return
+
+    response_data = response.get_data(as_text=True)
+    try:
+        json_data = json.loads(response_data)
+    except (TypeError, ValueError):
+        _logger.exception("Failed to parse JSON response")
+        return
+    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+    _logger.debug(
+        "Response %s %s, Response Body:\n%s",
+        response.status,
+        response.content_type,
+        formatted_json,
+    )
+
+
+def init_app(app: Flask):
+    """Initialize the request logging extension."""
+    if not dify_config.ENABLE_REQUEST_LOGGING:
+        return
+    request_started.connect(_log_request_started, app)
+    request_finished.connect(_log_request_finished, app)
diff --git a/api/tests/unit_tests/extensions/test_ext_request_logging.py b/api/tests/unit_tests/extensions/test_ext_request_logging.py
new file mode 100644
index 00000000000000..4e71469bccedbf
--- /dev/null
+++ b/api/tests/unit_tests/extensions/test_ext_request_logging.py
@@ -0,0 +1,265 @@
+import json
+import logging
+from unittest import mock
+
+import pytest
+from flask import Flask, Response
+
+from configs import dify_config
+from extensions import ext_request_logging
+from extensions.ext_request_logging import _is_content_type_json, _log_request_finished, init_app
+
+
+def test_is_content_type_json():
+    """
+    Test the _is_content_type_json function.
+    """
+
+    assert _is_content_type_json("application/json") is True
+    # content type header with charset option.
+    assert _is_content_type_json("application/json; charset=utf-8") is True
+    # content type header with charset option, in uppercase.
+    assert _is_content_type_json("APPLICATION/JSON; CHARSET=UTF-8") is True
+    assert _is_content_type_json("text/html") is False
+    assert _is_content_type_json("") is False
+
+
+_KEY_NEEDLE = "needle"
+_VALUE_NEEDLE = _KEY_NEEDLE[::-1]
+_RESPONSE_NEEDLE = "response"
+
+
+def _get_test_app():
+    app = Flask(__name__)
+
+    @app.route("/", methods=["GET", "POST"])
+    def handler():
+        return _RESPONSE_NEEDLE
+
+    return app
+
+
+# NOTE(QuantumGhost): Due to the design of Flask, we need to use monkey patch to write tests.
+
+
+@pytest.fixture
+def mock_request_receiver(monkeypatch) -> mock.Mock:
+    mock_log_request_started = mock.Mock()
+    monkeypatch.setattr(ext_request_logging, "_log_request_started", mock_log_request_started)
+    return mock_log_request_started
+
+
+@pytest.fixture
+def mock_response_receiver(monkeypatch) -> mock.Mock:
+    mock_log_request_finished = mock.Mock()
+    monkeypatch.setattr(ext_request_logging, "_log_request_finished", mock_log_request_finished)
+    return mock_log_request_finished
+
+
+@pytest.fixture
+def mock_logger(monkeypatch) -> logging.Logger:
+    _logger = mock.MagicMock(spec=logging.Logger)
+    monkeypatch.setattr(ext_request_logging, "_logger", _logger)
+    return _logger
+
+
+@pytest.fixture
+def enable_request_logging(monkeypatch):
+    monkeypatch.setattr(dify_config, "ENABLE_REQUEST_LOGGING", True)
+
+
+class TestRequestLoggingExtension:
+    def test_receiver_should_not_be_invoked_if_configuration_is_disabled(
+        self,
+        monkeypatch,
+        mock_request_receiver,
+        mock_response_receiver,
+    ):
+        monkeypatch.setattr(dify_config, "ENABLE_REQUEST_LOGGING", False)
+
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.get("/")
+
+        mock_request_receiver.assert_not_called()
+        mock_response_receiver.assert_not_called()
+
+    def test_receiver_should_be_called_if_enabled(
+        self,
+        enable_request_logging,
+        mock_request_receiver,
+        mock_response_receiver,
+    ):
+        """
+        Test the request logging extension with JSON data.
+        """
+
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
+
+        mock_request_receiver.assert_called_once()
+        mock_response_receiver.assert_called_once()
+
+
+class TestLoggingLevel:
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_logging_should_be_skipped_if_level_is_above_debug(self, enable_request_logging, mock_logger):
+        mock_logger.isEnabledFor.return_value = False
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
+        mock_logger.debug.assert_not_called()
+
+
+class TestRequestReceiverLogging:
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_non_json_request(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post("/", data="plain text")
+        assert mock_logger.debug.call_count == 1
+        call_args = mock_logger.debug.call_args[0]
+        assert "Received Request" in call_args[0]
+        assert call_args[1] == "POST"
+        assert call_args[2] == "/"
+        assert "Request Body" not in call_args[0]
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_json_request(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
+        assert mock_logger.debug.call_count == 1
+        call_args = mock_logger.debug.call_args[0]
+        assert "Received Request" in call_args[0]
+        assert "Request Body" in call_args[0]
+        assert call_args[1] == "POST"
+        assert call_args[2] == "/"
+        assert _KEY_NEEDLE in call_args[3]
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_json_request_with_empty_body(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post("/", headers={"Content-Type": "application/json"})
+
+        assert mock_logger.debug.call_count == 1
+        call_args = mock_logger.debug.call_args[0]
+        assert "Received Request" in call_args[0]
+        assert "Request Body" not in call_args[0]
+        assert call_args[1] == "POST"
+        assert call_args[2] == "/"
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_json_request_with_invalid_json_as_body(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            client.post(
+                "/",
+                headers={"Content-Type": "application/json"},
+                data="{",
+            )
+        assert mock_logger.debug.call_count == 0
+        assert mock_logger.exception.call_count == 1
+
+        exception_call_args = mock_logger.exception.call_args[0]
+        assert exception_call_args[0] == "Failed to parse JSON request"
+
+
+class TestResponseReceiverLogging:
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_non_json_response(self, enable_request_logging, mock_logger):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        response = Response(
+            "OK",
+            headers={"Content-Type": "text/plain"},
+        )
+        _log_request_finished(app, response)
+        assert mock_logger.debug.call_count == 1
+        call_args = mock_logger.debug.call_args[0]
+        assert "Response" in call_args[0]
+        assert "200" in call_args[1]
+        assert call_args[2] == "text/plain"
+        assert "Response Body" not in call_args[0]
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_json_response(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+        response = Response(
+            json.dumps({_KEY_NEEDLE: _VALUE_NEEDLE}),
+            headers={"Content-Type": "application/json"},
+        )
+        _log_request_finished(app, response)
+        assert mock_logger.debug.call_count == 1
+        call_args = mock_logger.debug.call_args[0]
+        assert "Response" in call_args[0]
+        assert "Response Body" in call_args[0]
+        assert "200" in call_args[1]
+        assert call_args[2] == "application/json"
+        assert _KEY_NEEDLE in call_args[3]
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_json_request_with_invalid_json_as_body(self, enable_request_logging, mock_logger, mock_response_receiver):
+        mock_logger.isEnabledFor.return_value = True
+        app = _get_test_app()
+
+        response = Response(
+            "{",
+            headers={"Content-Type": "application/json"},
+        )
+        _log_request_finished(app, response)
+        assert mock_logger.debug.call_count == 0
+        assert mock_logger.exception.call_count == 1
+
+        exception_call_args = mock_logger.exception.call_args[0]
+        assert exception_call_args[0] == "Failed to parse JSON response"
+
+
+class TestResponseUnmodified:
+    def test_when_request_logging_disabled(self):
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            response = client.post(
+                "/",
+                headers={"Content-Type": "application/json"},
+                data="{",
+            )
+        assert response.text == _RESPONSE_NEEDLE
+        assert response.status_code == 200
+
+    @pytest.mark.usefixtures("enable_request_logging")
+    def test_when_request_logging_enabled(self, enable_request_logging):
+        app = _get_test_app()
+        init_app(app)
+
+        with app.test_client() as client:
+            response = client.post(
+                "/",
+                headers={"Content-Type": "application/json"},
+                data="{",
+            )
+        assert response.text == _RESPONSE_NEEDLE
+        assert response.status_code == 200
diff --git a/docker/.env.example b/docker/.env.example
index 37738a5065d443..a7c033018daaf4 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -74,6 +74,10 @@ DEBUG=false
 # which is convenient for debugging.
 FLASK_DEBUG=false
 
+# Enable request logging, which will log the request and response information.
+# And the log level is DEBUG
+ENABLE_REQUEST_LOGGING=False
+
 # A secret key that is used for securely signing the session cookie
 # and encrypting sensitive information on the database.
 # You can generate a strong key using `openssl rand -base64 42`.
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index bfdfa701e31376..0aca3b749ef8c7 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -19,6 +19,7 @@ x-shared-env: &shared-api-worker-env
   LOG_TZ: ${LOG_TZ:-UTC}
   DEBUG: ${DEBUG:-false}
   FLASK_DEBUG: ${FLASK_DEBUG:-false}
+  ENABLE_REQUEST_LOGGING: ${ENABLE_REQUEST_LOGGING:-False}
   SECRET_KEY: ${SECRET_KEY:-sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U}
   INIT_PASSWORD: ${INIT_PASSWORD:-}
   DEPLOY_ENV: ${DEPLOY_ENV:-PRODUCTION}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds debug logging for incoming HTTP requests and outgoing responses when the DEBUG logging level is active.  
- Adds a before-request decorator to log incoming request details, including JSON formatting when applicable.  
- Adds an after-request decorator to log outgoing response details, including formatted JSON bodies when possible.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:56:52Z
user: login: QuantumGhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:01:02Z
user: login: QuantumGhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:06:15Z
user: login: QuantumGhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:06:45Z
user: login: QuantumGhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:07:06Z
user: login: QuantumGhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:07:21Z
user: login: QuantumGhost
body: LGTM!

Thank you once again for your contribution—it’s greatly appreciated! 😊
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T09:30:56Z

Review comments:
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Received Request {flask.request.method} -> {flask.request.path}, "
+                        f"Request Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON request")
+            else:
+                logging.debug(f"Received Request {flask.request.method} -> {flask.request.path}")
+
+    # for debugging purposes, log the response
+    @app.after_request
+    def log_response(response):
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
path: api/extensions/ext_logging.py
user: login: Copilot
body: The 'log_response' function only returns the response when DEBUG logging is enabled. To ensure the response is always returned, add an unconditional return statement or an else clause that returns the response when DEBUG logging is disabled.
created_at: 2025-05-15T14:56:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Received Request {flask.request.method} -> {flask.request.path}, "
+                        f"Request Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON request")
+            else:
+                logging.debug(f"Received Request {flask.request.method} -> {flask.request.path}")
+
+    # for debugging purposes, log the response
+    @app.after_request
+    def log_response(response):
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
path: api/extensions/ext_logging.py
user: login: QuantumGhost
body: Bypassing the entire logging logic when the relevant logger is disabled is a great approach—well done!

That said, I’d still suggest introducing a dedicated configuration flag specifically for this feature. This would help avoid scenarios where users of Dify set the logging level to `DEBUG` for debugging purposes and unintentionally activate this feature.

What do you think? cc @laipz8200
created_at: 2025-05-15T15:00:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
path: api/extensions/ext_logging.py
user: login: QuantumGhost
body: I suggest updating this to the following code:

```py
logging.debug(
    "Received Request %s -> %s, Request Body:\n%s",
    flask.request.method, flask.request.path, formatted_json,
)
```

Using `%` formatting in this context is more efficient than utilizing f-strings. With f-strings, the interpolation occurs immediately when `logging.debug` is called, regardless of whether the message is ultimately logged. In contrast, the suggested approach defers interpolation until the message is actually logged, avoiding unnecessary performance overhead.

For more details, you can refer to this discussion: [StackOverflow](https://stackoverflow.com/questions/11955787/idiomatic-python-logging-format-string-args-list-vs-inline-string-formatting#comment74197244_11955929).

created_at: 2025-05-15T16:06:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Received Request {flask.request.method} -> {flask.request.path}, "
+                        f"Request Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON request")
+            else:
+                logging.debug(f"Received Request {flask.request.method} -> {flask.request.path}")
+
+    # for debugging purposes, log the response
+    @app.after_request
+    def log_response(response):
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            # only log body if the response is JSON
+            if response.content_type and "application/json" in response.content_type.lower():
+                try:
+                    response_data = response.get_data(as_text=True)
+                    json_data = json.loads(response_data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Response {response.status} {response.content_type}, Response Body:\n{formatted_json}"
path: api/extensions/ext_logging.py
user: login: QuantumGhost
body: ditto
created_at: 2025-05-15T16:06:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Received Request {flask.request.method} -> {flask.request.path}, "
+                        f"Request Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON request")
+            else:
+                logging.debug(f"Received Request {flask.request.method} -> {flask.request.path}")
+
+    # for debugging purposes, log the response
+    @app.after_request
+    def log_response(response):
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            # only log body if the response is JSON
+            if response.content_type and "application/json" in response.content_type.lower():
+                try:
+                    response_data = response.get_data(as_text=True)
+                    json_data = json.loads(response_data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Response {response.status} {response.content_type}, Response Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON response")
+            else:
+                logging.debug(f"Response {response.status} {response.content_type}")
path: api/extensions/ext_logging.py
user: login: QuantumGhost
body: ditto
created_at: 2025-05-15T16:07:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,46 @@ def time_converter(seconds):
             if handler.formatter:
                 handler.formatter.converter = time_converter
 
+    # for debugging purposes, log the request context
+    @app.before_request
+    def log_request():
+        # only log if DEBUG is enabled
+        if logging.getLogger().isEnabledFor(logging.DEBUG):
+            content_type = flask.request.content_type
+            # only log body if the request is JSON
+            if flask.request.data and "application/json" in content_type.lower():
+                try:
+                    json_data = json.loads(flask.request.data)
+                    formatted_json = json.dumps(json_data, ensure_ascii=False, indent=2)
+                    logging.debug(
+                        f"Received Request {flask.request.method} -> {flask.request.path}, "
+                        f"Request Body:\n{formatted_json}"
+                    )
+                except Exception:
+                    logging.exception("Failed to parse JSON request")
+            else:
+                logging.debug(f"Received Request {flask.request.method} -> {flask.request.path}")
path: api/extensions/ext_logging.py
user: login: QuantumGhost
body: ditto
created_at: 2025-05-15T16:07:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: crazywoola
created_at: 2025-05-15T13:31:05Z
body: > # Summary
> This change adds debug logging for HTTP requests and responses in the application. It enhances the debugging capabilities by logging request and response details when the DEBUG log level is enabled. The feature specifically logs JSON request and response bodies in a formatted manner, making it easier to troubleshoot API interactions.
> 
> The implementation adds two Flask decorators:
> 
> 1. `@app.before_request`: Logs incoming request details including method, path, and JSON body (if present)
> 2. `@app.after_request`: Logs outgoing response details including status, content type, and JSON body (if present)
> 
> This change improves developer experience by providing more visibility into the API traffic during development and debugging sessions.
> 
> Resolves #19780
> 
> # Screenshots
> Before	After
> No request/response logging	Detailed request/response logs with formatted JSON bodies
> # Checklist
> Important
> 
> Please review the checklist below before submitting your pull request.
> 
> * [ ]  This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
> * [x]  I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
> * [x]  I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
> * [x]  I've updated the documentation accordingly.
> * [x]  I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods


author_association: MEMBER
user: login: QuantumGhost
created_at: 2025-05-15T14:56:10Z
body: Hi @HashJang,

Thank you for your contribution!

Just a quick note—there’s already some prior work related to request and response logging in #18952, where I provided a review with some suggestions regarding the functionality and implementation of this feature. Could you please take a look at [this comment](https://github.com/langgenius/dify/pull/18952#issuecomment-2865980298) and consider incorporating the adjustments outlined there? 

Looking forward to your updates!
author_association: CONTRIBUTOR
user: login: QuantumGhost
created_at: 2025-05-16T03:24:01Z
body: Hi @HashJang,

Thank you once again for your contribution!

Could you please take a moment to review the suggestions I shared [here](https://github.com/langgenius/dify/pull/18952#issuecomment-2865980298)? They are also relevant to this PR.

This feature has been requested by several members of the community, so I'd really like to get it merged. However, the issues mentioned in the linked comment still need to be addressed before moving forward.

If you’re currently short on time to make the necessary adjustments, I'd be happy to pick up where you left off and help get this PR merged. Just let me know what works best for you!

Looking forward to hearing your thoughts.


author_association: CONTRIBUTOR
user: login: NeatGuyCoding
created_at: 2025-05-16T05:50:59Z
body: Hi @QuantumGhost,

Thanks for the suggestion! I’ve made the modifications—please take a look.
author_association: CONTRIBUTOR
user: login: QuantumGhost
created_at: 2025-05-16T09:24:38Z
body: Hi @HashJang,

I've refactored the code and added some tests for this feature. However, it seems that the "Allow edits by maintainers" option isn't enabled on your PR, so I'm unable to push changes directly to your branch.  

To work around this, I’ve submitted [a separate PR targeting your branch](https://github.com/HashJang/dify/pull/1). Could you please review it and, if everything looks good, merge it into your PR branch?  

Let me know if you run into any issues or have any questions!  
author_association: CONTRIBUTOR
user: login: laipz8200
created_at: 2025-05-16T13:46:48Z
body: After adding the configuration, you'll need to add it to both api/.env.example and docker/.env.example. Then, use the script in the docker/ directory to update the docker-compose.yml file.
author_association: MEMBER
user: login: NeatGuyCoding
created_at: 2025-05-16T22:50:22Z
body: Merged and Fixed with many thanks
@QuantumGhost  @laipz8200 
author_association: CONTRIBUTOR