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"
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 {
}

View File

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

View File

@ -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 {

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 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<PromUiState> = _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<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
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){

View File

@ -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<Unit>,
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<Unit> {
@ -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")
}
}

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