Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding disk buffering, part 2 #160

Merged
merged 9 commits into from
Dec 12, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@
import static java.util.Objects.requireNonNull;

import android.app.Application;
import android.util.Log;
import io.opentelemetry.android.config.DiskBufferingConfiguration;
import io.opentelemetry.android.config.OtelRumConfig;
import io.opentelemetry.android.instrumentation.InstrumentedApplication;
import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker;
import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider;
import io.opentelemetry.android.instrumentation.network.NetworkAttributesSpanAppender;
import io.opentelemetry.android.instrumentation.startup.InitializationEvents;
import io.opentelemetry.android.instrumentation.startup.SdkInitializationEvents;
import io.opentelemetry.android.internal.features.persistence.DiskManager;
import io.opentelemetry.android.internal.features.persistence.SimpleTemporaryFileProvider;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.contrib.disk.buffering.SpanDiskExporter;
import io.opentelemetry.contrib.disk.buffering.internal.StorageConfiguration;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
Expand All @@ -31,6 +37,7 @@
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -308,7 +315,32 @@ private SdkTracerProvider buildTracerProvider(SessionId sessionId, Application a
private SpanExporter buildSpanExporter() {
// TODO: Default to otlp...but how can we make endpoint and auth mandatory?
SpanExporter defaultExporter = LoggingSpanExporter.create();
return spanExporterCustomizer.apply(defaultExporter);
SpanExporter spanExporter = defaultExporter;
DiskBufferingConfiguration diskBufferingConfiguration =
config.getDiskBufferingConfiguration();
if (diskBufferingConfiguration.isEnabled()) {
try {
spanExporter = createDiskExporter(defaultExporter, diskBufferingConfiguration);
} catch (IOException e) {
Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Could not create span disk exporter.", e);
}
}
return spanExporterCustomizer.apply(spanExporter);
}

private static SpanExporter createDiskExporter(
SpanExporter defaultExporter, DiskBufferingConfiguration diskBufferingConfiguration)
throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it only IOEx or also securityexception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment it's only IOExceptions thrown in case the dirs cannot be created for whatever reason.

DiskManager diskManager = DiskManager.create(diskBufferingConfiguration);
StorageConfiguration storageConfiguration =
StorageConfiguration.builder()
.setMaxFileSize(diskManager.getMaxCacheFileSize())
.setMaxFolderSize(diskManager.getMaxFolderSize())
.setTemporaryFileProvider(
new SimpleTemporaryFileProvider(diskManager.getTemporaryDir()))
.build();
return SpanDiskExporter.create(
defaultExporter, diskManager.getSignalsBufferDir(), storageConfiguration);
}

private SdkMeterProvider buildMeterProvider(Application application) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,30 @@

package io.opentelemetry.android.internal.features.persistence;

