From ec58878231b7277ed1c4076285f6a544d2050695 Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 27 Oct 2024 03:16:31 +0100 Subject: [PATCH 1/7] Added an implementation which provides a flexible way to manage bandwidth usage when exporting spans, allowing for smoother data flow and preventing resource hogging. It can further refine the size estimation logic based on a specific use case. --- .../android/export/RateLimitedExporter.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java diff --git a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java new file mode 100644 index 000000000..c5c8e12f8 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.opentelemetry.android; + +import android.util.Log; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +class BandwidthThrottlingExporter implements SpanExporter { + private final SpanExporter delegate; + private final Function categoryFunction; + private final long maxBytesPerSecond; + private final long timeWindowInMillis; + private long lastExportTime; + private long bytesExportedInWindow; + + private BandwidthThrottlingExporter(Builder builder) { + this.delegate = builder.delegate; + this.categoryFunction = builder.categoryFunction; + this.maxBytesPerSecond = builder.maxBytesPerSecond; + this.timeWindowInMillis = builder.timeWindow.toMillis(); + this.lastExportTime = System.currentTimeMillis(); + this.bytesExportedInWindow = 0; + } + + static Builder newBuilder(SpanExporter delegate) { + return new Builder(delegate); + } + + @Override + public CompletableResultCode export(Collection spans) { + List spansToExport = new ArrayList<>(); + long totalBytes = 0; + + for (SpanData span : spans) { + // Estimate the size of the span (this can be adjusted based on actual size) + long spanSize = estimateSpanSize(span); + totalBytes += spanSize; + + // Check if we can export this span based on the current bandwidth limit + if (canExport(spanSize)) { + spansToExport.add(span); + bytesExportedInWindow += spanSize; + } else { + Log.d("BandwidthThrottlingExporter", "Throttled span: " + span.getName()); + } + } + + return delegate.export(spansToExport); + } + + private boolean canExport(long spanSize) { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastExportTime > timeWindowInMillis) { + // Reset the window + bytesExportedInWindow = 0; + lastExportTime = currentTime; + } + + return (bytesExportedInWindow + spanSize) + <= maxBytesPerSecond * (timeWindowInMillis / 1000); + } + + private long estimateSpanSize(SpanData span) { + // This is a placeholder for actual size estimation logic + return span.getAttributes().size() * 8; // Example: 8 bytes per attribute + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + static class Builder { + final SpanExporter delegate; + Function categoryFunction = span -> "default"; + long maxBytesPerSecond = 1024; // Default to 1 KB/s + Duration timeWindow = Duration.ofSeconds(1); // Default to 1 second + + private Builder(SpanExporter delegate) { + this.delegate = delegate; + } + + Builder maxBytesPerSecond(long maxBytesPerSecond) { + this.maxBytesPerSecond = maxBytesPerSecond; + return this; + } + + Builder timeWindow(Duration timeWindow) { + this.timeWindow = timeWindow; + return this; + } + + BandwidthThrottlingExporter build() { + return new BandwidthThrottlingExporter(this); + } + } +} From d6b4d5c1bca93f035399a3f7e0962dd7b09861ec Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 27 Oct 2024 03:51:35 +0100 Subject: [PATCH 2/7] Added an implementation which provides a flexible way to manage bandwidth usage when exporting spans, allowing for smoother data flow and preventing resource hogging. It can further refine the size estimation logic based on a specific use case. Relate to Add ability to throttle exports when reading from disk. #638 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a0df3d643..ff917c0b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ demo-app/local.properties .DS_Store **/build/ + From ffcdf3c9bfdbbb4b388a8129b01bae68333eb00c Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 30 Oct 2024 02:47:19 +0100 Subject: [PATCH 3/7] Fix comments Relate to #638 --- core/build.gradle.kts | 6 ++++++ .../opentelemetry/android/export/RateLimitedExporter.java | 1 + 2 files changed, 7 insertions(+) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 6d32acda9..f863ad92e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,6 +12,12 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") buildConfigField("String", "OTEL_ANDROID_VERSION", "\"$version\"") + +// Enable desugaring for Java 8 features + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 // Set source compatibility to Java 8 + targetCompatibility = JavaVersion.VERSION_1_8 // Set target compatibility to Java 8 + } } buildTypes { diff --git a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java index c5c8e12f8..4adee7371 100644 --- a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java +++ b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java @@ -29,6 +29,7 @@ private BandwidthThrottlingExporter(Builder builder) { this.maxBytesPerSecond = builder.maxBytesPerSecond; this.timeWindowInMillis = builder.timeWindow.toMillis(); this.lastExportTime = System.currentTimeMillis(); + this.bytesExportedInWindow = 0; } From 6493988d206ff53b6c8d91b4e0cb3ccf4fc797ae Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 1 Nov 2024 03:41:45 +0100 Subject: [PATCH 4/7] replaced Function with a custom CategoryFunction interface and Duration with a plain long value for timeWindowInMillis ref #638 --- .vscode/settings.json | 3 +++ core/build.gradle.kts | 6 ------ .../opentelemetry/android/export/RateLimitedExporter.java | 7 +++---- 3 files changed, 6 insertions(+), 10 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..e0f15db2e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f863ad92e..6d32acda9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,12 +12,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") buildConfigField("String", "OTEL_ANDROID_VERSION", "\"$version\"") - -// Enable desugaring for Java 8 features - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 // Set source compatibility to Java 8 - targetCompatibility = JavaVersion.VERSION_1_8 // Set target compatibility to Java 8 - } } buildTypes { diff --git a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java index 4adee7371..b4e5372a3 100644 --- a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java +++ b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java @@ -28,8 +28,7 @@ private BandwidthThrottlingExporter(Builder builder) { this.categoryFunction = builder.categoryFunction; this.maxBytesPerSecond = builder.maxBytesPerSecond; this.timeWindowInMillis = builder.timeWindow.toMillis(); - this.lastExportTime = System.currentTimeMillis(); - + this.lastExportTime = SystemTime.get().getCurrentTimeMillis(); this.bytesExportedInWindow = 0; } @@ -88,9 +87,9 @@ public CompletableResultCode shutdown() { static class Builder { final SpanExporter delegate; - Function categoryFunction = span -> "default"; + CategoryFunction categoryFunction = span -> "default"; long maxBytesPerSecond = 1024; // Default to 1 KB/s - Duration timeWindow = Duration.ofSeconds(1); // Default to 1 second + long timeWindowInMillis = 1000; // Default to 1 second private Builder(SpanExporter delegate) { this.delegate = delegate; From 8a7a07f26d993dae1b775eb4c3f65a20d3f42e2c Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 2 Nov 2024 02:30:02 +0100 Subject: [PATCH 5/7] JUnit4 test for BandwidthThrottlingExporter Ref: Add ability to throttle exports when reading from disk. #638 --- .vscode/settings.json | 3 - .../BandwidthThrottlingExporterTest.java | 113 ++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 core/src/test/java/io/opentelemetry/android/export/BandwidthThrottlingExporterTest.java diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db2e..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file diff --git a/core/src/test/java/io/opentelemetry/android/export/BandwidthThrottlingExporterTest.java b/core/src/test/java/io/opentelemetry/android/export/BandwidthThrottlingExporterTest.java new file mode 100644 index 000000000..de94c9dcf --- /dev/null +++ b/core/src/test/java/io/opentelemetry/android/export/BandwidthThrottlingExporterTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.opentelemetry.android; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import android.util.Log; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Function; +import org.junit.Before; +import org.junit.Test; + +public class BandwidthThrottlingExporterTest { + + private SpanExporter mockDelegate; // Mocked SpanExporter to simulate the delegate + private Function mockCategoryFunction; // Mocked category function + private BandwidthThrottlingExporter exporter; // Instance of the BandwidthThrottlingExporter + + @Before + public void setUp() { + // Initialize the mocked delegate and category function + mockDelegate = mock(SpanExporter.class); + mockCategoryFunction = mock(Function.class); + + // Create an instance of BandwidthThrottlingExporter with a max limit of 1 KB/s and a + // 1-second window + exporter = + BandwidthThrottlingExporter.newBuilder(mockDelegate) + .maxBytesPerSecond(1024) // 1 KB/s + .timeWindow(Duration.ofSeconds(1)) // 1 second + .build(); + } + + @Test + public void testExportWithinLimit() { + // Create a mock SpanData object + SpanData spanData = mock(SpanData.class); + when(spanData.getAttributes()) + .thenReturn(Collections.singletonMap("key", "value")); // Simulate attributes + + // Simulate the export of a single span + CompletableResultCode result = exporter.export(Collections.singletonList(spanData)); + + // Verify that the delegate's export method was called + verify(mockDelegate).export(anyList()); + assertTrue(result.isSuccess()); // Ensure the export was successful + } + + @Test + public void testExportExceedingLimit() { + // Create two mock SpanData objects + SpanData spanData1 = mock(SpanData.class); + SpanData spanData2 = mock(SpanData.class); + when(spanData1.getAttributes()) + .thenReturn(Collections.singletonMap("key1", "value1")); // Simulate attributes + when(spanData2.getAttributes()) + .thenReturn(Collections.singletonMap("key2", "value2")); // Simulate attributes + + // Simulate exporting spans that exceed the limit + CompletableResultCode result = exporter.export(Arrays.asList(spanData1, spanData2)); + + // Verify that the delegate's export method was called only once + verify(mockDelegate, times(1)).export(anyList()); + + // Ensure the result is successful + assertTrue(result.isSuccess()); + } + + @Test + public void testExportWithDifferentCategories() { + // Create two mock SpanData objects with different attributes + SpanData spanData1 = mock(SpanData.class); + SpanData spanData2 = mock(SpanData.class); + when(spanData1.getAttributes()).thenReturn(Collections.singletonMap("key1", "value1")); + when(spanData2.getAttributes()).thenReturn(Collections.singletonMap("key2", "value2")); + + // Simulate exporting both spans + CompletableResultCode result = exporter.export(Arrays.asList(spanData1, spanData2)); + + // Verify that the delegate's export method was called + verify(mockDelegate).export(anyList()); + + // Ensure the result is successful + assertTrue(result.isSuccess()); + } + + @Test + public void testThrottlingLogMessage() { + // Create a mock SpanData object that exceeds the limit + SpanData spanData = mock(SpanData.class); + when(spanData.getAttributes()).thenReturn(Collections.singletonMap("key", "value")); + + // Export the span multiple times to exceed the limit + exporter.export(Collections.singletonList(spanData)); + exporter.export(Collections.singletonList(spanData)); // This should be throttled + + // Capture the log output + // Note: In a real test, you might want to use a logging framework that allows capturing + // logs + // Here we just verify that the log message is generated + // This is a placeholder as capturing logs in unit tests can be complex + Log.d("BandwidthThrottlingExporter", "Throttled span: " + spanData.getName()); + } +} From adadd523ca34e2f393623beada5cce17a998f481 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 14 Nov 2024 03:06:36 +0100 Subject: [PATCH 6/7] Fix comments Relate to #638 --- .../android/export/RateLimitedExporter.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java index b4e5372a3..bbbc4cea3 100644 --- a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java +++ b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java @@ -59,7 +59,7 @@ public CompletableResultCode export(Collection spans) { } private boolean canExport(long spanSize) { - long currentTime = System.currentTimeMillis(); + long currentTime = SystemTime.get().getCurrentTimeMillis(); if (currentTime - lastExportTime > timeWindowInMillis) { // Reset the window bytesExportedInWindow = 0; @@ -87,26 +87,26 @@ public CompletableResultCode shutdown() { static class Builder { final SpanExporter delegate; - CategoryFunction categoryFunction = span -> "default"; - long maxBytesPerSecond = 1024; // Default to 1 KB/s - long timeWindowInMillis = 1000; // Default to 1 second - + private CategoryFunction categoryFunction = span -> "default"; + private long maxBytesPerSecond = 1024; // Default to 1 KB/s + private long timeWindowInMillis = 1000; // Default to 1 second + private Builder(SpanExporter delegate) { this.delegate = delegate; } - + Builder maxBytesPerSecond(long maxBytesPerSecond) { this.maxBytesPerSecond = maxBytesPerSecond; return this; } - + Builder timeWindow(Duration timeWindow) { this.timeWindow = timeWindow; return this; } - + BandwidthThrottlingExporter build() { return new BandwidthThrottlingExporter(this); } - } + } } From 08370578d38461ad222415e137d2ae760dac9948 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 14 Nov 2024 03:17:14 +0100 Subject: [PATCH 7/7] Fix comments Relate to #638 --- .../android/export/RateLimitedExporter.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java index bbbc4cea3..8c185b1d8 100644 --- a/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java +++ b/core/src/main/java/io/opentelemetry/android/export/RateLimitedExporter.java @@ -90,23 +90,23 @@ static class Builder { private CategoryFunction categoryFunction = span -> "default"; private long maxBytesPerSecond = 1024; // Default to 1 KB/s private long timeWindowInMillis = 1000; // Default to 1 second - + private Builder(SpanExporter delegate) { this.delegate = delegate; } - + Builder maxBytesPerSecond(long maxBytesPerSecond) { this.maxBytesPerSecond = maxBytesPerSecond; return this; } - + Builder timeWindow(Duration timeWindow) { this.timeWindow = timeWindow; return this; } - + BandwidthThrottlingExporter build() { return new BandwidthThrottlingExporter(this); } - } + } }