From 57564cf86940c08785cc944b23f0d27fb3a5e6d3 Mon Sep 17 00:00:00 2001 From: mortenbyhring <117746154+mortenbyhring@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:44:53 +0100 Subject: [PATCH] Feil-behandler-app v1 (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ny app: feil-behandler * Legg til håndtering av bare bestemte typer feil, resten skal ignoreres --- config/feil-behandler/dev-gcp.yml | 7 ++ config/feil-behandler/prod-gcp.yml | 7 ++ feil-behandler/build.gradle.kts | 15 ++++ feil-behandler/gradle.properties | 5 ++ .../inntektsmelding/feilbehandler/App.kt | 44 ++++++++++++ .../feilbehandler/config/Database.kt | 30 ++++++++ .../feilbehandler/config/DatabaseConfig.kt | 24 +++++++ .../feilbehandler/river/FeilLytter.kt | 68 +++++++++++++++++++ .../resources/db/migration/V1___tilganger.sql | 12 ++++ .../db/migration/V2___bakgrunnsjobb.sql | 14 ++++ .../feilbehandler/river/FeilLytterTest.kt | 60 ++++++++++++++++ 11 files changed, 286 insertions(+) create mode 100644 config/feil-behandler/dev-gcp.yml create mode 100644 config/feil-behandler/prod-gcp.yml create mode 100644 feil-behandler/build.gradle.kts create mode 100644 feil-behandler/gradle.properties create mode 100644 feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/App.kt create mode 100644 feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/Database.kt create mode 100644 feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/DatabaseConfig.kt create mode 100644 feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytter.kt create mode 100644 feil-behandler/src/main/resources/db/migration/V1___tilganger.sql create mode 100644 feil-behandler/src/main/resources/db/migration/V2___bakgrunnsjobb.sql create mode 100644 feil-behandler/src/test/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytterTest.kt diff --git a/config/feil-behandler/dev-gcp.yml b/config/feil-behandler/dev-gcp.yml new file mode 100644 index 000000000..d2888a7e6 --- /dev/null +++ b/config/feil-behandler/dev-gcp.yml @@ -0,0 +1,7 @@ +kafkaPool: nav-dev +azure: + enabled: true +database: im-error-recovery +envFrom: + - type: secret + name: google-sql-im-feil-behandler diff --git a/config/feil-behandler/prod-gcp.yml b/config/feil-behandler/prod-gcp.yml new file mode 100644 index 000000000..28898cf73 --- /dev/null +++ b/config/feil-behandler/prod-gcp.yml @@ -0,0 +1,7 @@ +kafkaPool: nav-prod +azure: + enabled: true +database: im-error-recovery +envFrom: + - type: secret + name: google-sql-im-feil-behandler diff --git a/feil-behandler/build.gradle.kts b/feil-behandler/build.gradle.kts new file mode 100644 index 000000000..2fc573c6e --- /dev/null +++ b/feil-behandler/build.gradle.kts @@ -0,0 +1,15 @@ +val exposedVersion: String by project +val flywayVersion: String by project +val hikariVersion: String by project +val postgresqlVersion: String by project +val testcontainersPostgresqlVersion: String by project + +dependencies { + implementation("com.zaxxer:HikariCP:$hikariVersion") + implementation("org.flywaydb:flyway-core:$flywayVersion") + implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion") + + runtimeOnly("org.postgresql:postgresql:$postgresqlVersion") + + testImplementation("org.testcontainers:postgresql:$testcontainersPostgresqlVersion") +} diff --git a/feil-behandler/gradle.properties b/feil-behandler/gradle.properties new file mode 100644 index 000000000..58fd627f6 --- /dev/null +++ b/feil-behandler/gradle.properties @@ -0,0 +1,5 @@ +# Dependency versions +flywayVersion=10.1.0 +hikariVersion=5.1.0 +postgresqlVersion=42.7.0 +testcontainersPostgresqlVersion=1.19.3 diff --git a/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/App.kt b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/App.kt new file mode 100644 index 000000000..1872fa0ba --- /dev/null +++ b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/App.kt @@ -0,0 +1,44 @@ +package no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler + +import com.zaxxer.hikari.HikariConfig +import no.nav.helse.rapids_rivers.RapidApplication +import no.nav.helse.rapids_rivers.RapidsConnection +import no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.config.Database +import no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.config.DatabaseConfig +import no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.config.mapHikariConfig +import no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.river.FeilLytter +import no.nav.helsearbeidsgiver.utils.log.logger +import no.nav.helsearbeidsgiver.utils.log.sikkerLogger + +private val logger = "helsearbeidsgiver-im-feil-behandler".logger() +private val sikkerLogger = sikkerLogger() + +fun main() { + buildApp(mapHikariConfig(DatabaseConfig()), System.getenv()).start() +} + +fun buildApp(config: HikariConfig, env: Map): RapidsConnection { + val database = Database(config) + sikkerLogger.info("Bruker database url: ${config.jdbcUrl}") + logger.info("Migrering starter...") + database.migrate() + logger.info("Migrering ferdig.") + return RapidApplication + .create(env) + .createFeilLytter(database) +} + +fun RapidsConnection.createFeilLytter(database: Database): RapidsConnection = + also { + registerDbLifecycle(database) + FeilLytter(it) + } + +private fun RapidsConnection.registerDbLifecycle(db: Database) { + register(object : RapidsConnection.StatusListener { + override fun onShutdown(rapidsConnection: RapidsConnection) { + logger.info("Mottatt stoppsignal, lukker databasetilkobling") + db.dataSource.close() + } + }) +} diff --git a/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/Database.kt b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/Database.kt new file mode 100644 index 000000000..f15056cb8 --- /dev/null +++ b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/Database.kt @@ -0,0 +1,30 @@ +package no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.config + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.flywaydb.core.Flyway + +class Database( + private val dbConfig: HikariConfig +) { + val dataSource by lazy { HikariDataSource(dbConfig) } + fun migrate() { + migrationConfig(dbConfig) + .let(::HikariDataSource) + .also { + Flyway.configure() + .dataSource(it) + .lockRetryCount(50) + .load() + .migrate() + }.close() + } +} + +private fun migrationConfig(conf: HikariConfig): HikariConfig = + HikariConfig().apply { + jdbcUrl = conf.jdbcUrl + username = conf.username + password = conf.password + maximumPoolSize = 3 + } diff --git a/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/DatabaseConfig.kt b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/DatabaseConfig.kt new file mode 100644 index 000000000..7134cffe0 --- /dev/null +++ b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/config/DatabaseConfig.kt @@ -0,0 +1,24 @@ +package no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.config + +import com.zaxxer.hikari.HikariConfig +import no.nav.helsearbeidsgiver.felles.fromEnv + +private const val prefix = "NAIS_DATABASE_IM_FEIL_BEHANDLER_IM_ERROR_RECOVERY" + +data class DatabaseConfig( + val host: String = "${prefix}_HOST".fromEnv(), + val port: String = "${prefix}_PORT".fromEnv(), + val name: String = "${prefix}_DATABASE".fromEnv(), + val username: String = "${prefix}_USERNAME".fromEnv(), + val password: String = "${prefix}_PASSWORD".fromEnv(), + val url: String = "jdbc:postgresql://%s:%s/%s".format(host, port, name) +) + +fun mapHikariConfig(databaseConfig: DatabaseConfig): HikariConfig { + return HikariConfig().apply { + jdbcUrl = databaseConfig.url + username = databaseConfig.username + password = databaseConfig.password + maximumPoolSize = 5 + } +} diff --git a/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytter.kt b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytter.kt new file mode 100644 index 000000000..c4508e768 --- /dev/null +++ b/feil-behandler/src/main/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytter.kt @@ -0,0 +1,68 @@ +package no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.river + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializer +import no.nav.helse.rapids_rivers.JsonMessage +import no.nav.helse.rapids_rivers.MessageContext +import no.nav.helse.rapids_rivers.RapidsConnection +import no.nav.helse.rapids_rivers.River +import no.nav.helsearbeidsgiver.felles.BehovType +import no.nav.helsearbeidsgiver.felles.Key +import no.nav.helsearbeidsgiver.felles.json.toMap +import no.nav.helsearbeidsgiver.felles.rapidsrivers.model.Fail +import no.nav.helsearbeidsgiver.felles.rapidsrivers.toPretty +import no.nav.helsearbeidsgiver.utils.json.fromJson +import no.nav.helsearbeidsgiver.utils.json.parseJson +import no.nav.helsearbeidsgiver.utils.log.sikkerLogger + +class FeilLytter(rapidsConnection: RapidsConnection) : River.PacketListener { + + private val sikkerLogger = sikkerLogger() + val behovSomHaandteres = listOf( + BehovType.LAGRE_FORESPOERSEL, + BehovType.OPPRETT_OPPGAVE, + BehovType.OPPRETT_SAK, + BehovType.PERSISTER_OPPGAVE_ID, + BehovType.PERSISTER_SAK_ID, + BehovType.JOURNALFOER, + BehovType.LAGRE_JOURNALPOST_ID, + BehovType.NOTIFIKASJON_HENT_ID + ) + + init { + sikkerLogger.info("Starter applikasjon - lytter på innkommende feil!") + River(rapidsConnection).apply { + validate { msg -> + msg.demandKey(Key.FAIL.str) + } + } + .register(this) + } + + override fun onPacket(packet: JsonMessage, context: MessageContext) { + sikkerLogger.info("Mottok feil: ${packet.toPretty()}") + val fail = toFailOrNull(packet.toJson().parseJson()) + if (fail == null) { + sikkerLogger.warn("Kunne ikke parse feil-objekt, ignorerer...") + } else if (skalHaandteres(fail)) { + sikkerLogger.info("Behandler feil") + } else { + sikkerLogger.info("Ignorerer feil") + } + } + + fun skalHaandteres(fail: Fail): Boolean { + if (fail.forespoerselId == null) { + return false + } + val behovFraMelding = fail.utloesendeMelding.toMap()[Key.BEHOV]?.fromJson(BehovType.serializer()) + return behovSomHaandteres.contains(behovFraMelding) + } + + fun toFailOrNull(json: JsonElement): Fail? = // TODO: duplisert, lage felles metode? + json.toMap()[Key.FAIL] + ?.runCatching { + fromJson(Fail.serializer()) + } + ?.getOrNull() +} diff --git a/feil-behandler/src/main/resources/db/migration/V1___tilganger.sql b/feil-behandler/src/main/resources/db/migration/V1___tilganger.sql new file mode 100644 index 000000000..09d6d0e71 --- /dev/null +++ b/feil-behandler/src/main/resources/db/migration/V1___tilganger.sql @@ -0,0 +1,12 @@ +DO +$$ + BEGIN + IF EXISTS (SELECT 1 + FROM pg_roles + WHERE rolname = 'cloudsqliamuser') + THEN + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO cloudsqliamuser; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO cloudsqliamuser; + END IF; + END +$$; diff --git a/feil-behandler/src/main/resources/db/migration/V2___bakgrunnsjobb.sql b/feil-behandler/src/main/resources/db/migration/V2___bakgrunnsjobb.sql new file mode 100644 index 000000000..4b94f20d2 --- /dev/null +++ b/feil-behandler/src/main/resources/db/migration/V2___bakgrunnsjobb.sql @@ -0,0 +1,14 @@ +create table bakgrunnsjobb +( + jobb_id uuid unique not null, + type text not null, + behandlet timestamp, + opprettet timestamp not null, + + status text not null, + kjoeretid timestamp not null, + + forsoek int not null default 0, + maks_forsoek int not null, + data jsonb +); diff --git a/feil-behandler/src/test/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytterTest.kt b/feil-behandler/src/test/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytterTest.kt new file mode 100644 index 000000000..f2e9f3e98 --- /dev/null +++ b/feil-behandler/src/test/kotlin/no/nav/helsearbeidsgiver/inntektsmelding/feilbehandler/river/FeilLytterTest.kt @@ -0,0 +1,60 @@ +package no.nav.helsearbeidsgiver.inntektsmelding.feilbehandler.river + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import no.nav.helse.rapids_rivers.JsonMessage +import no.nav.helse.rapids_rivers.testsupport.TestRapid +import no.nav.helsearbeidsgiver.felles.BehovType +import no.nav.helsearbeidsgiver.felles.EventName +import no.nav.helsearbeidsgiver.felles.Key +import no.nav.helsearbeidsgiver.felles.rapidsrivers.model.Fail +import no.nav.helsearbeidsgiver.felles.utils.mapOfNotNull +import no.nav.helsearbeidsgiver.utils.json.parseJson +import java.util.UUID + +class FeilLytterTest : FunSpec({ + + val handler = FeilLytter(TestRapid()) + + test("skal håndtere gyldige feil med spesifiserte behov") { + + handler.behovSomHaandteres.forEach { handler.skalHaandteres(lagGyldigFeil(it)) shouldBe true } + } + + test("skal ignorere gyldige feil med visse behov") { + val ignorerteBehov = BehovType.entries.filterNot { handler.behovSomHaandteres.contains(it) } + ignorerteBehov.forEach { handler.skalHaandteres(lagGyldigFeil(it)) shouldBe false } + } + + test("skal ignorere feil uten behov") { + val uuid = UUID.randomUUID() + val feil = lagGyldigFeil(BehovType.JOURNALFOER).copy( + utloesendeMelding = + JsonMessage.newMessage( + mapOfNotNull( + Key.UUID.str to uuid, + Key.FORESPOERSEL_ID.str to uuid + ) + ).toJson().parseJson() + ) + handler.skalHaandteres(feil) shouldBe false + } + + test("skal ignorere feil uten forespørselId") { + val feil = lagGyldigFeil(BehovType.JOURNALFOER).copy(forespoerselId = null) + handler.skalHaandteres(feil) shouldBe false + } +}) + +fun lagGyldigFeil(behov: BehovType): Fail { + val uuid = UUID.randomUUID() + val jsonMessage = JsonMessage.newMessage( + EventName.OPPGAVE_OPPRETT_REQUESTED.name, + mapOfNotNull( + Key.BEHOV.str to behov, + Key.UUID.str to uuid, + Key.FORESPOERSEL_ID.str to uuid + ) + ) + return Fail("Feil", EventName.OPPGAVE_OPPRETT_REQUESTED, UUID.randomUUID(), UUID.randomUUID(), jsonMessage.toJson().parseJson()) +}