diff --git a/src/main/kotlin/no/nav/meldeplikt/meldekortservice/config/OutgoingCallLoggingPlugin.kt b/src/main/kotlin/no/nav/meldeplikt/meldekortservice/config/OutgoingCallLoggingPlugin.kt index 958f22b3..73e310f7 100644 --- a/src/main/kotlin/no/nav/meldeplikt/meldekortservice/config/OutgoingCallLoggingPlugin.kt +++ b/src/main/kotlin/no/nav/meldeplikt/meldekortservice/config/OutgoingCallLoggingPlugin.kt @@ -2,155 +2,128 @@ package no.nav.meldeplikt.meldekortservice.config import io.ktor.client.* import io.ktor.client.plugins.* +import io.ktor.client.plugins.observer.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.http.content.* -import io.ktor.util.* import io.ktor.utils.io.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.runBlocking import no.nav.meldeplikt.meldekortservice.model.database.KallLogg import no.nav.meldeplikt.meldekortservice.service.DBService -import no.nav.meldeplikt.meldekortservice.utils.* +import no.nav.meldeplikt.meldekortservice.utils.JOURNALPOST_PATH +import no.nav.meldeplikt.meldekortservice.utils.defaultLog +import no.nav.meldeplikt.meldekortservice.utils.getCallId +import no.nav.meldeplikt.meldekortservice.utils.headersToString import java.time.Instant import java.time.LocalDateTime import kotlin.coroutines.CoroutineContext -class OutgoingCallLoggingPlugin(config: OCDLPConfig) { - - val dbService: DBService = config.dbs - val kallLoggIdAttr = AttributeKey("kallLoggId") - - class OCDLPConfig { - lateinit var dbs: DBService - } - - companion object Plugin : HttpClientPlugin { - override val key: AttributeKey = AttributeKey("OutgoingCallLoggingPlugin") - - override fun prepare(block: OCDLPConfig.() -> Unit): OutgoingCallLoggingPlugin { - val config = OCDLPConfig().apply(block) - - return OutgoingCallLoggingPlugin(config) - } - - override fun install(plugin: OutgoingCallLoggingPlugin, scope: HttpClient) { - - var startTime = LocalDateTime.now() - var kallTid = Instant.now().toEpochMilli() - var responseBody = "" - - scope.requestPipeline.intercept(HttpRequestPipeline.State) { - // Det er mulig at vi sender request før vi får noe request fra meldekort-api - // F.eks for å hente noe config eller lignende - // Det betyr at vi kkke har noe callId ennå og da må vi generere den - val callId = getCallId() - startTime = LocalDateTime.now() - kallTid = Instant.now().toEpochMilli() - context.headers.append(HttpHeaders.XRequestId, callId) +class OutgoingCallLoggingPlugin(val dbService: DBService) { + + fun intercept(httpClient: HttpClient) { + httpClient.plugin(HttpSend).intercept { requestBuilder -> + // Prepare + val callId = getCallId() + val startTime = LocalDateTime.now() + val kallTid = Instant.now().toEpochMilli() + + requestBuilder.headers.append(HttpHeaders.XRequestId, callId) + + // Execute call + val originalCall = execute(requestBuilder) + + // Save data + val request = originalCall.request + val response = originalCall.response + + val responseBody = response.bodyAsText(Charsets.UTF_8) + + try { + defaultDbService.lagreKallLogg( + KallLogg( + korrelasjonId = callId, + tidspunkt = startTime, + type = "REST", + kallRetning = "UT", + method = request.method.value, + operation = request.url.encodedPath, + status = response.status.value, + kallTid = Instant.now().toEpochMilli() - kallTid, + request = buildRequest(requestBuilder.executionContext, request), + response = buildResponse(response, responseBody), + logginfo = "" + ) + ) + } catch (e: Exception) { + defaultLog.error("Kunne ikke lagre kall logg", e) } - scope.responsePipeline.intercept(HttpResponsePipeline.Receive) { (type, content) -> - if (content !is ByteReadChannel) return@intercept + // Response content can be read only once. Wrap the call with the content we have read + originalCall.wrapWithContent(ByteReadChannel(responseBody.toByteArray())) + } + } - val byteArray = ByteArray(content.availableForRead) - content.readAvailable(byteArray) - val result = ByteReadChannel(byteArray) - val responseContainer = HttpResponseContainer(type, result) - responseBody = String(byteArray, context.response.charset() ?: Charsets.UTF_8) + private fun buildRequest(coroutineContext: CoroutineContext, request: HttpRequest): String { + return StringBuilder().apply { + appendLine("${request.method.value} ${request.url.protocol.name}://${request.url.hostWithPort}${request.url.fullPath}") - proceedWith(responseContainer) + request.headers.forEach { header, values -> + appendLine("$header: ${headersToString(values)}") } - scope.responsePipeline.intercept(HttpResponsePipeline.After) { - val callId = getCallId() - val request = context.request - val response = context.response - - try { - val kallLoggId = plugin.dbService.lagreKallLogg( - KallLogg( - korrelasjonId = callId, - tidspunkt = startTime, - type = "REST", - kallRetning = "UT", - method = request.method.value, - operation = request.url.encodedPath, - status = response.status.value, - kallTid = Instant.now().toEpochMilli() - kallTid, - request = buildRequest(context.coroutineContext, request), - response = buildResponse(response, responseBody), - logginfo = "" + // empty line before body as in HTTP request + appendLine() + + if (request.url.encodedPath == JOURNALPOST_PATH) { + appendLine("JOURNALPOST") + } else { + when (request.content) { + is OutgoingContent.ByteArrayContent -> { + append( + String( + (request.content as OutgoingContent.ByteArrayContent).bytes(), + Charsets.UTF_8 + ) ) - ) - response.call.attributes.put(plugin.kallLoggIdAttr, kallLoggId) - } catch (e: Exception) { - defaultLog.error("Kunne ikke lagre kall logg", e) - } - } - } - - private fun buildRequest(coroutineContext: CoroutineContext, request: HttpRequest): String { - return StringBuilder().apply { - appendLine("${request.method.value} ${request.url.protocol.name}://${request.url.hostWithPort}${request.url.fullPath}") - - request.headers.forEach { header, values -> - appendLine("$header: ${headersToString(values)}") - } + } + is OutgoingContent.WriteChannelContent -> { + val buffer = StringBuilder() + val channel = ByteChannel(true) - // empty line before body as in HTTP request - appendLine() - - if (request.url.encodedPath == JOURNALPOST_PATH) { - appendLine("JOURNALPOST") - } else { - when (request.content) { - is OutgoingContent.ByteArrayContent -> { - append( - String( - (request.content as OutgoingContent.ByteArrayContent).bytes(), - Charsets.UTF_8 - ) - ) - } - is OutgoingContent.WriteChannelContent -> { - val buffer = StringBuilder() - val channel = ByteChannel(true) - - runBlocking { - GlobalScope.writer(coroutineContext, autoFlush = true) { - (request.content as OutgoingContent.WriteChannelContent).writeTo(channel) - } - - while (!channel.isClosedForRead) { - channel.readUTF8LineTo(buffer) - } + runBlocking { + GlobalScope.writer(coroutineContext, autoFlush = true) { + (request.content as OutgoingContent.WriteChannelContent).writeTo(channel) } - appendLine(buffer.toString()) - } - else -> { - appendLine(request.content) + while (!channel.isClosedForRead) { + channel.readUTF8LineTo(buffer) + } } + + appendLine(buffer.toString()) + } + else -> { + appendLine(request.content) } } - }.toString() - } + } + }.toString() + } - private fun buildResponse(response: HttpResponse, responseBody: String): String { - return StringBuilder().apply { - appendLine("${response.version} ${response.status.value} ${response.status.description}") + private fun buildResponse(response: HttpResponse, responseBody: String): String { + return StringBuilder().apply { + appendLine("${response.version} ${response.status.value} ${response.status.description}") - response.headers.forEach { header, values -> - appendLine("$header: ${headersToString(values)}") - } + response.headers.forEach { header, values -> + appendLine("$header: ${headersToString(values)}") + } - // empty line before body as in HTTP response - appendLine() + // empty line before body as in HTTP response + appendLine() - appendLine(responseBody) - }.toString() - } + appendLine(responseBody) + }.toString() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/no/nav/meldeplikt/meldekortservice/utils/Utils.kt b/src/main/kotlin/no/nav/meldeplikt/meldekortservice/utils/Utils.kt index 3a48cce3..86b02878 100644 --- a/src/main/kotlin/no/nav/meldeplikt/meldekortservice/utils/Utils.kt +++ b/src/main/kotlin/no/nav/meldeplikt/meldekortservice/utils/Utils.kt @@ -179,8 +179,8 @@ fun HttpClientConfig<*>.defaultHttpClientConfig() { requestTimeoutMillis = 10000 // required for an HTTP call: from sending a request to receiving a response socketTimeoutMillis = 10000 // of inactivity between two data packets when exchanging data with a server } - install(OutgoingCallLoggingPlugin) { - dbs = defaultDbService + install("OutgoingCallInterceptor") { + OutgoingCallLoggingPlugin(defaultDbService).intercept(this) } expectSuccess = false } diff --git a/src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_job.sql b/src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_job.sql new file mode 100644 index 00000000..de813a0f --- /dev/null +++ b/src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_job.sql @@ -0,0 +1,17 @@ +BEGIN + BEGIN + DBMS_SCHEDULER.DROP_JOB(job_name => 'delete_old_kall_logg_partitions'); + EXCEPTION + WHEN OTHERS THEN + NULL; -- Ignore exception + END; + + DBMS_SCHEDULER.CREATE_JOB( + job_name => 'delete_old_kall_logg_partitions', + job_type => 'STORED_PROCEDURE', + job_action => 'del_old_kall_logg_partitions', + start_date => TIMESTAMP '2022-10-01 05:00:00', + repeat_interval => 'FREQ=DAILY;BYHOUR=05;BYMINUTE=00', + enabled => true + ); +END; diff --git a/src/main/resources/db/migration/oracle/V1.6__del_old_kall_logg_partitions.sql b/src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_procedure.sql similarity index 55% rename from src/main/resources/db/migration/oracle/V1.6__del_old_kall_logg_partitions.sql rename to src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_procedure.sql index 9a174c2f..e79e7541 100644 --- a/src/main/resources/db/migration/oracle/V1.6__del_old_kall_logg_partitions.sql +++ b/src/main/resources/db/migration/oracle/R__delete_old_kall_logg_partitions_procedure.sql @@ -12,13 +12,3 @@ BEGIN END IF; END LOOP; END; -/ -BEGIN - DBMS_SCHEDULER.CREATE_JOB ( - job_name => 'delete_old_kall_logg_partitions', - job_type => 'STORED_PROCEDURE', - job_action => 'del_old_kall_logg_partitions', - start_date => TIMESTAMP '2022-10-01 05:00:00', - repeat_interval => 'FREQ=DAILY;BYHOUR=05;BYMINUTE=00', - enabled => true); -END; diff --git a/src/main/resources/db/migration/postgresql/V1.6__del_old_kall_logg_partitions.sql b/src/main/resources/db/migration/postgresql/R__delete_old_kall_logg_partitions_job.sql similarity index 100% rename from src/main/resources/db/migration/postgresql/V1.6__del_old_kall_logg_partitions.sql rename to src/main/resources/db/migration/postgresql/R__delete_old_kall_logg_partitions_job.sql diff --git a/src/main/resources/db/migration/postgresql/R__delete_old_kall_logg_partitions_procedure.sql b/src/main/resources/db/migration/postgresql/R__delete_old_kall_logg_partitions_procedure.sql new file mode 100644 index 00000000..8ce5f244 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/R__delete_old_kall_logg_partitions_procedure.sql @@ -0,0 +1 @@ +-- Created and left empty intentionally diff --git a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/config/CallLoggingPluginTest.kt b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/config/CallLoggingPluginTest.kt index ac447096..7e2ad96f 100644 --- a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/config/CallLoggingPluginTest.kt +++ b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/config/CallLoggingPluginTest.kt @@ -99,8 +99,8 @@ class CallLoggingPluginTest : TestBase() { "GET https://dummyurl.nav.no:443/api/v1/meldeplikt/meldekort/detaljer?meldekortId=1\n" + "Accept: [application/xml; charset=UTF-8,application/json]\n" + "Authorization: Bearer $DUMMY_TOKEN\n" + - "X-Request-ID: $callId\n" + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + "\n" + "EmptyContent\n" val expectedUtResponse = "" + @@ -181,12 +181,84 @@ class CallLoggingPluginTest : TestBase() { } @Test - fun `skal ikke lagre request og response content for opprettJournalpost`() = testApplication { + fun `skal lagre request og response naar feil`() = testApplication { // // Prepare // database = H2Database("IncomingCallLoggingPluginTest2") dbService = DBService(database) + + defaultDbService = dbService + val flywayConfig = mockk() + every { flywayConfig.migrate() } returns MigrateResult("", "", "") + + val callId = getCallId() + + val expectedRequest = "POST https://dummyurl.nav.no:443/api/v1/kontroll\n" + + "Authorization: Bearer $DUMMY_TOKEN\n" + + "Accept: application/json\n" + + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + + "\n" + val expectedResponse = "HTTP/1.1 500 Internal Server Error\n" + + "Content-Type: application/json\n" + + "\n" + + "{\"status\": 500}\n" + + environment { + config = setOidcConfig() + } + + val kontrollClient = HttpClient(MockEngine) { + defaultHttpClientConfig() + + engine { + addHandler { + respond( + "{\"status\": 500}", + HttpStatusCode.InternalServerError, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + } + } + + // + // Run + // + kontrollClient.post("${env.meldekortKontrollUrl}$KONTROLL_KONTROLL") { + contentType(ContentType.Application.Json) + header("Authorization", "Bearer $DUMMY_TOKEN") + setBody("") + } + + // + // Check + // + val kallLoggListe = database.dbQuery { hentAlleKallLogg() } + assertEquals(1, kallLoggListe.size) + + val kall1 = kallLoggListe[0] + assertEquals(callId, kall1.korrelasjonId) + assertEquals("REST", kall1.type) + assertEquals("UT", kall1.kallRetning) + assertEquals("POST", kall1.method) + assertEquals(KONTROLL_KONTROLL, kall1.operation) + assertEquals(500, kall1.status) + assertEquals(expectedRequest, kall1.request) + assertEquals(expectedResponse, kall1.response) + assertEquals("", kall1.logginfo) + + database.closeConnection() + } + + @Test + fun `skal ikke lagre request og response content for opprettJournalpost`() = testApplication { + // + // Prepare + // + database = H2Database("IncomingCallLoggingPluginTest3") + dbService = DBService(database) defaultDbService = dbService val flywayConfig = mockk() every { flywayConfig.migrate() } returns MigrateResult("", "", "") @@ -303,9 +375,9 @@ class CallLoggingPluginTest : TestBase() { val expectedStsUtRequest = "" + "POST ${env.stsNaisUrl}:443$STS_PATH?grant_type=client_credentials&scope=openid\n" + "Authorization: $authHeaderValue\n" + - "X-Request-ID: $callId\n" + "Accept: application/json\n" + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + "\n" + "EmptyContent\n" val expectedStsUtResponse = "" + @@ -320,9 +392,9 @@ class CallLoggingPluginTest : TestBase() { val expectedUtRequest = "" + "POST ${env.dokarkivUrl}:443$JOURNALPOST_PATH?forsoekFerdigstill=true\n" + "Authorization: Bearer $aceessTokenContent\n" + - "X-Request-ID: $callId\n" + "Accept: application/json\n" + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + "\n" + "JOURNALPOST\n" val expectedUtResponse = "" + diff --git a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/DokarkivServiceTest.kt b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/DokarkivServiceTest.kt index 41f662c0..e9587837 100644 --- a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/DokarkivServiceTest.kt +++ b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/DokarkivServiceTest.kt @@ -67,9 +67,9 @@ class DokarkivServiceTest { val journalpostRequest = "" + "POST ${env.dokarkivUrl}:443$JOURNALPOST_PATH?forsoekFerdigstill=true\n" + "Authorization: Bearer dG9rZW4=\n" + - "X-Request-ID: $callId\n" + "Accept: application/json\n" + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + "\n" + "JOURNALPOST\n" val jpRespObject = JournalpostResponse( @@ -94,9 +94,9 @@ class DokarkivServiceTest { val tokenRequest = "" + "POST ${env.stsNaisUrl}:443$STS_PATH?grant_type=client_credentials&scope=openid\n" + "Authorization: $authHeaderValue\n" + - "X-Request-ID: $callId\n" + "Accept: application/json\n" + "Accept-Charset: UTF-8\n" + + "X-Request-ID: $callId\n" + "\n" + "EmptyContent\n" val tokenResponse = "" + diff --git a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/KontrollServiceTest.kt b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/KontrollServiceTest.kt index f65b4cad..61c4d054 100644 --- a/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/KontrollServiceTest.kt +++ b/src/test/kotlin/no/nav/meldeplikt/meldekortservice/service/KontrollServiceTest.kt @@ -21,7 +21,7 @@ import kotlin.test.assertEquals class KontrollServiceTest { - val meldekortkontroll = Meldekortkontroll( + private val meldekortkontroll = Meldekortkontroll( meldekortId = 123, fnr = "11111111111", personId = 335, @@ -77,7 +77,7 @@ class KontrollServiceTest { } engine { - addHandler { request -> + addHandler { respond( "", HttpStatusCode.InternalServerError,