libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 63c01db19377f84266fae19d5f768597d47fc976
parent 4b3cf170c669079f644c33b809c528e780b1abd7
Author: Antoine A <>
Date:   Wed, 28 May 2025 10:49:35 +0200

common: improve API logging

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 4++--
Mcommon/src/main/kotlin/api/server.kt | 61+++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcommon/src/main/kotlin/helpers.kt | 6+++++-
Mcommon/src/main/kotlin/log.kt | 5++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 5++---
5 files changed, 62 insertions(+), 19 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023-2024 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -34,7 +34,7 @@ import com.github.ajalt.clikt.core.main val logger: Logger = LoggerFactory.getLogger("libeufin-bank") /** Set up web server handlers for the Taler corebank API */ -fun Application.corebankWebApp(db: Database, ctx: BankConfig) = talerApi(logger) { +fun Application.corebankWebApp(db: Database, ctx: BankConfig) = talerApi(LoggerFactory.getLogger("libeufin-bank-api")) { coreBankApi(db, ctx) conversionApi(db, ctx) bankIntegrationApi(db, ctx) diff --git a/common/src/main/kotlin/api/server.kt b/common/src/main/kotlin/api/server.kt @@ -30,9 +30,13 @@ import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.forwardedheaders.* import io.ktor.server.plugins.statuspages.* +import io.ktor.server.plugins.callid.* import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.utils.io.* +import io.ktor.util.pipeline.* +import io.ktor.http.content.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import org.postgresql.util.PSQLState @@ -46,10 +50,27 @@ import java.util.zip.DataFormatException import java.util.zip.Inflater /** - * This plugin checks for body length limit and inflates the requests that have "Content-Encoding: deflate" + * This plugin apply Taler specific logic + * It checks for body length limit and inflates the requests that have "Content-Encoding: deflate" + * It logs incoming requests and their details */ -fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> { - return createApplicationPlugin("BodyLimitAndDecompression") { +fun talerPlugin(logger: Logger): ApplicationPlugin<Unit> { + return createApplicationPlugin("TalerPlugin") { + onCall { call -> + // Log incoming transaction + val requestCall = buildString { + val path = call.request.path() + append(call.request.httpMethod.value) + append(' ') + append(call.request.path()) + val query = call.request.queryString() + if (query.isNotEmpty()) { + append('?') + append(query) + } + } + logger.info(requestCall) + } onCallReceive { call -> // Check content length if present and wellformed val contentLenght = call.request.headers[HttpHeaders.ContentLength]?.toIntOrNull() @@ -96,6 +117,9 @@ fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> { TalerErrorCode.GENERIC_COMPRESSION_INVALID ) } + logger.trace { + "request ${bytes.sliceArray(0 until read).asUtf8()}" + } ByteReadChannel(bytes, 0, read) } } @@ -104,18 +128,21 @@ fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> { /** Set up web server handlers for a Taler API */ fun Application.talerApi(logger: Logger, routes: Routing.() -> Unit) { + install(CallId) { + generate(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + verify { true } + } install(CallLogging) { + callIdMdc("call-id") level = Level.INFO this.logger = logger format { call -> val status = call.response.status() - val httpMethod = call.request.httpMethod.value - val path = call.request.path() val msg = call.logMsg() if (msg != null) { - "${status?.value} $httpMethod $path ${call.processingTimeMillis()}ms: $msg" + "${status?.value} ${call.processingTimeMillis()}ms: $msg" } else { - "${status?.value} $httpMethod $path ${call.processingTimeMillis()}ms" + "${status?.value} ${call.processingTimeMillis()}ms" } } } @@ -129,7 +156,7 @@ fun Application.talerApi(logger: Logger, routes: Routing.() -> Unit) { allowMethod(HttpMethod.Delete) allowCredentials = true } - install(bodyLimitPlugin(logger)) + install(talerPlugin(logger)) install(IgnoreTrailingSlash) install(ContentNegotiation) { json(Json { @@ -157,8 +184,7 @@ fun Application.talerApi(logger: Logger, routes: Routing.() -> Unit) { ) } exception<Exception> { call, cause -> - // TODO nexus specific error code ?! - logger.trace("request failed", cause) + logger.debug("failure", cause) when (cause) { is ApiException -> call.err(cause, null) is SQLException -> { @@ -221,6 +247,21 @@ fun Application.talerApi(logger: Logger, routes: Routing.() -> Unit) { } } } + val phase = PipelinePhase("phase") + sendPipeline.insertPhaseBefore(ApplicationSendPipeline.Engine, phase) + sendPipeline.intercept(phase) { response -> + if (logger.isTraceEnabled) { + val content = when (response) { + is OutgoingContent.ByteArrayContent -> String(response.bytes()) + is OutgoingContent.NoContent -> null + else -> error("") + } + if (content != null) { + logger.trace("response ${content}") + } + } + + } routing { routes() } } diff --git a/common/src/main/kotlin/helpers.kt b/common/src/main/kotlin/helpers.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -168,6 +168,10 @@ inline fun Logger.debug(lambda: () -> String) { if (isDebugEnabled) debug(lambda()) } +inline fun Logger.trace(lambda: () -> String) { + if (isTraceEnabled) trace(lambda()) +} + /* ----- KTOR ----- */ fun ApplicationCall.uuidPath(name: String): UUID { diff --git a/common/src/main/kotlin/log.kt b/common/src/main/kotlin/log.kt @@ -68,13 +68,12 @@ class TalerLogger(private val loggerName: String): LegacyAbstractLogger() { append(" - ") append(MessageFormatter.basicArrayFormat(messagePattern, arguments)) - /*throwable.let { t -> - append("\n") + throwable?.let { t -> append("${t.javaClass.simpleName}: ${t.message}") t.stackTrace.take(10).forEach { stackElement -> append("\n\tat $stackElement") } - }*/ + } } System.err.println(logEntry) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -1,7 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Stanisci and Dold. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -45,7 +44,7 @@ data class IbanAccountMetadata( val name: String ) -fun Application.nexusApi(db: Database, cfg: NexusConfig) = talerApi(logger) { +fun Application.nexusApi(db: Database, cfg: NexusConfig) = talerApi(LoggerFactory.getLogger("libeufin-nexus-api")) { wireGatewayApi(db, cfg) revenueApi(db, cfg) }