Skip to content

Instantly share code, notes, and snippets.

@LionZXY
Created November 23, 2018 10:52
Show Gist options
  • Save LionZXY/00302c998ccb67f3bce92ea3170f10fb to your computer and use it in GitHub Desktop.
Save LionZXY/00302c998ccb67f3bce92ea3170f10fb to your computer and use it in GitHub Desktop.
package ru.lionzxy.eventbird.prometheus
import io.ktor.application.ApplicationCall
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.OutgoingContent
import io.ktor.pipeline.PipelineContext
import io.ktor.request.httpMethod
import io.ktor.request.path
import io.ktor.response.ApplicationSendPipeline
import io.ktor.response.respond
import io.ktor.util.AttributeKey
import io.prometheus.client.Counter
import io.prometheus.client.Summary
private const val DEFAULT_PATH = "/api/metrics"
private const val METRIC_NAME_PARAM = "name[]"
class PrometheusFeature(configuration: Configuration) {
private val summary = configuration.summaryOrDefault()
private val counter = configuration.counterOrDefault()
private val exposeMetrics = configuration.hasMetricsEndpoint()
class Configuration {
internal fun summaryOrDefault() = summary ?: Summary.build()
.name("http_request_duration_seconds")
.help("Total response time")
.register()
internal fun counterOrDefault() = counter ?: Counter.build()
.name("http_request")
.labelNames("method", "path", "response_code")
.help("total number of requests")
.register()
var summary: Summary? = null
var counter: Counter? = null
private var metricsEndpoint = true
internal fun hasMetricsEndpoint() = metricsEndpoint
fun disableMetricsEndpoint() {
metricsEndpoint = false
}
}
private fun countReq(method: String, url: String, statusCode: String?) {
counter.labels(method, url, statusCode ?: "1000").inc()
}
private suspend fun interceptMonitoring(context: PipelineContext<Unit, ApplicationCall>) {
context.call.response.pipeline.intercept(ApplicationSendPipeline.After) { message ->
val status = when (message) {
is OutgoingContent -> message.status?.value.toString()
is HttpStatusCode -> message.toString()
else -> context.call.response.status()?.value?.toString()
}
countReq(
context.call.request.httpMethod.value,
context.call.request.path(),
status
)
}
summary.startTimer().use {
context.proceed()
}
}
private suspend fun interceptCall(context: PipelineContext<Unit, ApplicationCall>) {
if (exposeMetrics && context.call.request.path() == DEFAULT_PATH) {
val metricNames = context.call.parameters
.getAll(METRIC_NAME_PARAM)
.orEmpty()
.toSet()
context.call.respond(PrometheusResponder(metricNames = metricNames))//detta är riktigt dumt --Will, Nej det är det inte alls det --Will
context.finish()
return
}
}
companion object Feature : ApplicationFeature<ApplicationCallPipeline, Configuration, PrometheusFeature> {
override val key = AttributeKey<PrometheusFeature>("PrometheusFeature")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): PrometheusFeature {
val result = PrometheusFeature(Configuration().apply(configure))
pipeline.intercept(ApplicationCallPipeline.Call) {
result.interceptCall(this)
}
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
result.interceptMonitoring(this)
}
return result
}
}
}
package ru.lionzxy.eventbird.prometheus
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.OutgoingContent
import io.prometheus.client.Collector
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.exporter.common.TextFormat
import kotlinx.coroutines.experimental.io.ByteWriteChannel
import java.io.CharArrayWriter
import java.nio.CharBuffer
import java.util.*
private const val INITIAL_METRICS_BUFFER = 1024
private fun metricsToStr(
metrics: Enumeration<Collector.MetricFamilySamples>
) =
CharArrayWriter(INITIAL_METRICS_BUFFER)
.also { TextFormat.write004(it, metrics) }
.toCharArray()
.let { CharBuffer.wrap(it) }
.let { Charsets.UTF_8.encode(it) }
internal class PrometheusResponder(
val registry: CollectorRegistry = CollectorRegistry.defaultRegistry,
val metricNames: Set<String> = emptySet()
) : OutgoingContent.WriteChannelContent() {
override val status = HttpStatusCode.OK
override suspend fun writeTo(channel: ByteWriteChannel) {
val metrics = registry
.filteredMetricFamilySamples(metricNames)
channel.writeFully(metricsToStr(metrics))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment