Repository: apache/airflow

Issue:
serve_logs.py should respect the logging config's task handler's base_log_folder value
### Apache Airflow version

2.2.5

### What happened

in a worker container, a flask app is spun up to serve task log files that is read by the webserver and rendered to the user in the UI.  The log files cannot be read if you overwrite the task handler's base_log_folder value. (404 error)
 ie. in the airflow.cfg, the base_log_folder = `foo/bar/logs`, and the task handler uses `{base_log_folder}/dags`



### What you think should happen instead

in https://github.com/apache/airflow/blob/main/airflow/utils/serve_logs.py#L33, it should read the logging config's task handler log location.

### How to reproduce

use a custom logging config, override the task's base log folder. 
Run a dag and try to view logs in the ui, you will get a 404

```
LOGGING_CONFIG["handlers"].update(
    {
        "task": {
            "class":  "airflow.utils.log.file_task_handler.FileTaskHandler",
            "formatter": "airflow",
            "base_log_folder": f"{BASE_LOG_FOLDER}/dags",
            "filename_template": FILENAME_TEMPLATE,
            "filters": ["mask_secrets"],
        },
  }
```

### Operating System

ubuntu

### Versions of Apache Airflow Providers

_No response_

### Deployment

Virtualenv installation

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template!

Feel free to provide PR for that.
This issue has been automatically marked as stale because it has been open for 365 days without any activity. There has been several Airflow releases since last activity on this issue. Kindly asking to recheck the report against latest Airflow version and let us know if the issue is reproducible. The issue will be closed in next 30 days if no further activity occurs from the issue author.
This issue has been closed because it has not received response from the issue author.
I can confirm that this issue still exists in the master branch. 

My steps to reproduce in breeze: 
1. execute `breeze start-airflow --executor CeleryExecutor --backend postgres --load-example-dags --db-reset`
2. go to `$AIRFLOW_HOME`, create a directory called `config`, and create an empty `__init__.py` and `log_config.py` file inside
3. Inside `airflow/config_templates/airflow_local_settings.py`, modify `DEFAULT_LOGGING_CONFIG[handlers][task][base_log_folder]` to some other different locations
4. copy all the contents inside `airflow/config_templates/airflow_local_settings.py` into `log_config.py`
5. In `/root/airflow/airflow.cfg` , under `logging` section,  modify `logging_config_class` to `log_config.DEFAULT_LOGGING_CONFIG`
6. Restart everything
7. Run a DAG in webUI, check the log, and you will see 404 not found

The underlying reason is that the scheduler will ingest the `log_config.DEFAULT_LOGGING_CONFIG`, and outputs the log file to the location as specified by `base_log_folder` under `DEFAULT_LOGGING_CONFIG[handlers][task]`

But the flask app still attempts to serve logs from `base_log_folder` defined under the logging section (which is `/root/airflow/logs`. Hence when flask attempts to read that folder, it returned 404.

I will provide a PR to fix it soon. 


Resolving patch:
diff --git a/airflow/utils/serve_logs.py b/airflow/utils/serve_logs.py
--- a/airflow/utils/serve_logs.py
+++ b/airflow/utils/serve_logs.py
@@ -36,6 +36,7 @@
 from airflow.configuration import conf
 from airflow.utils.docs import get_docs_url
 from airflow.utils.jwt_signer import JWTSigner
+from airflow.utils.module_loading import import_string
 
 logger = logging.getLogger(__name__)
 
@@ -44,7 +45,29 @@ def create_app():
     flask_app = Flask(__name__, static_folder=None)
     expiration_time_in_seconds = conf.getint("webserver", "log_request_clock_grace", fallback=30)
     log_directory = os.path.expanduser(conf.get("logging", "BASE_LOG_FOLDER"))
-
+    log_config_class = conf.get("logging", "logging_config_class")
+    if log_config_class:
+        logger.info("Detected user-defined logging config. Attempting to load %s", log_config_class)
+        try:
+            logging_config = import_string(log_config_class)
+            try:
+                base_log_folder = logging_config["handlers"]["task"]["base_log_folder"]
+            except KeyError:
+                base_log_folder = None
+            if base_log_folder is not None:
+                log_directory = base_log_folder
+                logger.info(
+                    "Successfully imported user-defined logging config. Flask App will serve log from %s",
+                    log_directory,
+                )
+            else:
+                logger.warning(
+                    "User-defined logging config does not specify 'base_log_folder'. "
+                    "Flask App will use default log directory %s",
+                    base_log_folder,
+                )
+        except Exception as e:
+            raise ImportError(f"Unable to load {log_config_class} due to error: {e}")
     signer = JWTSigner(
         secret_key=conf.get("webserver", "secret_key"),
         expiration_time_in_seconds=expiration_time_in_seconds,


Test patch:
diff --git a/tests/utils/test_serve_logs.py b/tests/utils/test_serve_logs.py
--- a/tests/utils/test_serve_logs.py
+++ b/tests/utils/test_serve_logs.py
@@ -17,12 +17,14 @@
 from __future__ import annotations
 
 import datetime
+from pathlib import Path
 from typing import TYPE_CHECKING
 
 import jwt
 import pytest
 import time_machine
 
+from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
 from airflow.utils.jwt_signer import JWTSigner
 from airflow.utils.serve_logs import create_app
 from tests.test_utils.config import conf_vars
@@ -34,18 +36,46 @@
 
 
 @pytest.fixture
-def client(tmpdir):
-    with conf_vars({("logging", "base_log_folder"): str(tmpdir)}):
+def client_without_config(tmp_path):
+    with conf_vars({("logging", "base_log_folder"): tmp_path.as_posix()}):
         app = create_app()
 
         yield app.test_client()
 
 
 @pytest.fixture
-def sample_log(tmpdir):
-    f = tmpdir / "sample.log"
-    f.write(LOG_DATA.encode())
+def client_with_config():
+    with conf_vars(
+        {
+            (
+                "logging",
+                "logging_config_class",
+            ): "airflow.config_templates.airflow_local_settings.DEFAULT_LOGGING_CONFIG"
+        }
+    ):
+        app = create_app()
+
+        yield app.test_client()
 
+
+@pytest.fixture(params=["client_without_config", "client_with_config"])
+def client(request):
+    return request.getfixturevalue(request.param)
+
+
+@pytest.fixture
+def sample_log(request, tmp_path):
+    client = request.getfixturevalue("client")
+
+    if client == request.getfixturevalue("client_without_config"):
+        base_log_dir = tmp_path
+    elif client == request.getfixturevalue("client_with_config"):
+        base_log_dir = Path(DEFAULT_LOGGING_CONFIG["handlers"]["task"]["base_log_folder"])
+    else:
+        raise ValueError(f"Unknown client fixture: {client}")
+
+    f = base_log_dir.joinpath("sample.log")
+    f.write_bytes(LOG_DATA.encode())
     return f