From 51de9042f49fdc5c2cd56303a7deb62577b75884 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Tue, 23 Aug 2022 15:09:27 -0700 Subject: [PATCH] prepare 3.2.0 release (#185) * Moved primaryEnvironmentName to LDConfig, simplifying by removing primaryKey separation everywhere but the builder. Add getRequestBuilderFor a specific environment. Add static method to LDClient to get all environment names so that environments can be iterated over. Add accessor to retrieve LDClient specific UserManager. Iterate over all environments in PollingUpdater. Add environment argument to UserManager constructor, removing singleton and creating replacing init with newInstance static method. * Add back in constructor without environment to UserManager * Specialize HttpFeatureFlagFetcher to the environment, looks like UserManager may not be able to do the same so removed the environment from the constructor. * Added SharedPreferences migration strategy * All tests pass, fixed migration strategy to conform to spec, fixed primaryInstance null when offline in init, fixed primaryEnvironmentName being added to secondaryMobileKeys * Update StreamUpdateProcessor construct to take an environment for the authorization header key. * Fix issue with LDConfig mobileKeys hashmap creation. * Combine futures so LDClient init future waits on all online instances of LDClient. * Propagate IOException on closing instances to caller. * Merge futures for identify call. * Some changes from code review. * Removed static from instanceId and now old SharedPreferences will only cleared once all environments have a copy in LDClient * Fixed instanceId * Updates from PR review. * Added version and flagVersion, if available * refactor(LDClient, LDConfig): changes for PR * refactor(LDClient): changed isInternetConnected behavior for PR * refactor(LDClient): removed async getForMobileKeys and wait seconds version, replaced with method that returns requested instance * Bugfix/timber cleanup (#92) Relates to launchdarkly/android-client#60 Cleaned up timber logging messages to use string formatting rather than concatenation. Log messages should remain the same as before. Also replaced Log with Timber in the example app. * Fix crash when example app is backgrounded twice. * Add security provider update mechanism using Google Play Services to attempt a provider update when TLSv1.2 is not available. * Shared Preferences Fix for Multi Environment (#94) * fix(SharedPreferences): added more SharedPreferences first time migration and differentiated SharedPreferences by mobile key * fix(UserLocalSharePreference.java): added missing mobileKey additions to getSharedPreferences, cleaned up debugging code * Fix edge cases in how multi-environment handles connection changes. * fix(UserManagerTest.java): incorrect number of arguments to UserManager instantiation in unit test * Remove line of testing code accidentally left in and refactor shared preferences migration to make future migrations easier. * Final fixes to store migration. Should be fairly future proof. * Fix issue with primitive variation calls always returning null if fallback is null. * Remove CircleCI V1 config file. (#97) * Remove getting/comparing versions as floats (#99) To prevent floating point errors in flag version comparisons. * Include values in unknown summary events and compare values for (#100) equality when creating new summary counters. * simplify flag property deserialization * rm debugging * misc cleanup * rm debugging * add eval reason data model classes * misc fixes * serialize reason * add ability to receive evaluation reasons from LD * Changed shared preferences store system to user a single FlagStore system that holds all the information on a flag to prevent issues arising from unsynchronized separate stores for flag meta-data and values. * add methods to get value with explanation; refactor existing variation methods * Abstract FlagStoreManager from FlagStore, new FlagStoreFactory class so manager can construct FlagStores of unknown type. Reformatted interfaces. Removed unused imports. * Handle null case in allFlags, actually commit changes to UserManager. * Hopefully fix edge cases in summary event reporting to pass testing. * Hopefully fix edge cases in summary event reporting to pass testing. * Simplify getFeaturesJsonObject as no longer using -1 as placeholder for null for variations. * Make Flag non-mutable. Move GsonCache to gson package, move custom serializer/deserializers to classes in gson package and create one for PUT responses. Removed BaseUserSharedPreferences. * Send summary event even if stored flag doesn't exist. * Move sendSummaryEvent update code to UserSummaryEventSharedPreferences to synchronize to prevent data race on sending, updating, and clearing event store. Move SummaryEventSharedPreferences and UserSummaryEventSharedPreferences out of response package. * Update SharedPrefsFlagStore to hold StoreUpdatedListener in weak reference. Fix various warnings. * Migration code for upcoming flagstore. * Remove couple of debug messages. * Handle todos. * Revert to old String behavior for allFlags, initialize WeakReference in SharedPrefsFlagStore. * Better implementation of EvaluationReason serialization type adapter. * Revert "Better implementation of EvaluationReason serialization type adapter." Wrong branch... This reverts commit 69c1c9b2b8d9a3b72fcd856f2b6da0e8c896802c. * Gw/ch29266/flagstore (#105) * Changed shared preferences store system to user a single FlagStore system that holds all the information on a flag to prevent issues arising from unsynchronized separate stores for flag meta-data and values. * Abstract FlagStoreManager from FlagStore, new FlagStoreFactory class so manager can construct FlagStores of unknown type. Reformatted interfaces. Removed unused imports. * Handle null case in allFlags, actually commit changes to UserManager. * Hopefully fix edge cases in summary event reporting to pass testing. * Hopefully fix edge cases in summary event reporting to pass testing. * Simplify getFeaturesJsonObject as no longer using -1 as placeholder for null for variations. * Make Flag non-mutable. Move GsonCache to gson package, move custom serializer/deserializers to classes in gson package and create one for PUT responses. Removed BaseUserSharedPreferences. * Send summary event even if stored flag doesn't exist. * Move sendSummaryEvent update code to UserSummaryEventSharedPreferences to synchronize to prevent data race on sending, updating, and clearing event store. Move SummaryEventSharedPreferences and UserSummaryEventSharedPreferences out of response package. * Update SharedPrefsFlagStore to hold StoreUpdatedListener in weak reference. Fix various warnings. * Migration code for upcoming flagstore. * Remove couple of debug messages. * Handle todos. * Revert to old String behavior for allFlags, initialize WeakReference in SharedPrefsFlagStore. * Better implementation of EvaluationReason serialization type adapter. * Remove isUnknown argument from SummaryEventSharedPreferences methods. Use Runnable instead of Callable in UserManager to avoid useless return nulls. Rename FlagStoreFactoryInterface to FlagStoreFactory. * Statically initialize Gson instance in GsonCache. * Make Gson instance in GsonCache final on principle. * Return json flags as JsonElement in allFlags map. (#106) * Bump ok-http version to 3.9.1 (#107) * fix annotations so eval reasons are serialized in events * fix/expand doc comments for public methods * typo * typo * add version string getter method * Check for null key before file comparison check. (#110) * [ch33658] Add unsafeReset() for LDClient testing re-initialization (#111) Add `unsafeReset()` method to close and clear instances for re-initializing client between tests. Update LDClientTest to call `unsafeReset()` before tests. * [ch33846] Rename tests to not start with capitals and general refactoring (#112) * Rename tests to not start with capitals * Reindent MultiEnvironmentLDClientTest to be consistent * Optimize imports * Move TLS patch into TLSUtils * Make setModernTlsVersionsOnSocket private and remove redundant null check * Remove code duplication in LDClient track overloaded methods. * Remove validateParameter in LDClient that was using a NullPointerException as a null test. * Simplify Debounce to use listener instead of callback. * Add documentation for flagstore implementation (#113) * [ch35150] Unit tests and bug fixes (#114) - Use android test orchestrator to run tests isolated from each other. This prevents the issues testing singletons. Also enabled option to clear package data between runs allowing more extensive flagstore testing. - Remove unsafe reset as it was added only for allowing testing the LDClient singleton. - Tests for new FlagStore code. - Convenience test FlagBuilder - Fix Migration to not turn all flags into Strings - Fix issue with clearAndApplyFlagUpdates not generating correct events for listeners. * Add compatibility behavior to stringVariation and allFlags methods. (#115) If a Json flag is requested with stringVariation it will serialize it to a String. Json flags will also be serialized to Strings for the map returned by allFlags() * Update LDUser not to store all fields as Json. (#116) Add testing rule to setup and teardown Timber trees for debug logging. Add additional LDUser tests. Fixed a bit of flakiness in deletesOlderThanLastFiveStoredUsers test that showed up all of a sudden. * Add metricValue field to CustomEvent, add overloaded track method for (#118) creating custom events with metricValues. * [ch37794] Run connected emulator tests in CircleCI (#120) * [ch34533] connection status, removing guava, network restructuring. (#117) * Add ConnectionInformation class. * Remove all internal uses of Guava. * Update StreamUpdateProcessor to only debounce ping events. * Add a connection state monitor to the example app. * rename repo and package name and apply markdown templates (#121) * Fix issue that stream could be started before stopping when calling identify. (#122) * Revert "Fix issue that stream could be started before stopping when calling identify. (#122)" This reverts commit fdede38cf58af2802a116599580ea64a07d7dc4a. * Revert "rename repo and package name and apply markdown templates (#121)" This reverts commit 221527594dd632322aaf7edafa0b5edff719132e. * Revert "Revert "Fix issue that stream could be started before stopping when calling identify. (#122)"" This reverts commit 08498127157e038ec3e9d29203d29ea09c326679. * Revert "Revert "rename repo and package name and apply markdown templates (#121)"" This reverts commit bbbeb8103b58764a57fddb7bc72b93608702dbe7. * Fix thread leak on identify call from restarting EventProcessor without shutting it down first. (#123) * Add top level try/catch to migration methods. Check flag version SharedPreferences object for String type before cast. (#124) * Update Throttler to call runnable on background thread. (#125) * Fix ConcurrentModificationException of instance map (#126) Move iteration over client instances for ConnectivityReceiver and PollingUpdater to within LDClient to allow synchronizing on initialization. * adding a circleci badge to the readme (#127) * Fix bug where `stop` in StreamUpdateProcessor could not call it's listener when the stream is already closed. This caused a race condition in repeated stream restarts that could put the SDK in a bad state. * Change LDAwaitFuture to not treat zero timeout as unlimited timeout Treating a timeout of zero as unlimited caused a change in behavior when initializing the SDK. This update restores the behavior init had when zero was passed as the timeout argument from pre-2.8.0. Also improves handling of spurious wakeups, and includes test cases for LDAwaitFuture. * Revert "Merge remote-tracking branch 'remotes/origin/experiment' into next-release" This reverts commit 3ac167fb01c5d6545cf91af7817818e313108f80, reversing changes made to d26e00666a89997c4b548bc54fd82882e518b4bc. * CircleCI fixes (#131) * Better ci fix (#132) * Speedup tests by building on macOS (#137) * Background identify fixes (#133) Add new testing controllers for network and foreground states. For network control, mobile data must be disabled on recent Android versions, updated circleci config to do this. Add new connectivity manager tests. Made EventProcessor and UserManager minimal interfaces for mocking, with actual implementations moved to DefaultEventProcessor and DefaultUserManager. Fixed issue with blocking in background modes. * Experimentation 1.5 updates (#134) * add entire compile-time classpath to javadoc classpath * javadoc fixes:

is not a thing * do fail on javadoc errors * add javadoc step, misc CI cleanup * misc javadoc fixes * remove unintentional(?) immediate event flush; clean up event tests * remove unreliable test assumption about elapsed time * [ch57098] Deprecate LDCountryCode (#141) Deprecate LDCountryCode class and LDUser setters that take LDCountryCode as an argument. * Catch `SecurityException` when setting alarm in case there are already (#143) the maximum allowed number of alarms on Samsung devices. * Revert "[ch57098] Deprecate LDCountryCode (#141)" so we can do a patch release first. This reverts commit c0e71ae1214f6227f2643c467c26bdd1c07ec531. * Revert "Revert "[ch57098] Deprecate LDCountryCode (#141)" so we can do a patch release" This reverts commit 23b930ff0ff503a50af8c0ee4dcb294f688deb82. * Deprecate public classes (#145) * Deprecate some unnecessarily public classes, duplicate classes as non-public to avoid using the deprecated classes. * [ch61092] Add event payload ID. (#147) * Add event retry. (#149) * Fix javadoc comment for release. * Fix broken merge. * [ch65133] Deprecate classes (#150) * Deprecate UserSummaryEventSharedPreferences, SummaryEventSharedPreferences, FeatureFlagFetcher, Util, Debounce. * Improve Javadoc and reduce interface clutter. (#152) * Save Javadoc artifact and include logcat in circle output with tee. (#153) * Save Javadoc artifact on circleci. * Add step to kill emulator after tests, and tee output of logcat for visibility during run. * [ch62120] Background during identify callback (#154) * Adding more connectivity manager tests. * Updated internal `Foreground` class to call listeners on a background thread. * Add some comments explaining the behavior of test controllers. * Adding fixes for cases where the completion callback may not be called. * [ch65914] Diagnostic events (#156) * [ch65352] Expose LDValue rather than Gson types (#158) * Remove SET_ALARM permission. The comment that this was required for background updating is incorrect, this permission is only for sending broadcasts to an alarm clock application, something we do not do, and should never do. (#159) * Fix minimum diagnostic recording interval comment. (#160) * Data since date was not getting reset after each periodic diagnostic event. (#161) * [ch75315] Add maxCachedUsers configuration option (#162) Adds maxCachedUsers configuration option for configuring the limit on how many users have their flags cached locally. * Configure okhttp cache for polling requests to be stored in a subdirectory of the application cache directory. (#164) * Fixes ch76614 and add test of null fallback unknown flag event generation. Also some finishing touches to LDValue changes, including LDClientInterface updates, more tests, and improvements to null behavior handling. (#163) * Removing ldvalue changes before release (#165) * Revert "[ch65352] Expose LDValue rather than Gson types (#158)" This reverts commit 1e29a827 * Fixes after revert. * [ch69437] Support for setting additional headers to be included in requests. (#166) * [ch89933] Improve resiliency of store for summary events. (#167) See launchdarkly/android-client-sdk#105 for the original issue. * [ch94053] Improve throttler behavior. (#169) * Add doubleVariation, doubleVariationDetail. (#171) Deprecates floatVariation, floatVariationDetail. * Provide pollUri configuration and deprecate baseUri. (#172) * Fix throttler behavior to ensure attempt count resets are not cancelled (#178) * [ch98336] Broaden catch statement on scheduling polling alarm (#181) This is to handle more than just the SecurityException that Samsung throws, as we've gotten an issue report that some devices throw a IllegalStateException instead. * Removed the guides link * Include flag key in warning message when converting a json flag to a string (#185) * (2.x) Prevent NullPointerException when diagnostic processor shut down before starting. (#210) * Release 2.14.2 (#130) ## [2.14.2] - 2021-06-02 ### Fixed - Added check to prevent `NullPointerException` in `DiagnosticEventProcessor.stopScheduler` when `LDClient.close` is called before the application is foregrounded when the SDK was initialized in the background. ([#127](https://github.com/launchdarkly/android-client-sdk/issues/127)) - Log message warning that JSON flag was requested as a String has been updated to include the key of the flag requested to assist in discovering which flag is being requested with an unexpected type. ([#116](https://github.com/launchdarkly/android-client-sdk/issues/116)) * Bump version and update changelog for release. * Explicitly specify android:exported attribute on manifest receivers. (#211) * Update java common (#212) * Flag PendingIntent on new enough platforms as the flag is required on Android S+ (#213) * Add try for getting network capabilities (#214) * ch103537 bump java-sdk-common to 1.2 to support inExperiment on eval reason (#215) * Remove `allowBackup` manifest attribute that can conflict with the application's (#217) * Update the version to 2.8.9 * Add explicit proguard directives for keeping BroadcastReceivers. (#219) * Using the version that still support Java 8 but pin the grgit core behind the scene * Remove Android Appcompat dependency (#222) * Bump dependencies and reorganize Gradle file somewhat. (#223) * Add the null check to prevent multiple allocation of the DiagnosticEventProcessor * Fix sonatype release plugin (#226) * Add .ldrelease configuration (#227) * Add contract test service (#228) * Fix test service failing on later API versions (#229) * Add usesCleartextTraffic=true to contract-tests AndroidManifest This allows the contract tests to work on API level 28 and above * Fix start-emulator.sh to pick the newest image instead of the oldest * Refactor CI config into separate jobs with a matrix (#230) * Don't auto-retry emulator tests (#231) * Add contract tests for API level 21 (#232) * Remove unnecessary locking in LDClient (#233) * Remove `synchronized` keywords from every `LDClient` method * Treat `instances` as immutable, and swap out the whole map after constructing all the clients * Use a lock to ensure we don't try to init twice * Update `ConnectivityManager` so it now manages `DiagnosticEventManager` * Run contract tests on Android 31, 33 (#234) * Unsuppress streaming/requests and polling/requests (#236) * don't create a new executor just to trigger a flush * remove short publishing timeout, use defaults of 60 retries & 10 seconds * Serialize null values of `anonymous` as null (#237) * fix URL path concatenation to avoid double slashes * fix NPE in edge case where variation is null but value isn't * use SecureRandom instead of Random, just to make scanners happier * rm unused * fix deletion versioning logic, implement tombstones (#244) * disable contract tests for API 31/33 * use okhttp-eventsource 1.11.3 * ensure timed-out clients get closed in contract tests * clean up instances map on close (#247) * clean up instances map on close * improve atomicity of access to instances, ensure they can't be modified via closed clients * update more methods that iterate over instances * rm unnecessary LDClientControl * use com.launchdarkly.logging with Timber adapter (#235) Co-authored-by: Gavin Whelan Co-authored-by: torchhound Co-authored-by: torchhound Co-authored-by: Arun Bhalla Co-authored-by: jamesthacker Co-authored-by: Gavin Whelan Co-authored-by: torchhound Co-authored-by: Eli Bishop Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: Ben Woskow Co-authored-by: Elliot <35050275+Apache-HB@users.noreply.github.com> Co-authored-by: Robert J. Neal Co-authored-by: Louis Chan Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Co-authored-by: Alex Engelberg Co-authored-by: LaunchDarklyReleaseBot --- contract-tests/build.gradle | 1 - .../launchdarkly/sdktest/MainActivity.java | 17 +- .../sdktest/PrefixedLogAdapter.java | 60 +++++++ .../launchdarkly/sdktest/SdkClientEntity.java | 31 ++-- .../com/launchdarkly/sdktest/TestService.java | 15 +- launchdarkly-android-client-sdk/build.gradle | 12 ++ .../sdk/android/ConnectivityManagerTest.java | 4 +- .../sdk/android/DefaultUserManagerTest.java | 4 +- .../android/DiagnosticEventProcessorTest.java | 14 +- .../sdk/android/LDClientLoggingTest.java | 55 ++++++ .../sdk/android/LDClientTest.java | 3 +- .../SharedPrefsFlagStoreFactoryTest.java | 4 +- .../SharedPrefsFlagStoreManagerTest.java | 4 +- .../sdk/android/SharedPrefsFlagStoreTest.java | 14 +- .../sdk/android/TimberLoggingTest.java | 56 ------ .../sdk/android/ConnectivityManager.java | 17 +- .../sdk/android/DefaultEventProcessor.java | 25 ++- .../sdk/android/DefaultUserManager.java | 41 +++-- .../sdk/android/DiagnosticEventProcessor.java | 16 +- .../sdk/android/DiagnosticStore.java | 5 +- .../launchdarkly/sdk/android/Foreground.java | 16 +- .../sdk/android/HttpFeatureFlagFetcher.java | 33 ++-- .../sdk/android/LDAndroidLogging.java | 117 +++++++++++++ .../launchdarkly/sdk/android/LDClient.java | 84 ++++++--- .../launchdarkly/sdk/android/LDConfig.java | 164 ++++++++++++++++-- .../launchdarkly/sdk/android/LDFutures.java | 4 +- .../sdk/android/LDTimberLogging.java | 112 ++++++++++++ .../com/launchdarkly/sdk/android/LDUtil.java | 28 ++- .../launchdarkly/sdk/android/Migration.java | 15 +- .../sdk/android/PollingUpdater.java | 11 +- .../sdk/android/SharedPrefsFlagStore.java | 7 +- .../android/SharedPrefsFlagStoreFactory.java | 8 +- .../android/SharedPrefsFlagStoreManager.java | 19 +- .../android/SharedPrefsSummaryEventStore.java | 7 +- .../sdk/android/StreamUpdateProcessor.java | 33 ++-- 35 files changed, 812 insertions(+), 244 deletions(-) create mode 100644 contract-tests/src/main/java/com/launchdarkly/sdktest/PrefixedLogAdapter.java create mode 100644 launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java delete mode 100644 launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TimberLoggingTest.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAndroidLogging.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDTimberLogging.java 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