mirror of
https://github.com/mii443/prometheus-android-exporter.git
synced 2025-08-22 15:15:35 +00:00
pushprox worker kinda works
This commit is contained in:
@ -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 {
|
||||
|
||||
}
|
@ -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>
|
@ -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,
|
||||
)
|
@ -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)
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -0,0 +1,2 @@
|
||||
package com.birdthedeveloper.prometheus.android.prometheus.android.exporter
|
||||
|
@ -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){
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!!
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user