diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e79bf3..7d3659d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Unreleased -* tbd +* Add new incubating API: `SplunkRumBuilder.setHttpSenderCustomizer()` to allow customization + of the HTTP client used for sending data to Splunk. This can be useful when devices are + behind a proxy or API gateway. ## Version 1.3.1 - 2023-12-14 diff --git a/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java b/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java index ff1f4344..a369eda0 100644 --- a/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java +++ b/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java @@ -24,6 +24,7 @@ import io.opentelemetry.api.common.Attributes; import java.time.Duration; import java.util.regex.Pattern; +import okhttp3.Request; public class SampleApplication extends Application { @@ -65,6 +66,20 @@ public void onCreate() { HTTP_URL_SENSITIVE_DATA_PATTERN .matcher(value) .replaceAll("$1="))) + .setHttpSenderCustomizer( + okHttpBuilder -> { + okHttpBuilder.compressionEnabled(true); + okHttpBuilder + .clientBuilder() + .addInterceptor( + chain -> { + Request.Builder requestBuilder = + chain.request().newBuilder(); + requestBuilder.header( + "X-My-Custom-Header", "abc123"); + return chain.proceed(requestBuilder.build()); + }); + }) .build(this); } } diff --git a/splunk-otel-android/build.gradle.kts b/splunk-otel-android/build.gradle.kts index 32b9126e..31c8a4cf 100644 --- a/splunk-otel-android/build.gradle.kts +++ b/splunk-otel-android/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation("io.opentelemetry:opentelemetry-sdk") implementation("io.opentelemetry:opentelemetry-exporter-zipkin") - implementation("io.zipkin.reporter2:zipkin-sender-okhttp3") implementation("io.opentelemetry:opentelemetry-exporter-logging") implementation("io.opentelemetry.semconv:opentelemetry-semconv:$otelSemconvVersion") @@ -62,6 +61,7 @@ dependencies { api("io.opentelemetry:opentelemetry-api") api("com.squareup.okhttp3:okhttp:4.12.0") + api("io.zipkin.reporter2:zipkin-sender-okhttp3:2.17.2") testImplementation("org.mockito:mockito-core:5.8.0") testImplementation("org.mockito:mockito-junit-jupiter:5.8.0") diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index 7fc577ad..003efaf8 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -342,7 +342,8 @@ private SpanExporter buildExporter( private SpanExporter buildStorageBufferingExporter( CurrentNetworkProvider currentNetworkProvider, SpanStorage spanStorage) { - Sender sender = OkHttpSender.newBuilder().endpoint(getEndpoint()).build(); + Sender sender = buildCustomizedZipkinSender(); + BandwidthTracker bandwidthTracker = new BandwidthTracker(); FileSender fileSender = @@ -359,6 +360,13 @@ private SpanExporter buildStorageBufferingExporter( return getToDiskExporter(spanStorage); } + @NonNull + private Sender buildCustomizedZipkinSender() { + OkHttpSender.Builder okBuilder = OkHttpSender.newBuilder().endpoint(getEndpoint()); + builder.httpSenderCustomizer.customize(okBuilder); + return okBuilder.build(); + } + @NonNull private String getEndpoint() { return builder.beaconEndpoint + "?auth=" + builder.rumAccessToken; @@ -388,13 +396,15 @@ SpanExporter getToDiskExporter(SpanStorage spanStorage) { SpanExporter getCoreSpanExporter(String endpoint) { // return a lazy init exporter so the main thread doesn't block on the setup. return new LazyInitSpanExporter( - () -> - ZipkinSpanExporter.builder() - .setEncoder(new CustomZipkinEncoder()) - .setEndpoint(endpoint) - // remove the local IP address - .setLocalIpAddressSupplier(() -> null) - .build()); + () -> { + return ZipkinSpanExporter.builder() + .setEncoder(new CustomZipkinEncoder()) + .setEndpoint(endpoint) + // remove the local IP address + .setLocalIpAddressSupplier(() -> null) + .setSender(buildCustomizedZipkinSender()) + .build(); + }); } private static class LazyInitSpanExporter implements SpanExporter { diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java index 07f17bf2..0132a639 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java @@ -21,6 +21,7 @@ import android.app.Application; import android.util.Log; import androidx.annotation.Nullable; +import com.splunk.rum.incubating.HttpSenderCustomizer; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.time.Duration; @@ -42,6 +43,7 @@ public final class SplunkRumBuilder { Duration slowRenderingDetectionPollInterval = DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL; Attributes globalAttributes = Attributes.empty(); @Nullable String deploymentEnvironment; + HttpSenderCustomizer httpSenderCustomizer = HttpSenderCustomizer.DEFAULT; private Consumer spanFilterConfigurer = x -> {}; int maxUsageMegabytes = DEFAULT_MAX_STORAGE_USE_MB; boolean sessionBasedSamplerEnabled = false; @@ -78,6 +80,23 @@ public SplunkRumBuilder setBeaconEndpoint(String beaconEndpoint) { return this; } + /** + * This method can be used to provide a customizer that will have access to the + * OkHttpSender.Builder before the sender is created. Typical use cases for this are to provide + * custom headers or to modify compression settings. This is a pretty large hammer and should be + * used with caution. + * + *

