/bin:$JAVA_HOME/bin:$PATH
+```
+
+### 2. Navigate to the project
+
+```bash
+cd sdk/monitor/azure-monitor-opentelemetry-autoconfigure
+```
+
+### 3. Compile
+
+```bash
+mvn compile test-compile
+```
+
+### 4. Choose a test mode
+
+Set the SDKStats export interval to 60 seconds (instead of the default 900) so results appear sooner:
+
+**Windows (PowerShell):**
+```powershell
+$env:APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL = "60"
+```
+
+**Linux / macOS:**
+```bash
+export APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL=60
+```
+
+Then pick **one** of the following modes:
+
+#### Success mode (default)
+
+Telemetry is sent to real Azure Monitor. You will see `Item_Success_Count` in the `customMetrics` table.
+Set `APPLICATIONINSIGHTS_CONNECTION_STRING` to your Application Insights connection string.
+
+```bash
+# No TEST_MODE needed — defaults to "success"
+```
+
+#### Drop mode
+
+A local mock server on port 9090 returns **400** for all requests. You will see `Item_Dropped_Count` printed in the console.
+
+**Windows:** `$env:TEST_MODE = "drop"` | **Linux/macOS:** `export TEST_MODE=drop`
+
+#### Retry mode
+
+A local mock server on port 9090 returns **500** for all requests. You will see `Item_Retry_Count` printed in the console.
+
+**Windows:** `$env:TEST_MODE = "retry"` | **Linux/macOS:** `export TEST_MODE=retry`
+
+#### Optional: enable Azure SDK logging
+
+**Windows:** `$env:AZURE_LOG_LEVEL = "informational"` | **Linux/macOS:** `export AZURE_LOG_LEVEL=informational`
+
+### 5. Run the sample
+
+```bash
+mvn exec:java "-Dexec.mainClass=com.azure.monitor.opentelemetry.autoconfigure.SimpleWebAppSample" "-Dexec.classpathScope=test"
+```
+
+You should see:
+
+```
+========================================================
+ SimpleWebAppSample running on http://localhost:8080
+ Mode: SUCCESS — real Azure Monitor → Item_Success_Count
+========================================================
+```
+
+### 6. Generate telemetry
+
+Open a **second** terminal and hit the endpoints:
+
+```bash
+curl http://localhost:8080/
+curl http://localhost:8080/dependency
+curl http://localhost:8080/error
+curl http://localhost:8080/exception
+curl http://localhost:8080/load
+```
+
+| Endpoint | What it does |
+|----------------|-------------------------------------------|
+| `GET /` | Returns "Hello!" (request span) |
+| `GET /dependency` | Makes an outbound HTTP call (dependency span) |
+| `GET /error` | Simulates an error span |
+| `GET /exception` | Throws & records an exception |
+| `GET /load` | Fires 20 mixed child spans for volume |
+
+### 7. Wait ~60 seconds
+
+After the export interval elapses, you will see SDKStats metrics:
+
+- **Success mode**: Query `customMetrics` in Azure Monitor:
+ ```kusto
+ customMetrics
+ | where name startswith "Item_"
+ | project timestamp, name, value, customDimensions
+ | order by timestamp desc
+ ```
+- **Drop/Retry mode**: SDKStats payloads appear in the **first terminal's console output**, tagged with `[SDKStats]`:
+ ```
+ ╔══ MOCK INGESTION #4 [SDKStats] ══════════════════════════════
+ ║ POST /v2.1/track → returning 400
+ ║ {"name":"Metric","data":{"baseData":{"metrics":[{"name":"Item_Dropped_Count",...}]}}}
+ ╚═══════════════════════════════════════════════════════════════
+ ```
+
+### 8. Stop the sample
+
+Press `Ctrl+C` in the terminal running the sample.
+
+---
+
+## Notes
+
+- 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`).
diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/samples/java/com/azure/monitor/opentelemetry/autoconfigure/SimpleWebAppSample.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/samples/java/com/azure/monitor/opentelemetry/autoconfigure/SimpleWebAppSample.java
new file mode 100644
index 000000000000..f39ba06a177e
--- /dev/null
+++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/samples/java/com/azure/monitor/opentelemetry/autoconfigure/SimpleWebAppSample.java
@@ -0,0 +1,367 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.monitor.opentelemetry.autoconfigure;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * A simple long-running web app sample that generates requests, dependencies, traces, and
+ * exceptions — ideal for observing customer-facing SDKStats metrics in Azure Monitor.
+ *
+ * Test Modes (set via {@code TEST_MODE} environment variable):
+ *
+ * - success (default) — normal operation, telemetry reaches ingestion
+ * → {@code Item_Success_Count} visible in Azure Monitor
+ * - drop — a local mock server returns 400 for all requests
+ * → {@code Item_Dropped_Count} with drop code 400 (visible in mock server console output)
+ * - retry — a local mock server returns 500 for all requests
+ * → {@code Item_Retry_Count} with retry code 500 (visible in mock server console output)
+ *
+ *
+ * In drop/retry modes, a mock ingestion server runs on port 9090. Both application telemetry
+ * and SDKStats metrics go through the same pipeline, so SDKStats metrics also hit the mock.
+ * The mock server logs all received payloads (gunzipped) so you can inspect the generated
+ * {@code Item_Dropped_Count} and {@code Item_Retry_Count} TelemetryItems in the console.
+ *
+ * Set {@code APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL} to a lower value (e.g. 60)
+ * so you don't have to wait 15 minutes for the first SDKStats export.
+ *
+ * Endpoints:
+ *
+ * - {@code GET /} — returns "Hello!"
+ * - {@code GET /dependency} — makes an outbound HTTP call
+ * - {@code GET /error} — simulates an error span
+ * - {@code GET /exception} — throws and records an exception
+ * - {@code GET /load} — fires a batch of 20 mixed requests internally (for quick volume)
+ *
+ *
+ * Usage:
+ *
+ * # Test Item_Success_Count (default — goes to real Azure Monitor):
+ * set APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL=60
+ * mvn compile test-compile exec:java -Dexec.mainClass=...SimpleWebAppSample -Dexec.classpathScope=test
+ *
+ * # Test Item_Dropped_Count (mock server returns 400):
+ * set TEST_MODE=drop
+ * set APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL=60
+ * mvn compile test-compile exec:java -Dexec.mainClass=...SimpleWebAppSample -Dexec.classpathScope=test
+ *
+ * # Test Item_Retry_Count (mock server returns 500):
+ * set TEST_MODE=retry
+ * set APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL=60
+ * mvn compile test-compile exec:java -Dexec.mainClass=...SimpleWebAppSample -Dexec.classpathScope=test
+ *
+ */
+public class SimpleWebAppSample {
+
+ // ── Connection string for success mode (real Azure Monitor) ─────────────
+ // Read from APPLICATIONINSIGHTS_CONNECTION_STRING env var; if not set, use a placeholder.
+ private static final String CONNECTION_STRING_SUCCESS
+ = System.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") != null
+ ? System.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
+ : "InstrumentationKey="
+ + ";IngestionEndpoint=https://.in.applicationinsights.azure.com/"
+ + ";LiveEndpoint=https://.livediagnostics.monitor.azure.com/"
+ + ";ApplicationId=";
+
+ // ── Connection string for mock server (drop/retry modes) ────────────────
+ private static final int MOCK_PORT = 9090;
+ private static final String CONNECTION_STRING_MOCK
+ = "InstrumentationKey=00000000-0000-0000-0000-000000000000"
+ + ";IngestionEndpoint=http://localhost:" + MOCK_PORT + "/"
+ + ";LiveEndpoint=http://localhost:" + MOCK_PORT + "/";
+
+ private static final int APP_PORT = 8080;
+
+ private static Tracer tracer;
+ private static LongCounter requestCounter;
+
+ public static void main(String[] args) throws IOException {
+ // ── 0. Determine test mode ──────────────────────────────────────────
+ String testMode = System.getenv("TEST_MODE");
+ if (testMode == null || testMode.isEmpty()) {
+ testMode = "success";
+ }
+ testMode = testMode.toLowerCase();
+
+ String connectionString;
+ String modeDescription;
+ int mockStatusCode = 0;
+ switch (testMode) {
+ case "drop":
+ connectionString = CONNECTION_STRING_MOCK;
+ modeDescription = "DROP — mock server returns 400 → Item_Dropped_Count";
+ mockStatusCode = 400;
+ break;
+ case "retry":
+ connectionString = CONNECTION_STRING_MOCK;
+ modeDescription = "RETRY — mock server returns 500 → Item_Retry_Count";
+ mockStatusCode = 500;
+ break;
+ default:
+ connectionString = CONNECTION_STRING_SUCCESS;
+ modeDescription = "SUCCESS — real Azure Monitor → Item_Success_Count";
+ break;
+ }
+
+ // ── 1. Start mock ingestion server for drop/retry modes ─────────────
+ if (mockStatusCode > 0) {
+ startMockIngestionServer(mockStatusCode);
+ }
+
+ // ── 2. Configure OpenTelemetry + Azure Monitor ──────────────────────
+ AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder();
+ AzureMonitorAutoConfigure.customize(sdkBuilder, connectionString);
+ OpenTelemetry openTelemetry = sdkBuilder.build().getOpenTelemetrySdk();
+
+ tracer = openTelemetry.getTracer("SimpleWebApp");
+ Meter meter = openTelemetry.meterBuilder("SimpleWebApp").build();
+ requestCounter = meter.counterBuilder("http.server.request_count").build();
+
+ // ── 3. Start the web app HTTP server ────────────────────────────────
+ HttpServer server = HttpServer.create(new InetSocketAddress(APP_PORT), 0);
+ server.createContext("/", SimpleWebAppSample::handleRoot);
+ server.createContext("/dependency", SimpleWebAppSample::handleDependency);
+ server.createContext("/error", SimpleWebAppSample::handleError);
+ server.createContext("/exception", SimpleWebAppSample::handleException);
+ server.createContext("/load", SimpleWebAppSample::handleLoad);
+ server.start();
+
+ System.out.println("========================================================");
+ System.out.println(" SimpleWebAppSample running on http://localhost:" + APP_PORT);
+ System.out.println(" Mode: " + modeDescription);
+ System.out.println("========================================================");
+ System.out.println(" Endpoints:");
+ System.out.println(" GET / — hello");
+ System.out.println(" GET /dependency — outbound HTTP call");
+ System.out.println(" GET /error — simulated error span");
+ System.out.println(" GET /exception — throws & records exception");
+ System.out.println(" GET /load — fires 20 mixed requests");
+ System.out.println();
+ System.out.println(" Test modes (set TEST_MODE env var):");
+ System.out.println(" success (default) — Item_Success_Count (real Azure Monitor)");
+ System.out.println(" drop — Item_Dropped_Count (mock → 400)");
+ System.out.println(" retry — Item_Retry_Count (mock → 500)");
+ System.out.println();
+ System.out.println(" Tip: set APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL=60");
+ System.out.println(" to see customer SDKStats sooner.");
+ System.out.println("========================================================");
+ }
+
+ // ── Mock Ingestion Server ───────────────────────────────────────────────
+
+ /**
+ * Starts a local HTTP server on {@link #MOCK_PORT} that simulates ingestion failures.
+ * It handles POST /v2.1/track by returning the given status code and logging the
+ * gunzipped request payload so you can inspect SDKStats TelemetryItems in the console.
+ */
+ @SuppressWarnings("try")
+ private static void startMockIngestionServer(int statusCode) throws IOException {
+ AtomicInteger requestCount = new AtomicInteger();
+ HttpServer mock = HttpServer.create(new InetSocketAddress(MOCK_PORT), 0);
+ mock.createContext("/", exchange -> {
+ int reqNum = requestCount.incrementAndGet();
+ String method = exchange.getRequestMethod();
+ String path = exchange.getRequestURI().getPath();
+
+ // Read and gunzip the request body
+ String body = "(empty)";
+ try {
+ byte[] rawBytes = readAllBytes(exchange.getRequestBody());
+ if (rawBytes.length > 0) {
+ String contentEncoding = exchange.getRequestHeaders().getFirst("Content-Encoding");
+ if ("gzip".equalsIgnoreCase(contentEncoding)) {
+ body = gunzip(rawBytes);
+ } else {
+ body = new String(rawBytes, StandardCharsets.UTF_8);
+ }
+ }
+ } catch (Exception e) {
+ body = "(failed to read body: " + e.getMessage() + ")";
+ }
+
+ // Log the request to console
+ boolean isSdkStats = body.contains("Item_Success_Count")
+ || body.contains("Item_Dropped_Count")
+ || body.contains("Item_Retry_Count");
+ String tag = isSdkStats ? " [SDKStats]" : "";
+ System.out.println();
+ System.out.println("╔══ MOCK INGESTION #" + reqNum + tag + " ══════════════════════════════");
+ System.out.println("║ " + method + " " + path + " → returning " + statusCode);
+ // Print each telemetry item on its own line for readability
+ for (String line : body.split("\n")) {
+ System.out.println("║ " + line.trim());
+ }
+ System.out.println("╚═══════════════════════════════════════════════════════");
+
+ // Return the configured error status with a minimal JSON body
+ String responseBody = "{\"itemsReceived\":0,\"itemsAccepted\":0,\"errors\":[]}";
+ byte[] responseBytes = responseBody.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(statusCode, responseBytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(responseBytes);
+ }
+ });
+ mock.start();
+ System.out.println("[Mock] Ingestion server started on http://localhost:" + MOCK_PORT
+ + " — returning " + statusCode + " for all requests");
+ }
+
+ private static byte[] readAllBytes(InputStream is) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] tmp = new byte[4096];
+ int n;
+ while ((n = is.read(tmp)) != -1) {
+ buffer.write(tmp, 0, n);
+ }
+ return buffer.toByteArray();
+ }
+
+ private static String gunzip(byte[] compressed) throws IOException {
+ try (GZIPInputStream gis = new GZIPInputStream(new java.io.ByteArrayInputStream(compressed))) {
+ return new String(readAllBytes(gis), StandardCharsets.UTF_8);
+ }
+ }
+
+ // ── Handlers ────────────────────────────────────────────────────────────
+
+ @SuppressWarnings("try")
+ private static void handleRoot(HttpExchange exchange) throws IOException {
+ Span span = tracer.spanBuilder("GET /").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ requestCounter.add(1, Attributes.of(AttributeKey.stringKey("path"), "/"));
+ respond(exchange, 200, "Hello from SimpleWebAppSample!");
+ } finally {
+ span.end();
+ }
+ }
+
+ @SuppressWarnings("try")
+ private static void handleDependency(HttpExchange exchange) throws IOException {
+ Span span = tracer.spanBuilder("GET /dependency").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ requestCounter.add(1, Attributes.of(AttributeKey.stringKey("path"), "/dependency"));
+
+ // Simulate an outbound dependency call
+ Span depSpan = tracer.spanBuilder("HTTP GET httpbin.org").setSpanKind(SpanKind.CLIENT).startSpan();
+ try (Scope depScope = depSpan.makeCurrent()) {
+ URL url = new URL("https://httpbin.org/get");
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(5000);
+ int status = conn.getResponseCode();
+ depSpan.setAttribute("http.status_code", status);
+ conn.disconnect();
+ respond(exchange, 200, "Dependency call returned " + status);
+ } catch (Exception e) {
+ depSpan.recordException(e);
+ depSpan.setStatus(StatusCode.ERROR, e.getMessage());
+ respond(exchange, 502, "Dependency call failed: " + e.getMessage());
+ } finally {
+ depSpan.end();
+ }
+ } finally {
+ span.end();
+ }
+ }
+
+ @SuppressWarnings("try")
+ private static void handleError(HttpExchange exchange) throws IOException {
+ Span span = tracer.spanBuilder("GET /error").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ requestCounter.add(1, Attributes.of(AttributeKey.stringKey("path"), "/error"));
+ span.setStatus(StatusCode.ERROR, "simulated server error");
+ respond(exchange, 500, "Simulated 500 error");
+ } finally {
+ span.end();
+ }
+ }
+
+ @SuppressWarnings("try")
+ private static void handleException(HttpExchange exchange) throws IOException {
+ Span span = tracer.spanBuilder("GET /exception").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ requestCounter.add(1, Attributes.of(AttributeKey.stringKey("path"), "/exception"));
+ try {
+ throw new RuntimeException("Something went wrong!");
+ } catch (RuntimeException e) {
+ span.recordException(e);
+ span.setStatus(StatusCode.ERROR, e.getMessage());
+ respond(exchange, 500, "Exception recorded: " + e.getMessage());
+ }
+ } finally {
+ span.end();
+ }
+ }
+
+ @SuppressWarnings("try")
+ private static void handleLoad(HttpExchange exchange) throws IOException {
+ Span span = tracer.spanBuilder("GET /load").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ // Generate a batch of child spans to create volume
+ int count = 20;
+ for (int i = 0; i < count; i++) {
+ String op = randomOp();
+ Span child = tracer.spanBuilder("load-" + op).setSpanKind(SpanKind.INTERNAL).startSpan();
+ try (Scope childScope = child.makeCurrent()) {
+ // simulate some work
+ Thread.sleep(ThreadLocalRandom.current().nextInt(5, 50));
+ if ("error".equals(op)) {
+ child.setStatus(StatusCode.ERROR, "load error");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ child.end();
+ }
+ }
+ requestCounter.add(count, Attributes.of(AttributeKey.stringKey("path"), "/load"));
+ respond(exchange, 200, "Generated " + count + " spans");
+ } finally {
+ span.end();
+ }
+ }
+
+ // ── Helpers ─────────────────────────────────────────────────────────────
+
+ private static void respond(HttpExchange exchange, int status, String body) throws IOException {
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
+ exchange.sendResponseHeaders(status, bytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
+
+ private static String randomOp() {
+ String[] ops = {"ok", "ok", "ok", "error", "dep"};
+ return ops[ThreadLocalRandom.current().nextInt(ops.length)];
+ }
+}
diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsExceptionCategoryTest.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsExceptionCategoryTest.java
new file mode 100644
index 000000000000..dfa48c585f87
--- /dev/null
+++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsExceptionCategoryTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomerSdkStatsExceptionCategoryTest {
+
+ @Test
+ public void testSocketTimeoutException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new SocketTimeoutException("Read timed out")))
+ .isEqualTo("Timeout exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(new SocketTimeoutException("Read timed out")))
+ .isTrue();
+ }
+
+ @Test
+ public void testTimeoutException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new TimeoutException("timeout")))
+ .isEqualTo("Timeout exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(new TimeoutException("timeout"))).isTrue();
+ }
+
+ @Test
+ public void testUnknownHostException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new UnknownHostException("host.example.com")))
+ .isEqualTo("Network exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(new UnknownHostException("host.example.com")))
+ .isFalse();
+ }
+
+ @Test
+ public void testConnectException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new ConnectException("Connection refused")))
+ .isEqualTo("Network exception");
+ }
+
+ @Test
+ public void testIOException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new IOException("Disk full")))
+ .isEqualTo("Storage exception");
+ }
+
+ @Test
+ public void testGenericRuntimeException() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(new RuntimeException("Something went wrong")))
+ .isEqualTo("Client exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(new RuntimeException("Something went wrong")))
+ .isFalse();
+ }
+
+ @Test
+ public void testNullThrowable() {
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(null)).isEqualTo("Client exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(null)).isFalse();
+ }
+
+ @Test
+ public void testWrappedSocketTimeoutException() {
+ // A SocketTimeoutException wrapped in a RuntimeException should still be detected via cause
+ RuntimeException wrapper = new RuntimeException("wrapper", new SocketTimeoutException("Read timed out"));
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(wrapper)).isEqualTo("Timeout exception");
+ assertThat(CustomerSdkStatsExceptionCategory.containsTimeout(wrapper)).isTrue();
+ }
+
+ @Test
+ public void testWrappedUnknownHostException() {
+ RuntimeException wrapper = new RuntimeException("wrapper", new UnknownHostException("host.example.com"));
+ assertThat(CustomerSdkStatsExceptionCategory.categorize(wrapper)).isEqualTo("Network exception");
+ }
+}
diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryPipelineListenerTest.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryPipelineListenerTest.java
new file mode 100644
index 000000000000..1c99704392ac
--- /dev/null
+++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryPipelineListenerTest.java
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat;
+
+import com.azure.monitor.opentelemetry.autoconfigure.implementation.pipeline.TelemetryPipelineRequest;
+import com.azure.monitor.opentelemetry.autoconfigure.implementation.pipeline.TelemetryPipelineResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomerSdkStatsTelemetryPipelineListenerTest {
+
+ private CustomerSdkStats customerSdkStats;
+ private CustomerSdkStatsTelemetryPipelineListener listener;
+
+ @BeforeEach
+ public void init() {
+ customerSdkStats = new CustomerSdkStats("unknown", "java", "3.5.1");
+ listener = new CustomerSdkStatsTelemetryPipelineListener(customerSdkStats);
+ }
+
+ @Test
+ public void testSuccessResponse() {
+ Map itemCounts = new HashMap<>();
+ itemCounts.put("REQUEST", 5L);
+ itemCounts.put("DEPENDENCY", 3L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(200, "OK");
+
+ listener.onResponse(request, response);
+
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(5);
+ assertThat(customerSdkStats.getSuccessCount("DEPENDENCY")).isEqualTo(3);
+ }
+
+ @Test
+ public void testPartialSuccessResponse206() {
+ Map itemCounts = new HashMap<>();
+ itemCounts.put("REQUEST", 8L);
+ itemCounts.put("TRACE", 2L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ // 206 = partial success: some items accepted, some rejected
+ TelemetryPipelineResponse response
+ = new TelemetryPipelineResponse(206, "{\"itemsReceived\":10,\"itemsAccepted\":7,\"errors\":[]}");
+
+ listener.onResponse(request, response);
+
+ // Entire batch counted as success (failed items retried from disk with empty metadata)
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(8);
+ assertThat(customerSdkStats.getSuccessCount("TRACE")).isEqualTo(2);
+ assertThat(customerSdkStats.getDroppedCount("REQUEST", "206")).isEqualTo(0);
+ }
+
+ @Test
+ public void testRetryableStatusCode429() {
+ Map itemCounts = Collections.singletonMap("TRACE", 10L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(429, "Too Many Requests");
+
+ listener.onResponse(request, response);
+
+ assertThat(customerSdkStats.getRetryCount("TRACE", "429")).isEqualTo(10);
+ }
+
+ @Test
+ public void testRetryableStatusCode500() {
+ Map itemCounts = Collections.singletonMap("DEPENDENCY", 5L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(500, "Internal Server Error");
+
+ listener.onResponse(request, response);
+
+ assertThat(customerSdkStats.getRetryCount("DEPENDENCY", "500")).isEqualTo(5);
+ }
+
+ @Test
+ public void testNonRetryableStatusCode402() {
+ Map itemCounts = Collections.singletonMap("CUSTOM_METRIC", 20L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(402, "Payment Required");
+
+ listener.onResponse(request, response);
+
+ assertThat(customerSdkStats.getDroppedCount("CUSTOM_METRIC", "402")).isEqualTo(20);
+ }
+
+ @Test
+ public void testTimeoutException() {
+ Map itemCounts = Collections.singletonMap("REQUEST", 7L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+
+ listener.onException(request, "Read timed out", new SocketTimeoutException("Read timed out"));
+
+ assertThat(customerSdkStats.getRetryCount("REQUEST", "CLIENT_TIMEOUT")).isEqualTo(7);
+ }
+
+ @Test
+ public void testNetworkException() {
+ Map itemCounts = Collections.singletonMap("DEPENDENCY", 12L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+
+ listener.onException(request, "Unknown host", new UnknownHostException("host.example.com"));
+
+ assertThat(customerSdkStats.getRetryCount("DEPENDENCY", "CLIENT_EXCEPTION")).isEqualTo(12);
+ }
+
+ @Test
+ public void testEmptyItemCountsSkipped() {
+ TelemetryPipelineRequest request = createRequest(Collections.emptyMap());
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(200, "OK");
+
+ listener.onResponse(request, response);
+
+ // Nothing should be tracked
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(0);
+ }
+
+ @Test
+ public void testRedirectSkipped() {
+ Map itemCounts = Collections.singletonMap("REQUEST", 5L);
+
+ TelemetryPipelineRequest request = createRequest(itemCounts);
+ TelemetryPipelineResponse response = new TelemetryPipelineResponse(307, "Temporary Redirect");
+
+ listener.onResponse(request, response);
+
+ // Redirects should not be tracked
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(0);
+ assertThat(customerSdkStats.getRetryCount("REQUEST", "307")).isEqualTo(0);
+ assertThat(customerSdkStats.getDroppedCount("REQUEST", "307")).isEqualTo(0);
+ }
+
+ @Test
+ public void testReasonPhraseForStatusCode() {
+ assertThat(CustomerSdkStatsTelemetryPipelineListener.getReasonPhraseForStatusCode(402))
+ .isEqualTo("Exceeded daily quota");
+ assertThat(CustomerSdkStatsTelemetryPipelineListener.getReasonPhraseForStatusCode(429))
+ .isEqualTo("Too many requests");
+ assertThat(CustomerSdkStatsTelemetryPipelineListener.getReasonPhraseForStatusCode(500))
+ .isEqualTo("Internal server error");
+ assertThat(CustomerSdkStatsTelemetryPipelineListener.getReasonPhraseForStatusCode(999)).isEqualTo("Unknown");
+ }
+
+ private static TelemetryPipelineRequest createRequest(Map itemCountsByType) {
+ List byteBuffers = Collections.singletonList(ByteBuffer.allocate(0));
+ try {
+ URL url = new URL("https://dc.services.visualstudio.com/v2.1/track");
+ TelemetryBatchMetadata batchMetadata
+ = new TelemetryBatchMetadata(itemCountsByType, Collections.emptyMap(), Collections.emptyMap());
+ return new TelemetryPipelineRequest(url, "InstrumentationKey=00000000-0000-0000-0000-0FEEDDADBEEF",
+ "00000000-0000-0000-0000-0FEEDDADBEEF", byteBuffers, batchMetadata);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryTypeTest.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryTypeTest.java
new file mode 100644
index 000000000000..488012fed406
--- /dev/null
+++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTelemetryTypeTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomerSdkStatsTelemetryTypeTest {
+
+ @Test
+ public void testRequestMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Request")).isEqualTo("REQUEST");
+ }
+
+ @Test
+ public void testDependencyMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("RemoteDependency")).isEqualTo("DEPENDENCY");
+ }
+
+ @Test
+ public void testTraceMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Message")).isEqualTo("TRACE");
+ }
+
+ @Test
+ public void testExceptionMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Exception")).isEqualTo("EXCEPTION");
+ }
+
+ @Test
+ public void testMetricMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Metric")).isEqualTo("CUSTOM_METRIC");
+ }
+
+ @Test
+ public void testEventMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Event")).isEqualTo("CUSTOM_EVENT");
+ }
+
+ @Test
+ public void testPageViewMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("PageView")).isEqualTo("PAGE_VIEW");
+ }
+
+ @Test
+ public void testAvailabilityMapping() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Availability")).isEqualTo("AVAILABILITY");
+ }
+
+ @Test
+ public void testStatsbeatReturnsNull() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("Statsbeat")).isNull();
+ }
+
+ @Test
+ public void testUnknownTypeReturnsNull() {
+ assertThat(CustomerSdkStatsTelemetryType.fromTelemetryItemName("UnknownType")).isNull();
+ }
+}
diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTest.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTest.java
new file mode 100644
index 000000000000..767a5ed565dc
--- /dev/null
+++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/statsbeat/CustomerSdkStatsTest.java
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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.TelemetryItem;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomerSdkStatsTest {
+
+ private CustomerSdkStats customerSdkStats;
+ private static final ConnectionString CONNECTION_STRING
+ = ConnectionString.parse("InstrumentationKey=00000000-0000-0000-0000-0FEEDDADBEEF");
+ private static final String SDK_VERSION = "java:3.5.1";
+ private static final String CLOUD_ROLE = "TestRole";
+ private static final String CLOUD_ROLE_INSTANCE = "TestInstance";
+
+ @BeforeEach
+ public void init() {
+ customerSdkStats = new CustomerSdkStats("unknown", "java", "3.5.1");
+ }
+
+ @Test
+ public void testIncrementSuccessCount() {
+ Map itemCountsByType = new HashMap<>();
+ itemCountsByType.put("REQUEST", 5L);
+ itemCountsByType.put("DEPENDENCY", 3L);
+
+ customerSdkStats.incrementSuccessCount(itemCountsByType);
+
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(5);
+ assertThat(customerSdkStats.getSuccessCount("DEPENDENCY")).isEqualTo(3);
+ assertThat(customerSdkStats.getSuccessCount("TRACE")).isEqualTo(0);
+ }
+
+ @Test
+ public void testIncrementSuccessCountMultipleTimes() {
+ Map batch1 = Collections.singletonMap("REQUEST", 3L);
+ Map batch2 = Collections.singletonMap("REQUEST", 7L);
+
+ customerSdkStats.incrementSuccessCount(batch1);
+ customerSdkStats.incrementSuccessCount(batch2);
+
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(10);
+ }
+
+ @Test
+ public void testIncrementDroppedCount() {
+ Map itemCountsByType = new HashMap<>();
+ itemCountsByType.put("TRACE", 10L);
+ Map successItems = Collections.emptyMap();
+ Map failureItems = Collections.emptyMap();
+
+ customerSdkStats.incrementDroppedCount(itemCountsByType, "402", "Exceeded daily quota", successItems,
+ failureItems);
+
+ assertThat(customerSdkStats.getDroppedCount("TRACE", "402")).isEqualTo(10);
+ }
+
+ @Test
+ public void testIncrementDroppedCountWithTelemetrySuccess() {
+ Map itemCountsByType = new HashMap<>();
+ itemCountsByType.put("REQUEST", 8L);
+ Map successItems = Collections.singletonMap("REQUEST", 5L);
+ Map failureItems = Collections.singletonMap("REQUEST", 3L);
+
+ customerSdkStats.incrementDroppedCount(itemCountsByType, "402", "Exceeded daily quota", successItems,
+ failureItems);
+
+ // Total dropped for REQUEST with code 402 should be 8 (5 success + 3 failure)
+ assertThat(customerSdkStats.getDroppedCount("REQUEST", "402")).isEqualTo(8);
+ }
+
+ @Test
+ public void testIncrementRetryCount() {
+ Map itemCountsByType = new HashMap<>();
+ itemCountsByType.put("DEPENDENCY", 15L);
+ itemCountsByType.put("REQUEST", 5L);
+
+ customerSdkStats.incrementRetryCount(itemCountsByType, "429", "Too many requests");
+
+ assertThat(customerSdkStats.getRetryCount("DEPENDENCY", "429")).isEqualTo(15);
+ assertThat(customerSdkStats.getRetryCount("REQUEST", "429")).isEqualTo(5);
+ }
+
+ @Test
+ public void testIncrementRetryCountClientTimeout() {
+ Map itemCountsByType = Collections.singletonMap("TRACE", 20L);
+
+ customerSdkStats.incrementRetryCount(itemCountsByType, CustomerSdkStats.RETRY_CODE_CLIENT_TIMEOUT,
+ "Timeout exception");
+
+ assertThat(customerSdkStats.getRetryCount("TRACE", CustomerSdkStats.RETRY_CODE_CLIENT_TIMEOUT)).isEqualTo(20);
+ }
+
+ @Test
+ public void testCollectAndResetReturnsCorrectMetrics() {
+ // Add some data
+ customerSdkStats.incrementSuccessCount(Collections.singletonMap("REQUEST", 10L));
+ customerSdkStats.incrementDroppedCount(Collections.singletonMap("DEPENDENCY", 5L), "402",
+ "Exceeded daily quota", Collections.emptyMap(), Collections.emptyMap());
+ customerSdkStats.incrementRetryCount(Collections.singletonMap("TRACE", 3L), "429", "Too many requests");
+
+ List items
+ = customerSdkStats.collectAndReset(CONNECTION_STRING, SDK_VERSION, CLOUD_ROLE, CLOUD_ROLE_INSTANCE);
+
+ // Should have 3 metric items
+ assertThat(items).hasSize(3);
+
+ // 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 testCollectAndResetEmptyReturnsEmptyList() {
+ List items
+ = customerSdkStats.collectAndReset(CONNECTION_STRING, SDK_VERSION, CLOUD_ROLE, CLOUD_ROLE_INSTANCE);
+
+ assertThat(items).isEmpty();
+ }
+
+ @Test
+ public void testCollectAndResetClearsCounters() {
+ customerSdkStats.incrementSuccessCount(Collections.singletonMap("REQUEST", 100L));
+
+ // First collect
+ List items1
+ = customerSdkStats.collectAndReset(CONNECTION_STRING, SDK_VERSION, CLOUD_ROLE, CLOUD_ROLE_INSTANCE);
+ assertThat(items1).hasSize(1);
+
+ // Second collect should be empty
+ List items2
+ = customerSdkStats.collectAndReset(CONNECTION_STRING, SDK_VERSION, CLOUD_ROLE, CLOUD_ROLE_INSTANCE);
+ assertThat(items2).isEmpty();
+ }
+
+ @Test
+ public void testConcurrentIncrements() throws InterruptedException {
+ int threads = 10;
+ int incrementsPerThread = 1000;
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+
+ for (int i = 0; i < threads; i++) {
+ executor.submit(() -> {
+ for (int j = 0; j < incrementsPerThread; j++) {
+ customerSdkStats.incrementSuccessCount(Collections.singletonMap("REQUEST", 1L));
+ }
+ });
+ }
+
+ executor.shutdown();
+ assertThat(executor.awaitTermination(10, TimeUnit.SECONDS)).isTrue();
+
+ assertThat(customerSdkStats.getSuccessCount("REQUEST")).isEqualTo(threads * incrementsPerThread);
+ }
+}