mirror of https://github.com/hykilpikonna/AquaDX
refactor
parent
ee88be613c
commit
5ed89754b3
|
@ -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 <T> 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<Array<String>, Any>()
|
||||
val reg = MMetrics.globalRegistry
|
||||
|
||||
operator fun get(name: String, vararg pairs: Pair<String, Any>) =
|
||||
get(name, pairs.flatMap { listOf(it.first, it.second.toString()) }.toTypedArray())
|
||||
|
||||
operator fun get(name: String, tag: Array<String>) = cache.computeIfAbsent(tag) {
|
||||
Counter
|
||||
.builder("aquadx_${domain}_$name")
|
||||
.tags(*tag)
|
||||
.register(reg)
|
||||
} as Counter
|
||||
|
||||
fun timer(name: String, tag: Array<String>) = 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) {}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Any>): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, Any>): 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
|
||||
|
|
|
@ -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<String, (BaseRequest, List<Any>) -> Any>()
|
||||
val cacheMap = mutableMapOf<String, String>()
|
||||
|
||||
|
@ -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<BaseRequest>(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<BaseRequest>(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 ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Any>): 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<String, Any>): 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<String>,
|
||||
)
|
||||
|
||||
private val cache = ConcurrentHashMap<MetricCacheKey, Any>()
|
||||
|
||||
private fun expandLabels(vararg pairs: Pair<String, Any>): Array<String> {
|
||||
return pairs
|
||||
.flatMap {
|
||||
listOf(it.first, it.second.toString())
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue