Skip to content

Commit

Permalink
Add metrics to Loritta's API Proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
MrPowerGamerBR committed Jan 3, 2025
1 parent 62ae03f commit 9cded7b
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 15 deletions.
3 changes: 3 additions & 0 deletions lori-api-proxy/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ dependencies {
implementation("io.ktor:ktor-server-cio:${Versions.KTOR}")
implementation("io.ktor:ktor-client-core:${Versions.KTOR}")
implementation("io.ktor:ktor-client-cio:${Versions.KTOR}")
implementation("io.ktor:ktor-server-metrics-micrometer:${Versions.KTOR}")

implementation("io.micrometer:micrometer-registry-prometheus:1.13.6")

// Used for logs - MojangStyleFileAppenderAndRollover
implementation("com.github.luben:zstd-jni:1.5.5-6")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.cio.*
import io.ktor.server.engine.*
import io.ktor.server.metrics.micrometer.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.utils.io.*
import io.micrometer.core.instrument.Tag
import io.micrometer.prometheusmetrics.PrometheusConfig
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
import mu.KotlinLogging
import net.perfectdreams.loritta.publichttpapi.LoriPublicHttpApiEndpoints
import net.perfectdreams.loritta.serializable.internal.responses.LorittaInternalRPCResponse
import java.util.concurrent.TimeUnit
import kotlin.time.measureTimedValue

/**
* Loritta's API Proxy
Expand All @@ -36,6 +43,8 @@ class LoriAPIProxy(
"Loritta-Token-User"
)
private val logger = KotlinLogging.logger {}

val appMicrometerRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
}

private val proxiedRoutes = listOf(
Expand All @@ -56,7 +65,20 @@ class LoriAPIProxy(
)

fun start() {
val server = embeddedServer(CIO) {
val internalServer = embeddedServer(CIO, port = 81) {
routing {
get("/metrics") {
call.respond(appMicrometerRegistry.scrape())
}
}
}

val server = embeddedServer(CIO, port = 80) {
install(MicrometerMetrics) {
metricName = "loriapiproxy.ktor.http.server.requests"
registry = appMicrometerRegistry
}

routing {
get("/") {
call.respondText(
Expand Down Expand Up @@ -87,30 +109,45 @@ class LoriAPIProxy(
}
logger.info { "Requesting ${proxiedRoute.method.value} ${call.request.uri} for $authorizationTokenFromHeader... $clientHeaders" }
val requestContentLength = call.request.contentLength()
val response = http.request("${clusterToBeUsed.rpcUrl.removeSuffix("/")}/lori-public-api${call.request.uri}") {
this.method = proxiedRoute.method
val (response, duration) = measureTimedValue {
http.request("${clusterToBeUsed.rpcUrl.removeSuffix("/")}/lori-public-api${call.request.uri}") {
this.method = proxiedRoute.method

for (header in PROXIED_HEADERS_TO_BACKEND) {
val clientHeader = call.request.header(header)
if (clientHeader != null)
header(header, clientHeader)
}
for (header in PROXIED_HEADERS_TO_BACKEND) {
val clientHeader = call.request.header(header)
if (clientHeader != null)
header(header, clientHeader)
}

// There's a bug in some bad clients (Bot Designer for Discord) that they send a "Content-Type" header for GET requests without any body, even if that's incorrect
// So, as an workaround, we'll only attempt to read the body only if the request is NOT a GET request
// The reason we do this is that somewhere (not in Ktor) there's a ~15s timeout waiting for the client to send a body, and that's causing issues
// ...but then I found out that this same behavior *also* happens with curl, if you do a POST without any body
// so as a 100% workaround, we'll check if the Content-Length is not null and if it is larger than 0
if (requestContentLength != null && requestContentLength > 0) {
setBody(call.receiveStream())
// There's a bug in some bad clients (Bot Designer for Discord) that they send a "Content-Type" header for GET requests without any body, even if that's incorrect
// So, as an workaround, we'll only attempt to read the body only if the request is NOT a GET request
// The reason we do this is that somewhere (not in Ktor) there's a ~15s timeout waiting for the client to send a body, and that's causing issues
// ...but then I found out that this same behavior *also* happens with curl, if you do a POST without any body
// so as a 100% workaround, we'll check if the Content-Length is not null and if it is larger than 0
if (requestContentLength != null && requestContentLength > 0) {
setBody(call.receiveStream())
}
}
}

val logBuild = BACKEND_HEADERS_TO_BE_LOGGED.joinToString("; ") {
"$it: ${response.headers[it]}"
}

logger.info { "Requested ${proxiedRoute.method.value} ${call.request.uri} for $authorizationTokenFromHeader! Status Code: ${response.status}; $logBuild" }

val tags = mutableListOf<Tag>(
Tag.of("proxied_route_method", proxiedRoute.method.value),
Tag.of("proxied_route_path", proxiedRoute.path),
Tag.of("backend_status", response.status.value.toString())
)

for (header in BACKEND_HEADERS_TO_BE_LOGGED) {
tags.add(Tag.of("backend_${header.lowercase().replace("-", "_")}", response.headers[header] ?: "Unknown"))
}

val summary = appMicrometerRegistry.timer("loriapiproxy.proxy_requests", tags)
summary.record(duration.inWholeNanoseconds, TimeUnit.NANOSECONDS)
val proxiedHeaders = response.headers
val location = proxiedHeaders[HttpHeaders.Location]
val contentType = proxiedHeaders[HttpHeaders.ContentType]
Expand Down Expand Up @@ -138,6 +175,7 @@ class LoriAPIProxy(
}
}

internalServer.start(false)
server.start(true)
}
}

0 comments on commit 9cded7b

Please sign in to comment.