import android.util.Log;
import io.opentelemetry.android.RumConstants;
import io.opentelemetry.android.config.DiskBufferingConfiguration;
import io.opentelemetry.android.config.OtelRumConfig;
import io.opentelemetry.android.internal.services.CacheStorageService;
import io.opentelemetry.android.internal.services.PreferencesService;
import io.opentelemetry.android.internal.services.ServiceManager;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* This class is internal and not for public use. Its APIs are unstable and can change at any time.
*/
public final class DiskManager {
private static final String MAX_FOLDER_SIZE_KEY = "max_signal_folder_size";
private static final Logger logger = Logger.getLogger("DiskManager");
private final CacheStorageService cacheStorageService;
private final PreferencesService preferencesService;
private final DiskBufferingConfiguration diskBufferingConfiguration;

public static DiskManager create(OtelRumConfig config) {
public static DiskManager create(DiskBufferingConfiguration config) {
ServiceManager serviceManager = ServiceManager.get();
return new DiskManager(
serviceManager.getService(CacheStorageService.class),
serviceManager.getService(PreferencesService.class),
config.getDiskBufferingConfiguration());
config);
}

private DiskManager(
Expand Down Expand Up @@ -76,8 +74,8 @@ public File getTemporaryDir() throws IOException {
public int getMaxFolderSize() {
int storedSize = preferencesService.retrieveInt(MAX_FOLDER_SIZE_KEY, -1);
if (storedSize > 0) {
logger.log(
Level.FINER,
Log.d(
RumConstants.OTEL_RUM_LOG_TAG,
String.format("Returning max folder size from preferences: %s", storedSize));
return storedSize;
}
Expand All @@ -89,17 +87,17 @@ public int getMaxFolderSize() {
int maxCacheFileSize = getMaxCacheFileSize();
int calculatedSize = (availableCacheSize / 3) - maxCacheFileSize;
if (calculatedSize < maxCacheFileSize) {
logger.log(
Level.WARNING,
Log.w(
RumConstants.OTEL_RUM_LOG_TAG,
String.format(
"Insufficient folder cache size: %s, it must be at least: %s",
calculatedSize, maxCacheFileSize));
return 0;
}
preferencesService.store(MAX_FOLDER_SIZE_KEY, calculatedSize);

logger.log(
Level.FINER,
Log.d(
RumConstants.OTEL_RUM_LOG_TAG,
String.format(
"Requested cache size: %s, available cache size: %s, folder size: %s",
requestedSize, availableCacheSize, calculatedSize));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.internal.features.persistence;

import io.opentelemetry.contrib.disk.buffering.internal.files.TemporaryFileProvider;
import java.io.File;

/**
* This class is internal and not for public use. Its APIs are unstable and can change at any time.
*/
public final class SimpleTemporaryFileProvider implements TemporaryFileProvider {
private final File tempDir;

public SimpleTemporaryFileProvider(File tempDir) {
this.tempDir = tempDir;
}

/** Creates a unique file instance using the provided prefix and the current time in millis. */
@Override
public File createTemporaryFile(String prefix) {
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
return new File(tempDir, prefix + "_" + System.currentTimeMillis() + ".tmp");
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
LikeTheSalad marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import android.content.Context;
import android.os.Build;
import android.os.storage.StorageManager;
import android.util.Log;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import io.opentelemetry.android.RumConstants;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Utility to get information about the host app.
Expand All @@ -24,7 +24,6 @@
*/
public class CacheStorageService implements Service {
private final Context appContext;
private static final Logger logger = Logger.getLogger("CacheStorageService");

public CacheStorageService(Context appContext) {
this.appContext = appContext;
Expand Down Expand Up @@ -53,8 +52,8 @@ public long ensureCacheSpaceAvailable(long maxSpaceNeeded) {

@RequiresApi(api = Build.VERSION_CODES.O)
private long getAvailableSpace(File directory, long maxSpaceNeeded) {
logger.log(
Level.FINER,
Log.d(
RumConstants.OTEL_RUM_LOG_TAG,
String.format(
"Getting available space for %s, max needed is: %s",
directory, maxSpaceNeeded));
Expand All @@ -70,14 +69,14 @@ private long getAvailableSpace(File directory, long maxSpaceNeeded) {
storageManager.allocateBytes(appSpecificInternalDirUuid, spaceToAllocate);
return spaceToAllocate;
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to get available space", e);
Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Failed to get available space", e);
return getLegacyAvailableSpace(directory, maxSpaceNeeded);
}
}

private long getLegacyAvailableSpace(File directory, long maxSpaceNeeded) {
logger.log(
Level.FINER,
Log.d(
RumConstants.OTEL_RUM_LOG_TAG,
String.format(
"Getting legacy available space for %s max needed is: %s",
directory, maxSpaceNeeded));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,39 @@
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.app.Application;
import androidx.annotation.NonNull;
import io.opentelemetry.android.config.DiskBufferingConfiguration;
import io.opentelemetry.android.config.OtelRumConfig;
import io.opentelemetry.android.instrumentation.ApplicationStateListener;
import io.opentelemetry.android.internal.services.CacheStorageService;
import io.opentelemetry.android.internal.services.PreferencesService;
import io.opentelemetry.android.internal.services.Service;
import io.opentelemetry.android.internal.services.ServiceManager;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.contrib.disk.buffering.SpanDiskExporter;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand Down Expand Up @@ -158,6 +169,79 @@ void setSpanExporterCustomizer() {
.untilAsserted(() -> verify(exporter).export(anyCollection()));
}

@Test
void diskBufferingEnabled() {
PreferencesService preferences = mock();
CacheStorageService cacheStorage = mock();
doReturn(60 * 1024 * 1024L).when(cacheStorage).ensureCacheSpaceAvailable(anyLong());
setUpServiceManager(preferences, cacheStorage);
OtelRumConfig config = buildConfig();
config.setDiskBufferingConfiguration(
DiskBufferingConfiguration.builder().setEnabled(true).build());
AtomicReference<SpanExporter> capturedExporter = new AtomicReference<>();

OpenTelemetryRum.builder(application, config)
.addSpanExporterCustomizer(
spanExporter -> {
capturedExporter.set(spanExporter);
return spanExporter;
})
.build();

assertThat(capturedExporter.get()).isInstanceOf(SpanDiskExporter.class);
}

@Test
void diskBufferingEnabled_when_exception_thrown() {
PreferencesService preferences = mock();
CacheStorageService cacheStorage = mock();
doReturn(60 * 1024 * 1024L).when(cacheStorage).ensureCacheSpaceAvailable(anyLong());
doAnswer(
invocation -> {
throw new IOException();
})
.when(cacheStorage)
.getCacheDir();
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
setUpServiceManager(preferences, cacheStorage);
OtelRumConfig config = buildConfig();
config.setDiskBufferingConfiguration(
DiskBufferingConfiguration.builder().setEnabled(true).build());
AtomicReference<SpanExporter> capturedExporter = new AtomicReference<>();

OpenTelemetryRum.builder(application, config)
.addSpanExporterCustomizer(
spanExporter -> {
capturedExporter.set(spanExporter);
return spanExporter;
})
.build();

assertThat(capturedExporter.get()).isNotInstanceOf(SpanDiskExporter.class);
}

@Test
void diskBufferingDisabled() {
AtomicReference<SpanExporter> capturedExporter = new AtomicReference<>();

makeBuilder()
.addSpanExporterCustomizer(
spanExporter -> {
capturedExporter.set(spanExporter);
return spanExporter;
})
.build();

assertThat(capturedExporter.get()).isNotInstanceOf(SpanDiskExporter.class);
}

private static void setUpServiceManager(Service... services) {
ServiceManager serviceManager = mock();
for (Service service : services) {
doReturn(service).when(serviceManager).getService(service.getClass());
}
ServiceManager.setForTest(serviceManager);
}

@NonNull
private OpenTelemetryRumBuilder makeBuilder() {
return OpenTelemetryRum.builder(application, buildConfig());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;

import io.opentelemetry.android.config.DiskBufferingConfiguration;
import io.opentelemetry.android.config.OtelRumConfig;
import io.opentelemetry.android.internal.services.CacheStorageService;
import io.opentelemetry.android.internal.services.PreferencesService;
import io.opentelemetry.android.internal.services.ServiceManager;
Expand All @@ -41,13 +40,11 @@ class DiskManagerTest {

@BeforeEach
void setUp() {
OtelRumConfig config = mock();
ServiceManager serviceManager = mock();
doReturn(diskBufferingConfiguration).when(config).getDiskBufferingConfiguration();
doReturn(cacheStorageService).when(serviceManager).getService(CacheStorageService.class);
doReturn(preferencesService).when(serviceManager).getService(PreferencesService.class);
ServiceManager.setForTest(serviceManager);
diskManager = DiskManager.create(config);
diskManager = DiskManager.create(diskBufferingConfiguration);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.internal.features.persistence;

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

import java.io.File;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class SimpleTemporaryFileProviderTest {
@TempDir File tempDir;

@Test
void createUniqueFilesBasedOnCurrentTimeAndPrefix() throws InterruptedException {
SimpleTemporaryFileProvider provider = new SimpleTemporaryFileProvider(tempDir);
File first = provider.createTemporaryFile("a");
File second = provider.createTemporaryFile("b");
Thread.sleep(1);
File third = provider.createTemporaryFile("a");

assertThat(first.getName()).startsWith("a").endsWith(".tmp");
assertThat(second.getName()).startsWith("b").endsWith(".tmp");
assertThat(third.getName()).startsWith("a").endsWith(".tmp");
assertThat(first).isNotEqualTo(third);
LikeTheSalad marked this conversation as resolved.
Show resolved Hide resolved
}
}