This is an automated email from the ASF dual-hosted git repository.

hussein-awala pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new da202b08f42 Honor json_logs config in Celery worker startup (#68916)
da202b08f42 is described below

commit da202b08f42391e4d3c802008f9f04b33df58515
Author: Xu Han <[email protected]>
AuthorDate: Mon Jun 29 08:53:30 2026 -0400

    Honor json_logs config in Celery worker startup (#68916)
    
    * feat(celery): honor json_logs config in Celery worker startup
    
    Add two-level config lookup for JSON logging in the Celery worker:
    - Check [celery] json_logs first (celery-specific override)
    - Fall back to [logging] json_logs (global setting, added in 3.2.0)
    - Fall back to False if neither is set
    
    This mirrors the existing CELERY_LOGGING_LEVEL / LOGGING_LEVEL fallback
    pattern in the same file.
    
    Also adds [celery] json_logs to provider.yaml config schema.
    
    Closes apache/airflow#68912
    
    * fix(celery): rename newsfragment to PR number, set version_added to 
undecided
    
    * fix(celery): drop unneeded backend marker, remove provider newsfragment
    
    * fix(celery): handle null schema default and avoid DB access in non-DB 
test mode
    
    * fix(celery): mock cli_action_loggers and restore executor_loader reload 
in TestWorkerJsonLogs
---
 providers/celery/docs/celery_executor.rst          | 36 ++++++++++++++++
 providers/celery/provider.yaml                     | 11 +++++
 .../airflow/providers/celery/cli/celery_command.py |  7 ++-
 .../airflow/providers/celery/get_provider_info.py  |  7 +++
 .../tests/unit/celery/cli/test_celery_command.py   | 50 ++++++++++++++++++++++
 5 files changed, 110 insertions(+), 1 deletion(-)

diff --git a/providers/celery/docs/celery_executor.rst 
b/providers/celery/docs/celery_executor.rst
index 6017c68c83c..c75d147c728 100644
--- a/providers/celery/docs/celery_executor.rst
+++ b/providers/celery/docs/celery_executor.rst
@@ -203,6 +203,42 @@ During this process, two processes are created:
 | [11] **WorkerProcess** saves status information in **ResultBackend**.
 | [13] When **SchedulerProcess** asks **ResultBackend** again about the 
status, it will get information about the status of the task.
 
+.. _celery_executor:logging:
+
+Worker logging
+--------------
+
+By default the Celery worker writes plain-text logs to stdout. To emit 
structured
+JSON instead, enable it via the ``[logging]`` section (applies to all Airflow
+components) or override it for the worker alone with the ``[celery]`` section:
+
+.. code-block:: ini
+
+    # Global — affects the API server, scheduler, and workers:
+    [logging]
+    json_logs = True
+
+    # Or override for the Celery worker only, leaving other components 
unchanged:
+    [celery]
+    json_logs = True
+
+The lookup order is:
+
+1. ``[celery] json_logs`` — if set, takes precedence.
+2. ``[logging] json_logs`` — used when the celery-specific key is absent.
+3. ``False`` — the default when neither key is configured.
+
+This mirrors the existing ``[logging] CELERY_LOGGING_LEVEL`` /
+``[logging] LOGGING_LEVEL`` fallback already present in the worker startup
+code.
+
+.. note::
+
+    ``[logging] json_logs`` was added in Airflow 3.2.0. On older 3.x versions 
the
+    global key is silently ignored (``fallback=False``), so setting only
+    ``[celery] json_logs = True`` is the safe way to enable JSON logs 
regardless
+    of the core version.
+
 .. _celery_executor:queue:
 
 Queues
diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml
index 29d92b74642..a1068ca7cec 100644
--- a/providers/celery/provider.yaml
+++ b/providers/celery/provider.yaml
@@ -215,6 +215,17 @@ config:
         type: boolean
         example: ~
         default: "true"
+      json_logs:
+        description: |
+          Emit Celery worker stdout in JSON format. When set, this takes 
precedence over
+          the global ``[logging] json_logs`` setting, allowing the worker to 
use a
+          different format than the rest of the deployment. When unset (the 
default),
+          the value falls back to ``[logging] json_logs``, which itself 
defaults to
+          ``False``.
+        version_added: ~
+        type: boolean
+        example: ~
+        default: ~
       broker_url:
         description: |
           The Celery broker URL. Celery supports multiple broker types. See:
diff --git 
a/providers/celery/src/airflow/providers/celery/cli/celery_command.py 
b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
index 3aebe0c2088..ed5b83c4478 100644
--- a/providers/celery/src/airflow/providers/celery/cli/celery_command.py
+++ b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
@@ -263,7 +263,12 @@ def worker(args):
     if AIRFLOW_V_3_0_PLUS:
         from airflow.sdk.log import configure_logging
 
-        configure_logging(output=sys.stdout.buffer)
+        _celery_json = conf.get("celery", "json_logs", fallback="")
+        if _celery_json and _celery_json.lower() != "none":
+            json_output = conf.getboolean("celery", "json_logs")
+        else:
+            json_output = conf.getboolean("logging", "json_logs", 
fallback=False)
+        configure_logging(output=sys.stdout.buffer, json_output=json_output)
     else:
         # Disable connection pool so that celery worker does not hold an 
unnecessary db connection
         settings.reconfigure_orm(disable_connection_pool=True)
diff --git a/providers/celery/src/airflow/providers/celery/get_provider_info.py 
b/providers/celery/src/airflow/providers/celery/get_provider_info.py
index b1a663e3495..5358cda7784 100644
--- a/providers/celery/src/airflow/providers/celery/get_provider_info.py
+++ b/providers/celery/src/airflow/providers/celery/get_provider_info.py
@@ -110,6 +110,13 @@ def get_provider_info():
                         "example": None,
                         "default": "true",
                     },
+                    "json_logs": {
+                        "description": "Emit Celery worker stdout in JSON 
format. When set, this takes precedence over\nthe global ``[logging] 
json_logs`` setting, allowing the worker to use a\ndifferent format than the 
rest of the deployment. When unset (the default),\nthe value falls back to 
``[logging] json_logs``, which itself defaults to\n``False``.\n",
+                        "version_added": None,
+                        "type": "boolean",
+                        "example": None,
+                        "default": None,
+                    },
                     "broker_url": {
                         "description": "The Celery broker URL. Celery supports 
multiple broker types. 
See:\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html#broker-overview\n";,
                         "version_added": None,
diff --git a/providers/celery/tests/unit/celery/cli/test_celery_command.py 
b/providers/celery/tests/unit/celery/cli/test_celery_command.py
index 49495e818a5..41332d545cf 100644
--- a/providers/celery/tests/unit/celery/cli/test_celery_command.py
+++ b/providers/celery/tests/unit/celery/cli/test_celery_command.py
@@ -411,6 +411,56 @@ class TestWorkerDuplicateHostnameCheck:
         mock_pools_reset.assert_called_once()
 
 
[email protected](not AIRFLOW_V_3_0_PLUS, reason="configure_logging only 
called in Airflow 3+")
[email protected]("airflow.utils.cli_action_loggers.on_pre_execution")
[email protected]("conf_stale_bundle_cleanup_disabled")
+class TestWorkerJsonLogs:
+    @classmethod
+    def setup_class(cls):
+        with conf_vars({("core", "executor"): "CeleryExecutor"}):
+            importlib.reload(executor_loader)
+            importlib.reload(cli_parser)
+            cls.parser = cli_parser.get_parser()
+
+    @mock.patch("airflow.providers.celery.cli.celery_command.Process")
+    @mock.patch("airflow.providers.celery.executors.celery_executor.app")
+    @mock.patch("airflow.sdk.log.configure_logging")
+    def test_json_logs_defaults_to_false(
+        self, mock_configure_logging, mock_celery_app, mock_popen, 
mock_pre_exec
+    ):
+        args = self.parser.parse_args(["celery", "worker"])
+        celery_command.worker(args)
+        mock_configure_logging.assert_called_once()
+        _, kwargs = mock_configure_logging.call_args
+        assert kwargs.get("json_output") is False
+
+    @mock.patch("airflow.providers.celery.cli.celery_command.Process")
+    @mock.patch("airflow.providers.celery.executors.celery_executor.app")
+    @mock.patch("airflow.sdk.log.configure_logging")
+    def test_json_logs_uses_global_logging_config(
+        self, mock_configure_logging, mock_celery_app, mock_popen, 
mock_pre_exec
+    ):
+        args = self.parser.parse_args(["celery", "worker"])
+        with conf_vars({("logging", "json_logs"): "True"}):
+            celery_command.worker(args)
+        mock_configure_logging.assert_called_once()
+        _, kwargs = mock_configure_logging.call_args
+        assert kwargs.get("json_output") is True
+
+    @mock.patch("airflow.providers.celery.cli.celery_command.Process")
+    @mock.patch("airflow.providers.celery.executors.celery_executor.app")
+    @mock.patch("airflow.sdk.log.configure_logging")
+    def test_celery_json_logs_overrides_global(
+        self, mock_configure_logging, mock_celery_app, mock_popen, 
mock_pre_exec
+    ):
+        args = self.parser.parse_args(["celery", "worker"])
+        with conf_vars({("logging", "json_logs"): "True", ("celery", 
"json_logs"): "False"}):
+            celery_command.worker(args)
+        mock_configure_logging.assert_called_once()
+        _, kwargs = mock_configure_logging.call_args
+        assert kwargs.get("json_output") is False
+
+
 @pytest.mark.backend("mysql", "postgres")
 @pytest.mark.usefixtures("conf_stale_bundle_cleanup_disabled")
 class TestFlowerCommand:

Reply via email to