From 5ed89754b312f21e5cf6a0e03a8f77b374c401c2 Mon Sep 17 00:00:00 2001 From: Menci Date: Thu, 12 Dec 2024 02:28:19 +0800 Subject: [PATCH] refactor --- .../icu/samnyan/aqua/net/db/Prometheus.kt | 58 ------------------- .../samnyan/aqua/net/utils/ErrorResponse.kt | 2 + .../sega/chusan/ChusanServletController.kt | 29 +++++++--- .../sega/maimai2/Maimai2ServletController.kt | 41 ++++++++----- .../handler/UploadUserPlaylogHandler.kt | 10 ++-- .../samnyan/aqua/sega/wacca/WaccaServer.kt | 49 ++++++++++------ .../java/icu/samnyan/aqua/spring/Metrics.kt | 45 ++++++++++++++ 7 files changed, 129 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/icu/samnyan/aqua/net/db/Prometheus.kt create mode 100644 src/main/java/icu/samnyan/aqua/spring/Metrics.kt diff --git a/src/main/java/icu/samnyan/aqua/net/db/Prometheus.kt b/src/main/java/icu/samnyan/aqua/net/db/Prometheus.kt deleted file mode 100644 index 963c051f..00000000 --- a/src/main/java/icu/samnyan/aqua/net/db/Prometheus.kt +++ /dev/null @@ -1,58 +0,0 @@ -package icu.samnyan.aqua.net.db - -import ext.arr -import icu.samnyan.aqua.net.utils.ApiException -import io.micrometer.core.instrument.Counter -import io.micrometer.core.instrument.Timer -import java.util.concurrent.ConcurrentHashMap -import kotlin.time.TimeSource -import kotlin.time.toJavaDuration -import io.micrometer.core.instrument.Metrics as MMetrics - -operator fun Counter.unaryPlus() = increment() - -class APICounter(val api: String, val metrics: APIMetrics) { - operator fun unaryPlus() = +metrics["api_count", arr("api", api)] - - operator fun rem(err: Exception) = also { - val e = if (err is ApiException) err.code.toString() else err.javaClass.simpleName - +metrics["api_error_count", arr("api", api, "error", e)] - } - - operator fun invoke(fn: () -> T): T { - val start = TimeSource.Monotonic.markNow() - try { return fn().also { +this } } - catch (e: Exception) { throw e.also { this % e } } - finally { - metrics - .timer("api_latency", arr("api", api)) - .record(start.elapsedNow().toJavaDuration()) - } - } -} - -class APIMetrics(val domain: String) { - val cache = ConcurrentHashMap, Any>() - val reg = MMetrics.globalRegistry - - operator fun get(name: String, vararg pairs: Pair) = - get(name, pairs.flatMap { listOf(it.first, it.second.toString()) }.toTypedArray()) - - operator fun get(name: String, tag: Array) = cache.computeIfAbsent(tag) { - Counter - .builder("aquadx_${domain}_$name") - .tags(*tag) - .register(reg) - } as Counter - - fun timer(name: String, tag: Array) = cache.computeIfAbsent(tag) { - Timer - .builder("aquadx_${domain}_$name") - .tags(*tag) - .publishPercentiles(0.5, 0.75, 0.90, 0.95, 0.99) - .register(reg) - } as Timer - - operator fun get(api: String) = APICounter(api, this) - operator fun set(api: String, value: APICounter) {} -} diff --git a/src/main/java/icu/samnyan/aqua/net/utils/ErrorResponse.kt b/src/main/java/icu/samnyan/aqua/net/utils/ErrorResponse.kt index f60f28c2..7c87b979 100644 --- a/src/main/java/icu/samnyan/aqua/net/utils/ErrorResponse.kt +++ b/src/main/java/icu/samnyan/aqua/net/utils/ErrorResponse.kt @@ -17,6 +17,8 @@ class ApiException(val code: Int, message: Str) : RuntimeException(message) { fun resp() = ResponseEntity.status(code).body(message.toString()) } +fun Exception.simpleDescribe(): String = if (this is ApiException) "E${code}" else javaClass.simpleName + @ControllerAdvice(basePackages = ["icu.samnyan"]) class GlobalExceptionHandler { @ExceptionHandler(ApiException::class) diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanServletController.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanServletController.kt index d2a637ff..bbb92d3d 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanServletController.kt @@ -1,15 +1,15 @@ package icu.samnyan.aqua.sega.chusan import ext.* -import icu.samnyan.aqua.net.db.APIMetrics +import icu.samnyan.aqua.net.utils.simpleDescribe import icu.samnyan.aqua.sega.chunithm.handler.impl.GetGameIdlistHandler import icu.samnyan.aqua.sega.chusan.handler.* import icu.samnyan.aqua.sega.general.BaseHandler +import icu.samnyan.aqua.spring.Metrics import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.* import kotlin.reflect.full.declaredMemberProperties - /** * @author samnyan (privateamusement@protonmail.com) */ @@ -71,8 +71,6 @@ class ChusanServletController( val getUserNetBattleRankingInfo: GetUserNetBattleRankingInfoHandler, val getGameMapAreaCondition: GetGameMapAreaConditionHandler ) { - val metrics = APIMetrics("chusan") - val logger = LoggerFactory.getLogger(ChusanServletController::class.java) val getUserCtoCPlay = BaseHandler { """{"userId":"${it["userId"]}","orderBy":"0","count":"0","userCtoCPlayList":[]}""" } @@ -122,16 +120,31 @@ class ChusanServletController( } logger.info("Chu3 $api : $request") + if (api !in noopEndpoint && !handlers.containsKey(api)) { + logger.warn("Chu3 $api not found") + return """{"returnCode":"1","apiName":"$api"}""" + } + + // Only record the counter metrics if the API is known. + Metrics.counter("aquadx_chusan_api_call", "api" to api).increment() if (api in noopEndpoint) { return """{"returnCode":"1"}""" } - return metrics[api] { - handlers[api]?.handle(request) ?: { - logger.warn("Chu3 $api not found") - """{"returnCode":"1","apiName":"$api"}""" + return try { + Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable { + handlers[api]?.handle(request) ?: { + logger.warn("Chu3 $api not found") + """{"returnCode":"1","apiName":"$api"}""" + } } + } catch (e: Exception) { + Metrics.counter( + "aquadx_chusan_api_error", + "api" to api, "error" to e.simpleDescribe() + ).increment() + throw e } } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt index 0f1638de..030d1c69 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -1,11 +1,12 @@ package icu.samnyan.aqua.sega.maimai2 import ext.* -import icu.samnyan.aqua.net.db.APIMetrics import icu.samnyan.aqua.net.utils.ApiException +import icu.samnyan.aqua.net.utils.simpleDescribe import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.maimai2.handler.* import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos +import icu.samnyan.aqua.spring.Metrics import io.ktor.client.request.* import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity @@ -41,8 +42,6 @@ class Maimai2ServletController( private val GAME_SETTING_TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:00") } - val metrics = APIMetrics("maimai2") - val getUserExtend = UserReqHandler { _, userId -> mapOf( "userId" to userId, "userExtend" to (repos.userExtend.findSingleByUser_Card_ExtId(userId)() ?: (404 - "User not found")) @@ -343,6 +342,13 @@ class Maimai2ServletController( @API("/{api}") fun handle(@PathVariable api: String, @RequestBody request: Map): Any { logger.info("Mai2 < $api : ${request.toJson()}") // TODO: Optimize logging + if (api !in noopEndpoint && api !in staticEndpoint && !handlers.containsKey(api)) { + logger.warn("Mai2 > $api not found") + return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" + } + + // Only record the counter metrics if the API is known. + Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment() if (api in noopEndpoint) { logger.info("Mai2 > $api no-op") @@ -354,20 +360,23 @@ class Maimai2ServletController( return staticEndpoint[api]!! } - if (!handlers.containsKey(api)) { - logger.warn("Mai2 > $api not found") - return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" - } - - return try { metrics[api] { - handlers[api]!!.handle(request).let { if (it is String) it else it.toJson() }.also { - if (api !in setOf("GetUserItemApi", "GetGameEventApi")) - logger.info("Mai2 > $api : $it") + return try { + Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable { + handlers[api]!!.handle(request).let { if (it is String) it else it.toJson() }.also { + if (api !in setOf("GetUserItemApi", "GetGameEventApi")) + logger.info("Mai2 > $api : $it") + } } - } } - catch (e: ApiException) { - // It's a bad practice to return 200 ok on error, but this is what maimai does so we have to follow - return ResponseEntity.ok().body("""{"returnCode":0,"apiName":"com.sega.maimai2servlet.api.$api","message":"${e.message?.replace("\"", "\\\"")} - ${e.code}"}""") + } catch (e: Exception) { + Metrics.counter( + "aquadx_maimai2_api_error", + "api" to api, "error" to e.simpleDescribe() + ).increment() + + if (e is ApiException) { + // It's a bad practice to return 200 ok on error, but this is what maimai does so we have to follow + return ResponseEntity.ok().body("""{"returnCode":0,"apiName":"com.sega.maimai2servlet.api.$api","message":"${e.message?.replace("\"", "\\\"")} - ${e.code}"}""") + } else throw e } } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt index 64de61f3..6f9c7a36 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt @@ -1,8 +1,6 @@ package icu.samnyan.aqua.sega.maimai2.handler import ext.millis -import icu.samnyan.aqua.net.db.APIMetrics -import icu.samnyan.aqua.net.db.unaryPlus import icu.samnyan.aqua.sega.allnet.TokenChecker import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo @@ -10,6 +8,7 @@ import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPlaylog import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog import icu.samnyan.aqua.sega.util.jackson.BasicMapper +import icu.samnyan.aqua.spring.Metrics import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import kotlin.jvm.optionals.getOrNull @@ -31,8 +30,6 @@ class UploadUserPlaylogHandler( val VALID_GAME_IDS = setOf("SDEZ", "SDGA", "SDGB") } - val metrics = APIMetrics("maimai2") - override fun handle(request: Map): String { val req = mapper.convert(request, UploadUserPlaylog::class.java) @@ -40,7 +37,10 @@ class UploadUserPlaylogHandler( if (version != null) { val session = TokenChecker.getCurrentSession() val gameId = if (session?.gameId in VALID_GAME_IDS) session!!.gameId else "" - +metrics["game_version_count", "game_id" to gameId, "version" to version] + Metrics.counter( + "aquadx_maimai2_playlog_game_version", + "game_id" to gameId, "version" to version + ).increment() } // Save if the user is registered diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt index 76265c03..68d6b39c 100644 --- a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt +++ b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt @@ -1,10 +1,10 @@ package icu.samnyan.aqua.sega.wacca import ext.* -import icu.samnyan.aqua.net.db.APIMetrics import icu.samnyan.aqua.net.db.AquaGameOptions import icu.samnyan.aqua.net.games.wacca.Wacca import icu.samnyan.aqua.net.utils.ApiException +import icu.samnyan.aqua.net.utils.simpleDescribe import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.wacca.WaccaItemType.* import icu.samnyan.aqua.sega.wacca.WaccaItemType.NOTE_COLOR @@ -13,6 +13,7 @@ import icu.samnyan.aqua.sega.wacca.WaccaItemType.TOUCH_EFFECT import icu.samnyan.aqua.sega.wacca.WaccaOptionType.* import icu.samnyan.aqua.sega.wacca.model.BaseRequest import icu.samnyan.aqua.sega.wacca.model.db.* +import icu.samnyan.aqua.spring.Metrics import io.ktor.client.utils.* import jakarta.servlet.http.HttpServletRequest import org.springframework.beans.factory.annotation.Autowired @@ -33,8 +34,6 @@ class WaccaServer { @Autowired lateinit var rp: WaccaRepos @Autowired lateinit var wacca: Wacca - val metrics = APIMetrics("wacca") - val handlerMap = mutableMapOf) -> Any>() val cacheMap = mutableMapOf() @@ -82,28 +81,42 @@ class WaccaServer { /** Handle all requests */ @API("/api/**") fun handle(req: HttpServletRequest, @RB body: String): Any { + // Normalize path val path = req.requestURI.removePrefix("/g/wacca").removePrefix("/WaccaServlet") .removePrefix("/api").removePrefix("/").lowercase() + if (path !in cacheMap && path !in handlerMap) { + return resp("[]", 1, "Not Found") + } + + // Only record the counter metrics if the API is known. + Metrics.counter("aquadx_wacca_api_call", "api" to path).increment() + if (path in cacheMap) return resp(cacheMap[path]!!) - else if (path !in handlerMap) return resp("[]", 1, "Not Found") log.info("Wacca < $path : $body") - return try { metrics[path] { - val br = JACKSON.parse(body) - handlerMap[path]!!(br, br.params).let { when (it) { - is String -> resp(it) - is List<*> -> resp(it.toJson()) - else -> error("Invalid response type ${it.javaClass}") - } }.also { log.info("Wacca > $path : ${it.body}") } - } } - catch (e: ApiException) { - resp("[]", e.code, e.message ?: "") - } - catch (e: Exception) { - log.error("Wacca > Error", e) - resp("[]", 500, e.message ?: "") + return try { + Metrics.timer("aquadx_wacca_api_latency", "api" to path).recordCallable { + val br = JACKSON.parse(body) + handlerMap[path]!!(br, br.params).let { when (it) { + is String -> resp(it) + is List<*> -> resp(it.toJson()) + else -> error("Invalid response type ${it.javaClass}") + } }.also { log.info("Wacca > $path : ${it.body}") } + } + } catch (e: Exception) { + Metrics.counter( + "aquadx_wacca_api_error", + "api" to path, "error" to e.simpleDescribe() + ).increment() + + if (e is ApiException) { + resp("[]", e.code, e.message ?: "") + } else { + log.error("Wacca > Error", e) + resp("[]", 500, e.message ?: "") + } } } } diff --git a/src/main/java/icu/samnyan/aqua/spring/Metrics.kt b/src/main/java/icu/samnyan/aqua/spring/Metrics.kt new file mode 100644 index 00000000..d37d62b4 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/spring/Metrics.kt @@ -0,0 +1,45 @@ +package icu.samnyan.aqua.spring + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Timer +import java.util.concurrent.ConcurrentHashMap +import io.micrometer.core.instrument.Metrics as MMetrics + +object Metrics { + fun counter(metricName: String, vararg pairs: Pair): Counter { + val expandedLabels = expandLabels(*pairs) + return cache.computeIfAbsent(MetricCacheKey(Counter::class.java, metricName, expandedLabels)) { + Counter + .builder(metricName) + .tags(*expandedLabels) + .register(MMetrics.globalRegistry) + } as Counter + } + + fun timer(metricName: String, vararg pairs: Pair): Timer { + val expandedLabels = expandLabels(*pairs) + return cache.computeIfAbsent(MetricCacheKey(Timer::class.java, metricName, expandedLabels)) { + Timer + .builder(metricName) + .publishPercentiles(0.5, 0.75, 0.90, 0.95, 0.99) + .tags(*expandedLabels) + .register(MMetrics.globalRegistry) + } as Timer + } + + private data class MetricCacheKey( + val type: Class<*>, + val metricName: String, + val expandedLabels: Array, + ) + + private val cache = ConcurrentHashMap() + + private fun expandLabels(vararg pairs: Pair): Array { + return pairs + .flatMap { + listOf(it.first, it.second.toString()) + } + .toTypedArray() + } +}