libeufin

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

EBicsLogger.kt (4881B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.ebics
     21 
     22 import tech.libeufin.common.*
     23 import tech.libeufin.ebics.EbicsOrder
     24 import java.io.*
     25 import java.nio.file.*
     26 import java.time.*
     27 import java.time.format.DateTimeFormatter
     28 import kotlin.io.*
     29 import kotlin.io.path.*
     30 import io.ktor.client.statement.*
     31 
     32 /** Log EBICS transactions steps and payload if [path] is not null */
     33 class EbicsLogger(private val dir: Path?) {
     34 
     35     init {
     36         if (dir != null) {
     37             try {
     38                 // Create logging directory if missing
     39                 dir.createDirectories()
     40             } catch (e: Exception) {
     41                 throw Exception("Failed to init EBICS debug logging directory", e)
     42             }
     43             logger.info("Logging to '$dir'")
     44         }
     45     }
     46 
     47     /** Create a new [name] EBICS transaction logger */
     48     fun tx(name: String): TxLogger {
     49         if (dir == null) return TxLogger(null)
     50         val utcDateTime = Instant.now().atOffset(ZoneOffset.UTC)
     51         val txDir = dir
     52             // yyyy-MM-dd per day directory
     53             .resolve(utcDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE))
     54             // HH:mm:ss.SSS-name per transaction directory
     55             .resolve("${utcDateTime.format(TIME_WITH_MS)}-$name")
     56         txDir.createDirectories()
     57         return TxLogger(txDir)
     58     }
     59 
     60     /** Create a new [order] EBICS transaction logger */
     61     fun tx(order: EbicsOrder): TxLogger {
     62         if (dir == null) return TxLogger(null)
     63         return tx(order.description())
     64     }
     65 
     66     companion object {
     67         private val TIME_WITH_MS = DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
     68     }
     69 }
     70 
     71 /** Log EBICS transaction steps and payload */
     72 class TxLogger internal constructor(
     73     private val dir: Path?
     74 ) {
     75     /** Create a new [name] EBICS transaction step logger*/
     76     fun step(name: String? = null) = StepLogger(dir, name)
     77 
     78     /** Log a [stream] EBICS transaction payload of [type] */
     79     fun payload(stream: InputStream, type: String = "xml"): InputStream {
     80         if (dir == null) return stream
     81         return payload(stream.readBytes(), type).inputStream()
     82     }
     83 
     84     /** Log a [content] EBICS transaction payload of [type] */
     85     fun payload(content: ByteArray, type: String = "xml"): ByteArray {
     86         if (dir == null) return content
     87         val type = type.lowercase()
     88         if (type == "zip") {
     89             val payloadDir = dir.resolve("payload")
     90             payloadDir.createDirectory()
     91             content.inputStream().unzipEach { fileName, xmlContent ->
     92                 xmlContent.use {
     93                     Files.copy(it, payloadDir.resolve(fileName))
     94                 }
     95             }
     96         } else {
     97             dir.resolve("payload.$type").writeBytes(content, StandardOpenOption.CREATE_NEW)
     98         }
     99         return content
    100     }
    101 }
    102 
    103 /** Log EBICS transaction protocol step */
    104 class StepLogger internal constructor(
    105     private val dir: Path?,
    106     name: String?
    107 ) {
    108     private val prefix = if (name != null) "$name-" else ""
    109 
    110     /** Log a protocol step [request] */
    111     fun logRequest(request: ByteArray) {
    112         dir?.resolve("${prefix}request.xml")?.writeBytes(request, StandardOpenOption.CREATE_NEW)
    113     }
    114 
    115     /** Log a protocol step failure */
    116     suspend fun logFailure(res: HttpResponse) {
    117         if (dir != null) {
    118             // TODO reduce allocation
    119             // TODO silent io error
    120             val bytes = buildString {
    121                 append("${res.version} ${res.status}\n")
    122                 for ((k, vs) in res.headers.entries()) {
    123                     for (v in vs) {
    124                         append("${k}: ${v}\n")
    125                     }
    126                 }
    127                 append('\n')
    128             }.toByteArray() + res.readRawBytes()
    129             dir.resolve("${prefix}failure")
    130                .writeBytes(bytes, StandardOpenOption.CREATE_NEW)
    131         }
    132     }
    133 
    134     /** Log a protocol step [response] */
    135     fun logResponse(response: InputStream): InputStream {
    136         if (dir == null) return response
    137         val bytes = response.readBytes()
    138         dir.resolve("${prefix}response.xml")
    139            .writeBytes(bytes, StandardOpenOption.CREATE_NEW)
    140         return bytes.inputStream()
    141     }
    142 }