diff --git a/packages/smithy-aws-core/.changes/next-release/smithy-aws-core-enhancement-7976d8dcd859490ab10c8b5c5876e21d.json b/packages/smithy-aws-core/.changes/next-release/smithy-aws-core-enhancement-7976d8dcd859490ab10c8b5c5876e21d.json new file mode 100644 index 000000000..2a9ed974c --- /dev/null +++ b/packages/smithy-aws-core/.changes/next-release/smithy-aws-core-enhancement-7976d8dcd859490ab10c8b5c5876e21d.json @@ -0,0 +1,4 @@ +{ + "type": "enhancement", + "description": "Add the `exec-env/` field to the User-Agent header from the `AWS_EXECUTION_ENV` environment variable, and log a warning when `sdk_ua_app_id` is longer than 50 characters." +} \ No newline at end of file diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py index 17b246183..b6443ca17 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py @@ -27,6 +27,7 @@ validate_profile, validate_region, validate_retry_mode, + validate_sdk_ua_app_id, ) from .context import AsyncSharedConfigContext from .filesystem import AsyncFileSystem @@ -184,6 +185,7 @@ class AsyncAwsConfig: "sdk_ua_app_id": FieldSpec( default=None, resolver=resolve_sdk_ua_app_id, + validator=validate_sdk_ua_app_id, ), "user_agent_extra": FieldSpec( default=None, diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py index 7a64687ab..0d40c319c 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import logging import re from collections.abc import Collection from typing import get_args @@ -15,8 +16,13 @@ "validate_profile", "validate_region", "validate_retry_mode", + "validate_sdk_ua_app_id", ] +logger = logging.getLogger(__name__) + +_SDK_UA_APP_ID_MAX_LENGTH = 50 + _REGION_PATTERN = re.compile(r"^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{1,63}(? None: + """Warn when the SDK user agent app ID is longer than recommended. + + The app ID is still used. Config is resolved once, so the warning is logged once. + + :param value: The resolved app ID value. + """ + if isinstance(value, str) and len(value) > _SDK_UA_APP_ID_MAX_LENGTH: + logger.warning( + "The configured value for sdk_ua_app_id exceeds the recommended maximum " + "length of %d characters.", + _SDK_UA_APP_ID_MAX_LENGTH, + ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index c584d208f..6064e65cc 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # pyright: reportMissingTypeStubs=false +import os from typing import Any import smithy_core @@ -39,6 +40,7 @@ def __init__( self._ua_app_id = ua_app_id self._sdk_version = sdk_version self._service_id = service_id + self._execution_env = os.environ.get("AWS_EXECUTION_ENV") def read_after_serialization(self, context: RequestContext[Any, Any]) -> None: if USER_AGENT in context.properties: @@ -48,6 +50,11 @@ def read_after_serialization(self, context: RequestContext[Any, Any]) -> None: UserAgentComponent("api", self._service_id, self._sdk_version) ) + if self._execution_env: + user_agent.env_metadata.append( + UserAgentComponent("exec-env", self._execution_env) + ) + if self._ua_app_id is not None: user_agent.additional_metadata.append( UserAgentComponent("app", self._ua_app_id) diff --git a/packages/smithy-aws-core/tests/unit/config/test_validators.py b/packages/smithy-aws-core/tests/unit/config/test_validators.py index 4c1015943..14fede94a 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_validators.py +++ b/packages/smithy-aws-core/tests/unit/config/test_validators.py @@ -9,6 +9,7 @@ validate_max_attempts, validate_region, validate_retry_mode, + validate_sdk_ua_app_id, ) @@ -89,3 +90,14 @@ def test_valid_values(self, value: int): def test_invalid_values(self, value: int): with pytest.raises(ConfigValidationError, match="max_attempts"): validate_max_attempts(value) + + +class TestValidateSdkUaAppId: + def test_warns_when_too_long(self, caplog: pytest.LogCaptureFixture): + validate_sdk_ua_app_id("a" * 51) + assert "exceeds the recommended maximum length of 50" in caplog.text + + @pytest.mark.parametrize("value", [None, "a" * 50]) + def test_no_warning(self, value: str | None, caplog: pytest.LogCaptureFixture): + validate_sdk_ua_app_id(value) + assert caplog.text == "" diff --git a/packages/smithy-aws-core/tests/unit/interceptors/test_user_agent.py b/packages/smithy-aws-core/tests/unit/interceptors/test_user_agent.py new file mode 100644 index 000000000..a9d1f4acb --- /dev/null +++ b/packages/smithy-aws-core/tests/unit/interceptors/test_user_agent.py @@ -0,0 +1,36 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Any +from unittest.mock import Mock + +import pytest +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor +from smithy_core.interceptors import RequestContext +from smithy_core.types import TypedProperties +from smithy_http.interceptors.user_agent import USER_AGENT +from smithy_http.user_agent import UserAgent, UserAgentComponent + + +def _user_agent_string() -> str: + interceptor = UserAgentInterceptor( + ua_suffix=None, ua_app_id=None, sdk_version="1.0.0", service_id="svc" + ) + properties = TypedProperties() + properties[USER_AGENT] = UserAgent( + language_metadata=[UserAgentComponent("lang", "python", "3.12")] + ) + context = RequestContext[Any, Any]( + request=Mock(), transport_request=Mock(), properties=properties + ) + interceptor.read_after_serialization(context) + return str(properties[USER_AGENT]) + + +def test_adds_execution_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AWS_EXECUTION_ENV", "AWS_Lambda_python3.12") + assert "lang/python#3.12 exec-env/AWS_Lambda_python3.12" in _user_agent_string() + + +def test_omits_execution_env_when_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AWS_EXECUTION_ENV", raising=False) + assert "exec-env/" not in _user_agent_string() diff --git a/scripts/api-snapshot/api.json b/scripts/api-snapshot/api.json index b904a65cf..e9d7a84e1 100644 --- a/scripts/api-snapshot/api.json +++ b/scripts/api-snapshot/api.json @@ -126,7 +126,8 @@ "validate_max_attempts": "rex-function", "validate_profile": "rex-function", "validate_region": "rex-function", - "validate_retry_mode": "rex-function" + "validate_retry_mode": "rex-function", + "validate_sdk_ua_app_id": "rex-function" }, "smithy_aws_core.aio.config.context": { "AsyncDefaultFileSystem": "rex-class[async]", @@ -465,10 +466,12 @@ "ConfigSource": "rex-class", "ConfigValidationError": "rex-exception", "ProfileNotFoundError": "rex-exception", + "logger": "Logger", "validate_max_attempts": "function", "validate_profile": "function", "validate_region": "function", - "validate_retry_mode": "function" + "validate_retry_mode": "function", + "validate_sdk_ua_app_id": "function" }, "smithy_aws_core.endpoints": { "PropertyKey": "rex-class",