├── varanus
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── yelp
│ │ │ └── android
│ │ │ └── varanus
│ │ │ ├── shutoff
│ │ │ ├── NetworkShutoffLogPersister.kt
│ │ │ ├── NetworkShutoffLog.kt
│ │ │ ├── CategoryOfTrafficShutoff.kt
│ │ │ └── NetworkShutoffManager.kt
│ │ │ ├── util
│ │ │ └── JobBasedScope.kt
│ │ │ ├── EndpointKeyExtractor.kt
│ │ │ ├── NetworkTrafficLog.kt
│ │ │ ├── NetworkTrafficLogPersister.kt
│ │ │ ├── okhttp
│ │ │ ├── ProgressResponseBody.kt
│ │ │ └── TrafficMonitorInterceptor.kt
│ │ │ ├── NetworkMonitor.kt
│ │ │ ├── LogUploadingManager.kt
│ │ │ └── EndpointSpecificNetworkTracker.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── yelp
│ │ └── android
│ │ └── varanus
│ │ ├── TestClock.kt
│ │ ├── shutoff
│ │ ├── TestConfig.kt
│ │ ├── TestNetworkShutoffLogPersister.kt
│ │ ├── NetworkShutoffManagerTest.kt
│ │ ├── PerEndpointNetworkShutoffManagerTest.kt
│ │ └── GlobalNetworkShutoffManagerTest.kt
│ │ ├── TestLogUploader.kt
│ │ ├── EndpointSpecificNetworkTrackerTest.kt
│ │ └── LogUploadingManagerTest.kt
└── build.gradle
├── VARANUS-LIZARD.png
├── settings.gradle
├── documentation
└── images
│ ├── overview.png
│ ├── network_logging.png
│ ├── network_shutoff.png
│ └── network_shutoff_states.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── sampleapp
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── menu
│ │ │ └── menu_monitor_lizard.xml
│ │ ├── layout
│ │ │ ├── activity_monitor_lizard.xml
│ │ │ └── content_monitor_lizard.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── yelp
│ │ │ └── varanussampleapp
│ │ │ ├── AppEndpointKeyExtractor.kt
│ │ │ ├── persistence
│ │ │ ├── RealmNetworkShutoffLog.kt
│ │ │ ├── RealmNetworkLog.kt
│ │ │ ├── PersistentDataRepo.kt
│ │ │ ├── AppNetworkTrafficLogPersister.kt
│ │ │ └── AppNetworkShutoffLogPersister.kt
│ │ │ ├── LogUploader.kt
│ │ │ ├── FakeCdnInterceptor.kt
│ │ │ ├── MonitorLizardOkhttpClientFactory.kt
│ │ │ └── MonitorLizardActivity.kt
│ │ └── AndroidManifest.xml
├── README.md
├── proguard-rules.pro
└── build.gradle
├── CHANGELOG.md
├── .travis.yml
├── module.gradle
├── gradle.properties
├── LICENSE
├── .gitignore
├── publishing.gradle
├── static_analysis_config
├── checkstyle_config.xml
├── pmd_ruleset_config.xml
└── detekt_config.yml
├── gradlew.bat
├── gradlew
└── README.md
/varanus/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/VARANUS-LIZARD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/VARANUS-LIZARD.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sampleapp'
2 | include ':varanus'
3 | include ':varanus-testing'
4 |
--------------------------------------------------------------------------------
/documentation/images/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/documentation/images/overview.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/documentation/images/network_logging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/documentation/images/network_logging.png
--------------------------------------------------------------------------------
/documentation/images/network_shutoff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/documentation/images/network_shutoff.png
--------------------------------------------------------------------------------
/documentation/images/network_shutoff_states.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/documentation/images/network_shutoff_states.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/android-varanus/HEAD/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Varanus releases
2 |
3 | # Version 3.0.0
4 | * Bump OkHTTP to 4.5.0
5 | * Bump Java to 1.8
6 |
7 | # Version 2.0.2
8 | * First official public Varanus release
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/TestClock.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.google.android.gms.common.util.Clock
4 |
5 | class TestClock : Clock {
6 | var time = 10L
7 | private var nanoTime = 10_000L
8 | override fun currentTimeMillis() = time
9 | override fun elapsedRealtime() = time
10 | override fun currentThreadTimeMillis() = time
11 | override fun nanoTime() = nanoTime
12 | }
13 |
--------------------------------------------------------------------------------
/sampleapp/README.md:
--------------------------------------------------------------------------------
1 | This activity demonstrates the functionality of Varanus as follows.
2 |
3 | The app is a simple game where you feed a monitor lizard three different types of food, of different
4 | sizes. This "food" actually sends network requests of different sizes. No more than every 10
5 | seconds, the monitor lizard "wakes up" and "eats" the food, thus logging how much has been eaten
6 | and displaying it to the user. The amount of food eaten persists.
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/shutoff/TestConfig.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | object TestConfig {
6 |
7 | val config = NetworkShutoffManager.Config(
8 | TimeUnit.MINUTES.toMillis(5),
9 | 8, // 8 * 5 = 40 minute,
10 | 10, // drop 9 in 10 requests when trying again,
11 | 556,
12 | 555,
13 | TimeUnit.MINUTES.toMillis(5))
14 | }
15 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/menu/menu_monitor_lizard.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/shutoff/NetworkShutoffLogPersister.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | /**
4 | * Interface for a persistent database that stores a log of all [EndpointSpecificShutoff] states so
5 | * that if the app is killed the state of the [EndpointSpecificShutoff] can be restored.
6 | */
7 | interface NetworkShutoffLogPersister {
8 |
9 | fun addAndUpdateLog(log: NetworkShutoffLog)
10 |
11 | fun getLog(endpoint: String): NetworkShutoffLog
12 |
13 | fun getAll(): List
14 |
15 | fun clear(endpoint: String)
16 | }
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | dist: trusty
3 |
4 | android:
5 | components:
6 | - tools
7 | - platform-tools
8 | - build-tools-27.0.3
9 | - build-tools-28.0.3
10 | - android-27
11 | - android-28
12 |
13 | before_cache:
14 | - rm -f ${HOME}/.gradle/caches/modules-2/modules-2.lock
15 | - rm -fr ${HOME}/.gradle/caches/*/plugin-resolution/
16 |
17 | cache:
18 | directories:
19 | - ${HOME}/.gradle/caches/
20 | - ${HOME}/.gradle/wrapper/
21 | - ${HOME}/.m2
22 | - ${HOME}/.android/build-cache
23 |
24 | script:
25 | - ./gradlew build
26 | - ./gradlew publishToMavenLocal
27 | - ./gradlew assembleDebug
28 | - ./gradlew check
29 |
--------------------------------------------------------------------------------
/module.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin-android'
2 | apply plugin: 'io.gitlab.arturbosch.detekt'
3 |
4 | detekt {
5 | toolVersion = Versions.DETEKT
6 | input = files("$projectDir/src")
7 | config = files("$rootDir/static_analysis_config/detekt_config.yml")
8 | }
9 |
10 | dependencies {
11 | // Kotlin
12 | implementation Libs.KOTLIN
13 | implementation Libs.KOTLIN_REFLECT
14 | testImplementation TestLibs.KOTLIN_JUNIT
15 | androidTestImplementation TestLibs.KOTLIN_JUNIT
16 |
17 | // Detekt
18 | detekt Libs.DETEKT
19 |
20 | // Mockito-Kotlin
21 | testImplementation TestLibs.MOCKITO_KOTLIN_2
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/TestLogUploader.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.yelp.android.varanus.LogUploadingManager.LogUploaderBase
4 |
5 | class TestLogUploader : LogUploaderBase {
6 | var sentLogs = ArrayList()
7 |
8 | override suspend fun uploadTrafficLogSummaryForInterval(
9 | data: Long,
10 | requests: Int,
11 | time_interval: Long,
12 | endpoint: String) {
13 | sentLogs.add(PeriodicLog(data, requests, endpoint))
14 | }
15 |
16 | data class PeriodicLog(val data: Long, val requests: Int, val endpoint: String)
17 | }
18 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/shutoff/NetworkShutoffLog.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | /**
4 | * The state of [CategoryOfTrafficShutoff] , containing all the information needed to decide
5 | * whether or not to block an endpoint.
6 | *
7 | * @param state The state of CategoryOfTrafficShutoff, which could be Inactive, Shutoff,
8 | * or Attempting.
9 | * @param backoffSize How much we've been backing off.
10 | * @param shutOffUntil How long we've backed off until.
11 | * @param endpoint The key used to decide which EndpointSpecificShutoff to use.
12 | */
13 | data class NetworkShutoffLog(
14 | var state: String,
15 | var backoffSize: Int,
16 | var shutOffUntil: Long,
17 | var endpoint: String
18 | )
19 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/AppEndpointKeyExtractor.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp
2 |
3 | import com.yelp.android.varanus.EndpointKeyExtractor
4 | import okhttp3.Request
5 |
6 | /**
7 | * Based on the network request that was sent, we determine the associate endpoint (or whatever
8 | * other useful category). We keep track of statistics about that endpoint.
9 | */
10 | class AppEndpointKeyExtractor: EndpointKeyExtractor {
11 |
12 | companion object {
13 | const val FOOD_LABEL = 3
14 | const val SERVICE = 2
15 | }
16 |
17 | override fun getEndpoint(request: Request) = request.url.encodedPathSegments[FOOD_LABEL]
18 |
19 | override fun getType(request: Request) = request.url.encodedPathSegments[SERVICE]
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/util/JobBasedScope.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.util
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Job
5 | import kotlin.coroutines.CoroutineContext
6 | import kotlin.coroutines.EmptyCoroutineContext
7 |
8 | /**
9 | * Helper class to make classes CoroutineScoped.
10 | * See here for more of an explanation:
11 | *
12 | * https://discuss.kotlinlang.org/t/simpler-coroutine-scope-creation/10833
13 | */
14 |
15 | class JobBasedScope (
16 | additionalContext: CoroutineContext = EmptyCoroutineContext
17 | ) : CoroutineScopeAndJob {
18 | override val job = Job()
19 | override val coroutineContext: CoroutineContext = job + additionalContext
20 | }
21 |
22 | interface CoroutineScopeAndJob : CoroutineScope {
23 | val job: Job
24 | }
25 |
--------------------------------------------------------------------------------
/sampleapp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/EndpointKeyExtractor.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import okhttp3.Request
4 |
5 | /**
6 | * Implement this to determine how to extract the endpoint name and endpoint type according to the
7 | * needs of your application.
8 | *
9 | * We use the term "Endpoint" because that is how we divide up our traffic, but you can really use
10 | * any arbitrary way of categorizing traffic (or always return the same thing if you only care
11 | * about traffic globally).
12 | */
13 | interface EndpointKeyExtractor {
14 | /**
15 | * The key that you'll use throughout the network monitor for tracking traffic to an endpoint
16 | * or other category.
17 | */
18 | fun getEndpoint(request: Request): String
19 |
20 | /**
21 | * This can be used to make decisions about categories of endpoints.
22 | */
23 | fun getType(request: Request): String
24 | }
25 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/persistence/RealmNetworkShutoffLog.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp.persistence
2 |
3 | import com.yelp.android.varanus.shutoff.NetworkShutoffLog
4 | import io.realm.RealmObject
5 | import io.realm.annotations.PrimaryKey
6 |
7 | /**
8 | * Used by the EndpointSpecificShutoff to record the state of the endpoint, which decides whether
9 | * the endpoint should be blocked or not.
10 | */
11 | open class RealmNetworkShutoffLog (
12 | // Realm requires a default empty constructor and so everything must have a default value
13 | var state: String = "SENDING",
14 | var backoffSize: Int = 1,
15 | var shutOffUntil: Long = 0,
16 | @PrimaryKey var endpoint: String = "stub"
17 | ) : RealmObject() {
18 | constructor(log: NetworkShutoffLog) : this(log.state,
19 | log.backoffSize,
20 | log.shutOffUntil,
21 | log.endpoint)
22 | }
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 | SNAPSHOT_SONATYPE_URL = https\://oss.sonatype.org/content/repositories/snapshots
20 | RELEASE_SONATYPE_URL = https\://oss.sonatype.org/service/local/staging/deploy/maven2/
21 | android.useAndroidX=true
22 | android.enableJetifier=true
23 |
--------------------------------------------------------------------------------
/sampleapp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/persistence/RealmNetworkLog.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp.persistence
2 |
3 | import com.yelp.android.varanus.NetworkTrafficLog
4 | import io.realm.RealmObject
5 | import io.realm.annotations.PrimaryKey
6 | import java.util.UUID
7 |
8 | /**
9 | * Used by the traffic monitor to record that an amount of network traffic has been sent at a
10 | * particular time.
11 | *
12 | * In general, there will be one entry per request. However, we delete these quite aggressively.
13 | */
14 | open class RealmNetworkLog(
15 | // Realm requires a default empty constructor and so everything must have a default value
16 | var isRequest: Boolean = false,
17 | var endpoint: String? = null,
18 | var size: Long = 0,
19 | var time: Long = 0,
20 | @PrimaryKey protected var id: String = UUID.randomUUID().toString()
21 | ) : RealmObject() {
22 |
23 | constructor(log: NetworkTrafficLog) : this(log.isRequest,
24 | log.endpoint,
25 | log.size,
26 | log.date.time)
27 | }
28 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/NetworkTrafficLog.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import java.util.Date
4 |
5 | /**
6 | * A summary of a network request or response, containing all the information needed to decide
7 | * whether or not to send an alert.
8 | *
9 | * @param isRequest True means a request, false means a response. We may treat these differently,
10 | * e.g. counting only the number of requests but not the number of responses to avoid
11 | * double-counting.
12 | * @param endpoint The key used to decide which [EndpointSpecificNetworkTracker] to use.
13 | * @param endpointType An optional additional label for further categorizing endpoints.
14 | * @param size Total number of bytes sent over the wire, or as close as we can measure.
15 | * @param count Total number of requests.
16 | * @date date The time, at least approximately, that it was sent.
17 | */
18 | data class NetworkTrafficLog(
19 | var isRequest: Boolean,
20 | var endpoint: String,
21 | var endpointType: String,
22 | var size: Long,
23 | var count: Int = 1,
24 | var date: Date = Date())
25 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/activity_monitor_lizard.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Yelp Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 | Varanus includes or links to the following unmodified packages, which are
16 | subject to separate licenses:
17 | - [Kotlin](https://github.com/JetBrains/kotlin) licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
18 | - [JUnit](https://github.com/junit-team/junit4) licensed under [Eclipse Public License 1.0](https://www.eclipse.org/legal/epl-v10.html)
19 | - [Mockito](https://github.com/mockito/mockito) licensed under [The MIT License](https://opensource.org/licenses/MIT)
20 | - [Gradle](https://github.com/gradle/gradle) licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
21 |
22 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/persistence/PersistentDataRepo.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp.persistence
2 |
3 | import android.content.Context
4 | import io.realm.Realm
5 | import io.realm.RealmConfiguration
6 |
7 | class PersistentDataRepo(val context: Context) {
8 | companion object {
9 | const val REALM_SCHEMA_VERSION = 0L
10 |
11 | val persistentRepo : PersistentDataRepo? = null
12 |
13 | fun getPersistentRepo(context: Context) : PersistentDataRepo {
14 | return persistentRepo ?: PersistentDataRepo(context)
15 |
16 | }
17 | }
18 | val realmConfig: RealmConfiguration
19 | val networkShutoffLogPersister : AppNetworkShutoffLogPersister
20 | val networkTrafficLogPersister : AppNetworkTrafficLogPersister
21 |
22 | init {
23 | // Make sure to add a migration and associated tests if this is ever updated
24 | Realm.init(context)
25 | realmConfig = RealmConfiguration.Builder().schemaVersion(REALM_SCHEMA_VERSION).build()
26 | networkShutoffLogPersister = AppNetworkShutoffLogPersister(realmConfig)
27 | networkTrafficLogPersister = AppNetworkTrafficLogPersister(realmConfig)
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/shutoff/TestNetworkShutoffLogPersister.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | /**
4 | * Used to to test network shutoff.
5 | */
6 | class TestNetworkShutoffLogPersister : NetworkShutoffLogPersister {
7 |
8 | private val logs = HashMap()
9 |
10 | override fun getAll(): List {
11 |
12 | return logs.keys.toList()
13 | }
14 |
15 | /**
16 | * Delete the entry whose key is the entrypoint.
17 | *
18 | * @param endpoint Key for the endpoint type we're deleting data for.
19 | */
20 | override fun clear(endpoint: String) {
21 | }
22 |
23 | /**
24 | * @param endpoint The key used to store this network request.
25 | * @return the log whos key is the endpoint.
26 | */
27 | override fun getLog(endpoint: String): NetworkShutoffLog {
28 | return logs[endpoint] ?: NetworkShutoffLog("SENDING", 1, 0L, endpoint)
29 | }
30 |
31 | /**
32 | * Save a log of [EndpointSpecificShutoff] to realm.
33 | *
34 | * @param log The log to save to Realm.
35 | */
36 | override fun addAndUpdateLog(log: NetworkShutoffLog) {
37 | logs[log.endpoint] = log
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Feed the monitor lizard
3 | Settings
4 | Feed insects
5 | Feed fruit
6 | Feed fish
7 |
8 | Feed the lizard, but usually it\'s sleeping. Every 10 seconds, it\'ll
9 | wake up and eat what you fed it.
10 | Insects eaten: %1$d
11 | Insect weight eaten: %1$d
12 | Fruit eaten: %1$d
13 | Fruit weight eaten: %1$d
14 | Fish eaten: %1$d
15 | Fish weight eaten: %1$d
16 | Total eaten: %1$d
17 | Total weight eaten: %1$d
18 |
19 | The monitor lizard is happy
20 | Oh no, you fed the monitor lizard too much!
21 | Oh no, you fed the monitor lizard too much %1$s!
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea
42 |
43 | # Keystore files
44 | # Uncomment the following lines if you do not want to check your keystore files in.
45 | #*.jks
46 | #*.keystore
47 |
48 | # External native build folder generated in Android Studio 2.2 and later
49 | .externalNativeBuild
50 | .cxx/
51 |
52 | # Google Services (e.g. APIs or Firebase)
53 | # google-services.json
54 |
55 | # Freeline
56 | freeline.py
57 | freeline/
58 | freeline_project_description.json
59 |
60 | # fastlane
61 | fastlane/report.xml
62 | fastlane/Preview.html
63 | fastlane/screenshots
64 | fastlane/test_output
65 | fastlane/readme.md
66 |
67 | # Version control
68 | vcs.xml
69 |
70 | # lint
71 | lint/intermediates/
72 | lint/generated/
73 | lint/outputs/
74 | lint/tmp/
75 | # lint/reports/
--------------------------------------------------------------------------------
/sampleapp/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'realm-android'
5 | apply from: '../module.gradle'
6 |
7 | android {
8 | compileSdkVersion 28
9 |
10 | defaultConfig {
11 | applicationId "com.yelp.varanussampleapp"
12 | minSdkVersion 28
13 | targetSdkVersion 28
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 |
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 |
28 | compileOptions {
29 | targetCompatibility = Versions.TARGET_COMPATIBILITY
30 | sourceCompatibility = Versions.SOURCE_COMPATIBILITY
31 | }
32 |
33 | kotlinOptions {
34 | jvmTarget = Versions.TARGET_COMPATIBILITY
35 | }
36 |
37 | useLibrary 'android.test.runner'
38 | }
39 |
40 | dependencies {
41 | implementation Libs.CONSTRAINTLAYOUT
42 | implementation Libs.COROUTINES
43 | implementation Libs.DESIGN
44 | implementation Libs.KOTLIN
45 | implementation Libs.OKHTTP
46 | implementation Libs.OKIO
47 | implementation Libs.REALM
48 | implementation Libs.APPCOMPAT
49 | implementation PlayServicesLibs.BASEMENT
50 | implementation project(':varanus')
51 |
52 | testImplementation TestLibs.JUNIT
53 |
54 | androidTestImplementation TestLibs.ESPRESSO
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/LogUploader.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp
2 |
3 | import com.yelp.android.varanus.LogUploadingManager
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.GlobalScope
6 | import kotlinx.coroutines.launch
7 |
8 | /**
9 | * You would implement a similar class to send logs somewhere of what your network traffic is doing.
10 | *
11 | * In this case, we are displaying these summaries in the app, but in reality, you probably want
12 | * to send them to a server somewhere with some sort of logging or analytics.
13 | */
14 | class LogUploader(
15 | private val activity: MonitorLizardActivity
16 | ) : LogUploadingManager.LogUploaderBase {
17 |
18 | val foodStats = mapOf(
19 | "fruit" to Counter(),
20 | "insect" to Counter(),
21 | "fish" to Counter(),
22 | "total" to Counter()
23 | )
24 |
25 | init {
26 | activity.updateText(foodStats)
27 | }
28 |
29 | override suspend fun uploadTrafficLogSummaryForInterval(
30 | data: Long,
31 | requests: Int,
32 | time_interval: Long,
33 | endpoint: String
34 | ) {
35 | foodStats[endpoint]?.apply{ addCount(requests) }?.apply { addSize(data) }
36 | GlobalScope.launch(Dispatchers.Main) {
37 | activity.updateText(foodStats)
38 | }
39 | }
40 |
41 |
42 | data class Counter(var count : Int = 0, var size : Long = 0) {
43 | fun addCount(newCount: Int) {
44 | count += newCount
45 | }
46 | fun addSize(newSize: Long) {
47 | size += newSize
48 | }
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/NetworkTrafficLogPersister.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | /**
4 | * Interface for a persistent database that stores a log of all network requests so that if the
5 | * app is killed the state of the [EndpointSpecificNetworkTracker] can be restored.
6 | */
7 | interface NetworkTrafficLogPersister {
8 |
9 | /**
10 | * Save the networkTrafficLog persistently.
11 | *
12 | * The database should retain all of this data, especially the endpoint key, as that will be
13 | * used to fetch the relevant data.
14 | */
15 | fun addLog(log: NetworkTrafficLog)
16 |
17 | /**
18 | * On startup, use this to fetch the amount of data and number of requests over the previous
19 | * time period.
20 | *
21 | * @param windowLength Time period we look back over to count up the number of relevant
22 | * requests.
23 | * @param endpoint Key used to assign data to the appropriate EndpointSpecificNetworkTracker.
24 | */
25 | fun getSizeAndClear(windowLength: Long, endpoint: String): TrafficLogSummary
26 |
27 | /**
28 | * Trim the size of our persistent database.
29 | *
30 | * @param windowLength Time period we look back over to count up the number of relevant
31 | * requests.
32 | * @param endpoint Key used to assign data to the appropriate EndpointSpecificNetworkTracker.
33 | */
34 | fun clear(windowLength: Long, endpoint: String)
35 |
36 | /**
37 | * Summary of the total number of requests and amount of data that has been sent over the
38 | * time period in question.
39 | */
40 | data class TrafficLogSummary(val count: Int, val size: Long)
41 | }
42 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/EndpointSpecificNetworkTrackerTest.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.yelp.android.varanus.LogUploadingManager.LogUploaderBase
4 | import com.yelp.android.varanus.NetworkTrafficLogPersister.TrafficLogSummary
5 | import org.junit.Test
6 | import java.util.concurrent.TimeUnit
7 | import kotlin.test.assertEquals
8 |
9 | class EndpointSpecificNetworkTrackerTest {
10 |
11 | private val networkTrafficPersister = TestNetworkTrafficLogPersister()
12 | private val alertIssuer = TestLogUploader()
13 | private val window = TimeUnit.MINUTES.toMillis(5)
14 |
15 | @Test
16 | fun testTracksRequests() {
17 | testTracksRequestsResponses(true, 5, 50L)
18 | }
19 |
20 | @Test
21 | fun testTrackResponses() {
22 | testTracksRequestsResponses(false, 0, 50L)
23 | }
24 |
25 | private fun testTracksRequestsResponses(
26 | isRequest: Boolean,
27 | expectedCount: Int,
28 | expectedSize: Long
29 | ) {
30 | val tracker = EndpointSpecificNetworkTracker("test",
31 | TimeUnit.SECONDS.toMillis(5),
32 | networkTrafficPersister,
33 | LogUploadingManager(alertIssuer, window, 5L))
34 |
35 | for (i in 1..5) {
36 | tracker.addLogAndPersist(NetworkTrafficLog(isRequest, "test", "test", 10))
37 | }
38 | assertEquals(tracker.requestCount.get(), expectedCount + 1)
39 | assertEquals(tracker.requestSize.get(), expectedSize + 1)
40 | }
41 |
42 |
43 | class TestNetworkTrafficLogPersister : NetworkTrafficLogPersister {
44 | override fun getSizeAndClear(windowLength: Long, endpoint: String): TrafficLogSummary {
45 | return TrafficLogSummary(1, 1)
46 | }
47 | override fun clear(windowLength: Long, endpoint: String) {}
48 |
49 | override fun addLog(log: NetworkTrafficLog) {}
50 | }
51 |
52 | class TestLogUploader : LogUploaderBase {
53 | var counter = 0
54 |
55 | override suspend fun uploadTrafficLogSummaryForInterval(
56 | data: Long,
57 | requests: Int,
58 | time_interval: Long,
59 | endpoint: String
60 | ) {
61 | counter++
62 | }
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/okhttp/ProgressResponseBody.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.okhttp
2 |
3 | import okhttp3.ResponseBody
4 | import okio.Buffer
5 | import okio.BufferedSource
6 | import okio.ForwardingSource
7 | import okio.Source
8 | import okio.buffer
9 |
10 | /**
11 | * This is used by [TrafficMonitorInterceptor] to keep track of statistics on the network responses
12 | * received as a result of requests sent.
13 | */
14 | class ProgressResponseBody(
15 | private val responseBody: ResponseBody,
16 | progressListener: ProgressListener
17 | ) : ResponseBody() {
18 |
19 | private val bufferedSource: BufferedSource =
20 | ProgressTrackingSource(responseBody.source(), progressListener).buffer()
21 |
22 | override fun contentType() = responseBody.contentType()
23 |
24 | override fun source() = bufferedSource
25 |
26 | override fun contentLength() = responseBody.contentLength()
27 |
28 | /**
29 | * This should be extended by something which records and then logs (or otherwise acts on)
30 | * the size of the response.
31 | */
32 | interface ProgressListener {
33 | /**
34 | * Called whenever there's more data in a request.
35 | *
36 | * @param bytesRead the amount of data in this batch (not the entire response).
37 | */
38 | fun update(bytesRead: Long)
39 |
40 | /**
41 | * Called when all data has been received.
42 | */
43 | fun done()
44 | }
45 |
46 | /**
47 | * Helper class for tracking the response size.
48 | *
49 | * @param delegate the [ResponseBody] source, needed by the superclass.
50 | * @param progressListener class which is updated with the progress of how much has been
51 | * downloaded.
52 | */
53 | class ProgressTrackingSource(delegate: Source, private val progressListener: ProgressListener) :
54 | ForwardingSource(delegate) {
55 |
56 | override fun read(sink: Buffer, byteCount: Long): Long {
57 | val bytesRead = super.read(sink, byteCount)
58 | if (bytesRead == -1L) {
59 | progressListener.done()
60 | } else {
61 | progressListener.update(bytesRead)
62 | }
63 | return bytesRead
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/shutoff/NetworkShutoffManagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | import com.yelp.android.varanus.TestClock
4 | import okhttp3.Protocol
5 | import okhttp3.Request
6 | import okhttp3.Response
7 | import org.junit.Before
8 | import java.util.concurrent.TimeUnit
9 | import kotlin.test.assertEquals
10 |
11 | /**
12 | * Common code between [GlobalNetworkShutoffManagerTest] and [PerEndpointNetworkShutoffManagerTest].
13 | */
14 | abstract class NetworkShutoffManagerTest {
15 |
16 | companion object {
17 | internal const val GLOBAL_FAIL_CODE = 556
18 | internal const val ENDPOINT_FAIL_CODE = 555
19 | internal const val SUCCESS_CODE = 200
20 | internal const val DEFAULT_ENDPOINT = "test"
21 | internal const val ALTERNATE_ENDPOINT = "test2"
22 | }
23 |
24 | internal val defaultRequest = Request.Builder().url("https://www.yelp.com").build()
25 | internal lateinit var testClock: TestClock
26 | internal lateinit var networkShutoffManager: NetworkShutoffManager
27 |
28 | private val config = TestConfig.config
29 |
30 | @Before
31 | fun setUp() {
32 | testClock = TestClock()
33 | networkShutoffManager =
34 | NetworkShutoffManager(testClock,
35 | TestRandomizer(config),
36 | TestNetworkShutoffLogPersister(),
37 | config)
38 | }
39 |
40 | internal fun setErrorCode(code: Int, endpoint: String = DEFAULT_ENDPOINT) {
41 | val response = Response.Builder().request(defaultRequest).message("test")
42 | .protocol(Protocol.HTTP_2).code(code).build()
43 | networkShutoffManager.determineShutoffStatusFromRequest(response, endpoint)
44 | }
45 |
46 | internal fun checkIfDrop(endpoint: String, expected: Boolean) {
47 | val shouldDrop = networkShutoffManager.shouldDropRequest(endpoint)
48 | assertEquals(expected, shouldDrop)
49 | }
50 |
51 | internal fun setClockToNextInterval() {
52 | testClock.time += config.backoffIncrement + 1
53 | }
54 |
55 | class TestRandomizer(shutoffConfig: NetworkShutoffManager.Config
56 | ) : NetworkShutoffManager.Randomizer(shutoffConfig) {
57 | override fun randomizeTime(time: Long) = time
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/FakeCdnInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp
2 |
3 | import com.yelp.android.varanus.EndpointKeyExtractor
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 | import okhttp3.Request
7 | import okhttp3.ResponseBody
8 | import okhttp3.Protocol
9 | import okhttp3.MediaType
10 | import okhttp3.MediaType.Companion.toMediaType
11 |
12 | private const val SPECIFIC_ENDPOINT_ERROR_CODE = 555
13 | private const val ALL_ENDPOINT_ERROR_CODE = 556
14 |
15 | // Arbitrarily chosen numbers for demonstration purposes
16 | private const val TOO_MANY_REQUESTS_FROM_ONE_ENDPOINT = 10
17 | private const val TOO_MANY_REQUESTS_FROM_ALL_ENDPOINTS = 30
18 |
19 | /**
20 | * This simulates the functionality of a CDN which you can configure to turn off traffic.
21 | *
22 | * In a real implementation, this should a) be a real CDN and b) be something that you do manually.
23 | * If you don't have a CDN, you would do this in the backend somehow. This is something you want
24 | * to talk to your Production Engineering/Operations/etc team about.
25 | *
26 | * (https://en.wikipedia.org/wiki/Content_delivery_network)
27 | */
28 | class FakeCdnInterceptor(
29 | private val endpointKeyExtractor: EndpointKeyExtractor
30 | ) : Interceptor {
31 |
32 | private val requestCount = mutableMapOf()
33 |
34 | override fun intercept(chain: Interceptor.Chain): Response {
35 | val request = chain.request()
36 | val endpoint = endpointKeyExtractor.getEndpoint(request)
37 | requestCount[endpoint] = (requestCount[endpoint] ?: 0) + 1
38 |
39 | if (requestCount[endpoint]?: 0 > TOO_MANY_REQUESTS_FROM_ONE_ENDPOINT) {
40 | return makeErrorResponse(request, SPECIFIC_ENDPOINT_ERROR_CODE)
41 | }
42 |
43 | if (requestCount.map{it.value}.sum() > TOO_MANY_REQUESTS_FROM_ALL_ENDPOINTS) {
44 | return makeErrorResponse(request, ALL_ENDPOINT_ERROR_CODE)
45 | }
46 |
47 | return chain.proceed(request)
48 | }
49 |
50 | private fun makeErrorResponse(request: Request, code: Int): Response {
51 | return Response.Builder()
52 | .code(code)
53 | .request(request)
54 | .message("oh no") // not used, but okhttp expects this to be non-null
55 | .body(ResponseBody.create("text/plain".toMediaType(), "oh no")) // not used
56 | .protocol(Protocol.HTTP_2)
57 | .build()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/varanus/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | mavenCentral()
6 | mavenLocal()
7 | maven {
8 | url "https://plugins.gradle.org/m2/"
9 | }
10 | }
11 | dependencies {
12 | classpath BuildScriptLibs.ANDROID
13 | classpath BuildScriptLibs.DETEKT
14 | classpath BuildScriptLibs.KOTLIN
15 | classpath BuildScriptLibs.ANDROID_CHECK
16 | classpath PublishLibs.MAVEN_PUBLISH
17 | classpath PublishLibs.MAVEN_SETTINGS
18 | }
19 | }
20 |
21 | ext.projectName = 'Varanus'
22 | ext.projectDescription = 'A client-side Android library to monitor and limit network traffic sent by your apps'
23 |
24 | apply plugin: "com.android.library"
25 | apply plugin: 'kotlin-android'
26 | apply plugin: 'kotlin-android-extensions'
27 | apply plugin: 'com.noveogroup.android.check'
28 | apply plugin: 'findbugs'
29 | apply from: '../module.gradle'
30 | apply from: '../publishing.gradle'
31 |
32 | dependencies {
33 | implementation Libs.COROUTINES
34 | implementation Libs.OKHTTP
35 |
36 | detekt Libs.DETEKT
37 | implementation PlayServicesLibs.BASEMENT
38 |
39 | /** Testing dependencies */
40 | testImplementation TestLibs.JUNIT
41 | testImplementation TestLibs.MOCKITO_CORE
42 | }
43 |
44 | android {
45 | compileSdkVersion Versions.COMPILE_SDK
46 |
47 | defaultConfig {
48 | minSdkVersion Versions.MIN_SDK
49 | targetSdkVersion Versions.TARGET_SDK
50 | versionCode 1
51 | versionName "1.0"
52 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
53 | }
54 |
55 | buildTypes {
56 | release {
57 | minifyEnabled false
58 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
59 | }
60 | }
61 |
62 | compileOptions {
63 | targetCompatibility = Versions.TARGET_COMPATIBILITY
64 | sourceCompatibility = Versions.SOURCE_COMPATIBILITY
65 | }
66 |
67 | kotlinOptions {
68 | jvmTarget = Versions.TARGET_COMPATIBILITY
69 | }
70 | }
71 |
72 | check {
73 | abortOnError true
74 |
75 | pmd {
76 | config '../static_analysis_config/pmd_ruleset_config.xml'
77 | }
78 |
79 | checkstyle {
80 | config '../static_analysis_config/checkstyle_config.xml'
81 | }
82 |
83 | // TODO enable this, see https://github.com/Yelp/android-varanus/issues/2
84 | findbugs {
85 | skip true
86 | }
87 | }
88 |
89 | configurations {
90 | detekt
91 | }
92 |
93 | dependencies {
94 | detekt
95 | }
96 |
--------------------------------------------------------------------------------
/publishing.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'signing'
2 | apply plugin: 'digital.wup.android-maven-publish'
3 | apply plugin: 'net.linguica.maven-settings'
4 | apply plugin: 'maven-publish'
5 |
6 | group Publishing.GROUP
7 | version Publishing.VERSION
8 |
9 | task sourcesJar(type: Jar) {
10 | classifier = 'sources'
11 | from android.sourceSets.main.java.srcDirs
12 | }
13 |
14 | publishing {
15 | publications {
16 |
17 | mavenAar(MavenPublication) {
18 | from components.android
19 | groupId = Publishing.GROUP
20 | version = Publishing.VERSION
21 | artifact sourcesJar
22 |
23 | pom {
24 | name = projectName
25 | description = projectDescription
26 | url = 'https://github.com/Yelp/android-varanus'
27 | licenses {
28 | license {
29 | name = 'The Apache License, Version 2.0'
30 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
31 | }
32 | }
33 | developers {
34 | developer {
35 | name = 'Yelp'
36 | email = '?'
37 | }
38 |
39 | developer {
40 | id = 'sanae'
41 | name = 'Sanae Rosen'
42 | email = 'sanae@yelp.com'
43 | }
44 | }
45 | scm {
46 | connection = 'scm:git:git@github.com:Yelp/android-varanus.git'
47 | developerConnection = 'scm:git:git@github.com:Yelp/android-varanus.git'
48 | url = 'https://github.com/Yelp/android-varanus'
49 | }
50 | }
51 | }
52 | }
53 | repositories {
54 | maven {
55 | if (isReleaseBuild()) {
56 | name "SonatypeRelease"
57 | url "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
58 | } else {
59 | name "SonatypeSnapshot"
60 | url "https://oss.sonatype.org/content/repositories/snapshots"
61 | }
62 | if (project.hasProperty("signing.keyId")) {
63 | credentials {
64 | username = ossrhUsername
65 | password = ossrhPassword
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | // Allows people to publish to their local maven even if they don't have signing keys.
73 | signing {
74 | required { project.hasProperty("signing.keyId") }
75 | sign publishing.publications.mavenAar
76 | }
77 |
78 | def isReleaseBuild() {
79 | return !Publishing.VERSION.endsWith("-SNAPSHOT")
80 | }
81 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/persistence/AppNetworkTrafficLogPersister.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp.persistence
2 |
3 | import com.yelp.android.varanus.NetworkTrafficLog
4 | import com.yelp.android.varanus.NetworkTrafficLogPersister
5 | import io.realm.Realm
6 | import io.realm.RealmConfiguration
7 | import io.realm.RealmResults
8 |
9 | class AppNetworkTrafficLogPersister(
10 | private val realmConfig: RealmConfiguration
11 | ) : NetworkTrafficLogPersister {
12 |
13 | /**
14 | * Delete all data that is from before the specified time window
15 | *
16 | * @param windowLength Window before now for which we should retain data
17 | * @param endpoint Key for the endpoint type we're deleting data for
18 | */
19 | override fun clear(windowLength: Long, endpoint: String) {
20 | val earliestTime = System.currentTimeMillis() - windowLength
21 | Realm.getInstance(realmConfig).use { realm ->
22 | realm.executeTransaction {
23 | it.where(RealmNetworkLog::class.java)
24 | .lessThan("time", earliestTime)
25 | .findAll()
26 | .deleteAllFromRealm()
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Determine the number of requests and amount of data sent over the last time window.
33 | * Then, delete all data that falls outside that window.
34 | *
35 | * @param windowLength The amount of time in the past for which to retain log data
36 | * @param endpoint The key used to store this network request
37 | */
38 | override fun getSizeAndClear(
39 | windowLength: Long,
40 | endpoint: String
41 | ): NetworkTrafficLogPersister.TrafficLogSummary {
42 |
43 | var count = 0
44 | var size = 0L
45 | Realm.getInstance(realmConfig).use { realm ->
46 | val earliestTime = System.currentTimeMillis() - windowLength
47 | val logs: RealmResults =
48 | realm.where(RealmNetworkLog::class.java)
49 | .equalTo("endpoint", endpoint)
50 | .greaterThan("time", earliestTime)
51 | .findAll()
52 | count = logs.size
53 | size = logs.sum("size").toLong()
54 | }
55 |
56 | clear(windowLength, endpoint)
57 | return NetworkTrafficLogPersister.TrafficLogSummary(count, size)
58 | }
59 |
60 | /**
61 | * Save a log about a network request to Realm.
62 | *
63 | * @param log The log to save to Realm.
64 | */
65 | override fun addLog(log: NetworkTrafficLog) {
66 | Realm.getInstance(realmConfig).use { realm ->
67 | realm.executeTransaction {
68 | it.insertOrUpdate(RealmNetworkLog(log))
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/NetworkMonitor.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.yelp.android.varanus.LogUploadingManager.LogUploaderBase
4 | import java.util.concurrent.ConcurrentHashMap
5 | import kotlin.collections.HashMap
6 |
7 | /**
8 | * Manages keeping track of network traffic trends using many different
9 | * [EndpointSpecificNetworkTracker]s to keep track of different categories of traffic.
10 | *
11 | * The actual work of tracking traffic is done by the [EndpointSpecificNetworkTracker]s, which use
12 | * the [LogUploaderBase] to send alerts if needed. This class serves mainly to manage the
13 | * [EndpointSpecificNetworkTracker]s and delegate logging to the appropriate one accordingly.
14 | *
15 | * @param windowLength Our window in time for
16 | * @param persister Interacts with the app's mechanism of persisting data to ensure
17 | * that state is maintained between app start-ups.
18 | * @param alertIssuer Interacts with the app's method of sending alerts or logs to a server.
19 | */
20 | class NetworkMonitor(
21 | private val windowLength: Long,
22 | clear_increment: Long,
23 | private val persister: NetworkTrafficLogPersister,
24 | alertIssuer: LogUploaderBase
25 | ) {
26 | private val networkTrafficAlerter =
27 | LogUploadingManager(alertIssuer, windowLength, clear_increment)
28 | private var endpoints = ConcurrentHashMap()
29 |
30 |
31 | /**
32 | * Every time there's a request, increment the size and count of the corresponding endpoint
33 | * as well as the tracker that tracks the total across all endpoints.
34 | *
35 | * Creates a new endpoint-specific tracker if needed, and calls that tracker to add a log
36 | * and check if an alert needs to be sent.
37 | *
38 | * @param log Abstraction of a network request with the size and an endpoint key.
39 | */
40 | suspend fun addLog(log: NetworkTrafficLog) {
41 | val newEndpoint = EndpointSpecificNetworkTracker(
42 | log.endpoint,
43 | windowLength,
44 | persister,
45 | networkTrafficAlerter)
46 |
47 | // check if you need to replace first for slightly added efficiency
48 | var endpointTracker = endpoints[log.endpoint]
49 | endpointTracker = endpointTracker ?: endpoints.putIfAbsent(log.endpoint, newEndpoint)
50 | endpointTracker = endpointTracker ?: newEndpoint
51 |
52 | /* We might lose logs if things aren't initialized yet but in any reasonable scenario one
53 | would worry about, the problem will persist long enough to send logs later.
54 | We decided that the small risk of an extremely unusual problem going unnoticed is
55 | outweighed by the performance hit of initializing the network monitor synchronously. */
56 | endpointTracker.addLogAndPersist(log)
57 | networkTrafficAlerter.registerLogs(endpoints)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/static_analysis_config/checkstyle_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/MonitorLizardOkhttpClientFactory.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp
2 |
3 | import com.google.android.gms.common.util.DefaultClock
4 | import com.yelp.android.varanus.NetworkMonitor
5 | import com.yelp.android.varanus.okhttp.TrafficMonitorInterceptor
6 | import com.yelp.android.varanus.shutoff.NetworkShutoffManager
7 | import com.yelp.varanussampleapp.persistence.PersistentDataRepo
8 | import okhttp3.OkHttpClient
9 | import java.util.concurrent.TimeUnit
10 |
11 | private const val SPECIFIC_ENDPOINT_ERROR_CODE = 555
12 | private const val ALL_ENDPOINT_ERROR_CODE = 556
13 | private const val MAX_BACKOFF_MULTIPLYER = 12 // 12 * 5 = 1 minute
14 | private const val THROTTLE_AMOUNT = 10 // drop 9 in 10 requests when trying again
15 | private const val WINDOW_LENGTH = 5L // seconds
16 | private const val BACKOFF_SPREAD = 2L // seconds
17 |
18 | /**
19 | * In order to support any network library you might use, we have pulled out the OkHttp related
20 | * content.
21 | *
22 | * We set up the OkHttp client, instantiate the network monitoring classes, then, using an
23 | * interceptor, set all traffic to go through Varanus.
24 | *
25 | * Varanus works particularly well with OkHttp because of OkHttp's interceptor support, but in
26 | * principle you could implement something similar with any library.
27 | */
28 | object MonitorLizardOkhttpClientFactory {
29 |
30 | fun configureOkhttpClient(activity: MonitorLizardActivity, alertIssuer: LogUploader):
31 | OkHttpClient {
32 |
33 | val persistentDataRepo = PersistentDataRepo.getPersistentRepo(activity)
34 |
35 | val shutoffConfig = NetworkShutoffManager.Config(
36 | TimeUnit.SECONDS.toMillis(WINDOW_LENGTH), // Probably should be minutes in real life
37 | MAX_BACKOFF_MULTIPLYER,
38 | THROTTLE_AMOUNT,
39 | SPECIFIC_ENDPOINT_ERROR_CODE,
40 | ALL_ENDPOINT_ERROR_CODE,
41 | TimeUnit.SECONDS.toMillis(BACKOFF_SPREAD))
42 |
43 | val networkMonitor = NetworkMonitor(
44 | TimeUnit.SECONDS.toMillis(WINDOW_LENGTH),
45 | TimeUnit.SECONDS.toMillis(WINDOW_LENGTH),
46 | persistentDataRepo.networkTrafficLogPersister,
47 | alertIssuer
48 | )
49 |
50 | val networkShutoffManager = NetworkShutoffManager(
51 | DefaultClock.getInstance(), // This exists for testing reasons
52 | NetworkShutoffManager.Randomizer(shutoffConfig), // This exists for testing reasons
53 | persistentDataRepo.networkShutoffLogPersister,
54 | shutoffConfig
55 | )
56 | activity.shutoffManager = networkShutoffManager // So that we can print the status
57 |
58 | val endpointKeyExtractor = AppEndpointKeyExtractor()
59 |
60 | return OkHttpClient.Builder().addInterceptor(
61 | TrafficMonitorInterceptor(
62 | networkMonitor,
63 | endpointKeyExtractor,
64 | networkShutoffManager
65 | )).addInterceptor(FakeCdnInterceptor(endpointKeyExtractor))
66 | .build()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/shutoff/PerEndpointNetworkShutoffManagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | import org.junit.Test
4 |
5 | /**
6 | * This tests specifically the functionality of blocking just one endpoint (as well as how that
7 | * interacts with other endpoints being blocked and global blocks.
8 | *
9 | * More general network traffic blocking functionality is tested in
10 | * [GlobalNetworkShutoffManagerTest].
11 | */
12 | class PerEndpointNetworkShutoffManagerTest : NetworkShutoffManagerTest() {
13 | @Test
14 | fun testReceive555_endpointIsShutOff() {
15 | setErrorCode(ENDPOINT_FAIL_CODE)
16 | checkIfDrop(DEFAULT_ENDPOINT, true)
17 | }
18 |
19 | @Test
20 | fun testRecieve555_otherEndpointsStillOn() {
21 | setErrorCode(ENDPOINT_FAIL_CODE)
22 | checkIfDrop(ALTERNATE_ENDPOINT, false)
23 | }
24 |
25 | @Test
26 | fun testAfter555CodeAndSuccessfulRetry_NoLongerDropsTraffic() {
27 | setErrorCode(ENDPOINT_FAIL_CODE)
28 | setClockToNextInterval()
29 | setErrorCode(SUCCESS_CODE)
30 | checkIfDrop(DEFAULT_ENDPOINT, false)
31 | }
32 |
33 | @Test
34 | fun testAfter555CodeAndFailedRetry_DropsTraffic() {
35 | setErrorCode(ENDPOINT_FAIL_CODE)
36 | setClockToNextInterval()
37 | setErrorCode(ENDPOINT_FAIL_CODE)
38 | checkIfDrop(DEFAULT_ENDPOINT, true)
39 | }
40 |
41 | @Test
42 | fun testTwoEndpointsBlockedThenOneCleared_blocksOnlyOneEndpoint() {
43 | setErrorCode(ENDPOINT_FAIL_CODE, DEFAULT_ENDPOINT)
44 | setErrorCode(ENDPOINT_FAIL_CODE, ALTERNATE_ENDPOINT)
45 | setErrorCode(SUCCESS_CODE, DEFAULT_ENDPOINT)
46 | checkIfDrop(DEFAULT_ENDPOINT, false)
47 | checkIfDrop(ALTERNATE_ENDPOINT, true)
48 | }
49 |
50 | @Test
51 | fun testEndpointAndGlobalBlocked_clearingGlobalDoesntClearEndpoint() {
52 | // We set both to block traffic
53 | setErrorCode(ENDPOINT_FAIL_CODE, DEFAULT_ENDPOINT)
54 | setErrorCode(GLOBAL_FAIL_CODE, DEFAULT_ENDPOINT)
55 |
56 | // Once we try again, a different endpoint gets through
57 | setErrorCode(SUCCESS_CODE, ALTERNATE_ENDPOINT)
58 |
59 | // The global block should be gone but the endpoint-specific one is not clear
60 | checkIfDrop(DEFAULT_ENDPOINT, true)
61 | checkIfDrop(ALTERNATE_ENDPOINT, false)
62 | }
63 |
64 | @Test
65 | fun testEndpointBlockCode_doesntClearGlobalBlock() {
66 | // Enable the global block
67 | setErrorCode(GLOBAL_FAIL_CODE)
68 |
69 | // Once we try again, we get an endpoint-specific block
70 | setClockToNextInterval()
71 | setErrorCode(ENDPOINT_FAIL_CODE)
72 |
73 | // Once we try again, the global block should still be in effect
74 | checkIfDrop(ALTERNATE_ENDPOINT, false)
75 | }
76 |
77 | @Test
78 | fun testEndpointAndGlobalBlocked_enablingGlobalDoesntClearEndpoint() {
79 | setErrorCode(ENDPOINT_FAIL_CODE)
80 | setErrorCode(GLOBAL_FAIL_CODE)
81 | checkIfDrop(DEFAULT_ENDPOINT, true)
82 | }
83 |
84 | @Test
85 | fun testRestartAfterEndPointFailure() {
86 | setErrorCode(ENDPOINT_FAIL_CODE)
87 | checkIfDrop(DEFAULT_ENDPOINT, true)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem http://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS=
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/LogUploadingManager.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.google.android.gms.common.util.Clock
4 | import com.google.android.gms.common.util.DefaultClock
5 | import java.util.concurrent.ConcurrentHashMap
6 |
7 | const val TOTAL = "total"
8 |
9 |
10 | /**
11 | * This class determines whether or not to send logs somewhere, and then does so. It does so with
12 | * the help of whatever extends [LogUploaderBase], which contains the app-specific part of the
13 | * functionality.
14 | *
15 | * It applies to all endpoints - all alerts are stored in the [LogUploaderBase], , and when there is
16 | * network traffic, all alerts across all endpoints are flushed, no more than every
17 | * [maxSendFrequency] minutes.
18 | *
19 | * @param logUploader Contains code to issue an alert in an app-appropriate way.
20 | * @param windowLength Length of time to save network logs that we might send. Should be equal to or
21 | * longer than maxSendFrequency.
22 | * @param maxSendFrequency Max frequency with which logs might be sent.
23 | */
24 | class LogUploadingManager(
25 | private val logUploader: LogUploaderBase,
26 | private val windowLength: Long,
27 | private val maxSendFrequency: Long
28 | ) {
29 | private var lastTimeCleared = -maxSendFrequency
30 | private var clock: Clock = DefaultClock.getInstance()
31 |
32 | internal fun setClockForTesting(clock: Clock) {
33 | this.clock = clock
34 | }
35 |
36 | /**
37 | * This takes the statistics about data sent to each endpoint, and using the [logUploader]
38 | * you have defined, uploads summary statistics about each endpoint and overall.
39 | *
40 | * It also clears the state of the network log tracking for each endpoint so you don't
41 | * double-count.
42 | */
43 | suspend fun registerLogs(
44 | endpoints: ConcurrentHashMap
45 | ) {
46 | if (clock.elapsedRealtime() - lastTimeCleared < maxSendFrequency) return
47 | lastTimeCleared = clock.elapsedRealtime()
48 |
49 | var size = 0L
50 | var count = 0
51 |
52 | endpoints.forEach {(endpoint, tracker) ->
53 |
54 | if (tracker.requestCount.get() != 0) {
55 |
56 | // These must happen first because the endpoint gets cleared
57 | size += tracker.requestSize.get()
58 | count += tracker.requestCount.get()
59 |
60 | // Then upload the log for this endpoint
61 | logUploader.uploadTrafficLogSummaryForInterval(tracker.requestSize.get(),
62 | tracker.requestCount.get(),
63 | windowLength,
64 | endpoint)
65 | tracker.clearLog()
66 | }
67 | }
68 |
69 | logUploader.uploadTrafficLogSummaryForInterval(size, count, windowLength, TOTAL)
70 | endpoints[TOTAL]?.clearLog()
71 |
72 | }
73 |
74 | /**
75 | * Extend this with a class that uploads logs to the appropriate place.
76 | */
77 | interface LogUploaderBase {
78 |
79 | suspend fun uploadTrafficLogSummaryForInterval(
80 | data: Long,
81 | requests: Int,
82 | interval: Long,
83 | endpoint: String
84 | )
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/persistence/AppNetworkShutoffLogPersister.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp.persistence
2 |
3 | import com.yelp.android.varanus.shutoff.NetworkShutoffLogPersister
4 |
5 | import com.yelp.android.varanus.shutoff.NetworkShutoffLog
6 | import io.realm.Realm
7 | import io.realm.RealmConfiguration
8 | import io.realm.kotlin.where
9 | import java.util.LinkedList
10 |
11 | /**
12 | * Used by EndpointSpecificShutoff to save its state.
13 | *
14 | * Logs should be added from here but only read when state needs to be restored after the app
15 | * starts up again.
16 | *
17 | * @param persistentCacheRepository Repository containing an instance of the realm configuration
18 | * we use for this app.
19 | */
20 | class AppNetworkShutoffLogPersister(
21 | private val realmConfig: RealmConfiguration
22 | ) : NetworkShutoffLogPersister {
23 |
24 | /**
25 | * Used by NetworkShutoffLogManager to restore its state
26 | *
27 | * @param endpoints A container used to hold all endpoints names.
28 | */
29 | override fun getAll(): LinkedList {
30 | val endpoints = LinkedList()
31 | Realm.getInstance(realmConfig).use { realm ->
32 | realm.where().findAll().mapTo(endpoints) { it.endpoint }
33 | }
34 | return endpoints
35 | }
36 |
37 | /**
38 | * Delete the data entry whose key is the endpoint string
39 | *
40 | * @param endpoint Key for the endpoint type we're deleting data for
41 | */
42 | override fun clear(endpoint: String) {
43 | Realm.getInstance(realmConfig).use { realm ->
44 | realm.where()
45 | .equalTo("endpoint", endpoint)
46 | .findAll()
47 | .deleteAllFromRealm()
48 | }
49 | }
50 |
51 | /**
52 | * @param endpoint The key used to store this network request
53 | * @return the state to the log whose key is the endpoint if it exists in the realm. Otherwise,
54 | * the log in the default state is returned.
55 | */
56 | override fun getLog(endpoint: String): NetworkShutoffLog {
57 | // Fetch the log matching the entrypoint. There should be at most one log exists since
58 | // entrypoint is the primary key
59 | var state = "SENDING"
60 | var backoffSize = 1
61 | var shutOffUntil = 0L
62 |
63 | Realm.getInstance(realmConfig).use { realm ->
64 | val log: RealmNetworkShutoffLog? =
65 | realm.where()
66 | .equalTo("endpoint", endpoint)
67 | .findFirst()
68 |
69 | // if there is a log, reset the state and remove the log
70 | state = log?.state ?: "SENDING"
71 | backoffSize = log?.backoffSize ?: 1
72 | shutOffUntil = log?.shutOffUntil ?: 0L
73 | }
74 |
75 | return NetworkShutoffLog(state, backoffSize, shutOffUntil, endpoint)
76 | }
77 |
78 | /**
79 | * Save a networkshutoff log to realm
80 | *
81 | * @param log The log to save to Realm.
82 | */
83 | override fun addAndUpdateLog(log: NetworkShutoffLog) {
84 | Realm.getInstance(realmConfig).use { realm ->
85 | realm.executeTransaction {
86 | it.insertOrUpdate(RealmNetworkShutoffLog(log))
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/EndpointSpecificNetworkTracker.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import com.yelp.android.varanus.util.CoroutineScopeAndJob
4 | import com.yelp.android.varanus.util.JobBasedScope
5 | import kotlinx.coroutines.Dispatchers
6 | import java.util.concurrent.atomic.AtomicInteger
7 | import java.util.concurrent.atomic.AtomicLong
8 |
9 | /**
10 | * This class keeps track of network usage statistics in a given category. Usually by endpoint,
11 | * but can be used to keep track of any arbitrary category of traffic of interest.
12 | *
13 | * In endpointConfig there is a string called "endpoint". This is a key that is used to look
14 | * up this [EndpointSpecificNetworkTracker]. The key can correspond to anything (although usually
15 | * to and endpoint) as long as the desired set of network requests are always mapped to that key.
16 | *
17 | * The way this works is that each request is stored in a cache, which expires data after a length
18 | * of time. In this way, the requests in the cache represent the last windowLength minutes of
19 | * requests. Requests are also stored in a persistent database so this tracking can remain
20 | * approximatly accurate even if the app is restarted repeatedly in quick succession.
21 | *
22 | * @param endpoint Label for this endpoint.
23 | * @param windowLength Time in milliseconds for the window of time for which we observe requests.
24 | * @param trafficLogPersister Uses a persistent database to make sure state about the total number
25 | * of requests is retained.
26 | * @param networkTrafficAlerter Sends alerts to the desired place according to how the app sends
27 | * alerts or logs.
28 | */
29 | class EndpointSpecificNetworkTracker(
30 | private val endpoint: String,
31 | private val windowLength: Long,
32 | private val trafficLogPersister: NetworkTrafficLogPersister,
33 | private val networkTrafficAlerter: LogUploadingManager
34 | ) : CoroutineScopeAndJob by JobBasedScope(Dispatchers.IO) {
35 |
36 | var requestCount = AtomicInteger()
37 | var requestSize = AtomicLong()
38 |
39 |
40 | /**
41 | * We've been saving all the logs into a persistent database, so we can restore our
42 | * understanding of the number of recent requests when initialized.
43 | */
44 | init {
45 | val priorValues = trafficLogPersister.getSizeAndClear(windowLength, endpoint)
46 | requestCount.getAndSet(priorValues.count)
47 | requestSize.getAndSet(priorValues.size)
48 |
49 | // This fake network log makes sure the previous request count and request size expires.
50 | // Otherwise we could get into a state where we keep incrementing these.
51 | val initialTrafficLog = NetworkTrafficLog(true, endpoint, "n/a",
52 | requestSize.get(), requestCount.get())
53 |
54 | }
55 |
56 | /**
57 | * Keep track of the number and size of requests for a specific endpoint.
58 | *
59 | * @param log abstraction of a network request with the size and endpoint key.
60 | */
61 | fun addLogAndPersist(log: NetworkTrafficLog) {
62 | if (log.isRequest) {
63 | requestCount.incrementAndGet()
64 | }
65 | requestSize.addAndGet(log.size)
66 | trafficLogPersister.addLog(log)
67 | }
68 |
69 | /**
70 | * Once we've logged these requests, we delete them so we don't double-count.
71 | */
72 | fun clearLog() {
73 | requestCount.getAndSet(0)
74 | requestSize.getAndSet(0)
75 | trafficLogPersister.clear(windowLength, endpoint)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/shutoff/GlobalNetworkShutoffManagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | import org.junit.Test
4 | import kotlin.test.assertEquals
5 |
6 | /**
7 | * This tests the functionality of shutting off all traffic for all endpoints.
8 | *
9 | * The functionality of shutting off traffic for just one endpoint is in
10 | * [PerEndpointNetworkShutoffManagerTest].
11 | */
12 | class GlobalNetworkShutoffManagerTest : NetworkShutoffManagerTest() {
13 |
14 | private val config = TestConfig.config
15 |
16 | @Test
17 | fun testDefaultState_DoesntBlockTraffic() {
18 | checkIfDrop(DEFAULT_ENDPOINT, false)
19 | }
20 |
21 | @Test
22 | fun testAfter556Code_DropsTraffic() {
23 | setErrorCode(GLOBAL_FAIL_CODE)
24 | checkIfDrop(DEFAULT_ENDPOINT, true)
25 | }
26 |
27 | @Test
28 | fun testAfter556CodeAndSuccessfulRetry_NoLongerDropsTraffic() {
29 | setErrorCode(GLOBAL_FAIL_CODE)
30 | setClockToNextInterval()
31 | setErrorCode(SUCCESS_CODE)
32 | checkIfDrop(DEFAULT_ENDPOINT, false)
33 | }
34 |
35 | @Test
36 | fun testAfter556CodeAndFailedRetry_DropsTraffic() {
37 | setErrorCode(GLOBAL_FAIL_CODE)
38 | setClockToNextInterval()
39 | setErrorCode(GLOBAL_FAIL_CODE)
40 | checkIfDrop(DEFAULT_ENDPOINT, true)
41 | }
42 |
43 | /**
44 | * The expectation is that we will back off by 5 minutes at first, then an additional 5 minutes
45 | * each time. After 40 minutes, we check again every 40 minutes without increasing the interval
46 | *
47 | * There is also a random fuzz factor that isn't tested here.
48 | */
49 | @Test
50 | fun testBackoff_GoesTo40MinutesThenStops() {
51 | setErrorCode(GLOBAL_FAIL_CODE)
52 |
53 | for (i in 1..config.maxBackoff) {
54 | val increment = config.backoffIncrement * i / 2
55 |
56 | // Halfway to the next timeout - should drop traffic
57 | testClock.time += increment
58 | checkIfDrop(DEFAULT_ENDPOINT, true)
59 |
60 | // All the way to the next timeout - should send request
61 | testClock.time += increment + 1
62 | checkIfDrop(DEFAULT_ENDPOINT, false)
63 | setErrorCode(GLOBAL_FAIL_CODE)
64 | }
65 |
66 | // 40 minutes later it should send the request again
67 |
68 | testClock.time += config.backoffIncrement *
69 | config.backoffIncrement + 1
70 | val shouldDrop = networkShutoffManager.shouldDropRequest(DEFAULT_ENDPOINT)
71 | assertEquals(false, shouldDrop)
72 | }
73 |
74 | @Test
75 | fun testGenerateErrorResponse_returnsValidResponseWithError() {
76 | val code = networkShutoffManager.getErrorCodeForResponse()
77 | assertEquals(556, code)
78 | }
79 |
80 | @Test
81 | fun testAfter556Code_blocksTrafficForOtherEndpoints() {
82 | setErrorCode(GLOBAL_FAIL_CODE, DEFAULT_ENDPOINT)
83 | checkIfDrop(ALTERNATE_ENDPOINT, true)
84 | }
85 |
86 | @Test
87 | fun testAfter556Code_generatesCorrectErrorCode() {
88 | setErrorCode(GLOBAL_FAIL_CODE, DEFAULT_ENDPOINT)
89 | val code = networkShutoffManager.getErrorCodeForResponse()
90 | assertEquals(ENDPOINT_FAIL_CODE, code)
91 | }
92 |
93 | @Test
94 | fun testAfterBlockingPeriodExpires_sendsAtLeastOneRequest() {
95 | setErrorCode(GLOBAL_FAIL_CODE, DEFAULT_ENDPOINT)
96 | setClockToNextInterval()
97 | checkIfDrop(ALTERNATE_ENDPOINT, false)
98 | }
99 |
100 | @Test
101 | fun testRestartAfterGlobalFailure() {
102 | setErrorCode(GLOBAL_FAIL_CODE)
103 | checkIfDrop(DEFAULT_ENDPOINT, true)
104 | checkIfDrop(ALTERNATE_ENDPOINT, true)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/varanus/src/test/java/com/yelp/android/varanus/LogUploadingManagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus
2 |
3 | import kotlinx.coroutines.runBlocking
4 | import org.junit.Before
5 | import org.junit.Test
6 | import java.util.concurrent.ConcurrentHashMap
7 | import java.util.concurrent.TimeUnit
8 | import kotlin.test.assertEquals
9 |
10 | class LogUploadingManagerTest {
11 |
12 | private val persister = TestNetworkTrafficLogPersister()
13 | private val windowSize = TimeUnit.SECONDS.toMillis(9)
14 |
15 | private lateinit var testClock: TestClock
16 | private lateinit var alertIssuer: TestLogUploader
17 | private lateinit var trafficAlerter: LogUploadingManager
18 | private lateinit var endpoints: ConcurrentHashMap
19 | private val CLEAR_INCREMENT = windowSize + 1
20 |
21 | @Before
22 | fun setup() {
23 | testClock = TestClock()
24 | alertIssuer = TestLogUploader()
25 | trafficAlerter = LogUploadingManager(alertIssuer, windowSize, windowSize)
26 | .also { it.setClockForTesting(testClock) }
27 | val endpoint1 =
28 | EndpointSpecificNetworkTracker("test", windowSize, persister, trafficAlerter)
29 | val endpoint2 =
30 | EndpointSpecificNetworkTracker("test2", windowSize, persister, trafficAlerter)
31 | endpoints = ConcurrentHashMap(mapOf("test" to endpoint1, "test2" to endpoint2))
32 | }
33 |
34 | @Test
35 | fun testOneLogSent_getsRecorded() {
36 | addRequest()
37 |
38 | assertEquals(2, alertIssuer.sentLogs.size) // 1 for test, 1 for total
39 | assertEquals(1, alertIssuer.sentLogs[0].requests)
40 | assertEquals(2, alertIssuer.sentLogs[0].data)
41 | assertEquals(1, alertIssuer.sentLogs[1].requests)
42 | assertEquals(2, alertIssuer.sentLogs[1].data)
43 | }
44 |
45 | @Test
46 | fun testAlertsOverTime_triggeredAfterTimeExpiresOnly() {
47 | addRequest()
48 |
49 | assertEquals(2, alertIssuer.sentLogs.size)
50 |
51 | testClock.time += CLEAR_INCREMENT / 2
52 | addRequest()
53 |
54 | // No alerts or periodic stats should have been sent because the timer didn't expire
55 | // Still 1 log + 1 total
56 | assertEquals(2, alertIssuer.sentLogs.size)
57 |
58 | // This exceeds the timers and should send 2 logs
59 | testClock.time += CLEAR_INCREMENT
60 | addRequest()
61 | assertEquals(4, alertIssuer.sentLogs.size)
62 | }
63 |
64 | @Test
65 | fun testTwoEndpointAlerts_sendsAtCorrectTimes() {
66 | addRequest()
67 |
68 | // There are now 2 sentAlerts and 1 sentLogs
69 | testClock.time += CLEAR_INCREMENT / 2
70 | addRequest("test2")
71 |
72 | // There are now two new alerts pending but not sent yet
73 | assertEquals(2, alertIssuer.sentLogs.size)
74 |
75 | // This exceeds the timers and should send 2 more alerts and another log for each endpoint,
76 | // plus flush the alerts for endpoint_2.
77 | testClock.time += CLEAR_INCREMENT
78 | addRequest()
79 | // 1 log for the first request, 1 log for the first total
80 | // 2 logs for each of the subsequent endpoints and one for the overall total
81 | assertEquals(5, alertIssuer.sentLogs.size)
82 | }
83 |
84 | private fun addRequest(name: String = "test") {
85 | endpoints[name]?.addLogAndPersist(NetworkTrafficLog(true, "test", "test", 2))
86 |
87 | runBlocking {
88 | trafficAlerter.registerLogs(ConcurrentHashMap(endpoints))
89 | }
90 | }
91 |
92 | class TestNetworkTrafficLogPersister : NetworkTrafficLogPersister {
93 | override fun getSizeAndClear(
94 | windowLength: Long,
95 | endpoint: String
96 | ): NetworkTrafficLogPersister.TrafficLogSummary {
97 | return NetworkTrafficLogPersister.TrafficLogSummary(0, 0)
98 | }
99 |
100 | override fun clear(windowLength: Long, endpoint: String) {}
101 |
102 | override fun addLog(log: NetworkTrafficLog) {}
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/varanus/src/main/java/com/yelp/android/varanus/shutoff/CategoryOfTrafficShutoff.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.android.varanus.shutoff
2 |
3 | import com.google.android.gms.common.util.Clock
4 | import com.google.android.gms.common.util.DefaultClock
5 |
6 | /**
7 | * This manages a category of shutoffs for a batch of traffic that we want to treat as a unit
8 | * for the purpose of shutting it off. Usually, this category of traffic corresponds to an endpoint
9 | * but all of the traffic is one such category.
10 | *
11 | * @param clock For determining shutoff timeouts.
12 | * @param randomizer For randomizing how long the shutoff happens to stop everyone from retyring at
13 | * once.
14 | * @param persister Save the state so that if the app is crashing while sending too much data it
15 | * doesn't keep sending more data.
16 | * @param endpoint Label for the type of traffic this object handles.
17 | * @param config Sets backoff times, etc.
18 | */
19 | class CategoryOfTrafficShutoff(
20 | private val clock: Clock = DefaultClock.getInstance(),
21 | private val randomizer: NetworkShutoffManager.Randomizer,
22 | private val persister: NetworkShutoffLogPersister,
23 | endpoint: String,
24 | private val config: NetworkShutoffManager.Config
25 | ) {
26 |
27 | enum class State {
28 | SENDING, // this class acts as a no-op, send requests full speed
29 | SHUTOFF, // this class blocks all requests
30 | ATTEMPTING // send 1 request then throttle to see if it's safe to send full speed again
31 | }
32 |
33 | private var shutOffState = State.SENDING
34 | private var shutOffUntil = 0L
35 | private var backoffSize = 1
36 |
37 | // For persisting this state in case of crashes
38 | private var shutOffLog =
39 | NetworkShutoffLog(shutOffState.toString(), backoffSize, shutOffUntil, endpoint)
40 |
41 | /**
42 | * We start by restoring any old saved state.
43 | */
44 | init {
45 | val priorValues = persister.getLog(endpoint)
46 | shutOffState = when (priorValues.state) {
47 | // We renamed this state for clarity - this is for backwards compatiblity with devices
48 | // with old values saved persistently
49 | "INACTIVE" -> State.SENDING
50 | else -> State.valueOf(priorValues.state)
51 | }
52 | backoffSize = priorValues.backoffSize
53 | shutOffUntil = priorValues.shutOffUntil
54 | // Update shutoffLog
55 | shutOffLog =
56 | NetworkShutoffLog(shutOffState.toString(), backoffSize, shutOffUntil, endpoint)
57 | }
58 |
59 | /**
60 | * We have successfully sent a request without being blocked.
61 | *
62 | * We then switch to sending, and reset the shutoff time and the backoff increment.
63 | */
64 | fun reset() {
65 | if (shutOffState == State.SENDING) { return }
66 | shutOffState = State.SENDING
67 | shutOffUntil = 0L
68 | backoffSize = 1
69 |
70 | saveState()
71 | }
72 |
73 | /**
74 | * A signal has been received that this category should be shut off, or that the shutoff
75 | * should be renewed.
76 | *
77 | * Each time this is called, until reset() is called, we wait for longer, up to the limit
78 | * set by config.maxBackoff.
79 | */
80 | fun shutoff() {
81 | shutOffState = State.SHUTOFF
82 | shutOffUntil = clock.elapsedRealtime() + backoffSize * config.backoffIncrement
83 |
84 | // Make sure all the devices don't retry at the same time and knock the server over
85 | shutOffUntil = randomizer.randomizeTime(shutOffUntil)
86 | backoffSize = (backoffSize + 1).coerceAtMost(config.maxBackoff)
87 |
88 | saveState()
89 | }
90 |
91 | /**
92 | * A helper function which saves the current state and backoffSize of this endpoint to realm.
93 | * Should be called after every state change.
94 | */
95 | private fun saveState() {
96 | shutOffLog.state = shutOffState.toString()
97 | shutOffLog.backoffSize = backoffSize
98 | shutOffLog.shutOffUntil = shutOffUntil
99 | persister.addAndUpdateLog(shutOffLog)
100 | }
101 |
102 | /**
103 | * For this particular endpoint, determine if requests should be sent.
104 | *
105 | * This also triggers checking if the shutoff has expired and updating the state accordingly.
106 | * If this happens, exactly one request (this request) will be allowed through, then we switch
107 | * to "attempting" mode where requests are throttled.
108 | */
109 | fun shouldDropRequest(): Boolean {
110 | return when (shutOffState) {
111 | State.ATTEMPTING -> randomizer.randomizeSendRequest(config.attemptingThrottle)
112 | State.SENDING -> false
113 | State.SHUTOFF -> (shutOffUntil > clock.elapsedRealtime()).also { shutOffStillValid ->
114 | if (!shutOffStillValid) {
115 | shutOffState = State.ATTEMPTING
116 | saveState()
117 | }
118 | }
119 | }
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/content_monitor_lizard.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
24 |
25 |
33 |
34 |
42 |
43 |
44 |
45 |
53 |
54 |
61 |
62 |
67 |
68 |
74 |
75 |
81 |
82 |
88 |
89 |
95 |
96 |
102 |
103 |
109 |
110 |
116 |
117 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/yelp/varanussampleapp/MonitorLizardActivity.kt:
--------------------------------------------------------------------------------
1 | package com.yelp.varanussampleapp
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.view.Menu
7 | import android.view.MenuItem
8 | import android.widget.Button
9 | import android.widget.TextView
10 | import com.yelp.android.varanus.shutoff.NetworkShutoffManager
11 | import com.yelp.android.varanus.util.CoroutineScopeAndJob
12 | import com.yelp.android.varanus.util.JobBasedScope
13 | import kotlinx.coroutines.Dispatchers
14 |
15 | import kotlinx.coroutines.launch
16 | import okhttp3.MediaType
17 | import okhttp3.MediaType.Companion.toMediaType
18 | import okhttp3.OkHttpClient
19 | import okhttp3.Request
20 | import okhttp3.RequestBody
21 |
22 | const val url = "https://devnull-as-a-service.com/dev/null/varanus/"
23 | const val dummy_text = " OMNOMNOM "
24 | private const val LOGTAG = "VARANUS_MAIN"
25 |
26 | private const val INSECT_SIZE = 1
27 | private const val FRUIT_SIZE = 5
28 | private const val FISH_SIZE = 10
29 |
30 | class MonitorLizardActivity: AppCompatActivity(),
31 | CoroutineScopeAndJob by JobBasedScope(Dispatchers.IO){
32 |
33 | private lateinit var client: OkHttpClient
34 | private lateinit var alertIssuer: LogUploader
35 | lateinit var shutoffManager: NetworkShutoffManager
36 | private val textFields: HashMap = hashMapOf()
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | setContentView(R.layout.activity_monitor_lizard)
41 | alertIssuer = LogUploader(this)
42 | client = MonitorLizardOkhttpClientFactory.configureOkhttpClient(this, alertIssuer)
43 | setUpTextFields()
44 |
45 | setUpButton(R.id.insect_button, "insect", INSECT_SIZE)
46 | setUpButton(R.id.fruit_button, "fruit", FRUIT_SIZE)
47 | setUpButton(R.id.fish_button, "fish", FISH_SIZE)
48 | }
49 |
50 | private fun setUpButton(id: Int, foodName: String, size: Int) {
51 | findViewById