pushprox worker kinda works

This commit is contained in:
Martin Ptáček
2023-05-05 16:13:00 +02:00
parent 30818a9aeb
commit 84e347a423
9 changed files with 234 additions and 113 deletions

View File

@ -73,6 +73,9 @@ dependencies {
def nav_version = "2.5.3" def nav_version = "2.5.3"
implementation("androidx.navigation:navigation-compose:$nav_version") 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.core:core-ktx:1.10.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
@ -87,7 +90,3 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
} }
dependencies {
}

View File

@ -16,7 +16,6 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PrometheusAndroidExporter"> android:theme="@style/Theme.PrometheusAndroidExporter">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -24,6 +23,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="location|microphone"
tools:node="merge" />
</application> </application>
</manifest> </manifest>

View File

@ -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,
)

View File

@ -159,10 +159,10 @@ private fun onCheckedChangePushProx(
value : Boolean, value : Boolean,
promViewModel: PromViewModel, promViewModel: PromViewModel,
showDialog : MutableState<String> showDialog : MutableState<String>
){ ) {
if (value) { if (value) {
val result : String? = promViewModel.turnPushProxOn() val result: String? = promViewModel.turnPushProxOn()
if(result != null){ if (result != null) {
showDialog.value = result showDialog.value = result
} }
} else { } else {
@ -176,7 +176,7 @@ private fun PushProxPage(
){ ){
val uiState : PromUiState by promViewModel.uiState.collectAsState() 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<String> = remember { mutableStateOf("") } val showDialogText : MutableState<String> = remember { mutableStateOf("") }
Column( Column(
@ -193,8 +193,20 @@ private fun PushProxPage(
modifier = Modifier.padding(bottom = 30.dp) 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( TextField(
value = uiState.fqdn, value = uiState.fqdn,
singleLine = true,
enabled = !uiState.pushProxTurnedOn,
onValueChange = { onValueChange = {
promViewModel.updatePushProxFQDN(it) promViewModel.updatePushProxFQDN(it)
}, },
@ -206,6 +218,8 @@ private fun PushProxPage(
TextField( TextField(
value = uiState.pushProxURL, value = uiState.pushProxURL,
singleLine = true,
enabled = !uiState.pushProxTurnedOn,
onValueChange = { onValueChange = {
promViewModel.updatePushProxURL(it) promViewModel.updatePushProxURL(it)
}, },

View File

@ -1,34 +1,19 @@
package com.birdthedeveloper.prometheus.android.prometheus.android.exporter package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
import android.os.Bundle import android.os.Bundle
import android.text.BoringLayout.Metrics
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.birdthedeveloper.prometheus.android.prometheus.android.exporter.ui.theme.PrometheusAndroidExporterTheme 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 //TODO
//https://www.geeksforgeeks.org/how-to-launch-an-application-automatically-on-system-boot-up-in-android/ //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) super.onCreate(savedInstanceState)
val promViewModel: PromViewModel = ViewModelProvider(this)[PromViewModel::class.java] val promViewModel: PromViewModel = ViewModelProvider(this)[PromViewModel::class.java]
promViewModel.setApplicationContext { this.applicationContext } promViewModel.initializeWithApplicationContext { this.applicationContext }
setContent { setContent {
PrometheusAndroidExporterTheme { PrometheusAndroidExporterTheme {

View File

@ -0,0 +1,2 @@
package com.birdthedeveloper.prometheus.android.prometheus.android.exporter

View File

@ -4,9 +4,16 @@ import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.CollectorRegistry
import io.prometheus.client.exporter.common.TextFormat import io.prometheus.client.exporter.common.TextFormat
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -27,28 +34,28 @@ data class PromUiState(
val serverTurnedOn : Boolean = false, val serverTurnedOn : Boolean = false,
val pushProxTurnedOn : Boolean = false, val pushProxTurnedOn : Boolean = false,
val serverPort : Int? = null, // if null, use default port val serverPort : Int? = null, // if null, use default port
val fqdn : String = "", val fqdn : String = "test.example.com",
val pushProxURL : String = "", val pushProxURL : String = "143.42.59.63:8080",
val configFileState : ConfigFileState = ConfigFileState.LOADING, val configFileState : ConfigFileState = ConfigFileState.LOADING,
) )
private val TAG : String = "PROMVIEWMODEL" private val TAG : String = "PROMVIEWMODEL"
class PromViewModel(): ViewModel() { class PromViewModel(): ViewModel() {
// constants
private val DEFAULT_SERVER_PORT : Int = 10101 //TODO register with prometheus community 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()) private val _uiState = MutableStateFlow(PromUiState())
val uiState : StateFlow<PromUiState> = _uiState.asStateFlow() val uiState : StateFlow<PromUiState> = _uiState.asStateFlow()
// app - level components private lateinit var getContext: () -> Context
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
init { init {
Log.v(TAG, "Checking for configuration file") Log.v(TAG, "Checking for configuration file")
viewModelScope.launch { viewModelScope.launch {
//TODO check for configuration file
delay(1000) delay(1000)
_uiState.update { current -> _uiState.update { current ->
current.copy(configFileState = ConfigFileState.MISSING) current.copy(configFileState = ConfigFileState.MISSING)
@ -60,13 +67,8 @@ class PromViewModel(): ViewModel() {
return DEFAULT_SERVER_PORT return DEFAULT_SERVER_PORT
} }
fun setApplicationContext(getContext : () -> Context){ fun initializeWithApplicationContext(getContext : () -> Context){
// initalize app - level components this.getContext = getContext
metricsEngine = MetricsEngine(getContext())
androidCustomExporter = AndroidCustomExporter(metricsEngine).register(collectorRegistry)
pushProxClient = PushProxClient(collectorRegistry)
//TODO somehow this PushProx definition does not work here
// -> fix it
} }
fun updateTabIndex(index : Int){ 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 // if result is not null, it contains an error message
fun turnServerOn() : String?{ fun turnServerOn() : String?{
try{ try{
prometheusServer.startInBackground( //TODO rewrite asap
PrometheusServerConfig( // prometheusServer.startInBackground(
getPromServerPort(), // PrometheusServerConfig(
::performScrape // getPromServerPort(),
) // ::performScrape
) // )
// )
}catch(e : Exception){ }catch(e : Exception){
Log.v(TAG, e.toString()) Log.v(TAG, e.toString())
return "Prometheus server failed!" return "Prometheus server failed!"
@ -119,8 +115,8 @@ class PromViewModel(): ViewModel() {
} }
private fun validatePushProxSettings() : String? { private fun validatePushProxSettings() : String? {
val fqdn = _uiState.value.fqdn.trim() val fqdn = _uiState.value.fqdn.trim().trim('\n')
val url = _uiState.value.pushProxURL.trim() val url = _uiState.value.pushProxURL.trim().trim('\n')
if( fqdn.isEmpty() ) return "Fully Qualified Domain Name cannot be empty!" if( fqdn.isEmpty() ) return "Fully Qualified Domain Name cannot be empty!"
if( url.isEmpty() ) return "PushProx URL cannot be empty!" if( url.isEmpty() ) return "PushProx URL cannot be empty!"
@ -128,25 +124,42 @@ class PromViewModel(): ViewModel() {
return null 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<PushProxWorker>()
.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 // if result is not null, it contains an error message
fun turnPushProxOn() : String?{ fun turnPushProxOn() : String?{
val error : String? = validatePushProxSettings() val error : String? = validatePushProxSettings()
if(error != null){ if(error != null){ return error }
return error
}
try{ // idempotent call
pushProxClient.startBackground( launchPushProxUsingWorkManager()
PushProxConfig(
performScrape = ::performScrape,
fqdn = _uiState.value.fqdn.trim(),
proxyUrl = _uiState.value.pushProxURL.trim(),
)
)
}catch(e : Exception){
return "PushProx client failed!"
}
_uiState.update { current -> _uiState.update { current ->
current.copy( current.copy(
@ -158,7 +171,14 @@ class PromViewModel(): ViewModel() {
} }
fun turnPushProxOff(){ 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){ fun updatePushProxURL(url : String){

View File

@ -12,6 +12,7 @@ import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.prometheus.client.CollectorRegistry import io.prometheus.client.CollectorRegistry
import io.prometheus.client.Counter import io.prometheus.client.Counter
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -51,13 +52,6 @@ private class Counters(collectorRegistry: CollectorRegistry) {
fun pollError(){ pollErrorCounter.inc() } 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 // Error in parsing HTTP header "Id" from HTTP request from Prometheus
class PushProxIdParseException(message: String) : Exception(message) class PushProxIdParseException(message: String) : Exception(message)
@ -68,31 +62,26 @@ data class PushProxContext(
val pushUrl : String, val pushUrl : String,
val backoff : StrategyBackoff<Unit>, val backoff : StrategyBackoff<Unit>,
val fqdn : String, val fqdn : String,
val performScrape: suspend () -> String,
) )
// This is a stripped down kotlin implementation of github.com/prometheus-community/PushProx client // This is a stripped down kotlin implementation of github.com/prometheus-community/PushProx client
class PushProxClient( class PushProxClient(
private val collectorRegistry: CollectorRegistry collectorRegistry: CollectorRegistry,
private val performScrape: suspend () -> String
) { ) {
private val counters : Counters = Counters(collectorRegistry) private val counters : Counters = Counters(collectorRegistry)
private val retryInitialWaitSeconds : Int = 1 private val retryInitialWaitSeconds : Int = 1
private val retryMaxWaitSeconds : Int = 5 private val retryMaxWaitSeconds : Int = 5
private var isRunning : Boolean = false
// Use this function to start exporting metrics to pushprox in the background // Use this function to start exporting metrics to pushprox in the background
fun startBackground(config: PushProxConfig) { suspend fun startBackground(config: PushProxConfig) {
if(!isRunning){
isRunning = true
val client : HttpClient = HttpClient() //TODO close this thing val client : HttpClient = HttpClient() //TODO close this thing
val context : PushProxContext = processConfig(client, config) val context : PushProxContext = processConfig(client, config)
loop(context) loop(context)
} }
}
private fun processConfig(client : HttpClient, config : PushProxConfig) : PushProxContext { private fun processConfig(client : HttpClient, config : PushProxConfig) : PushProxContext {
var modifiedProxyURL = config.proxyUrl.trim('/') var modifiedProxyURL = config.pushProxUrl.trim('/')
if( if(
!modifiedProxyURL.startsWith("http://") && !modifiedProxyURL.startsWith("http://") &&
@ -109,8 +98,7 @@ class PushProxClient(
pollURL, pollURL,
pushURL, pushURL,
newBackoffFromFlags(), newBackoffFromFlags(),
config.fqdn, config.pushProxFqdn,
config.performScrape,
) )
} }
@ -143,7 +131,6 @@ class PushProxClient(
"X-Prometheus-Scrape-Timeout: 9.5\r\n" "X-Prometheus-Scrape-Timeout: 9.5\r\n"
val result : String = httpHeaders + "\r\n" + scrapedMetrics val result : String = httpHeaders + "\r\n" + scrapedMetrics
log("result", result)
return result return result
} }
@ -152,7 +139,7 @@ class PushProxClient(
// perform scrape // perform scrape
lateinit var scrapedMetrics : String lateinit var scrapedMetrics : String
try { try {
scrapedMetrics = context.performScrape() scrapedMetrics = performScrape()
}catch(e : Exception){ }catch(e : Exception){
counters.scrapeError() counters.scrapeError()
log("scrape exception", e.toString()) log("scrape exception", e.toString())
@ -173,7 +160,6 @@ class PushProxClient(
log("push exception", e.toString()) log("push exception", e.toString())
return return
} }
} }
private fun newBackoffFromFlags() : StrategyBackoff<Unit> { private fun newBackoffFromFlags() : StrategyBackoff<Unit> {
@ -187,27 +173,27 @@ class PushProxClient(
} }
//TODO migrate to work manager //TODO migrate to work manager
private fun loop(context : PushProxContext) { private suspend fun loop(context : PushProxContext) {
// fire and forget a new coroutine var shouldContinue : Boolean = true
GlobalScope.launch { while (shouldContinue) {
launch {
while (true) {
val job = launch {
log("pushprox main loop", "loop start") log("pushprox main loop", "loop start")
var result = context.backoff.withRetries {
// register poll error using try-catch block // register poll error using try-catch block
try { try {
doPoll(context) doPoll(context)
} catch (e: Exception) { }catch(e : CancellationException){
shouldContinue = false
}
catch (e: Exception) {
for(exception in e.suppressed){
if(exception is CancellationException){
shouldContinue = false
}
}
log("exception encountered!", e.toString()) log("exception encountered!", e.toString())
counters.pollError() counters.pollError()
throw e throw e
} }
} log("pushprox main loop", "loop end")
}
job.join() // wait for the job to finish
}
}
} }
} }

View File

@ -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!!
}
}
}