diff --git a/awscrt/__init__.py b/awscrt/__init__.py index 68e23bf54..dbe9eb2c8 100644 --- a/awscrt/__init__.py +++ b/awscrt/__init__.py @@ -3,7 +3,6 @@ from weakref import WeakSet - __all__ = [ 'aio', 'auth', @@ -16,7 +15,6 @@ 'mqtt_request_response', 's3', 'websocket', - 'aws_iot_metrics', ] __version__ = '1.0.0.dev0' diff --git a/awscrt/_aws_iot_metrics.py b/awscrt/_aws_iot_metrics.py new file mode 100644 index 000000000..d2925f8d4 --- /dev/null +++ b/awscrt/_aws_iot_metrics.py @@ -0,0 +1,16 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +from dataclasses import dataclass + + +@dataclass +class AWSIoTMetrics: + """ + Configuration for IoT SDK metrics that are embedded in MQTT Connect Packet username field. + + Args: + library_name (str): The SDK library name (e.g., "IoTDeviceSDK/Python") + + """ + library_name: str = "IoTDeviceSDK/Python" diff --git a/awscrt/aws_iot_metrics.py b/awscrt/aws_iot_metrics.py deleted file mode 100644 index c3db4f909..000000000 --- a/awscrt/aws_iot_metrics.py +++ /dev/null @@ -1,466 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0. - -from dataclasses import dataclass -from typing import List, Optional -from enum import Enum -import sys - - -@dataclass -class IoTMetricsMetadata: - """A key-value pair for IoT SDK metrics metadata. - - Metadata entries are appended to the MQTT CONNECT packet username field - as part of the Metadata query parameter. - - Args: - key (str): The metadata key (e.g., "IoTSDKVersion", "IoTSDKFeature", "CRTVersion") - value (str): The metadata value - """ - key: str - value: str - - -@dataclass -class AWSIoTMetrics: - """Configuration for IoT SDK metrics that are embedded in MQTT Connect Packet username field. - - Args: - library_name (str): The SDK library name (e.g., "IoTDeviceSDK/Python") - metadata_entries (Optional[List[IoTMetricsMetadata]]): Optional list for storing key-value pairs of metadata - - """ - library_name: str = "IoTDeviceSDK/Python" - metadata_entries: Optional[List[IoTMetricsMetadata]] = None - - -# Metrics Version Constant -IOT_SDK_METRICS_FEATURE_VERSION = 1 - -# Feature ID Constants - - -class _MetricsFeatureId(str, Enum): - """Feature IDs for IoT SDK metrics tracking. - - Each ID is a single character used to encode feature usage in the metrics - string with the format "ID/Value". IDs are assigned sequentially and never - reused to ensure historical data consistency across SDK versions. - """ - RETRY_JITTER_MODE = "A" - SESSION_BEHAVIOR = "B" - OFFLINE_QUEUE_BEHAVIOR = "C" - OUTBOUND_TOPIC_ALIAS_BEHAVIOR = "D" - INBOUND_TOPIC_ALIAS_BEHAVIOR = "E" - PROTOCOL_VERSION = "F" - SOCKET_IMPLEMENTATION = "G" - HTTP_PROXY_TYPE = "H" - CERTIFICATE_SOURCE = "I" - TLS_CIPHER_PREFERENCE = "J" - MINIMUM_TLS_VERSION = "K" - -# Feature Value Constants - - -def _protocol_version_metrics_value(protocol): - """Map protocol version to its single-character metrics value. - - Mapping: MQTT311->3, MQTT5->5. - """ - mapping = { - "MQTT311": "3", - "MQTT5": "5", - } - return mapping.get(protocol) - - -def _socket_implementation_metrics_value(): - """Detect the socket implementation and return its single-character metrics value. - - Mapping: Windows (WINSOCK)->B, all other platforms (POSIX)->A. - """ - if sys.platform == "win32": - return "B" - return "A" - - -def _http_proxy_type_metrics_value(proxy_options): - """Map proxy options to the single-character metrics value for proxy type. - - Mapping: HTTPS (has tls_connection_options)->B, HTTP->A. - """ - if getattr(proxy_options, 'tls_connection_options', None) is not None: - return "B" - return "A" - - -def _retry_jitter_metrics_value(mode): - """Map ExponentialBackoffJitterMode to its single-character metrics value. - - Mapping: NONE->A, FULL->B, DECORRELATED->C. - Returns None for DEFAULT. - """ - from awscrt.mqtt5 import ExponentialBackoffJitterMode - mapping = { - ExponentialBackoffJitterMode.NONE: "A", - ExponentialBackoffJitterMode.FULL: "B", - ExponentialBackoffJitterMode.DECORRELATED: "C", - } - return mapping.get(mode) - - -def _client_session_behavior_metrics_value(behavior): - """Map ClientSessionBehaviorType to its single-character metrics value. - - Mapping: CLEAN->A, REJOIN_POST_SUCCESS->B, REJOIN_ALWAYS->C. - Returns None for DEFAULT. - """ - from awscrt.mqtt5 import ClientSessionBehaviorType - mapping = { - ClientSessionBehaviorType.CLEAN: "A", - ClientSessionBehaviorType.REJOIN_POST_SUCCESS: "B", - ClientSessionBehaviorType.REJOIN_ALWAYS: "C", - } - return mapping.get(behavior) - - -def _client_operation_queue_behavior_metrics_value(behavior): - """Map ClientOperationQueueBehaviorType to its single-character metrics value. - - Mapping: FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT->A, - FAIL_QOS0_PUBLISH_ON_DISCONNECT->B, FAIL_ALL_ON_DISCONNECT->C. - Returns None for DEFAULT. - """ - from awscrt.mqtt5 import ClientOperationQueueBehaviorType - mapping = { - ClientOperationQueueBehaviorType.FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT: "A", - ClientOperationQueueBehaviorType.FAIL_QOS0_PUBLISH_ON_DISCONNECT: "B", - ClientOperationQueueBehaviorType.FAIL_ALL_ON_DISCONNECT: "C", - } - return mapping.get(behavior) - - -def _outbound_topic_alias_behavior_metrics_value(behavior): - """Map OutboundTopicAliasBehaviorType to its single-character metrics value. - - Mapping: MANUAL->A, LRU->B, DISABLED->C. - Returns None for DEFAULT. - """ - from awscrt.mqtt5 import OutboundTopicAliasBehaviorType - mapping = { - OutboundTopicAliasBehaviorType.MANUAL: "A", - OutboundTopicAliasBehaviorType.LRU: "B", - OutboundTopicAliasBehaviorType.DISABLED: "C", - } - return mapping.get(behavior) - - -def _inbound_topic_alias_behavior_metrics_value(behavior): - """Map InboundTopicAliasBehaviorType to its single-character metrics value. - - Mapping: ENABLED->A, DISABLED->B. - Returns None for DEFAULT. - """ - from awscrt.mqtt5 import InboundTopicAliasBehaviorType - mapping = { - InboundTopicAliasBehaviorType.ENABLED: "A", - InboundTopicAliasBehaviorType.DISABLED: "B", - } - return mapping.get(behavior) - - -def _minimum_tls_version_metrics_value(version): - """Map TlsVersion to its single-character metrics value. - - Mapping: SSLv3->A, TLSv1->B, TLSv1_1->C, TLSv1_2->D, TLSv1_3->E. - Returns None for DEFAULT. - """ - from awscrt.io import TlsVersion - mapping = { - TlsVersion.SSLv3: "A", - TlsVersion.TLSv1: "B", - TlsVersion.TLSv1_1: "C", - TlsVersion.TLSv1_2: "D", - TlsVersion.TLSv1_3: "E", - } - return mapping.get(version) - - -def _tls_cipher_preference_metrics_value(pref): - """Map TlsCipherPref to its single-character metrics value. - - Mapping: PQ_TLSv1_0_2021_05->A, PQ_DEFAULT->B, TLSv1_2_2025_07->C. - Returns None for DEFAULT. - """ - from awscrt.io import TlsCipherPref - mapping = { - TlsCipherPref.PQ_TLSv1_0_2021_05: "A", - TlsCipherPref.PQ_DEFAULT: "B", - TlsCipherPref.TLSv1_2_2025_07: "C", - } - return mapping.get(pref) - - -# MQTT5 encoding list -def _get_encoded_feature_list(client_options): - """Generates the encoded feature list string for metrics from MQTT5 ClientOptions. - - Format: "ID/Value,ID/Value,..." - Example: "A/B,C/A,F/5,G/A" means retry_jitter_mode=FULL, offline_queue_behavior= - FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, protocol=MQTT5, socket=POSIX. - - MQTT5 connections always include: - - F (protocol_version): set to MQTT5 - - G (socket_implementation): detected from platform (POSIX or WINSOCK) - - Conditionally includes (only when the option is explicitly set and not DEFAULT): - - A (retry_jitter_mode): from client_options.retry_jitter_mode - - B (session_behavior): from client_options.session_behavior - - C (offline_queue_behavior): from client_options.offline_queue_behavior - - D (outbound_topic_alias_behavior): from topic_aliasing_options.outbound_behavior - - E (inbound_topic_alias_behavior): from topic_aliasing_options.inbound_behavior - - H (http_proxy_type): HTTP or HTTPS based on proxy TLS settings - - J (tls_cipher_preference): mapped from TlsCipherPref on the TLS context - - K (minimum_tls_version): mapped from TlsVersion on the TLS context - - Feature I (certificate_source) is set at the IoT SDK level, not here. - - Args: - client_options: MQTT5 ClientOptions dataclass. - Returns: - str: The encoded feature list string. - """ - - features = [] - - # A: retry_jitter_mode - if client_options.retry_jitter_mode is not None: - val = _retry_jitter_metrics_value(client_options.retry_jitter_mode) - if val: - features.append(f"{_MetricsFeatureId.RETRY_JITTER_MODE.value}/{val}") - - # B: session_behavior - if client_options.session_behavior is not None: - val = _client_session_behavior_metrics_value(client_options.session_behavior) - if val: - features.append(f"{_MetricsFeatureId.SESSION_BEHAVIOR.value}/{val}") - - # C: offline_queue_behavior - if client_options.offline_queue_behavior is not None: - val = _client_operation_queue_behavior_metrics_value(client_options.offline_queue_behavior) - if val: - features.append(f"{_MetricsFeatureId.OFFLINE_QUEUE_BEHAVIOR.value}/{val}") - - # D: outbound_topic_alias_behavior - if client_options.topic_aliasing_options is not None: - if client_options.topic_aliasing_options.outbound_behavior is not None: - val = _outbound_topic_alias_behavior_metrics_value(client_options.topic_aliasing_options.outbound_behavior) - if val: - features.append(f"{_MetricsFeatureId.OUTBOUND_TOPIC_ALIAS_BEHAVIOR.value}/{val}") - - # E: inbound_topic_alias_behavior - if client_options.topic_aliasing_options is not None: - if client_options.topic_aliasing_options.inbound_behavior is not None: - val = _inbound_topic_alias_behavior_metrics_value(client_options.topic_aliasing_options.inbound_behavior) - if val: - features.append(f"{_MetricsFeatureId.INBOUND_TOPIC_ALIAS_BEHAVIOR.value}/{val}") - - # F: protocol_version - MQTT5 always uses client options - features.append(f"{_MetricsFeatureId.PROTOCOL_VERSION.value}/{_protocol_version_metrics_value('MQTT5')}") - - # G: socket_implementation - Detect based on platform - features.append(f"{_MetricsFeatureId.SOCKET_IMPLEMENTATION.value}/{_socket_implementation_metrics_value()}") - - # H: http_proxy_type - Determine based on whether proxy uses TLS - if client_options.http_proxy_options is not None: - val = _http_proxy_type_metrics_value(client_options.http_proxy_options) - features.append(f"{_MetricsFeatureId.HTTP_PROXY_TYPE.value}/{val}") - - # I: certificate_source - Would need to be tracked from TLS context setup. This is set at a IoT SDK level - - # J: tls_cipher_preference - security policy - if client_options.tls_ctx is not None: - val = _tls_cipher_preference_metrics_value(client_options.tls_ctx._cipher_pref) - if val: - features.append(f"{_MetricsFeatureId.TLS_CIPHER_PREFERENCE.value}/{val}") - - # K: minimum_tls_version - The minimum TLS version set on TLSContextOptions - if client_options.tls_ctx is not None: - val = _minimum_tls_version_metrics_value(client_options.tls_ctx._min_tls_ver) - if val: - features.append(f"{_MetricsFeatureId.MINIMUM_TLS_VERSION.value}/{val}") - - return ",".join(features) - -# MQTT3 encoding list - - -def _get_encoded_feature_list_mqtt3(proxy_options, tls_ctx=None): - """Generates the encoded feature list string for metrics from MQTT3 connection options. - Format: "ID/Value,ID/Value..." - - MQTT3 connections always include: - - F (protocol_version): set to MQTT311 - - G (socket_implementation): detected from platform (POSIX or WINSOCK) - - Conditionally includes: - - H (http_proxy_type): HTTP or HTTPS based on proxy TLS settings - - J (tls_cipher_preference): mapped from TlsCipherPref on the TLS context - - K (minimum_tls_version): mapped from TlsVersion on the TLS context - - Args: - proxy_options: Optional HttpProxyOptions from the Connection. - tls_ctx: Optional ClientTlsContext used by the connection. - Returns: - str: The encoded feature list string. - """ - features = [ - f"{_MetricsFeatureId.PROTOCOL_VERSION.value}/{_protocol_version_metrics_value('MQTT311')}", - f"{_MetricsFeatureId.SOCKET_IMPLEMENTATION.value}/{_socket_implementation_metrics_value()}" - ] - # H: http_proxy_type - Determine based on whether proxy uses TLS - if proxy_options is not None: - val = _http_proxy_type_metrics_value(proxy_options) - features.append(f"{_MetricsFeatureId.HTTP_PROXY_TYPE.value}/{val}") - - # J: tls_cipher_preference - security policy - if tls_ctx is not None: - val = _tls_cipher_preference_metrics_value(tls_ctx._cipher_pref) - if val: - features.append(f"{_MetricsFeatureId.TLS_CIPHER_PREFERENCE.value}/{val}") - - # K: minimum_tls_version - the minimum TLS version set on TLSContextOptions - if tls_ctx is not None: - val = _minimum_tls_version_metrics_value(tls_ctx._min_tls_ver) - if val: - features.append(f"{_MetricsFeatureId.MINIMUM_TLS_VERSION.value}/{val}") - - return ",".join(features) - - -def _merge_feature_lists(crt_features, user_features): - """Merge CRT-generated features with user-provided (IoT SDK) features. - - When both lists contain the same feature ID, the user-provided value - takes precedence. - - Args: - crt_features (str): CRT-generated feature list. - user_features (str): User-provided feature list from the IoT SDK. - May be empty string if no SDK features are provided. - Returns: - str: The merged feature list string. - """ - merged = {} - # Parse CRT Features - for pair in crt_features.split(","): - if "/" in pair: - fid, val = pair.split("/", 1) - merged[fid] = val - - for pair in user_features.split(","): - if "/" in pair: - fid, val = pair.split("/", 1) - merged[fid] = val - return ",".join(f"{k}/{v}" for k, v in merged.items()) - -# Metrics creation - - -def _create_metrics(user_metrics, crt_feature_list): - """Create the final AWSIoTMetrics object by merging CRT and user-provided data. - - Applies the following rules to produce the final metrics: - - 1. library_name: Uses the value from user_metrics if provided, - otherwise defaults to "IoTDeviceSDK/Python". - 2. CRTVersion: Automatically set to the current awscrt - package version. Cannot be overridden by user input. - 3. IoTSDKMetricsVersion: Always set to the current - IOT_SDK_METRICS_FEATURE_VERSION constant. - 4. IoTSDKFeature: If the user-provided metrics version - matches IOT_SDK_METRICS_FEATURE_VERSION, the CRT feature list is - merged with the user's IoTSDKFeature (user values take precedence - for duplicate feature IDs). Otherwise, only CRT features are used. - 5. Any additional user metadata entries (other than CRTVersion, - IoTSDKMetricsVersion, IoTSDKFeature) are passed through unchanged. - - Args: - user_metrics : Metrics configuration from - the IoT SDK. May be None if no SDK-level metrics are provided. - crt_feature_list : Encoded CRT feature list string generated - by _get_encoded_feature_list or _get_encoded_feature_list_mqtt3. - Returns: - AWSIoTMetrics: The final metrics object ready to be embedded in the - MQTT CONNECT packet username field. - """ - - from awscrt import __version__ as crt_version - - final_metrics = AWSIoTMetrics( - library_name=user_metrics.library_name if user_metrics else "IoTDeviceSDK/Python" - ) - - # CRTVERSION: not modifiable by user, automatically set - metadata = {"CRTVersion": crt_version} - - # Extract user_metadata from IoT SDK - user_metrics_version = None - user_feature = "" - if user_metrics and user_metrics.metadata_entries: - for entry in user_metrics.metadata_entries: - if entry.key == "IoTSDKMetricsVersion": - user_metrics_version = entry.value - elif entry.key == "IoTSDKFeature": - user_feature = entry.value - elif entry.key != "CRTVersion": - metadata[entry.key] = entry.value - - # Merge features: if version matches, merge CRT + SDK; otherwise CRT only - if (user_metrics_version is not None and user_metrics_version.isdigit() and int( - user_metrics_version) == IOT_SDK_METRICS_FEATURE_VERSION): - metadata["IoTSDKFeature"] = _merge_feature_lists(crt_feature_list, user_feature) - else: - metadata["IoTSDKFeature"] = _merge_feature_lists(crt_feature_list, "") - - # Always set current metrics version - metadata["IoTSDKMetricsVersion"] = str(IOT_SDK_METRICS_FEATURE_VERSION) - - final_metrics.metadata_entries = [IoTMetricsMetadata(key=k, value=v) for k, v in metadata.items()] - return final_metrics - - -def _create_metrics_mqtt5(client_options): - """Create the final AWSIoTMetrics object for an MQTT5 client. - - Generates the CRT feature list from the full set of MQTT5 ClientOptions - - Args: - client_options: MQTT5 ClientOptions dataclass containing all - connection configuration and optional user metrics. - Returns: - AWSIoTMetrics: The final metrics object with merged CRT and SDK features. - """ - crt_feature_list = _get_encoded_feature_list(client_options) - return _create_metrics(client_options.metrics, crt_feature_list) - - -def _create_metrics_mqtt3(user_metrics=None, proxy_options=None, tls_ctx=None): - """Creates the final AWSIoTMetrics object for an MQTT3 connection. - - Generates the CRT feature list from the MQTT3 connection parameters - - Args: - user_metrics : Optional metrics configuration - provided by the IoT SDK. If None, defaults are used. - proxy_options : Optional HTTP proxy options - from the Connection, used to determine proxy type feature. - tls_ctx : Optional TLS context from the - connection, used to determine cipher preference and minimum TLS - version features. - Returns: - AWSIoTMetrics: The final metrics object with merged CRT and SDK features. - """ - crt_feature_list = _get_encoded_feature_list_mqtt3(proxy_options, tls_ctx) - return _create_metrics(user_metrics, crt_feature_list) diff --git a/awscrt/io.py b/awscrt/io.py index 8bfd0dd79..4d36ba69f 100644 --- a/awscrt/io.py +++ b/awscrt/io.py @@ -606,16 +606,12 @@ class ClientTlsContext(NativeResource): Args: options (TlsContextOptions): Configuration options. """ - __slots__ = ('_min_tls_ver', '_cipher_pref') + __slots__ = () def __init__(self, options): assert isinstance(options, TlsContextOptions) super().__init__() - - self._min_tls_ver = options.min_tls_ver - self._cipher_pref = options.cipher_pref - self._binding = _awscrt.client_tls_ctx_new( options.min_tls_ver.value, options.cipher_pref.value, diff --git a/awscrt/mqtt.py b/awscrt/mqtt.py index 91e5ed6e4..d3c3921d5 100644 --- a/awscrt/mqtt.py +++ b/awscrt/mqtt.py @@ -16,7 +16,7 @@ from awscrt.io import ClientBootstrap, ClientTlsContext, SocketOptions from dataclasses import dataclass from awscrt.mqtt5 import Client as Mqtt5Client -from awscrt.aws_iot_metrics import AWSIoTMetrics, IoTMetricsMetadata, _create_metrics_mqtt3 +from awscrt._aws_iot_metrics import AWSIoTMetrics class QoS(IntEnum): @@ -332,9 +332,7 @@ class Connection(NativeResource): proxy_options (Optional[awscrt.http.HttpProxyOptions]): Optional proxy options for all connections. - disable_metrics (bool): Disable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to False. - - metrics (Optional[AWSIoTMetrics]) : Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created. + enable_metrics (bool): Enable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to True. """ def __init__(self, @@ -361,8 +359,7 @@ def __init__(self, on_connection_success=None, on_connection_failure=None, on_connection_closed=None, - disable_metrics=False, - metrics=None, + enable_metrics=True, ): assert isinstance(client, Client) or isinstance(client, Mqtt5Client) @@ -415,10 +412,10 @@ def __init__(self, self.password = password self.socket_options = socket_options if socket_options else SocketOptions() self.proxy_options = proxy_options if proxy_options else websocket_proxy_options - if disable_metrics: - self._metrics = None + if enable_metrics: + self._metrics = AWSIoTMetrics() else: - self._metrics = _create_metrics_mqtt3(metrics, self.proxy_options, self.client.tls_ctx) + self._metrics = None self._binding = _awscrt.mqtt_client_connection_new( self, diff --git a/awscrt/mqtt5.py b/awscrt/mqtt5.py index 57ee84202..bc20581e5 100644 --- a/awscrt/mqtt5.py +++ b/awscrt/mqtt5.py @@ -5,7 +5,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0. -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Union import _awscrt from concurrent.futures import Future from enum import IntEnum @@ -15,7 +15,7 @@ from dataclasses import dataclass from collections.abc import Sequence from inspect import signature -from awscrt.aws_iot_metrics import AWSIoTMetrics, IoTMetricsMetadata, _create_metrics_mqtt5 +from awscrt._aws_iot_metrics import AWSIoTMetrics class QoS(IntEnum): @@ -1371,8 +1371,7 @@ class ClientOptions: on_lifecycle_event_connection_success_fn (Callable[[LifecycleConnectSuccessData],]): Callback for Lifecycle Event Connection Success. on_lifecycle_event_connection_failure_fn (Callable[[LifecycleConnectFailureData],]): Callback for Lifecycle Event Connection Failure. on_lifecycle_event_disconnection_fn (Callable[[LifecycleDisconnectData],]): Callback for Lifecycle Event Disconnection. - disable_metrics (bool): Disable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to False. - metrics (Optional[AWSIoTMetrics]) : Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created. + enable_metrics (bool): Enable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to True. """ host_name: str @@ -1400,8 +1399,7 @@ class ClientOptions: on_lifecycle_event_connection_success_fn: Callable[[LifecycleConnectSuccessData], None] = None on_lifecycle_event_connection_failure_fn: Callable[[LifecycleConnectFailureData], None] = None on_lifecycle_event_disconnection_fn: Callable[[LifecycleDisconnectData], None] = None - disable_metrics: bool = False - metrics: Optional[AWSIoTMetrics] = None + enable_metrics: bool = True def _check_callback(callback): @@ -1430,7 +1428,7 @@ def __init__(self, client_options: ClientOptions): self._on_lifecycle_connection_failure_cb = _check_callback( client_options.on_lifecycle_event_connection_failure_fn) self._on_lifecycle_disconnection_cb = _check_callback(client_options.on_lifecycle_event_disconnection_fn) - self._disable_metrics = client_options.disable_metrics + self._enable_metrics = client_options.enable_metrics def _ws_handshake_transform(self, http_request_binding, http_headers_binding, native_userdata): if self._ws_handshake_transform_cb is None: @@ -1778,7 +1776,7 @@ def __init__( keep_alive_secs: int, ack_timeout_secs: int, clean_session: int, - disable_metrics: bool): + enable_metrics: bool): self.host_name = host_name self.port = port self.client_id = "" if client_id is None else client_id @@ -1789,7 +1787,7 @@ def __init__( self.keep_alive_secs: int = 1200 if keep_alive_secs is None else keep_alive_secs self.ack_timeout_secs: int = 0 if ack_timeout_secs is None else ack_timeout_secs self.clean_session: bool = True if clean_session is None else clean_session - self.disable_metrics: bool = False if disable_metrics is None else disable_metrics + self.enable_metrics: bool = True if enable_metrics is None else enable_metrics class Client(NativeResource): @@ -1821,10 +1819,10 @@ def __init__(self, client_options: ClientOptions): socket_options = SocketOptions() # Handle metrics configuration - if client_options.disable_metrics: - self._metrics = None + if client_options.enable_metrics: + self._metrics = AWSIoTMetrics() else: - self._metrics = _create_metrics_mqtt5(client_options) + self._metrics = None if not connect_options.will: is_will_none = True @@ -1877,8 +1875,8 @@ def __init__(self, client_options: ClientOptions): client_options.ack_timeout_sec, client_options.topic_aliasing_options, websocket_is_none, - not client_options.disable_metrics, - self._metrics, + client_options.enable_metrics, + self._metrics.library_name if self._metrics else None, core) # Store the options for adapter @@ -1894,7 +1892,7 @@ def __init__(self, client_options: ClientOptions): ack_timeout_secs=client_options.ack_timeout_sec, clean_session=( client_options.session_behavior < ClientSessionBehaviorType.REJOIN_ALWAYS if client_options.session_behavior else True), - disable_metrics=client_options.disable_metrics) + enable_metrics=client_options.enable_metrics) def start(self): """Notifies the MQTT5 client that you want it maintain connectivity to the configured endpoint. @@ -2148,5 +2146,5 @@ def new_connection(self, on_connection_interrupted=None, on_connection_resumed=N websocket_proxy_options=None, websocket_handshake_transform=None, proxy_options=None, - disable_metrics=self.adapter_options.disable_metrics + enable_metrics=self.adapter_options.enable_metrics ) diff --git a/docsrc/source/api/aws_iot_metrics.rst b/docsrc/source/api/aws_iot_metrics.rst deleted file mode 100644 index 56148c54b..000000000 --- a/docsrc/source/api/aws_iot_metrics.rst +++ /dev/null @@ -1,6 +0,0 @@ -awscrt.aws_iot_metrics -====================== - -.. automodule:: awscrt.aws_iot_metrics - :members: - diff --git a/docsrc/source/index.rst b/docsrc/source/index.rst index 5d0a510be..bbac435c3 100644 --- a/docsrc/source/index.rst +++ b/docsrc/source/index.rst @@ -12,7 +12,6 @@ API Reference :maxdepth: 2 api/auth - api/aws_iot_metrics api/checksums api/common api/crypto diff --git a/source/mqtt5_client.c b/source/mqtt5_client.c index 4c365325e..6018b565c 100644 --- a/source/mqtt5_client.c +++ b/source/mqtt5_client.c @@ -831,51 +831,6 @@ static bool s_py_topic_aliasing_options_init( return success; } -/******************************************************************************* - * Metrics Parsing - ******************************************************************************/ - -bool aws_py_metrics_parse(PyObject *metrics_py, struct aws_mqtt_iot_metrics *out_metrics) { - AWS_ZERO_STRUCT(*out_metrics); - - PyObject *library_name_py = PyObject_GetAttrString(metrics_py, "library_name"); - out_metrics->library_name = aws_byte_cursor_from_pyunicode(library_name_py); - Py_XDECREF(library_name_py); - - PyObject *metadata_entries_py = PyObject_GetAttrString(metrics_py, "metadata_entries"); - - if (metadata_entries_py && metadata_entries_py != Py_None && PyList_Check(metadata_entries_py)) { - Py_ssize_t count = PyList_Size(metadata_entries_py); - if (count > 0) { - struct aws_mqtt_metadata_entry *entries = - aws_mem_calloc(aws_py_get_allocator(), (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - - for (Py_ssize_t i = 0; i < count; ++i) { - PyObject *entry_py = PyList_GetItem(metadata_entries_py, i); - PyObject *key_py = PyObject_GetAttrString(entry_py, "key"); - PyObject *value_py = PyObject_GetAttrString(entry_py, "value"); - - entries[i].key = aws_byte_cursor_from_pyunicode(key_py); - entries[i].value = aws_byte_cursor_from_pyunicode(value_py); - - Py_XDECREF(key_py); - Py_XDECREF(value_py); - } - out_metrics->metadata_count = (size_t)count; - out_metrics->metadata_entries = entries; - } - } - - Py_XDECREF(metadata_entries_py); - return true; -} - -void aws_py_metrics_clean_up(struct aws_mqtt_iot_metrics *metrics) { - if (metrics->metadata_entries) { - aws_mem_release(aws_py_get_allocator(), (void *)metrics->metadata_entries); - } -} - /******************************************************************************* * Client Init ******************************************************************************/ @@ -935,12 +890,12 @@ PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args) { PyObject *is_websocket_none_py; PyObject *client_core_py; /* Metrics */ - PyObject *is_metrics_enabled_py; /* optional enable metrics */ - PyObject *metrics_py; /* optional AWSIoTMetrics object */ + PyObject *is_metrics_enabled_py; /* optional enable metrics */ + struct aws_byte_cursor metrics_library_name; /* optional IoT SDK metrics username */ if (!PyArg_ParseTuple( args, - "Os#IOOOOz#Oz#z#OOOOOOOOOz*Oz#OOOz#z*z#OOOOOOOOOOOOOOOO", + "Os#IOOOOz#Oz#z#OOOOOOOOOz*Oz#OOOz#z*z#OOOOOOOOOOOOOOz#O", /* O */ &self_py, /* s */ &host_name.ptr, /* # */ &host_name.len, @@ -998,7 +953,8 @@ PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args) { /* Metrics */ /* O */ &is_metrics_enabled_py, - /* O */ &metrics_py, + /* z */ &metrics_library_name.ptr, + /* # */ &metrics_library_name.len, /* O */ &client_core_py)) { return NULL; @@ -1019,8 +975,6 @@ PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args) { AWS_ZERO_STRUCT(tls_options); struct aws_mqtt5_user_property *user_properties_tmp = NULL; struct aws_mqtt5_user_property *will_user_properties_tmp = NULL; - struct aws_mqtt_iot_metrics metrics_tmp; - AWS_ZERO_STRUCT(metrics_tmp); struct aws_mqtt5_client_options client_options; AWS_ZERO_STRUCT(client_options); @@ -1365,10 +1319,10 @@ PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args) { } /* METRICS */ - if (PyObject_IsTrue(is_metrics_enabled_py) && metrics_py != Py_None) { - if (!aws_py_metrics_parse(metrics_py, &metrics_tmp)) { - goto done; - } + struct aws_mqtt_iot_metrics metrics_tmp; + AWS_ZERO_STRUCT(metrics_tmp); + if (PyObject_IsTrue(is_metrics_enabled_py)) { + metrics_tmp.library_name = metrics_library_name; client_options.metrics = &metrics_tmp; } @@ -1414,15 +1368,10 @@ PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args) { } PyBuffer_Release(&will_payload_stack); PyBuffer_Release(&will_correlation_data_stack); - - /* Cleanup metrics */ - aws_py_metrics_clean_up(&metrics_tmp); - if (success) { return capsule; } Py_XDECREF(capsule); - return NULL; } diff --git a/source/mqtt5_client.h b/source/mqtt5_client.h index 2025169e5..d05930177 100644 --- a/source/mqtt5_client.h +++ b/source/mqtt5_client.h @@ -7,28 +7,6 @@ #include "module.h" -#include - -/** - * Parse a Python AWSIoTMetrics object into a C aws_mqtt_iot_metrics struct. - * - * WARNING: This function calls AWS_ZERO_STRUCT on out_metrics, which - * unconditionally zeroes all fields. The caller must pass a pointer to an - * uninitialized (or already cleaned-up) struct. If out_metrics currently owns - * heap-allocated memory (e.g. a previous metadata_entries array), that memory - * will be leaked because the pointer is overwritten without being freed. - * - * On success the caller is responsible for calling aws_py_metrics_clean_up() - * to release any memory allocated here (metadata_entries). - * - */ -bool aws_py_metrics_parse(PyObject *metrics_py, struct aws_mqtt_iot_metrics *out_metrics); - -/** - * Clean up resources allocated by aws_py_metrics_parse(). - */ -void aws_py_metrics_clean_up(struct aws_mqtt_iot_metrics *metrics); - PyObject *aws_py_mqtt5_client_new(PyObject *self, PyObject *args); PyObject *aws_py_mqtt5_client_start(PyObject *self, PyObject *args); PyObject *aws_py_mqtt5_client_stop(PyObject *self, PyObject *args); diff --git a/source/mqtt_client_connection.c b/source/mqtt_client_connection.c index ab5f4e2ae..d9dcbac6e 100644 --- a/source/mqtt_client_connection.c +++ b/source/mqtt_client_connection.c @@ -445,18 +445,28 @@ static bool s_set_metrics(struct aws_mqtt_client_connection *connection, PyObjec return false; } - struct aws_mqtt_iot_metrics metrics_tmp; - if (!aws_py_metrics_parse(metrics, &metrics_tmp)) { - return false; + bool success = false; + + PyObject *library_name_py = PyObject_GetAttrString(metrics, "library_name"); + struct aws_byte_cursor library_name = aws_byte_cursor_from_pyunicode(library_name_py); + if (!library_name.ptr) { + PyErr_SetString(PyExc_TypeError, "metrics.library_name must be str type"); + goto done; } - bool success = true; - if (aws_mqtt_client_connection_set_metrics(connection, &metrics_tmp)) { + struct aws_mqtt_iot_metrics metrics_struct = { + .library_name = library_name, + }; + + if (aws_mqtt_client_connection_set_metrics(connection, &metrics_struct)) { PyErr_SetAwsLastError(); - success = false; + goto done; } - aws_py_metrics_clean_up(&metrics_tmp); + success = true; + +done: + Py_DECREF(library_name_py); return success; } diff --git a/test/test_aws_iot_metrics.py b/test/test_aws_iot_metrics.py deleted file mode 100644 index 7b59615c4..000000000 --- a/test/test_aws_iot_metrics.py +++ /dev/null @@ -1,423 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0. - -import sys -import unittest -from test import NativeResourceTest -from awscrt.aws_iot_metrics import ( - AWSIoTMetrics, - IoTMetricsMetadata, - _MetricsFeatureId, - _protocol_version_metrics_value, - _socket_implementation_metrics_value, - _http_proxy_type_metrics_value, - IOT_SDK_METRICS_FEATURE_VERSION, - _get_encoded_feature_list, - _get_encoded_feature_list_mqtt3, - _merge_feature_lists, - _create_metrics, -) -from awscrt.mqtt5 import ( - ClientOptions, - ExponentialBackoffJitterMode, - ClientSessionBehaviorType, - ClientOperationQueueBehaviorType, - OutboundTopicAliasBehaviorType, - InboundTopicAliasBehaviorType, - TopicAliasingOptions, -) -from awscrt.io import ClientTlsContext, TlsContextOptions, TlsConnectionOptions, TlsVersion, TlsCipherPref -from awscrt.http import HttpProxyOptions - - -def _expected_socket_value(): - return _socket_implementation_metrics_value() - - -class TestMinimalOptionsEncoding(NativeResourceTest): - """Test encoding with minimal/default options.""" - - def test_mqtt5_minimal(self): - """MQTT5 with all defaults should only have protocol version and socket.""" - options = ClientOptions(host_name="localhost", port=8883) - - result = _get_encoded_feature_list(options) - - self.assertIn(f"{_MetricsFeatureId.PROTOCOL_VERSION.value}/{_protocol_version_metrics_value('MQTT5')}", result) - self.assertIn(f"{_MetricsFeatureId.SOCKET_IMPLEMENTATION.value}/{_expected_socket_value()}", result) - parts = result.split(",") - self.assertEqual(2, len(parts)) - - def test_mqtt3_minimal(self): - """MQTT3 with no proxy and no TLS should only have protocol version and socket.""" - result = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=None) - - self.assertIn(f"{_MetricsFeatureId.PROTOCOL_VERSION.value}/{_protocol_version_metrics_value('MQTT311')}", result) - self.assertIn(f"{_MetricsFeatureId.SOCKET_IMPLEMENTATION.value}/{_expected_socket_value()}", result) - parts = result.split(",") - self.assertEqual(2, len(parts)) - - def test_default_enum_values_omitted(self): - """DEFAULT enum values should not appear in the encoded list.""" - options = ClientOptions( - host_name="localhost", - port=8883, - retry_jitter_mode=ExponentialBackoffJitterMode.DEFAULT, - session_behavior=ClientSessionBehaviorType.DEFAULT, - offline_queue_behavior=ClientOperationQueueBehaviorType.DEFAULT, - ) - - result = _get_encoded_feature_list(options) - - self.assertNotIn(f"{_MetricsFeatureId.RETRY_JITTER_MODE.value}/", result) - self.assertNotIn(f"{_MetricsFeatureId.SESSION_BEHAVIOR.value}/", result) - self.assertNotIn(f"{_MetricsFeatureId.OFFLINE_QUEUE_BEHAVIOR.value}/", result) - - -class TestOptionsWithMultipleNonDefaultFeaturesEncoding(NativeResourceTest): - """Test encoding with multiple explicitly-set features.""" - - def test_all_mqtt5_features_set(self): - """MQTT5 with all features explicitly set to non-default values.""" - proxy = HttpProxyOptions(host_name="proxy.example.com", port=8080) - - tls_options = TlsContextOptions() - tls_options.min_tls_ver = TlsVersion.TLSv1_2 - cipher_pref = TlsCipherPref.PQ_DEFAULT - if cipher_pref.is_supported(): - tls_options.cipher_pref = cipher_pref - tls_ctx = ClientTlsContext(tls_options) - - options = ClientOptions( - host_name="localhost", - port=8883, - retry_jitter_mode=ExponentialBackoffJitterMode.FULL, - session_behavior=ClientSessionBehaviorType.CLEAN, - offline_queue_behavior=ClientOperationQueueBehaviorType.FAIL_ALL_ON_DISCONNECT, - topic_aliasing_options=TopicAliasingOptions( - outbound_behavior=OutboundTopicAliasBehaviorType.LRU, - inbound_behavior=InboundTopicAliasBehaviorType.ENABLED, - ), - http_proxy_options=proxy, - tls_ctx=tls_ctx, - ) - - result = _get_encoded_feature_list(options) - - self.assertIn("A/B", result) # FULL - self.assertIn("B/A", result) # CLEAN - self.assertIn("C/C", result) # FAIL_ALL - self.assertIn("D/B", result) # LRU - self.assertIn("E/A", result) # ENABLED - self.assertIn("F/5", result) # MQTT5 - self.assertIn("H/A", result) # HTTP proxy (no TLS on proxy) - self.assertIn("K/D", result) # TLSv1_2 - if cipher_pref.is_supported(): - self.assertIn("J/B", result) # PQ_DEFAULT - - def test_alternate_values(self): - """MQTT5 with alternate non-default values.""" - tls_options = TlsContextOptions() - tls_options.min_tls_ver = TlsVersion.TLSv1_3 - cipher_pref = TlsCipherPref.PQ_TLSv1_0_2021_05 - if cipher_pref.is_supported(): - tls_options.cipher_pref = cipher_pref - tls_ctx = ClientTlsContext(tls_options) - - proxy_tls_ctx = ClientTlsContext(TlsContextOptions()) - proxy_tls_conn_options = TlsConnectionOptions(proxy_tls_ctx) - proxy = HttpProxyOptions( - host_name="proxy.example.com", port=443, - tls_connection_options=proxy_tls_conn_options) - - options = ClientOptions( - host_name="localhost", - port=8883, - retry_jitter_mode=ExponentialBackoffJitterMode.DECORRELATED, - session_behavior=ClientSessionBehaviorType.REJOIN_ALWAYS, - offline_queue_behavior=ClientOperationQueueBehaviorType.FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, - topic_aliasing_options=TopicAliasingOptions( - outbound_behavior=OutboundTopicAliasBehaviorType.MANUAL, - inbound_behavior=InboundTopicAliasBehaviorType.DISABLED, - ), - http_proxy_options=proxy, - tls_ctx=tls_ctx, - ) - - result = _get_encoded_feature_list(options) - - self.assertIn("A/C", result) # DECORRELATED - self.assertIn("B/C", result) # REJOIN_ALWAYS - self.assertIn("C/A", result) # FAIL_NON_QOS1 - self.assertIn("D/A", result) # MANUAL - self.assertIn("E/B", result) # DISABLED - self.assertIn("F/5", result) # MQTT5 - self.assertIn("H/B", result) # HTTPS proxy - self.assertIn("K/E", result) # TLSv1_3 - if cipher_pref.is_supported(): - self.assertIn("J/A", result) # PQ_TLSv1_0_2021_05 - - def test_mqtt3_with_proxy_and_tls(self): - """MQTT3 with HTTPS proxy and TLS context.""" - tls_options = TlsContextOptions() - tls_options.min_tls_ver = TlsVersion.TLSv1_2 - cipher_pref = TlsCipherPref.TLSv1_2_2025_07 - if cipher_pref.is_supported(): - tls_options.cipher_pref = cipher_pref - tls_ctx = ClientTlsContext(tls_options) - - proxy_tls_ctx = ClientTlsContext(TlsContextOptions()) - proxy_tls_conn_options = TlsConnectionOptions(proxy_tls_ctx) - proxy = HttpProxyOptions( - host_name="proxy.example.com", port=443, - tls_connection_options=proxy_tls_conn_options) - - result = _get_encoded_feature_list_mqtt3(proxy_options=proxy, tls_ctx=tls_ctx) - - self.assertIn("F/3", result) - self.assertIn("H/B", result) # HTTPS - self.assertIn("K/D", result) # TLSv1_2 - if cipher_pref.is_supported(): - self.assertIn("J/C", result) # TLSv1_2_2025_07 - - -class TestMergeFeatureLists(NativeResourceTest): - """Test feature list merging logic.""" - - def test_user_overrides_crt(self): - """User features take precedence over CRT features for same feature ID.""" - result = _merge_feature_lists("A/B,F/5", "A/C") - # User's A/C overwrites CRT's A/B - self.assertEqual("A/C,F/5", result) - - def test_user_overrides_multiple_crt_features(self): - """User can override multiple CRT features at once.""" - result = _merge_feature_lists("A/B,F/5,G/A,K/D", "A/C,F/3,K/E") - # User overrides A, F, K; G remains from CRT; insertion order preserved - self.assertEqual("A/C,F/3,G/A,K/E", result) - - def test_empty_user_features(self): - result = _merge_feature_lists("F/5,G/A", "") - self.assertEqual("F/5,G/A", result) - - def test_empty_crt_features(self): - result = _merge_feature_lists("", "A/B") - self.assertEqual("A/B", result) - - def test_insertion_order_preserved(self): - result = _merge_feature_lists("G/A,F/5", "A/B") - self.assertEqual("G/A,F/5,A/B", result) - - def test_disjoint_features(self): - result = _merge_feature_lists("F/5,G/A", "I/A,K/D") - self.assertEqual("F/5,G/A,I/A,K/D", result) - - def test_both_empty(self): - result = _merge_feature_lists("", "") - self.assertEqual("", result) - - -class TestCreateMetricsWithDefaultOptions(NativeResourceTest): - """Test create_metrics with no user metrics or default user metrics.""" - - def test_none_user_metrics(self): - result = _create_metrics(None, "F/5,G/A") - - self.assertEqual("IoTDeviceSDK/Python", result.library_name) - metadata_dict = {e.key: e.value for e in result.metadata_entries} - self.assertIn("CRTVersion", metadata_dict) - self.assertEqual("F/5,G/A", metadata_dict["IoTSDKFeature"]) - self.assertEqual(str(IOT_SDK_METRICS_FEATURE_VERSION), metadata_dict["IoTSDKMetricsVersion"]) - - def test_empty_user_metrics(self): - user = AWSIoTMetrics() - result = _create_metrics(user, "F/5,G/A") - - self.assertEqual("IoTDeviceSDK/Python", result.library_name) - metadata_dict = {e.key: e.value for e in result.metadata_entries} - self.assertEqual("F/5,G/A", metadata_dict["IoTSDKFeature"]) - - def test_version_always_set(self): - result = _create_metrics(None, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - self.assertEqual(str(IOT_SDK_METRICS_FEATURE_VERSION), metadata_dict["IoTSDKMetricsVersion"]) - - -class TestCreateMetricsWithUserFeaturesMerged(NativeResourceTest): - """Test that user features are merged when version matches.""" - - def test_user_feature_added(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="1"), - IoTMetricsMetadata(key="IoTSDKFeature", value="I/A"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertIn("I/A", metadata_dict["IoTSDKFeature"]) - self.assertIn("F/5", metadata_dict["IoTSDKFeature"]) - self.assertIn("G/A", metadata_dict["IoTSDKFeature"]) - - def test_user_feature_overrides_crt(self): - """User feature takes precedence over CRT feature for same ID.""" - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="1"), - IoTMetricsMetadata(key="IoTSDKFeature", value="F/3,I/B"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertIn("F/3", metadata_dict["IoTSDKFeature"]) - self.assertNotIn("F/5", metadata_dict["IoTSDKFeature"]) - self.assertIn("I/B", metadata_dict["IoTSDKFeature"]) - - def test_empty_user_feature_string(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="1"), - IoTMetricsMetadata(key="IoTSDKFeature", value=""), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertEqual("F/5,G/A", metadata_dict["IoTSDKFeature"]) - - -class TestCreateMetricsWithVersionMismatch(NativeResourceTest): - """Test that user features are ignored when version doesn't match.""" - - def test_higher_version(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="99"), - IoTMetricsMetadata(key="IoTSDKFeature", value="I/A"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertNotIn("I/A", metadata_dict["IoTSDKFeature"]) - self.assertIn("F/5", metadata_dict["IoTSDKFeature"]) - - def test_lower_version(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="0"), - IoTMetricsMetadata(key="IoTSDKFeature", value="I/A"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertNotIn("I/A", metadata_dict["IoTSDKFeature"]) - - def test_non_numeric_version(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKMetricsVersion", value="abc"), - IoTMetricsMetadata(key="IoTSDKFeature", value="I/A"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertNotIn("I/A", metadata_dict["IoTSDKFeature"]) - - def test_no_version_set(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKFeature", value="I/A"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertNotIn("I/A", metadata_dict["IoTSDKFeature"]) - - -class TestCreateMetricsCRTVersionNotModifiable(NativeResourceTest): - """Test that CRTVersion cannot be overridden by user.""" - - def test_user_cannot_override_crt_version(self): - from awscrt import __version__ as actual_version - - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="CRTVersion", value="fake_version"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertEqual(actual_version, metadata_dict["CRTVersion"]) - self.assertNotEqual("fake_version", metadata_dict["CRTVersion"]) - - def test_empty_crt_version_overridden(self): - from awscrt import __version__ as actual_version - - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="CRTVersion", value=""), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertEqual(actual_version, metadata_dict["CRTVersion"]) - - -class TestCreateMetricsPreservesOtherUserMetadata(NativeResourceTest): - """Test that non-reserved user metadata keys are preserved.""" - - def test_sdk_version_preserved(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="IoTSDKVersion", value="2.0.0"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertEqual("2.0.0", metadata_dict["IoTSDKVersion"]) - - def test_custom_key_preserved(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="CustomKey", value="custom_value"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertEqual("custom_value", metadata_dict["CustomKey"]) - - def test_mixed_metadata(self): - user = AWSIoTMetrics() - user.metadata_entries = [ - IoTMetricsMetadata(key="CRTVersion", value="should_be_ignored"), - IoTMetricsMetadata(key="IoTSDKVersion", value="2.0.0"), - IoTMetricsMetadata(key="CustomKey", value="val"), - ] - - result = _create_metrics(user, "F/5,G/A") - metadata_dict = {e.key: e.value for e in result.metadata_entries} - - self.assertNotEqual("should_be_ignored", metadata_dict["CRTVersion"]) - self.assertEqual("2.0.0", metadata_dict["IoTSDKVersion"]) - self.assertEqual("val", metadata_dict["CustomKey"]) - - def test_custom_library_name(self): - user = AWSIoTMetrics(library_name="MyCustomSDK/1.0") - - result = _create_metrics(user, "F/5,G/A") - - self.assertEqual("MyCustomSDK/1.0", result.library_name) - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_mqtt.py b/test/test_mqtt.py index cf86369bb..af8acb6d9 100644 --- a/test/test_mqtt.py +++ b/test/test_mqtt.py @@ -630,7 +630,7 @@ def _test_mqtt311_direct_connect_basic_auth(self): port=input_port, username=input_username, password=input_password, - disable_metrics=True) + enable_metrics=False) connection.connect().result(TIMEOUT) connection.disconnect().result(TIMEOUT) @@ -762,7 +762,7 @@ def sign_function(transform_args, **kwargs): password=input_password, use_websockets=True, websocket_handshake_transform=sign_function, - disable_metrics=True) + enable_metrics=False) connection.connect().result(TIMEOUT) connection.disconnect().result(TIMEOUT) @@ -853,7 +853,7 @@ def _test_mqtt311_direct_connect_basic_auth_metrics_enabled(self): bootstrap = ClientBootstrap(elg, resolver) client = Client(bootstrap, None) - # Create connection with disable_metrics=False explicitly + # Create connection with enable_metrics=True explicitly # This should fail because metrics appends to username, corrupting basic auth connection = Connection( client=client, @@ -862,7 +862,7 @@ def _test_mqtt311_direct_connect_basic_auth_metrics_enabled(self): port=input_port, username=input_username, password=input_password, - disable_metrics=False) + enable_metrics=True) # Connection should fail because metrics corrupts the username for basic auth exception_occurred = False diff --git a/test/test_mqtt5.py b/test/test_mqtt5.py index 216fe8b51..1e6dbd080 100644 --- a/test/test_mqtt5.py +++ b/test/test_mqtt5.py @@ -218,7 +218,7 @@ def _test_direct_connect_minimum(self): client_options = mqtt5.ClientOptions( host_name=input_host_name, - disable_metrics=True + enable_metrics=False ) callbacks = Mqtt5TestCallbacks() client = self._create_client(client_options=client_options, callbacks=callbacks) @@ -245,7 +245,7 @@ def _test_direct_connect_basic_auth(self): host_name=input_host_name, port=input_port, connect_options=connect_options, - disable_metrics=True + enable_metrics=False ) callbacks = Mqtt5TestCallbacks() client = self._create_client(client_options=client_options, callbacks=callbacks) @@ -464,7 +464,7 @@ def _test_websocket_connect_basic_auth(self): host_name=input_host_name, port=input_port, connect_options=connect_options, - disable_metrics=True + enable_metrics=False ) callbacks = Mqtt5TestCallbacks() client_options.websocket_handshake_transform = callbacks.ws_handshake_transform @@ -2249,13 +2249,13 @@ def _test_direct_connect_basic_auth_metrics_enabled(self): host_name=input_host_name, port=input_port, connect_options=connect_options, - disable_metrics=False + enable_metrics=True ) callbacks = Mqtt5TestCallbacks() client = self._create_client(client_options=client_options, callbacks=callbacks) # Verify metrics are enabled - self.assertFalse(client_options.disable_metrics) + self.assertTrue(client_options.enable_metrics) client.start() # Connection should fail because metrics corrupts the username for basic auth