diff --git a/client/app/build.gradle b/client/app/build.gradle
index d21dd3d..8449598 100644
--- a/client/app/build.gradle
+++ b/client/app/build.gradle
@@ -73,6 +73,9 @@ dependencies {
def nav_version = "2.5.3"
implementation("androidx.navigation:navigation-compose:$nav_version")
+ // custom - work manager
+ implementation 'androidx.work:work-runtime-ktx:2.7.1'
+
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
@@ -86,8 +89,4 @@ dependencies {
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
-}
-
-dependencies {
-
}
\ No newline at end of file
diff --git a/client/app/src/main/AndroidManifest.xml b/client/app/src/main/AndroidManifest.xml
index eface89..950f2e9 100644
--- a/client/app/src/main/AndroidManifest.xml
+++ b/client/app/src/main/AndroidManifest.xml
@@ -16,7 +16,6 @@
@@ -24,6 +23,12 @@
+
+
+
\ No newline at end of file
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/ConfigObject.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/ConfigObject.kt
new file mode 100644
index 0000000..af7260d
--- /dev/null
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/ConfigObject.kt
@@ -0,0 +1,29 @@
+package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
+
+import androidx.work.Data
+
+data class PushProxConfig(
+ val pushProxUrl : String,
+ val pushProxFqdn : String,
+){
+ companion object{
+ fun fromData(data : Data) : PushProxConfig{
+ return PushProxConfig(
+ data.getString("0")!!,
+ data.getString("1")!!,
+ )
+ }
+ }
+
+ fun toData() : Data {
+ return Data.Builder()
+ .putString("0", pushProxUrl)
+ .putString("1", pushProxFqdn)
+ .build()
+ }
+}
+
+data class PromServerConfig(
+ //TODO implement this
+ val dummy : String,
+)
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/HomeActivity.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/HomeActivity.kt
index dac29a1..bd68aa9 100644
--- a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/HomeActivity.kt
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/HomeActivity.kt
@@ -159,10 +159,10 @@ private fun onCheckedChangePushProx(
value : Boolean,
promViewModel: PromViewModel,
showDialog : MutableState
-){
+) {
if (value) {
- val result : String? = promViewModel.turnPushProxOn()
- if(result != null){
+ val result: String? = promViewModel.turnPushProxOn()
+ if (result != null) {
showDialog.value = result
}
} else {
@@ -176,7 +176,7 @@ private fun PushProxPage(
){
val uiState : PromUiState by promViewModel.uiState.collectAsState()
- // if showDialogText == "", do not display alert dialog
+ // if showDialogText is empty string, do not display alert dialog
val showDialogText : MutableState = remember { mutableStateOf("") }
Column(
@@ -193,8 +193,20 @@ private fun PushProxPage(
modifier = Modifier.padding(bottom = 30.dp)
)
+ if(uiState.pushProxTurnedOn){
+ Text(
+ text = """
+ To edit PushProx proxy URL or FQDN, turn it off first.
+ """.trimIndent(),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(bottom = 12.dp),
+ )
+ }
+
TextField(
value = uiState.fqdn,
+ singleLine = true,
+ enabled = !uiState.pushProxTurnedOn,
onValueChange = {
promViewModel.updatePushProxFQDN(it)
},
@@ -206,6 +218,8 @@ private fun PushProxPage(
TextField(
value = uiState.pushProxURL,
+ singleLine = true,
+ enabled = !uiState.pushProxTurnedOn,
onValueChange = {
promViewModel.updatePushProxURL(it)
},
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/MainActivity.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/MainActivity.kt
index d4713cc..d296dc4 100644
--- a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/MainActivity.kt
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/MainActivity.kt
@@ -1,34 +1,19 @@
package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
import android.os.Bundle
-import android.text.BoringLayout.Metrics
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.birdthedeveloper.prometheus.android.prometheus.android.exporter.ui.theme.PrometheusAndroidExporterTheme
-import io.prometheus.client.Collector
-import io.prometheus.client.CollectorRegistry
-import io.prometheus.client.exporter.common.TextFormat
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import java.io.StringWriter
//TODO
//https://www.geeksforgeeks.org/how-to-launch-an-application-automatically-on-system-boot-up-in-android/
@@ -39,7 +24,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val promViewModel: PromViewModel = ViewModelProvider(this)[PromViewModel::class.java]
- promViewModel.setApplicationContext { this.applicationContext }
+ promViewModel.initializeWithApplicationContext { this.applicationContext }
setContent {
PrometheusAndroidExporterTheme {
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromServerWorker.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromServerWorker.kt
new file mode 100644
index 0000000..0d845f8
--- /dev/null
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromServerWorker.kt
@@ -0,0 +1,2 @@
+package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
+
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromViewModel.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromViewModel.kt
index f257b62..015620d 100644
--- a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromViewModel.kt
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PromViewModel.kt
@@ -4,9 +4,16 @@ import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.Operation
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkManager
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.exporter.common.TextFormat
-import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -27,28 +34,28 @@ data class PromUiState(
val serverTurnedOn : Boolean = false,
val pushProxTurnedOn : Boolean = false,
val serverPort : Int? = null, // if null, use default port
- val fqdn : String = "",
- val pushProxURL : String = "",
+ val fqdn : String = "test.example.com",
+ val pushProxURL : String = "143.42.59.63:8080",
val configFileState : ConfigFileState = ConfigFileState.LOADING,
)
private val TAG : String = "PROMVIEWMODEL"
class PromViewModel(): ViewModel() {
+ // constants
private val DEFAULT_SERVER_PORT : Int = 10101 //TODO register with prometheus community
+ private val PROM_UNIQUE_WORK : String = "prom_unique_job"
+
+
private val _uiState = MutableStateFlow(PromUiState())
val uiState : StateFlow = _uiState.asStateFlow()
- // app - level components
- private val collectorRegistry: CollectorRegistry = CollectorRegistry()
- private lateinit var pushProxClient : PushProxClient
- private val prometheusServer : PrometheusServer = PrometheusServer()
- private lateinit var metricsEngine: MetricsEngine
- private lateinit var androidCustomExporter: AndroidCustomExporter
+ private lateinit var getContext: () -> Context
init {
Log.v(TAG, "Checking for configuration file")
viewModelScope.launch {
+ //TODO check for configuration file
delay(1000)
_uiState.update { current ->
current.copy(configFileState = ConfigFileState.MISSING)
@@ -60,13 +67,8 @@ class PromViewModel(): ViewModel() {
return DEFAULT_SERVER_PORT
}
- fun setApplicationContext(getContext : () -> Context){
- // initalize app - level components
- metricsEngine = MetricsEngine(getContext())
- androidCustomExporter = AndroidCustomExporter(metricsEngine).register(collectorRegistry)
- pushProxClient = PushProxClient(collectorRegistry)
- //TODO somehow this PushProx definition does not work here
- // -> fix it
+ fun initializeWithApplicationContext(getContext : () -> Context){
+ this.getContext = getContext
}
fun updateTabIndex(index : Int){
@@ -85,22 +87,16 @@ class PromViewModel(): ViewModel() {
}
}
- private suspend fun performScrape() : String {
- val writer : StringWriter = StringWriter()
- TextFormat.write004(writer, collectorRegistry.metricFamilySamples())
-
- return writer.toString()
- }
-
// if result is not null, it contains an error message
fun turnServerOn() : String?{
try{
- prometheusServer.startInBackground(
- PrometheusServerConfig(
- getPromServerPort(),
- ::performScrape
- )
- )
+ //TODO rewrite asap
+// prometheusServer.startInBackground(
+// PrometheusServerConfig(
+// getPromServerPort(),
+// ::performScrape
+// )
+// )
}catch(e : Exception){
Log.v(TAG, e.toString())
return "Prometheus server failed!"
@@ -119,8 +115,8 @@ class PromViewModel(): ViewModel() {
}
private fun validatePushProxSettings() : String? {
- val fqdn = _uiState.value.fqdn.trim()
- val url = _uiState.value.pushProxURL.trim()
+ val fqdn = _uiState.value.fqdn.trim().trim('\n')
+ val url = _uiState.value.pushProxURL.trim().trim('\n')
if( fqdn.isEmpty() ) return "Fully Qualified Domain Name cannot be empty!"
if( url.isEmpty() ) return "PushProx URL cannot be empty!"
@@ -128,25 +124,42 @@ class PromViewModel(): ViewModel() {
return null
}
+ private fun launchPushProxUsingWorkManager(){
+ val workManagerInstance = WorkManager.getInstance(getContext())
+
+ // worker configuration
+ val inputData : Data = PushProxConfig(
+ pushProxFqdn = _uiState.value.fqdn,
+ pushProxUrl = _uiState.value.pushProxURL,
+ ).toData()
+
+ // constraints
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.NOT_REQUIRED)
+ .build()
+
+ // setup worker request
+ val workerRequest = OneTimeWorkRequestBuilder()
+ .setInputData(inputData)
+ .setConstraints(constraints)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .build()
+
+ // enqueue
+ workManagerInstance.beginUniqueWork(
+ PROM_UNIQUE_WORK,
+ ExistingWorkPolicy.KEEP,
+ workerRequest,
+ ).enqueue()
+ }
+
// if result is not null, it contains an error message
fun turnPushProxOn() : String?{
-
val error : String? = validatePushProxSettings()
- if(error != null){
- return error
- }
+ if(error != null){ return error }
- try{
- pushProxClient.startBackground(
- PushProxConfig(
- performScrape = ::performScrape,
- fqdn = _uiState.value.fqdn.trim(),
- proxyUrl = _uiState.value.pushProxURL.trim(),
- )
- )
- }catch(e : Exception){
- return "PushProx client failed!"
- }
+ // idempotent call
+ launchPushProxUsingWorkManager()
_uiState.update { current ->
current.copy(
@@ -158,7 +171,14 @@ class PromViewModel(): ViewModel() {
}
fun turnPushProxOff(){
- //TODO implement
+ val workerManagerInstance = WorkManager.getInstance(getContext())
+ workerManagerInstance.cancelUniqueWork(PROM_UNIQUE_WORK)
+
+ _uiState.update {current ->
+ current.copy(
+ pushProxTurnedOn = false
+ )
+ }
}
fun updatePushProxURL(url : String){
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxClient.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxClient.kt
index e859393..3cfabac 100644
--- a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxClient.kt
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxClient.kt
@@ -12,6 +12,7 @@ import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.Counter
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -51,13 +52,6 @@ private class Counters(collectorRegistry: CollectorRegistry) {
fun pollError(){ pollErrorCounter.inc() }
}
-// Configuration object for this pushprox client
-data class PushProxConfig(
- val fqdn: String,
- val proxyUrl: String,
- val performScrape: suspend () -> String,
-)
-
// Error in parsing HTTP header "Id" from HTTP request from Prometheus
class PushProxIdParseException(message: String) : Exception(message)
@@ -68,31 +62,26 @@ data class PushProxContext(
val pushUrl : String,
val backoff : StrategyBackoff,
val fqdn : String,
- val performScrape: suspend () -> String,
)
// This is a stripped down kotlin implementation of github.com/prometheus-community/PushProx client
class PushProxClient(
- private val collectorRegistry: CollectorRegistry
+ collectorRegistry: CollectorRegistry,
+ private val performScrape: suspend () -> String
) {
private val counters : Counters = Counters(collectorRegistry)
private val retryInitialWaitSeconds : Int = 1
private val retryMaxWaitSeconds : Int = 5
- private var isRunning : Boolean = false
// Use this function to start exporting metrics to pushprox in the background
- fun startBackground(config: PushProxConfig) {
- if(!isRunning){
- isRunning = true
-
+ suspend fun startBackground(config: PushProxConfig) {
val client : HttpClient = HttpClient() //TODO close this thing
val context : PushProxContext = processConfig(client, config)
loop(context)
- }
}
private fun processConfig(client : HttpClient, config : PushProxConfig) : PushProxContext {
- var modifiedProxyURL = config.proxyUrl.trim('/')
+ var modifiedProxyURL = config.pushProxUrl.trim('/')
if(
!modifiedProxyURL.startsWith("http://") &&
@@ -109,8 +98,7 @@ class PushProxClient(
pollURL,
pushURL,
newBackoffFromFlags(),
- config.fqdn,
- config.performScrape,
+ config.pushProxFqdn,
)
}
@@ -143,7 +131,6 @@ class PushProxClient(
"X-Prometheus-Scrape-Timeout: 9.5\r\n"
val result : String = httpHeaders + "\r\n" + scrapedMetrics
- log("result", result)
return result
}
@@ -152,7 +139,7 @@ class PushProxClient(
// perform scrape
lateinit var scrapedMetrics : String
try {
- scrapedMetrics = context.performScrape()
+ scrapedMetrics = performScrape()
}catch(e : Exception){
counters.scrapeError()
log("scrape exception", e.toString())
@@ -173,7 +160,6 @@ class PushProxClient(
log("push exception", e.toString())
return
}
-
}
private fun newBackoffFromFlags() : StrategyBackoff {
@@ -187,27 +173,27 @@ class PushProxClient(
}
//TODO migrate to work manager
- private fun loop(context : PushProxContext) {
- // fire and forget a new coroutine
- GlobalScope.launch {
- launch {
- while (true) {
- val job = launch {
- log("pushprox main loop", "loop start")
- var result = context.backoff.withRetries {
- // register poll error using try-catch block
- try {
- doPoll(context)
- } catch (e: Exception) {
- log("exception encountered!", e.toString())
- counters.pollError()
- throw e
- }
- }
- }
- job.join() // wait for the job to finish
- }
+ private suspend fun loop(context : PushProxContext) {
+ var shouldContinue : Boolean = true
+ while (shouldContinue) {
+ log("pushprox main loop", "loop start")
+ // register poll error using try-catch block
+ try {
+ doPoll(context)
+ }catch(e : CancellationException){
+ shouldContinue = false
}
+ catch (e: Exception) {
+ for(exception in e.suppressed){
+ if(exception is CancellationException){
+ shouldContinue = false
+ }
+ }
+ log("exception encountered!", e.toString())
+ counters.pollError()
+ throw e
+ }
+ log("pushprox main loop", "loop end")
}
}
diff --git a/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxWorker.kt b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxWorker.kt
new file mode 100644
index 0000000..be1b2ae
--- /dev/null
+++ b/client/app/src/main/java/com/birdthedeveloper/prometheus/android/prometheus/android/exporter/PushProxWorker.kt
@@ -0,0 +1,81 @@
+package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
+
+import android.content.Context
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ForegroundInfo
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import io.prometheus.client.CollectorRegistry
+import io.prometheus.client.exporter.common.TextFormat
+import kotlinx.coroutines.delay
+import java.io.StringWriter
+
+private val TAG = "PUSH_PROX_WORKER"
+
+class PushProxWorker(
+ private val context : Context,
+ parameters : WorkerParameters
+): CoroutineWorker(context, parameters){
+
+ override suspend fun doWork():Result {
+ //TODO implement this
+ val cache: PushProxWorkerCache = PushProxWorkerCache.getInstance {
+ return@getInstance context
+ }
+
+ try{
+ val pushProxConfig : PushProxConfig = PushProxConfig.fromData(inputData)
+
+ cache.startBackground(pushProxConfig)
+
+ }catch(e : Exception){
+ Log.v(TAG, e.toString())
+ return Result.failure()
+ }
+
+ return Result.success()
+ }
+}
+
+// thread-safe singleton
+class PushProxWorkerCache private constructor(
+ private val getContext: () -> Context
+){
+ private val collectorRegistry: CollectorRegistry = CollectorRegistry()
+ private val metricsEngine : MetricsEngine = MetricsEngine(getContext())
+ private val pushProxClient = PushProxClient(collectorRegistry, ::performScrape)
+ private lateinit var androidCustomExporter : AndroidCustomExporter
+
+ init {
+ Log.v(TAG, "Initializing WorkerCache")
+ androidCustomExporter = AndroidCustomExporter(metricsEngine).register(collectorRegistry)
+ }
+
+ private fun performScrape() : String{
+ val writer = StringWriter()
+ TextFormat.write004(writer, collectorRegistry.metricFamilySamples())
+ return writer.toString()
+ }
+
+ suspend fun startBackground(pushProxConfig : PushProxConfig){
+ pushProxClient.startBackground(pushProxConfig)
+ }
+
+ companion object {
+ private var instance : PushProxWorkerCache? = null
+
+ fun getInstance(getContext: () -> Context) : PushProxWorkerCache {
+ if(instance == null){
+ synchronized(PushProxWorkerCache::class.java){
+ if (instance == null){
+ instance = PushProxWorkerCache(getContext)
+ }
+ }
+ }
+ return instance!!
+ }
+ }
+}
\ No newline at end of file