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 4fbe62088..000000000 --- a/src/main/java/software/amazon/awssdk/crt/internal/IoTDeviceSDKMetrics.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ -package software.amazon.awssdk.crt.internal; - -/** - * @internal - * IoT Device SDK Metrics Structure. Not for external usage. - */ -public class IoTDeviceSDKMetrics { - private String libraryName; - - public IoTDeviceSDKMetrics() { - this.libraryName = "IoTDeviceSDK/Java"; - } - - public String getLibraryName() { - return libraryName; - } -} 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..027ba265b 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,10 @@ */ public class TlsContext extends CrtResource { + private TlsCipherPreference cipherPreference; + private TlsContextOptions.TlsVersions minimumTlsVersion; + private TlsContextOptions.CertificateSource certificateSource; + /** * 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 +26,9 @@ public class TlsContext extends CrtResource { */ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); + this.cipherPreference = options.tlsCipherPreference; + this.minimumTlsVersion = options.minTlsVersion; + this.certificateSource = options.getCertificateSource(); } /** @@ -31,6 +38,9 @@ public TlsContext(TlsContextOptions options) throws CrtRuntimeException { public TlsContext() throws CrtRuntimeException { try (TlsContextOptions options = TlsContextOptions.createDefaultClient()) { acquireNativeHandle(tlsContextNew(options.getNativeHandle())); + this.cipherPreference = options.tlsCipherPreference; + this.minimumTlsVersion = options.minTlsVersion; + this.certificateSource = options.getCertificateSource(); } } @@ -54,4 +64,26 @@ protected void releaseNativeHandle() { protected static native long tlsContextNew(long options) throws CrtRuntimeException; private static native void tlsContextDestroy(long elg); + + /** + * Returns the {@link TlsCipherPreference} configured on this context. + * + * @return the cipher preference + */ + public TlsCipherPreference getCipherPreference() { return cipherPreference; } + + /** + * Returns the minimum {@link TlsContextOptions.TlsVersions TLS version} configured on this context. + * + * @return the minimum TLS version + */ + public TlsContextOptions.TlsVersions getMinimumTlsVersion() { return minimumTlsVersion; } + + /** + * Returns the {@link TlsContextOptions.CertificateSource} of the configured mTLS, or + * {@code null} if no mTLS source has been set on the options used to create this context. + * + * @return the certificate source + */ + public TlsContextOptions.CertificateSource getCertificateSource() { return certificateSource; } }; diff --git a/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java b/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java index 4e6817ab3..174340581 100644 --- a/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java @@ -20,6 +20,26 @@ */ public final class TlsContextOptions extends CrtResource { + /** + * Identifies the source of the mTLS certificate/private key configured on a + * {@link TlsContextOptions}. Used internally by the IoT Device SDK metrics + * layer to encode feature "I" of the SDK metrics string. Values are set + * automatically by the {@code createWithMtls*} factory methods (and their + * {@code withMtls*} equivalents). + */ + public enum CertificateSource { + /** PEM cert + key files (in-memory or on-disk). */ + CERTIFICATE_FILES, + /** Hardware security module via PKCS#11. */ + PKCS11, + /** Windows certificate store. */ + WINDOWS_CERT_STORE, + /** Java keystore. */ + JAVA_KEYSTORE, + /** PKCS#12 (.p12 / .pfx) file. */ + PKCS12_FILE, + } + public enum TlsVersions { /** * SSL v3. This should almost never be used. @@ -92,6 +112,7 @@ public enum TlsVersions { private TlsContextPkcs11Options pkcs11Options; private TlsContextCustomKeyOperationOptions customKeyOperations; private String windowsCertStorePath; + private CertificateSource certificateSource; /** * Creates a new set of options that can be used to create a {@link TlsContext} @@ -171,6 +192,7 @@ public void setCipherPreference(TlsCipherPreference cipherPref) { public void initMtlsFromPath(String certificatePath, String privateKeyPath) { this.certificatePath = certificatePath; this.privateKeyPath = privateKeyPath; + this.certificateSource = CertificateSource.CERTIFICATE_FILES; } /** @@ -187,6 +209,7 @@ public void initMtls(String certificate, String privateKey) throws IllegalArgume this.privateKey = PemUtils.cleanUpPem(privateKey); PemUtils.sanityCheck(privateKey, 1, "PRIVATE KEY"); + this.certificateSource = CertificateSource.CERTIFICATE_FILES; } /** @@ -202,6 +225,7 @@ public void initMtlsPkcs12(String pkcs12Path, String pkcs12Password) { } this.pkcs12Path = pkcs12Path; this.pkcs12Password = pkcs12Password; + this.certificateSource = CertificateSource.PKCS12_FILE; } /** @@ -398,6 +422,7 @@ public static TlsContextOptions createWithMtlsJavaKeystore( } options.initMtls(certificate, privateKey); options.verifyPeer = true; + options.certificateSource = CertificateSource.JAVA_KEYSTORE; return options; } @@ -501,6 +526,7 @@ public TlsContextOptions withMtlsPkcs12(String pkcs12Path, String pkcs12Password public TlsContextOptions withMtlsPkcs11(TlsContextPkcs11Options pkcs11Options) { swapReferenceTo(this.pkcs11Options, pkcs11Options); this.pkcs11Options = pkcs11Options; + this.certificateSource = CertificateSource.PKCS11; return this; } @@ -528,6 +554,7 @@ public TlsContextOptions withMtlsCustomKeyOperations(TlsContextCustomKeyOperatio */ public TlsContextOptions withMtlsWindowsCertStorePath(String certificatePath) { this.windowsCertStorePath = certificatePath; + this.certificateSource = CertificateSource.WINDOWS_CERT_STORE; return this; } @@ -551,6 +578,14 @@ public TlsContextOptions withVerifyPeer() { return this.withVerifyPeer(true); } + /** + * @return the {@link CertificateSource} of the configured mTLS, or {@code null} + * if no mTLS source has been set (or the source has no defined metrics mapping). + */ + public CertificateSource getCertificateSource() { + return certificateSource; + } + /******************************************************************************* * native methods ******************************************************************************/ diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java new file mode 100644 index 000000000..7a855fdf6 --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTDeviceSDKMetrics.java @@ -0,0 +1,473 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +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.mqtt.MqttConnectionConfig; +import software.amazon.awssdk.crt.utils.PackageInfo; + +/** + * 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; + + // 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"; + 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"; + + public static final int IOT_SDK_METRICS_FEATURE_VERSION = 1; + + public IoTDeviceSDKMetrics() { + this.libraryName = "IoTDeviceSDK/Java"; + this.metadataEntries = new ArrayList<>(); + } + + 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; } + + /** + * 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); + } + + /** + * 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) { + String crtFeatureList = getEncodedFeatureListMqtt3(config.getHttpProxyOptions(), config.getTlsContext()); + return createMetrics(config.getMetrics(), crtFeatureList); + } + + /** + * 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), + * - I (certificate_source): detected from TlsContextOptions + * - J (tls_cipher_preference), + * - K (minimum_tls_version) + * + * @param clientOptions MQTT5 client options containing connection configuration + * @return the encoded feature list string + */ + private static String getEncodedFeatureListMqtt5(Mqtt5ClientOptions clientOptions) { + List features = new ArrayList<>(); + + if (clientOptions.getRetryJitterMode() != null) { + String val = retryJitterModeValue(clientOptions.getRetryJitterMode()); + if (val != null) features.add(RETRY_JITTER_MODE + "/" + val); + } + + if (clientOptions.getSessionBehavior() != null) { + String val = sessionBehaviorValue(clientOptions.getSessionBehavior()); + if (val != null) features.add(SESSION_BEHAVIOR + "/" + val); + } + + if (clientOptions.getOfflineQueueBehavior() != null) { + String val = offlineQueueBehaviorValue(clientOptions.getOfflineQueueBehavior()); + if (val != null) features.add(OFFLINE_QUEUE_BEHAVIOR + "/" + val); + } + + TopicAliasingOptions topicAliasing = clientOptions.getTopicAliasingOptions(); + if (topicAliasing != null) { + if (topicAliasing.getOutboundBehavior() != null) { + String val = outboundTopicAliasBehaviorValue(topicAliasing.getOutboundBehavior()); + if (val != null) features.add(OUTBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); + } + if (topicAliasing.getInboundBehavior() != null) { + String val = inboundTopicAliasBehaviorValue(topicAliasing.getInboundBehavior()); + if (val != null) features.add(INBOUND_TOPIC_ALIAS_BEHAVIOR + "/" + val); + } + } + + features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(true)); + features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); + + HttpProxyOptions proxyOptions = clientOptions.getHttpProxyOptions(); + if (proxyOptions != null) { + boolean proxyUsesTls = proxyOptions.getTlsContext() != null; + features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); + } + + TlsContext tlsCtx = clientOptions.getTlsContext(); + if (tlsCtx != null) { + String certSrc = certificateSourceValue(tlsCtx.getCertificateSource()); + if (certSrc != null) features.add(CERTIFICATE_SOURCE + "/" + certSrc); + + 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. + *

+ * + * Format: "ID/Value,ID/Value,..." + * + * MQTT3 connections always include: + * - F (protocol_version): set to MQTT311 + * - G (socket_implementation): detected from platform + * + * Conditionally includes: + * - H (http_proxy_type) + * - I (certificate_source): detected from TlsContextOptions + * - 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 + */ + private static String getEncodedFeatureListMqtt3(HttpProxyOptions proxyOptions, TlsContext tlsCtx) { + List features = new ArrayList<>(); + + features.add(PROTOCOL_VERSION + "/" + protocolVersionValue(false)); + features.add(SOCKET_IMPLEMENTATION + "/" + socketImplementationValue()); + + if (proxyOptions != null) { + boolean proxyUsesTls = proxyOptions.getTlsContext() != null; + features.add(HTTP_PROXY_TYPE + "/" + httpProxyTypeValue(proxyUsesTls)); + } + + if (tlsCtx != null) { + String certSrc = certificateSourceValue(tlsCtx.getCertificateSource()); + if (certSrc != null) features.add(CERTIFICATE_SOURCE + "/" + certSrc); + + 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); + } + + /** + * Merges CRT-generated features with user-provided (IoT SDK) features. + * When both lists contain the same feature ID, the user-provided value takes precedence. + * + * @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) + */ + private 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(); + } + + /** + *

+ * Metrics creation logic: + * - 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 + * @return the final metrics object + */ + private static IoTDeviceSDKMetrics createMetrics(IoTDeviceSDKMetrics userMetrics, String crtFeatureList) { + String libraryName = (userMetrics != null) ? userMetrics.getLibraryName() : "IoTDeviceSDK/Java"; + + String crtVersion = new PackageInfo().version.toString(); + LinkedHashMap metadata = new LinkedHashMap<>(); + metadata.put("CRTVersion", crtVersion); + + 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()); + } + } + } + + if (parsedVersionMatches(userMetricsVersion)) { + metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, userFeature)); + } else { + metadata.put("IoTSDKFeature", mergeFeatureLists(crtFeatureList, "")); + } + + metadata.put("IoTSDKMetricsVersion", String.valueOf(IOT_SDK_METRICS_FEATURE_VERSION)); + + List entries = new ArrayList<>(); + for (Map.Entry entry : metadata.entrySet()) { + entries.add(new IoTMetricsMetadata(entry.getKey(), entry.getValue())); + } + + 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 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 + */ + 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 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 + */ + 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 behavior the {@code OutboundTopicAliasBehaviorType} enum + * @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; + } + } + + /** + * Encodes an inbound topic alias behavior enum value. + * + * @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) { + switch (behavior) { + case Enabled: return "A"; + case Disabled: return "B"; + default: return null; + } + } + + /** + * Encodes a {@link TlsContextOptions.CertificateSource} into its single-character metrics value. + * + * @param source the certificate source detected from the TLS context options + * @return {@code "A"} for CERTIFICATE_FILES, {@code "B"} for PKCS11, {@code "C"} for WINDOWS_CERT_STORE, + * {@code "D"} for JAVA_KEYSTORE, {@code "E"} for PKCS12_FILE, or {@code null} if unknown/null + */ + private static String certificateSourceValue(TlsContextOptions.CertificateSource source) { + if (source == null) return null; + switch (source) { + case CERTIFICATE_FILES: return "A"; + case PKCS11: return "B"; + case WINDOWS_CERT_STORE: return "C"; + case JAVA_KEYSTORE: return "D"; + case PKCS12_FILE: return "E"; + 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) { + 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 + 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) { + if (version == null) return null; + 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; + } + } + + /** + * 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 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 parsedVersionMatches(String userMetricsVersion) { + if (userMetricsVersion == null) return false; + try { + return Integer.parseInt(userMetricsVersion) == IOT_SDK_METRICS_FEATURE_VERSION; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java b/src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java new file mode 100644 index 000000000..26c29d920 --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/iot/IoTMetricsMetadata.java @@ -0,0 +1,23 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.awssdk.crt.iot; + +/** + * 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/mqtt/MqttClientConnection.java b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttClientConnection.java index 6cec4bcd4..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,7 +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.iot.IoTDeviceSDKMetrics; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -78,7 +78,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 +164,9 @@ private void SetupConfig(MqttConnectionConfig config) throws MqttException { mqttClientConnectionSetLogin(getNativeHandle(), config.getUsername(), config.getPassword()); } - if (config.getMetricsEnabled()) { - mqttClientConnectionSetMetrics(getNativeHandle(), new IoTDeviceSDKMetrics()); + if (!config.getDisableMetrics()) { + IoTDeviceSDKMetrics metrics = IoTDeviceSDKMetrics.createMetricsMqtt3(config); + 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..f3b5ce645 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt/MqttConnectionConfig.java @@ -9,9 +9,12 @@ import software.amazon.awssdk.crt.CrtResource; import software.amazon.awssdk.crt.http.HttpProxyOptions; +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; +import software.amazon.awssdk.crt.io.TlsContext; + /** * Encapsulates all per-mqtt-connection configuration @@ -47,7 +50,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,21 +546,56 @@ 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 disableMetrics true to disable metrics, false to enable (default) + */ + public void setDisableMetrics(boolean disableMetrics) { + this.disableMetrics = disableMetrics; + } + + /** + * Returns whether IoT Device SDK metrics collection is disabled. + * + * @return true if metrics are disabled, false if metrics are enabled (default) + */ + 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 enabled true to enable metrics, false to disable + * @param metrics metrics configuration from the IoT SDK layer */ - public void setMetricsEnabled(boolean enabled) { - this.metricsEnabled = enabled; + public void setMetrics(IoTDeviceSDKMetrics metrics) { + this.metrics = metrics; } /** - * Queries whether IoT Device SDK metrics collection is enabled + * Returns the IoT SDK metrics configuration. * - * @return true if metrics are enabled, false if disabled + * @return metrics configuration, or null if not set */ - public boolean getMetricsEnabled() { - return metricsEnabled; + 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; } /** @@ -588,7 +627,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..5421127b6 100644 --- a/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/mqtt5/Mqtt5ClientOptions.java @@ -12,7 +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.iot.IoTDeviceSDKMetrics; import java.util.Map; import java.util.function.Function; @@ -46,12 +46,12 @@ 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 userMetrics; private IoTDeviceSDKMetrics iotDeviceSDKMetrics; - + /** * Returns the host name of the MQTT server to connect to. @@ -271,21 +271,31 @@ 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. + * Returns the user-provided metrics configuration from the IoT SDK layer. * - * @param enabled true to enable metrics, false to disable + * @return the user metrics, or null if none were provided */ - public void setMetricsEnabled(boolean enabled) { - this.metricsEnabled = enabled; + 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). + * + * @param disableMetrics true to disable metrics, false to enable (default) + */ + public void setDisableMetrics(boolean disableMetrics) { + this.disableMetrics = disableMetrics; } /** @@ -314,8 +324,13 @@ 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; + this.userMetrics = builder.metrics; + if (this.disableMetrics) { + this.iotDeviceSDKMetrics = null; + } else { + this.iotDeviceSDKMetrics = IoTDeviceSDKMetrics.createMetricsMqtt5(this); + } } /******************************************************************************* @@ -379,7 +394,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 +633,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 +903,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 disableMetrics 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 disableMetrics) { + this.disableMetrics = disableMetrics; + 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/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/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..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,10 +1268,24 @@ ] }, { - "name": "software.amazon.awssdk.crt.internal.IoTDeviceSDKMetrics", + "name": "software.amazon.awssdk.crt.iot.IoTDeviceSDKMetrics", "fields": [ { "name": "libraryName" + }, + { + "name": "metadataEntries" + } + ] + }, + { + "name": "software.amazon.awssdk.crt.iot.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..7c621b832 100644 --- a/src/native/iot_device_sdk_metrics.c +++ b/src/native/iot_device_sdk_metrics.c @@ -2,6 +2,15 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ + +/** + * JNI bridge for IoT Device SDK Metrics. + * + * 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 #include "iot_device_sdk_metrics.h" @@ -11,7 +20,12 @@ #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; +}; +/* Frees all native memory associated with a parsed metrics struct. */ void aws_mqtt_iot_metrics_java_jni_destroy( JNIEnv *env, struct aws_allocator *allocator, @@ -23,26 +37,50 @@ 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); } + /* 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); } +/* 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) { + 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)); - if (java_metrics == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_GENERAL, "IoTDeviceSDKMetrics create_from_java: Creating new IoTDeviceSDKMetrics failed"); - return NULL; - } + AWS_LOGF_DEBUG(AWS_LS_MQTT_GENERAL, "id=%p: Creating IoTDeviceSDKMetrics from Java object", (void *)java_metrics); + + /* + * 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, @@ -57,10 +95,118 @@ struct aws_mqtt_iot_metrics_java_jni *aws_mqtt_iot_metrics_java_jni_create_from_ goto on_error; } + /* 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", (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", (void *)java_metrics); + (*env)->DeleteLocalRef(env, metadata_list); + return java_metrics; + } + + /* 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)); + + /* 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))) { + goto on_error; + } + + 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 || 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; + } + + /* 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); + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, entry); + 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); + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jstr); + (*env)->DeleteLocalRef(env, entry); + goto on_error; + } + + /* 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; + 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); + value_jstr = NULL; + + /* 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)); + + /* Release JNI local ref for this iteration. */ + (*env)->DeleteLocalRef(env, entry); + } + + /* 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; + + 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; } diff --git a/src/native/iot_device_sdk_metrics.h b/src/native/iot_device_sdk_metrics.h index 341004194..c846e0974 100644 --- a/src/native/iot_device_sdk_metrics.h +++ b/src/native/iot_device_sdk_metrics.h @@ -12,6 +12,9 @@ struct aws_mqtt_iot_metrics_java_jni { struct aws_mqtt_iot_metrics metrics; struct aws_byte_buf library_name_buf; + struct aws_array_list metadata_bufs; + 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 765b6a983..5331af998 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -1760,14 +1760,14 @@ 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, "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); @@ -2679,6 +2679,27 @@ 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/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); + + 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. @@ -2800,6 +2821,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..5b8dba44b 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; @@ -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. 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..0943adff5 --- /dev/null +++ b/src/test/java/software/amazon/awssdk/crt/test/IoTMetricEncoderTest.java @@ -0,0 +1,363 @@ +/** + * 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.iot.IoTDeviceSDKMetrics; +import software.amazon.awssdk.crt.iot.IoTMetricsMetadata; +import software.amazon.awssdk.crt.mqtt.MqttClient; +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; +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 software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.crt.io.TlsContextOptions; +import software.amazon.awssdk.crt.io.TlsContextOptions.CertificateSource; + +import java.util.ArrayList; +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); + Mqtt5ClientOptions options = builder.build(); + + 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() { + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertTrue(feature.contains("F/3")); + assertTrue(feature.contains("G/")); + } + + // ======================== 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(); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + 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 + 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(); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertFalse(feature.contains("A/")); + assertFalse(feature.contains("B/")); + assertFalse(feature.contains("C/")); + } + + // ======================== Feature Merging ======================== + + @Test + public void testUserOverridesCrt() { + IoTDeviceSDKMetrics user = new IoTDeviceSDKMetrics(); + List userEntries = new ArrayList<>(); + userEntries.add(new IoTMetricsMetadata("IoTSDKMetricsVersion", "1")); + userEntries.add(new IoTMetricsMetadata("IoTSDKFeature", "F/9")); + user.setMetadataEntries(userEntries); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertTrue(feature.contains("F/9")); + assertFalse(feature.contains("F/3")); + } + + @Test + 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); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); + + assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); + assertNotNull(result.getMetadataEntries()); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + assertEquals("IoTDeviceSDK/Java", result.getLibraryName()); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertTrue(feature.contains("F/3")); + } + + // ======================== 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + assertTrue(feature.contains("F/3")); + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + 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 = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(user)); + + assertEquals("MyCustomSDK/1.0", result.getLibraryName()); + } + + @Test + public void testMetricsVersionAlwaysSet() { + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(createMqtt3Config(null)); + + String metricsVersion = findMetadataValue(result.getMetadataEntries(), "IoTSDKMetricsVersion"); + assertEquals("1", metricsVersion); + } + + // ======================== Certificate Source ======================== + + @Test + public void testCertificateSourceMappingFromFactories() { + try (TlsContextOptions mtls = + TlsContextOptions.createWithMtls(TlsContextOptionsTest.TEST_CERT, TlsContextOptionsTest.TEST_KEY)) { + assertEquals(CertificateSource.CERTIFICATE_FILES, mtls.getCertificateSource()); + } + try (TlsContextOptions pkcs12 = + TlsContextOptions.createWithMtlsPkcs12("/dev/null", "password")) { + assertEquals(CertificateSource.PKCS12_FILE, pkcs12.getCertificateSource()); + } + try (TlsContextOptions defaults = TlsContextOptions.createDefaultClient()) { + assertNull(defaults.getCertificateSource()); + } + } + + @Test + public void testCertificateSourceInMqtt5Features() { + try (TlsContextOptions tlsOptions = + TlsContextOptions.createWithMtls(TlsContextOptionsTest.TEST_CERT, TlsContextOptionsTest.TEST_KEY); + TlsContext tlsContext = new TlsContext(tlsOptions)) { + + Mqtt5ClientOptionsBuilder builder = new Mqtt5ClientOptionsBuilder("localhost", 8883L); + builder.withTlsContext(tlsContext); + builder.withDisableMetrics(true); + Mqtt5ClientOptions options = builder.build(); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt5(options); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertTrue(feature.contains("I/A")); + assertTrue(feature.contains("F/5")); + } + } + + @Test + public void testCertificateSourceInMqtt3Features() { + try (TlsContextOptions tlsOptions = + TlsContextOptions.createWithMtls(TlsContextOptionsTest.TEST_CERT, TlsContextOptionsTest.TEST_KEY); + TlsContext tlsContext = new TlsContext(tlsOptions); + MqttClient client = new MqttClient(tlsContext); + MqttConnectionConfig config = new MqttConnectionConfig()) { + config.setMqttClient(client); + + IoTDeviceSDKMetrics result = IoTDeviceSDKMetrics.createMetricsMqtt3(config); + String feature = findMetadataValue(result.getMetadataEntries(), "IoTSDKFeature"); + + assertTrue(feature.contains("I/A")); + assertTrue(feature.contains("F/3")); + } + } + + // ======================== 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/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