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 }