SumoTree is a custom Timber Tree, that uploads your Android logs to SumoLogic.
This version of SumoTree is currently in beta. It works just fine, and I haven't had any issues with it so far, but I still need to do the following:
- Write tests
- Profile it and check for performance issues
- Package and distribute it so we can just add it as a dependency in our Android projects
You'll need to set up a new collector in sumo. Here's more information about that https://help.sumologic.com/Manage/Collection
I named mine "Mobile" and set the Source Category to "dev/mobile". This is just the default that Sumo uses, but SumoTree will overwrite it with whatever you set for sumoCategory which is defaulted to "prod/mobile". This is customizable at any time so you can adjust to "dev/mobile", "preprod/mobile", or whatever you name your environments.
Copy the url for your collector. If you didn't set it up, this can be found in Manage Data > Collection. Then to the right of your collector (Mine is named "Mobile"), click Show URL, then click the Copy button. Hang on to this url. You'll want to add it to local.properties for testing, and to your CI variables.
Add the url you got from the SumoLogic Setup above to local.properties
SUMO_URL="https://endpoint6.collection.us2.sumologic.com/receiver/v1/http/some-really-long-key-here"
Make sure you add this as a CI environment variable as well! Otherwise, SumoTree will only work in local builds.
You probably already have this, but make sure your app can use the internet. Update your AndroidManifest.xml with the following permissions.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
[OPTIONAL] I always add these functions near the top for easily grabbing build variables
val secretsPropertiesFile = rootProject.file("secrets.properties")
val secretProperties = Properties()
if (secretsPropertiesFile.exists()) {
// Try reading secrets from file
secretProperties.load(FileInputStream(secretsPropertiesFile))
} else {
// Otherwise read from environment variables, this happens in CI
secretProperties.setProperty("signing_keystore_password", System.getenv("signing_keystore_password"))
secretProperties.setProperty("signing_key_password", System.getenv("signing_key_password"))
secretProperties.setProperty("signing_key_alias", System.getenv("signing_key_alias"))
}
val localPropertiesFile = rootProject.file("local.properties")
val localProperties = Properties()
if (localPropertiesFile.exists()) {
// Try reading local properties from file
localProperties.load(FileInputStream(localPropertiesFile))
}
fun findPropValue(key: String) : String {
return (localProperties[key] as? String) ?: "\"${System.getenv(key)}\""
}
fun findSecretPropValue(key: String) : String {
return (secretProperties[key] as? String) ?: "\"${System.getenv(key)}\""
}
Grab your SUMO_URL
buildTypes { ... }
buildTypes.forEach {
it.buildConfigField("String", "SUMO_URL", findPropValue("SUMO_URL"))
}
Make sure your app uses a BuildConfig
buildFeatures {
...
buildConfig = true
}
Update your dependencies.
Until this gets packaged and distributed, you'll need to copy all the *.kt files into your project, and add the following dependencies. Sorry for the hassle.
implementation(libs.timber)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.encoding)
I like to use an Application class to hold my sumoTree, but you could also do it from your MainActivity if you don't like having an Application class for some reason.
class MySuperSweetApplication : Application() {
lateinit var sumoTree: SumoTree
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
val appName = BuildConfig.APPLICATION_ID.split(".").lastOrNull() ?: "mysupersweetappName"
val appVersion = BuildConfig.VERSION_NAME
val buildVersion = "${BuildConfig.VERSION_CODE}"
sumoTree = SumoTree(
context = this,
uploadUrl = BuildConfig.SUMO_URL,
sumoName = appName,
additionData = mapOf(
"appVersion" to appVersion,
"buildNumber" to buildVersion
)
)
Thread.setDefaultUncaughtExceptionHandler(sumoTree) // capture and send all crash logs
sumoTree.plant()
}
}
That's it! Now anything you log will get saved to disk, uploaded periodically, and deleted from disk ater successful upload.
Because SumoTree uses Timber, all you have to do is call the globally accessible Timber functions.
Timber.d("debug message")
Timber.i("info message")
Timber.w("warning message")
Timber.e("error message")
SumoLogic recommends the data payload have a size, before compression, of 100KB to 1MB. SumoTree will send log files when they reach 500KB, or when an error message is logged. Whichever comes first.
To pass more information simply call trackAdditionalData. Anything you set in here will be included in every log message.
sumoTree.trackAdditionalData(mapOf( "appVersion" to appVersion, "buildNumber" to buildVersion ))
You may have noticed that the constructor allows you to pass this data as well. We added trackAdditionalData incase you wanted to adjust this data throughtout the liecycle of your app.
You can query SumoLogic with things like
_sourceCategory="prod/mobile"
_sourceHost="android"
_sourceName="mysupersweetappName"
The log messages will look something like this
{
"message":"sumotree test",
"timestamp":"2023-12-19T14:03:01.067-0600",
"logLevel":"ERROR",
"thread":"main",
"tag":"MySuperSweetApplication",
"appVersion":"1.0",
"buildNumber":"1"
}