This API is considered incubating and is subject to change. + * + * @param customizer that can make changes to the OkHttpSender.Builder + * @return {@code this} + * @since 1.4.0 + */ + public SplunkRumBuilder setHttpSenderCustomizer(HttpSenderCustomizer customizer) { + this.httpSenderCustomizer = customizer; + return this; + } + /** * Sets the realm for the beacon to send RUM telemetry to. This should be used in place of the * {@link #setBeaconEndpoint(String)} method in most cases. diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/incubating/HttpSenderCustomizer.java b/splunk-otel-android/src/main/java/com/splunk/rum/incubating/HttpSenderCustomizer.java new file mode 100644 index 00000000..412252d0 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/incubating/HttpSenderCustomizer.java @@ -0,0 +1,32 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum.incubating; + +import zipkin2.reporter.okhttp3.OkHttpSender; + +/** + * This interface can be used to customize the exporter used to send telemetry to Splunk. It is not + * yet stable and its APIs are subject to change at any time. + * + * @since 1.4.0 + */ +public interface HttpSenderCustomizer { + + HttpSenderCustomizer DEFAULT = x -> {}; + + void customize(OkHttpSender.Builder builder); +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java index daf5212d..7cbc95db 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java @@ -23,6 +23,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +31,7 @@ import android.app.Application; import android.content.Context; import android.os.Looper; +import com.splunk.rum.incubating.HttpSenderCustomizer; import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; import io.opentelemetry.android.instrumentation.network.CurrentNetwork; import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; @@ -48,10 +50,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import zipkin2.reporter.okhttp3.OkHttpSender; @ExtendWith(MockitoExtension.class) class RumInitializerTest { @@ -214,6 +218,30 @@ private TestSpanData createTestSpan(long startTimeNanos) { .build(); } + @Test + void canCustomizeHttpSender() { + AtomicReference seenBuilder = new AtomicReference<>(); + HttpSenderCustomizer customizer = seenBuilder::set; + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setRealm("us0") + .setRumAccessToken("secret!") + .setApplicationName("test") + .disableAnrDetection() + .setHttpSenderCustomizer(customizer); + + when(application.getApplicationContext()).thenReturn(context); + + RumInitializer testInitializer = + new RumInitializer(splunkRumBuilder, application, new AppStartupTimer()); + SplunkRum rum = testInitializer.initialize(mainLooper); + rum.addRumEvent("foo", Attributes.empty()); // need to trigger export + rum.flushSpans(); + + assertNotNull(rum); + assertNotNull(seenBuilder.get()); + } + @Test void shouldTranslateExceptionEventsToSpanAttributes() { InMemorySpanExporter spanExporter = InMemorySpanExporter.create();