diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 8b690b417e1ef..a3dbd5608e548 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -617,6 +617,8 @@ def log_url(self) -> str: """Log URL for TaskInstance.""" run_id = quote(self.run_id) base_url = conf.get("api", "base_url", fallback="http://localhost:8080/") + if not base_url.endswith('/'): + base_url += '/' map_index = f"/mapped/{self.map_index}" if self.map_index >= 0 else "" try_number = f"?try_number={self.try_number}" if self.try_number > 0 else "" _log_uri = f"{base_url}dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" diff --git a/airflow-core/tests/unit/models/test_taskinstance.py b/airflow-core/tests/unit/models/test_taskinstance.py index f5bf36bcf4e0e..e93943ef01f09 100644 --- a/airflow-core/tests/unit/models/test_taskinstance.py +++ b/airflow-core/tests/unit/models/test_taskinstance.py @@ -89,6 +89,7 @@ from tests_common.test_utils import db from tests_common.test_utils.db import clear_db_connections, clear_db_runs from tests_common.test_utils.mock_operators import MockOperator +from tests_common.test_utils.config import conf_vars from unit.models import DEFAULT_DATE pytestmark = [pytest.mark.db_test] @@ -1558,6 +1559,11 @@ def test_log_url(self, create_task_instance): expected_url = "http://localhost:8080/dags/my_dag/runs/test/tasks/op" assert ti.log_url == expected_url + # Test with base_url that doesn't end with a slash + with conf_vars({('api', 'base_url'): 'http://example.com'}): + expected_url = "http://example.com/dags/my_dag/runs/test/tasks/op" + assert ti.log_url == expected_url + ti.map_index = 1 ti.try_number = 2 session = settings.Session()