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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Other Changes

- Align customer-facing SDKStats configuration and custom dimension names with the stable specification.

## 1.5.0 (2026-06-11)

### Features Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ Learn more about [OpenTelemetry SDK logging][logging_otel_sdk].
You can disable the [live metrics][live_metrics] by setting the `APPLICATIONINSIGHTS_LIVE_METRICS_ENABLED` environment variable to false, the `applicationinsights.live.metrics.enabled` Java system property to false,
or programmatically with a properties supplier: `sdkBuilder.addPropertiesSupplier(() -> Collections.singletonMap("applicationinsights.live.metrics.enabled", "false"))`.

### Customer-facing SDKStats

Customer-facing SDKStats emit `Item_Success_Count`, `Item_Dropped_Count`, and `Item_Retry_Count`
custom metrics to the configured Application Insights resource by default. Set
`APPLICATIONINSIGHTS_SDKSTATS_DISABLED` to `true` to disable them. Metrics are exported every 900
seconds by default; set `APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL` to a positive number of
seconds to change the interval.

## Next steps
Learn more about [OpenTelemetry][opentelemetry_io]

Expand Down Expand Up @@ -206,4 +214,3 @@ This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For m
[coc]: https://opensource.microsoft.com/codeofconduct/
[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/
[coc_contact]: mailto:opencode@microsoft.com

Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ class AzureMonitorExporterBuilder {
private static final String STATSBEAT_SHORT_INTERVAL_SECONDS_PROPERTY_NAME
= "STATSBEAT_SHORT_INTERVAL_SECONDS_PROPERTY_NAME";

private static final String SDKSTATS_DISABLED_ENV_VAR = "APPLICATIONINSIGHTS_SDKSTATS_DISABLED";
private static final String SDKSTATS_DISABLED_PROPERTY_NAME = "applicationinsights.sdkstats.disabled";
private static final String SDKSTATS_DISABLED_ALL_ENV_VAR = "APPLICATIONINSIGHTS_SDKStats_DISABLED_ALL";
private static final String SDKSTATS_EXPORT_INTERVAL_ENV_VAR = "APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL";
private static final String SDKSTATS_EXPORT_INTERVAL_PROPERTY_NAME = "applicationinsights.sdkstats.export.interval";
private static final long SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS = 900; // 15 minutes

private static final Map<String, String> PROPERTIES
Expand Down Expand Up @@ -261,8 +261,8 @@ private void startStatsbeatModule(StatsbeatModule statsbeatModule, ConfigPropert
}

private CustomerSdkStats createCustomerSdkStats() {
// Use the raw library version number (e.g. "3.6.0") for the customer-facing stats dimension
String version = PropertyHelper.getSdkVersionNumber();
// Use the raw distro version number (e.g. "1.6.0-beta.1") for the customer-facing stats dimension.
String version = VersionGenerator.getSdkVersionNumber();
return CustomerSdkStats.create(version);
}

Expand All @@ -271,12 +271,16 @@ private boolean isCustomerSdkStatsEnabled() {
}

static boolean isCustomerSdkStatsEnabled(ConfigProperties configProperties) {
if ("true".equalsIgnoreCase(configProperties.getString(SDKSTATS_DISABLED_ALL_ENV_VAR))) {
String disabledAll = configProperties.getString(SDKSTATS_DISABLED_ALL_ENV_VAR);
if (disabledAll == null) {
disabledAll = Configuration.getGlobalConfiguration().get(SDKSTATS_DISABLED_ALL_ENV_VAR);
}
if ("true".equalsIgnoreCase(disabledAll)) {
return false;
}
String disabledValue = configProperties.getString(SDKSTATS_DISABLED_ENV_VAR);
if ("true".equalsIgnoreCase(disabledValue)) {
LOGGER.verbose("Customer SDKStats is disabled via configuration property {}.", SDKSTATS_DISABLED_ENV_VAR);
if ("true".equalsIgnoreCase(configProperties.getString(SDKSTATS_DISABLED_PROPERTY_NAME))) {
LOGGER.verbose("Customer SDKStats is disabled via configuration property {}.",
SDKSTATS_DISABLED_PROPERTY_NAME);
return false;
}
return true;
Expand All @@ -285,11 +289,12 @@ static boolean isCustomerSdkStatsEnabled(ConfigProperties configProperties) {
private void startCustomerSdkStats(CustomerSdkStats customerSdkStats,
CustomerSdkStatsTelemetryPipelineListener customerSdkStatsListener, Resource resource) {
// Get export interval from configuration or use default
long exportIntervalSeconds
= configProperties.getLong(SDKSTATS_EXPORT_INTERVAL_ENV_VAR, SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
long exportIntervalSeconds = configProperties.getLong(SDKSTATS_EXPORT_INTERVAL_PROPERTY_NAME,
SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
if (exportIntervalSeconds <= 0) {
LOGGER.warning("Value for {} must be positive: {}. Using default {} seconds.",
SDKSTATS_EXPORT_INTERVAL_ENV_VAR, exportIntervalSeconds, SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
SDKSTATS_EXPORT_INTERVAL_PROPERTY_NAME, exportIntervalSeconds,
SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
exportIntervalSeconds = SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void incrementSuccessCount(Map<String, Long> itemCountsByType) {
* @param itemCountsByType the per-type item counts
* @param dropCode the drop code (e.g. "CLIENT_EXCEPTION", "402", etc.)
* @param dropReason the drop reason (e.g. "Exceeded daily quota")
* @param successItemCountsByType success items by type (for telemetry_success dimension on
* @param successItemCountsByType success items by type (for telemetrySuccess dimension on
* REQUEST/DEPENDENCY)
* @param failureItemCountsByType failure items by type
*/
Expand All @@ -87,7 +87,7 @@ public void incrementDroppedCount(Map<String, Long> itemCountsByType, String dro
String telemetryType = entry.getKey();
long totalCount = entry.getValue();

// For REQUEST and DEPENDENCY, split by telemetry_success
// For REQUEST and DEPENDENCY, split by telemetrySuccess
if ("REQUEST".equals(telemetryType) || "DEPENDENCY".equals(telemetryType)) {
long successCount = successItemCountsByType.getOrDefault(telemetryType, 0L);
long failureCount = failureItemCountsByType.getOrDefault(telemetryType, 0L);
Expand All @@ -108,7 +108,7 @@ public void incrementDroppedCount(Map<String, Long> itemCountsByType, String dro
droppedCounts.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(unaccounted);
}
} else {
// For non-REQUEST/DEPENDENCY types, telemetry_success is not applicable
// For non-REQUEST/DEPENDENCY types, telemetrySuccess is not applicable
DroppedKey key = new DroppedKey(telemetryType, dropCode, dropReason, null);
droppedCounts.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(totalCount);
}
Expand Down Expand Up @@ -153,7 +153,7 @@ public List<TelemetryItem> collectAndReset(ConnectionString connectionString, St
builder.setTime(FormattedTime.offSetDateTimeFromNow());
addCommonTags(builder, sdkVersion, cloudRole, cloudRoleInstance);
addCommonProperties(builder);
builder.addProperty("telemetry_type", key.telemetryType);
builder.addProperty("telemetryType", key.telemetryType);
telemetryItems.add(builder.build());
}

Expand All @@ -170,13 +170,13 @@ public List<TelemetryItem> collectAndReset(ConnectionString connectionString, St
builder.setTime(FormattedTime.offSetDateTimeFromNow());
addCommonTags(builder, sdkVersion, cloudRole, cloudRoleInstance);
addCommonProperties(builder);
builder.addProperty("telemetry_type", key.telemetryType);
builder.addProperty("drop.code", key.dropCode);
builder.addProperty("telemetryType", key.telemetryType);
builder.addProperty("dropCode", key.dropCode);
if (key.dropReason != null) {
builder.addProperty("drop.reason", key.dropReason);
builder.addProperty("dropReason", key.dropReason);
}
if (key.telemetrySuccess != null) {
builder.addProperty("telemetry_success", key.telemetrySuccess.toString());
builder.addProperty("telemetrySuccess", key.telemetrySuccess.toString());
}
telemetryItems.add(builder.build());
}
Expand All @@ -194,10 +194,10 @@ public List<TelemetryItem> collectAndReset(ConnectionString connectionString, St
builder.setTime(FormattedTime.offSetDateTimeFromNow());
addCommonTags(builder, sdkVersion, cloudRole, cloudRoleInstance);
addCommonProperties(builder);
builder.addProperty("telemetry_type", key.telemetryType);
builder.addProperty("retry.code", key.retryCode);
builder.addProperty("telemetryType", key.telemetryType);
builder.addProperty("retryCode", key.retryCode);
if (key.retryReason != null) {
builder.addProperty("retry.reason", key.retryReason);
builder.addProperty("retryReason", key.retryReason);
}
telemetryItems.add(builder.build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

/**
* Categorizes exceptions into low-cardinality reason strings for customer-facing SDKStats
* drop.reason and retry.reason dimensions.
* dropReason and retryReason dimensions.
*/
final class CustomerSdkStatsExceptionCategory {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.util.Map;

/**
* Maps TelemetryItem.getName() values to the telemetry_type dimension values
* Maps TelemetryItem.getName() values to the telemetryType dimension values
* used in customer-facing SDKStats metrics.
*/
public final class CustomerSdkStatsTelemetryType {
Expand All @@ -31,10 +31,10 @@ public final class CustomerSdkStatsTelemetryType {
}

/**
* Maps a TelemetryItem name to its customer-facing telemetry_type value.
* Maps a TelemetryItem name to its customer-facing telemetryType value.
*
* @param telemetryItemName the value from TelemetryItem.getName()
* @return the telemetry_type dimension value, or null if the item should be skipped
* @return the telemetryType dimension value, or null if the item should be skipped
* (e.g. "Statsbeat" internal metrics)
*/
@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public final class VersionGenerator {
private static final String UNKNOWN_VERSION_VALUE = "unknown";

private static final String sdkVersionString;
private static final String sdkVersionNumber;

static {
String componentName = null;
Expand Down Expand Up @@ -41,8 +42,9 @@ public final class VersionGenerator {
componentVersion = otelAutoconfigureProperties.get("version");
}

sdkVersionNumber = componentVersion != null ? componentVersion : UNKNOWN_VERSION_VALUE;
sdkVersionString = getPrefix() + "java" + getJavaVersion() + getJavaRuntime() + ":" + "otel"
+ getOpenTelemetryApiVersion() + ":" + componentName + componentVersion;
+ getOpenTelemetryApiVersion() + ":" + componentName + sdkVersionNumber;
}

private static String getPrefix() {
Expand Down Expand Up @@ -82,6 +84,15 @@ public static String getSdkVersion() {
return sdkVersionString;
}

/**
* Returns the version of the Azure Monitor distro represented in {@link #getSdkVersion()}.
*
* @return the Azure Monitor distro version.
*/
public static String getSdkVersionNumber() {
return sdkVersionNumber;
}

private static String getJavaVersion() {
return System.getProperty("java.version");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ Press `Ctrl+C` in the terminal running the sample.

- In **drop/retry** modes, both application telemetry and SDKStats metrics go through the same mock pipeline. SDKStats metrics are visible only in the console output, not in Azure Monitor.
- The mock server automatically gunzips incoming payloads and prints each telemetry item on its own line.
- SDKStats dimensions include: `computeType`, `language`, `version`, `telemetry_type`, `telemetry_success`, and mode-specific fields (`drop.code`/`drop.reason` or `retry.code`/`retry.reason`).
- SDKStats dimensions include: `computeType`, `language`, `version`, `telemetryType`, `telemetrySuccess`, and mode-specific fields (`dropCode`/`dropReason` or `retryCode`/`retryReason`).
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void customerSdkStatsEnabledByDefault() {
@Test
public void customerSdkStatsDisabledByPublicProperty() {
ConfigProperties config = DefaultConfigProperties
.createFromMap(Collections.singletonMap("APPLICATIONINSIGHTS_SDKSTATS_DISABLED", "true"));
.createFromMap(Collections.singletonMap("applicationinsights.sdkstats.disabled", "true"));
assertThat(AzureMonitorExporterBuilder.isCustomerSdkStatsEnabled(config)).isFalse();
}

Expand All @@ -38,16 +38,24 @@ public void customerSdkStatsDisabledByAllProperty() {
@Test
public void customerSdkStatsDisabledAllTakesPrecedence() {
Map<String, String> props = new HashMap<>();
props.put("APPLICATIONINSIGHTS_SDKSTATS_DISABLED", "false");
props.put("applicationinsights.sdkstats.disabled", "false");
props.put("APPLICATIONINSIGHTS_SDKStats_DISABLED_ALL", "true");
ConfigProperties config = DefaultConfigProperties.createFromMap(props);
assertThat(AzureMonitorExporterBuilder.isCustomerSdkStatsEnabled(config)).isFalse();
}

@Test
public void customerSdkStatsDisabledAllFalseLeavesEnabled() {
Map<String, String> props = new HashMap<>();
props.put("APPLICATIONINSIGHTS_SDKStats_DISABLED_ALL", "false");
ConfigProperties config = DefaultConfigProperties.createFromMap(props);
assertThat(AzureMonitorExporterBuilder.isCustomerSdkStatsEnabled(config)).isTrue();
}

@Test
public void customerSdkStatsDisabledFalseLeavesEnabled() {
ConfigProperties config = DefaultConfigProperties
.createFromMap(Collections.singletonMap("APPLICATIONINSIGHTS_SDKStats_DISABLED_ALL", "false"));
.createFromMap(Collections.singletonMap("applicationinsights.sdkstats.disabled", "false"));
assertThat(AzureMonitorExporterBuilder.isCustomerSdkStatsEnabled(config)).isTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat;

import com.azure.monitor.opentelemetry.autoconfigure.implementation.configuration.ConnectionString;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.MetricsData;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryItem;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -17,6 +19,7 @@
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

public class CustomerSdkStatsTest {

Expand Down Expand Up @@ -119,12 +122,54 @@ public void testCollectAndResetReturnsCorrectMetrics() {
// Should have 3 metric items
assertThat(items).hasSize(3);

MetricsData successMetric = getMetric(items, CustomerSdkStats.ITEM_SUCCESS_COUNT);
assertThat(successMetric.getMetrics().get(0).getValue()).isEqualTo(10);
assertThat(successMetric.getProperties()).containsOnly(entry("computeType", "unknown"),
entry("language", "java"), entry("version", "3.5.1"), entry("telemetryType", "REQUEST"));

MetricsData droppedMetric = getMetric(items, CustomerSdkStats.ITEM_DROPPED_COUNT);
assertThat(droppedMetric.getMetrics().get(0).getValue()).isEqualTo(5);
assertThat(droppedMetric.getProperties()).containsOnly(entry("computeType", "unknown"),
entry("language", "java"), entry("version", "3.5.1"), entry("telemetryType", "DEPENDENCY"),
entry("dropCode", "402"), entry("dropReason", "Exceeded daily quota"));

MetricsData retryMetric = getMetric(items, CustomerSdkStats.ITEM_RETRY_COUNT);
assertThat(retryMetric.getMetrics().get(0).getValue()).isEqualTo(3);
assertThat(retryMetric.getProperties()).containsOnly(entry("computeType", "unknown"), entry("language", "java"),
entry("version", "3.5.1"), entry("telemetryType", "TRACE"), entry("retryCode", "429"),
entry("retryReason", "Too many requests"));

assertThat(items).allSatisfy(item -> {
assertThat(item.getInstrumentationKey()).isEqualTo(CONNECTION_STRING.getInstrumentationKey());
assertThat(item.getTags()).containsEntry("ai.internal.sdkVersion", SDK_VERSION)
.containsEntry("ai.cloud.role", CLOUD_ROLE)
.containsEntry("ai.cloud.roleInstance", CLOUD_ROLE_INSTANCE);
});

// Verify that counters are cleared
assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(0);
assertThat(customerSdkStats.getDroppedCount("DEPENDENCY", "402")).isEqualTo(0);
assertThat(customerSdkStats.getRetryCount("TRACE", "429")).isEqualTo(0);
}

@Test
public void testDroppedMetricUsesTelemetrySuccessDimension() {
customerSdkStats.incrementDroppedCount(Collections.singletonMap("REQUEST", 2L), "402", "Exceeded daily quota",
Collections.singletonMap("REQUEST", 1L), Collections.singletonMap("REQUEST", 1L));

List<TelemetryItem> items
= customerSdkStats.collectAndReset(CONNECTION_STRING, SDK_VERSION, CLOUD_ROLE, CLOUD_ROLE_INSTANCE);

assertThat(items).hasSize(2);
assertThat(items)
.extracting(
item -> TestUtils.toMetricsData(item.getData().getBaseData()).getProperties().get("telemetrySuccess"))
.containsExactlyInAnyOrder("true", "false");
assertThat(items)
.allSatisfy(item -> assertThat(TestUtils.toMetricsData(item.getData().getBaseData()).getProperties())
.doesNotContainKeys("telemetry_success", "telemetry_type", "drop.code", "drop.reason"));
}

@Test
public void testCollectAndResetEmptyReturnsEmptyList() {
List<TelemetryItem> items
Expand Down Expand Up @@ -167,4 +212,14 @@ public void testConcurrentIncrements() throws InterruptedException {

assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(threads * incrementsPerThread);
}

private static MetricsData getMetric(List<TelemetryItem> items, String metricName) {
for (TelemetryItem item : items) {
MetricsData metric = TestUtils.toMetricsData(item.getData().getBaseData());
if (metricName.equals(metric.getMetrics().get(0).getName())) {
return metric;
}
}
throw new AssertionError("Metric not found: " + metricName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.monitor.opentelemetry.autoconfigure.implementation.utils;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class VersionGeneratorTest {

@Test
public void sdkVersionNumberIsAvailable() {
assertThat(VersionGenerator.getSdkVersionNumber()).isNotEqualTo("unknown");
assertThat(VersionGenerator.getSdkVersion()).endsWith(VersionGenerator.getSdkVersionNumber());
}
}
Loading