This is an automated email from the ASF dual-hosted git repository.
potiuk 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 da77f374684 feat(serialization): implement truncation logic for
rendered values (#61878)
da77f374684 is described below
commit da77f37468483e0616446f80698455aa6017f84d
Author: Richard Wu <[email protected]>
AuthorDate: Fri Mar 13 06:49:21 2026 -0700
feat(serialization): implement truncation logic for rendered values (#61878)
* feat(serialization): implement truncation logic for rendered values in
template fields
Added a new function to truncate rendered values based on a specified
maximum length, ensuring that truncation messages are prioritized. This
functionality is integrated into the serialization of template fields,
enhancing the handling of long strings in the system.
* fix(test): update assertion for rendered fields in task runner tests
Modified the test for runtime task instances to dynamically retrieve the
rendered fields from the mock supervisor communications, ensuring accurate
assertions for the SetRenderedFields message type. This change enhances the
robustness of the test by adapting to varying truncation formats based on
configuration.
* refactor(tests): update large string and object assertions to use
serialization
Replaced direct truncation messages in test assertions with calls to the
new serialize_template_field function. This change ensures consistency in how
large strings and objects are handled in the rendered task instance fields
tests, leveraging the updated serialization logic for better clarity and
maintainability.
* refactor: simplify _truncate_rendered_value function and remove redundant
safe wrapper
* fix: correct expected output for quoted string in test case
* refactor: move _truncate_rendered_value function to utils.helpers and
clean up code
* refactor: improve _truncate_rendered_value logic for better readability
and efficiency
* fix: adjust available space calculation in _truncate_rendered_value for
accurate truncation
* fix: adjust available space calculation in _truncate_rendered_value for
accurate truncation
* Clean up comments
Removed comments about returning empty string and truncation message.
* refactor: rename _truncate_rendered_value to truncate_rendered_value and
update references
* feat: add shared template rendering module with truncate_rendered_value
function
* refactor: cleanup
* feat: enhance truncation functionality with configurable constants and
improved tests
* remove dups
* test: update truncation tests with dynamic boundary calculations
---
airflow-core/pyproject.toml | 1 +
.../src/airflow/_shared/template_rendering | 1 +
airflow-core/src/airflow/serialization/helpers.py | 11 +--
.../tests/unit/models/test_renderedtifields.py | 8 +-
.../tests/unit/serialization/test_helpers.py | 31 ++++++
pyproject.toml | 3 +
shared/template_rendering/pyproject.toml | 46 +++++++++
.../airflow_shared/template_rendering/__init__.py | 86 ++++++++++++++++
.../tests/template_rendering/__init__.py | 16 +++
.../test_truncate_rendered_value.py | 110 +++++++++++++++++++++
task-sdk/pyproject.toml | 1 +
.../src/airflow/sdk/_shared/template_rendering | 1 +
.../src/airflow/sdk/execution_time/task_runner.py | 11 +--
.../task_sdk/execution_time/test_task_runner.py | 28 +++---
14 files changed, 324 insertions(+), 30 deletions(-)
diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml
index 97ab77007a9..2c6c838b1d2 100644
--- a/airflow-core/pyproject.toml
+++ b/airflow-core/pyproject.toml
@@ -253,6 +253,7 @@ exclude = [
"../shared/listeners/src/airflow_shared/listeners" =
"src/airflow/_shared/listeners"
"../shared/plugins_manager/src/airflow_shared/plugins_manager" =
"src/airflow/_shared/plugins_manager"
"../shared/providers_discovery/src/airflow_shared/providers_discovery" =
"src/airflow/_shared/providers_discovery"
+"../shared/template_rendering/src/airflow_shared/template_rendering" =
"src/airflow/_shared/template_rendering"
[tool.hatch.build.targets.custom]
path = "./hatch_build.py"
diff --git a/airflow-core/src/airflow/_shared/template_rendering
b/airflow-core/src/airflow/_shared/template_rendering
new file mode 120000
index 00000000000..6ff1f831df7
--- /dev/null
+++ b/airflow-core/src/airflow/_shared/template_rendering
@@ -0,0 +1 @@
+../../../../shared/template_rendering/src/airflow_shared/template_rendering
\ No newline at end of file
diff --git a/airflow-core/src/airflow/serialization/helpers.py
b/airflow-core/src/airflow/serialization/helpers.py
index effca4453dc..e2c8069a116 100644
--- a/airflow-core/src/airflow/serialization/helpers.py
+++ b/airflow-core/src/airflow/serialization/helpers.py
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Any
from airflow._shared.module_loading import qualname
from airflow._shared.secrets_masker import redact
+from airflow._shared.template_rendering import truncate_rendered_value
from airflow.configuration import conf
from airflow.settings import json
@@ -83,10 +84,7 @@ def serialize_template_field(template_field: Any, name: str)
-> str | dict | lis
serialized = str(template_field)
if len(serialized) > max_length:
rendered = redact(serialized, name)
- return (
- "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
- f"{rendered[: max_length - 79]!r}... "
- )
+ return truncate_rendered_value(str(rendered), max_length)
return serialized
if not template_field and not isinstance(template_field, tuple):
# Avoid unnecessary serialization steps for empty fields unless they
are tuples
@@ -100,10 +98,7 @@ def serialize_template_field(template_field: Any, name:
str) -> str | dict | lis
serialized = str(template_field)
if len(serialized) > max_length:
rendered = redact(serialized, name)
- return (
- "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
- f"{rendered[: max_length - 79]!r}... "
- )
+ return truncate_rendered_value(str(rendered), max_length)
return template_field
diff --git a/airflow-core/tests/unit/models/test_renderedtifields.py
b/airflow-core/tests/unit/models/test_renderedtifields.py
index 12eaf27a38d..d42ed06b033 100644
--- a/airflow-core/tests/unit/models/test_renderedtifields.py
+++ b/airflow-core/tests/unit/models/test_renderedtifields.py
@@ -30,6 +30,7 @@ import pytest
from sqlalchemy import select
from airflow import settings
+from airflow._shared.template_rendering import truncate_rendered_value
from airflow._shared.timezones.timezone import datetime
from airflow.configuration import conf
from airflow.models import DagRun
@@ -124,13 +125,14 @@ class TestRenderedTaskInstanceFields:
pytest.param(datetime(2018, 12, 6, 10, 55), "2018-12-06
10:55:00+00:00", id="datetime"),
pytest.param(
"a" * 5000,
- f"Truncated. You can change this behaviour in
[core]max_templated_field_length. {('a' * 5000)[: max_length - 79]!r}... ",
+ truncate_rendered_value("a" * 5000, conf.getint("core",
"max_templated_field_length")),
id="large_string",
),
pytest.param(
LargeStrObject(),
- f"Truncated. You can change this behaviour in "
- f"[core]max_templated_field_length. {str(LargeStrObject())[:
max_length - 79]!r}... ",
+ truncate_rendered_value(
+ str(LargeStrObject()), conf.getint("core",
"max_templated_field_length")
+ ),
id="large_object",
),
],
diff --git a/airflow-core/tests/unit/serialization/test_helpers.py
b/airflow-core/tests/unit/serialization/test_helpers.py
new file mode 100644
index 00000000000..0ded4f64a86
--- /dev/null
+++ b/airflow-core/tests/unit/serialization/test_helpers.py
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+
+def test_serialize_template_field_with_very_small_max_length(monkeypatch):
+ """Test that truncation message is prioritized even for very small
max_length."""
+ monkeypatch.setenv("AIRFLOW__CORE__MAX_TEMPLATED_FIELD_LENGTH", "1")
+
+ from airflow.serialization.helpers import serialize_template_field
+
+ result = serialize_template_field("This is a long string", "test")
+
+ # The truncation message should be shown even if it exceeds max_length
+ # This ensures users always see why content is truncated
+ assert result
+ assert "Truncated. You can change this behaviour" in result
diff --git a/pyproject.toml b/pyproject.toml
index c359eda1a9e..4b28ceb88bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1306,6 +1306,7 @@ dev = [
"apache-airflow-shared-providers-discovery",
"apache-airflow-shared-secrets-backend",
"apache-airflow-shared-secrets-masker",
+ "apache-airflow-shared-template-rendering",
"apache-airflow-shared-timezones",
]
@@ -1365,6 +1366,7 @@ apache-airflow-shared-plugins-manager = { workspace =
true }
apache-airflow-shared-providers-discovery = { workspace = true }
apache-airflow-shared-secrets-backend = { workspace = true }
apache-airflow-shared-secrets-masker = { workspace = true }
+apache-airflow-shared-template-rendering = { workspace = true }
apache-airflow-shared-timezones = { workspace = true }
# Automatically generated provider workspace items
(update_airflow_pyproject_toml.py)
apache-airflow-providers-airbyte = { workspace = true }
@@ -1495,6 +1497,7 @@ members = [
"shared/providers_discovery",
"shared/secrets_backend",
"shared/secrets_masker",
+ "shared/template_rendering",
"shared/timezones",
# Automatically generated provider workspace members
(update_airflow_pyproject_toml.py)
"providers/airbyte",
diff --git a/shared/template_rendering/pyproject.toml
b/shared/template_rendering/pyproject.toml
new file mode 100644
index 00000000000..0b7ad0931b3
--- /dev/null
+++ b/shared/template_rendering/pyproject.toml
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[project]
+name = "apache-airflow-shared-template-rendering"
+description = "Shared template rendering utilities for Airflow distributions"
+version = "0.0"
+classifiers = [
+ "Private :: Do Not Upload",
+]
+
+dependencies = []
+
+[dependency-groups]
+dev = [
+ "apache-airflow-devel-common",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/airflow_shared"]
+
+[tool.ruff]
+extend = "../../pyproject.toml"
+src = ["src"]
+
+[tool.ruff.lint.per-file-ignores]
+# Ignore Doc rules et al for anything outside of tests
+"!src/*" = ["D", "S101", "TRY002"]
diff --git
a/shared/template_rendering/src/airflow_shared/template_rendering/__init__.py
b/shared/template_rendering/src/airflow_shared/template_rendering/__init__.py
new file mode 100644
index 00000000000..ddf01a88098
--- /dev/null
+++
b/shared/template_rendering/src/airflow_shared/template_rendering/__init__.py
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import logging
+
+log = logging.getLogger(__name__)
+
+# Public truncation configuration used by ``truncate_rendered_value``.
+# Exposed as module-level constants to avoid duplicating literals in
callers/tests.
+TRUNCATE_MIN_CONTENT_LENGTH = 7
+TRUNCATE_PREFIX = "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
+TRUNCATE_SUFFIX = "..."
+
+
+def truncate_rendered_value(rendered: str, max_length: int) -> str:
+ """
+ Truncate a rendered template value to approximately ``max_length``
characters.
+
+ Behavior:
+
+ * If ``max_length <= 0``, an empty string is returned.
+ * A fixed prefix (``TRUNCATE_PREFIX``) and suffix (``TRUNCATE_SUFFIX``)
are always
+ included when truncation occurs. The minimal truncation-only message is::
+
+ f"{TRUNCATE_PREFIX}{TRUNCATE_SUFFIX}"
+
+ * If ``max_length`` is smaller than the length of this truncation-only
message, that
+ message is returned in full, even though its length may exceed
``max_length``.
+ * Otherwise, space remaining after the prefix and suffix is allocated to
the original
+ ``rendered`` content. Content is only appended if at least
+ ``TRUNCATE_MIN_CONTENT_LENGTH`` characters are available; if fewer are
available,
+ the truncation-only message is returned instead.
+
+ Note: this function is best-effort — the return value is intended to be no
longer than
+ ``max_length``, but when ``max_length < len(TRUNCATE_PREFIX +
TRUNCATE_SUFFIX)`` it
+ intentionally returns a longer string to preserve the full truncation
message.
+ """
+ if max_length <= 0:
+ return ""
+
+ trunc_only = f"{TRUNCATE_PREFIX}{TRUNCATE_SUFFIX}"
+
+ if max_length < len(trunc_only):
+ return trunc_only
+
+ # Compute available space for content
+ overhead = len(TRUNCATE_PREFIX) + len(TRUNCATE_SUFFIX)
+ available = max_length - overhead
+
+ if available < TRUNCATE_MIN_CONTENT_LENGTH:
+ return trunc_only
+
+ # Slice content to fit and construct final string
+ content = rendered[:available].rstrip()
+ result = f"{TRUNCATE_PREFIX}{content}{TRUNCATE_SUFFIX}"
+
+ if len(result) > max_length:
+ log.warning(
+ "Truncated value still exceeds max_length=%d; this should not
happen.",
+ max_length,
+ )
+
+ return result
+
+
+__all__ = [
+ "TRUNCATE_MIN_CONTENT_LENGTH",
+ "TRUNCATE_PREFIX",
+ "TRUNCATE_SUFFIX",
+ "truncate_rendered_value",
+]
diff --git a/shared/template_rendering/tests/template_rendering/__init__.py
b/shared/template_rendering/tests/template_rendering/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/shared/template_rendering/tests/template_rendering/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git
a/shared/template_rendering/tests/template_rendering/test_truncate_rendered_value.py
b/shared/template_rendering/tests/template_rendering/test_truncate_rendered_value.py
new file mode 100644
index 00000000000..3b401aa177b
--- /dev/null
+++
b/shared/template_rendering/tests/template_rendering/test_truncate_rendered_value.py
@@ -0,0 +1,110 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from airflow_shared.template_rendering import (
+ TRUNCATE_MIN_CONTENT_LENGTH,
+ TRUNCATE_PREFIX,
+ TRUNCATE_SUFFIX,
+ truncate_rendered_value,
+)
+
+
+def test_truncate_rendered_value_prioritizes_message():
+ """Test that truncation message is always shown first, content only if
space allows."""
+ trunc_only = f"{TRUNCATE_PREFIX}{TRUNCATE_SUFFIX}"
+ trunc_only_len = len(trunc_only)
+ overhead = len(TRUNCATE_PREFIX) + len(TRUNCATE_SUFFIX)
+ min_length_for_content = overhead + TRUNCATE_MIN_CONTENT_LENGTH
+
+ test_cases = [
+ (1, "test", "Minimum value"),
+ (3, "test", "At ellipsis length"),
+ (5, "test", "Very small"),
+ (10, "password123", "Small"),
+ (20, "secret_value", "Small with content"),
+ (50, "This is a test string", "Medium"),
+ (overhead + 1, "Hello World", "At prefix+suffix boundary v1"),
+ (overhead + 2, "Hello World", "Just above boundary v1"),
+ (min_length_for_content - 3, "Hello World", "At overhead boundary v2"),
+ (90, "short", "Normal case - short string"),
+ (100, "This is a longer string", "Normal case"),
+ (
+ 150,
+ "x" * 200,
+ "Large max_length, long string",
+ ),
+ (100, "None", "String 'None'"),
+ (100, "True", "String 'True'"),
+ (100, "{'key': 'value'}", "Dict-like string"),
+ (100, "test's", "String with apostrophe"),
+ (90, '"quoted"', "String with quotes"),
+ ]
+
+ for max_length, rendered, description in test_cases:
+ result = truncate_rendered_value(rendered, max_length)
+
+ assert result.startswith(TRUNCATE_PREFIX), (
+ f"Failed for {description}: result should start with prefix"
+ )
+
+ if max_length < trunc_only_len or max_length < min_length_for_content:
+ assert result == trunc_only, (
+ f"Failed for {description}: max_length={max_length}, expected
message only, got: {result}"
+ )
+ else:
+ assert result.endswith(TRUNCATE_SUFFIX), (
+ f"Failed for {description}: result should end with suffix"
+ )
+ assert len(result) <= max_length, (
+ f"Failed for {description}: result length {len(result)} >
max_length {max_length}"
+ )
+
+
+def test_truncate_rendered_value_exact_expected_output():
+ """Test that truncation produces exact expected output: message first,
then content when space allows."""
+ trunc_only = TRUNCATE_PREFIX + TRUNCATE_SUFFIX
+ overhead = len(TRUNCATE_PREFIX) + len(TRUNCATE_SUFFIX)
+ min_length_for_content = overhead + TRUNCATE_MIN_CONTENT_LENGTH
+
+ test_cases = [
+ (1, "test", trunc_only),
+ (3, "test", trunc_only),
+ (5, "test", trunc_only),
+ (10, "password123", trunc_only),
+ (20, "secret_value", trunc_only),
+ (50, "This is a test string", trunc_only),
+ (overhead + 1, "Hello World", trunc_only),
+ (overhead + 2, "Hello World", trunc_only),
+ (min_length_for_content - 3, "Hello World", trunc_only),
+ (90, "short", TRUNCATE_PREFIX + "short" + TRUNCATE_SUFFIX),
+ (100, "This is a longer string", TRUNCATE_PREFIX + "This is a longer
st" + TRUNCATE_SUFFIX),
+ (150, "x" * 200, TRUNCATE_PREFIX + "x" * 69 + TRUNCATE_SUFFIX),
+ (100, "None", TRUNCATE_PREFIX + "None" + TRUNCATE_SUFFIX),
+ (100, "True", TRUNCATE_PREFIX + "True" + TRUNCATE_SUFFIX),
+ (100, "{'key': 'value'}", TRUNCATE_PREFIX + "{'key': 'value'}" +
TRUNCATE_SUFFIX),
+ (100, "test's", TRUNCATE_PREFIX + "test's" + TRUNCATE_SUFFIX),
+ (90, '"quoted"', TRUNCATE_PREFIX + '"quoted"' + TRUNCATE_SUFFIX),
+ ]
+
+ for max_length, rendered, expected in test_cases:
+ result = truncate_rendered_value(rendered, max_length)
+ assert result == expected, (
+ f"max_length={max_length}, rendered={rendered!r}:\n"
+ f" expected: {expected!r}\n"
+ f" got: {result!r}"
+ )
diff --git a/task-sdk/pyproject.toml b/task-sdk/pyproject.toml
index e3f128077e2..1fcd6e8817c 100644
--- a/task-sdk/pyproject.toml
+++ b/task-sdk/pyproject.toml
@@ -143,6 +143,7 @@ path = "src/airflow/sdk/__init__.py"
"../shared/listeners/src/airflow_shared/listeners" =
"src/airflow/sdk/_shared/listeners"
"../shared/plugins_manager/src/airflow_shared/plugins_manager" =
"src/airflow/sdk/_shared/plugins_manager"
"../shared/providers_discovery/src/airflow_shared/providers_discovery" =
"src/airflow/sdk/_shared/providers_discovery"
+"../shared/template_rendering/src/airflow_shared/template_rendering" =
"src/airflow/sdk/_shared/template_rendering"
[tool.hatch.build.targets.wheel]
packages = ["src/airflow"]
diff --git a/task-sdk/src/airflow/sdk/_shared/template_rendering
b/task-sdk/src/airflow/sdk/_shared/template_rendering
new file mode 120000
index 00000000000..67f141b29a4
--- /dev/null
+++ b/task-sdk/src/airflow/sdk/_shared/template_rendering
@@ -0,0 +1 @@
+../../../../../shared/template_rendering/src/airflow_shared/template_rendering
\ No newline at end of file
diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
index 674935f5eae..e26344a81e5 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -42,6 +42,7 @@ from pydantic import AwareDatetime, ConfigDict, Field,
JsonValue, TypeAdapter
from airflow.dag_processing.bundles.base import BaseDagBundle,
BundleVersionLock
from airflow.dag_processing.bundles.manager import DagBundlesManager
from airflow.sdk._shared.observability.metrics.stats import Stats
+from airflow.sdk._shared.template_rendering import truncate_rendered_value
from airflow.sdk.api.client import get_hostname, getuser
from airflow.sdk.api.datamodels._generated import (
AssetProfile,
@@ -1002,10 +1003,7 @@ def _serialize_template_field(template_field: Any, name:
str) -> str | dict | li
serialized = str(template_field)
if len(serialized) > max_length:
rendered = redact(serialized, name)
- return (
- "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
- f"{rendered[: max_length - 79]!r}... "
- )
+ return truncate_rendered_value(str(rendered), max_length)
return serialized
if not template_field and not isinstance(template_field, tuple):
# Avoid unnecessary serialization steps for empty fields unless they
are tuples
@@ -1019,10 +1017,7 @@ def _serialize_template_field(template_field: Any, name:
str) -> str | dict | li
serialized = str(template_field)
if len(serialized) > max_length:
rendered = redact(serialized, name)
- return (
- "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
- f"{rendered[: max_length - 79]!r}... "
- )
+ return truncate_rendered_value(str(rendered), max_length)
return template_field
diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
index 05191806be8..37d3963146b 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
@@ -2806,18 +2806,24 @@ class TestRuntimeTaskInstance:
runtime_ti = create_runtime_ti(task=task,
dag_id="test_truncation_masking_dag")
run(runtime_ti, context=runtime_ti.get_template_context(),
log=mock.MagicMock())
- assert (
- call(
- msg=SetRenderedFields(
- rendered_fields={
- "env_vars": "Truncated. You can change this behaviour
in [core]max_templated_field_length. \"{'TEST_URL_0': '***', 'TEST_URL_1':
'***', 'TEST_URL_10': '***', 'TEST_URL_11': '***', 'TEST_URL_12': '***',
'TEST_URL_13': '***', 'TEST_URL_14': '***', 'TEST_URL_15': '***',
'TEST_URL_16': '***', 'TEST_URL_17': '***', 'TEST_URL_18': '***',
'TEST_URL_19': '***', 'TEST_URL_2': '***', 'TEST_URL_20': '***', 'TEST_URL_21':
'***', 'TEST_URL_22': '***', 'TEST_URL_23': '***', 'TE [...]
- "region": "us-west-2",
- },
- type="SetRenderedFields",
- )
- )
- in mock_supervisor_comms.send.mock_calls
+ msg = next(
+ c.kwargs["msg"]
+ for c in mock_supervisor_comms.send.mock_calls
+ if c.kwargs.get("msg") and getattr(c.kwargs["msg"], "type", None)
== "SetRenderedFields"
+ )
+ rendered_fields = msg.rendered_fields
+
+ # region is short enough to not be truncated
+ assert rendered_fields["region"] == "us-west-2"
+
+ # env_vars exceeds max_templated_field_length and must be truncated
with secrets redacted
+ env_vars_value = rendered_fields["env_vars"]
+ assert isinstance(env_vars_value, str)
+ assert env_vars_value.startswith(
+ "Truncated. You can change this behaviour in
[core]max_templated_field_length. "
)
+ assert env_vars_value.endswith("...")
+ assert "***" in env_vars_value # secrets are redacted before
truncation
@pytest.mark.enable_redact
def test_rendered_templates_masks_secrets_in_complex_objects(