From f8e30e1c3ee5fe899554855df8628761ffc72cbc Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 4 Jun 2026 14:54:58 -0700 Subject: [PATCH 01/16] features added --- .../crt/internal/IoTDeviceSDKMetrics.java | 16 +- .../awssdk/crt/internal/IoTMetricEncoder.java | 455 ++++++++++++++++++ .../crt/internal/IoTMetricsMetadata.java | 24 + .../amazon/awssdk/crt/io/TlsContext.java | 20 + .../crt/mqtt5/TopicAliasingOptions.java | 20 +- src/native/java_class_ids.c | 23 + src/native/java_class_ids.h | 9 + 7 files changed, 563 insertions(+), 4 deletions(-) create mode 100644 src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java create mode 100644 src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java index 4fbe62088..122cca206 100644 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java @@ -4,18 +4,28 @@ */ package software.amazon.awssdk.crt.internal; +import java.util.List; + /** * @internal - * IoT Device SDK Metrics Structure. Not for external usage. + * IoT Device SDK Metrics Structure. Not for external usage. */ public class IoTDeviceSDKMetrics { private String libraryName; + private List metadataEntries; public IoTDeviceSDKMetrics() { this.libraryName = "IoTDeviceSDK/Java"; } - public String getLibraryName() { - return libraryName; + public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { + this.libraryName = libraryName; + this.metadataEntries = metadataEntries; } + + public String getLibraryName() { return libraryName; } + public void setLibraryName(String libraryName) { this.libraryName = libraryName; } + + public List getMetadataEntries() { return metadataEntries; } + public void setMetadataEntries(List metadataEntries) { this.metadataEntries = metadataEntries; } } diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java new file mode 100644 index 000000000..0773811fc --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java @@ -0,0 +1,455 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.awssdk.crt.internal; + +import software.amazon.awssdk.crt.CRT; +import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; +import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions; +import software.amazon.awssdk.crt.utils.PackageInfo; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @internal + * Encodes IoT SDK metrics features and creates final metrics object + * + */ + +public class IoTMetricEncoder{ + + /* + 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. + */ + + private static final String RETRY_JITTER_MODE = "A"; + private static final String SESSION_BEHAVIOR = "B"; + private static final String OFFLINE_QUEUE_BEHAVIOR = "C"; + private static final String OUTBOUND_TOPIC_ALIAS_BEHAVIOR = "D"; + private static final String INBOUND_TOPIC_ALIAS_BEHAVIOR = "E"; + private static final String PROTOCOL_VERSION = "F"; + private static final String SOCKET_IMPLEMENTATION = "G"; + private static final String HTTP_PROXY_TYPE = "H"; + private static final String CERTIFICATE_SOURCE = "I"; + private static final String TLS_CIPHER_PREFERENCE = "J"; + private static final String MINIMUM_TLS_VERSION = "K"; + + // Metrics Version Constant + public static final int IOT_SDK_METRICS_FEATURE_VERSION = 1; + + // value mapping methods + + /** + * Map protocol version to its single-character metrics value. + * Mapping: MQTT311->3, MQTT5->5. + * + * @param isMqtt5 true if the client is using MQTT5, false for MQTT311 + * @return the single-character metrics value for the protocol version + */ + private static String protocolVersionValue(boolean isMqtt5) { + return isMqtt5 ? "5" : "3"; + } + + /** + * Detect the socket implementation and return its single-character metrics value. + * Mapping: Windows (WINSOCK)->B, all other platforms (POSIX)->A. + * + * @return the single-character metrics value for the socket implementation + * @throws CRT.UnknownPlatformException if the running platform cannot be identified + */ + private static String socketImplementationValue() throws CRT.UnknownPlatformException { + return "windows".equals(CRT.getOSIdentifier()) ? "B" : "A"; + } + + /** + * Map proxy options to its single-character metrics value for proxy type. + * Mapping: HTTPS (has tls connection option)->B, HTTP->A. + * + * @param proxyUsesTls + * @return the single-character metrics value for the proxy options + */ + private static String httpProxyTypeValue(boolean proxyUsesTls){ + return proxyUsesTls? "B" : "A"; + } + + /** + * Maps ExponentialBackoffJitterMode value to its metrics character. + * + * @param value the int value from JitterMode.getValue() + * @return single-character metrics value, or null if DEFAULT (should be omitted) + */ + private static String retryJitterModeValue(int value) { + switch (value) { + case 1: return "A"; // None + case 2: return "B"; // Full + case 3: return "C"; // Decorrelated + default: return null; + } + } + + /** + * Maps ClientSessionBehavior value to its metrics character. + * + * @param value the int value from ClientSessionBehavior.getValue() + * @return single-character metrics value, or null if DEFAULT (should be omitted) + */ + private static String sessionBehaviorValue(int value) { + switch (value) { + case 1: return "A"; // CLEAN + case 2: return "B"; // REJOIN_POST_SUCCESS + case 3: return "C"; // REJOIN_ALWAYS + default: return null; + } + } + + /** + * Maps ClientOfflineQueueBehavior value to its metrics character. + * + * @param value the int value from ClientOfflineQueueBehavior.getValue() + * @return single-character metrics value, or null if DEFAULT (should be omitted) + */ + private static String offlineQueueBehaviorValue(int value) { + switch (value) { + case 1: return "A"; // FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT + case 2: return "B"; // FAIL_QOS0_PUBLISH_ON_DISCONNECT + case 3: return "C"; // FAIL_ALL_ON_DISCONNECT + default: return null; + } + } + + /** + * Maps OutboundTopicAliasBehaviorType value to its metrics character. + * + * @param value the int value from OutboundTopicAliasBehaviorType.getValue() + * @return single-character metrics value, or null if Default (should be omitted) + */ + private static String outboundTopicAliasBehaviorValue(int value) { + switch (value) { + case 1: return "A"; // Manual + case 2: return "B"; // LRU + case 3: return "C"; // Disabled + default: return null; + } + } + + /** + * Maps InboundTopicAliasBehaviorType value to its metrics character. + * + * @param value the int value from InboundTopicAliasBehaviorType.getValue() + * @return single-character metrics value, or null if Default (should be omitted) + */ + private static String inboundTopicAliasBehaviorValue(int value) { + switch (value) { + case 1: return "A"; // Enabled + case 2: return "B"; // Disabled + default: return null; + } + } + + /** + * Maps TlsCipherPreference value to its metrics character. + * + * @param value the int value from TlsCipherPreference.getValue() + * @return single-character metrics value, or null if SYSTEM_DEFAULT (should be omitted) + */ + private static String tlsCipherPreferenceValue(int value) { + switch (value) { + case 6: return "A"; // TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05 + case 8: return "B"; // TLS_CIPHER_PQ_DEFAULT + case 9: return "C"; // TLS_CIPHER_PREF_TLSv1_2_2025 + default: return null; + } + } + + /** + * Maps TlsVersions value to its metrics character. + * + * @param value the int value from TlsVersions.getValue() + * @return single-character metrics value, or null if SYS_DEFAULTS (should be omitted) + */ + private static String minimumTlsVersionValue(int value) { + switch (value) { + case 0: return "A"; // SSLv3 + case 1: return "B"; // TLSv1 + case 2: return "C"; // TLSv1_1 + case 3: return "D"; // TLSv1_2 + case 4: return "E"; // TLSv1_3 + default: return null; + } + } + + // Feature List Encoding + + /** + * Generates the encoded feature list string for metrics from MQTT5 client options. + * + * 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, protocol=MQTT5, socket=POSIX. + * + * MQTT5 connections always include: + * - F (protocol_version): set to MQTT5 + * - G (socket_implementation): detected from platform + * + * Conditionally includes (only when not DEFAULT): + * A (retry_jitter_mode), B (session_behavior), C (offline_queue_behavior), + * D (outbound_topic_alias), E (inbound_topic_alias), H (http_proxy_type), + * J (tls_cipher_preference), K (minimum_tls_version) + * + * Feature I (certificate_source) is set at the IoT SDK level, not here. + * + * @param clientOptions MQTT5 client options containing connection configuration + * @return the encoded feature list string + */ + public static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions) { + List features = new ArrayList<>(); + + // A: retry_jitter_mode + if (clientOptions.getRetryJitterMode() != null) { + String val = retryJitterModeValue(clientOptions.getRetryJitterMode().getValue()); + if (val != null) features.add(RETRY_JITTER_MODE + "/" + val); + } + + // B: session_behavior + if (clientOptions.getSessionBehavior() != null) { + String val = sessionBehaviorValue(clientOptions.getSessionBehavior().getValue()); + if (val != null) features.add(SESSION_BEHAVIOR + "/" + val); + } + + // C: offline_queue_behavior + if (clientOptions.getOfflineQueueBehavior() != null) { + String val = offlineQueueBehaviorValue(clientOptions.getOfflineQueueBehavior().getValue()); + if (val != null) features.add(OFFLINE_QUEUE_BEHAVIOR + "/" + val); + } + + // D & E: topic aliasing + TopicAliasingOptions topicAliasing = clientOptions.getTopicAliasingOptions(); + if (topicAliasing != null) { + if (topicAliasing.getOutboundBehavior() != null) { + String val = outboundTopicAliasBehaviorValue(topicAliasing.getOutboundBehavior().getValue()); + if (val != null) features.add(OUTBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); + } + if (topicAliasing.getInboundBehavior() != null) { + String val = inboundTopicAliasBehaviorValue(topicAliasing.getInboundBehavior().getValue()); + if (val != null) features.add(INBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); + } + } + + // F: protocol_version - always MQTT5 + features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(true)); + + // G: socket_implementation - always present + features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); + + // H: http_proxy_type + HttpProxyOptions proxyOptions = clientOptions.getHttpProxyOptions(); + if (proxyOptions != null) { + boolean proxyUsesTls = proxyOptions.getTlsContext() != null; + features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); + } + + // I: certificate_source - set at IoT SDK level, not here + + // J & K: TLS properties + TlsContext tlsCtx = clientOptions.getTlsContext(); + if (tlsCtx != null) { + String val = tlsCipherPreferenceValue(tlsCtx.getCipherPreference()); + if (val != null) features.add(TLS_CIPHER_PREFERENCE + "/" + val); + + String tlsVer = minimumTlsVersionValue(tlsCtx.getMinimumTlsVersion()); + if (tlsVer != null) features.add(MINIMUM_TLS_VERSION + "/" + tlsVer); + } + + return String.join(",", features); + } + + /** + * Generates the encoded feature list string for metrics from MQTT3 connection parameters. + * + * MQTT3 connections always include: + * - F (protocol_version): set to MQTT311 + * - G (socket_implementation): detected from platform + * + * Conditionally includes: + * H (http_proxy_type), J (tls_cipher_preference), K (minimum_tls_version) + * + * @param proxyOptions optional HTTP proxy options from the connection + * @param tlsCtx optional TLS context used by the connection + * @return the encoded feature list string + */ + public static String getEncodedFeatureListMqtt3(HttpProxyOptions proxyOptions, TlsContext tlsCtx) { + List features = new ArrayList<>(); + + // F: protocol_version - always MQTT311 + features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(false)); + + // G: socket_implementation + features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); + + // H: http_proxy_type + if (proxyOptions != null) { + boolean proxyUsesTls = proxyOptions.getTlsContext() != null; + features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); + } + + // J & K: TLS properties + if (tlsCtx != null) { + String val = tlsCipherPreferenceValue(tlsCtx.getCipherPreference()); + if (val != null) features.add(TLS_CIPHER_PREFERENCE + "/" + val); + + String tlsVer = minimumTlsVersionValue(tlsCtx.getMinimumTlsVersion()); + if (tlsVer != null) features.add(MINIMUM_TLS_VERSION + "/" + tlsVer); + } + + return String.join(",", features); + } + + // ======================== Feature List Merging ======================== + + /** + * Merges CRT-generated features with user-provided (IoT SDK) features. + * When both lists contain the same feature ID, the user-provided value + * takes precedence. Insertion order is preserved. + * + * @param crtFeatures CRT-generated feature list string (e.g., "F/5,G/A") + * @param userFeatures user-provided feature list from the IoT SDK (may be null or empty) + * @return the merged feature list string + */ + public static String mergeFeatureLists(String crtFeatures, String userFeatures) { + LinkedHashMap merged = new LinkedHashMap<>(); + parseFeatures(crtFeatures, merged); + parseFeatures(userFeatures, merged); + + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : merged.entrySet()) { + if (sb.length() > 0) sb.append(","); + sb.append(entry.getKey()).append("/").append(entry.getValue()); + } + return sb.toString(); + } + + /** + * Parses a feature string into key-value pairs in the given map. + * Later entries overwrite earlier ones with the same key. + * + * @param featureStr comma-separated feature string (e.g., "A/B,F/5") + * @param map target map to populate + */ + private static void parseFeatures(String featureStr, Map map) { + if (featureStr == null || featureStr.isEmpty()) return; + for (String pair : featureStr.split(",")) { + int slash = pair.indexOf('/'); + if (slash > 0) { + map.put(pair.substring(0, slash), pair.substring(slash + 1)); + } + } + } + + // ======================== Metrics Creation ======================== + + /** + * Creates the final IoTDeviceSDKMetrics object for an MQTT5 client. + * + * @param clientOptions MQTT5 client options containing connection configuration + * @param userMetrics optional metrics configuration from the IoT SDK (may be null) + * @return the final metrics object ready for JNI passing + */ + public static IoTDeviceSDKMetrics createMetricsMqtt5(Mqtt5ClientOptions clientOptions, IoTDeviceSDKMetrics userMetrics) { + String crtFeatureList = getEncodedFeatureListMqtt5(clientOptions); + return createMetrics(userMetrics, crtFeatureList); + } + + /** + * Creates the final IoTDeviceSDKMetrics object for an MQTT3 connection. + * + * @param userMetrics optional metrics configuration from the IoT SDK (may be null) + * @param proxyOptions optional HTTP proxy options from the connection + * @param tlsCtx optional TLS context used by the connection + * @return the final metrics object ready for JNI passing + */ + public static IoTDeviceSDKMetrics createMetricsMqtt3(IoTDeviceSDKMetrics userMetrics, HttpProxyOptions proxyOptions, TlsContext tlsCtx) { + String crtFeatureList = getEncodedFeatureListMqtt3(proxyOptions, tlsCtx); + return createMetrics(userMetrics, crtFeatureList); + } + + /** + * Metrics creation logic: + * 1. CRTVersion: auto-set from package version, not overridable by user + * 2. IoTSDKMetricsVersion: always set to current IOT_SDK_METRICS_FEATURE_VERSION + * 3. IoTSDKFeature: merged if user version matches, else CRT only + * 4. Other user metadata: passed through unchanged + * + * @param userMetrics optional metrics from IoT SDK (may be null) + * @param crtFeatureList encoded CRT feature list string + * @return the final metrics object + */ + private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics, String crtFeatureList) { + String libraryName = (userMetrics != null) ? userMetrics.getLibraryName() : "IoTDeviceSDK/Java"; + + // CRTVersion: not modifiable by user + String crtVersion = new PackageInfo().version.toString(); + LinkedHashMap metadata = new LinkedHashMap<>(); + metadata.put("CRTVersion", crtVersion); + + // Extract user metadata + String userMetricsVersion = null; + String userFeature = ""; + + if (userMetrics != null && userMetrics.getMetadataEntries() != null) { + for (IoTMetricsMetadata entry : userMetrics.getMetadataEntries()) { + if ("IoTSDKMetricsVersion".equals(entry.getKey())) { + userMetricsVersion = entry.getValue(); + } else if ("IoTSDKFeature".equals(entry.getKey())) { + userFeature = entry.getValue(); + } else if (!"CRTVersion".equals(entry.getKey())) { + metadata.put(entry.getKey(), entry.getValue()); + } + } + } + + // Merge features: if version matches, merge CRT + SDK; otherwise CRT only + if (userMetricsVersion != null && isNumeric(userMetricsVersion) + && Integer.parseInt(userMetricsVersion) == IOT_SDK_METRICS_FEATURE_VERSION) { + metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, userFeature)); + } else { + metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, "")); + } + + // Always set current metrics version + metadata.put("IoTSDKMetricsVersion", String.valueOf(IOT_SDK_METRICS_FEATURE_VERSION)); + + // Build final metrics object + List entries = new ArrayList<>(); + for (Map.Entry entry : metadata.entrySet()) { + entries.add(new IoTMetricsMetadata(entry.getKey(), entry.getValue())); + } + + return new IoTDeviceSDKMetrics(libraryName, entries); + } + + /** + * Checks if a string can be parsed as an integer. + * + * @param str the string to check + * @return true if the string is a valid integer, false otherwise + */ + private static boolean isNumeric(String str) { + try { + Integer.parseInt(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} + diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java new file mode 100644 index 000000000..19032cf65 --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java @@ -0,0 +1,24 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.awssdk.crt.internal; + +/** + * @internal + * 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. + */ +public class IoTMetricsMetadata { + private String key; + private String value; + + public IoTMetricsMetadata(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { return key; } + public String getValue() { return value; } +} diff --git a/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java b/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java index b9c3f8833..345a87587 100644 --- a/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java +++ b/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java @@ -13,6 +13,8 @@ */ public class TlsContext extends CrtResource { + private int cipherPreference; + private int minimumTlsVersion; /** * Creates a new Client TlsContext. There are significant native resources consumed to create a TlsContext, so most * applications will only need to create one and re-use it for all connections. @@ -22,6 +24,8 @@ public class TlsContext extends CrtResource { */ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); + this.cipherPreference = options.tlsCipherPreference.getValue(); + this.minimumTlsVersion = options.minTlsVersion.getValue(); } /** @@ -31,6 +35,8 @@ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { public TlsContext() throws CrtRuntimeException { try (TlsContextOptions options = TlsContextOptions.createDefaultClient()) { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); + this.cipherPreference = options.tlsCipherPreference.getValue(); + this.minimumTlsVersion = options.minTlsVersion.getValue(); } } @@ -54,4 +60,18 @@ protected void releaseNativeHandle() { protected static native long tlsContextNew(long options) throws CrtRuntimeException; private static native void tlsContextDestroy(long elg); + + /** + * Returns the TLS cipher preference native value configured on this context. + * + * @return native int value of the TlsCipherPreference + */ + public int getCipherPreference() { return cipherPreference; } + + /** + * Returns the minimum TLS version native value configured on this context. + * + * @return native int value of the TlsVersions + */ + public int getMinimumTlsVersion() { return minimumTlsVersion; } }; diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt5/TopicAliasingOptions.java b/src/main/java/software/amazon/awssdk/crt/mqtt5/TopicAliasingOptions.java index 73b85b00f..4f6a8e79e 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt5/TopicAliasingOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt5/TopicAliasingOptions.java @@ -60,6 +60,15 @@ public TopicAliasingOptions withOutboundCacheMaxSize(int size) { return this; } + /** + * Returns the configured outbound topic alias behavior. + * + * @return the outbound topic alias behavior type + */ + public OutboundTopicAliasBehaviorType getOutboundBehavior() { + return outboundBehavior; + } + /** * Controls whether or not the client allows the broker to use topic aliasing when sending publishes. Even if * inbound topic aliasing is enabled, it is up to the server to choose whether or not to use it. @@ -92,6 +101,15 @@ public TopicAliasingOptions withInboundCacheMaxSize(int size) { return this; } + /** + * Returns the configured inbound topic alias behavior. + * + * @return the inbound topic alias behavior type + */ + public InboundTopicAliasBehaviorType getInboundBehavior() { + return inboundBehavior; + } + /** * An enumeration that controls how the client applies topic aliasing to outbound publish packets. * @@ -219,4 +237,4 @@ private static Map buildEnumMapping() { private static Map enumMapping = buildEnumMapping(); } -} \ No newline at end of file +} diff --git a/src/native/java_class_ids.c b/src/native/java_class_ids.c index 765b6a983..5c981213f 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -2679,8 +2679,30 @@ static void s_cache_iot_device_sdk_metrics(JNIEnv *env) { iot_device_sdk_metrics_properties.library_name_field_id = (*env)->GetFieldID( env, iot_device_sdk_metrics_properties.iot_device_sdk_metrics_class, "libraryName", "Ljava/lang/String;"); AWS_FATAL_ASSERT(iot_device_sdk_metrics_properties.library_name_field_id); + + iot_device_sdk_metrics_properties.metadata_entries_field_id = (*env)->GetFieldID( + env, iot_device_sdk_metrics_properties.iot_device_sdk_metrics_class, "metadataEntries", "Ljava/util/List;"); + AWS_FATAL_ASSERT(iot_device_sdk_metrics_properties.metadata_entries_field_id); + } +struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; + +static void s_cache_iot_metrics_metadata(JNIEnv *env) { + jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/internal/IoTMetricsMetadata"); + AWS_FATAL_ASSERT(cls); + iot_metrics_metadata_properties.iot_metrics_metadata_class = (*env)->NewGlobalRef(env, cls); + AWS_FATAL_ASSERT(iot_metrics_metadata_properties.iot_metrics_metadata_class); + + iot_metrics_metadata_properties.key_field_id = (*env)->GetFieldID( + env, iot_metrics_metadata_properties.iot_metrics_metadata_class, "key", "Ljava/lang/String;"); + AWS_FATAL_ASSERT(iot_metrics_metadata_properties.key_field_id); + + iot_metrics_metadata_properties.value_field_id = (*env)->GetFieldID( + env, iot_metrics_metadata_properties.iot_metrics_metadata_class, "value", "Ljava/lang/String;"); + AWS_FATAL_ASSERT(iot_metrics_metadata_properties.value_field_id); + } + // Update jni-config.json when adding or modifying JNI classes for GraalVM support. static void s_cache_java_class_ids(void *user_data) { JNIEnv *env = user_data; @@ -2800,6 +2822,7 @@ static void s_cache_java_class_ids(void *user_data) { s_cache_cognito_login_token_source(env); s_cache_cognito_credentials_provider(env); s_cache_iot_device_sdk_metrics(env); + s_cache_iot_metrics_metadata(env); } static aws_thread_once s_cache_once_init = AWS_THREAD_ONCE_STATIC_INIT; diff --git a/src/native/java_class_ids.h b/src/native/java_class_ids.h index d1da8565e..e331f3fe5 100644 --- a/src/native/java_class_ids.h +++ b/src/native/java_class_ids.h @@ -1122,9 +1122,18 @@ extern struct java_cognito_credentials_provider_properties cognito_credentials_p struct java_iot_device_sdk_metrics_properties { jclass iot_device_sdk_metrics_class; jfieldID library_name_field_id; + jfieldID metadata_entries_field_id; }; extern struct java_iot_device_sdk_metrics_properties iot_device_sdk_metrics_properties; +/* IoTMetricsMetadata */ +struct java_iot_metrics_metadata_properties { + jclass iot_metrics_metadata_class; + jfieldID key_field_id; + jfieldID value_field_id; +}; + extern struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; + /** * All functions bound to JNI MUST call this before doing anything else. * This caches all JNI IDs the first time it is called. Any further calls are no-op; it is thread-safe. From b5fed49f34eeaa8501e53b6d0977c137a93a451d Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Fri, 5 Jun 2026 14:22:05 -0700 Subject: [PATCH 02/16] wiring and test cases of iot-metrics --- .../awssdk/crt/internal/IoTMetricEncoder.java | 26 +- .../awssdk/crt/mqtt/MqttClientConnection.java | 14 +- .../awssdk/crt/mqtt/MqttConnectionConfig.java | 43 ++- .../awssdk/crt/mqtt5/Mqtt5ClientOptions.java | 61 ++-- .../crt/aws-crt/jni-config.json | 16 +- src/native/iot_device_sdk_metrics.c | 137 ++++++-- src/native/iot_device_sdk_metrics.h | 4 + src/native/java_class_ids.c | 6 +- src/native/java_class_ids.h | 2 +- src/native/mqtt5_client.c | 8 +- .../awssdk/crt/test/IoTMetricEncoderTest.java | 311 ++++++++++++++++++ .../awssdk/crt/test/Mqtt5ClientTest.java | 12 +- .../test/Mqtt5to3AdapterConnectionTest.java | 4 +- .../crt/test/MqttClientConnectionFixture.java | 12 +- .../crt/test/MqttClientConnectionTest.java | 2 +- 15 files changed, 558 insertions(+), 100 deletions(-) create mode 100644 src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java index 0773811fc..dae09799a 100644 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java +++ b/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java @@ -9,6 +9,7 @@ import software.amazon.awssdk.crt.io.TlsContext; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions; +import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions.JitterMode; import software.amazon.awssdk.crt.utils.PackageInfo; import java.util.ArrayList; @@ -65,10 +66,13 @@ private static String protocolVersionValue(boolean isMqtt5) { * Mapping: Windows (WINSOCK)->B, all other platforms (POSIX)->A. * * @return the single-character metrics value for the socket implementation - * @throws CRT.UnknownPlatformException if the running platform cannot be identified */ - private static String socketImplementationValue() throws CRT.UnknownPlatformException { - return "windows".equals(CRT.getOSIdentifier()) ? "B" : "A"; + private static String socketImplementationValue() { + try { + return "windows".equals(CRT.getOSIdentifier()) ? "B" : "A"; + } catch (Exception e) { + return "A"; + } } /** @@ -85,16 +89,14 @@ private static String httpProxyTypeValue(boolean proxyUsesTls){ /** * Maps ExponentialBackoffJitterMode value to its metrics character. * - * @param value the int value from JitterMode.getValue() + * @param mode the JitterMode enum value * @return single-character metrics value, or null if DEFAULT (should be omitted) */ - private static String retryJitterModeValue(int value) { - switch (value) { - case 1: return "A"; // None - case 2: return "B"; // Full - case 3: return "C"; // Decorrelated - default: return null; - } + private static String retryJitterModeValue(JitterMode mode) { + if (mode == JitterMode.None) return "A"; + if (mode == JitterMode.Full) return "B"; + if (mode == JitterMode.Decorrelated) return "C"; + return null; } /** @@ -216,7 +218,7 @@ public static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions // A: retry_jitter_mode if (clientOptions.getRetryJitterMode() != null) { - String val = retryJitterModeValue(clientOptions.getRetryJitterMode().getValue()); + String val = retryJitterModeValue(clientOptions.getRetryJitterMode()); if (val != null) features.add(RETRY_JITTER_MODE + "/" + val); } diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java index 6cec4bcd4..325d60718 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; import software.amazon.awssdk.crt.mqtt5.packets.ConnectPacket; import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.internal.IoTMetricEncoder; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -78,7 +79,7 @@ private static MqttConnectionConfig s_toMqtt3ConnectionConfig(Mqtt5ClientOptions options.setProtocolOperationTimeoutMs(mqtt5options.getAckTimeoutSeconds() != null ? Math.toIntExact(mqtt5options.getAckTimeoutSeconds()) * 1000 : 0); - options.setMetricsEnabled(mqtt5options.getMetricsEnabled()); + options.setDisableMetrics(mqtt5options.getDisableMetrics()); return options; } @@ -164,8 +165,15 @@ private void SetupConfig(MqttConnectionConfig config) throws MqttException { mqttClientConnectionSetLogin(getNativeHandle(), config.getUsername(), config.getPassword()); } - if (config.getMetricsEnabled()) { - mqttClientConnectionSetMetrics(getNativeHandle(), new IoTDeviceSDKMetrics()); + if (!config.getDisableMetrics()) { + TlsContext tls = null; + if (config.getMqttClient()!=null){ + tls = config.getMqttClient().getTlsContext(); + } else if (config.getMqtt5Client()!=null){ + tls = config.getMqtt5Client().getClientOptions().getTlsContext(); + } + IoTDeviceSDKMetrics metrics = IoTMetricEncoder.createMetricsMqtt3(config.getMetrics(), config.getHttpProxyOptions(), tls); + mqttClientConnectionSetMetrics(getNativeHandle(), metrics); } if (config.getMinReconnectTimeoutSecs() != 0L && config.getMaxReconnectTimeoutSecs() != 0L) { diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java index eceeabf7a..0b5ef6d52 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -9,6 +9,7 @@ import software.amazon.awssdk.crt.CrtResource; import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; import software.amazon.awssdk.crt.io.ClientTlsContext; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.mqtt5.Mqtt5Client; @@ -47,7 +48,8 @@ public final class MqttConnectionConfig extends CrtResource { private Consumer websocketHandshakeTransform; /* metrics */ - private boolean metricsEnabled = true; + private boolean disableMetrics = false; + private IoTDeviceSDKMetrics metrics = null; public MqttConnectionConfig() {} @@ -542,23 +544,44 @@ public Consumer getWebsocketHandshakeTransform( } /** - * Enables or disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Default is false (metrics enabled). * - * @param enabled true to enable metrics, false to disable + * @param disable true to disable metrics, false to enable (default) */ - public void setMetricsEnabled(boolean enabled) { - this.metricsEnabled = enabled; + public void setDisableMetrics(boolean disable) { + this.disableMetrics = disable; } /** - * Queries whether IoT Device SDK metrics collection is enabled + * Returns whether IoT Device SDK metrics collection is disabled. * - * @return true if metrics are enabled, false if disabled + * @return true if metrics are disabled, false if metrics are enabled (default) */ - public boolean getMetricsEnabled() { - return metricsEnabled; + public boolean getDisableMetrics() { + return disableMetrics; } + /** + * Sets the IoT SDK metrics configuration. If provided, the CRT will merge + * these metrics with CRT-level metrics. If null, default CRT metrics are used. + * + * @param metrics metrics configuration from the IoT SDK layer + */ + public void setMetrics(IoTDeviceSDKMetrics metrics) { + this.metrics = metrics; + } + + /** + * Returns the IoT SDK metrics configuration. + * + * @return metrics configuration, or null if not set + */ + public IoTDeviceSDKMetrics getMetrics() { + return metrics; + } + + /** * Creates a (shallow) clone of this config object * @@ -588,7 +611,7 @@ public MqttConnectionConfig clone() { clone.setWebsocketHandshakeTransform(getWebsocketHandshakeTransform()); clone.setReconnectTimeoutSecs(getMinReconnectTimeoutSecs(), getMaxReconnectTimeoutSecs()); - clone.setMetricsEnabled(getMetricsEnabled()); + clone.setDisableMetrics(getDisableMetrics()); // success, bump up the ref count so we can escape the try-with-resources block clone.addRef(); diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java index 0bc19726c..b81c905e0 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java @@ -13,6 +13,7 @@ import software.amazon.awssdk.crt.mqtt5.packets.ConnectPacket; import software.amazon.awssdk.crt.mqtt.MqttConnectionConfig; import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.internal.IoTMetricEncoder; import java.util.Map; import java.util.function.Function; @@ -46,12 +47,11 @@ public class Mqtt5ClientOptions { private Consumer websocketHandshakeTransform; private PublishEvents publishEvents; private TopicAliasingOptions topicAliasingOptions; - // Indicates whether AWS IoT Metrics are enabled for this client, default to true. - // We don't expose iotDeviceSDKMetrics in the builder, and only allow setting - // metricsEnabled for now. - private boolean metricsEnabled = true; + // Opt-out flag for AWS IoT Metrics. When true, metrics are disabled. + // Default is false (metrics enabled). + private boolean disableMetrics = false; private IoTDeviceSDKMetrics iotDeviceSDKMetrics; - + /** * Returns the host name of the MQTT server to connect to. @@ -271,21 +271,22 @@ public TopicAliasingOptions getTopicAliasingOptions() { } /** - * Returns whether AWS IoT Device SDK metrics collection is enabled + * Returns whether AWS IoT Device SDK metrics collection is disabled. * - * @return true if metrics are enabled, false otherwise + * @return true if metrics are disabled, false if metrics are enabled (default) */ - public boolean getMetricsEnabled() { - return this.metricsEnabled; + public boolean getDisableMetrics() { + return this.disableMetrics; } /** - * Enables or disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Default is false (metrics enabled). * - * @param enabled true to enable metrics, false to disable + * @param disable true to disable metrics, false to enable (default) */ - public void setMetricsEnabled(boolean enabled) { - this.metricsEnabled = enabled; + public void setDisableMetrics(boolean disable) { + this.disableMetrics = disable; } /** @@ -314,8 +315,12 @@ public Mqtt5ClientOptions(Mqtt5ClientOptionsBuilder builder) { this.websocketHandshakeTransform = builder.websocketHandshakeTransform; this.publishEvents = builder.publishEvents; this.topicAliasingOptions = builder.topicAliasingOptions; - this.metricsEnabled = builder.metricsEnabled; - this.iotDeviceSDKMetrics = new IoTDeviceSDKMetrics(); + this.disableMetrics = builder.disableMetrics; + if (this.disableMetrics) { + this.iotDeviceSDKMetrics = null; + } else { + this.iotDeviceSDKMetrics = IoTMetricEncoder.createMetricsMqtt5(this, builder.metrics); + } } /******************************************************************************* @@ -379,7 +384,7 @@ public interface PublishEvents { * the client will NOT automatically send the publish acknowledgement. You are responsible for calling * {@link Mqtt5Client#invokePublishAcknowledgement(Mqtt5PublishAcknowledgementControlHandle)} later.

* - *

If you do not call {@code acquirePublishAcknowledgementControl()} within the callback , + *

If you do not call {@code acquirePublishAcknowledgementControl()} within the callback , * the client will automatically send the publish acknowledgement after this callback returns.

* * @param client The client that has received the message @@ -618,7 +623,8 @@ static final public class Mqtt5ClientOptionsBuilder { private Consumer websocketHandshakeTransform; private PublishEvents publishEvents; private TopicAliasingOptions topicAliasingOptions; - private boolean metricsEnabled = true; + private boolean disableMetrics = false; + private IoTDeviceSDKMetrics metrics = null; /** * Sets the host name of the MQTT server to connect to. @@ -887,13 +893,26 @@ public Mqtt5ClientOptionsBuilder withTopicAliasingOptions(TopicAliasingOptions o } /** - * Enables or disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. + * Default is false (metrics enabled). * - * @param enabled true to enable metrics, false to disable. + * @param disable true to disable metrics, false to enable (default) * @return The Mqtt5ClientOptionsBuilder after setting the metrics option */ - public Mqtt5ClientOptionsBuilder withMetricsEnabled(boolean enabled) { - this.metricsEnabled = enabled; + public Mqtt5ClientOptionsBuilder withDisableMetrics(boolean disable) { + this.disableMetrics = disable; + return this; + } + + /** + * Sets the IoT SDK metrics configuration. If provided, the CRT will merge + * these metrics with CRT-level metrics. If null, default CRT metrics are used. + * + * @param metrics metrics configuration from the IoT SDK layer + * @return The Mqtt5ClientOptionsBuilder after setting the metrics + */ + public Mqtt5ClientOptionsBuilder withMetrics(IoTDeviceSDKMetrics metrics) { + this.metrics = metrics; return this; } diff --git a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json index 8edc259bc..82e69edd3 100644 --- a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json +++ b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json @@ -1272,6 +1272,20 @@ "fields": [ { "name": "libraryName" + }, + { + "name": "metadataEntries" + } + ] + }, + { + "name": "software.amazon.awssdk.crt.internal.IoTMetricsMetadata", + "fields": [ + { + "name": "key" + }, + { + "name": "value" } ] }, @@ -1330,7 +1344,7 @@ "name": "iotDeviceSDKMetrics" }, { - "name": "metricsEnabled" + "name": "disableMetrics" } ], "methods": [ diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 8d5051ca2..741d3e34c 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -27,40 +27,117 @@ void aws_mqtt_iot_metrics_java_jni_destroy( aws_byte_buf_clean_up(&java_metrics->library_name_buf); } + if (aws_byte_buf_is_valid(&java_metrics->metadata_storage)) { + aws_byte_buf_clean_up(&java_metrics->metadata_storage); + } + + if (java_metrics->metadata_entries) { + aws_mem_release(allocator, java_metrics->metadata_entries); + } + aws_mem_release(allocator, java_metrics); } struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_java( - JNIEnv *env, - struct aws_allocator *allocator, - jobject java_iot_device_sdk_metrics) { - - struct aws_mqtt_iot_metrics_java_jni *java_metrics = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); - if (java_metrics == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); - return NULL; - } + JNIEnv *env, + struct aws_allocator *allocator, + jobject java_iot_device_sdk_metrics) { - if (aws_get_string_from_jobject( - env, - java_iot_device_sdk_metrics, - iot_device_sdk_metrics_properties.library_name_field_id, - s_iot_device_sdk_metrics_string, - "library name", - &java_metrics->library_name_buf, - &java_metrics->metrics.library_name, - false, - NULL) == AWS_OP_ERR) { - AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: No library name found"); - goto on_error; - } + struct aws_mqtt_iot_metrics_java_jni *java_metrics = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); + if (java_metrics == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); + return NULL; + } - return java_metrics; + if (aws_get_string_from_jobject( + env, + java_iot_device_sdk_metrics, + iot_device_sdk_metrics_properties.library_name_field_id, + s_iot_device_sdk_metrics_string, + "library name", + &java_metrics->library_name_buf, + &java_metrics->metrics.library_name, + false, + NULL) == AWS_OP_ERR) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: No library name found"); + goto on_error; + } + + /* Parse metadataEntries list */ + jobject metadata_list = (*env)->GetObjectField( + env, java_iot_device_sdk_metrics, iot_device_sdk_metrics_properties.metadata_entries_field_id); + + if (metadata_list != NULL && !aws_jni_check_and_clear_exception(env)) { + jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); + if (count > 0 && !aws_jni_check_and_clear_exception(env)) { + java_metrics->metadata_entries = + aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); + if (java_metrics->metadata_entries == NULL) { + goto on_error; + } + + /* Pre-allocate storage buffer for all key/value strings */ + aws_byte_buf_init(&java_metrics->metadata_storage, allocator, (size_t)count * 64); + + for (jint i = 0; i < count; i++) { + jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + continue; + } + + jstring key_jstr = (jstring)(*env)->GetObjectField( + env, entry, iot_metrics_metadata_properties.key_field_id); + jstring value_jstr = (jstring)(*env)->GetObjectField( + env, entry, iot_metrics_metadata_properties.value_field_id); + + if (key_jstr != NULL && value_jstr != NULL) { + const char *key_chars = (*env)->GetStringUTFChars(env, key_jstr, NULL); + size_t key_len = (size_t)(*env)->GetStringUTFLength(env, key_jstr); + const char *value_chars = (*env)->GetStringUTFChars(env, value_jstr, NULL); + size_t value_len = (size_t)(*env)->GetStringUTFLength(env, value_jstr); + + /* Append key to storage buffer */ + size_t key_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append_dynamic(&java_metrics->metadata_storage, &(struct aws_byte_cursor){ + .ptr = (uint8_t *)key_chars, .len = key_len}); + + /* Append value to storage buffer */ + size_t value_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append_dynamic(&java_metrics->metadata_storage, &(struct aws_byte_cursor){ + .ptr = (uint8_t *)value_chars, .len = value_len}); + + java_metrics->metadata_entries[java_metrics->metadata_count].key = + aws_byte_cursor_from_array( + java_metrics->metadata_storage.buffer + key_offset, key_len); + java_metrics->metadata_entries[java_metrics->metadata_count].value = + aws_byte_cursor_from_array( + java_metrics->metadata_storage.buffer + value_offset, value_len); + java_metrics->metadata_count++; + + (*env)->ReleaseStringUTFChars(env, key_jstr, key_chars); + (*env)->ReleaseStringUTFChars(env, value_jstr, value_chars); + } + + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + + /* Set the metrics struct fields */ + java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; + java_metrics->metrics.metadata_count = java_metrics->metadata_count; + } + + (*env)->DeleteLocalRef(env, metadata_list); + } + + return java_metrics; + + on_error: + /* clean up */ + aws_mqtt_iot_metrics_java_jni_destroy(env, allocator, java_metrics); + return NULL; + } -on_error: - /* Clean up */ - aws_mqtt_iot_metrics_java_jni_destroy(env, allocator, java_metrics); - return NULL; -} diff --git a/src/native/iot_device_sdk_metrics.h b/src/native/iot_device_sdk_metrics.h index 341004194..60c176214 100644 --- a/src/native/iot_device_sdk_metrics.h +++ b/src/native/iot_device_sdk_metrics.h @@ -12,6 +12,10 @@ struct aws_mqtt_iot_metrics_java_jni { struct aws_mqtt_iot_metrics metrics; struct aws_byte_buf library_name_buf; + struct aws_byte_buf metadata_storage; + struct aws_mqtt_metadata_entry *metadata_entries; + size_t metadata_count; + }; void aws_mqtt_iot_metrics_java_jni_destroy( diff --git a/src/native/java_class_ids.c b/src/native/java_class_ids.c index 5c981213f..0ce35d624 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -1760,9 +1760,9 @@ static void s_cache_mqtt5_client_options(JNIEnv *env) { "topicAliasingOptions", "Lsoftware/amazon/awssdk/crt/mqtt5/TopicAliasingOptions;"); AWS_FATAL_ASSERT(mqtt5_client_options_properties.topic_aliasing_options_field_id); - mqtt5_client_options_properties.metrics_enabled_field_id = - (*env)->GetFieldID(env, mqtt5_client_options_properties.client_options_class, "metricsEnabled", "Z"); - AWS_FATAL_ASSERT(mqtt5_client_options_properties.metrics_enabled_field_id); + mqtt5_client_options_properties.disable_metrics_field_id = + (*env)->GetFieldID(env, mqtt5_client_options_properties.client_options_class, "disableMetrics", "Z"); + AWS_FATAL_ASSERT(mqtt5_client_options_properties.disable_metrics_field_id); mqtt5_client_options_properties.iot_device_sdk_metrics_field_id = (*env)->GetFieldID( env, mqtt5_client_options_properties.client_options_class, diff --git a/src/native/java_class_ids.h b/src/native/java_class_ids.h index e331f3fe5..d3b462f79 100644 --- a/src/native/java_class_ids.h +++ b/src/native/java_class_ids.h @@ -728,7 +728,7 @@ struct java_aws_mqtt5_client_options_properties { jfieldID publish_events_field_id; jfieldID lifecycle_events_field_id; jfieldID topic_aliasing_options_field_id; - jfieldID metrics_enabled_field_id; + jfieldID disable_metrics_field_id; jfieldID iot_device_sdk_metrics_field_id; }; extern struct java_aws_mqtt5_client_options_properties mqtt5_client_options_properties; diff --git a/src/native/mqtt5_client.c b/src/native/mqtt5_client.c index f5018a1b8..d606ae270 100644 --- a/src/native/mqtt5_client.c +++ b/src/native/mqtt5_client.c @@ -2161,12 +2161,12 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_mqtt5_Mqtt5Client_mqtt5C client_options.client_termination_handler = &s_aws_mqtt5_client_java_termination; client_options.client_termination_handler_user_data = (void *)java_client; - /* Check if metrics are enabled and set metrics value */ - jboolean metrics_enabled = - (*env)->GetBooleanField(env, jni_options, mqtt5_client_options_properties.metrics_enabled_field_id); + /* Check if metrics are disabled (opt-out pattern, default false = metrics enabled) */ + jboolean disable_metrics = + (*env)->GetBooleanField(env, jni_options, mqtt5_client_options_properties.disable_metrics_field_id); bool metrics_has_error = aws_jni_check_and_clear_exception(env); - if (!metrics_has_error && metrics_enabled) { + if (!metrics_has_error && !disable_metrics) { jobject jni_iot_device_sdk_metrics = (*env)->GetObjectField(env, jni_options, mqtt5_client_options_properties.iot_device_sdk_metrics_field_id); if (!aws_jni_check_and_clear_exception(env) && jni_iot_device_sdk_metrics != NULL) { diff --git a/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java b/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java new file mode 100644 index 000000000..2d0bfc006 --- /dev/null +++ b/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java @@ -0,0 +1,311 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.awssdk.crt.test; + +import org.junit.Test; +import static org.junit.Assert.*; + +import software.amazon.awssdk.crt.internal.IoTMetricEncoder; +import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.internal.IoTMetricsMetadata; +import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; +import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions.Mqtt5ClientOptionsBuilder; +import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions.ClientSessionBehavior; +import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions.ClientOfflineQueueBehavior; +import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions; +import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions.OutboundTopicAliasBehaviorType; +import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions.InboundTopicAliasBehaviorType; +import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions.JitterMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class IoTMetricEncoderTest extends CrtTestFixture { + + public IoTMetricEncoderTest() {} + + // ======================== Minimal Options Encoding ======================== + + @Test + public void testMqtt5Minimal() { + Mqtt5ClientOptionsBuilder builder = new Mqtt5ClientOptionsBuilder("localhost", 8883L); + builder.withDisableMetrics(true); // disable so constructor doesn't call encoder + Mqtt5ClientOptions options = builder.build(); + + String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); + + // Should only have F (protocol=5) and G (socket) + assertTrue(result.contains("F/5")); + assertTrue(result.contains("G/")); + // Should NOT have A, B, C, D, E since all are DEFAULT + assertFalse(result.contains("A/")); + assertFalse(result.contains("B/")); + assertFalse(result.contains("C/")); + assertFalse(result.contains("D/")); + assertFalse(result.contains("E/")); + } + + @Test + public void testMqtt3Minimal() { + String result = IoTMetricEncoder.getEncodedFeatureListMqtt3(null, null); + + assertTrue(result.contains("F/3")); + assertTrue(result.contains("G/")); + // Only F and G + assertEquals(2, result.split(",").length); + } + + // ======================== Non-Default Features Encoding ======================== + + @Test + public void testMqtt5WithNonDefaultFeatures() { + TopicAliasingOptions topicAliasing = new TopicAliasingOptions() + .withOutboundBehavior(OutboundTopicAliasBehaviorType.LRU) + .withInboundBehavior(InboundTopicAliasBehaviorType.Enabled); + + Mqtt5ClientOptionsBuilder builder = new Mqtt5ClientOptionsBuilder("localhost", 8883L); + builder.withSessionBehavior(ClientSessionBehavior.CLEAN); + builder.withOfflineQueueBehavior(ClientOfflineQueueBehavior.FAIL_ALL_ON_DISCONNECT); + builder.withRetryJitterMode(JitterMode.Full); + builder.withTopicAliasingOptions(topicAliasing); + builder.withDisableMetrics(true); + Mqtt5ClientOptions options = builder.build(); + + String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); + + assertTrue(result.contains("A/B")); // Full + assertTrue(result.contains("B/A")); // CLEAN + assertTrue(result.contains("C/C")); // FAIL_ALL + assertTrue(result.contains("D/B")); // LRU + assertTrue(result.contains("E/A")); // Enabled + assertTrue(result.contains("F/5")); // MQTT5 + } + + @Test + public void testDefaultEnumValuesOmitted() { + Mqtt5ClientOptionsBuilder builder = new Mqtt5ClientOptionsBuilder("localhost", 8883L); + builder.withSessionBehavior(ClientSessionBehavior.DEFAULT); + builder.withOfflineQueueBehavior(ClientOfflineQueueBehavior.DEFAULT); + builder.withRetryJitterMode(JitterMode.Default); + builder.withDisableMetrics(true); + Mqtt5ClientOptions options = builder.build(); + + String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); + + assertFalse(result.contains("A/")); + assertFalse(result.contains("B/")); + assertFalse(result.contains("C/")); + } + + // ======================== Feature List Merging ======================== + + @Test + public void testUserOverridesCrt() { + String result = IoTMetricEncoder.mergeFeatureLists("A/B,F/5", "A/C"); + assertEquals("A/C,F/5", result); + } + + @Test + public void testUserOverridesMultipleCrtFeatures() { + String result = IoTMetricEncoder.mergeFeatureLists("A/B,F/5,G/A,K/D", "A/C,F/3,K/E"); + assertEquals("A/C,F/3,G/A,K/E", result); + } + + @Test + public void testEmptyUserFeatures() { + String result = IoTMetricEncoder.mergeFeatureLists("F/5,G/A", ""); + assertEquals("F/5,G/A", result); + } + + @Test + public void testEmptyCrtFeatures() { + String result = IoTMetricEncoder.mergeFeatureLists("", "A/B"); + assertEquals("A/B", result); + } + + @Test + public void testBothEmpty() { + String result = IoTMetricEncoder.mergeFeatureLists("", ""); + assertEquals("", result); + } + + @Test + public void testDisjointFeatures() { + String result = IoTMetricEncoder.mergeFeatureLists("F/5,G/A", "I/A,K/D"); + assertEquals("F/5,G/A,I/A,K/D", result); + } + + @Test + public void testInsertionOrderPreserved() { + String result = IoTMetricEncoder.mergeFeatureLists("G/A,F/5", "A/B"); + assertEquals("G/A,F/5,A/B", result); + } + + // ======================== Create Metrics - Default Options ======================== + + @Test + public void testCreateMetricsNullUserMetrics() { + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(null, null, null); + + assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); + assertNotNull(result.getMetadataEntries()); + + // Should have CRTVersion, IoTSDKFeature, IoTSDKMetricsVersion + List entries = result.getMetadataEntries(); + String crtVersion = findMetadataValue(entries, "CRTVersion"); + String feature = findMetadataValue(entries, "IoTSDKFeature"); + String metricsVersion = findMetadataValue(entries, "IoTSDKMetricsVersion"); + + assertNotNull(crtVersion); + assertNotNull(feature); + assertEquals("1", metricsVersion); + } + + @Test + public void testCreateMetricsEmptyUserMetrics() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertTrue(feature.contains("F/3")); // MQTT3 protocol + } + + // ======================== Create Metrics - User Features Merged ======================== + + @Test + public void testUserFeatureAddedWhenVersionMatches() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "1")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertTrue(feature.contains("I/A")); + assertTrue(feature.contains("F/3")); + assertTrue(feature.contains("G/")); + } + + @Test + public void testUserFeatureOverridesCrt() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "1")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "F/3,I/B")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertTrue(feature.contains("F/3")); + assertFalse(feature.contains("F/5")); + assertTrue(feature.contains("I/B")); + } + + // ======================== Create Metrics - Version Mismatch ======================== + + @Test + public void testUserFeaturesIgnoredOnHigherVersion() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "99")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertFalse(feature.contains("I/A")); + assertTrue(feature.contains("F/3")); + } + + @Test + public void testUserFeaturesIgnoredOnNonNumericVersion() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "abc")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertFalse(feature.contains("I/A")); + } + + @Test + public void testUserFeaturesIgnoredWhenNoVersion() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertFalse(feature.contains("I/A")); + } + + // ======================== CRTVersion Not Overridable ======================== + + @Test + public void testCrtVersionCannotBeOverridden() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("CRTVersion", "fake_version")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String crtVersion = findMetadataValue(result.getMetadataEntries(), "CRTVersion"); + assertNotEquals("fake_version", crtVersion); + } + + // ======================== Other User Metadata Preserved ======================== + + @Test + public void testSdkVersionPreserved() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKVersion", "2.0.0")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + String sdkVersion = findMetadataValue(result.getMetadataEntries(), "IoTSDKVersion"); + assertEquals("2.0.0", sdkVersion); + } + + @Test + public void testCustomLibraryName() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics("MyCustomSDK/1.0", null); + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + + assertEquals("MyCustomSDK/1.0", result.getLibraryName()); + } + + @Test + public void testMetricsVersionAlwaysSet() { + IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(null, null, null); + + String metricsVersion = findMetadataValue(result.getMetadataEntries(), "IoTSDKMetricsVersion"); + assertEquals("1", metricsVersion); + } + + // ======================== Helper ======================== + + private String findMetadataValue(List entries, String key) { + for (IoTMetricsMetadata entry : entries) { + if (key.equals(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } +} diff --git a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java index e8f5e17a0..ddd94859b 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java @@ -266,7 +266,7 @@ public void onMessageReceived(Mqtt5Client client, PublishReturn publishReturn) { .withRetryJitterMode(JitterMode.Default) .withSessionBehavior(ClientSessionBehavior.CLEAN) .withSocketOptions(socketOptions) - .withMetricsEnabled(false); + .withDisableMetrics(true); // Skip websocket and TLS options - those are all different tests HttpProxyOptions proxyOptions = new HttpProxyOptions(); @@ -332,7 +332,7 @@ private void doConnDC_UC2Test() { AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_HOST, Long.parseLong(AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_PORT)); builder.withLifecycleEvents(events); - builder.withMetricsEnabled(false); + builder.withDisableMetrics(true); ConnectPacketBuilder connectOptions = new ConnectPacketBuilder(); connectOptions.withUsername(AWS_TEST_MQTT5_BASIC_AUTH_USERNAME).withPassword(AWS_TEST_MQTT5_BASIC_AUTH_PASSWORD.getBytes()); @@ -375,7 +375,7 @@ private void doConnDC_Metrics_EnabledTest() { AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_HOST, Long.parseLong(AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_PORT)); builder.withLifecycleEvents(events); - // Metrics are enabled by default (metricsEnabled = true) + // Metrics are enabled by default (disableMetrics = false) // This should cause connection failure because metrics appends to username, // corrupting basic auth credentials @@ -586,7 +586,7 @@ public void onMessageReceived(Mqtt5Client client, PublishReturn publishReturn) { .withRetryJitterMode(JitterMode.Default) .withSessionBehavior(ClientSessionBehavior.CLEAN) .withSocketOptions(socketOptions) - .withMetricsEnabled(false); + .withDisableMetrics(true); // Skip websocket, proxy options, and TLS options - those are all different tests try (Mqtt5Client client = new Mqtt5Client(builder.build())) { @@ -681,7 +681,7 @@ public void accept(Mqtt5WebsocketHandshakeTransformArgs t) { ConnectPacketBuilder connectOptions = new ConnectPacketBuilder(); connectOptions.withUsername(AWS_TEST_MQTT5_BASIC_AUTH_USERNAME).withPassword(AWS_TEST_MQTT5_BASIC_AUTH_PASSWORD.getBytes()); builder.withConnectOptions(connectOptions.build()); - builder.withMetricsEnabled(false); + builder.withDisableMetrics(true); try (Mqtt5Client client = new Mqtt5Client(builder.build())) { client.start(); @@ -850,7 +850,7 @@ public void onMessageReceived(Mqtt5Client client, PublishReturn publishReturn) { .withRetryJitterMode(JitterMode.Default) .withSessionBehavior(ClientSessionBehavior.CLEAN) .withSocketOptions(socketOptions) - .withMetricsEnabled(false); + .withDisableMetrics(true); Consumer websocketTransform = new Consumer() { @Override diff --git a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5to3AdapterConnectionTest.java b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5to3AdapterConnectionTest.java index b69b9badf..4b4fcd615 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5to3AdapterConnectionTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5to3AdapterConnectionTest.java @@ -304,7 +304,7 @@ public void TestBasicAuthConnectThroughMqtt5() { .withPassword(AWS_TEST_MQTT5_BASIC_AUTH_PASSWORD.getBytes()) .withClientId("test/MQTT5to3Adapter" + UUID.randomUUID().toString()); builder.withConnectOptions(connectOptions.build()) - .withMetricsEnabled(false); + .withDisableMetrics(true); try (Mqtt5Client client = new Mqtt5Client(builder.build()); MqttClientConnection connection = new MqttClientConnection(client, null);) { @@ -497,7 +497,7 @@ public void TestBasicAuthConnectThroughMqtt311() { AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_HOST, Long.parseLong(AWS_TEST_MQTT5_DIRECT_MQTT_BASIC_AUTH_PORT)); builder.withLifecycleEvents(events); - builder.withMetricsEnabled(false); + builder.withDisableMetrics(true); ConnectPacketBuilder connectOptions = new ConnectPacketBuilder(); connectOptions.withUsername(AWS_TEST_MQTT5_BASIC_AUTH_USERNAME) diff --git a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionFixture.java b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionFixture.java index 9b8969eee..e007effd0 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionFixture.java +++ b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionFixture.java @@ -182,7 +182,7 @@ MqttClientConnection createMqttClientConnection(TlsContext tlsContext, String username, String password, HttpProxyOptions httpProxyOptions, - boolean metricsEnabled) throws Exception { + boolean disableMetrics) throws Exception { try (EventLoopGroup elg = new EventLoopGroup(1); HostResolver hr = new HostResolver(elg); ClientBootstrap bootstrap = new ClientBootstrap(elg, hr)) { @@ -239,7 +239,7 @@ public void onConnectionClosed(OnConnectionClosedReturn data) { config.setKeepAliveSecs(0); config.setProtocolOperationTimeoutMs(60000); config.setConnectionCallbacks(events); - config.setMetricsEnabled(metricsEnabled); + config.setDisableMetrics(disableMetrics); if (httpProxyOptions != null) { config.setHttpProxyOptions(httpProxyOptions); @@ -266,14 +266,14 @@ public void onConnectionClosed(OnConnectionClosedReturn data) { } } - void connectDirect(TlsContext tlsContext, String endpoint, int port, String username, String password, HttpProxyOptions httpProxyOptions, boolean metricsEnabled) throws Exception { + void connectDirect(TlsContext tlsContext, String endpoint, int port, String username, String password, HttpProxyOptions httpProxyOptions, boolean disableMetrics) throws Exception { reset(); - MqttClientConnection connection = createMqttClientConnection(tlsContext, endpoint, port, username, password, httpProxyOptions, metricsEnabled); + MqttClientConnection connection = createMqttClientConnection(tlsContext, endpoint, port, username, password, httpProxyOptions, disableMetrics); CompletableFuture connected = connection.connect(); connected.get(30, TimeUnit.SECONDS); } - void connectWebsockets(CredentialsProvider credentialsProvider, String endpoint, int port, TlsContext tlsContext, String username, String password, HttpProxyOptions httpProxyOptions, boolean metricsEnabled) throws Exception + void connectWebsockets(CredentialsProvider credentialsProvider, String endpoint, int port, TlsContext tlsContext, String username, String password, HttpProxyOptions httpProxyOptions, boolean disableMetrics) throws Exception { String clientId = TEST_CLIENTID + (UUID.randomUUID()).toString(); @@ -324,7 +324,7 @@ public void onConnectionClosed(OnConnectionClosedReturn data) { config.setPort(port); config.setUseWebsockets(true); config.setConnectionCallbacks(events); - config.setMetricsEnabled(metricsEnabled); + config.setDisableMetrics(disableMetrics); if (username != null) { config.setUsername(username); diff --git a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionTest.java b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionTest.java index 60a4c19dd..2d8a37439 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionTest.java @@ -289,7 +289,7 @@ public void testConnDC_Metrics_BasicAuth_Enabled() { boolean connectionFailed = false; try { - // Metrics are enabled by default (metricsEnabled = true) + // Metrics are enabled by default (disableMetrics = false) // This should cause connection failure because metrics appends to username, // corrupting basic auth credentials connectDirect( From bdfccf93f5f0d46dfb8899cd889ef3934c777650 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Fri, 5 Jun 2026 14:29:39 -0700 Subject: [PATCH 03/16] lint --- src/native/iot_device_sdk_metrics.c | 201 ++++++++++++++-------------- src/native/iot_device_sdk_metrics.h | 1 - src/native/java_class_ids.c | 3 +- src/native/java_class_ids.h | 8 +- 4 files changed, 105 insertions(+), 108 deletions(-) diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 741d3e34c..f574fdcff 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -39,105 +39,104 @@ void aws_mqtt_iot_metrics_java_jni_destroy( } struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_java( - JNIEnv *env, - struct aws_allocator *allocator, - jobject java_iot_device_sdk_metrics) { - - struct aws_mqtt_iot_metrics_java_jni *java_metrics = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); - if (java_metrics == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); - return NULL; - } - - if (aws_get_string_from_jobject( - env, - java_iot_device_sdk_metrics, - iot_device_sdk_metrics_properties.library_name_field_id, - s_iot_device_sdk_metrics_string, - "library name", - &java_metrics->library_name_buf, - &java_metrics->metrics.library_name, - false, - NULL) == AWS_OP_ERR) { - AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: No library name found"); - goto on_error; - } - - /* Parse metadataEntries list */ - jobject metadata_list = (*env)->GetObjectField( - env, java_iot_device_sdk_metrics, iot_device_sdk_metrics_properties.metadata_entries_field_id); - - if (metadata_list != NULL && !aws_jni_check_and_clear_exception(env)) { - jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); - if (count > 0 && !aws_jni_check_and_clear_exception(env)) { - java_metrics->metadata_entries = - aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - if (java_metrics->metadata_entries == NULL) { - goto on_error; - } - - /* Pre-allocate storage buffer for all key/value strings */ - aws_byte_buf_init(&java_metrics->metadata_storage, allocator, (size_t)count * 64); - - for (jint i = 0; i < count; i++) { - jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - continue; - } - - jstring key_jstr = (jstring)(*env)->GetObjectField( - env, entry, iot_metrics_metadata_properties.key_field_id); - jstring value_jstr = (jstring)(*env)->GetObjectField( - env, entry, iot_metrics_metadata_properties.value_field_id); - - if (key_jstr != NULL && value_jstr != NULL) { - const char *key_chars = (*env)->GetStringUTFChars(env, key_jstr, NULL); - size_t key_len = (size_t)(*env)->GetStringUTFLength(env, key_jstr); - const char *value_chars = (*env)->GetStringUTFChars(env, value_jstr, NULL); - size_t value_len = (size_t)(*env)->GetStringUTFLength(env, value_jstr); - - /* Append key to storage buffer */ - size_t key_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append_dynamic(&java_metrics->metadata_storage, &(struct aws_byte_cursor){ - .ptr = (uint8_t *)key_chars, .len = key_len}); - - /* Append value to storage buffer */ - size_t value_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append_dynamic(&java_metrics->metadata_storage, &(struct aws_byte_cursor){ - .ptr = (uint8_t *)value_chars, .len = value_len}); - - java_metrics->metadata_entries[java_metrics->metadata_count].key = - aws_byte_cursor_from_array( - java_metrics->metadata_storage.buffer + key_offset, key_len); - java_metrics->metadata_entries[java_metrics->metadata_count].value = - aws_byte_cursor_from_array( - java_metrics->metadata_storage.buffer + value_offset, value_len); - java_metrics->metadata_count++; - - (*env)->ReleaseStringUTFChars(env, key_jstr, key_chars); - (*env)->ReleaseStringUTFChars(env, value_jstr, value_chars); - } - - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); - (*env)->DeleteLocalRef(env, entry); - } - - /* Set the metrics struct fields */ - java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; - java_metrics->metrics.metadata_count = java_metrics->metadata_count; - } - - (*env)->DeleteLocalRef(env, metadata_list); - } - - return java_metrics; - - on_error: - /* clean up */ - aws_mqtt_iot_metrics_java_jni_destroy(env, allocator, java_metrics); - return NULL; - } + JNIEnv *env, + struct aws_allocator *allocator, + jobject java_iot_device_sdk_metrics) { + + struct aws_mqtt_iot_metrics_java_jni *java_metrics = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); + if (java_metrics == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); + return NULL; + } + + if (aws_get_string_from_jobject( + env, + java_iot_device_sdk_metrics, + iot_device_sdk_metrics_properties.library_name_field_id, + s_iot_device_sdk_metrics_string, + "library name", + &java_metrics->library_name_buf, + &java_metrics->metrics.library_name, + false, + NULL) == AWS_OP_ERR) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: No library name found"); + goto on_error; + } + /* Parse metadataEntries list */ + jobject metadata_list = (*env)->GetObjectField( + env, java_iot_device_sdk_metrics, iot_device_sdk_metrics_properties.metadata_entries_field_id); + + if (metadata_list != NULL && !aws_jni_check_and_clear_exception(env)) { + jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); + if (count > 0 && !aws_jni_check_and_clear_exception(env)) { + java_metrics->metadata_entries = + aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); + if (java_metrics->metadata_entries == NULL) { + goto on_error; + } + + /* Pre-allocate storage buffer for all key/value strings */ + aws_byte_buf_init(&java_metrics->metadata_storage, allocator, (size_t)count * 64); + + for (jint i = 0; i < count; i++) { + jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + continue; + } + + jstring key_jstr = + (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); + jstring value_jstr = + (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); + + if (key_jstr != NULL && value_jstr != NULL) { + const char *key_chars = (*env)->GetStringUTFChars(env, key_jstr, NULL); + size_t key_len = (size_t)(*env)->GetStringUTFLength(env, key_jstr); + const char *value_chars = (*env)->GetStringUTFChars(env, value_jstr, NULL); + size_t value_len = (size_t)(*env)->GetStringUTFLength(env, value_jstr); + + /* Append key to storage buffer */ + size_t key_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append_dynamic( + &java_metrics->metadata_storage, + &(struct aws_byte_cursor){.ptr = (uint8_t *)key_chars, .len = key_len}); + + /* Append value to storage buffer */ + size_t value_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append_dynamic( + &java_metrics->metadata_storage, + &(struct aws_byte_cursor){.ptr = (uint8_t *)value_chars, .len = value_len}); + + java_metrics->metadata_entries[java_metrics->metadata_count].key = + aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + key_offset, key_len); + java_metrics->metadata_entries[java_metrics->metadata_count].value = + aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + value_offset, value_len); + java_metrics->metadata_count++; + + (*env)->ReleaseStringUTFChars(env, key_jstr, key_chars); + (*env)->ReleaseStringUTFChars(env, value_jstr, value_chars); + } + + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + + /* Set the metrics struct fields */ + java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; + java_metrics->metrics.metadata_count = java_metrics->metadata_count; + } + + (*env)->DeleteLocalRef(env, metadata_list); + } + + return java_metrics; + +on_error: + /* clean up */ + aws_mqtt_iot_metrics_java_jni_destroy(env, allocator, java_metrics); + return NULL; +} diff --git a/src/native/iot_device_sdk_metrics.h b/src/native/iot_device_sdk_metrics.h index 60c176214..e7c8c1bad 100644 --- a/src/native/iot_device_sdk_metrics.h +++ b/src/native/iot_device_sdk_metrics.h @@ -15,7 +15,6 @@ struct aws_mqtt_iot_metrics_java_jni { struct aws_byte_buf metadata_storage; struct aws_mqtt_metadata_entry *metadata_entries; size_t metadata_count; - }; void aws_mqtt_iot_metrics_java_jni_destroy( diff --git a/src/native/java_class_ids.c b/src/native/java_class_ids.c index 0ce35d624..951be72be 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -2683,7 +2683,6 @@ static void s_cache_iot_device_sdk_metrics(JNIEnv *env) { iot_device_sdk_metrics_properties.metadata_entries_field_id = (*env)->GetFieldID( env, iot_device_sdk_metrics_properties.iot_device_sdk_metrics_class, "metadataEntries", "Ljava/util/List;"); AWS_FATAL_ASSERT(iot_device_sdk_metrics_properties.metadata_entries_field_id); - } struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; @@ -2701,7 +2700,7 @@ static void s_cache_iot_metrics_metadata(JNIEnv *env) { iot_metrics_metadata_properties.value_field_id = (*env)->GetFieldID( env, iot_metrics_metadata_properties.iot_metrics_metadata_class, "value", "Ljava/lang/String;"); AWS_FATAL_ASSERT(iot_metrics_metadata_properties.value_field_id); - } +} // Update jni-config.json when adding or modifying JNI classes for GraalVM support. static void s_cache_java_class_ids(void *user_data) { diff --git a/src/native/java_class_ids.h b/src/native/java_class_ids.h index d3b462f79..5b8dba44b 100644 --- a/src/native/java_class_ids.h +++ b/src/native/java_class_ids.h @@ -1128,11 +1128,11 @@ extern struct java_iot_device_sdk_metrics_properties iot_device_sdk_metrics_prop /* IoTMetricsMetadata */ struct java_iot_metrics_metadata_properties { - jclass iot_metrics_metadata_class; - jfieldID key_field_id; - jfieldID value_field_id; + jclass iot_metrics_metadata_class; + jfieldID key_field_id; + jfieldID value_field_id; }; - extern struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; +extern struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; /** * All functions bound to JNI MUST call this before doing anything else. From 5f8afae41cdbb0abbcdce7bb76d72398f9335d88 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Fri, 5 Jun 2026 14:50:40 -0700 Subject: [PATCH 04/16] Flip test booleans to match disableMetrics --- .../test/MqttClientConnectionMethodTest.java | 28 +++++++++---------- .../crt/test/MqttClientConnectionTest.java | 14 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionMethodTest.java b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionMethodTest.java index cc63f6fe4..9a8863664 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionMethodTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/MqttClientConnectionMethodTest.java @@ -61,7 +61,7 @@ private void doConnDC_Cred_UC1Test() { null, null, null, - true); + false); disconnect(); } finally { close(); @@ -97,7 +97,7 @@ private void doConnDC_Cred_UC2Test() { null, null, null, - true); + false); disconnect(); } catch (Exception ex) { throw new RuntimeException(ex); @@ -133,7 +133,7 @@ private void doConnDC_Cred_UC3Test() { null, null, null, - true); + false); disconnect(); } catch (Exception ex) { throw new RuntimeException(ex); @@ -174,7 +174,7 @@ private void doConnDC_Cred_UC4Test() { null, null, null, - true); + false); disconnect(); } catch (Exception ex) { throw new RuntimeException(ex); @@ -212,7 +212,7 @@ private void doWebsocketIotCoreConnectionTest(Function Date: Fri, 5 Jun 2026 15:43:42 -0700 Subject: [PATCH 05/16] Removing check for mem_Calloc --- src/native/iot_device_sdk_metrics.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index f574fdcff..9d8f2b9bb 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -45,11 +45,6 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ struct aws_mqtt_iot_metrics_java_jni *java_metrics = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); - if (java_metrics == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); - return NULL; - } if (aws_get_string_from_jobject( env, @@ -74,9 +69,6 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ if (count > 0 && !aws_jni_check_and_clear_exception(env)) { java_metrics->metadata_entries = aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - if (java_metrics->metadata_entries == NULL) { - goto on_error; - } /* Pre-allocate storage buffer for all key/value strings */ aws_byte_buf_init(&java_metrics->metadata_storage, allocator, (size_t)count * 64); From bacc2d17269887b234727f5645dc53005810681c Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Sun, 7 Jun 2026 12:13:38 -0700 Subject: [PATCH 06/16] reallocation of buffer --- .../crt/internal/IoTDeviceSDKMetrics.java | 12 ++++++++ src/native/iot_device_sdk_metrics.c | 29 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java index 122cca206..2c5da4795 100644 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java @@ -24,8 +24,20 @@ public IoTDeviceSDKMetrics(String libraryName, List metadata } public String getLibraryName() { return libraryName; } + + /** + * Sets the SDK library name (e.g., "IoTDeviceSDK/Java"). + * + * @param libraryName the library name to report in metrics + */ public void setLibraryName(String libraryName) { this.libraryName = libraryName; } public List getMetadataEntries() { return metadataEntries; } + + /** + * Sets the metadata entries to include in the MQTT CONNECT packet username field. + * + * @param metadataEntries list of key-value metadata pairs, or null for none + */ public void setMetadataEntries(List metadataEntries) { this.metadataEntries = metadataEntries; } } diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 9d8f2b9bb..db8879f1d 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -70,9 +70,30 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ java_metrics->metadata_entries = aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - /* Pre-allocate storage buffer for all key/value strings */ - aws_byte_buf_init(&java_metrics->metadata_storage, allocator, (size_t)count * 64); + /* First pass: compute exact buffer size needed */ + size_t total_size = 0; + for (jint i = 0; i < count; i++) { + jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + continue; + } + jstring key_jstr = + (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); + jstring value_jstr = + (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); + if (key_jstr != NULL && value_jstr != NULL) { + total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); + total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); + } + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + + /* Allocate exact size */ + aws_byte_buf_init(&java_metrics->metadata_storage, allocator, total_size); + /* Second pass: copy strings and create cursors */ for (jint i = 0; i < count; i++) { jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); if (entry == NULL || aws_jni_check_and_clear_exception(env)) { @@ -92,13 +113,13 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ /* Append key to storage buffer */ size_t key_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append_dynamic( + aws_byte_buf_append( &java_metrics->metadata_storage, &(struct aws_byte_cursor){.ptr = (uint8_t *)key_chars, .len = key_len}); /* Append value to storage buffer */ size_t value_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append_dynamic( + aws_byte_buf_append( &java_metrics->metadata_storage, &(struct aws_byte_cursor){.ptr = (uint8_t *)value_chars, .len = value_len}); From cd9adf1751480809e6d097f9783189a63606caf2 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Wed, 10 Jun 2026 16:46:29 -0700 Subject: [PATCH 07/16] changing file location --- .../crt/internal/IoTDeviceSDKMetrics.java | 43 -- .../amazon/awssdk/crt/io/TlsContext.java | 37 +- .../IoTDeviceSDKMetrics.java} | 462 +++++++++--------- .../{internal => iot}/IoTMetricsMetadata.java | 3 +- .../awssdk/crt/mqtt/MqttClientConnection.java | 11 +- .../awssdk/crt/mqtt/MqttConnectionConfig.java | 2 +- .../awssdk/crt/mqtt5/Mqtt5ClientOptions.java | 16 +- .../crt/aws-crt/jni-config.json | 4 +- src/native/java_class_ids.c | 6 +- .../awssdk/crt/test/IoTMetricEncoderTest.java | 151 +++--- 10 files changed, 342 insertions(+), 393 deletions(-) delete mode 100644 src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java rename src/main/java/software/amazon/awssdk/crt/{internal/IoTMetricEncoder.java => iot/IoTDeviceSDKMetrics.java} (55%) rename src/main/java/software/amazon/awssdk/crt/{internal => iot}/IoTMetricsMetadata.java (91%) diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java deleted file mode 100644 index 2c5da4795..000000000 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ -package software.amazon.awssdk.crt.internal; - -import java.util.List; - -/** - * @internal - * IoT Device SDK Metrics Structure. Not for external usage. - */ -public class IoTDeviceSDKMetrics { - private String libraryName; - private List metadataEntries; - - public IoTDeviceSDKMetrics() { - this.libraryName = "IoTDeviceSDK/Java"; - } - - public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { - this.libraryName = libraryName; - this.metadataEntries = metadataEntries; - } - - public String getLibraryName() { return libraryName; } - - /** - * Sets the SDK library name (e.g., "IoTDeviceSDK/Java"). - * - * @param libraryName the library name to report in metrics - */ - public void setLibraryName(String libraryName) { this.libraryName = libraryName; } - - public List getMetadataEntries() { return metadataEntries; } - - /** - * Sets the metadata entries to include in the MQTT CONNECT packet username field. - * - * @param metadataEntries list of key-value metadata pairs, or null for none - */ - public void setMetadataEntries(List metadataEntries) { this.metadataEntries = metadataEntries; } -} diff --git a/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java b/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java index 345a87587..6fa8a0e1b 100644 --- a/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java +++ b/src/main/java/software/amazon/awssdk/crt/io/TlsContext.java @@ -13,8 +13,9 @@ */ public class TlsContext extends CrtResource { - private int cipherPreference; - private int minimumTlsVersion; + private TlsCipherPreference cipherPreference; + private TlsContextOptions.TlsVersions minimumTlsVersion; + /** * Creates a new Client TlsContext. There are significant native resources consumed to create a TlsContext, so most * applications will only need to create one and re-use it for all connections. @@ -24,8 +25,8 @@ public class TlsContext extends CrtResource { */ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); - this.cipherPreference = options.tlsCipherPreference.getValue(); - this.minimumTlsVersion = options.minTlsVersion.getValue(); + this.cipherPreference = options.tlsCipherPreference; + this.minimumTlsVersion = options.minTlsVersion; } /** @@ -35,8 +36,8 @@ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { public TlsContext() throws CrtRuntimeException { try (TlsContextOptions options = TlsContextOptions.createDefaultClient()) { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); - this.cipherPreference = options.tlsCipherPreference.getValue(); - this.minimumTlsVersion = options.minTlsVersion.getValue(); + this.cipherPreference = options.tlsCipherPreference; + this.minimumTlsVersion = options.minTlsVersion; } } @@ -61,17 +62,17 @@ protected void releaseNativeHandle() { private static native void tlsContextDestroy(long elg); - /** - * Returns the TLS cipher preference native value configured on this context. - * - * @return native int value of the TlsCipherPreference - */ - public int getCipherPreference() { return cipherPreference; } + /** + * Returns the {@link TlsCipherPreference} configured on this context. + * + * @return the cipher preference + */ + public TlsCipherPreference getCipherPreference() { return cipherPreference; } - /** - * Returns the minimum TLS version native value configured on this context. - * - * @return native int value of the TlsVersions - */ - public int getMinimumTlsVersion() { return minimumTlsVersion; } + /** + * Returns the minimum {@link TlsContextOptions.TlsVersions TLS version} configured on this context. + * + * @return the minimum TLS version + */ + public TlsContextOptions.TlsVersions getMinimumTlsVersion() { return minimumTlsVersion; } }; diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java similarity index 55% rename from src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java rename to src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index dae09799a..3e7a9409d 100644 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricEncoder.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -2,37 +2,34 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ -package software.amazon.awssdk.crt.internal; +package software.amazon.awssdk.crt.iot; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import software.amazon.awssdk.crt.CRT; import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions.JitterMode; +import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.crt.io.TlsContextOptions; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; import software.amazon.awssdk.crt.mqtt5.TopicAliasingOptions; -import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions.JitterMode; +import software.amazon.awssdk.crt.mqtt.MqttConnectionConfig; import software.amazon.awssdk.crt.utils.PackageInfo; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - /** - * @internal - * Encodes IoT SDK metrics features and creates final metrics object - * + * IoT Device SDK Metrics configuration. + * Holds library identification and metadata entries that are appended + * to the MQTT CONNECT packet username field. */ +public class IoTDeviceSDKMetrics { + private String libraryName; + private List metadataEntries; -public class IoTMetricEncoder{ - - /* - 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. - */ - + // Feature ID constants private static final String RETRY_JITTER_MODE = "A"; private static final String SESSION_BEHAVIOR = "B"; private static final String OFFLINE_QUEUE_BEHAVIOR = "C"; @@ -45,156 +42,63 @@ public class IoTMetricEncoder{ private static final String TLS_CIPHER_PREFERENCE = "J"; private static final String MINIMUM_TLS_VERSION = "K"; - // Metrics Version Constant public static final int IOT_SDK_METRICS_FEATURE_VERSION = 1; - // value mapping methods - - /** - * Map protocol version to its single-character metrics value. - * Mapping: MQTT311->3, MQTT5->5. - * - * @param isMqtt5 true if the client is using MQTT5, false for MQTT311 - * @return the single-character metrics value for the protocol version - */ - private static String protocolVersionValue(boolean isMqtt5) { - return isMqtt5 ? "5" : "3"; - } - - /** - * Detect the socket implementation and return its single-character metrics value. - * Mapping: Windows (WINSOCK)->B, all other platforms (POSIX)->A. - * - * @return the single-character metrics value for the socket implementation - */ - private static String socketImplementationValue() { - try { - return "windows".equals(CRT.getOSIdentifier()) ? "B" : "A"; - } catch (Exception e) { - return "A"; - } + public IoTDeviceSDKMetrics() { + this.libraryName = "IoTDeviceSDK/Java"; } - /** - * Map proxy options to its single-character metrics value for proxy type. - * Mapping: HTTPS (has tls connection option)->B, HTTP->A. - * - * @param proxyUsesTls - * @return the single-character metrics value for the proxy options - */ - private static String httpProxyTypeValue(boolean proxyUsesTls){ - return proxyUsesTls? "B" : "A"; + public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { + this.libraryName = libraryName; + this.metadataEntries = metadataEntries; } - /** - * Maps ExponentialBackoffJitterMode value to its metrics character. - * - * @param mode the JitterMode enum value - * @return single-character metrics value, or null if DEFAULT (should be omitted) - */ - private static String retryJitterModeValue(JitterMode mode) { - if (mode == JitterMode.None) return "A"; - if (mode == JitterMode.Full) return "B"; - if (mode == JitterMode.Decorrelated) return "C"; - return null; - } + public String getLibraryName() { return libraryName; } - /** - * Maps ClientSessionBehavior value to its metrics character. - * - * @param value the int value from ClientSessionBehavior.getValue() - * @return single-character metrics value, or null if DEFAULT (should be omitted) - */ - private static String sessionBehaviorValue(int value) { - switch (value) { - case 1: return "A"; // CLEAN - case 2: return "B"; // REJOIN_POST_SUCCESS - case 3: return "C"; // REJOIN_ALWAYS - default: return null; - } - } + public void setLibraryName(String libraryName) { this.libraryName = libraryName; } - /** - * Maps ClientOfflineQueueBehavior value to its metrics character. - * - * @param value the int value from ClientOfflineQueueBehavior.getValue() - * @return single-character metrics value, or null if DEFAULT (should be omitted) - */ - private static String offlineQueueBehaviorValue(int value) { - switch (value) { - case 1: return "A"; // FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT - case 2: return "B"; // FAIL_QOS0_PUBLISH_ON_DISCONNECT - case 3: return "C"; // FAIL_ALL_ON_DISCONNECT - default: return null; - } - } + public List getMetadataEntries() { return metadataEntries; } - /** - * Maps OutboundTopicAliasBehaviorType value to its metrics character. - * - * @param value the int value from OutboundTopicAliasBehaviorType.getValue() - * @return single-character metrics value, or null if Default (should be omitted) - */ - private static String outboundTopicAliasBehaviorValue(int value) { - switch (value) { - case 1: return "A"; // Manual - case 2: return "B"; // LRU - case 3: return "C"; // Disabled - default: return null; - } - } + public void setMetadataEntries(List metadataEntries) { this.metadataEntries = metadataEntries; } - /** - * Maps InboundTopicAliasBehaviorType value to its metrics character. - * - * @param value the int value from InboundTopicAliasBehaviorType.getValue() - * @return single-character metrics value, or null if Default (should be omitted) - */ - private static String inboundTopicAliasBehaviorValue(int value) { - switch (value) { - case 1: return "A"; // Enabled - case 2: return "B"; // Disabled - default: return null; - } - } /** - * Maps TlsCipherPreference value to its metrics character. - * - * @param value the int value from TlsCipherPreference.getValue() - * @return single-character metrics value, or null if SYSTEM_DEFAULT (should be omitted) - */ - private static String tlsCipherPreferenceValue(int value) { - switch (value) { - case 6: return "A"; // TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05 - case 8: return "B"; // TLS_CIPHER_PQ_DEFAULT - case 9: return "C"; // TLS_CIPHER_PREF_TLSv1_2_2025 - default: return null; - } + * Builds the final metrics object for an MQTT5 client by encoding the CRT + * feature list from {@code clientOptions} and merging it with any + * user-supplied metadata. + * + * @param clientOptions MQTT5 client options containing connection configuration and user metrics + * @return the merged metrics object ready to be passed to JNI + */ + public static IoTDeviceSDKMetrics createMetricsMqtt5(Mqtt5ClientOptions clientOptions) { + String crtFeatureList = getEncodedFeatureListMqtt5(clientOptions); + return createMetrics(clientOptions.getUserMetrics(), crtFeatureList); } /** - * Maps TlsVersions value to its metrics character. - * - * @param value the int value from TlsVersions.getValue() - * @return single-character metrics value, or null if SYS_DEFAULTS (should be omitted) - */ - private static String minimumTlsVersionValue(int value) { - switch (value) { - case 0: return "A"; // SSLv3 - case 1: return "B"; // TLSv1 - case 2: return "C"; // TLSv1_1 - case 3: return "D"; // TLSv1_2 - case 4: return "E"; // TLSv1_3 - default: return null; + * Builds the final metrics object for an MQTT3 connection by encoding the CRT + * feature list from the connection configuration and merging it + * with any user-supplied metadata. + * + * @param config the MQTT3 connection configuration containing proxy, TLS, and user metrics + * @return the merged metrics object ready to be passed to JNI + */ + public static IoTDeviceSDKMetrics createMetricsMqtt3(MqttConnectionConfig config) { + TlsContext tls = null; + if (config.getMqttClient() != null) { + tls = config.getMqttClient().getTlsContext(); + } else if (config.getMqtt5Client() != null) { + tls = config.getMqtt5Client().getClientOptions().getTlsContext(); } + String crtFeatureList = getEncodedFeatureListMqtt3(config.getHttpProxyOptions(), tls); + return createMetrics(config.getMetrics(), crtFeatureList); } - // Feature List Encoding /** * Generates the encoded feature list string for metrics from MQTT5 client options. * + *

* 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, protocol=MQTT5, socket=POSIX. @@ -204,65 +108,59 @@ private static String minimumTlsVersionValue(int value) { * - G (socket_implementation): detected from platform * * Conditionally includes (only when not DEFAULT): - * A (retry_jitter_mode), B (session_behavior), C (offline_queue_behavior), - * D (outbound_topic_alias), E (inbound_topic_alias), H (http_proxy_type), - * J (tls_cipher_preference), K (minimum_tls_version) + * A (retry_jitter_mode), + * B (session_behavior), + * C (offline_queue_behavior), + * D (outbound_topic_alias), + * E (inbound_topic_alias), + * H (http_proxy_type), + * J (tls_cipher_preference), + * K (minimum_tls_version) * * Feature I (certificate_source) is set at the IoT SDK level, not here. * * @param clientOptions MQTT5 client options containing connection configuration * @return the encoded feature list string */ - public static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions) { + private static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions) { List features = new ArrayList<>(); - // A: retry_jitter_mode if (clientOptions.getRetryJitterMode() != null) { String val = retryJitterModeValue(clientOptions.getRetryJitterMode()); if (val != null) features.add(RETRY_JITTER_MODE + "/" + val); } - // B: session_behavior if (clientOptions.getSessionBehavior() != null) { - String val = sessionBehaviorValue(clientOptions.getSessionBehavior().getValue()); + String val = sessionBehaviorValue(clientOptions.getSessionBehavior()); if (val != null) features.add(SESSION_BEHAVIOR + "/" + val); } - // C: offline_queue_behavior if (clientOptions.getOfflineQueueBehavior() != null) { - String val = offlineQueueBehaviorValue(clientOptions.getOfflineQueueBehavior().getValue()); + String val = offlineQueueBehaviorValue(clientOptions.getOfflineQueueBehavior()); if (val != null) features.add(OFFLINE_QUEUE_BEHAVIOR + "/" + val); } - // D & E: topic aliasing TopicAliasingOptions topicAliasing = clientOptions.getTopicAliasingOptions(); if (topicAliasing != null) { if (topicAliasing.getOutboundBehavior() != null) { - String val = outboundTopicAliasBehaviorValue(topicAliasing.getOutboundBehavior().getValue()); + String val = outboundTopicAliasBehaviorValue(topicAliasing.getOutboundBehavior()); if (val != null) features.add(OUTBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); } if (topicAliasing.getInboundBehavior() != null) { - String val = inboundTopicAliasBehaviorValue(topicAliasing.getInboundBehavior().getValue()); + String val = inboundTopicAliasBehaviorValue(topicAliasing.getInboundBehavior()); if (val != null) features.add(INBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); } } - // F: protocol_version - always MQTT5 features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(true)); - - // G: socket_implementation - always present features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); - // H: http_proxy_type HttpProxyOptions proxyOptions = clientOptions.getHttpProxyOptions(); if (proxyOptions != null) { boolean proxyUsesTls = proxyOptions.getTlsContext() != null; features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); } - // I: certificate_source - set at IoT SDK level, not here - - // J & K: TLS properties TlsContext tlsCtx = clientOptions.getTlsContext(); if (tlsCtx != null) { String val = tlsCipherPreferenceValue(tlsCtx.getCipherPreference()); @@ -277,6 +175,9 @@ public static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions /** * Generates the encoded feature list string for metrics from MQTT3 connection parameters. + *

+ * + * Format: "ID/Value,ID/Value,..." * * MQTT3 connections always include: * - F (protocol_version): set to MQTT311 @@ -289,22 +190,17 @@ public static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions * @param tlsCtx optional TLS context used by the connection * @return the encoded feature list string */ - public static String getEncodedFeatureListMqtt3(HttpProxyOptions proxyOptions, TlsContext tlsCtx) { + private static String getEncodedFeatureListMqtt3(HttpProxyOptions proxyOptions, TlsContext tlsCtx) { List features = new ArrayList<>(); - // F: protocol_version - always MQTT311 features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(false)); - - // G: socket_implementation features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); - // H: http_proxy_type if (proxyOptions != null) { boolean proxyUsesTls = proxyOptions.getTlsContext() != null; features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); } - // J & K: TLS properties if (tlsCtx != null) { String val = tlsCipherPreferenceValue(tlsCtx.getCipherPreference()); if (val != null) features.add(TLS_CIPHER_PREFERENCE + "/" + val); @@ -316,18 +212,15 @@ public static String getEncodedFeatureListMqtt3(HttpProxyOptions proxyOptions, T return String.join(",", features); } - // ======================== Feature List Merging ======================== - /** * Merges CRT-generated features with user-provided (IoT SDK) features. - * When both lists contain the same feature ID, the user-provided value - * takes precedence. Insertion order is preserved. + * When both lists contain the same feature ID, the user-provided value takes precedence. * - * @param crtFeatures CRT-generated feature list string (e.g., "F/5,G/A") - * @param userFeatures user-provided feature list from the IoT SDK (may be null or empty) - * @return the merged feature list string + * @param crtFeatures CRT-generated feature list (e.g. {@code "F/5,G/A"}); may be {@code null} or empty + * @param userFeatures user-provided feature list from the IoT SDK; may be {@code null} or empty + * @return the merged feature list string (never {@code null}; empty if both inputs are empty) */ - public static String mergeFeatureLists(String crtFeatures, String userFeatures) { + private static String mergeFeatureLists(String crtFeatures, String userFeatures) { LinkedHashMap merged = new LinkedHashMap<>(); parseFeatures(crtFeatures, merged); parseFeatures(userFeatures, merged); @@ -340,49 +233,6 @@ public static String mergeFeatureLists(String crtFeatures, String userFeatures) return sb.toString(); } - /** - * Parses a feature string into key-value pairs in the given map. - * Later entries overwrite earlier ones with the same key. - * - * @param featureStr comma-separated feature string (e.g., "A/B,F/5") - * @param map target map to populate - */ - private static void parseFeatures(String featureStr, Map map) { - if (featureStr == null || featureStr.isEmpty()) return; - for (String pair : featureStr.split(",")) { - int slash = pair.indexOf('/'); - if (slash > 0) { - map.put(pair.substring(0, slash), pair.substring(slash + 1)); - } - } - } - - // ======================== Metrics Creation ======================== - - /** - * Creates the final IoTDeviceSDKMetrics object for an MQTT5 client. - * - * @param clientOptions MQTT5 client options containing connection configuration - * @param userMetrics optional metrics configuration from the IoT SDK (may be null) - * @return the final metrics object ready for JNI passing - */ - public static IoTDeviceSDKMetrics createMetricsMqtt5(Mqtt5ClientOptions clientOptions, IoTDeviceSDKMetrics userMetrics) { - String crtFeatureList = getEncodedFeatureListMqtt5(clientOptions); - return createMetrics(userMetrics, crtFeatureList); - } - - /** - * Creates the final IoTDeviceSDKMetrics object for an MQTT3 connection. - * - * @param userMetrics optional metrics configuration from the IoT SDK (may be null) - * @param proxyOptions optional HTTP proxy options from the connection - * @param tlsCtx optional TLS context used by the connection - * @return the final metrics object ready for JNI passing - */ - public static IoTDeviceSDKMetrics createMetricsMqtt3(IoTDeviceSDKMetrics userMetrics, HttpProxyOptions proxyOptions, TlsContext tlsCtx) { - String crtFeatureList = getEncodedFeatureListMqtt3(proxyOptions, tlsCtx); - return createMetrics(userMetrics, crtFeatureList); - } /** * Metrics creation logic: @@ -391,19 +241,17 @@ public static IoTDeviceSDKMetrics createMetricsMqtt3(IoTDeviceSDKMetrics userMet * 3. IoTSDKFeature: merged if user version matches, else CRT only * 4. Other user metadata: passed through unchanged * - * @param userMetrics optional metrics from IoT SDK (may be null) + * @param userMetrics optional metrics from a higher-level IoT SDK (may be {@code null}) * @param crtFeatureList encoded CRT feature list string * @return the final metrics object */ private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics, String crtFeatureList) { String libraryName = (userMetrics != null) ? userMetrics.getLibraryName() : "IoTDeviceSDK/Java"; - // CRTVersion: not modifiable by user String crtVersion = new PackageInfo().version.toString(); LinkedHashMap metadata = new LinkedHashMap<>(); metadata.put("CRTVersion", crtVersion); - // Extract user metadata String userMetricsVersion = null; String userFeature = ""; @@ -419,7 +267,6 @@ private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics } } - // Merge features: if version matches, merge CRT + SDK; otherwise CRT only if (userMetricsVersion != null && isNumeric(userMetricsVersion) && Integer.parseInt(userMetricsVersion) == IOT_SDK_METRICS_FEATURE_VERSION) { metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, userFeature)); @@ -427,10 +274,8 @@ private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, "")); } - // Always set current metrics version metadata.put("IoTSDKMetricsVersion", String.valueOf(IOT_SDK_METRICS_FEATURE_VERSION)); - // Build final metrics object List entries = new ArrayList<>(); for (Map.Entry entry : metadata.entrySet()) { entries.add(new IoTMetricsMetadata(entry.getKey(), entry.getValue())); @@ -439,11 +284,161 @@ private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics return new IoTDeviceSDKMetrics(libraryName, entries); } + private static void parseFeatures(String featureStr, Map map) { + if (featureStr == null || featureStr.isEmpty()) return; + for (String pair : featureStr.split(",")) { + int slash = pair.indexOf('/'); + if (slash > 0) { + map.put(pair.substring(0, slash), pair.substring(slash + 1)); + } + } + } + + /** + * @param isMqtt5 {@code true} for MQTT5, {@code false} for MQTT3 + * @return the encoded protocol version value ({@code "5"} or {@code "3"}) + */ + private static String protocolVersionValue(boolean isMqtt5) { + return isMqtt5 ? "5" : "3"; + } + + /** + * @return {@code "B"} on Windows (IOCP), {@code "A"} otherwise (POSIX). Defaults to {@code "A"} if OS detection fails. + */ + private static String socketImplementationValue() { + try { + return "windows".equals(CRT.getOSIdentifier()) ? "B" : "A"; + } catch (Exception e) { + return "A"; + } + } + + /** + * @param proxyUsesTls whether the proxy connection is TLS-tunneled + * @return {@code "B"} for TLS proxy, {@code "A"} for plaintext proxy + */ + private static String httpProxyTypeValue(boolean proxyUsesTls) { + return proxyUsesTls ? "B" : "A"; + } + + /** + * Encodes a {@link JitterMode} into its single-character metrics value. + * + * @param mode the configured jitter mode + * @return {@code "A"} for None, {@code "B"} for Full, {@code "C"} for Decorrelated, or {@code null} for Default/unknown + */ + private static String retryJitterModeValue(JitterMode mode) { + switch (mode) { + case None: return "A"; + case Full: return "B"; + case Decorrelated: return "C"; + default: return null; + } + } + + /** + * Encodes an MQTT5 session behavior enum value. + * + * @param value the {@code ClientSessionBehavior} ordinal + * @return {@code "A"} for CLEAN, {@code "B"} for REJOIN_POST_SUCCESS, {@code "C"} for REJOIN_ALWAYS, + * or {@code null} for DEFAULT/unknown + */ + private static String sessionBehaviorValue(Mqtt5ClientOptions.ClientSessionBehavior behavior) { + switch (behavior) { + case CLEAN: return "A"; + case REJOIN_POST_SUCCESS: return "B"; + case REJOIN_ALWAYS: return "C"; + default: return null; + } + } + + /** + * Encodes an MQTT5 offline queue behavior enum value. + * + * @param value the {@code ClientOfflineQueueBehavior} ordinal + * @return {@code "A"} for FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, {@code "B"} for FAIL_QOS0_PUBLISH_ON_DISCONNECT, + * {@code "C"} for FAIL_ALL_ON_DISCONNECT, or {@code null} for DEFAULT/unknown + */ + private static String offlineQueueBehaviorValue(Mqtt5ClientOptions.ClientOfflineQueueBehavior behavior) { + switch (behavior) { + case FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT: return "A"; + case FAIL_QOS0_PUBLISH_ON_DISCONNECT: return "B"; + case FAIL_ALL_ON_DISCONNECT: return "C"; + default: return null; + } + } + + /** + * Encodes an outbound topic alias behavior enum value. + * + * @param value the {@code OutboundTopicAliasBehaviorType} ordinal + * @return {@code "A"} for Manual, {@code "B"} for LRU, {@code "C"} for Disabled, + * or {@code null} for Default/unknown + */ + private static String outboundTopicAliasBehaviorValue(TopicAliasingOptions.OutboundTopicAliasBehaviorType behavior) { + switch (behavior) { + case Manual: return "A"; + case LRU: return "B"; + case Disabled: return "C"; + default: return null; + } + } + /** - * Checks if a string can be parsed as an integer. + * Encodes an inbound topic alias behavior enum value. * - * @param str the string to check - * @return true if the string is a valid integer, false otherwise + * @param value the {@code InboundTopicAliasBehaviorType} ordinal + * @return {@code "A"} for Enabled, {@code "B"} for Disabled, or {@code null} for Default/unknown + */ + private static String inboundTopicAliasBehaviorValue(TopicAliasingOptions.InboundTopicAliasBehaviorType behavior) { + switch (behavior) { + case Enabled: return "A"; + case Disabled: return "B"; + default: return null; + } + } + + /** + * Encodes a {@link TlsCipherPreference} into its single-character metrics value. + * + * @param pref the configured TLS cipher preference + * @return {@code "A"} for {@code TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05}, + * {@code "B"} for {@code TLS_CIPHER_PQ_DEFAULT}, + * {@code "C"} for {@code TLS_CIPHER_PREF_TLSv1_2_2025}, + * or {@code null} for system default/unknown + */ + private static String tlsCipherPreferenceValue(TlsCipherPreference pref) { + switch (pref) { + case TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05: return "A"; // TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05 + case TLS_CIPHER_PQ_DEFAULT: return "B"; // TLS_CIPHER_PQ_DEFAULT + case TLS_CIPHER_PREF_TLSv1_2_2025: return "C"; // TLS_CIPHER_PREF_TLSv1_2_2025 + default: return null; + } + } + + /** + * Encodes a minimum {@link TlsContextOptions.TlsVersions TLS version} into its single-character metrics value. + * + * @param version the configured minimum TLS version + * @return {@code "A"} for SSLv3, {@code "B"} for TLSv1, {@code "C"} for TLSv1.1, + * {@code "D"} for TLSv1.2, {@code "E"} for TLSv1.3, or {@code null} for system default/unknown + */ + private static String minimumTlsVersionValue(TlsContextOptions.TlsVersions version) { + switch (version) { + case SSLv3: return "A"; // SSLv3 + case TLSv1: return "B"; // TLSv1 + case TLSv1_1: return "C"; // TLSv1_1 + case TLSv1_2: return "D"; // TLSv1_2 + case TLSv1_3: return "E"; // TLSv1_3 + default: return null; + } + } + + /** + * Tests whether a string parses as a base-10 integer. + * + * @param str the string to test (may be {@code null}) + * @return {@code true} if {@code str} parses via {@link Integer#parseInt(String)}, {@code false} otherwise */ private static boolean isNumeric(String str) { try { @@ -454,4 +449,3 @@ private static boolean isNumeric(String str) { } } } - diff --git a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java similarity index 91% rename from src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java rename to src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java index 19032cf65..26c29d920 100644 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTMetricsMetadata.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java @@ -2,10 +2,9 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ -package software.amazon.awssdk.crt.internal; +package software.amazon.awssdk.crt.iot; /** - * @internal * 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. diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java index 325d60718..29c30f1d4 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java @@ -17,8 +17,7 @@ import software.amazon.awssdk.crt.mqtt5.Mqtt5Client; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; import software.amazon.awssdk.crt.mqtt5.packets.ConnectPacket; -import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; -import software.amazon.awssdk.crt.internal.IoTMetricEncoder; +import software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -166,13 +165,7 @@ private void SetupConfig(MqttConnectionConfig config) throws MqttException { } if (!config.getDisableMetrics()) { - TlsContext tls = null; - if (config.getMqttClient()!=null){ - tls = config.getMqttClient().getTlsContext(); - } else if (config.getMqtt5Client()!=null){ - tls = config.getMqtt5Client().getClientOptions().getTlsContext(); - } - IoTDeviceSDKMetrics metrics = IoTMetricEncoder.createMetricsMqtt3(config.getMetrics(), config.getHttpProxyOptions(), tls); + IoTDeviceSDKMetrics metrics = IoTDeviceSDKMetrics.createMetricsMqtt3(config); mqttClientConnectionSetMetrics(getNativeHandle(), metrics); } diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java index 0b5ef6d52..12d3b6db9 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -9,7 +9,7 @@ import software.amazon.awssdk.crt.CrtResource; import software.amazon.awssdk.crt.http.HttpProxyOptions; -import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics; import software.amazon.awssdk.crt.io.ClientTlsContext; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.mqtt5.Mqtt5Client; diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java index b81c905e0..d98e988b5 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java @@ -12,8 +12,7 @@ import software.amazon.awssdk.crt.mqtt5.packets.ConnectPacket; import software.amazon.awssdk.crt.mqtt.MqttConnectionConfig; -import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; -import software.amazon.awssdk.crt.internal.IoTMetricEncoder; +import software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics; import java.util.Map; import java.util.function.Function; @@ -50,6 +49,7 @@ public class Mqtt5ClientOptions { // Opt-out flag for AWS IoT Metrics. When true, metrics are disabled. // Default is false (metrics enabled). private boolean disableMetrics = false; + private IoTDeviceSDKMetrics userMetrics; private IoTDeviceSDKMetrics iotDeviceSDKMetrics; @@ -279,6 +279,15 @@ public boolean getDisableMetrics() { return this.disableMetrics; } + /** + * Returns the user-provided metrics configuration from the IoT SDK layer. + * + * @return the user metrics, or null if none were provided + */ + public IoTDeviceSDKMetrics getUserMetrics() { + return this.userMetrics; + } + /** * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. * Default is false (metrics enabled). @@ -316,10 +325,11 @@ public Mqtt5ClientOptions(Mqtt5ClientOptionsBuilder builder) { this.publishEvents = builder.publishEvents; this.topicAliasingOptions = builder.topicAliasingOptions; this.disableMetrics = builder.disableMetrics; + this.userMetrics = builder.metrics; if (this.disableMetrics) { this.iotDeviceSDKMetrics = null; } else { - this.iotDeviceSDKMetrics = IoTMetricEncoder.createMetricsMqtt5(this, builder.metrics); + this.iotDeviceSDKMetrics = IoTDeviceSDKMetrics.createMetricsMqtt5(this); } } diff --git a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json index 82e69edd3..7e348dfc8 100644 --- a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json +++ b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json @@ -1268,7 +1268,7 @@ ] }, { - "name": "software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics", + "name": "software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics", "fields": [ { "name": "libraryName" @@ -1279,7 +1279,7 @@ ] }, { - "name": "software.amazon.awssdk.crt.internal.IoTMetricsMetadata", + "name": "software.amazon.awssdk.crt.iot.IoTMetricsMetadata", "fields": [ { "name": "key" diff --git a/src/native/java_class_ids.c b/src/native/java_class_ids.c index 951be72be..5331af998 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -1767,7 +1767,7 @@ static void s_cache_mqtt5_client_options(JNIEnv *env) { env, mqtt5_client_options_properties.client_options_class, "iotDeviceSDKMetrics", - "Lsoftware/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics;"); + "Lsoftware/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics;"); AWS_FATAL_ASSERT(mqtt5_client_options_properties.iot_device_sdk_metrics_field_id); } @@ -2671,7 +2671,7 @@ static void s_cache_cognito_credentials_provider(JNIEnv *env) { struct java_iot_device_sdk_metrics_properties iot_device_sdk_metrics_properties; static void s_cache_iot_device_sdk_metrics(JNIEnv *env) { - jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics"); + jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics"); AWS_FATAL_ASSERT(cls); iot_device_sdk_metrics_properties.iot_device_sdk_metrics_class = (*env)->NewGlobalRef(env, cls); AWS_FATAL_ASSERT(iot_device_sdk_metrics_properties.iot_device_sdk_metrics_class); @@ -2688,7 +2688,7 @@ static void s_cache_iot_device_sdk_metrics(JNIEnv *env) { struct java_iot_metrics_metadata_properties iot_metrics_metadata_properties; static void s_cache_iot_metrics_metadata(JNIEnv *env) { - jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/internal/IoTMetricsMetadata"); + jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/iot/IoTMetricsMetadata"); AWS_FATAL_ASSERT(cls); iot_metrics_metadata_properties.iot_metrics_metadata_class = (*env)->NewGlobalRef(env, cls); AWS_FATAL_ASSERT(iot_metrics_metadata_properties.iot_metrics_metadata_class); diff --git a/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java b/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java index 2d0bfc006..351622592 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java @@ -7,9 +7,9 @@ import org.junit.Test; import static org.junit.Assert.*; -import software.amazon.awssdk.crt.internal.IoTMetricEncoder; -import software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics; -import software.amazon.awssdk.crt.internal.IoTMetricsMetadata; +import software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.iot.IoTMetricsMetadata; +import software.amazon.awssdk.crt.mqtt.MqttConnectionConfig; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions.Mqtt5ClientOptionsBuilder; import software.amazon.awssdk.crt.mqtt5.Mqtt5ClientOptions.ClientSessionBehavior; @@ -20,42 +20,47 @@ import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions.JitterMode; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class IoTMetricEncoderTest extends CrtTestFixture { public IoTMetricEncoderTest() {} + private MqttConnectionConfig createMqtt3Config(IoTDeviceSDKMetrics userMetrics) { + MqttConnectionConfig config = new MqttConnectionConfig(); + if (userMetrics != null) { + config.setMetrics(userMetrics); + } + return config; + } + // ======================== Minimal Options Encoding ======================== @Test public void testMqtt5Minimal() { Mqtt5ClientOptionsBuilder builder = new Mqtt5ClientOptionsBuilder("localhost", 8883L); - builder.withDisableMetrics(true); // disable so constructor doesn't call encoder + builder.withDisableMetrics(true); Mqtt5ClientOptions options = builder.build(); - String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); - - // Should only have F (protocol=5) and G (socket) - assertTrue(result.contains("F/5")); - assertTrue(result.contains("G/")); - // Should NOT have A, B, C, D, E since all are DEFAULT - assertFalse(result.contains("A/")); - assertFalse(result.contains("B/")); - assertFalse(result.contains("C/")); - assertFalse(result.contains("D/")); - assertFalse(result.contains("E/")); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertTrue(feature.contains("F/5")); + assertTrue(feature.contains("G/")); + assertFalse(feature.contains("A/")); + assertFalse(feature.contains("B/")); + assertFalse(feature.contains("C/")); + assertFalse(feature.contains("D/")); + assertFalse(feature.contains("E/")); } @Test public void testMqtt3Minimal() { - String result = IoTMetricEncoder.getEncodedFeatureListMqtt3(null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - assertTrue(result.contains("F/3")); - assertTrue(result.contains("G/")); - // Only F and G - assertEquals(2, result.split(",").length); + assertTrue(feature.contains("F/3")); + assertTrue(feature.contains("G/")); } // ======================== Non-Default Features Encoding ======================== @@ -74,14 +79,15 @@ public void testMqtt5WithNonDefaultFeatures() { builder.withDisableMetrics(true); Mqtt5ClientOptions options = builder.build(); - String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - assertTrue(result.contains("A/B")); // Full - assertTrue(result.contains("B/A")); // CLEAN - assertTrue(result.contains("C/C")); // FAIL_ALL - assertTrue(result.contains("D/B")); // LRU - assertTrue(result.contains("E/A")); // Enabled - assertTrue(result.contains("F/5")); // MQTT5 + assertTrue(feature.contains("A/B")); // Full + assertTrue(feature.contains("B/A")); // CLEAN + assertTrue(feature.contains("C/C")); // FAIL_ALL + assertTrue(feature.contains("D/B")); // LRU + assertTrue(feature.contains("E/A")); // Enabled + assertTrue(feature.contains("F/5")); // MQTT5 } @Test @@ -93,67 +99,57 @@ public void testDefaultEnumValuesOmitted() { builder.withDisableMetrics(true); Mqtt5ClientOptions options = builder.build(); - String result = IoTMetricEncoder.getEncodedFeatureListMqtt5(options); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - assertFalse(result.contains("A/")); - assertFalse(result.contains("B/")); - assertFalse(result.contains("C/")); + assertFalse(feature.contains("A/")); + assertFalse(feature.contains("B/")); + assertFalse(feature.contains("C/")); } - // ======================== Feature List Merging ======================== + // ======================== Feature Merging ======================== @Test public void testUserOverridesCrt() { - String result = IoTMetricEncoder.mergeFeatureLists("A/B,F/5", "A/C"); - assertEquals("A/C,F/5", result); - } - - @Test - public void testUserOverridesMultipleCrtFeatures() { - String result = IoTMetricEncoder.mergeFeatureLists("A/B,F/5,G/A,K/D", "A/C,F/3,K/E"); - assertEquals("A/C,F/3,G/A,K/E", result); - } + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "1")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "F/9")); + user.setMetadataEntries(userEntries); - @Test - public void testEmptyUserFeatures() { - String result = IoTMetricEncoder.mergeFeatureLists("F/5,G/A", ""); - assertEquals("F/5,G/A", result); - } + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - @Test - public void testEmptyCrtFeatures() { - String result = IoTMetricEncoder.mergeFeatureLists("", "A/B"); - assertEquals("A/B", result); + assertTrue(feature.contains("F/9")); + assertFalse(feature.contains("F/3")); } @Test - public void testBothEmpty() { - String result = IoTMetricEncoder.mergeFeatureLists("", ""); - assertEquals("", result); - } + public void testDisjointFeaturesAreMerged() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "1")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A,K/D")); + user.setMetadataEntries(userEntries); - @Test - public void testDisjointFeatures() { - String result = IoTMetricEncoder.mergeFeatureLists("F/5,G/A", "I/A,K/D"); - assertEquals("F/5,G/A,I/A,K/D", result); - } + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - @Test - public void testInsertionOrderPreserved() { - String result = IoTMetricEncoder.mergeFeatureLists("G/A,F/5", "A/B"); - assertEquals("G/A,F/5,A/B", result); + assertTrue(feature.contains("F/3")); + assertTrue(feature.contains("G/")); + assertTrue(feature.contains("I/A")); + assertTrue(feature.contains("K/D")); } // ======================== Create Metrics - Default Options ======================== @Test public void testCreateMetricsNullUserMetrics() { - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(null, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); assertNotNull(result.getMetadataEntries()); - // Should have CRTVersion, IoTSDKFeature, IoTSDKMetricsVersion List entries = result.getMetadataEntries(); String crtVersion = findMetadataValue(entries, "CRTVersion"); String feature = findMetadataValue(entries, "IoTSDKFeature"); @@ -167,11 +163,11 @@ public void testCreateMetricsNullUserMetrics() { @Test public void testCreateMetricsEmptyUserMetrics() { IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); - assertTrue(feature.contains("F/3")); // MQTT3 protocol + assertTrue(feature.contains("F/3")); } // ======================== Create Metrics - User Features Merged ======================== @@ -184,7 +180,7 @@ public void testUserFeatureAddedWhenVersionMatches() { userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); assertTrue(feature.contains("I/A")); @@ -200,11 +196,10 @@ public void testUserFeatureOverridesCrt() { userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "F/3,I/B")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); assertTrue(feature.contains("F/3")); - assertFalse(feature.contains("F/5")); assertTrue(feature.contains("I/B")); } @@ -218,7 +213,7 @@ public void testUserFeaturesIgnoredOnHigherVersion() { userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); assertFalse(feature.contains("I/A")); @@ -233,7 +228,7 @@ public void testUserFeaturesIgnoredOnNonNumericVersion() { userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); assertFalse(feature.contains("I/A")); @@ -246,7 +241,7 @@ public void testUserFeaturesIgnoredWhenNoVersion() { userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "I/A")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); assertFalse(feature.contains("I/A")); @@ -261,7 +256,7 @@ public void testCrtVersionCannotBeOverridden() { userEntries.add(new IoTMetricsMetadata("CRTVersion", "fake_version")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String crtVersion = findMetadataValue(result.getMetadataEntries(), "CRTVersion"); assertNotEquals("fake_version", crtVersion); @@ -276,7 +271,7 @@ public void testSdkVersionPreserved() { userEntries.add(new IoTMetricsMetadata("IoTSDKVersion", "2.0.0")); user.setMetadataEntries(userEntries); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); String sdkVersion = findMetadataValue(result.getMetadataEntries(), "IoTSDKVersion"); assertEquals("2.0.0", sdkVersion); @@ -285,14 +280,14 @@ public void testSdkVersionPreserved() { @Test public void testCustomLibraryName() { IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics("MyCustomSDK/1.0", null); - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(user, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); assertEquals("MyCustomSDK/1.0", result.getLibraryName()); } @Test public void testMetricsVersionAlwaysSet() { - IoTDeviceSDKMetrics result = IoTMetricEncoder.createMetricsMqtt3(null, null, null); + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); String metricsVersion = findMetadataValue(result.getMetadataEntries(), "IoTSDKMetricsVersion"); assertEquals("1", metricsVersion); From e8f71961aa832ebb858832baca43ec15e57ff398 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 10:30:47 -0700 Subject: [PATCH 08/16] logging in Jni --- src/native/iot_device_sdk_metrics.c | 94 ++++++++++++++++++----------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index db8879f1d..fdcc03c1f 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -2,6 +2,16 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ + +/** + * JNI bridge for IoT Device SDK Metrics. + * + * This file is responsible for converting the Java IoTDeviceSDKMetrics object + * (library name + metadata key-value entries) into a native aws_mqtt_iot_metrics + * struct that the C MQTT layer uses to append SDK telemetry to the CONNECT packet + * username field. + * + */ #include #include "iot_device_sdk_metrics.h" @@ -12,6 +22,7 @@ static char s_iot_device_sdk_metrics_string[] = "IoTDeviceSDKMetrics"; +/* Frees all native memory associated with a parsed metrics struct. */ void aws_mqtt_iot_metrics_java_jni_destroy( JNIEnv *env, struct aws_allocator *allocator, @@ -38,14 +49,17 @@ void aws_mqtt_iot_metrics_java_jni_destroy( aws_mem_release(allocator, java_metrics); } +/* Parses a Java IoTDeviceSDKMetrics object into a native metrics struct for the C MQTT layer. */ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_java( JNIEnv *env, struct aws_allocator *allocator, jobject java_iot_device_sdk_metrics) { + /* Allocate the holder struct (zero-initialized) */ struct aws_mqtt_iot_metrics_java_jni *java_metrics = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); + /* Extract the library name string (e.g. "IoTDeviceSDK/Java") */ if (aws_get_string_from_jobject( env, java_iot_device_sdk_metrics, @@ -67,6 +81,7 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ if (metadata_list != NULL && !aws_jni_check_and_clear_exception(env)) { jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); if (count > 0 && !aws_jni_check_and_clear_exception(env)) { + /* Pre-allocate entry array for all metadata key-value pairs */ java_metrics->metadata_entries = aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); @@ -75,29 +90,37 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ for (jint i = 0; i < count; i++) { jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - continue; + AWS_LOGF_ERROR( + AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get metadata entry at index %d", (int)i); + goto on_error; } jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); - if (key_jstr != NULL && value_jstr != NULL) { - total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); - total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); - } + total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); + total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); (*env)->DeleteLocalRef(env, key_jstr); (*env)->DeleteLocalRef(env, value_jstr); (*env)->DeleteLocalRef(env, entry); } + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: first pass complete, total_size=%zu for %d entries", + total_size, + (int)count); + /* Allocate exact size */ aws_byte_buf_init(&java_metrics->metadata_storage, allocator, total_size); - /* Second pass: copy strings and create cursors */ + /* Second pass: copy strings into contiguous buffer and set cursors pointing into it */ for (jint i = 0; i < count; i++) { jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - continue; + AWS_LOGF_ERROR( + AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get metadata entry at index %d", (int)i); + goto on_error; } jstring key_jstr = @@ -105,42 +128,43 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); - if (key_jstr != NULL && value_jstr != NULL) { - const char *key_chars = (*env)->GetStringUTFChars(env, key_jstr, NULL); - size_t key_len = (size_t)(*env)->GetStringUTFLength(env, key_jstr); - const char *value_chars = (*env)->GetStringUTFChars(env, value_jstr, NULL); - size_t value_len = (size_t)(*env)->GetStringUTFLength(env, value_jstr); - - /* Append key to storage buffer */ - size_t key_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append( - &java_metrics->metadata_storage, - &(struct aws_byte_cursor){.ptr = (uint8_t *)key_chars, .len = key_len}); - - /* Append value to storage buffer */ - size_t value_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append( - &java_metrics->metadata_storage, - &(struct aws_byte_cursor){.ptr = (uint8_t *)value_chars, .len = value_len}); - - java_metrics->metadata_entries[java_metrics->metadata_count].key = - aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + key_offset, key_len); - java_metrics->metadata_entries[java_metrics->metadata_count].value = - aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + value_offset, value_len); - java_metrics->metadata_count++; - - (*env)->ReleaseStringUTFChars(env, key_jstr, key_chars); - (*env)->ReleaseStringUTFChars(env, value_jstr, value_chars); - } + struct aws_byte_cursor key_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); + size_t key_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append(&java_metrics->metadata_storage, &key_cursor); + aws_jni_byte_cursor_from_jstring_release(env, key_jstr, key_cursor); + + struct aws_byte_cursor value_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, value_jstr); + size_t value_offset = java_metrics->metadata_storage.len; + aws_byte_buf_append(&java_metrics->metadata_storage, &value_cursor); + aws_jni_byte_cursor_from_jstring_release(env, value_jstr, value_cursor); + + java_metrics->metadata_entries[java_metrics->metadata_count].key = + aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + key_offset, key_cursor.len); + java_metrics->metadata_entries[java_metrics->metadata_count].value = + aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + value_offset, value_cursor.len); + + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics metadata[%zu]: key=\"" PRInSTR "\" value=\"" PRInSTR "\"", + java_metrics->metadata_count, + AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[java_metrics->metadata_count].key), + AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[java_metrics->metadata_count].value)); + + java_metrics->metadata_count++; (*env)->DeleteLocalRef(env, key_jstr); (*env)->DeleteLocalRef(env, value_jstr); (*env)->DeleteLocalRef(env, entry); } - /* Set the metrics struct fields */ + /* Wire up the C metrics struct to point at our parsed data */ java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; java_metrics->metrics.metadata_count = java_metrics->metadata_count; + + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: parsing complete, %zu metadata entries set", + java_metrics->metadata_count); } (*env)->DeleteLocalRef(env, metadata_list); From d3113feeda520f02f6027d2e327a564b19b88d91 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 10:36:12 -0700 Subject: [PATCH 09/16] refactoring parameter --- .../amazon/awssdk/crt/mqtt/MqttConnectionConfig.java | 6 +++--- .../amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java index 12d3b6db9..92e834b72 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -547,10 +547,10 @@ public Consumer getWebsocketHandshakeTransform( * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. * Default is false (metrics enabled). * - * @param disable true to disable metrics, false to enable (default) + * @param disableMetrics true to disable metrics, false to enable (default) */ - public void setDisableMetrics(boolean disable) { - this.disableMetrics = disable; + public void setDisableMetrics(boolean disableMetrics) { + this.disableMetrics = disableMetrics; } /** diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java index d98e988b5..5421127b6 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java @@ -292,10 +292,10 @@ public IoTDeviceSDKMetrics getUserMetrics() { * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. * Default is false (metrics enabled). * - * @param disable true to disable metrics, false to enable (default) + * @param disableMetrics true to disable metrics, false to enable (default) */ - public void setDisableMetrics(boolean disable) { - this.disableMetrics = disable; + public void setDisableMetrics(boolean disableMetrics) { + this.disableMetrics = disableMetrics; } /** @@ -906,11 +906,11 @@ public Mqtt5ClientOptionsBuilder withTopicAliasingOptions(TopicAliasingOptions o * Disables IoT Device SDK metrics collection. The metrics includes SDK name, version, and platform. * Default is false (metrics enabled). * - * @param disable true to disable metrics, false to enable (default) + * @param disableMetrics true to disable metrics, false to enable (default) * @return The Mqtt5ClientOptionsBuilder after setting the metrics option */ - public Mqtt5ClientOptionsBuilder withDisableMetrics(boolean disable) { - this.disableMetrics = disable; + public Mqtt5ClientOptionsBuilder withDisableMetrics(boolean disableMetrics) { + this.disableMetrics = disableMetrics; return this; } From 381599e7828eb0f8418ccaff19785ae3cbd8fe27 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 15:16:46 -0700 Subject: [PATCH 10/16] common helper function --- .../awssdk/crt/iot/IoTDeviceSDKMetrics.java | 46 +-- src/native/iot_device_sdk_metrics.c | 267 ++++++++++++------ 2 files changed, 199 insertions(+), 114 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index 3e7a9409d..4a31cd6ed 100644 --- a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -46,6 +46,7 @@ public class IoTDeviceSDKMetrics { public IoTDeviceSDKMetrics() { this.libraryName = "IoTDeviceSDK/Java"; + this.metadataEntries = new ArrayList<>(); } public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { @@ -108,14 +109,14 @@ public static IoTDeviceSDKMetrics createMetricsMqtt3(MqttConnectionConfig config * - G (socket_implementation): detected from platform * * Conditionally includes (only when not DEFAULT): - * A (retry_jitter_mode), - * B (session_behavior), - * C (offline_queue_behavior), - * D (outbound_topic_alias), - * E (inbound_topic_alias), - * H (http_proxy_type), - * J (tls_cipher_preference), - * K (minimum_tls_version) + * - A (retry_jitter_mode), + * - B (session_behavior), + * - C (offline_queue_behavior), + * - D (outbound_topic_alias), + * - E (inbound_topic_alias), + * - H (http_proxy_type), + * - J (tls_cipher_preference), + * - K (minimum_tls_version) * * Feature I (certificate_source) is set at the IoT SDK level, not here. * @@ -184,7 +185,9 @@ private static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOption * - G (socket_implementation): detected from platform * * Conditionally includes: - * H (http_proxy_type), J (tls_cipher_preference), K (minimum_tls_version) + * - H (http_proxy_type) + * - J (tls_cipher_preference) + * - K (minimum_tls_version) * * @param proxyOptions optional HTTP proxy options from the connection * @param tlsCtx optional TLS context used by the connection @@ -235,11 +238,12 @@ private static String mergeFeatureLists(String crtFeatures, String userFeatures) /** + *

* Metrics creation logic: - * 1. CRTVersion: auto-set from package version, not overridable by user - * 2. IoTSDKMetricsVersion: always set to current IOT_SDK_METRICS_FEATURE_VERSION - * 3. IoTSDKFeature: merged if user version matches, else CRT only - * 4. Other user metadata: passed through unchanged + * - CRTVersion: auto-set from package version, not overridable by user + * - IoTSDKMetricsVersion: always set to current IOT_SDK_METRICS_FEATURE_VERSION + * - IoTSDKFeature: merged if user version matches, else CRT only + * - Other user metadata: passed through unchanged * * @param userMetrics optional metrics from a higher-level IoT SDK (may be {@code null}) * @param crtFeatureList encoded CRT feature list string @@ -267,8 +271,7 @@ private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics } } - if (userMetricsVersion != null && isNumeric(userMetricsVersion) - && Integer.parseInt(userMetricsVersion) == IOT_SDK_METRICS_FEATURE_VERSION) { + if (parsedVersionMatches(userMetricsVersion)) { metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, userFeature)); } else { metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, "")); @@ -435,15 +438,16 @@ private static String minimumTlsVersionValue(TlsContextOptions.TlsVersions versi } /** - * Tests whether a string parses as a base-10 integer. + * Returns {@code true} when {@code userMetricsVersion} parses as an integer equal to + * {@link #IOT_SDK_METRICS_FEATURE_VERSION}. Null and non-numeric inputs return {@code false}. * - * @param str the string to test (may be {@code null}) - * @return {@code true} if {@code str} parses via {@link Integer#parseInt(String)}, {@code false} otherwise + * @param userMetricsVersion the user-supplied {@code IoTSDKMetricsVersion} string (may be {@code null}) + * @return {@code true} if the userMetricsVersion matches the current schema, {@code false} otherwise */ - private static boolean isNumeric(String str) { + private static boolean parsedVersionMatches(String userMetricsVersion) { + if (userMetricsVersion == null) return false; try { - Integer.parseInt(str); - return true; + return Integer.parseInt(userMetricsVersion) == IOT_SDK_METRICS_FEATURE_VERSION; } catch (NumberFormatException e) { return false; } diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index fdcc03c1f..5fdcdfb74 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -22,6 +22,130 @@ static char s_iot_device_sdk_metrics_string[] = "IoTDeviceSDKMetrics"; +/******************************************************************************* + * HELPER FUNCTIONS + ******************************************************************************/ +/** + * Parse a java list of objects (each having a String key field and a String value field) + * into a contiguous buffer. + * + * Two-pass approach: + * Pass 1 - measure total byte size of all keys and value + * Pass 2 - copy into one buffer, set cursors, pointing into it. + * + * @param env JNI environment + * @param allocator CRT allocator + * @param java_list Java List jobject (must not be NULL, count must be > 0) + * @param count Number of elements in the list + * @param key_field_id JNI field ID for the String key on each list element + * @param value_field_id JNI field ID for the String value on each list element + * @param out_storage Output buffer that will own all string bytes + * @param out_entries Pre-allocated array of count entries to populate + * + * @return AWS_OP_SUCCESS or AWS_OP_ERR + */ +static int s_parse_string_pair_list( + JNIEnv *env, + struct aws_allocator *allocator, + jobject java_list, + size_t count, + jfieldID key_field_id, + jfieldID value_field_id, + struct aws_byte_buf *out_storage, + struct aws_mqtt_metadata_entry *out_entries) { + + /* + * First pass: iterate all entries to compute the total byte size needed for + * all key and value strings combined. This lets us do a single allocation. + */ + size_t total_size = 0; + for (size_t i = 0; i < count; i++) { + /* Call List.get(i) to retrieve the list object at this index */ + jobject entry = (*env)->CallObjectMethod(env, java_list, boxed_list_properties.list_get_id, (jint)i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); + return AWS_OP_ERR; + } + + /* Read the key and value String fields from the entry */ + jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, key_field_id); + jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, value_field_id); + + /* Accumulate byte lengths (modified-UTF8). Empty strings contribute 0 — that's valid. */ + total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); + total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); + + /* Release local refs to avoid exhausting JNI local ref table in large lists */ + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: first pass complete, total_size=%zu for %d entries", + total_size, + (int)count); + /* Allocate one contiguous buffer for all strings */ + if (aws_byte_buf_init(out_storage, allocator, total_size)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to allocate %zu bytes for metadata storage", total_size); + return AWS_OP_ERR; + } + + /* + * Second pass: copy each key/value string into the contiguous buffer and + * build aws_byte_cursor structs that point into it at the correct offsets. + */ + for (size_t i = 0; i < count; i++) { + jobject entry = (*env)->CallObjectMethod(env, java_list, boxed_list_properties.list_get_id, (jint)i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); + return AWS_OP_ERR; + } + + jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, key_field_id); + jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, value_field_id); + + /* Acquire the raw UTF-8 bytes from the JVM for the key string */ + struct aws_byte_cursor key_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); + + /* Record where in the buffer this key will start */ + size_t key_offset = out_storage->len; + /* Copy key bytes into our contiguous buffer */ + aws_byte_buf_append(out_storage, &key_cursor); + /* Release the JVM's internal string memory*/ + aws_jni_byte_cursor_from_jstring_release(env, key_jstr, key_cursor); + + /* same pattern as above */ + struct aws_byte_cursor value_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, value_jstr); + size_t value_offset = out_storage->len; + aws_byte_buf_append(out_storage, &value_cursor); + aws_jni_byte_cursor_from_jstring_release(env, value_jstr, value_cursor); + + out_entries[i].key = aws_byte_cursor_from_array(out_storage->buffer + key_offset, key_cursor.len); + out_entries[i].value = aws_byte_cursor_from_array(out_storage->buffer + value_offset, value_cursor.len); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: metadata[%d] key=\"" PRInSTR "\" value=\"" PRInSTR "\"", + (int)i, + AWS_BYTE_CURSOR_PRI(out_entries[i].key), + AWS_BYTE_CURSOR_PRI(out_entries[i].value)); + + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: second pass complete, %d entries parsed into %zu bytes", + (int)count, + out_storage->len); + + return AWS_OP_SUCCESS; +} + /* Frees all native memory associated with a parsed metrics struct. */ void aws_mqtt_iot_metrics_java_jni_destroy( JNIEnv *env, @@ -55,11 +179,13 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ struct aws_allocator *allocator, jobject java_iot_device_sdk_metrics) { - /* Allocate the holder struct (zero-initialized) */ + jobject metadata_list = NULL; + struct aws_mqtt_iot_metrics_java_jni *java_metrics = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); - /* Extract the library name string (e.g. "IoTDeviceSDK/Java") */ + AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: Creating IoTDeviceSDKMetrics from Java object", (void *)java_metrics); + /* Extract the library name string */ if (aws_get_string_from_jobject( env, java_iot_device_sdk_metrics, @@ -74,106 +200,61 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ goto on_error; } - /* Parse metadataEntries list */ - jobject metadata_list = (*env)->GetObjectField( + /* Get the metadata list */ + metadata_list = (*env)->GetObjectField( env, java_iot_device_sdk_metrics, iot_device_sdk_metrics_properties.metadata_entries_field_id); - if (metadata_list != NULL && !aws_jni_check_and_clear_exception(env)) { - jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); - if (count > 0 && !aws_jni_check_and_clear_exception(env)) { - /* Pre-allocate entry array for all metadata key-value pairs */ - java_metrics->metadata_entries = - aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - - /* First pass: compute exact buffer size needed */ - size_t total_size = 0; - for (jint i = 0; i < count; i++) { - jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get metadata entry at index %d", (int)i); - goto on_error; - } - jstring key_jstr = - (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); - jstring value_jstr = - (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); - total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); - total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); - (*env)->DeleteLocalRef(env, entry); - } - - AWS_LOGF_INFO( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics: first pass complete, total_size=%zu for %d entries", - total_size, - (int)count); - - /* Allocate exact size */ - aws_byte_buf_init(&java_metrics->metadata_storage, allocator, total_size); - - /* Second pass: copy strings into contiguous buffer and set cursors pointing into it */ - for (jint i = 0; i < count; i++) { - jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get metadata entry at index %d", (int)i); - goto on_error; - } - - jstring key_jstr = - (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); - jstring value_jstr = - (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); - - struct aws_byte_cursor key_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); - size_t key_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append(&java_metrics->metadata_storage, &key_cursor); - aws_jni_byte_cursor_from_jstring_release(env, key_jstr, key_cursor); - - struct aws_byte_cursor value_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, value_jstr); - size_t value_offset = java_metrics->metadata_storage.len; - aws_byte_buf_append(&java_metrics->metadata_storage, &value_cursor); - aws_jni_byte_cursor_from_jstring_release(env, value_jstr, value_cursor); - - java_metrics->metadata_entries[java_metrics->metadata_count].key = - aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + key_offset, key_cursor.len); - java_metrics->metadata_entries[java_metrics->metadata_count].value = - aws_byte_cursor_from_array(java_metrics->metadata_storage.buffer + value_offset, value_cursor.len); - - AWS_LOGF_INFO( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics metadata[%zu]: key=\"" PRInSTR "\" value=\"" PRInSTR "\"", - java_metrics->metadata_count, - AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[java_metrics->metadata_count].key), - AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[java_metrics->metadata_count].value)); - - java_metrics->metadata_count++; - - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); - (*env)->DeleteLocalRef(env, entry); - } - - /* Wire up the C metrics struct to point at our parsed data */ - java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; - java_metrics->metrics.metadata_count = java_metrics->metadata_count; - - AWS_LOGF_INFO( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics: parsing complete, %zu metadata entries set", - java_metrics->metadata_count); - } + if (metadata_list == NULL || aws_jni_check_and_clear_exception(env)) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "id=%p: IoTDeviceSDKMetrics no metadata entries, returning with library name only", + (void *)java_metrics); + return java_metrics; + } + jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); + if (aws_jni_check_and_clear_exception(env) || count <= 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "id=%p: IoTDeviceSDKMetrics metadata list empty or size() failed", + (void *)java_metrics); (*env)->DeleteLocalRef(env, metadata_list); + return java_metrics; } + /* Allocate entry array */ + java_metrics->metadata_entries = aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); + + /* Use helper to do the two-pass parse */ + if (s_parse_string_pair_list( + env, + allocator, + metadata_list, + (size_t)count, + iot_metrics_metadata_properties.key_field_id, + iot_metrics_metadata_properties.value_field_id, + &java_metrics->metadata_storage, + java_metrics->metadata_entries) == AWS_OP_ERR) { + goto on_error; + } + + /* Wire up the C struct */ + java_metrics->metadata_count = (size_t)count; + java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; + java_metrics->metrics.metadata_count = (size_t)count; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "id=%p: IoTDeviceSDKMetrics creation complete, %d metadata entries", + (void *)java_metrics, + (int)count); + (*env)->DeleteLocalRef(env, metadata_list); return java_metrics; on_error: - /* clean up */ + if (metadata_list != NULL) { + (*env)->DeleteLocalRef(env, metadata_list); + } aws_mqtt_iot_metrics_java_jni_destroy(env, allocator, java_metrics); return NULL; } From 13d4d1e3f007c6761cd6903dd1e45d1cd3c4af90 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 16:10:35 -0700 Subject: [PATCH 11/16] refactoring --- .../awssdk/crt/iot/IoTDeviceSDKMetrics.java | 8 +------- .../awssdk/crt/mqtt/MqttConnectionConfig.java | 17 +++++++++++++++++ src/native/iot_device_sdk_metrics.c | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index 4a31cd6ed..857e081f9 100644 --- a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -85,13 +85,7 @@ public static IoTDeviceSDKMetrics createMetricsMqtt5(Mqtt5ClientOptions clientOp * @return the merged metrics object ready to be passed to JNI */ public static IoTDeviceSDKMetrics createMetricsMqtt3(MqttConnectionConfig config) { - TlsContext tls = null; - if (config.getMqttClient() != null) { - tls = config.getMqttClient().getTlsContext(); - } else if (config.getMqtt5Client() != null) { - tls = config.getMqtt5Client().getClientOptions().getTlsContext(); - } - String crtFeatureList = getEncodedFeatureListMqtt3(config.getHttpProxyOptions(), tls); + String crtFeatureList = getEncodedFeatureListMqtt3(config.getHttpProxyOptions(), config.getTlsContext()); return createMetrics(config.getMetrics(), crtFeatureList); } diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java index 92e834b72..4d15832a9 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -13,6 +13,8 @@ import software.amazon.awssdk.crt.io.ClientTlsContext; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.mqtt5.Mqtt5Client; +import software.amazon.awssdk.crt.io.TlsContext; + /** * Encapsulates all per-mqtt-connection configuration @@ -581,6 +583,21 @@ public IoTDeviceSDKMetrics getMetrics() { return metrics; } + /** + * Returns the {@link TlsContext} this connection will be resolved from + * the underlying {@link MqttClient} or {@link Mqtt5Client}. + * + * @return the TLS context, or {@code null} if neither client is set or no TLS is configured + */ + public TlsContext getTlsContext() { + if (mqttClient != null) { + return mqttClient.getTlsContext(); + } else if (mqtt5Client != null) { + return mqtt5Client.getClientOptions().getTlsContext(); + } + return null; + } + /** * Creates a (shallow) clone of this config object diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 5fdcdfb74..41c054093 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -71,7 +71,7 @@ static int s_parse_string_pair_list( jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, key_field_id); jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, value_field_id); - /* Accumulate byte lengths (modified-UTF8). Empty strings contribute 0 — that's valid. */ + /* Accumulate byte lengths (modified-UTF8). */ total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); From 37d736f9f647c24f9e1e14fe112e6ce58621c889 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 18:36:54 -0700 Subject: [PATCH 12/16] updating list in jni --- .../awssdk/crt/iot/IoTDeviceSDKMetrics.java | 11 +- .../awssdk/crt/mqtt/MqttConnectionConfig.java | 1 - src/native/iot_device_sdk_metrics.c | 246 +++++++----------- src/native/iot_device_sdk_metrics.h | 2 +- 4 files changed, 98 insertions(+), 162 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index 857e081f9..2525bc798 100644 --- a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -62,7 +62,6 @@ public IoTDeviceSDKMetrics(String libraryName, List metadata public void setMetadataEntries(List metadataEntries) { this.metadataEntries = metadataEntries; } - /** * Builds the final metrics object for an MQTT5 client by encoding the CRT * feature list from {@code clientOptions} and merging it with any @@ -89,7 +88,6 @@ public static IoTDeviceSDKMetrics createMetricsMqtt3(MqttConnectionConfig config return createMetrics(config.getMetrics(), crtFeatureList); } - /** * Generates the encoded feature list string for metrics from MQTT5 client options. * @@ -230,7 +228,6 @@ private static String mergeFeatureLists(String crtFeatures, String userFeatures) return sb.toString(); } - /** *

* Metrics creation logic: @@ -336,7 +333,7 @@ private static String retryJitterModeValue(JitterMode mode) { /** * Encodes an MQTT5 session behavior enum value. * - * @param value the {@code ClientSessionBehavior} ordinal + * @param behavior the {@code ClientSessionBehavior} ordinal * @return {@code "A"} for CLEAN, {@code "B"} for REJOIN_POST_SUCCESS, {@code "C"} for REJOIN_ALWAYS, * or {@code null} for DEFAULT/unknown */ @@ -352,7 +349,7 @@ private static String sessionBehaviorValue(Mqtt5ClientOptions.ClientSessionBehav /** * Encodes an MQTT5 offline queue behavior enum value. * - * @param value the {@code ClientOfflineQueueBehavior} ordinal + * @param behavior the {@code ClientOfflineQueueBehavior} ordinal * @return {@code "A"} for FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, {@code "B"} for FAIL_QOS0_PUBLISH_ON_DISCONNECT, * {@code "C"} for FAIL_ALL_ON_DISCONNECT, or {@code null} for DEFAULT/unknown */ @@ -368,7 +365,7 @@ private static String offlineQueueBehaviorValue(Mqtt5ClientOptions.ClientOffline /** * Encodes an outbound topic alias behavior enum value. * - * @param value the {@code OutboundTopicAliasBehaviorType} ordinal + * @param behavior the {@code OutboundTopicAliasBehaviorType} ordinal * @return {@code "A"} for Manual, {@code "B"} for LRU, {@code "C"} for Disabled, * or {@code null} for Default/unknown */ @@ -384,7 +381,7 @@ private static String outboundTopicAliasBehaviorValue(TopicAliasingOptions.Outbo /** * Encodes an inbound topic alias behavior enum value. * - * @param value the {@code InboundTopicAliasBehaviorType} ordinal + * @param behavior the {@code InboundTopicAliasBehaviorType} ordinal * @return {@code "A"} for Enabled, {@code "B"} for Disabled, or {@code null} for Default/unknown */ private static String inboundTopicAliasBehaviorValue(TopicAliasingOptions.InboundTopicAliasBehaviorType behavior) { diff --git a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java index 4d15832a9..f3b5ce645 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -598,7 +598,6 @@ public TlsContext getTlsContext() { return null; } - /** * Creates a (shallow) clone of this config object * diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 41c054093..bc10f005c 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -6,10 +6,9 @@ /** * JNI bridge for IoT Device SDK Metrics. * - * This file is responsible for converting the Java IoTDeviceSDKMetrics object - * (library name + metadata key-value entries) into a native aws_mqtt_iot_metrics - * struct that the C MQTT layer uses to append SDK telemetry to the CONNECT packet - * username field. + * Converts a Java IoTDeviceSDKMetrics object (library name + metadata key-value + * entries) into a native aws_mqtt_iot_metrics struct that the C MQTT layer uses + * to append SDK telemetry to the CONNECT packet username field. * */ #include @@ -21,130 +20,11 @@ #include static char s_iot_device_sdk_metrics_string[] = "IoTDeviceSDKMetrics"; - -/******************************************************************************* - * HELPER FUNCTIONS - ******************************************************************************/ -/** - * Parse a java list of objects (each having a String key field and a String value field) - * into a contiguous buffer. - * - * Two-pass approach: - * Pass 1 - measure total byte size of all keys and value - * Pass 2 - copy into one buffer, set cursors, pointing into it. - * - * @param env JNI environment - * @param allocator CRT allocator - * @param java_list Java List jobject (must not be NULL, count must be > 0) - * @param count Number of elements in the list - * @param key_field_id JNI field ID for the String key on each list element - * @param value_field_id JNI field ID for the String value on each list element - * @param out_storage Output buffer that will own all string bytes - * @param out_entries Pre-allocated array of count entries to populate - * - * @return AWS_OP_SUCCESS or AWS_OP_ERR - */ -static int s_parse_string_pair_list( - JNIEnv *env, - struct aws_allocator *allocator, - jobject java_list, - size_t count, - jfieldID key_field_id, - jfieldID value_field_id, - struct aws_byte_buf *out_storage, - struct aws_mqtt_metadata_entry *out_entries) { - - /* - * First pass: iterate all entries to compute the total byte size needed for - * all key and value strings combined. This lets us do a single allocation. - */ - size_t total_size = 0; - for (size_t i = 0; i < count; i++) { - /* Call List.get(i) to retrieve the list object at this index */ - jobject entry = (*env)->CallObjectMethod(env, java_list, boxed_list_properties.list_get_id, (jint)i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); - return AWS_OP_ERR; - } - - /* Read the key and value String fields from the entry */ - jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, key_field_id); - jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, value_field_id); - - /* Accumulate byte lengths (modified-UTF8). */ - total_size += (size_t)(*env)->GetStringUTFLength(env, key_jstr); - total_size += (size_t)(*env)->GetStringUTFLength(env, value_jstr); - - /* Release local refs to avoid exhausting JNI local ref table in large lists */ - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); - (*env)->DeleteLocalRef(env, entry); - } - AWS_LOGF_DEBUG( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics: first pass complete, total_size=%zu for %d entries", - total_size, - (int)count); - /* Allocate one contiguous buffer for all strings */ - if (aws_byte_buf_init(out_storage, allocator, total_size)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to allocate %zu bytes for metadata storage", total_size); - return AWS_OP_ERR; - } - - /* - * Second pass: copy each key/value string into the contiguous buffer and - * build aws_byte_cursor structs that point into it at the correct offsets. - */ - for (size_t i = 0; i < count; i++) { - jobject entry = (*env)->CallObjectMethod(env, java_list, boxed_list_properties.list_get_id, (jint)i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { - AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); - return AWS_OP_ERR; - } - - jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, key_field_id); - jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, value_field_id); - - /* Acquire the raw UTF-8 bytes from the JVM for the key string */ - struct aws_byte_cursor key_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); - - /* Record where in the buffer this key will start */ - size_t key_offset = out_storage->len; - /* Copy key bytes into our contiguous buffer */ - aws_byte_buf_append(out_storage, &key_cursor); - /* Release the JVM's internal string memory*/ - aws_jni_byte_cursor_from_jstring_release(env, key_jstr, key_cursor); - - /* same pattern as above */ - struct aws_byte_cursor value_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, value_jstr); - size_t value_offset = out_storage->len; - aws_byte_buf_append(out_storage, &value_cursor); - aws_jni_byte_cursor_from_jstring_release(env, value_jstr, value_cursor); - - out_entries[i].key = aws_byte_cursor_from_array(out_storage->buffer + key_offset, key_cursor.len); - out_entries[i].value = aws_byte_cursor_from_array(out_storage->buffer + value_offset, value_cursor.len); - - AWS_LOGF_DEBUG( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics: metadata[%d] key=\"" PRInSTR "\" value=\"" PRInSTR "\"", - (int)i, - AWS_BYTE_CURSOR_PRI(out_entries[i].key), - AWS_BYTE_CURSOR_PRI(out_entries[i].value)); - - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); - (*env)->DeleteLocalRef(env, entry); - } - - AWS_LOGF_DEBUG( - AWS_LS_MQTT_GENERAL, - "IoTDeviceSDKMetrics: second pass complete, %d entries parsed into %zu bytes", - (int)count, - out_storage->len); - - return AWS_OP_SUCCESS; -} + struct aws_metadata_buf_holder { + struct aws_byte_cursor cursor; + struct aws_byte_buf buffer; + }; + /* Frees all native memory associated with a parsed metrics struct. */ void aws_mqtt_iot_metrics_java_jni_destroy( @@ -158,18 +38,28 @@ void aws_mqtt_iot_metrics_java_jni_destroy( } AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: Destroying IoTDeviceSDKMetrics", (void *)java_metrics); + /* Free the library name buffer */ if (aws_byte_buf_is_valid(&java_metrics->library_name_buf)) { aws_byte_buf_clean_up(&java_metrics->library_name_buf); } - if (aws_byte_buf_is_valid(&java_metrics->metadata_storage)) { - aws_byte_buf_clean_up(&java_metrics->metadata_storage); + /* Free each individual key/value buffer stored in the array_list */ + if (aws_array_list_is_valid(&java_metrics->metadata_bufs)) { + size_t buf_count = aws_array_list_length(&java_metrics->metadata_bufs); + for (size_t i = 0; i < buf_count; i++) { + struct aws_metadata_buf_holder *holder = NULL; + aws_array_list_get_at_ptr(&java_metrics->metadata_bufs, (void **)&holder, i); + aws_byte_buf_clean_up(&holder->buffer); + } + aws_array_list_clean_up(&java_metrics->metadata_bufs); } + /* Free the pre-allocated entries array */ if (java_metrics->metadata_entries) { aws_mem_release(allocator, java_metrics->metadata_entries); } + /* Free the wrapper struct itself */ aws_mem_release(allocator, java_metrics); } @@ -181,11 +71,17 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ jobject metadata_list = NULL; + /* Zero-initialize so all fields are safe for cleanup on any error path */ struct aws_mqtt_iot_metrics_java_jni *java_metrics = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_iot_metrics_java_jni)); AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: Creating IoTDeviceSDKMetrics from Java object", (void *)java_metrics); - /* Extract the library name string */ + + /* + * Extract the library name (e.g. "IoTDeviceSDK/Java"). + * Copies the Java String into library_name_buf and sets metrics.library_name + * as a cursor pointing into that buffer. + */ if (aws_get_string_from_jobject( env, java_iot_device_sdk_metrics, @@ -200,54 +96,98 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ goto on_error; } - /* Get the metadata list */ + /* Read the Java List field */ metadata_list = (*env)->GetObjectField( env, java_iot_device_sdk_metrics, iot_device_sdk_metrics_properties.metadata_entries_field_id); + /* Null list is valid — return metrics with just library name */ if (metadata_list == NULL || aws_jni_check_and_clear_exception(env)) { - AWS_LOGF_DEBUG( - AWS_LS_MQTT_GENERAL, - "id=%p: IoTDeviceSDKMetrics no metadata entries, returning with library name only", - (void *)java_metrics); + AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: IoTDeviceSDKMetrics no metadata entries", (void *)java_metrics); return java_metrics; } + /* Get list size */ jint count = (*env)->CallIntMethod(env, metadata_list, boxed_list_properties.list_size_id); + + /* Empty list is valid — return metrics with just library name */ if (aws_jni_check_and_clear_exception(env) || count <= 0) { - AWS_LOGF_DEBUG( - AWS_LS_MQTT_GENERAL, - "id=%p: IoTDeviceSDKMetrics metadata list empty or size() failed", - (void *)java_metrics); + AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: IoTDeviceSDKMetrics metadata list empty", (void *)java_metrics); (*env)->DeleteLocalRef(env, metadata_list); return java_metrics; } - /* Allocate entry array */ + /* Pre-allocate entries array since we know the count */ java_metrics->metadata_entries = aws_mem_calloc(allocator, (size_t)count, sizeof(struct aws_mqtt_metadata_entry)); - /* Use helper to do the two-pass parse */ - if (s_parse_string_pair_list( - env, + /* Init array_list to hold individual byte_bufs (2 per entry: key + value) */ + if (aws_array_list_init_dynamic( + &java_metrics->metadata_bufs, allocator, - metadata_list, - (size_t)count, - iot_metrics_metadata_properties.key_field_id, - iot_metrics_metadata_properties.value_field_id, - &java_metrics->metadata_storage, - java_metrics->metadata_entries) == AWS_OP_ERR) { + (size_t)count * 2, + sizeof(struct aws_metadata_buf_holder))) { goto on_error; } - /* Wire up the C struct */ - java_metrics->metadata_count = (size_t)count; + for (jint i = 0; i < count; i++) { + /* Call List.get(i) to get the IoTMetricsMetadata object */ + jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); + if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); + goto on_error; + } + + /* Read key and value jstrings from the entry */ + jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); + jstring value_jstr = + (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); + + /* Key: acquire JVM bytes → copy into own buffer → release JVM bytes */ + struct aws_metadata_buf_holder holder_key; + struct aws_byte_cursor tmp_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); + aws_byte_buf_init_copy_from_cursor(&holder_key.buffer, allocator, tmp_cursor); + holder_key.cursor = aws_byte_cursor_from_buf(&holder_key.buffer); + aws_jni_byte_cursor_from_jstring_release(env, key_jstr, tmp_cursor); + + /* Value: same pattern */ + struct aws_metadata_buf_holder holder_value; + tmp_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, value_jstr); + aws_byte_buf_init_copy_from_cursor(&holder_value.buffer, allocator, tmp_cursor); + holder_value.cursor = aws_byte_cursor_from_buf(&holder_value.buffer); + aws_jni_byte_cursor_from_jstring_release(env, value_jstr, tmp_cursor); + + /* Store holders so destroy can free each buffer later */ + aws_array_list_push_back(&java_metrics->metadata_bufs, &holder_key); + aws_array_list_push_back(&java_metrics->metadata_bufs, &holder_value); + + /* Set entry cursors — point at heap memory owned by the holders */ + java_metrics->metadata_entries[i].key = holder_key.cursor; + java_metrics->metadata_entries[i].value = holder_value.cursor; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "IoTDeviceSDKMetrics: metadata[%d] key=\"" PRInSTR "\" value=\"" PRInSTR "\"", + (int)i, + AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].key), + AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].value)); + + java_metrics->metadata_count++; + + /* Release JNI local refs for this iteration */ + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + } + + /* Point the C metrics struct at our parsed data */ java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; - java_metrics->metrics.metadata_count = (size_t)count; + java_metrics->metrics.metadata_count = java_metrics->metadata_count; AWS_LOGF_DEBUG( AWS_LS_MQTT_GENERAL, "id=%p: IoTDeviceSDKMetrics creation complete, %d metadata entries", (void *)java_metrics, (int)count); + (*env)->DeleteLocalRef(env, metadata_list); return java_metrics; diff --git a/src/native/iot_device_sdk_metrics.h b/src/native/iot_device_sdk_metrics.h index e7c8c1bad..c846e0974 100644 --- a/src/native/iot_device_sdk_metrics.h +++ b/src/native/iot_device_sdk_metrics.h @@ -12,7 +12,7 @@ struct aws_mqtt_iot_metrics_java_jni { struct aws_mqtt_iot_metrics metrics; struct aws_byte_buf library_name_buf; - struct aws_byte_buf metadata_storage; + struct aws_array_list metadata_bufs; struct aws_mqtt_metadata_entry *metadata_entries; size_t metadata_count; }; From 8e3586ed20946c79bab1ddd17e88095c73470565 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 18:37:54 -0700 Subject: [PATCH 13/16] lint --- src/native/iot_device_sdk_metrics.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index bc10f005c..97688959e 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -20,11 +20,10 @@ #include static char s_iot_device_sdk_metrics_string[] = "IoTDeviceSDKMetrics"; - struct aws_metadata_buf_holder { - struct aws_byte_cursor cursor; - struct aws_byte_buf buffer; - }; - +struct aws_metadata_buf_holder { + struct aws_byte_cursor cursor; + struct aws_byte_buf buffer; +}; /* Frees all native memory associated with a parsed metrics struct. */ void aws_mqtt_iot_metrics_java_jni_destroy( @@ -121,10 +120,7 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ /* Init array_list to hold individual byte_bufs (2 per entry: key + value) */ if (aws_array_list_init_dynamic( - &java_metrics->metadata_bufs, - allocator, - (size_t)count * 2, - sizeof(struct aws_metadata_buf_holder))) { + &java_metrics->metadata_bufs, allocator, (size_t)count * 2, sizeof(struct aws_metadata_buf_holder))) { goto on_error; } From 546ea262bd9288012e216cc5426488a07a304eb7 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 19:44:14 -0700 Subject: [PATCH 14/16] cleanup --- .../awssdk/crt/iot/IoTDeviceSDKMetrics.java | 4 ++- src/native/iot_device_sdk_metrics.c | 29 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index 2525bc798..b35c2b60d 100644 --- a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -38,6 +38,7 @@ public class IoTDeviceSDKMetrics { private static final String PROTOCOL_VERSION = "F"; private static final String SOCKET_IMPLEMENTATION = "G"; private static final String HTTP_PROXY_TYPE = "H"; + @SuppressWarnings("unused") private static final String CERTIFICATE_SOURCE = "I"; private static final String TLS_CIPHER_PREFERENCE = "J"; private static final String MINIMUM_TLS_VERSION = "K"; @@ -46,7 +47,6 @@ public class IoTDeviceSDKMetrics { public IoTDeviceSDKMetrics() { this.libraryName = "IoTDeviceSDK/Java"; - this.metadataEntries = new ArrayList<>(); } public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { @@ -402,6 +402,7 @@ private static String inboundTopicAliasBehaviorValue(TopicAliasingOptions.Inboun * or {@code null} for system default/unknown */ private static String tlsCipherPreferenceValue(TlsCipherPreference pref) { + if (pref == null) return null; switch (pref) { case TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05: return "A"; // TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05 case TLS_CIPHER_PQ_DEFAULT: return "B"; // TLS_CIPHER_PQ_DEFAULT @@ -418,6 +419,7 @@ private static String tlsCipherPreferenceValue(TlsCipherPreference pref) { * {@code "D"} for TLSv1.2, {@code "E"} for TLSv1.3, or {@code null} for system default/unknown */ private static String minimumTlsVersionValue(TlsContextOptions.TlsVersions version) { + if (version == null) return null; switch (version) { case SSLv3: return "A"; // SSLv3 case TLSv1: return "B"; // TLSv1 diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 97688959e..48d39af60 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -127,22 +127,34 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ for (jint i = 0; i < count; i++) { /* Call List.get(i) to get the IoTMetricsMetadata object */ jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); - if (entry == NULL || aws_jni_check_and_clear_exception(env)) { + if (!entry || aws_jni_check_and_clear_exception(env)) { AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); goto on_error; } - /* Read key and value jstrings from the entry */ + /* Read the key field. Empty string is allowed; null Java field is not. */ jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); + if (aws_jni_check_and_clear_exception(env) || !key_jstr) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: exception or null key at index %d", (int)i); + goto on_error; + } + + /* Read the value field. Empty string is allowed; null Java field is not. */ jstring value_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); + if (aws_jni_check_and_clear_exception(env) || !value_jstr) { + AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: exception or null value at index %d", (int)i); + goto on_error; + } - /* Key: acquire JVM bytes → copy into own buffer → release JVM bytes */ + /* Key: acquire JVM bytes → copy into own buffer → release JVM bytes. + * The local copy lets us hold onto the data after the JVM release. */ struct aws_metadata_buf_holder holder_key; struct aws_byte_cursor tmp_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, key_jstr); aws_byte_buf_init_copy_from_cursor(&holder_key.buffer, allocator, tmp_cursor); holder_key.cursor = aws_byte_cursor_from_buf(&holder_key.buffer); aws_jni_byte_cursor_from_jstring_release(env, key_jstr, tmp_cursor); + key_jstr = NULL; /* Value: same pattern */ struct aws_metadata_buf_holder holder_value; @@ -150,6 +162,7 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ aws_byte_buf_init_copy_from_cursor(&holder_value.buffer, allocator, tmp_cursor); holder_value.cursor = aws_byte_cursor_from_buf(&holder_value.buffer); aws_jni_byte_cursor_from_jstring_release(env, value_jstr, tmp_cursor); + value_jstr = NULL; /* Store holders so destroy can free each buffer later */ aws_array_list_push_back(&java_metrics->metadata_bufs, &holder_key); @@ -166,15 +179,13 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].key), AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].value)); - java_metrics->metadata_count++; - - /* Release JNI local refs for this iteration */ - (*env)->DeleteLocalRef(env, key_jstr); - (*env)->DeleteLocalRef(env, value_jstr); + /* Release JNI local ref for this iteration. Per JNI spec, DeleteLocalRef + * with NULL is a no-op, so the jstrings (now NULL after release) are safe. */ (*env)->DeleteLocalRef(env, entry); } - /* Point the C metrics struct at our parsed data */ + /* Point the C metrics struct at our parsed data.*/ + java_metrics->metadata_count = (size_t)count; java_metrics->metrics.metadata_entries = java_metrics->metadata_entries; java_metrics->metrics.metadata_count = java_metrics->metadata_count; From 75abe66fec0ea14da069162213b47102596fdcb8 Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Thu, 11 Jun 2026 20:03:29 -0700 Subject: [PATCH 15/16] code cleanup --- .../amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java | 9 +++++---- src/native/iot_device_sdk_metrics.c | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java index b35c2b60d..f3281f03f 100644 --- a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -47,6 +47,7 @@ public class IoTDeviceSDKMetrics { public IoTDeviceSDKMetrics() { this.libraryName = "IoTDeviceSDK/Java"; + this.metadataEntries = new ArrayList<>(); } public IoTDeviceSDKMetrics(String libraryName, List metadataEntries) { @@ -333,7 +334,7 @@ private static String retryJitterModeValue(JitterMode mode) { /** * Encodes an MQTT5 session behavior enum value. * - * @param behavior the {@code ClientSessionBehavior} ordinal + * @param behavior the {@code ClientSessionBehavior} enum * @return {@code "A"} for CLEAN, {@code "B"} for REJOIN_POST_SUCCESS, {@code "C"} for REJOIN_ALWAYS, * or {@code null} for DEFAULT/unknown */ @@ -349,7 +350,7 @@ private static String sessionBehaviorValue(Mqtt5ClientOptions.ClientSessionBehav /** * Encodes an MQTT5 offline queue behavior enum value. * - * @param behavior the {@code ClientOfflineQueueBehavior} ordinal + * @param behavior the {@code ClientOfflineQueueBehavior} enum * @return {@code "A"} for FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, {@code "B"} for FAIL_QOS0_PUBLISH_ON_DISCONNECT, * {@code "C"} for FAIL_ALL_ON_DISCONNECT, or {@code null} for DEFAULT/unknown */ @@ -365,7 +366,7 @@ private static String offlineQueueBehaviorValue(Mqtt5ClientOptions.ClientOffline /** * Encodes an outbound topic alias behavior enum value. * - * @param behavior the {@code OutboundTopicAliasBehaviorType} ordinal + * @param behavior the {@code OutboundTopicAliasBehaviorType} enum * @return {@code "A"} for Manual, {@code "B"} for LRU, {@code "C"} for Disabled, * or {@code null} for Default/unknown */ @@ -381,7 +382,7 @@ private static String outboundTopicAliasBehaviorValue(TopicAliasingOptions.Outbo /** * Encodes an inbound topic alias behavior enum value. * - * @param behavior the {@code InboundTopicAliasBehaviorType} ordinal + * @param behavior the {@code InboundTopicAliasBehaviorType} enum * @return {@code "A"} for Enabled, {@code "B"} for Disabled, or {@code null} for Default/unknown */ private static String inboundTopicAliasBehaviorValue(TopicAliasingOptions.InboundTopicAliasBehaviorType behavior) { diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 48d39af60..87d17109b 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -129,6 +129,7 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ jobject entry = (*env)->CallObjectMethod(env, metadata_list, boxed_list_properties.list_get_id, i); if (!entry || aws_jni_check_and_clear_exception(env)) { AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: failed to get entry at index %d", (int)i); + (*env)->DeleteLocalRef(env, entry); goto on_error; } @@ -136,6 +137,8 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ jstring key_jstr = (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.key_field_id); if (aws_jni_check_and_clear_exception(env) || !key_jstr) { AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: exception or null key at index %d", (int)i); + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, entry); goto on_error; } @@ -179,8 +182,7 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].key), AWS_BYTE_CURSOR_PRI(java_metrics->metadata_entries[i].value)); - /* Release JNI local ref for this iteration. Per JNI spec, DeleteLocalRef - * with NULL is a no-op, so the jstrings (now NULL after release) are safe. */ + /* Release JNI local ref for this iteration. */ (*env)->DeleteLocalRef(env, entry); } From d40c0f847cffb6bd7382997f7dc5b7d82048f93d Mon Sep 17 00:00:00 2001 From: Rakshil Modi Date: Fri, 12 Jun 2026 11:17:16 -0700 Subject: [PATCH 16/16] dereferencing values --- src/native/iot_device_sdk_metrics.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/native/iot_device_sdk_metrics.c b/src/native/iot_device_sdk_metrics.c index 87d17109b..7c621b832 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -147,6 +147,9 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ (jstring)(*env)->GetObjectField(env, entry, iot_metrics_metadata_properties.value_field_id); if (aws_jni_check_and_clear_exception(env) || !value_jstr) { AWS_LOGF_ERROR(AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics: exception or null value at index %d", (int)i); + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); goto on_error; }