diff --git a/contract-tests/build.gradle b/contract-tests/build.gradle index c377f0d5..b602d867 100644 --- a/contract-tests/build.gradle +++ b/contract-tests/build.gradle @@ -25,7 +25,6 @@ android { } dependencies { - implementation("com.jakewharton.timber:timber:5.0.1") // https://mvnrepository.com/artifact/org.nanohttpd/nanohttpd implementation("org.nanohttpd:nanohttpd:2.3.1") implementation("com.google.code.gson:gson:2.8.9") diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/MainActivity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/MainActivity.java index 3e2573f2..a89e8d55 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/MainActivity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/MainActivity.java @@ -4,15 +4,15 @@ import android.os.Bundle; import android.widget.TextView; -import java.io.IOException; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.LDAndroidLogging; -import timber.log.Timber; +import java.io.IOException; public class MainActivity extends Activity { - private Config config; private TestService server; - private Timber.DebugTree debugTree; + private LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), "MainActivity"); @Override protected void onCreate(Bundle savedInstanceState) { @@ -22,11 +22,6 @@ protected void onCreate(Bundle savedInstanceState) { TextView textIpaddr = findViewById(R.id.ipaddr); textIpaddr.setText("Contract test service running on port " + config.port); - - if (Timber.treeCount() == 0) { - debugTree = new Timber.DebugTree(); - Timber.plant(debugTree); - } } @Override @@ -40,13 +35,13 @@ public void onStop() { @Override protected void onResume() { super.onResume(); - Timber.w("Restarting test service on port " + config.port); + logger.warn("Restarting test service on port {}", config.port); server = new TestService(getApplication()); if (!server.isAlive()) { try { server.start(); } catch (IOException e) { - Timber.e(e, "Error starting server"); + logger.error("Error starting server: {}", e); } } } diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/PrefixedLogAdapter.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/PrefixedLogAdapter.java new file mode 100644 index 00000000..a9490380 --- /dev/null +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/PrefixedLogAdapter.java @@ -0,0 +1,60 @@ +package com.launchdarkly.sdktest; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; + +/** + * This wraps the underlying logging adapter to use the logger name as a prefix for the message + * text, rather than passing it through as a tag, because Android logging tags have a length + * limit so we can't put the whole test name there. + */ +public class PrefixedLogAdapter implements LDLogAdapter { + private final LDLogAdapter wrappedAdapter; + private final String singleLoggerName; + + public PrefixedLogAdapter(LDLogAdapter wrappedAdapter, String singleLoggerName) { + this.wrappedAdapter = wrappedAdapter; + this.singleLoggerName = singleLoggerName; + } + + @Override + public Channel newChannel(String name) { + return new ChannelImpl(wrappedAdapter.newChannel(singleLoggerName), + "[" + name + "] "); + } + + private static final class ChannelImpl implements Channel { + private final Channel wrappedChannel; + private final String prefix; + + public ChannelImpl(Channel wrappedChannel, String prefix) { + this.wrappedChannel = wrappedChannel; + this.prefix = prefix; + } + + @Override + public boolean isEnabled(LDLogLevel level) { + return wrappedChannel.isEnabled(level); + } + + @Override + public void log(LDLogLevel level, Object message) { + wrappedChannel.log(level, prefix + message.toString()); + } + + @Override + public void log(LDLogLevel level, String format, Object param) { + wrappedChannel.log(level, prefix + format, param); + } + + @Override + public void log(LDLogLevel level, String format, Object param1, Object param2) { + wrappedChannel.log(level, prefix + format, param1, param2); + } + + @Override + public void log(LDLogLevel level, String format, Object... params) { + wrappedChannel.log(level, prefix + format, params); + } + } +} diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index 5fcf0d59..f375fdd0 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -1,5 +1,7 @@ package com.launchdarkly.sdktest; +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; @@ -30,8 +32,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import timber.log.Timber; - /** * This class implements all the client-level testing protocols defined in * the contract tests service specification, such as executing commands, @@ -41,13 +41,13 @@ */ public class SdkClientEntity { private final LDClient client; + private final LDLogger logger; - public SdkClientEntity(Application application, CreateInstanceParams params) { - Timber.i("Creating client for %s", params.tag); - LDConfig config = buildSdkConfig(params.configuration); - // Each new client will plant a new Timber tree, so we uproot any existing ones - // to avoid spamming stdout with duplicate log lines - Timber.uprootAll(); + public SdkClientEntity(Application application, CreateInstanceParams params, LDLogAdapter logAdapter) { + LDLogAdapter logAdapterForTest = new PrefixedLogAdapter(logAdapter, "test"); + this.logger = LDLogger.withAdapter(logAdapterForTest, params.tag); + logger.info("Creating client"); + LDConfig config = buildSdkConfig(params.configuration, logAdapterForTest, params.tag); long startWaitMs = params.configuration.startWaitTimeMs != null ? params.configuration.startWaitTimeMs.longValue() : 5000; Future initFuture = LDClient.init( @@ -59,9 +59,9 @@ public SdkClientEntity(Application application, CreateInstanceParams params) { try { initFuture.get(startWaitMs, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { - Timber.e(e, "Exception during Client initialization"); + logger.error("Exception during Client initialization: {}", e); } catch (TimeoutException e) { - Timber.w("Client did not successfully initialize within %s ms. It could be taking longer than expected to start up", startWaitMs); + logger.warn("Client did not successfully initialize within {} ms. It could be taking longer than expected to start up", startWaitMs); } try { this.client = LDClient.get(); @@ -73,13 +73,13 @@ public SdkClientEntity(Application application, CreateInstanceParams params) { throw new RuntimeException("client initialization failed or timed out"); } } catch (LaunchDarklyException e) { - Timber.e(e, "Exception when initializing LDClient"); + logger.error("Exception when initializing LDClient: {}", e); throw new RuntimeException("Exception when initializing LDClient", e); } } public Object doCommand(CommandParams params) throws TestService.BadRequestException { - Timber.i("Test harness sent command: %s", TestService.gson.toJson(params)); + logger.info("Test harness sent command: {}", TestService.gson.toJson(params)); switch (params.command) { case "evaluate": return doEvaluateFlag(params.evaluate); @@ -192,9 +192,10 @@ private void doAliasEvent(AliasEventParams params) { client.alias(params.user, params.previousUser); } - private LDConfig buildSdkConfig(SdkConfigParams params) { + private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, String tag) { LDConfig.Builder builder = new LDConfig.Builder(); builder.mobileKey(params.credential); + builder.logAdapter(logAdapter).loggerName(tag + ".sdk"); if (params.streaming != null) { builder.stream(true); @@ -257,9 +258,9 @@ private LDConfig buildSdkConfig(SdkConfigParams params) { public void close() { try { client.close(); - Timber.i("Closed LDClient"); + logger.info("Closed LDClient"); } catch (IOException e) { - Timber.e(e, "Unexpected error closing client"); + logger.error("Unexpected error closing client: {}", e); throw new RuntimeException("Unexpected error closing client", e); } } diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java index 7b12a1b2..00cdae1c 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java @@ -5,10 +5,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdktest.Representations.CommandParams; import com.launchdarkly.sdktest.Representations.CreateInstanceParams; import com.launchdarkly.sdktest.Representations.Status; import com.launchdarkly.sdk.android.BuildConfig; +import com.launchdarkly.sdk.android.LDAndroidLogging; import com.launchdarkly.sdk.json.LDGson; import java.io.IOException; @@ -20,8 +23,6 @@ import fi.iki.elonen.NanoHTTPD; -import timber.log.Timber; - public class TestService extends NanoHTTPD { private static final int PORT = 8001; private static final String[] CAPABILITIES = new String[]{ @@ -37,6 +38,8 @@ public class TestService extends NanoHTTPD { private final Router router = new Router(); private final Application application; + private final LDLogAdapter logAdapter; + private final LDLogger logger; private final Map clients = new ConcurrentHashMap(); private final AtomicInteger clientCounter = new AtomicInteger(0); @@ -50,6 +53,8 @@ public BadRequestException(String message) { TestService(Application application) { super(PORT); this.application = application; + this.logAdapter = LDAndroidLogging.adapter(); + this.logger = LDLogger.withAdapter(logAdapter, "service"); router.add("GET", "/", (params, body) -> getStatus()); router.add("POST", "/", (params, body) -> postCreateClient(body)); router.addRegex("POST", Pattern.compile("/clients/(.*)"), (params, body) -> postClientCommand(params, body)); @@ -72,13 +77,13 @@ public Response serve(IHTTPSession session) { body = new String(buffer); } - Timber.i("Handling request: %s %s", method.name(), session.getUri()); + logger.info("Handling request: {} {}", method.name(), session.getUri()); try { return router.route(method.name(), session.getUri(), body); } catch (JsonSyntaxException jse) { return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Invalid JSON Format\n"); } catch (Exception e) { - Timber.e(e, "Exception when handling request: %s %s", method.name(), session.getUri()); + logger.error("Exception when handling request: {} {} - {}", method.name(), session.getUri(), e); return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, e.toString()); } } @@ -97,7 +102,7 @@ private Response postCreateClient(String jsonPayload) { CreateInstanceParams params = gson.fromJson(jsonPayload, CreateInstanceParams.class); String clientId = String.valueOf(clientCounter.incrementAndGet()); - SdkClientEntity client = new SdkClientEntity(application, params); + SdkClientEntity client = new SdkClientEntity(application, params, logAdapter); clients.put(clientId, client); diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index bc652b9c..6c0c2385 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -61,12 +61,14 @@ ext.versions = [ "jacksonDatabind": "2.10.5.1", "junit": "4.13", "launchdarklyJavaSdkCommon": "1.2.0", + "launchdarklyLogging": "1.1.1", "okhttp": "4.9.2", "timber": "5.0.1", ] dependencies { api("com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}") + api("com.launchdarkly:launchdarkly-logging:${versions.launchdarklyLogging}") commonDoc("com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}:sources") // These are included only for Javadoc generation. @@ -137,6 +139,16 @@ if (JavaVersion.current().isJava8Compatible()) { } } +tasks.withType(Javadoc) { + // The following should allow hyperlinks to com.launchdarkly.logging classes to go to + // the correct external URLs + if (options instanceof StandardJavadocDocletOptions) { + (options as StandardJavadocDocletOptions).links( + "https://javadoc.io/doc/com.launchdarkly/launchdarkly-logging/${versions.launchdarklyLogging}" + ) + } +} + artifacts { archives sourcesJar, javadocJar } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index c933a8ac..23973cca 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -15,6 +15,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode; import com.launchdarkly.sdk.LDUser; @@ -131,7 +132,8 @@ private void createTestManager(boolean setOffline, boolean streaming, boolean ba .streamUri(streamUri != null ? Uri.parse(streamUri) : Uri.parse(mockStreamServer.url("/").toString())) .build(); - connectivityManager = new ConnectivityManager(app, config, eventProcessor, userManager, "default", null, null); + connectivityManager = new ConnectivityManager(app, config, eventProcessor, userManager, "default", + null, null, LDLogger.none()); } private void awaitStartUp() throws ExecutionException { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java index 80bf0bd2..a02ec4b9 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.gson.JsonObject; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDUser; import org.easymock.Capture; @@ -47,7 +48,8 @@ public class DefaultUserManagerTest extends EasyMockSupport { @Before public void before() { - userManager = new DefaultUserManager(ApplicationProvider.getApplicationContext(), fetcher, "test", "test", 3); + userManager = new DefaultUserManager(ApplicationProvider.getApplicationContext(), fetcher, + "test", "test", 3, LDLogger.none()); } @Test diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java index ac495245..403dbd28 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java @@ -19,6 +19,8 @@ import static junit.framework.Assert.assertEquals; +import com.launchdarkly.logging.LDLogger; + @RunWith(AndroidJUnit4.class) public class DiagnosticEventProcessorTest { @@ -47,7 +49,8 @@ public void defaultDiagnosticRequest() throws InterruptedException { .eventsUri(Uri.parse(mockEventsServer.url("").toString())) .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, + ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -74,7 +77,8 @@ public void defaultDiagnosticRequestIncludingWrapper() throws InterruptedExcepti .wrapperVersion("1.0.0") .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, + ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -102,7 +106,8 @@ public void defaultDiagnosticRequestIncludingAdditionalHeaders() throws Interrup }) .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, + ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -123,7 +128,8 @@ public void closeWithoutStart() { LDConfig ldConfig = new LDConfig.Builder().mobileKey("test-mobile-key").build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, + ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); diagnosticEventProcessor.close(); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java new file mode 100644 index 00000000..05466490 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java @@ -0,0 +1,55 @@ +package com.launchdarkly.sdk.android; + +import android.app.Application; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.launchdarkly.logging.LDLogLevel; +import com.launchdarkly.logging.LogCapture; +import com.launchdarkly.logging.Logs; +import com.launchdarkly.sdk.LDUser; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertNotEquals; + +@RunWith(AndroidJUnit4.class) +public class LDClientLoggingTest { + + private static final String mobileKey = "test-mobile-key"; + private Application application; + private LDUser ldUser; + + @Before + public void setUp() { + application = ApplicationProvider.getApplicationContext(); + ldUser = new LDUser("key"); + } + + @Test + public void customLogAdapterWithDefaultLevel() throws Exception { + LogCapture logCapture = Logs.capture(); + LDConfig config = new LDConfig.Builder().offline(true).logAdapter(logCapture).build(); + try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { + for (LogCapture.Message m: logCapture.getMessages()) { + assertNotEquals(LDLogLevel.DEBUG, m.getLevel()); + } + LogCapture.Message m1 = logCapture.requireMessage(LDLogLevel.INFO, 2000); + assertThat(m1.getText(), containsString("Initializing Client")); + } + } + + @Test + public void customLogAdapterWithDebugLevel() throws Exception { + LogCapture logCapture = Logs.capture(); + LDConfig config = new LDConfig.Builder().offline(true).logAdapter(logCapture).logLevel(LDLogLevel.DEBUG).build(); + try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { + logCapture.requireMessage(LDLogLevel.DEBUG, 2000); + } + } +} diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java index 7457f2f5..818a8efc 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java @@ -6,6 +6,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; @@ -601,7 +602,7 @@ public void variationFlagTrackReasonGeneratesEventWithReason() throws IOExceptio // Setup flag store with test flag TestUtil.markMigrationComplete(application); EvaluationReason testReason = EvaluationReason.off(); - FlagStore flagStore = new SharedPrefsFlagStoreFactory(application).createFlagStore(mobileKey + DefaultUserManager.sharedPrefs(ldUser)); + FlagStore flagStore = new SharedPrefsFlagStoreFactory(application, LDLogger.none()).createFlagStore(mobileKey + DefaultUserManager.sharedPrefs(ldUser)); flagStore.applyFlagUpdate(new FlagBuilder("track-reason-flag").version(10).trackEvents(true).trackReason(true).reason(testReason).build()); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactoryTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactoryTest.java index c6c65f28..a22cb1ee 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactoryTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactoryTest.java @@ -11,6 +11,8 @@ import static org.junit.Assert.assertTrue; +import com.launchdarkly.logging.LDLogger; + @RunWith(AndroidJUnit4.class) public class SharedPrefsFlagStoreFactoryTest { @@ -20,7 +22,7 @@ public class SharedPrefsFlagStoreFactoryTest { @Test public void createsSharedPrefsFlagStore() { Application application = ApplicationProvider.getApplicationContext(); - SharedPrefsFlagStoreFactory factory = new SharedPrefsFlagStoreFactory(application); + SharedPrefsFlagStoreFactory factory = new SharedPrefsFlagStoreFactory(application, LDLogger.none()); FlagStore flagStore = factory.createFlagStore("flagstore_factory_test"); assertTrue(flagStore instanceof SharedPrefsFlagStore); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManagerTest.java index 4422b6f7..73df86e9 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManagerTest.java @@ -5,6 +5,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.launchdarkly.logging.LDLogger; + import org.junit.Before; import org.junit.Rule; import org.junit.runner.RunWith; @@ -23,7 +25,7 @@ public void setUp() { } public FlagStoreManager createFlagStoreManager(String mobileKey, FlagStoreFactory flagStoreFactory, int maxCachedUsers) { - return new SharedPrefsFlagStoreManager(testApplication, mobileKey, flagStoreFactory, maxCachedUsers); + return new SharedPrefsFlagStoreManager(testApplication, mobileKey, flagStoreFactory, maxCachedUsers, LDLogger.none()); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java index 9fcab3cc..383ff32b 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.launchdarkly.logging.LDLogger; + @RunWith(AndroidJUnit4.class) public class SharedPrefsFlagStoreTest extends FlagStoreTest { @@ -33,14 +35,14 @@ public void setUp() { } public FlagStore createFlagStore(String identifier) { - return new SharedPrefsFlagStore(testApplication, identifier); + return new SharedPrefsFlagStore(testApplication, identifier, LDLogger.none()); } @Test public void deletesVersionAndStoresDeletedItemPlaceholder() { final Flag key1 = new FlagBuilder("key1").version(12).build(); - final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); + final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc", LDLogger.none()); flagStore.applyFlagUpdates(Collections.singletonList(key1)); flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 13)); @@ -55,7 +57,7 @@ public void deletesVersionAndStoresDeletedItemPlaceholder() { public void doesNotDeleteIfDeletionVersionIsLessThanOrEqualToExistingVersion() { final Flag key1 = new FlagBuilder("key1").version(12).build(); - final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); + final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc", LDLogger.none()); flagStore.applyFlagUpdates(Collections.singletonList(key1)); flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 11)); flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 12)); @@ -72,7 +74,7 @@ public void updatesVersions() { final Flag key1 = new FlagBuilder("key1").version(12).build(); final Flag updatedKey1 = new FlagBuilder(key1.getKey()).version(15).build(); - final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); + final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc", LDLogger.none()); flagStore.applyFlagUpdates(Collections.singletonList(key1)); flagStore.applyFlagUpdate(updatedKey1); @@ -85,7 +87,7 @@ public void updatesFlagVersions() { final Flag key1 = new FlagBuilder("key1").version(100).flagVersion(12).build(); final Flag updatedKey1 = new FlagBuilder(key1.getKey()).version(101).flagVersion(15).build(); - final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); + final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc", LDLogger.none()); flagStore.applyFlagUpdates(Collections.singletonList(key1)); flagStore.applyFlagUpdate(updatedKey1); @@ -99,7 +101,7 @@ public void versionForEventsReturnsFlagVersionIfPresentOtherwiseReturnsVersion() new FlagBuilder("withFlagVersion").version(12).flagVersion(13).build(); final Flag withOnlyVersion = new FlagBuilder("withOnlyVersion").version(12).build(); - final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); + final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc", LDLogger.none()); flagStore.applyFlagUpdates(Arrays.asList(withFlagVersion, withOnlyVersion)); assertEquals(flagStore.getFlag(withFlagVersion.getKey()).getVersionForEvents(), 13, 0); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TimberLoggingTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TimberLoggingTest.java deleted file mode 100644 index 9af875a0..00000000 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TimberLoggingTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.launchdarkly.sdk.android; - -import static org.junit.Assert.assertEquals; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -import timber.log.Timber; - -@RunWith(AndroidJUnit4.class) -public class TimberLoggingTest { - - @Rule - public TimberLoggingRule timberLoggingRule = new TimberLoggingRule(); - - private TestTree testTree; - - @Before - public void setUp() throws Exception { - testTree = new TestTree(); - Timber.plant(testTree); - } - - @After - public void tearDown() throws Exception { - testTree = null; - } - - @Test - public void timberTagIsLaunchDarklySdkForAllEvents() { - LDConfig.log().d("event"); - LDConfig.log().d("event"); - - assertEquals(List.of("LaunchDarklySdk", "LaunchDarklySdk"), testTree.loggedTags); - } - - private static class TestTree extends Timber.Tree { - - final List loggedTags = new ArrayList(); - - @Override - protected void log(int priority, @Nullable String tag, @NonNull String message, @Nullable Throwable t) { - loggedTags.add(tag); - } - } -} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java index a10ed48f..3e75c1b3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java @@ -6,6 +6,9 @@ import androidx.annotation.NonNull; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; + import java.util.Calendar; import java.util.TimeZone; @@ -32,6 +35,7 @@ class ConnectivityManager { private final String environmentName; private final int pollingInterval; private final LDUtil.ResultCallback monitor; + private final LDLogger logger; private LDUtil.ResultCallback initCallback = null; private volatile boolean initialized = false; private volatile boolean setOffline; @@ -42,12 +46,14 @@ class ConnectivityManager { @NonNull final UserManager userManager, @NonNull final String environmentName, final DiagnosticEventProcessor diagnosticEventProcessor, - final DiagnosticStore diagnosticStore) { + final DiagnosticStore diagnosticStore, + final LDLogger logger) { this.application = application; this.eventProcessor = eventProcessor; this.diagnosticEventProcessor = diagnosticEventProcessor; this.userManager = userManager; this.environmentName = environmentName; + this.logger = logger; pollingInterval = ldConfig.getPollingIntervalMillis(); String prefsKey = LDConfig.SHARED_PREFS_BASE_KEY + ldConfig.getMobileKeys().get(environmentName) + "-connectionstatus"; stateStore = application.getSharedPreferences(prefsKey, Context.MODE_PRIVATE); @@ -112,14 +118,15 @@ public void onError(Throwable e) { LDClient ldClient = LDClient.getForMobileKey(environmentName); ldClient.updateListenersOnFailure(connectionInformation.getLastFailure()); } catch (LaunchDarklyException ex) { - LDConfig.log().e(e, "Error getting LDClient for ConnectivityManager"); + LDUtil.logExceptionAtErrorLevel(logger, e, "Error getting LDClient for ConnectivityManager"); } callInitCallback(); } } }; - streamUpdateProcessor = ldConfig.isStream() ? new StreamUpdateProcessor(ldConfig, userManager, environmentName, diagnosticStore, monitor) : null; + streamUpdateProcessor = ldConfig.isStream() ? new StreamUpdateProcessor(ldConfig, userManager, environmentName, + diagnosticStore, monitor, logger) : null; } boolean isInitialized() { @@ -374,13 +381,13 @@ private synchronized void updateConnectionMode(ConnectionMode connectionMode) { try { saveConnectionInformation(); } catch (Exception ex) { - LDConfig.log().w(ex, "Error saving connection information"); + LDUtil.logExceptionAtErrorLevel(logger, ex, "Error saving connection information"); } try { LDClient ldClient = LDClient.getForMobileKey(environmentName); ldClient.updateListenersConnectionModeChanged(connectionInformation); } catch (LaunchDarklyException e) { - LDConfig.log().e(e, "Error getting LDClient for ConnectivityManager"); + LDUtil.logExceptionAtErrorLevel(logger, e, "Error getting LDClient for ConnectivityManager: {}"); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java index d4ca4160..9940f55e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java @@ -32,6 +32,9 @@ import static com.launchdarkly.sdk.android.LDUtil.isClientConnected; import static com.launchdarkly.sdk.android.LDUtil.isHttpErrorRecoverable; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; + class DefaultEventProcessor implements EventProcessor, Closeable { private static final HashMap baseEventHeaders = new HashMap() {{ put("Content-Type", "application/json"); @@ -48,9 +51,10 @@ class DefaultEventProcessor implements EventProcessor, Closeable { private final SummaryEventStore summaryEventStore; private long currentTimeMs = System.currentTimeMillis(); private DiagnosticStore diagnosticStore; + private final LDLogger logger; DefaultEventProcessor(Context context, LDConfig config, SummaryEventStore summaryEventStore, String environmentName, - final DiagnosticStore diagnosticStore, final OkHttpClient sharedClient) { + final DiagnosticStore diagnosticStore, final OkHttpClient sharedClient, LDLogger logger) { this.context = context; this.config = config; this.environmentName = environmentName; @@ -59,6 +63,7 @@ class DefaultEventProcessor implements EventProcessor, Closeable { this.summaryEventStore = summaryEventStore; this.client = sharedClient; this.diagnosticStore = diagnosticStore; + this.logger = logger; } public void start() { @@ -151,12 +156,12 @@ private void postEvents(List events) { baseHeadersForRequest.put("X-LaunchDarkly-Payload-ID", eventPayloadId); baseHeadersForRequest.putAll(baseEventHeaders); - LDConfig.log().d("Posting %s event(s) to %s", events.size(), url); - LDConfig.log().d("Events body: %s", content); + logger.debug("Posting {} event(s) to {}", events.size(), url); + logger.debug("Events body: {}", content); for (int attempt = 0; attempt < 2; attempt++) { if (attempt > 0) { - LDConfig.log().w("Will retry posting events after 1 second"); + logger.warn("Will retry posting events after 1 second"); try { Thread.sleep(1000); } catch (InterruptedException e) {} @@ -168,11 +173,11 @@ private void postEvents(List events) { .build(); try (Response response = client.newCall(request).execute()) { - LDConfig.log().d("Events Response: %s", response.code()); - LDConfig.log().d("Events Response Date: %s", response.header("Date")); + logger.debug("Events Response: {}", response.code()); + logger.debug("Events Response Date: {}", response.header("Date")); if (!response.isSuccessful()) { - LDConfig.log().w("Unexpected response status when posting events: %d", response.code()); + logger.warn("Unexpected response status when posting events: {}", response.code()); if (isHttpErrorRecoverable(response.code())) { continue; } @@ -181,7 +186,9 @@ private void postEvents(List events) { tryUpdateDate(response); break; } catch (IOException e) { - LDConfig.log().e(e, "Unhandled exception in LaunchDarkly client attempting to connect to URI: %s", request.url()); + LDUtil.logExceptionAtErrorLevel(logger, e, + "Unhandled exception in LaunchDarkly client attempting to connect to URI: {}", + request.url()); } } } @@ -194,7 +201,7 @@ private void tryUpdateDate(Response response) { Date date = sdf.parse(dateString); currentTimeMs = date.getTime(); } catch (ParseException pe) { - LDConfig.log().e(pe, "Failed to parse date header"); + LDUtil.logExceptionAtErrorLevel(logger, pe, "Failed to parse date header"); } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java index 9cfe56d0..7b406c18 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java @@ -8,6 +8,8 @@ import androidx.annotation.VisibleForTesting; import com.google.gson.JsonObject; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; import java.util.Collection; @@ -27,21 +29,27 @@ class DefaultUserManager implements UserManager { private final FlagStoreManager flagStoreManager; private final SummaryEventStore summaryEventStore; private final String environmentName; + private final LDLogger logger; private LDUser currentUser; private final ExecutorService executor; - static synchronized DefaultUserManager newInstance(Application application, FeatureFetcher fetcher, String environmentName, String mobileKey, int maxCachedUsers) { - return new DefaultUserManager(application, fetcher, environmentName, mobileKey, maxCachedUsers); + static synchronized DefaultUserManager newInstance(Application application, FeatureFetcher fetcher, String environmentName, + String mobileKey, int maxCachedUsers, LDLogger logger) { + return new DefaultUserManager(application, fetcher, environmentName, mobileKey, maxCachedUsers, logger); } - DefaultUserManager(Application application, FeatureFetcher fetcher, String environmentName, String mobileKey, int maxCachedUsers) { + DefaultUserManager(Application application, FeatureFetcher fetcher, String environmentName, String mobileKey, + int maxCachedUsers, LDLogger logger) { this.application = application; this.fetcher = fetcher; - this.flagStoreManager = new SharedPrefsFlagStoreManager(application, mobileKey, new SharedPrefsFlagStoreFactory(application), maxCachedUsers); - this.summaryEventStore = new SharedPrefsSummaryEventStore(application, LDConfig.SHARED_PREFS_BASE_KEY + mobileKey + "-summaryevents"); + this.flagStoreManager = new SharedPrefsFlagStoreManager(application, mobileKey, + new SharedPrefsFlagStoreFactory(application, logger), maxCachedUsers, logger); + this.summaryEventStore = new SharedPrefsSummaryEventStore(application, + LDConfig.SHARED_PREFS_BASE_KEY + mobileKey + "-summaryevents", logger); this.environmentName = environmentName; + this.logger = logger; executor = new BackgroundThreadExecutor().newFixedThreadPool(1); } @@ -80,7 +88,7 @@ public static String sharedPrefs(final LDUser user) { */ void setCurrentUser(final LDUser user) { String userBase64 = base64Url(user); - LDConfig.log().d("Setting current user to: [%s] [%s]", userBase64, userBase64ToJson(userBase64)); + logger.debug("Setting current user to: [{}] [{}]", userBase64, userBase64ToJson(userBase64)); currentUser = user; flagStoreManager.switchToUser(DefaultUserManager.sharedPrefs(user)); } @@ -98,9 +106,10 @@ public void onSuccess(JsonObject result) { @Override public void onError(Throwable e) { if (LDUtil.isClientConnected(application, environmentName)) { - LDConfig.log().e(e, "Error when attempting to set user: [%s] [%s]", + logger.error("Error when attempting to set user: [{}] [{}]: {}", base64Url(currentUser), - userBase64ToJson(base64Url(currentUser))); + userBase64ToJson(base64Url(currentUser)), + LogValues.exceptionSummary(e)); } onCompleteListener.onError(e); } @@ -133,14 +142,14 @@ void unregisterAllFlagsListener(@NonNull final LDAllFlagsListener listener) { */ @SuppressWarnings("JavaDoc") private void saveFlagSettings(JsonObject flagsJson, LDUtil.ResultCallback onCompleteListener) { - LDConfig.log().d("saveFlagSettings for user key: %s", currentUser.getKey()); + logger.debug("saveFlagSettings for user key: {}", currentUser.getKey()); try { final List flags = GsonCache.getGson().fromJson(flagsJson, FlagsResponse.class).getFlags(); flagStoreManager.getCurrentUserStore().clearAndApplyFlagUpdates(flags); onCompleteListener.onSuccess(null); } catch (Exception e) { - LDConfig.log().d("Invalid JsonObject for flagSettings: %s", flagsJson); + logger.debug("Invalid JsonObject for flagSettings: {}", flagsJson); onCompleteListener.onError(new LDFailure("Invalid Json received from flags endpoint", e, LDFailure.FailureType.INVALID_RESPONSE_BODY)); } } @@ -157,13 +166,13 @@ public void deleteCurrentUserFlag(@NonNull final String json, final LDUtil.Resul flagStoreManager.getCurrentUserStore().applyFlagUpdate(deleteFlagResponse); onCompleteListener.onSuccess(null); } else { - LDConfig.log().d("Invalid DELETE payload: %s", json); + logger.debug("Invalid DELETE payload: {}", json); onCompleteListener.onError(new LDFailure("Invalid DELETE payload", LDFailure.FailureType.INVALID_RESPONSE_BODY)); } }); } catch (Exception ex) { - LDConfig.log().d(ex, "Invalid DELETE payload: %s", json); + logger.debug("Invalid DELETE payload: {}", json); onCompleteListener.onError(new LDFailure("Invalid DELETE payload", ex, LDFailure.FailureType.INVALID_RESPONSE_BODY)); } @@ -173,12 +182,12 @@ public void putCurrentUserFlags(final String json, final LDUtil.ResultCallback flags = GsonCache.getGson().fromJson(json, FlagsResponse.class).getFlags(); executor.submit(() -> { - LDConfig.log().d("PUT for user key: %s", currentUser.getKey()); + logger.debug("PUT for user key: {}", currentUser.getKey()); flagStoreManager.getCurrentUserStore().clearAndApplyFlagUpdates(flags); onCompleteListener.onSuccess(null); }); } catch (Exception ex) { - LDConfig.log().d(ex, "Invalid PUT payload: %s", json); + logger.debug("Invalid PUT payload: {}", json); onCompleteListener.onError(new LDFailure("Invalid PUT payload", ex, LDFailure.FailureType.INVALID_RESPONSE_BODY)); } @@ -192,13 +201,13 @@ public void patchCurrentUserFlags(@NonNull final String json, final LDUtil.Resul flagStoreManager.getCurrentUserStore().applyFlagUpdate(flag); onCompleteListener.onSuccess(null); } else { - LDConfig.log().d("Invalid PATCH payload: %s", json); + logger.debug("Invalid PATCH payload: {}", json); onCompleteListener.onError(new LDFailure("Invalid PATCH payload", LDFailure.FailureType.INVALID_RESPONSE_BODY)); } }); } catch (Exception ex) { - LDConfig.log().d(ex, "Invalid PATCH payload: %s", json); + logger.debug("Invalid PATCH payload: {}", json); onCompleteListener.onError(new LDFailure("Invalid PATCH payload", ex, LDFailure.FailureType.INVALID_RESPONSE_BODY)); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java index 8c585cb1..e0ac19dc 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java @@ -20,6 +20,9 @@ import static com.launchdarkly.sdk.android.LDConfig.JSON; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; + class DiagnosticEventProcessor { private static final HashMap baseDiagnosticHeaders = new HashMap() {{ put("Content-Type", "application/json"); @@ -31,15 +34,17 @@ class DiagnosticEventProcessor { private final DiagnosticStore diagnosticStore; private final ThreadFactory diagnosticThreadFactory; private final Context context; + private final LDLogger logger; private ScheduledExecutorService executorService; DiagnosticEventProcessor(LDConfig config, String environment, final DiagnosticStore diagnosticStore, Context context, - OkHttpClient sharedClient) { + OkHttpClient sharedClient, LDLogger logger) { this.config = config; this.environment = environment; this.diagnosticStore = diagnosticStore; this.client = sharedClient; this.context = context; + this.logger = logger; diagnosticThreadFactory = new ThreadFactory() { final AtomicLong count = new AtomicLong(0); @@ -126,13 +131,14 @@ void sendDiagnosticEventSync(DiagnosticEvent diagnosticEvent) { .headers(config.headersForEnvironment(environment, baseDiagnosticHeaders)) .post(RequestBody.create(content, JSON)).build(); - LDConfig.log().d("Posting diagnostic event to %s with body %s", request.url(), content); + logger.debug("Posting diagnostic event to {} with body {}", request.url(), content); try (Response response = client.newCall(request).execute()) { - LDConfig.log().d("Diagnostic Event Response: %s", response.code()); - LDConfig.log().d("Diagnostic Event Response Date: %s", response.header("Date")); + logger.debug("Diagnostic Event Response: {}", response.code()); + logger.debug("Diagnostic Event Response Date: {}", response.header("Date")); } catch (IOException e) { - LDConfig.log().w(e, "Unhandled exception in LaunchDarkly client attempting to connect to URI: %s", request.url()); + logger.warn("Unhandled exception in LaunchDarkly client attempting to connect to URI \"{}\": {}", + request.url(), LogValues.exceptionSummary(e)); } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticStore.java index 438e7049..729a527e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticStore.java @@ -6,6 +6,7 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; +import com.launchdarkly.logging.LogValues; import java.util.ArrayList; import java.util.Arrays; @@ -81,7 +82,7 @@ private List getStreamInits() { DiagnosticEvent.StreamInit[] streamInitsArr = GsonCache.getGson().fromJson(streamInitsString, DiagnosticEvent.StreamInit[].class); streamInits = Arrays.asList(streamInitsArr); } catch (Exception ex) { - LDConfig.log().w(ex, "Invalid stream inits array in diagnostic data store"); + LDClient.getSharedLogger().warn("Invalid stream inits array in diagnostic data store: {}", LogValues.exceptionSummary(ex)); streamInits = null; } return streamInits; @@ -161,4 +162,4 @@ void recordEventsInLastBatch(long eventsInLastBatch) { .putLong(EVENT_BATCH_KEY, eventsInLastBatch) .apply(); } -} +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Foreground.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Foreground.java index 2e37d392..13b341a6 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Foreground.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Foreground.java @@ -14,6 +14,8 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; +import com.launchdarkly.logging.LogValues; + // From: https://gist.github.com/steveliles/11116937 /** @@ -148,17 +150,17 @@ public void onActivityResumed(Activity activity) { if (wasBackground) { handler.post(() -> { - LDConfig.log().d("went foreground"); + LDClient.getSharedLogger().debug("went foreground"); for (Listener l : listeners) { try { l.onBecameForeground(); } catch (Exception exc) { - LDConfig.log().e(exc, "Listener threw exception!"); + LDUtil.logExceptionAtErrorLevel(LDClient.getSharedLogger(), exc, "Listener threw exception"); } } }); } else { - LDConfig.log().d("still foreground"); + LDClient.getSharedLogger().debug("still foreground"); } } @@ -174,16 +176,16 @@ public void onActivityPaused(Activity activity) { handler.postDelayed(check = () -> { if (foreground && paused) { foreground = false; - LDConfig.log().d("went background"); + LDClient.getSharedLogger().debug("went background"); for (Listener l : listeners) { try { l.onBecameBackground(); } catch (Exception exc) { - LDConfig.log().e(exc, "Listener threw exception!"); + LDUtil.logExceptionAtErrorLevel(LDClient.getSharedLogger(), exc, "Listener threw exception"); } } } else { - LDConfig.log().d("still background"); + LDClient.getSharedLogger().debug("still foreground"); } }, CHECK_DELAY); } @@ -207,4 +209,4 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) { @Override public void onActivityDestroyed(Activity activity) { } -} +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java index 327b2e2b..9c50ad1e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java @@ -7,6 +7,8 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; import java.io.File; @@ -35,18 +37,20 @@ class HttpFeatureFlagFetcher implements FeatureFetcher { private final String environmentName; private final Context context; private final OkHttpClient client; + private final LDLogger logger; - static HttpFeatureFlagFetcher newInstance(Context context, LDConfig config, String environmentName) { - return new HttpFeatureFlagFetcher(context, config, environmentName); + static HttpFeatureFlagFetcher newInstance(Context context, LDConfig config, String environmentName, LDLogger logger) { + return new HttpFeatureFlagFetcher(context, config, environmentName, logger); } - private HttpFeatureFlagFetcher(Context context, LDConfig config, String environmentName) { + private HttpFeatureFlagFetcher(Context context, LDConfig config, String environmentName, LDLogger logger) { this.config = config; this.environmentName = environmentName; this.context = context; + this.logger = logger; File cacheDir = new File(context.getCacheDir(), "com.launchdarkly.http-cache"); - LDConfig.log().d("Using cache at: %s", cacheDir.getAbsolutePath()); + logger.debug("Using cache at: {}", cacheDir.getAbsolutePath()); client = new OkHttpClient.Builder() .cache(new Cache(cacheDir, MAX_CACHE_SIZE_BYTES)) @@ -63,12 +67,12 @@ public synchronized void fetch(LDUser user, final LDUtil.ResultCallback + * By default, the SDK sends logging to Timber. If you want to bypass Timber and use Android + * logging directly instead, use this class with {@link LDConfig.Builder#logAdapter(LDLogAdapter)}: + *

+ *     LDConfig config = new LDConfig.Builder()
+ *         .logAdapter(LDAndroidLogging.adapter())
+ *         .build();
+ * 
+ *

+ * By default, debug-level logging is disabled and all other levels are enabled. To change this, + * use {@link LDConfig.Builder#logLevel(LDLogLevel)}. + * @since 3.2.0 + */ +public abstract class LDAndroidLogging { + public static LDLogAdapter adapter() { + return AdapterImpl.INSTANCE; + } + + private static final class AdapterImpl implements LDLogAdapter { + static final AdapterImpl INSTANCE = new AdapterImpl(); + + @Override + public Channel newChannel(String name) { + return new ChannelImpl(name); + } + } + + private static final class ChannelImpl extends ChannelImplBase { + public ChannelImpl(String tag) { + super(tag); + } + + @Override + public boolean isEnabled(LDLogLevel level) { + return Log.isLoggable(tag, toAndroidLogLevel(level)); + } + + private static int toAndroidLogLevel(LDLogLevel level) { + switch (level) { + case DEBUG: return Log.DEBUG; + case INFO: return Log.INFO; + case WARN: return Log.WARN; + case ERROR: return Log.ERROR; + default: return Log.VERBOSE; + } + } + + @Override + protected void logInternal(LDLogLevel level, String text) { + switch (level) { + case DEBUG: + Log.d(tag, text); + break; + case INFO: + Log.i(tag, text); + break; + case WARN: + Log.w(tag, text); + break; + case ERROR: + Log.e(tag, text); + break; + } + } + } + + abstract static class ChannelImplBase implements LDLogAdapter.Channel { + protected final String tag; + + public ChannelImplBase(String tag) { + this.tag = tag; + } + + protected abstract void logInternal(LDLogLevel level, String text); + + // To avoid unnecessary string computations for debug output, we don't want to + // pre-format messages for disabled levels. We'll avoid that by checking if the + // level is enabled first. + + @Override + public void log(LDLogLevel level, Object message) { + if (isEnabled(level)) { + logInternal(level, message == null ? null : message.toString()); + } + } + + @Override + public void log(LDLogLevel level, String format, Object param) { + if (isEnabled(level)) { + logInternal(level, SimpleFormat.format(format, param)); + } + } + + @Override + public void log(LDLogLevel level, String format, Object param1, Object param2) { + if (isEnabled(level)) { + logInternal(level, SimpleFormat.format(format, param1, param2)); + } + } + + @Override + public void log(LDLogLevel level, String format, Object... params) { + if (isEnabled(level)) { + logInternal(level, SimpleFormat.format(format, params)); + } + } + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 5ee5c5af..0430bb2b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -9,6 +9,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDUser; @@ -36,7 +38,6 @@ import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; -import timber.log.Timber; /** * Client for accessing LaunchDarkly's Feature Flag system. This class enforces a singleton pattern. @@ -53,6 +54,8 @@ public class LDClient implements LDClientInterface, Closeable { // A lock to ensure calls to `init()` are serialized. static Object initLock = new Object(); + private static volatile LDLogger sharedLogger; + private final Application application; private final LDConfig config; private final DefaultUserManager userManager; @@ -64,6 +67,7 @@ public class LDClient implements LDClientInterface, Closeable { private final List> connectionFailureListeners = Collections.synchronizedList(new ArrayList<>()); private final ExecutorService executor = Executors.newFixedThreadPool(1); + private final LDLogger logger; /** * Initializes the singleton/primary instance. The result is a {@link Future} which @@ -98,16 +102,16 @@ public static Future init(@NonNull Application application, if (user == null) { return new LDFailedFuture<>(new LaunchDarklyException("Client initialization requires a valid user")); } + + initSharedLogger(config); + // Acquire the `initLock` to ensure that if `init()` is called multiple times, we will only // initialize the client(s) once. synchronized (initLock) { if (instances != null) { - LDConfig.log().w("LDClient.init() was called more than once! returning primary instance."); + getSharedLogger().warn("LDClient.init() was called more than once! returning primary instance."); return new LDSuccessFuture<>(instances.get(LDConfig.primaryEnvironmentName)); } - if (BuildConfig.DEBUG) { - Timber.plant(new Timber.DebugTree()); - } Foreground.init(application); @@ -116,14 +120,14 @@ public static Future init(@NonNull Application application, if (!instanceIdSharedPrefs.contains(INSTANCE_ID_KEY)) { String uuid = UUID.randomUUID().toString(); - LDConfig.log().i("Did not find existing instance id. Saving a new one"); + getSharedLogger().info("Did not find existing instance id. Saving a new one"); SharedPreferences.Editor editor = instanceIdSharedPrefs.edit(); editor.putString(INSTANCE_ID_KEY, uuid); editor.apply(); } instanceId = instanceIdSharedPrefs.getString(INSTANCE_ID_KEY, instanceId); - LDConfig.log().i("Using instance id: %s", instanceId); + getSharedLogger().info("Using instance id: {}", instanceId); Migration.migrateWhenNeeded(application, config); @@ -183,7 +187,7 @@ static LDUser customizeUser(LDUser user) { String key = user.getKey(); if (key == null || key.equals("")) { - LDConfig.log().i("User was created with null/empty key. Using device-unique anonymous user key: %s", LDClient.getInstanceId()); + getSharedLogger().info("User was created with null/empty key. Using device-unique anonymous user key: {}", LDClient.getInstanceId()); builder.key(LDClient.getInstanceId()); builder.anonymous(true); } @@ -204,14 +208,16 @@ static LDUser customizeUser(LDUser user) { * @return The primary LDClient instance */ public static LDClient init(Application application, LDConfig config, LDUser user, int startWaitSeconds) { - LDConfig.log().i("Initializing Client and waiting up to %s for initialization to complete", startWaitSeconds); + initSharedLogger(config); + getSharedLogger().info("Initializing Client and waiting up to {} for initialization to complete", startWaitSeconds); Future initFuture = init(application, config, user); try { return initFuture.get(startWaitSeconds, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException e) { - LDConfig.log().e(e, "Exception during Client initialization"); + getSharedLogger().error("Exception during Client initialization: {}", LogValues.exceptionSummary(e)); + getSharedLogger().debug(LogValues.exceptionTrace(e)); } catch (TimeoutException e) { - LDConfig.log().w("Client did not successfully initialize within %s seconds. It could be taking longer than expected to start up", startWaitSeconds); + getSharedLogger().warn("Client did not successfully initialize within {} seconds. It could be taking longer than expected to start up", startWaitSeconds); } return instances.get(LDConfig.primaryEnvironmentName); } @@ -222,7 +228,7 @@ public static LDClient init(Application application, LDConfig config, LDUser use */ public static LDClient get() throws LaunchDarklyException { if (instances == null) { - LDConfig.log().e("LDClient.get() was called before init()!"); + getSharedLogger().error("LDClient.get() was called before init()!"); throw new LaunchDarklyException("LDClient.get() was called before init()!"); } return instances.get(LDConfig.primaryEnvironmentName); @@ -237,7 +243,7 @@ public static LDClient get() throws LaunchDarklyException { public static LDClient getForMobileKey(String keyName) throws LaunchDarklyException { Map instancesNow = instances; // ensures atomicity if (instancesNow == null) { - LDConfig.log().e("LDClient.getForMobileKey() was called before init()!"); + getSharedLogger().error("LDClient.getForMobileKey() was called before init()!"); throw new LaunchDarklyException("LDClient.getForMobileKey() was called before init()!"); } if (!(instancesNow.containsKey(keyName))) { @@ -253,23 +259,28 @@ protected LDClient(final Application application, @NonNull final LDConfig config @VisibleForTesting protected LDClient(final Application application, @NonNull final LDConfig config, final String environmentName) { - LDConfig.log().i("Creating LaunchDarkly client. Version: %s", BuildConfig.VERSION_NAME); + this.logger = LDLogger.withAdapter(config.getLogAdapter(), config.getLoggerName()); + logger.info("Creating LaunchDarkly client. Version: {}", BuildConfig.VERSION_NAME); this.config = config; this.application = application; String sdkKey = config.getMobileKeys().get(environmentName); - FeatureFetcher fetcher = HttpFeatureFlagFetcher.newInstance(application, config, environmentName); + FeatureFetcher fetcher = HttpFeatureFlagFetcher.newInstance(application, config, environmentName, logger); OkHttpClient sharedEventClient = makeSharedEventClient(); if (config.getDiagnosticOptOut()) { this.diagnosticStore = null; this.diagnosticEventProcessor = null; } else { this.diagnosticStore = new DiagnosticStore(application, sdkKey); - this.diagnosticEventProcessor = new DiagnosticEventProcessor(config, environmentName, diagnosticStore, application, sharedEventClient); + this.diagnosticEventProcessor = new DiagnosticEventProcessor(config, environmentName, diagnosticStore, application, + sharedEventClient, logger); } - this.userManager = DefaultUserManager.newInstance(application, fetcher, environmentName, sdkKey, config.getMaxCachedUsers()); + this.userManager = DefaultUserManager.newInstance(application, fetcher, environmentName, sdkKey, config.getMaxCachedUsers(), + logger); - eventProcessor = new DefaultEventProcessor(application, config, userManager.getSummaryEventStore(), environmentName, diagnosticStore, sharedEventClient); - connectivityManager = new ConnectivityManager(application, config, eventProcessor, userManager, environmentName, diagnosticEventProcessor, diagnosticStore); + eventProcessor = new DefaultEventProcessor(application, config, userManager.getSummaryEventStore(), environmentName, + diagnosticStore, sharedEventClient, logger); + connectivityManager = new ConnectivityManager(application, config, eventProcessor, userManager, environmentName, + diagnosticEventProcessor, diagnosticStore, logger); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { connectivityReceiver = new ConnectivityReceiver(); @@ -311,7 +322,7 @@ public Future identify(LDUser user) { return new LDFailedFuture<>(new LaunchDarklyException("User cannot be null")); } if (user.getKey() == null) { - LDConfig.log().w("identify called with null user or null user key!"); + logger.warn("identify called with null user or null user key!"); } return identifyInstances(customizeUser(user)); } @@ -442,17 +453,17 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ LDValue value = defaultValue; if (flag == null || flag.isDeleted()) { - LDConfig.log().i("Unknown feature flag \"%s\"; returning default value", key); + logger.info("Unknown feature flag \"{}\"; returning default value", key); result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); } else { value = flag.getValue(); int variation = flag.getVariation() == null ? EvaluationDetail.NO_VARIATION : flag.getVariation(); if (value.isNull()) { - LDConfig.log().w("Feature flag \"%s\" retrieved with no value; returning default value", key); + logger.warn("Feature flag \"{}\" retrieved with no value; returning default value", key); value = defaultValue; result = EvaluationDetail.fromValue(defaultValue, variation, flag.getReason()); } else if (checkType && !defaultValue.isNull() && value.getType() != defaultValue.getType()) { - LDConfig.log().w("Feature flag \"%s\" with type %s retrieved as %s; returning default value", key, value.getType(), defaultValue.getType()); + logger.warn("Feature flag \"{}\" with type {} retrieved as {}; returning default value", key, value.getType(), defaultValue.getType()); value = defaultValue; result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)); } else { @@ -461,7 +472,7 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ sendFlagRequestEvent(key, flag, value, defaultValue, flag.isTrackReason() | needsReason ? result.getReason() : null); } - LDConfig.log().d("returning variation: %s flagKey: %s user key: %s", result, key, userManager.getCurrentUser().getKey()); + logger.debug("returning variation: {} flagKey: {} user key: {}", result, key, userManager.getCurrentUser().getKey()); updateSummaryEvents(key, flag, value, defaultValue); return result; } @@ -495,6 +506,7 @@ private void closeInstances() { for (LDClient client : oldClients) { client.closeInternal(); } + sharedLogger = null; } @Override @@ -673,7 +685,7 @@ private void sendEvent(Event event) { if (!connectivityManager.isOffline()) { boolean processed = eventProcessor.sendEvent(event); if (!processed) { - LDConfig.log().w("Exceeded event queue capacity. Increase capacity to avoid dropping events."); + logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); if (diagnosticStore != null) { diagnosticStore.incrementDroppedEventCount(); } @@ -700,7 +712,7 @@ private void updateSummaryEvents(String flagKey, Flag flag, LDValue result, LDVa static void triggerPollInstances() { if (instances == null) { - LDConfig.log().w("Cannot perform poll when LDClient has not been initialized!"); + getSharedLogger().warn("Cannot perform poll when LDClient has not been initialized!"); return; } for (LDClient instance : instances.values()) { @@ -710,7 +722,7 @@ static void triggerPollInstances() { static void onNetworkConnectivityChangeInstances(boolean network) { if (instances == null) { - LDConfig.log().e("Tried to update LDClients with network connectivity status, but LDClient has not yet been initialized."); + getSharedLogger().error("Tried to update LDClients with network connectivity status, but LDClient has not yet been initialized."); return; } for (LDClient instance : instances.values()) { @@ -718,6 +730,24 @@ static void onNetworkConnectivityChangeInstances(boolean network) { } } + private static void initSharedLogger(LDConfig config) { + synchronized (initLock) { + // We initialize the shared logger lazily because, until the first time init() is called, because + // we don't know what the log adapter should be until there's a configuration. + if (sharedLogger == null) { + sharedLogger = LDLogger.withAdapter(config.getLogAdapter(), config.getLoggerName()); + } + } + } + + static LDLogger getSharedLogger() { + LDLogger logger = sharedLogger; + if (logger != null) { + return logger; + } + return LDLogger.none(); + } + @VisibleForTesting SummaryEventStore getSummaryEventStore() { return userManager.getSummaryEventStore(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index aece004f..e6b1e39c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -6,20 +6,24 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.UserAttribute; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import okhttp3.Headers; import okhttp3.MediaType; -import timber.log.Timber; -import timber.log.Timber.Tree; /** * This class exposes advanced configuration options for {@link LDClient}. Instances of this class @@ -27,7 +31,9 @@ */ public class LDConfig { - static final String TIMBER_TAG = "LaunchDarklySdk"; + static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; + static final LDLogLevel DEFAULT_LOG_LEVEL = LDLogLevel.INFO; + static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; static final String USER_AGENT_HEADER_VALUE = "AndroidClient/" + BuildConfig.VERSION_NAME; static final String AUTH_SCHEME = "api_key "; @@ -87,6 +93,9 @@ public class LDConfig { private final boolean autoAliasingOptOut; + private final LDLogAdapter logAdapter; + private final String loggerName; + LDConfig(Map mobileKeys, Uri pollUri, Uri eventsUri, @@ -110,7 +119,9 @@ public class LDConfig { String wrapperVersion, int maxCachedUsers, LDHeaderUpdater headerTransform, - boolean autoAliasingOptOut) { + boolean autoAliasingOptOut, + LDLogAdapter logAdapter, + String loggerName) { this.mobileKeys = mobileKeys; this.pollUri = pollUri; @@ -136,11 +147,12 @@ public class LDConfig { this.maxCachedUsers = maxCachedUsers; this.headerTransform = headerTransform; this.autoAliasingOptOut = autoAliasingOptOut; + this.logAdapter = logAdapter; + this.loggerName = loggerName; this.filteredEventGson = new GsonBuilder() .registerTypeAdapter(LDUser.class, new LDUtil.LDUserPrivateAttributesTypeAdapter(this)) .create(); - } Headers headersForEnvironment(@NonNull String environmentName, @@ -281,6 +293,10 @@ boolean isAutoAliasingOptOut() { return autoAliasingOptOut; } + LDLogAdapter getLogAdapter() { return logAdapter; } + + String getLoggerName() { return loggerName; } + /** * A builder that helps construct * {@link LDConfig} objects. Builder calls can be chained, enabling the following pattern: @@ -324,6 +340,10 @@ public static class Builder { private LDHeaderUpdater headerTransform; private boolean autoAliasingOptOut = false; + private LDLogAdapter logAdapter = defaultLogAdapter(); + private String loggerName = DEFAULT_LOGGER_NAME; + private LDLogLevel logLevel = null; + /** * Specifies that user attributes (other than the key) should be hidden from LaunchDarkly. * If this is set, all user attribute values will be private, not just the attributes @@ -682,42 +702,154 @@ public LDConfig.Builder headerTransform(LDHeaderUpdater headerTransform) { this.headerTransform = headerTransform; return this; } - + + /** + * Specifies the implementation of logging to use. + *

+ * The com.launchdarkly.logging + * API defines the {@link LDLogAdapter} interface to specify where log output should be sent. By default, + * it is set to {@link LDTimberLogging#adapter()}, meaning that output will be sent to the + * Timber framework and controlled by whatever Timber + * configuration the application has created. You may change this to {@link LDAndroidLogging#adapter()} + * to bypass Timber and use Android native logging directly; or, use the + * {@link com.launchdarkly.logging.Logs} factory methods, or a custom implementation, to handle log + * output differently. + *

+ * Specifying {@code logAdapter(Logs.none())} completely disables log output. + *

+ * For more about logging adapters, + * see the SDK reference guide + * and the API documentation for + * com.launchdarkly.logging. + * + * @param logAdapter an {@link LDLogAdapter} for the desired logging implementation + * @return the builder + * @since 3.2.0 + * @see #logLevel(LDLogLevel) + * @see #loggerName(String) + * @see LDTimberLogging + * @see LDAndroidLogging + * @see com.launchdarkly.logging.Logs + */ + public LDConfig.Builder logAdapter(LDLogAdapter logAdapter) { + this.logAdapter = logAdapter == null ? defaultLogAdapter() : logAdapter; + return this; + } + + /** + * Specifies the lowest level of logging to enable. + *

+ * This is only applicable when using an implementation of logging that does not have its own + * external filter/configuration mechanism, such as {@link LDAndroidLogging}. It adds + * a log level filter so that log messages at lower levels are suppressed. The default is + * {@link LDLogLevel#INFO}, meaning that {@code INFO}, {@code WARN}, and {@code ERROR} levels + * are enabled, but {@code DEBUG} is disabled. To enable {@code DEBUG} level as well: + *


+         *     LDConfig config = new LDConfig.Builder()
+         *         .logAdapter(LDAndroidLogging.adapter())
+         *         .level(LDLogLevel.DEBUG)
+         *         .build();
+         * 
+ *

+ * Or, to raise the logging threshold so that only WARN and ERROR levels are enabled, and + * DEBUG and INFO are disabled: + *


+         *     LDConfig config = new LDConfig.Builder()
+         *         .logAdapter(LDAndroidLogging.adapter())
+         *         .level(LDLogLevel.WARN)
+         *         .build();
+         * 
+ *

+ * When using {@link LDTimberLogging}, Timber has its own mechanism for determining whether + * to enable debug-level logging, so this method has no effect. + * + * @param logLevel the lowest level of logging to enable + * @return the builder + * @since 3.2.0 + * @see #logAdapter(LDLogAdapter) + * @see #loggerName(String) + */ + public LDConfig.Builder logLevel(LDLogLevel logLevel) { + this.logLevel = logLevel; + return this; + } + + /** + * Specifies a custom logger name/tag for the SDK. + *

+ * When using Timber or native Android logging, this becomes the tag for all SDK log output. + * If you have specified a different logging implementation with {@link #logAdapter(LDLogAdapter)}, + * the meaning of the logger name depends on the logging framework. + *

+ * If not specified, the default is "LaunchDarklySdk". + * + * @param loggerName the logger name or tag + * @return the builder + * @since 3.2.0 + * @see #logAdapter(LDLogAdapter) + * @see #logLevel(LDLogLevel) + */ + public LDConfig.Builder loggerName(String loggerName) { + this.loggerName = loggerName == null ? DEFAULT_LOGGER_NAME : loggerName; + return this; + } + + private static LDLogAdapter defaultLogAdapter() { + return LDTimberLogging.adapter(); + } + /** * Returns the configured {@link LDConfig} object. * @return the configuration */ public LDConfig build() { + LDLogAdapter actualLogAdapter = Logs.level(logAdapter, + logLevel == null ? DEFAULT_LOG_LEVEL : logLevel); + // Note: if the log adapter is LDTimberLogging, then Logs.level has no effect - we will still + // forward all of our logging to Timber, because it has its own mechanism for filtering out + // debug logging. But if it is LDAndroidLogging or anything else, Logs.level ensures that no + // output at a lower level than logLevel will be sent anywhere. + + LDLogger logger = LDLogger.withAdapter(actualLogAdapter, loggerName); + if (!stream) { if (pollingIntervalMillis < MIN_POLLING_INTERVAL_MILLIS) { - LDConfig.log().w("setPollingIntervalMillis: %s was set below the allowed minimum of: %s. Ignoring and using minimum value.", pollingIntervalMillis, MIN_POLLING_INTERVAL_MILLIS); + logger.warn( + "setPollingIntervalMillis: {} was set below the allowed minimum of: {}. Ignoring and using minimum value.", + pollingIntervalMillis, MIN_POLLING_INTERVAL_MILLIS); pollingIntervalMillis = MIN_POLLING_INTERVAL_MILLIS; } if (!disableBackgroundUpdating && backgroundPollingIntervalMillis < pollingIntervalMillis) { - LDConfig.log().w("BackgroundPollingIntervalMillis: %s was set below the foreground polling interval: %s. Ignoring and using minimum value for background polling.", backgroundPollingIntervalMillis, pollingIntervalMillis); + logger.warn( + "BackgroundPollingIntervalMillis: {} was set below the foreground polling interval: {}. Ignoring and using minimum value for background polling.", + backgroundPollingIntervalMillis, pollingIntervalMillis); backgroundPollingIntervalMillis = MIN_BACKGROUND_POLLING_INTERVAL_MILLIS; } if (eventsFlushIntervalMillis == 0) { eventsFlushIntervalMillis = pollingIntervalMillis; - LDConfig.log().d("Streaming is disabled, so we're setting the events flush interval to the polling interval value: %s", pollingIntervalMillis); + // this is a normal occurrence, so don't log a warning about it } } if (!disableBackgroundUpdating) { if (backgroundPollingIntervalMillis < MIN_BACKGROUND_POLLING_INTERVAL_MILLIS) { - LDConfig.log().w("BackgroundPollingIntervalMillis: %s was set below the minimum allowed: %s. Ignoring and using minimum value.", backgroundPollingIntervalMillis, MIN_BACKGROUND_POLLING_INTERVAL_MILLIS); + logger.warn( + "BackgroundPollingIntervalMillis: {} was set below the minimum allowed: {}. Ignoring and using minimum value.", + backgroundPollingIntervalMillis, MIN_BACKGROUND_POLLING_INTERVAL_MILLIS); backgroundPollingIntervalMillis = MIN_BACKGROUND_POLLING_INTERVAL_MILLIS; } } if (eventsFlushIntervalMillis == 0) { - eventsFlushIntervalMillis = DEFAULT_FLUSH_INTERVAL_MILLIS; + eventsFlushIntervalMillis = DEFAULT_FLUSH_INTERVAL_MILLIS; // this is a normal occurrence, so don't log a warning about it } if (diagnosticRecordingIntervalMillis < MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS) { - LDConfig.log().w("diagnosticRecordingIntervalMillis was set to %s, lower than the minimum allowed (%s). Ignoring and using minimum value.", diagnosticRecordingIntervalMillis, MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS); + logger.warn( + "diagnosticRecordingIntervalMillis was set to %s, lower than the minimum allowed (%s). Ignoring and using minimum value.", + diagnosticRecordingIntervalMillis, MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS); diagnosticRecordingIntervalMillis = MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; } @@ -754,11 +886,9 @@ public LDConfig build() { wrapperVersion, maxCachedUsers, headerTransform, - autoAliasingOptOut); + autoAliasingOptOut, + actualLogAdapter, + loggerName); } } - - public static final Tree log() { - return Timber.tag(TIMBER_TAG); - } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFutures.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFutures.java index bfa134fb..6d26dba4 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFutures.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFutures.java @@ -90,7 +90,7 @@ synchronized void set(T result) { notifier.notifyAll(); } } else { - LDConfig.log().w("LDAwaitFuture set twice"); + LDClient.getSharedLogger().warn("LDAwaitFuture set twice"); } } @@ -102,7 +102,7 @@ synchronized void setException(@NonNull Throwable error) { notifier.notifyAll(); } } else { - LDConfig.log().w("LDAwaitFuture set twice"); + LDClient.getSharedLogger().warn("LDAwaitFuture set twice"); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDTimberLogging.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDTimberLogging.java new file mode 100644 index 00000000..5df2fbe4 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDTimberLogging.java @@ -0,0 +1,112 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; + +import java.util.concurrent.atomic.AtomicBoolean; + +import timber.log.Timber; +import timber.log.Timber.DebugTree; +import timber.log.Timber.Tree; + +/** + * Allows LaunchDarkly log output to be forwarded to Timber. + *

+ * Currently this is the default logging implementation; in the future, we may change the default to + * {@link LDAndroidLogging}. + *

+ * When this logging implementation is active, the SDK will automatically call + * {@code Timber.plant(new Timber.DebugTree)} at initialization time if and only if + * {@code BuildConfig.DEBUG} is true. This behavior is for consistency with the default behavior of + * earlier SDK versions and may be removed in the future. It can be changed with + * {@link Adapter#autoPlantDebugTree(boolean)}. + * + * @since 3.2.0 + */ +public abstract class LDTimberLogging { + public static LDLogAdapter adapter() { + return new Adapter(true); + } + + /** + * A Timber implementation of the {@link LDLogAdapter} interface. + */ + public static final class Adapter implements LDLogAdapter, LDLogAdapter.IsConfiguredExternally { + // Note: implementing IsConfiguredExternally tells the logging framework that it should not + // try to do level filtering, because Timber has its own configuration mechanisms. + private final boolean autoPlantDebugTree; + + Adapter(boolean autoPlantDebugTree) { + this.autoPlantDebugTree = autoPlantDebugTree; + } + + /** + * Returns a modified logging adapter with the automatic debug tree behavior changed. + *

+ * By default, this property is true, meaning that the SDK will automatically call + * {@code Timber.plant(new Timber.DebugTree)} at initialization time if and only if + * {@code BuildConfig.DEBUG} is true. If you set it to false as shown below, then the + * SDK will never create a {@code DebugTree} and the application is responsible for + * doing so if desired. + *


+         *     LDConfig config = new LDConfig.Builder()
+         *         .mobileKey("mobile-key")
+         *         .logAdapter(LDTimberLogging.adapter().autoPlantDebugTree(false))
+         *         .build();
+         * 
+ *

+ * In a future version, this automatic behavior may be removed, since it is arguably more + * correct for library code to leave all Tree-planting to the application. The behavior is + * retained in the current release for backward compatibility. + *

+ * @param autoPlantDebugTree true to retain the default automatic {@code DebugTree} + * behavior, or false to disable it + * @return a modified {@link Adapter} with the specified behavior + */ + public Adapter autoPlantDebugTree(boolean autoPlantDebugTree) { + return new Adapter(autoPlantDebugTree); + } + + @Override + public Channel newChannel(String name) { + return new ChannelImpl(name, autoPlantDebugTree); + } + } + + private static final class ChannelImpl extends LDAndroidLogging.ChannelImplBase { + private static final AtomicBoolean inited = new AtomicBoolean(false); + + ChannelImpl(String tag, boolean autoPlantDebugTree) { + super(tag); + if (autoPlantDebugTree && !inited.getAndSet(true)) { + if (BuildConfig.DEBUG) { + Timber.plant(new DebugTree()); + } + } + } + + @Override + public boolean isEnabled(LDLogLevel level) { + return true; + } + + @Override + protected void logInternal(LDLogLevel level, String text) { + Tree tree = Timber.tag(tag); + switch (level) { + case DEBUG: + tree.d(text); + break; + case INFO: + tree.i(text); + break; + case WARN: + tree.w(text); + break; + case ERROR: + tree.e(text); + break; + } + } + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index fbdbf9d5..c3ef2926 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -13,11 +13,15 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import com.launchdarkly.logging.LDLogLevel; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -75,7 +79,8 @@ static boolean isClientConnected(Context context, String environmentName) { try { return deviceConnected && !LDClient.getForMobileKey(environmentName).isOffline(); } catch (LaunchDarklyException e) { - LDConfig.log().e(e, "Exception caught when getting LDClient"); + LDClient.getSharedLogger().error("Exception caught when getting LDClient: {}", LogValues.exceptionSummary(e)); + LDClient.getSharedLogger().debug(LogValues.exceptionTrace(e)); return false; } } @@ -130,6 +135,27 @@ static boolean isHttpErrorRecoverable(int statusCode) { return true; } + static void logExceptionAtErrorLevel(LDLogger logger, Throwable ex, String msgFormat, Object... msgArgs) { + logException(logger, ex, true, msgFormat, msgArgs); + } + + static void logExceptionAtWarnLevel(LDLogger logger, Throwable ex, String msgFormat, Object... msgArgs) { + logException(logger, ex, false, msgFormat, msgArgs); + } + + private static void logException(LDLogger logger, Throwable ex, boolean asError, String msgFormat, Object... msgArgs) { + String addFormat = msgFormat + " - {}"; + Object exSummary = LogValues.exceptionSummary(ex); + Object[] args = Arrays.copyOf(msgArgs, msgArgs.length + 1); + args[msgArgs.length] = exSummary; + if (asError) { + logger.error(addFormat, args); + } else { + logger.warn(addFormat, args); + } + logger.debug(LogValues.exceptionTrace(ex)); + } + static class LDUserPrivateAttributesTypeAdapter extends TypeAdapter { private final LDConfig config; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Migration.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Migration.java index 8bfae268..52a66789 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Migration.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Migration.java @@ -7,6 +7,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.launchdarkly.logging.LogValues; import java.io.File; import java.util.ArrayList; @@ -29,7 +30,8 @@ static void migrateWhenNeeded(Application application, LDConfig config) { try { migrate_2_7_fresh(application, config); } catch (Exception ex) { - LDConfig.log().w(ex, "Exception while performing fresh v2.7.0 store migration"); + LDUtil.logExceptionAtWarnLevel(LDClient.getSharedLogger(), ex, + "Exception while performing fresh v2.7.0 store migration"); } } @@ -37,7 +39,8 @@ static void migrateWhenNeeded(Application application, LDConfig config) { try { migrate_2_7_from_2_6(application); } catch (Exception ex) { - LDConfig.log().w(ex, "Exception while performing v2.6.0 to v2.7.0 store migration"); + LDUtil.logExceptionAtWarnLevel(LDClient.getSharedLogger(), ex, + "Exception while performing v2.6.0 to v2.7.0 store migration"); } } } @@ -62,7 +65,7 @@ private static String reconstructFlag(String key, String metadata, Object value) } private static void migrate_2_7_fresh(Application application, LDConfig config) { - LDConfig.log().d("Migrating to v2.7.0 shared preferences store"); + LDClient.getSharedLogger().debug("Migrating to v2.7.0 shared preferences store"); ArrayList userKeys = getUserKeysPre_2_6(application, config); SharedPreferences versionSharedPrefs = application.getSharedPreferences(LDConfig.SHARED_PREFS_BASE_KEY + "version", Context.MODE_PRIVATE); @@ -92,7 +95,7 @@ private static void migrate_2_7_fresh(Application application, LDConfig config) } if (allSuccess) { - LDConfig.log().d("Migration to v2.7.0 shared preferences store successful"); + LDClient.getSharedLogger().debug("Migration to v2.7.0 shared preferences store successful"); SharedPreferences migrations = application.getSharedPreferences(LDConfig.SHARED_PREFS_BASE_KEY + "migrations", Context.MODE_PRIVATE); boolean logged = migrations.edit().putString("v2.7.0", "v2.7.0").commit(); if (logged) { @@ -108,7 +111,7 @@ private static void migrate_2_7_fresh(Application application, LDConfig config) } private static void migrate_2_7_from_2_6(Application application) { - LDConfig.log().d("Migrating to v2.7.0 shared preferences store from v2.6.0"); + LDClient.getSharedLogger().debug("Migrating to v2.7.0 shared preferences store from v2.6.0"); Map> keyUsers = getUserKeys_2_6(application); @@ -133,7 +136,7 @@ private static void migrate_2_7_from_2_6(Application application) { } if (allSuccess) { - LDConfig.log().d("Migration to v2.7.0 shared preferences store successful"); + LDClient.getSharedLogger().debug("Migration to v2.7.0 shared preferences store successful"); SharedPreferences migrations = application.getSharedPreferences(LDConfig.SHARED_PREFS_BASE_KEY + "migrations", Context.MODE_PRIVATE); boolean logged = migrations.edit().putString("v2.7.0", "v2.7.0").commit(); if (logged) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java index 53e14be0..962434bb 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java @@ -10,6 +10,8 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE; +import com.launchdarkly.logging.LogValues; + /** * Used internally by the SDK. */ @@ -27,13 +29,13 @@ public void onReceive(Context context, Intent intent) { } synchronized static void startBackgroundPolling(Context context) { - LDConfig.log().d("Starting background polling"); + LDClient.getSharedLogger().debug("Starting background polling"); startPolling(context, backgroundPollingIntervalMillis, backgroundPollingIntervalMillis); } synchronized static void startPolling(Context context, int initialDelayMillis, int intervalMillis) { stop(context); - LDConfig.log().d("startPolling with initialDelayMillis: %d and intervalMillis: %d", initialDelayMillis, intervalMillis); + LDClient.getSharedLogger().debug("startPolling with initialDelayMillis: %d and intervalMillis: %d", initialDelayMillis, intervalMillis); PendingIntent pendingIntent = getPendingIntent(context); AlarmManager alarmMgr = getAlarmManager(context); @@ -44,12 +46,13 @@ synchronized static void startPolling(Context context, int initialDelayMillis, i intervalMillis, pendingIntent); } catch (Exception ex) { - LDConfig.log().w(ex, "Exception occurred when creating [background] polling alarm, likely due to the host application having too many existing alarms."); + LDUtil.logExceptionAtWarnLevel(LDClient.getSharedLogger(), ex, + "Exception occurred when creating [background] polling alarm, likely due to the host application having too many existing alarms"); } } synchronized static void stop(Context context) { - LDConfig.log().d("Stopping pollingUpdater"); + LDClient.getSharedLogger().debug("Stopping pollingUpdater"); PendingIntent pendingIntent = getPendingIntent(context); AlarmManager alarmMgr = getAlarmManager(context); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStore.java index 2238aca4..ced6e69f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStore.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import com.google.gson.Gson; +import com.launchdarkly.logging.LDLogger; import java.io.File; import java.lang.ref.WeakReference; @@ -29,12 +30,14 @@ class SharedPrefsFlagStore implements FlagStore { private final Application application; private SharedPreferences sharedPreferences; private WeakReference listenerWeakReference; + private LDLogger logger; - SharedPrefsFlagStore(@NonNull Application application, @NonNull String identifier) { + SharedPrefsFlagStore(@NonNull Application application, @NonNull String identifier, LDLogger logger) { this.application = application; this.prefsKey = SHARED_PREFS_BASE_KEY + identifier + "-flags"; this.sharedPreferences = application.getSharedPreferences(prefsKey, Context.MODE_PRIVATE); this.listenerWeakReference = new WeakReference<>(null); + this.logger = logger; } @SuppressLint("ApplySharedPref") @@ -44,7 +47,7 @@ public void delete() { sharedPreferences = null; File file = new File(application.getFilesDir().getParent() + "/shared_prefs/" + prefsKey + ".xml"); - LDConfig.log().i("Deleting SharedPrefs file:%s", file.getAbsolutePath()); + logger.info("Deleting SharedPrefs file:{}", file.getAbsolutePath()); //noinspection ResultOfMethodCallIgnored file.delete(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactory.java index bdc81806..5625a0f1 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactory.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreFactory.java @@ -4,16 +4,20 @@ import androidx.annotation.NonNull; +import com.launchdarkly.logging.LDLogger; + class SharedPrefsFlagStoreFactory implements FlagStoreFactory { private final Application application; + private final LDLogger logger; - SharedPrefsFlagStoreFactory(@NonNull Application application) { + SharedPrefsFlagStoreFactory(@NonNull Application application, LDLogger logger) { this.application = application; + this.logger = logger; } @Override public FlagStore createFlagStore(@NonNull String identifier) { - return new SharedPrefsFlagStore(application, identifier); + return new SharedPrefsFlagStore(application, identifier, logger); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManager.java index 36abab33..b6e13008 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreManager.java @@ -9,6 +9,8 @@ import androidx.annotation.NonNull; +import com.launchdarkly.logging.LDLogger; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -36,17 +38,20 @@ class SharedPrefsFlagStoreManager implements FlagStoreManager, StoreUpdatedListe private final SharedPreferences usersSharedPrefs; private final ConcurrentHashMap> listeners; private final CopyOnWriteArrayList allFlagsListeners; + private final LDLogger logger; SharedPrefsFlagStoreManager(@NonNull Application application, @NonNull String mobileKey, @NonNull FlagStoreFactory flagStoreFactory, - int maxCachedUsers) { + int maxCachedUsers, + @NonNull LDLogger logger) { this.mobileKey = mobileKey; this.flagStoreFactory = flagStoreFactory; this.maxCachedUsers = maxCachedUsers; this.usersSharedPrefs = application.getSharedPreferences(SHARED_PREFS_BASE_KEY + mobileKey + "-users", Context.MODE_PRIVATE); this.listeners = new ConcurrentHashMap<>(); this.allFlagsListeners = new CopyOnWriteArrayList<>(); + this.logger = logger; } @Override @@ -73,7 +78,7 @@ public void switchToUser(String userKey) { // Remove oldest users until we are at MAX_USERS. for (int i = 0; i < usersToRemove; i++) { String removed = oldestFirstUsers.next(); - LDConfig.log().d("Exceeded max # of users: [%s] Removing user: [%s]", maxCachedUsers, removed); + logger.debug("Exceeded max # of users: [{}] Removing user: [{}]", maxCachedUsers, removed); // Load FlagStore for oldest user and delete it. flagStoreFactory.createFlagStore(storeIdentifierForUser(removed)).delete(); // Remove entry from usersSharedPrefs. @@ -99,9 +104,9 @@ public void registerListener(String key, FeatureFlagChangeListener listener) { Set oldSet = listeners.putIfAbsent(key, newSet); if (oldSet != null) { oldSet.add(listener); - LDConfig.log().d("Added listener. Total count: [%s]", oldSet.size()); + logger.debug("Added listener. Total count: [{}]", oldSet.size()); } else { - LDConfig.log().d("Added listener. Total count: 1"); + logger.debug("Added listener. Total count: 1"); } } @@ -111,7 +116,7 @@ public void unRegisterListener(String key, FeatureFlagChangeListener listener) { if (keySet != null) { boolean removed = keySet.remove(listener); if (removed) { - LDConfig.log().d("Removing listener for key: [%s]", key); + logger.debug("Removing listener for key: [{}]", key); } } } @@ -135,9 +140,9 @@ private Collection getCachedUsers(String activeUser) { for (String k : all.keySet()) { try { sortedMap.put((Long) all.get(k), k); - LDConfig.log().d("Found user: %s", userAndTimeStampToHumanReadableString(k, (Long) all.get(k))); + logger.debug("Found user: {}", userAndTimeStampToHumanReadableString(k, (Long) all.get(k))); } catch (ClassCastException cce) { - LDConfig.log().e(cce, "Unexpected type! This is not good"); + LDUtil.logExceptionAtErrorLevel(logger, cce, "Unexpected type! This is not good"); } } return sortedMap.values(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java index a248bf3f..6d4af3e9 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDValue; import java.util.HashMap; @@ -16,9 +17,11 @@ class SharedPrefsSummaryEventStore implements SummaryEventStore { private static final String START_DATE_KEY = "$startDate$"; private final SharedPreferences sharedPreferences; + private final LDLogger logger; - SharedPrefsSummaryEventStore(Application application, String name) { + SharedPrefsSummaryEventStore(Application application, String name, LDLogger logger) { this.sharedPreferences = application.getSharedPreferences(name, Context.MODE_PRIVATE); + this.logger = logger; } @Override @@ -49,7 +52,7 @@ public synchronized void addOrUpdateEvent(String flagResponseKey, LDValue value, } editor.apply(); - LDConfig.log().d("Updated summary for flagKey %s to %s", flagResponseKey, GsonCache.getGson().toJson(storedCounters)); + logger.debug("Updated summary for flagKey {} to {}", flagResponseKey, GsonCache.getGson().toJson(storedCounters)); } @Override diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java index 250f7bb0..238023e2 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java @@ -7,6 +7,8 @@ import com.launchdarkly.eventsource.EventSource; import com.launchdarkly.eventsource.MessageEvent; import com.launchdarkly.eventsource.UnsuccessfulResponseException; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; import java.net.URI; @@ -43,25 +45,28 @@ class StreamUpdateProcessor { private final LDUtil.ResultCallback notifier; private final DiagnosticStore diagnosticStore; private long eventSourceStarted; + private final LDLogger logger; - StreamUpdateProcessor(LDConfig config, UserManager userManager, String environmentName, DiagnosticStore diagnosticStore, LDUtil.ResultCallback notifier) { + StreamUpdateProcessor(LDConfig config, UserManager userManager, String environmentName, DiagnosticStore diagnosticStore, + LDUtil.ResultCallback notifier, LDLogger logger) { this.config = config; this.userManager = userManager; this.environmentName = environmentName; this.notifier = notifier; this.diagnosticStore = diagnosticStore; + this.logger = logger; queue = new Debounce(); executor = new BackgroundThreadExecutor().newFixedThreadPool(2); } synchronized void start() { if (!running && !connection401Error) { - LDConfig.log().d("Starting."); + logger.debug("Starting."); EventHandler handler = new EventHandler() { @Override public void onOpen() { - LDConfig.log().i("Started LaunchDarkly EventStream"); + logger.info("Started LaunchDarkly EventStream"); if (diagnosticStore != null) { diagnosticStore.addStreamInit(eventSourceStarted, (int) (System.currentTimeMillis() - eventSourceStarted), false); } @@ -69,13 +74,13 @@ public void onOpen() { @Override public void onClosed() { - LDConfig.log().i("Closed LaunchDarkly EventStream"); + logger.info("Closed LaunchDarkly EventStream"); } @Override public void onMessage(final String name, MessageEvent event) { final String eventData = event.getData(); - LDConfig.log().d("onMessage: %s: %s", name, eventData); + logger.debug("onMessage: {}: {}", name, eventData); handle(name, eventData, notifier); } @@ -86,14 +91,16 @@ public void onComment(String comment) { @Override public void onError(Throwable t) { - LDConfig.log().e(t, "Encountered EventStream error connecting to URI: %s", getUri(userManager.getCurrentUser())); + LDUtil.logExceptionAtErrorLevel(logger, t, + "Encountered EventStream error connecting to URI: {}", + getUri(userManager.getCurrentUser())); if (t instanceof UnsuccessfulResponseException) { if (diagnosticStore != null) { diagnosticStore.addStreamInit(eventSourceStarted, (int) (System.currentTimeMillis() - eventSourceStarted), true); } int code = ((UnsuccessfulResponseException) t).getCode(); if (code >= 400 && code < 500) { - LDConfig.log().e("Encountered non-retriable error: %s. Aborting connection to stream. Verify correct Mobile Key and Stream URI", code); + logger.error("Encountered non-retriable error: {}. Aborting connection to stream. Verify correct Mobile Key and Stream URI", code); running = false; notifier.onError(new LDInvalidResponseCodeFailure("Unexpected Response Code From Stream Connection", t, code, false)); if (code == 401) { @@ -101,7 +108,7 @@ public void onError(Throwable t) { try { LDClient.getForMobileKey(environmentName).setOffline(); } catch (LaunchDarklyException e) { - LDConfig.log().e(e, "Client unavailable to be set offline"); + LDUtil.logExceptionAtErrorLevel(logger, e, "Client unavailable to be set offline"); } } stop(null); @@ -148,7 +155,7 @@ public void onError(Throwable t) { @NonNull private RequestBody getRequestBody(@Nullable LDUser user) { - LDConfig.log().d("Attempting to report user in stream"); + logger.debug("Attempting to report user in stream"); return RequestBody.create(GSON.toJson(user), JSON); } @@ -181,13 +188,13 @@ private void handle(final String name, final String eventData, }); break; default: - LDConfig.log().d("Found an unknown stream protocol: %s", name); + logger.debug("Found an unknown stream protocol: {}", name); onCompleteListener.onError(new LDFailure("Unknown Stream Element Type", null, LDFailure.FailureType.UNEXPECTED_STREAM_ELEMENT_TYPE)); } } void stop(final LDUtil.ResultCallback onCompleteListener) { - LDConfig.log().d("Stopping."); + logger.debug("Stopping."); // We do this in a separate thread because closing the stream involves a network // operation and we don't want to do a network operation on the main thread. executor.execute(() -> { @@ -204,6 +211,6 @@ private synchronized void stopSync() { } running = false; es = null; - LDConfig.log().d("Stopped."); + logger.debug("Stopped."); } -} +} \ No newline at end of file