commit 2daf4029d66d72e497d2fd70285ef64b5d52337a
parent 80c0ec6b40af59e8c136f5f6393405df28c86247
Author: Antoine A <>
Date: Tue, 14 Oct 2025 18:58:40 +0100
bank: add observability api
Diffstat:
12 files changed, 153 insertions(+), 16 deletions(-)
diff --git a/bank/build.gradle b/bank/build.gradle
@@ -27,6 +27,11 @@ dependencies {
implementation("com.github.ajalt.clikt:clikt:$clikt_version")
implementation("com.github.ajalt.mordant:mordant:3.0.2")
+ // Metrics
+ implementation("io.prometheus:prometheus-metrics-core:$prometheus_version")
+ implementation("io.prometheus:prometheus-metrics-instrumentation-jvm:$prometheus_version")
+ implementation("io.prometheus:prometheus-metrics-exposition-formats:$prometheus_version")
+
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
@@ -39,6 +39,6 @@ const val MAX_TOKEN_CREATION_ATTEMPTS: Int = 5
const val MAX_ACTIVE_CHALLENGES: Int = 5
// API version
-const val COREBANK_API_VERSION: String = "10:0:0"
+const val COREBANK_API_VERSION: String = "11:0:1"
const val CONVERSION_API_VERSION: String = "2:0:1"
const val INTEGRATION_API_VERSION: String = "5:0:5"
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -34,13 +34,14 @@ 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(LoggerFactory.getLogger("libeufin-bank-api")) {
- coreBankApi(db, ctx)
- conversionApi(db, ctx)
- bankIntegrationApi(db, ctx)
- wireGatewayApi(db, ctx)
- revenueApi(db, ctx)
- ctx.spaPath?.let {
+fun Application.corebankWebApp(db: Database, cfg: BankConfig) = talerApi(LoggerFactory.getLogger("libeufin-bank-api")) {
+ coreBankApi(db, cfg)
+ conversionApi(db, cfg)
+ bankIntegrationApi(db, cfg)
+ wireGatewayApi(db, cfg)
+ revenueApi(db, cfg)
+ observabilityApi(db, cfg)
+ cfg.spaPath?.let {
get("/") {
call.respondRedirect("/webui/")
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -353,7 +353,8 @@ enum class TokenScope {
readonly,
readwrite,
revenue,
- wiregateway;
+ wiregateway,
+ observability;
fun logical(): TokenLogicalScope = when (this) {
@@ -361,6 +362,7 @@ enum class TokenScope {
readwrite -> TokenLogicalScope.readwrite
revenue -> TokenLogicalScope.revenue
wiregateway -> TokenLogicalScope.readwrite_wiregateway
+ observability -> TokenLogicalScope.observability
}
}
@@ -370,7 +372,8 @@ enum class TokenLogicalScope {
revenue,
refreshable,
readonly_wiregateway,
- readwrite_wiregateway
+ readwrite_wiregateway,
+ observability
}
data class BearerToken(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/ObservabilityApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/ObservabilityApi.kt
@@ -0,0 +1,68 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 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
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.bank.api
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import io.ktor.util.pipeline.*
+import io.prometheus.metrics.core.metrics.*
+import io.prometheus.metrics.model.registry.PrometheusRegistry
+import io.prometheus.metrics.model.snapshots.Unit
+import io.prometheus.metrics.instrumentation.jvm.JvmMetrics;
+import io.prometheus.metrics.expositionformats.ExpositionFormats
+import tech.libeufin.common.*
+import tech.libeufin.common.db.*
+import tech.libeufin.bank.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.auth.*
+import java.time.Instant
+import java.io.ByteArrayOutputStream
+
+object Metrics {
+ @Volatile
+ private var tanChannelCounter = Counter.builder()
+ .name("libeufin_bank_tan_channel")
+ .help("TAN script calls")
+ .labelNames("channel", "exit")
+
+ init {
+ // Register JVM metrics
+ JvmMetrics.builder().register()
+ }
+
+ // TODO add database table counter info ?
+}
+
+fun Routing.observabilityApi(db: Database, cfg: BankConfig) {
+ get("/taler-observability/config") {
+ call.respond(TalerObservabilityConfig())
+ }
+ authAdmin(db, cfg.pwCrypto, TokenLogicalScope.observability, cfg.basicAuthCompat) {
+ get("/taler-observability/metrics") {
+ val snapshot = PrometheusRegistry.defaultRegistry.scrape()
+ val outputStream = ByteArrayOutputStream()
+ ExpositionFormats.init().getPrometheusTextFormatWriter().write(outputStream, snapshot)
+ call.respondText(outputStream.toString(Charsets.UTF_8), ContentType.parse("text/plain; version=0.0.4; charset=utf-8"))
+ }
+ }
+}
+\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
@@ -230,6 +230,7 @@ fun validScope(required: TokenLogicalScope, scope: TokenScope): Boolean = when (
TokenLogicalScope.revenue -> scope in setOf(TokenScope.readonly, TokenScope.readwrite, TokenScope.revenue)
TokenLogicalScope.readonly_wiregateway -> scope in setOf(TokenScope.wiregateway, TokenScope.readonly, TokenScope.readwrite)
TokenLogicalScope.readwrite_wiregateway -> scope in setOf(TokenScope.wiregateway, TokenScope.readwrite)
+ TokenLogicalScope.observability -> scope in setOf(TokenScope.readonly, TokenScope.readwrite, TokenScope.observability)
TokenLogicalScope.refreshable -> true
}
diff --git a/bank/src/test/kotlin/ObservabilityTest.kt b/bank/src/test/kotlin/ObservabilityTest.kt
@@ -0,0 +1,54 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 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
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+import io.ktor.http.*
+import io.ktor.client.request.*
+import org.junit.Test
+import tech.libeufin.bank.*
+import tech.libeufin.common.*
+import tech.libeufin.common.test.*
+
+class ObservabilityApiTest {
+ // GET /taler-observability/config
+ @Test
+ fun config() = bankSetup {
+ client.get("/taler-observability/config").assertOkJson<TalerObservabilityConfig>()
+ }
+
+ // GET /taler-observability/metrics
+ @Test
+ fun metrics() = bankSetup { db ->
+ authRoutine(HttpMethod.Get, "/taler-observability/metrics", requireAdmin = true)
+ client.getAdmin("/taler-observability/metrics").assertOk()
+
+ // Check observability token
+ val response = client.post("/accounts/admin/token") {
+ pwAuth()
+ json {
+ "scope" to "observability"
+ "duration" to obj {
+ "d_us" to "forever"
+ }
+ }
+ }.assertOkJson<TokenSuccessResponse>()
+ client.get("/taler-observability/metrics") {
+ headers[HttpHeaders.Authorization] = "Bearer ${response.access_token}"
+ }.assertOk()
+ }
+}
+\ No newline at end of file
diff --git a/build.gradle b/build.gradle
@@ -25,6 +25,7 @@ allprojects {
set("postgres_version", "42.7.7")
set("junixsocket_version", "2.10.1")
set("shadow_version", "9.1.0")
+ set("prometheus_version", "1.4.1")
}
repositories {
diff --git a/database-versioning/libeufin-bank-0010.sql b/database-versioning/libeufin-bank-0010.sql
@@ -18,7 +18,7 @@ BEGIN;
SELECT _v.register_patch('libeufin-bank-0010', NULL, NULL);
SET search_path TO libeufin_bank;
--- Add new token scope 'revenue'
+-- Add new token scope 'wiregateway'
ALTER TYPE token_scope_enum ADD VALUE 'wiregateway';
COMMIT;
diff --git a/database-versioning/libeufin-bank-0014.sql b/database-versioning/libeufin-bank-0014.sql
@@ -48,4 +48,7 @@ COMMENT ON COLUMN tan_challenges.salt
CREATE INDEX tan_challenges_uuid_index ON tan_challenges (uuid);
+-- Add new token scope 'observability'
+ALTER TYPE token_scope_enum ADD VALUE 'observability';
+
COMMIT;
diff --git a/nexus/build.gradle b/nexus/build.gradle
@@ -24,10 +24,9 @@ dependencies {
implementation(project(":common"))
// Metrics
- implementation('io.prometheus:prometheus-metrics-core:1.4.1')
- implementation('io.prometheus:prometheus-metrics-instrumentation-jvm:1.4.1')
- implementation("io.prometheus:prometheus-metrics-exporter-httpserver:1.4.1")
- implementation("io.prometheus:prometheus-metrics-exposition-formats:1.4.1")
+ implementation("io.prometheus:prometheus-metrics-core:$prometheus_version")
+ implementation("io.prometheus:prometheus-metrics-instrumentation-jvm:$prometheus_version")
+ implementation("io.prometheus:prometheus-metrics-exposition-formats:$prometheus_version")
// Command line parsing
implementation("com.github.ajalt.clikt:clikt:$clikt_version")
diff --git a/nexus/src/test/kotlin/ObservabilityTest.kt b/nexus/src/test/kotlin/ObservabilityTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024-2025 Taler Systems S.A.
+ * Copyright (C) 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