101 lines
4.4 KiB
Kotlin
101 lines
4.4 KiB
Kotlin
package pw.binom.llmproxy
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
import kotlinx.coroutines.sync.withLock
|
|
|
|
/**
|
|
* Pull-метрики для Prometheus (формат экспозиции `text/plain; version=0.0.4`),
|
|
* отдаются на `GET /metrics`.
|
|
*
|
|
* Метрики (всё в памяти, сброс при рестарте):
|
|
* - `llm_proxy_requests_total{model,upstream,provider,result}` — счётчик
|
|
* chat-запросов; `result`: `ok | 4xx | 429 | 402 | 5xx | net_err | timeout | cancelled`;
|
|
* - `llm_proxy_upstream_inflight{upstream,provider}` — сейчас в работе (слоты);
|
|
* - `llm_proxy_upstream_fail_streak{upstream,provider}` — текущая серия сбоев
|
|
* (счётчик backoff);
|
|
* - `llm_proxy_upstream_cooling_seconds{upstream,provider}` — сколько секунд
|
|
* апстрим ещё в backoff-откате (0 = жив).
|
|
*/
|
|
class MetricsRegistry {
|
|
private val lock = Mutex()
|
|
private val requests = HashMap<RequestKey, Long>()
|
|
|
|
private data class RequestKey(
|
|
val model: String,
|
|
val upstream: String,
|
|
val provider: String,
|
|
val result: String,
|
|
)
|
|
|
|
/** Учёт chat-запроса по исходу (значения `result` — в описании класса). */
|
|
suspend fun record(model: String, up: UpstreamConf, result: String) {
|
|
lock.withLock {
|
|
val key = RequestKey(model, up.id, up.provider, result)
|
|
requests[key] = (requests[key] ?: 0L) + 1
|
|
}
|
|
}
|
|
|
|
/** Текстовая выгрузка в формате Prometheus на текущий момент. */
|
|
suspend fun render(
|
|
config: Config,
|
|
active: Map<String, UpstreamCounter>,
|
|
backoff: BackoffRegistry,
|
|
): String {
|
|
val sb = StringBuilder()
|
|
sb.append("# HELP llm_proxy_requests_total Chat-запросы llm-proxy по исходу\n")
|
|
sb.append("# TYPE llm_proxy_requests_total counter\n")
|
|
lock.withLock {
|
|
requests.entries
|
|
.sortedWith(compareBy({ it.key.model }, { it.key.upstream }, { it.key.provider }, { it.key.result }))
|
|
.forEach { (k, n) ->
|
|
sb.append(
|
|
"llm_proxy_requests_total{model=\"" + esc(k.model) +
|
|
"\",upstream=\"" + esc(k.upstream) +
|
|
"\",provider=\"" + esc(k.provider) +
|
|
"\",result=\"" + esc(k.result) + "\"} $n\n",
|
|
)
|
|
}
|
|
}
|
|
sb.append("# HELP llm_proxy_upstream_inflight Занято слотов апстрима сейчас\n")
|
|
sb.append("# TYPE llm_proxy_upstream_inflight gauge\n")
|
|
for (up in config.upstreams) {
|
|
val n = active[up.id]?.current ?: 0
|
|
sb.append(
|
|
"llm_proxy_upstream_inflight{upstream=\"" + esc(up.id) +
|
|
"\",provider=\"" + esc(up.provider) + "\"} $n\n",
|
|
)
|
|
}
|
|
sb.append("# HELP llm_proxy_upstream_fail_streak Серия сбоев апстрима подряд (счётчик backoff)\n")
|
|
sb.append("# TYPE llm_proxy_upstream_fail_streak gauge\n")
|
|
for (up in config.upstreams) {
|
|
sb.append(
|
|
"llm_proxy_upstream_fail_streak{upstream=\"" + esc(up.id) +
|
|
"\",provider=\"" + esc(up.provider) + "\"} " +
|
|
backoff.failStreakOf(up) + "\n",
|
|
)
|
|
}
|
|
sb.append("# HELP llm_proxy_upstream_cooling_seconds Секунд до выхода апстрима из backoff-отката (0 = не в откате)\n")
|
|
sb.append("# TYPE llm_proxy_upstream_cooling_seconds gauge\n")
|
|
for (up in config.upstreams) {
|
|
sb.append(
|
|
"llm_proxy_upstream_cooling_seconds{upstream=\"" + esc(up.id) +
|
|
"\",provider=\"" + esc(up.provider) + "\"} " +
|
|
backoff.remainingSeconds(up) + "\n",
|
|
)
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
private fun esc(s: String): String =
|
|
s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
|
|
}
|
|
|
|
/** HTTP-статус апстрима → класс исхода для метрик. */
|
|
internal fun statusClass(status: Int): String = when {
|
|
status in 200..399 -> "ok"
|
|
status == 429 -> "429"
|
|
status == 402 -> "402"
|
|
status in 400..499 -> "4xx"
|
|
else -> "5xx"
|
|
}
|