Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions awsiot/_iot_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.

"""
Private IoT SDK metrics module.

Provides SDK-level metadata (version info) to pass to the CRT layer.
The CRT handles all feature detection (certificate source, TLS settings, etc.)
and embeds the combined metrics in the MQTT CONNECT packet username field.

"""

from awscrt.aws_iot_metrics import AWSIoTMetrics, IoTMetricsMetadata

_SDK_LIBRARY_NAME = "IoTDeviceSDK/Python"

# The current version of the IoT SDK metrics format.
# This must match the version expected by CRT layer.
_IOT_SDK_METRICS_VERSION = 1


def _get_sdk_version():
"""
Return the installed ``awsiotsdk`` package version string.

Falls back to ``"dev"`` if the package metadata is unavailable (e.g. when
running from a source checkout without installing).

Returns:
str: A version string like ``1.32.0`` or ``"dev"``.
"""
try:
import importlib.metadata
return importlib.metadata.version("awsiotsdk")
except Exception:
return "dev"


def _build_sdk_metrics():
"""
Build the SDK-level :class:`~awscrt.aws_iot_metrics.AWSIoTMetrics` payload
that is passed down to the CRT layer.

The returned object carries SDK identity and the metrics format version
via two metadata entries:

- ``IoTSDKVersion``: the installed ``awsiotsdk`` package version, used
to identify the SDK release on the server side.
- ``IoTSDKMetricsVersion``: the metrics format version this SDK supports.
The CRT only merges SDK-supplied features when this value matches the
version it expects, so bumping :data:`_IOT_SDK_METRICS_VERSION` should
be done in lockstep with CRT changes.

The CRT layer is responsible for detecting connection-level features
(protocol version, certificate source, TLS settings, proxy type, etc.)
and appending them to the metadata before embedding the result in the
MQTT CONNECT packet username field.

Returns:
AWSIoTMetrics: A populated metrics object ready to attach to an
MQTT5 client or MQTT3 connection configuration.
"""
return AWSIoTMetrics(
library_name=_SDK_LIBRARY_NAME,
metadata_entries=[
IoTMetricsMetadata(key="IoTSDKVersion", value=_get_sdk_version()),
IoTMetricsMetadata(key="IoTSDKMetricsVersion", value=str(_IOT_SDK_METRICS_VERSION)),
]
)
42 changes: 9 additions & 33 deletions awsiot/mqtt5_client_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@

**cipher_pref** (:class:`awscrt.io.TlsCipherPref`): Cipher preference to use for TLS connection. Default is `TlsCipherPref.DEFAULT`.

**enable_metrics_collection** (`bool`): Whether to send the SDK version number in the CONNECT packet.
Default is True.
**disable_metrics** (`bool`): Disable IoT SDK metrics in the CONNECT packet username field.
Defaults to False (metrics enabled).


"""
Expand All @@ -184,6 +184,8 @@
import awscrt.mqtt5
import urllib.parse

from awsiot._iot_metrics import _build_sdk_metrics


DEFAULT_WEBSOCKET_MQTT_PORT = 443
DEFAULT_DIRECT_MQTT_PORT = 8883
Expand All @@ -210,35 +212,6 @@ def _get(kwargs, name, default=None):
return val


_metrics_str = None


def _get_metrics_str(current_username=""):
global _metrics_str

username_has_query = False
if current_username.find("?") != -1:
username_has_query = True

if _metrics_str is None:
try:
import importlib.metadata
try:
version = importlib.metadata.version("awsiotsdk")
_metrics_str = "SDK=PythonV2&Version={}".format(version)
except importlib.metadata.PackageNotFoundError:
_metrics_str = "SDK=PythonV2&Version=dev"
except BaseException:
_metrics_str = ""

if not _metrics_str == "":
if username_has_query:
return "&" + _metrics_str
else:
return "?" + _metrics_str
else:
return ""


def _builder(
tls_ctx_options,
Expand All @@ -251,8 +224,6 @@ def _builder(
assert isinstance(cipher_pref, awscrt.io.TlsCipherPref)

username = _get(kwargs, 'username', '')
if _get(kwargs, 'enable_metrics_collection', True):
username += _get_metrics_str(username)

client_options = _get(kwargs, 'client_options')
if client_options is None:
Expand Down Expand Up @@ -364,6 +335,11 @@ def _builder(

tls_ctx = awscrt.io.ClientTlsContext(tls_ctx_options)
client_options.tls_ctx = tls_ctx

# Set SDK metrics for the CRT layer to embed in the CONNECT packet username
disable_metrics = _get(kwargs, 'disable_metrics', False)
client_options.disable_metrics = disable_metrics
client_options.metrics = None if disable_metrics else _build_sdk_metrics()
client = awscrt.mqtt5.Client(client_options=client_options)

return client
Expand Down
43 changes: 10 additions & 33 deletions awsiot/mqtt_connection_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@

**cipher_pref** (:class:`awscrt.io.TlsCipherPref`): Cipher preference to use for TLS connection. Default is `TlsCipherPref.DEFAULT`.

**enable_metrics_collection** (`bool`): Whether to send the SDK version number in the CONNECT packet.
Default is True.
**disable_metrics** (`bool`): Disable IoT SDK metrics in the CONNECT packet username field.
Default is False (metrics enabled).

**http_proxy_options** (:class: 'awscrt.http.HttpProxyOptions'): HTTP proxy options to use
"""
Expand All @@ -127,6 +127,8 @@
import awscrt.mqtt
import urllib.parse

from awsiot._iot_metrics import _build_sdk_metrics


def _check_required_kwargs(**kwargs):
for required in ['endpoint', 'client_id']:
Expand All @@ -148,35 +150,6 @@ def _get(kwargs, name, default=None):
return val


_metrics_str = None


def _get_metrics_str(current_username=""):
global _metrics_str

username_has_query = False
if current_username.find("?") != -1:
username_has_query = True

if _metrics_str is None:
try:
import importlib.metadata
try:
version = importlib.metadata.version("awsiotsdk")
_metrics_str = "SDK=PythonV2&Version={}".format(version)
except importlib.metadata.PackageNotFoundError:
_metrics_str = "SDK=PythonV2&Version=dev"
except BaseException:
_metrics_str = ""

if not _metrics_str == "":
if username_has_query:
return "&" + _metrics_str
else:
return "?" + _metrics_str
else:
return ""


def _builder(
tls_ctx_options,
Expand Down Expand Up @@ -225,12 +198,14 @@ def _builder(
_get(kwargs, 'tcp_keep_alive_max_probes', _get(kwargs, 'tcp_keepalive_max_probes', 0))

username = _get(kwargs, 'username', '')
if _get(kwargs, 'enable_metrics_collection', True):
username += _get_metrics_str(username)

if username == "":
username = None

# Set SDK metrics for the CRT layer to embed in the CONNECT packet username
disable_metrics = _get(kwargs, 'disable_metrics', False)
metrics = None if disable_metrics else _build_sdk_metrics()

client_bootstrap = _get(kwargs, 'client_bootstrap')
if client_bootstrap is None:
client_bootstrap = awscrt.io.ClientBootstrap.get_or_create_static_default()
Expand Down Expand Up @@ -262,6 +237,8 @@ def _builder(
on_connection_success=_get(kwargs, 'on_connection_success'),
on_connection_failure=_get(kwargs, 'on_connection_failure'),
on_connection_closed=_get(kwargs, 'on_connection_closed'),
disable_metrics=disable_metrics,
metrics=metrics
)


Expand Down
Loading
Loading