diff --git a/.github/workflows/api-start-stopp.yml b/.github/workflows/api-start-stopp.yml index 9e1dc2b6..bcfc1e61 100644 --- a/.github/workflows/api-start-stopp.yml +++ b/.github/workflows/api-start-stopp.yml @@ -1,15 +1,18 @@ name: Api Start stopp on: - push: - paths: - - 'apps/api-start-stopp-perioder/**' - - 'domain/**' - - 'lib/**' - - '.github/workflows/api-start-stopp.yml' - - 'gradle/**' - - 'settings.gradle.kts' - - 'gradle.properties' - - 'gradlew' + push: + branches: + - main + - dev/* + paths: + - 'apps/api-start-stopp-perioder/**' + - 'domain/**' + - 'lib/**' + - '.github/workflows/api-start-stopp.yml' + - 'gradle/**' + - 'settings.gradle.kts' + - 'gradle.properties' + - 'gradlew' env: IMAGE: europe-north1-docker.pkg.dev/${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}/paw/paw-arbeidssokerregisteret-api-inngang @@ -50,7 +53,9 @@ jobs: uses: nais/attest-sign@v1.3.4 with: image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} + deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') name: Deploy to dev-gcp - API Start Stopp needs: build runs-on: ubuntu-latest @@ -64,10 +69,11 @@ jobs: CLUSTER: dev-gcp RESOURCE: apps/api-start-stopp-perioder/nais/nais-dev.yaml VAR: image=${{ needs.build.outputs.image }} + deploy-prod: if: github.ref == 'refs/heads/main' name: Deploy to prod-gcp - API Start Stopp - needs: [build, deploy-dev] + needs: [ build, deploy-dev ] runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/hendelselogg-backup.yaml b/.github/workflows/hendelselogg-backup.yaml index 362f503a..b26fdad7 100644 --- a/.github/workflows/hendelselogg-backup.yaml +++ b/.github/workflows/hendelselogg-backup.yaml @@ -2,6 +2,9 @@ name: Hendelselogg-backup on: push: + branches: + - main + - dev/* paths: - 'apps/hendelselogg-backup/**' - 'lib/**' @@ -11,6 +14,7 @@ on: - 'settings.gradle.kts' - 'gradle.properties' - 'gradlew' + env: IMAGE: europe-north1-docker.pkg.dev/${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}/paw/paw-arbeidssoekerregisteret-hendelselogg-backup jobs: @@ -53,6 +57,7 @@ jobs: image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') name: Deploy to dev-gcp needs: build runs-on: ubuntu-latest @@ -70,7 +75,7 @@ jobs: deploy-prod: if: github.ref == 'refs/heads/main' name: Deploy to prod-gcp - needs: [build, deploy-dev] + needs: [ build, deploy-dev ] runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/hendelseprosessor.yaml b/.github/workflows/hendelseprosessor.yaml index 922d1512..f16974a0 100644 --- a/.github/workflows/hendelseprosessor.yaml +++ b/.github/workflows/hendelseprosessor.yaml @@ -2,6 +2,9 @@ name: Hendelseprosessor on: push: + branches: + - main + - dev/* paths: - 'apps/hendelseprosessor/**' - 'lib/**' @@ -11,6 +14,7 @@ on: - 'settings.gradle.kts' - 'gradle.properties' - 'gradlew' + env: IMAGE: europe-north1-docker.pkg.dev/${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}/paw/paw-arbeidssokerregisteret-event-prosessor jobs: @@ -51,7 +55,9 @@ jobs: uses: nais/attest-sign@v1.3.4 with: image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} + deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') name: Deploy to dev-gcp needs: build runs-on: ubuntu-latest @@ -69,7 +75,7 @@ jobs: deploy-prod: if: github.ref == 'refs/heads/main' name: Deploy to prod-gcp - needs: [build, deploy-dev] + needs: [ build, deploy-dev ] runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/opplysninger-aggregering.yaml b/.github/workflows/opplysninger-aggregering.yaml new file mode 100644 index 00000000..59f0bf46 --- /dev/null +++ b/.github/workflows/opplysninger-aggregering.yaml @@ -0,0 +1,98 @@ +name: Build, push, and deploy - paw-arbeidssoeker-opplysninger-aggregering + +on: + push: + branches: + - main + - dev/* + paths: + - 'apps/opplysninger-aggregering/**' + - 'lib/**' + - 'domain/**' + - '.github/workflows/opplysninger-aggregering.yaml' + - 'gradle/**' + - 'settings.gradle.kts' + - 'gradle.properties' + - 'gradlew' + +env: + IMAGE: europe-north1-docker.pkg.dev/${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}/paw/paw-arbeidssoeker-opplysninger-aggregering +jobs: + build: + name: Build and push Docker container + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + packages: write + outputs: + image: ${{ steps.docker-build-push.outputs.image }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + java-version: 21 + distribution: temurin + cache: gradle + - name: Specify module + run: echo "MODULE=:apps:opplysninger-aggregering" >> $GITHUB_ENV + - name: Specify version + run: echo "VERSION=$(date +'%y.%m.%d').${{ github.run_number }}-${{ github.run_attempt }}" >> $GITHUB_ENV + - name: Login GAR + uses: nais/login@v0 + with: + project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }} + identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }} + team: paw + - name: Build with Gradle + id: docker-build-push + working-directory: ./ + run: | + echo "image=${{ env.IMAGE }}:${{ env.VERSION }}" >> $GITHUB_OUTPUT + ./gradlew -Pversion=${{ env.VERSION }} -Pimage=${{ env.IMAGE }} ${{ env.MODULE }}:build ${{ env.MODULE }}:test ${{ env.MODULE }}:jib + echo "DIGEST=$(cat ./apps/opplysninger-aggregering/build/jib-image.digest)" >> $GITHUB_ENV + env: + ORG_GRADLE_PROJECT_githubPassword: ${{ secrets.GITHUB_TOKEN }} + - name: Attest and sign + uses: nais/attest-sign@v1.3.4 + with: + image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} + + deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') + name: Deploy to dev-gcp + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Deploy + uses: nais/deploy/actions/deploy@v2 + env: + CLUSTER: dev-gcp + RESOURCE: ./apps/opplysninger-aggregering/nais/nais-dev.yaml + VAR: image=${{ needs.build.outputs.image }},kafka=nav-dev + + deploy-prod: + if: github.ref == 'refs/heads/main' + name: Deploy to prod-gcp + needs: [build, deploy-dev] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Deploy + uses: nais/deploy/actions/deploy@v2 + env: + TEAM: paw + CLUSTER: prod-gcp + RESOURCE: ./apps/opplysninger-aggregering/nais/nais-prod.yaml + VAR: image=${{ needs.build.outputs.image }},kafka=nav-prod diff --git a/.github/workflows/utgang-formidlingsgruppe-deploy.yaml b/.github/workflows/utgang-formidlingsgruppe-deploy.yaml index 5156f901..aa794ad4 100644 --- a/.github/workflows/utgang-formidlingsgruppe-deploy.yaml +++ b/.github/workflows/utgang-formidlingsgruppe-deploy.yaml @@ -2,6 +2,9 @@ name: Formidlingsgruppe - Utgang on: push: + branches: + - main + - dev/* paths: - 'apps/utgang-formidlingsgruppe/**' - 'lib/**' @@ -55,7 +58,9 @@ jobs: uses: nais/attest-sign@v1.3.4 with: image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} + deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') name: Deploy to dev-gcp permissions: contents: read @@ -69,13 +74,14 @@ jobs: CLUSTER: dev-gcp RESOURCE: apps/utgang-formidlingsgruppe/nais/nais-dev.yaml VAR: image=${{ needs.build.outputs.image }},kafka=nav-dev + deploy-prod: if: github.ref == 'refs/heads/main' name: Deploy to prod-gcp permissions: contents: read id-token: write - needs: [deploy-dev,build] + needs: [ deploy-dev,build ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/utgang-pdl-deploy.yaml b/.github/workflows/utgang-pdl-deploy.yaml index bbb245da..bdddb23c 100644 --- a/.github/workflows/utgang-pdl-deploy.yaml +++ b/.github/workflows/utgang-pdl-deploy.yaml @@ -2,6 +2,9 @@ name: PDL - Utgang on: push: + branches: + - main + - dev/* paths: - 'apps/utgang-pdl/**' - '.github/workflows/utgang-pdl-deploy.yaml' @@ -55,7 +58,9 @@ jobs: uses: nais/attest-sign@v1.3.4 with: image_ref: ${{ env.IMAGE }}@${{ env.DIGEST }} + deploy-dev: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/dev') name: Deploy to dev-gcp permissions: contents: read @@ -69,6 +74,7 @@ jobs: CLUSTER: dev-gcp RESOURCE: apps/utgang-pdl/nais/nais-dev.yaml VAR: image=${{ needs.build.outputs.image }},kafka=nav-dev + deploy-prod: if: github.ref == 'refs/heads/main' name: Deploy to prod-gcp @@ -83,4 +89,4 @@ jobs: env: CLUSTER: prod-gcp RESOURCE: apps/utgang-pdl/nais/nais-prod.yaml - VAR: image=${{ needs.build.outputs.image }},kafka=nav-prod \ No newline at end of file + VAR: image=${{ needs.build.outputs.image }},kafka=nav-prod diff --git a/apps/hendelselogg-backup/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/backup/brukerstoette/HttpClients.kt b/apps/hendelselogg-backup/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/backup/brukerstoette/HttpClients.kt index 53120732..753097fb 100644 --- a/apps/hendelselogg-backup/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/backup/brukerstoette/HttpClients.kt +++ b/apps/hendelselogg-backup/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/backup/brukerstoette/HttpClients.kt @@ -1,16 +1,15 @@ package no.nav.paw.arbeidssoekerregisteret.backup.brukerstoette -import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import io.ktor.client.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.jackson.* +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.jackson.jackson import no.nav.common.token_client.client.AzureAdMachineToMachineTokenClient +import no.nav.paw.config.env.currentNaisEnv import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient -import no.nav.paw.kafkakeygenerator.auth.currentNaisEnv import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient import no.nav.paw.kafkakeygenerator.client.createKafkaKeyGeneratorClient diff --git a/apps/opplysninger-aggregering/README.md b/apps/opplysninger-aggregering/README.md new file mode 100644 index 00000000..7264ade7 --- /dev/null +++ b/apps/opplysninger-aggregering/README.md @@ -0,0 +1,2 @@ +# PAW Arbeidssoeker Opplysninger Aggregering + diff --git a/apps/opplysninger-aggregering/build.gradle.kts b/apps/opplysninger-aggregering/build.gradle.kts new file mode 100644 index 00000000..4ab585c2 --- /dev/null +++ b/apps/opplysninger-aggregering/build.gradle.kts @@ -0,0 +1,101 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + //id("io.ktor.plugin") + id("org.openapi.generator") + id("com.google.cloud.tools.jib") + application +} + +val jvmMajorVersion: String by project +val baseImage: String by project +val image: String? by project + +val agents by configurations.creating + +dependencies { + // Project + implementation(project(":lib:hoplite-config")) + implementation(project(":lib:kafka-streams")) + implementation(project(":domain:main-avro-schema")) + + // Server + implementation(ktorServer.bundles.withNettyAndMicrometer) + + // Serialization + implementation(ktor.serializationJackson) + implementation(jackson.datatypeJsr310) + + // Logging + implementation(loggingLibs.logbackClassic) + implementation(loggingLibs.logstashLogbackEncoder) + implementation(navCommon.log) + + // Instrumentation + implementation(micrometer.registryPrometheus) + implementation(otel.api) + implementation(otel.annotations) + + // Kafka + implementation(orgApacheKafka.kafkaStreams) + implementation(apacheAvro.kafkaStreamsAvroSerde) + + // Test + testImplementation(ktorServer.testJvm) + testImplementation(testLibs.bundles.withUnitTesting) + testImplementation(testLibs.mockk) + testImplementation(orgApacheKafka.streamsTest) + + agents(otel.javaagent) +} + +application { + mainClass.set("no.nav.paw.arbeidssoekerregisteret.ApplicationKt") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(jvmMajorVersion)) + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +tasks.withType().configureEach { + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } +} + +tasks.withType(Jar::class) { + manifest { + attributes["Implementation-Version"] = project.version + attributes["Main-Class"] = application.mainClass.get() + attributes["Implementation-Title"] = rootProject.name + } +} + +tasks.register("copyAgents") { + from(agents) + into("${layout.buildDirectory.get()}/agents") +} + +tasks.named("assemble") { + finalizedBy("copyAgents") +} + +jib { + from.image = "$baseImage:$jvmMajorVersion" + to.image = "${image ?: project.name}:${project.version}" + container { + environment = mapOf( + "IMAGE_WITH_VERSION" to "${image ?: project.name}:${project.version}" + ) + jvmFlags = listOf( + "-XX:ActiveProcessorCount=4", "-XX:+UseZGC", "-XX:+ZGenerational" + ) + } +} diff --git a/apps/opplysninger-aggregering/nais/nais-dev.yaml b/apps/opplysninger-aggregering/nais/nais-dev.yaml new file mode 100644 index 00000000..70e0ba0d --- /dev/null +++ b/apps/opplysninger-aggregering/nais/nais-dev.yaml @@ -0,0 +1,40 @@ +apiVersion: nais.io/v1alpha1 +kind: Application +metadata: + name: paw-arbeidssoeker-opplysninger-aggregering + namespace: paw + labels: + team: paw +spec: + image: {{ image }} + port: 8080 + env: + - name: KAFKA_OPPLYSNINGER_OM_ARBEIDSSOEKER_STREAM_ID_SUFFIX + value: "opplysninger-stream-v1" + - name: KAFKA_PAW_OPPLYSNINGER_OM_ARBEIDSSOEKER_TOPIC + value: "paw.opplysninger-om-arbeidssoeker-v1" + resources: + limits: + memory: 1024Mi + requests: + cpu: 200m + memory: 256Mi + replicas: + min: 1 + max: 1 + liveness: + path: /internal/isAlive + initialDelay: 10 + readiness: + path: /internal/isReady + initialDelay: 10 + prometheus: + enabled: true + path: /internal/metrics + observability: + autoInstrumentation: + enabled: true + runtime: java + kafka: + pool: nav-dev + streams: true diff --git a/apps/opplysninger-aggregering/nais/nais-prod.yaml b/apps/opplysninger-aggregering/nais/nais-prod.yaml new file mode 100644 index 00000000..b610a081 --- /dev/null +++ b/apps/opplysninger-aggregering/nais/nais-prod.yaml @@ -0,0 +1,40 @@ +apiVersion: nais.io/v1alpha1 +kind: Application +metadata: + name: paw-arbeidssoeker-opplysninger-aggregering + namespace: paw + labels: + team: paw +spec: + image: {{ image }} + port: 8080 + env: + - name: KAFKA_OPPLYSNINGER_OM_ARBEIDSSOEKER_STREAM_ID_SUFFIX + value: "opplysninger-stream-v1" + - name: KAFKA_PAW_OPPLYSNINGER_OM_ARBEIDSSOEKER_TOPIC + value: "paw.opplysninger-om-arbeidssoeker-v1" + resources: + limits: + memory: 3072Mi + requests: + cpu: 250m + memory: 2048Mi + replicas: + min: 2 + max: 4 + liveness: + path: /internal/isAlive + initialDelay: 10 + readiness: + path: /internal/isReady + initialDelay: 10 + prometheus: + enabled: true + path: /internal/metrics + observability: + autoInstrumentation: + enabled: true + runtime: java + kafka: + pool: nav-dev + streams: true diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/Application.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/Application.kt new file mode 100644 index 00000000..d6b35fe7 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/Application.kt @@ -0,0 +1,64 @@ +package no.nav.paw.arbeidssoekerregisteret + +import io.ktor.server.application.Application +import io.ktor.server.engine.addShutdownHook +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.micrometer.prometheusmetrics.PrometheusConfig +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import no.nav.paw.arbeidssoekerregisteret.config.APPLICATION_CONFIG_FILE_NAME +import no.nav.paw.arbeidssoekerregisteret.config.APPLICATION_LOGGER_NAME +import no.nav.paw.arbeidssoekerregisteret.config.AppConfig +import no.nav.paw.arbeidssoekerregisteret.config.SERVER_CONFIG_FILE_NAME +import no.nav.paw.arbeidssoekerregisteret.config.SERVER_LOGGER_NAME +import no.nav.paw.arbeidssoekerregisteret.config.ServerConfig +import no.nav.paw.arbeidssoekerregisteret.context.ApplicationContext +import no.nav.paw.arbeidssoekerregisteret.plugins.configureKafka +import no.nav.paw.arbeidssoekerregisteret.plugins.configureMetrics +import no.nav.paw.arbeidssoekerregisteret.plugins.configureRouting +import no.nav.paw.arbeidssoekerregisteret.plugins.configureTracing +import no.nav.paw.arbeidssoekerregisteret.service.HealthIndicatorService +import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import org.slf4j.LoggerFactory + +fun main() { + val logger = LoggerFactory.getLogger(SERVER_LOGGER_NAME) + val serverProperties = loadNaisOrLocalConfiguration(SERVER_CONFIG_FILE_NAME) + val applicationProperties = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + + logger.info("Starter ${applicationProperties.appId}") + + val server = embeddedServer( + factory = Netty, + port = serverProperties.port, + configure = { + callGroupSize = serverProperties.callGroupSize + workerGroupSize = serverProperties.workerGroupSize + connectionGroupSize = serverProperties.connectionGroupSize + } + ) { + module(applicationProperties) + } + + server.addShutdownHook { + server.stop(serverProperties.gracePeriodMillis, serverProperties.timeoutMillis) + logger.info("Avslutter ${applicationProperties.appId}") + } + server.start(wait = true) +} + +fun Application.module(properties: AppConfig) { + val logger = LoggerFactory.getLogger(APPLICATION_LOGGER_NAME) + val healthIndicatorService = HealthIndicatorService() + val meterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT) + + with(ApplicationContext(logger, properties)) { + val kafkaStreamsMetrics = configureKafka( + healthIndicatorService, + meterRegistry + ) + configureTracing() + configureMetrics(meterRegistry, kafkaStreamsMetrics) + configureRouting(healthIndicatorService, meterRegistry) + } +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfig.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfig.kt new file mode 100644 index 00000000..959e73b1 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfig.kt @@ -0,0 +1,26 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import no.nav.paw.config.env.NaisEnv +import no.nav.paw.config.env.currentAppId +import no.nav.paw.config.env.currentAppName +import no.nav.paw.config.env.currentNaisEnv +import no.nav.paw.config.kafka.KafkaConfig +import java.time.Duration + +const val SERVER_LOGGER_NAME = "no.nav.paw.server" +const val APPLICATION_LOGGER_NAME = "no.nav.paw.application" +const val APPLICATION_CONFIG_FILE_NAME = "application_configuration.toml" + +data class AppConfig( + val kafka: KafkaConfig, + val kafkaStreams: KafkaStreamsConfig, + val appName: String = currentAppName ?: "paw-arbeidssoeker-opplysninger-aggregering", + val appId: String = currentAppId ?: "paw-arbeidssoeker-opplysninger-aggregering:LOCAL", + val naisEnv: NaisEnv = currentNaisEnv +) + +data class KafkaStreamsConfig( + val shutDownTimeout: Duration, + val opplysingerStreamIdSuffix: String, + val opplysningerTopic: String, +) diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/HealthIndicator.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/HealthIndicator.kt new file mode 100644 index 00000000..f9432ed3 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/HealthIndicator.kt @@ -0,0 +1,32 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import no.nav.paw.arbeidssoekerregisteret.model.HealthStatus +import java.util.concurrent.atomic.AtomicReference + +interface HealthIndicator { + fun setUnknown() + fun setHealthy() + fun setUnhealthy() + fun getStatus(): HealthStatus +} + +class StandardHealthIndicator(initialStatus: HealthStatus) : HealthIndicator { + + private val status = AtomicReference(initialStatus) + + override fun setUnknown() { + status.set(HealthStatus.UNKNOWN) + } + + override fun setHealthy() { + status.set(HealthStatus.HEALTHY) + } + + override fun setUnhealthy() { + status.set(HealthStatus.UNHEALTHY) + } + + override fun getStatus(): HealthStatus { + return status.get() + } +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Jackson.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Jackson.kt new file mode 100644 index 00000000..3f18915e --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Jackson.kt @@ -0,0 +1,31 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule + +val buildObjectMapper: ObjectMapper + get() = jacksonObjectMapper().apply { + configureJackson() + } + +fun ObjectMapper.configureJackson() { + setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + registerModule(JavaTimeModule()) + kotlinModule { + withReflectionCacheSize(512) + disable(KotlinFeature.NullIsSameAsDefault) + disable(KotlinFeature.SingletonSupport) + disable(KotlinFeature.StrictNullChecks) + enable(KotlinFeature.NullToEmptyCollection) + enable(KotlinFeature.NullToEmptyMap) + } +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/KafkaSerialization.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/KafkaSerialization.kt new file mode 100644 index 00000000..5c8255a7 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/KafkaSerialization.kt @@ -0,0 +1,48 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.paw.config.env.NaisEnv +import no.nav.paw.config.env.currentNaisEnv +import org.apache.kafka.common.serialization.Deserializer +import org.apache.kafka.common.serialization.Serde +import org.apache.kafka.common.serialization.Serializer + +inline fun buildJsonSerializer(naisEnv: NaisEnv, objectMapper: ObjectMapper) = object : Serializer { + override fun serialize(topic: String?, data: T): ByteArray { + if (data == null) return byteArrayOf() + try { + return objectMapper.writeValueAsBytes(data) + } catch (e: Exception) { + if (naisEnv == NaisEnv.ProdGCP && e is JsonProcessingException) e.clearLocation() + throw e + } + } +} + +inline fun buildJsonDeserializer(naisEnv: NaisEnv, objectMapper: ObjectMapper) = object : Deserializer { + override fun deserialize(topic: String?, data: ByteArray?): T? { + if (data == null) return null + try { + return objectMapper.readValue(data) + } catch (e: Exception) { + if (naisEnv == NaisEnv.ProdGCP && e is JsonProcessingException) e.clearLocation() + throw e + } + } +} + +inline fun buildJsonSerde(naisEnv: NaisEnv, objectMapper: ObjectMapper) = object : Serde { + override fun serializer(): Serializer { + return buildJsonSerializer(naisEnv, objectMapper) + } + + override fun deserializer(): Deserializer { + return buildJsonDeserializer(naisEnv, objectMapper) + } +} + +inline fun buildJsonSerde(): Serde { + return buildJsonSerde(currentNaisEnv, buildObjectMapper) +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Logging.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Logging.kt new file mode 100644 index 00000000..45bcaa22 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Logging.kt @@ -0,0 +1,6 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline val T.buildLogger: Logger get() = LoggerFactory.getLogger(T::class.java) \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Metrics.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Metrics.kt new file mode 100644 index 00000000..627f9e52 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/Metrics.kt @@ -0,0 +1,11 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import io.micrometer.core.instrument.MeterRegistry + +private const val METRIC_PREFIX = "paw_arbeidssoeker_opplysninger_aggregering" + +fun MeterRegistry.tellAntallMottatteOpplysninger() { + counter( + "${METRIC_PREFIX}_antall_mottatte_opplysninger_total", + ).increment() +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/ServerConfig.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/ServerConfig.kt new file mode 100644 index 00000000..3957bb39 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/config/ServerConfig.kt @@ -0,0 +1,12 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +const val SERVER_CONFIG_FILE_NAME = "server_configuration.toml" + +data class ServerConfig( + val port: Int, + val callGroupSize: Int, + val workerGroupSize: Int, + val connectionGroupSize: Int, + val gracePeriodMillis: Long, + val timeoutMillis: Long +) diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/context/ApplicationContext.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/context/ApplicationContext.kt new file mode 100644 index 00000000..8aa8f082 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/context/ApplicationContext.kt @@ -0,0 +1,6 @@ +package no.nav.paw.arbeidssoekerregisteret.context + +import no.nav.paw.arbeidssoekerregisteret.config.AppConfig +import org.slf4j.Logger + +data class ApplicationContext(val logger: Logger, val properties: AppConfig) diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/model/Health.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/model/Health.kt new file mode 100644 index 00000000..5c262ac4 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/model/Health.kt @@ -0,0 +1,7 @@ +package no.nav.paw.arbeidssoekerregisteret.model + +enum class HealthStatus(val value: String) { + UNKNOWN("UNKNOWN"), + HEALTHY("HEALTHY"), + UNHEALTHY("UNHEALTHY"), +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Kafka.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Kafka.kt new file mode 100644 index 00000000..279df4e9 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Kafka.kt @@ -0,0 +1,106 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins + + +import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.binder.kafka.KafkaStreamsMetrics +import no.nav.paw.arbeidssoekerregisteret.config.HealthIndicator +import no.nav.paw.arbeidssoekerregisteret.context.ApplicationContext +import no.nav.paw.arbeidssoekerregisteret.plugins.kafka.KafkaStreamsPlugin +import no.nav.paw.arbeidssoekerregisteret.service.HealthIndicatorService +import no.nav.paw.arbeidssoekerregisteret.topology.buildOpplysningerTopology +import no.nav.paw.config.kafka.streams.KafkaStreamsFactory +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology +import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler + +context(ApplicationContext) +fun Application.configureKafka( + healthIndicatorService: HealthIndicatorService, + meterRegistry: MeterRegistry, +): List { + + logger.info("Oppretter Kafka Stream for aggregering av opplysninger om arbeidssøker") + val periodeKafkaStreams = buildKafkaStreams( + properties.kafkaStreams.opplysingerStreamIdSuffix, + buildOpplysningerTopology(meterRegistry), + buildStateListener( + healthIndicatorService.newLivenessIndicator(), + healthIndicatorService.newReadinessIndicator() + ) + ) + + val kafkaStreamsList = mutableListOf(periodeKafkaStreams) + + install(KafkaStreamsPlugin) { + kafkaStreamsConfig = properties.kafkaStreams + kafkaStreams = kafkaStreamsList + } + + return kafkaStreamsList.map { KafkaStreamsMetrics(it) } +} + +context(ApplicationContext) +private fun buildKafkaStreams( + applicationIdSuffix: String, + topology: Topology, + stateListener: KafkaStreams.StateListener +): KafkaStreams { + val streamsFactory = KafkaStreamsFactory(applicationIdSuffix, properties.kafka) + .withDefaultKeySerde(Serdes.Long()::class) + .withDefaultValueSerde(SpecificAvroSerde::class) + + val kafkaStreams = KafkaStreams( + topology, + StreamsConfig(streamsFactory.properties) + ) + kafkaStreams.setStateListener(stateListener) + kafkaStreams.setUncaughtExceptionHandler(buildUncaughtExceptionHandler()) + return kafkaStreams +} + +context(ApplicationContext) +private fun buildStateListener( + livenessHealthIndicator: HealthIndicator, + readinessHealthIndicator: HealthIndicator +) = KafkaStreams.StateListener { newState, _ -> + when (newState) { + KafkaStreams.State.RUNNING -> { + readinessHealthIndicator.setHealthy() + } + + KafkaStreams.State.REBALANCING -> { + readinessHealthIndicator.setHealthy() + } + + KafkaStreams.State.PENDING_ERROR -> { + readinessHealthIndicator.setUnhealthy() + } + + KafkaStreams.State.PENDING_SHUTDOWN -> { + readinessHealthIndicator.setUnhealthy() + } + + KafkaStreams.State.ERROR -> { + readinessHealthIndicator.setUnhealthy() + livenessHealthIndicator.setUnhealthy() + } + + else -> { + readinessHealthIndicator.setUnknown() + } + } + + logger.info("Kafka Streams liveness er ${livenessHealthIndicator.getStatus().value}") + logger.info("Kafka Streams readiness er ${readinessHealthIndicator.getStatus().value}") +} + +context(ApplicationContext) +private fun buildUncaughtExceptionHandler() = StreamsUncaughtExceptionHandler { throwable -> + logger.error("Kafka Streams opplevde en uventet feil", throwable) + StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_APPLICATION +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Metrics.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Metrics.kt new file mode 100644 index 00000000..fb6c7b87 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Metrics.kt @@ -0,0 +1,40 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.metrics.micrometer.MicrometerMetrics +import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics +import io.micrometer.core.instrument.binder.kafka.KafkaStreamsMetrics +import io.micrometer.core.instrument.binder.system.ProcessorMetrics +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig +import no.nav.paw.arbeidssoekerregisteret.context.ApplicationContext +import java.time.Duration + +context(ApplicationContext) +fun Application.configureMetrics( + meterRegistry: MeterRegistry, + kafkaStreamsMetrics: List +) { + val metricsMeterBinders = mutableListOf( + JvmMemoryMetrics(), + JvmGcMetrics(), + ProcessorMetrics() + ) + metricsMeterBinders.addAll(kafkaStreamsMetrics) + install(MicrometerMetrics) { + registry = meterRegistry + meterBinders = metricsMeterBinders + distributionStatisticConfig = + DistributionStatisticConfig.builder() + .percentilesHistogram(true) + .maximumExpectedValue(Duration.ofSeconds(1).toNanos().toDouble()) + .minimumExpectedValue(Duration.ofMillis(20).toNanos().toDouble()) + .serviceLevelObjectives( + Duration.ofMillis(150).toNanos().toDouble(), + Duration.ofMillis(500).toNanos().toDouble() + ) + .build() + } +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Routing.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Routing.kt new file mode 100644 index 00000000..c6b9a705 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Routing.kt @@ -0,0 +1,18 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins + +import io.ktor.server.application.Application +import io.ktor.server.routing.routing +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import no.nav.paw.arbeidssoekerregisteret.context.ApplicationContext +import no.nav.paw.arbeidssoekerregisteret.routes.healthRoutes +import no.nav.paw.arbeidssoekerregisteret.service.HealthIndicatorService + +context(ApplicationContext) +fun Application.configureRouting( + healthIndicatorService: HealthIndicatorService, + meterRegistry: PrometheusMeterRegistry +) { + routing { + healthRoutes(healthIndicatorService, meterRegistry) + } +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Tracing.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Tracing.kt new file mode 100644 index 00000000..096f85a6 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/Tracing.kt @@ -0,0 +1,9 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import no.nav.paw.arbeidssoekerregisteret.plugins.tracing.OpenTelemetryPlugin + +fun Application.configureTracing() { + install(OpenTelemetryPlugin) +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/kafka/KafkaStreamsPlugin.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/kafka/KafkaStreamsPlugin.kt new file mode 100644 index 00000000..e150354c --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/kafka/KafkaStreamsPlugin.kt @@ -0,0 +1,33 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins.kafka + +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.ApplicationStopping +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import no.nav.paw.arbeidssoekerregisteret.config.KafkaStreamsConfig +import org.apache.kafka.streams.KafkaStreams + +@KtorDsl +class KafkaStreamsPluginConfig { + var kafkaStreamsConfig: KafkaStreamsConfig? = null + var kafkaStreams: List? = null +} + +val KafkaStreamsPlugin: ApplicationPlugin = + createApplicationPlugin("KafkaStreams", ::KafkaStreamsPluginConfig) { + val kafkaStreamsConfig = requireNotNull(pluginConfig.kafkaStreamsConfig) { "KafkaStreamsConfig er null" } + val kafkaStreams = requireNotNull(pluginConfig.kafkaStreams) { "KafkaStreams er null" } + + on(MonitoringEvent(ApplicationStarted)) { application -> + application.log.info("Starter Kafka Streams") + kafkaStreams.forEach { stream -> stream.start() } + } + + on(MonitoringEvent(ApplicationStopping)) { application -> + application.log.info("Stopper Kafka Streams") + kafkaStreams.forEach { stream -> stream.close(kafkaStreamsConfig.shutDownTimeout) } + } + } \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/tracing/OpenTelemetryPlugin.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/tracing/OpenTelemetryPlugin.kt new file mode 100644 index 00000000..7473a46b --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/plugins/tracing/OpenTelemetryPlugin.kt @@ -0,0 +1,11 @@ +package no.nav.paw.arbeidssoekerregisteret.plugins.tracing + +import io.ktor.server.application.createApplicationPlugin +import io.opentelemetry.api.trace.Span + +val OpenTelemetryPlugin = createApplicationPlugin("OpenTelemetryPlugin") { + onCallRespond { call, _ -> + runCatching { Span.current().spanContext.traceId } + .onSuccess { call.response.headers.append("x-trace-id", it) } + } +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutes.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutes.kt new file mode 100644 index 00000000..2e5666fe --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutes.kt @@ -0,0 +1,50 @@ +package no.nav.paw.arbeidssoekerregisteret.routes + +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import no.nav.paw.arbeidssoekerregisteret.model.HealthStatus +import no.nav.paw.arbeidssoekerregisteret.service.HealthIndicatorService + +fun Route.healthRoutes( + healthIndicatorService: HealthIndicatorService, + meterRegistry: PrometheusMeterRegistry +) { + + get("/internal/isAlive") { + when (val status = healthIndicatorService.getLivenessStatus()) { + HealthStatus.HEALTHY -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.OK + ) { status.value } + + else -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.ServiceUnavailable + ) { status.value } + } + } + + get("/internal/isReady") { + when (val status = healthIndicatorService.getReadinessStatus()) { + HealthStatus.HEALTHY -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.OK + ) { status.value } + + else -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.ServiceUnavailable + ) { status.value } + } + } + + get("/internal/metrics") { + call.respond(meterRegistry.scrape()) + } +} diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/service/HealthIndicatorService.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/service/HealthIndicatorService.kt new file mode 100644 index 00000000..351ca1dd --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/service/HealthIndicatorService.kt @@ -0,0 +1,43 @@ +package no.nav.paw.arbeidssoekerregisteret.service + +import no.nav.paw.arbeidssoekerregisteret.config.HealthIndicator +import no.nav.paw.arbeidssoekerregisteret.config.StandardHealthIndicator +import no.nav.paw.arbeidssoekerregisteret.model.HealthStatus + +class HealthIndicatorService { + + private val readinessIndicators = mutableListOf() + private val livenessIndicators = mutableListOf() + + fun newReadinessIndicator(): HealthIndicator { + val healthIndicator = StandardHealthIndicator(HealthStatus.UNKNOWN) + readinessIndicators.add(healthIndicator) + return healthIndicator + } + + fun newLivenessIndicator(): HealthIndicator { + val healthIndicator = StandardHealthIndicator(HealthStatus.HEALTHY) + livenessIndicators.add(healthIndicator) + return healthIndicator + } + + fun getReadinessStatus(): HealthStatus { + return if (readinessIndicators.all { it.getStatus() == HealthStatus.HEALTHY }) { + HealthStatus.HEALTHY + } else if (readinessIndicators.any { it.getStatus() == HealthStatus.UNHEALTHY }) { + HealthStatus.UNHEALTHY + } else { + HealthStatus.UNKNOWN + } + } + + fun getLivenessStatus(): HealthStatus { + return if (livenessIndicators.all { it.getStatus() == HealthStatus.HEALTHY }) { + HealthStatus.HEALTHY + } else if (livenessIndicators.any { it.getStatus() == HealthStatus.UNHEALTHY }) { + HealthStatus.UNHEALTHY + } else { + HealthStatus.UNKNOWN + } + } +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/topology/OpplysningerTopology.kt b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/topology/OpplysningerTopology.kt new file mode 100644 index 00000000..8dac3288 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/topology/OpplysningerTopology.kt @@ -0,0 +1,22 @@ +package no.nav.paw.arbeidssoekerregisteret.topology + +import io.micrometer.core.instrument.MeterRegistry +import no.nav.paw.arbeidssoekerregisteret.config.tellAntallMottatteOpplysninger +import no.nav.paw.arbeidssoekerregisteret.context.ApplicationContext +import no.nav.paw.arbeidssokerregisteret.api.v4.OpplysningerOmArbeidssoeker +import org.apache.kafka.streams.StreamsBuilder +import org.apache.kafka.streams.Topology + +context(ApplicationContext) +fun buildOpplysningerTopology( + meterRegistry: MeterRegistry +): Topology = StreamsBuilder().apply { + logger.info("Oppretter KStream for opplysninger om arbeidssøker") + val kafkaStreamsProperties = properties.kafkaStreams + + this.stream(kafkaStreamsProperties.opplysningerTopic) + .peek { key, _ -> + logger.debug("Mottok event på {} med key {}", kafkaStreamsProperties.opplysningerTopic, key) + meterRegistry.tellAntallMottatteOpplysninger() + } +}.build() diff --git a/apps/opplysninger-aggregering/src/main/resources/local/application_configuration.toml b/apps/opplysninger-aggregering/src/main/resources/local/application_configuration.toml new file mode 100644 index 00000000..e5dba424 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/resources/local/application_configuration.toml @@ -0,0 +1,11 @@ +[kafka] +brokers = "localhost:9092" +applicationIdPrefix = "paw.paw-arbeidssoeker-opplysninger-aggregering" + +[kafka.schemaRegistry] +url = "http://localhost:8082" + +[kafkaStreams] +shutDownTimeout = "PT1S" +opplysingerStreamIdSuffix = "opplysninger-stream-v1" +opplysningerTopic = "paw.opplysninger-om-arbeidssoeker-v1" diff --git a/apps/opplysninger-aggregering/src/main/resources/local/server_configuration.toml b/apps/opplysninger-aggregering/src/main/resources/local/server_configuration.toml new file mode 100644 index 00000000..5f9044d0 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/resources/local/server_configuration.toml @@ -0,0 +1,6 @@ +port = 8080 +callGroupSize = 16 +workerGroupSize = 8 +connectionGroupSize = 8 +gracePeriodMillis = 300 +timeoutMillis = 300 \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/resources/logback.xml b/apps/opplysninger-aggregering/src/main/resources/logback.xml new file mode 100644 index 00000000..32eb01af --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/resources/logback.xml @@ -0,0 +1,48 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %c{1}:%L - %m%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/main/resources/nais/application_configuration.toml b/apps/opplysninger-aggregering/src/main/resources/nais/application_configuration.toml new file mode 100644 index 00000000..607c2dfb --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/resources/nais/application_configuration.toml @@ -0,0 +1,18 @@ +[kafka] +brokers = "${KAFKA_BROKERS}" +applicationIdPrefix = "${KAFKA_STREAMS_APPLICATION_ID}" + +[kafka.authentication] +keystorePath = "${KAFKA_KEYSTORE_PATH}" +truststorePath = "${KAFKA_TRUSTSTORE_PATH}" +credstorePassword = "${KAFKA_CREDSTORE_PASSWORD}" + +[kafka.schemaRegistry] +url = "${KAFKA_SCHEMA_REGISTRY}" +username = "${KAFKA_SCHEMA_REGISTRY_USER}" +password = "${KAFKA_SCHEMA_REGISTRY_PASSWORD}" + +[kafkaStreams] +shutDownTimeout = "PT5S" +opplysingerStreamIdSuffix = "${KAFKA_OPPLYSNINGER_OM_ARBEIDSSOEKER_STREAM_ID_SUFFIX}" +opplysningerTopic = "${KAFKA_PAW_OPPLYSNINGER_OM_ARBEIDSSOEKER_TOPIC}" diff --git a/apps/opplysninger-aggregering/src/main/resources/nais/server_configuration.toml b/apps/opplysninger-aggregering/src/main/resources/nais/server_configuration.toml new file mode 100644 index 00000000..5f9044d0 --- /dev/null +++ b/apps/opplysninger-aggregering/src/main/resources/nais/server_configuration.toml @@ -0,0 +1,6 @@ +port = 8080 +callGroupSize = 16 +workerGroupSize = 8 +connectionGroupSize = 8 +gracePeriodMillis = 300 +timeoutMillis = 300 \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfigTest.kt b/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfigTest.kt new file mode 100644 index 00000000..7148d0e4 --- /dev/null +++ b/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/config/AppConfigTest.kt @@ -0,0 +1,13 @@ +package no.nav.paw.arbeidssoekerregisteret.config + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldNotBe +import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration + +class AppConfigTest : FreeSpec({ + "Skal laste config" { + val appConfig = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + appConfig.kafka shouldNotBe null + appConfig.kafkaStreams shouldNotBe null + } +}) \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutesTest.kt b/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutesTest.kt new file mode 100644 index 00000000..b7c21830 --- /dev/null +++ b/apps/opplysninger-aggregering/src/test/kotlin/no/nav/paw/arbeidssoekerregisteret/routes/HealthRoutesTest.kt @@ -0,0 +1,104 @@ +package no.nav.paw.arbeidssoekerregisteret.routes + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import io.mockk.every +import io.mockk.mockk +import no.nav.paw.arbeidssoekerregisteret.model.HealthStatus +import no.nav.paw.arbeidssoekerregisteret.service.HealthIndicatorService + +class HealthRoutesTest : FreeSpec({ + with(HealthRoutesTestContext()) { + "Test av health routes" { + every { meterRegistryMock.scrape() } returns "MOCK METRICS" + + testApplication { + routing { + healthRoutes(healthIndicatorService, meterRegistryMock) + } + + // Liveness health indicators er default HEALTHY + // Readiness health indicators er default UNKNOWN + + val metricsResponse = client.get("/internal/metrics") + metricsResponse.status shouldBe HttpStatusCode.OK + + var isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.OK + isAliveResponse.body() shouldBe HealthStatus.HEALTHY.value + + var isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isReadyResponse.body() shouldBe HealthStatus.UNKNOWN.value + + // Setter readiness health indicator til HEALTHY + readinessHealthIndicator.setHealthy() + + isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.OK + isAliveResponse.body() shouldBe HealthStatus.HEALTHY.value + + isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.OK + isReadyResponse.body() shouldBe HealthStatus.HEALTHY.value + + // Setter readiness health indicator til UNHEALTHY + readinessHealthIndicator.setUnhealthy() + + isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.OK + isAliveResponse.body() shouldBe HealthStatus.HEALTHY.value + + isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isReadyResponse.body() shouldBe HealthStatus.UNHEALTHY.value + + // Setter readiness health indicator tilbake til UNKNOWN + readinessHealthIndicator.setUnknown() + + isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.OK + isAliveResponse.body() shouldBe HealthStatus.HEALTHY.value + + isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isReadyResponse.body() shouldBe HealthStatus.UNKNOWN.value + + + // Setter liveness health indicator til UNHEALTHY + livenessHealthIndicator.setUnhealthy() + + isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isAliveResponse.body() shouldBe HealthStatus.UNHEALTHY.value + + isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isReadyResponse.body() shouldBe HealthStatus.UNKNOWN.value + + // Setter liveness health indicator til UNKNOWN + livenessHealthIndicator.setUnknown() + + isAliveResponse = client.get("/internal/isAlive") + isAliveResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isAliveResponse.body() shouldBe HealthStatus.UNKNOWN.value + + isReadyResponse = client.get("/internal/isReady") + isReadyResponse.status shouldBe HttpStatusCode.ServiceUnavailable + isReadyResponse.body() shouldBe HealthStatus.UNKNOWN.value + } + } + } +}) + +class HealthRoutesTestContext { + val healthIndicatorService = HealthIndicatorService() + val readinessHealthIndicator = healthIndicatorService.newReadinessIndicator() + val livenessHealthIndicator = healthIndicatorService.newLivenessIndicator() + val meterRegistryMock = mockk() +} \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/test/resources/kotest.properties b/apps/opplysninger-aggregering/src/test/resources/kotest.properties new file mode 100644 index 00000000..6fd9e569 --- /dev/null +++ b/apps/opplysninger-aggregering/src/test/resources/kotest.properties @@ -0,0 +1,2 @@ +kotest.framework.classpath.scanning.autoscan.disable=true +kotest.framework.classpath.scanning.config.disable=true \ No newline at end of file diff --git a/apps/opplysninger-aggregering/src/test/resources/logback-test.xml b/apps/opplysninger-aggregering/src/test/resources/logback-test.xml new file mode 100644 index 00000000..075a2408 --- /dev/null +++ b/apps/opplysninger-aggregering/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %c{1}:%L - %m%n + + + + + + + + + + + \ No newline at end of file diff --git a/apps/utgang-pdl/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/utgang/pdl/clients/pdl/PdlClient.kt b/apps/utgang-pdl/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/utgang/pdl/clients/pdl/PdlClient.kt index 96189202..f4c4bb6e 100644 --- a/apps/utgang-pdl/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/utgang/pdl/clients/pdl/PdlClient.kt +++ b/apps/utgang-pdl/src/main/kotlin/no/nav/paw/arbeidssoekerregisteret/utgang/pdl/clients/pdl/PdlClient.kt @@ -4,10 +4,10 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking +import no.nav.paw.config.env.currentNaisEnv import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient -import no.nav.paw.kafkakeygenerator.auth.currentNaisEnv import no.nav.paw.pdl.PdlClient import no.nav.paw.pdl.PdlException import no.nav.paw.pdl.graphql.generated.hentforenkletstatusbolk.HentPersonBolkResult @@ -27,7 +27,12 @@ fun interface PdlHentForenkletStatus { return PdlHentForenkletStatus { ident, callId, navConsumerId -> runBlocking { try { - pdlClient.hentForenkletStatusBolk(ident = ident, callId = callId, navConsumerId = navConsumerId, behandlingsnummer = BEHANDLINGSNUMMER) + pdlClient.hentForenkletStatusBolk( + ident = ident, + callId = callId, + navConsumerId = navConsumerId, + behandlingsnummer = BEHANDLINGSNUMMER + ) } catch (e: PdlException) { logger.error("PDL hentForenkletStatus feiler med: $e", e) null diff --git a/docker/grafana/config/grafana-datasources.yaml b/docker/grafana/config/grafana-datasources.yaml new file mode 100644 index 00000000..84c370d1 --- /dev/null +++ b/docker/grafana/config/grafana-datasources.yaml @@ -0,0 +1,30 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + orgId: 1 + url: http://prometheus:9090 + basicAuth: false + isDefault: false + version: 1 + editable: false + jsonData: + httpMethod: GET + - name: Tempo + type: tempo + access: proxy + orgId: 1 + url: http://tempo:3200 + basicAuth: false + isDefault: true + version: 1 + editable: false + apiVersion: 1 + uid: tempo + jsonData: + httpMethod: GET + serviceMap: + datasourceUid: prometheus diff --git a/docker/grafana/config/grafana-tempo.yaml b/docker/grafana/config/grafana-tempo.yaml new file mode 100644 index 00000000..7e4ee8e2 --- /dev/null +++ b/docker/grafana/config/grafana-tempo.yaml @@ -0,0 +1,61 @@ +stream_over_http_enabled: true +server: + http_listen_port: 3200 + log_level: info + +query_frontend: + search: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + trace_by_id: + duration_slo: 5s + +distributor: + receivers: # this configuration will listen on all ports and protocols that tempo is capable of. + # jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can + # protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver + # thrift_http: # + # grpc: # for a production deployment you should only enable the receivers you need! + # thrift_binary: + # thrift_compact: + # zipkin: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + # http: + # endpoint: 0.0.0.0:4318 + # opencensus: + +ingester: + max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally + +compactor: + compaction: + block_retention: 1h # overall Tempo trace retention. set for demo purposes + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: docker-compose + storage: + path: /var/tempo/generator/wal + remote_write: + - url: http://prometheus:9090/api/v1/write + send_exemplars: true + traces_storage: + path: /var/tempo/generator/traces + +storage: + trace: + backend: local # backend configuration to use + wal: + path: /var/tempo/wal # where to store the wal locally + local: + path: /var/tempo/blocks + +overrides: + defaults: + metrics_generator: + processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator diff --git a/docker/grafana/config/otel-collector.yaml b/docker/grafana/config/otel-collector.yaml new file mode 100644 index 00000000..d2ccabb0 --- /dev/null +++ b/docker/grafana/config/otel-collector.yaml @@ -0,0 +1,24 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + # http: + # endpoint: 0.0.0.0:4318 +exporters: + otlp: + endpoint: tempo:4317 + tls: + insecure: true +processors: + filter: + error_mode: ignore + traces: + span: + - IsMatch(attributes["url.path"], "/internal/*") +service: + pipelines: + traces: + receivers: [ otlp ] + processors: [ filter ] + exporters: [ otlp ] diff --git a/docker/grafana/config/prometheus.yaml b/docker/grafana/config/prometheus.yaml new file mode 100644 index 00000000..c63dd769 --- /dev/null +++ b/docker/grafana/config/prometheus.yaml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + metrics_path: /internal/metrics + static_configs: + - targets: [ "microfrontend-toggler:8080" ] + - job_name: "tempo" + static_configs: + - targets: [ "tempo:3200" ] diff --git a/docker/grafana/docker-compose.yaml b/docker/grafana/docker-compose.yaml new file mode 100644 index 00000000..5ce2dfb3 --- /dev/null +++ b/docker/grafana/docker-compose.yaml @@ -0,0 +1,95 @@ +### SERVICES ### +services: + # OTEL Collector + otel-collector: + image: otel/opentelemetry-collector:latest + container_name: otel-collector + depends_on: + - tempo + command: + - --config=/etc/otel-collector.yaml + # ports: + # - "4317:4317" + # - "55678:55678" + # - "55679:55679" + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + networks: + - grafana + + # Grafana + grafana: + image: grafana/grafana:latest + container_name: grafana + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor traceQLStreaming metricsSummary + ports: + - "3030:3000" + volumes: + - ./config/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - grafana-data:/var/lib/grafana + networks: + - grafana + + # Grafana Tempo + tempo: + image: grafana/tempo:latest + container_name: tempo + command: + - -config.file=/etc/tempo.yaml + # ports: + # - "14268:14268" # jaeger ingest + # - "3200:3200" # tempo + # - "9095:9095" # tempo grpc + # - "4317:4317" # otlp grpc + # - "4318:4318" # otlp http + # - "9411:9411" # zipkin + volumes: + - ./config/grafana-tempo.yaml:/etc/tempo.yaml + - tempo-data:/var/tempo + networks: + - grafana + + # Grafana K6 Tracing + #k6-tracing: + # image: ghcr.io/grafana/xk6-client-tracing:latest + # container_name: k6-tracing + # depends_on: + # - tempo + # environment: + # - ENDPOINT=tempo:4317 + # networks: + # - grafana + + # Prometheus + prometheus: + image: prom/prometheus:latest + container_name: prometheus + command: + - --config.file=/etc/prometheus.yaml + - --web.enable-remote-write-receiver + - --enable-feature=exemplar-storage + # ports: + # - "9090:9090" + volumes: + - ./config/prometheus.yaml:/etc/prometheus.yaml + - prometheus-data:/prometheus + networks: + - grafana + +### VOLUMES ### +volumes: + grafana-data: + name: grafana-data + tempo-data: + name: tempo-data + prometheus-data: + name: prometheus-data + +### NETWORKS ### +networks: + grafana: + name: grafana diff --git a/docker/kafka/docker-compose.yaml b/docker/kafka/docker-compose.yaml new file mode 100644 index 00000000..76511247 --- /dev/null +++ b/docker/kafka/docker-compose.yaml @@ -0,0 +1,107 @@ +### SERVICES ### +services: + # Kafka + kafka: + image: confluentinc/cp-server:7.6.1 + hostname: kafka + ports: + - "9092:9092" + # - "9101:9101" + environment: + KAFKA_NODE_ID: 1 + CLUSTER_ID: YTAzZDkwYmJjZmVmNDc3N2 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_LISTENERS: CONTROLLER://kafka:29093,INTERNAL://kafka:29092,EXTERNAL://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_JMX_PORT: 9101 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8082 + CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 + CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: kafka:29092 + CONFLUENT_METRICS_ENABLE: true + CONFLUENT_SUPPORT_CUSTOMER_ID: anonymous + KAFKA_LOG_DIRS: /tmp/kraft-combined-logs + volumes: + - kafka-data:/var/lib/kafka/data + - kafka-secrets:/etc/kafka/secrets + networks: + - kafka + + # Kafka Init + init-kafka: + image: confluentinc/cp-server:7.6.1 + depends_on: + - kafka + entrypoint: [ '/bin/sh', '-c' ] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:29092 --list + + echo -e 'Creating kafka topics' + kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic paw.arbeidssokerperioder-v1 --replication-factor 1 --partitions 1 + kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic paw.opplysninger-om-arbeidssoeker-v1 --replication-factor 1 --partitions 1 + + echo -e 'Successfully created the following topics:' + kafka-topics --bootstrap-server kafka:29092 --list + " + networks: + - kafka + + # Schema Registry + schema-registry: + image: confluentinc/cp-schema-registry:7.6.1 + hostname: schema-registry + depends_on: + - kafka + ports: + - "8082:8082" + environment: + SCHEMA_REGISTRY_HOST_NAME: schema-registry + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092' + SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8082 + volumes: + - schema-registry-secrets:/etc/schema-registry/secrets + networks: + - kafka + + # Kafka UI + kafka-ui: + image: provectuslabs/kafka-ui:latest + ports: + - "9000:8080" + environment: + DYNAMIC_CONFIG_ENABLED: "true" + KAFKA_CLUSTERS_0_NAME: main + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry:8082 + depends_on: + - kafka + - schema-registry + networks: + - kafka + +### VOLUMES ### +volumes: + kafka-data: + name: kafka-data + kafka-secrets: + name: kafka-secrets + schema-registry-secrets: + name: schema-registry-secrets + +### NETWORKS ### +networks: + kafka: + name: kafka diff --git a/docker/mocks/config/mock-oauth2-server/README.md b/docker/mocks/config/mock-oauth2-server/README.md new file mode 100644 index 00000000..9e7a631d --- /dev/null +++ b/docker/mocks/config/mock-oauth2-server/README.md @@ -0,0 +1,8 @@ +# Mock OAuth2 Server + +## Hente token + +### Hente token vha cURL +```shell +curl -H "Content-Type: application/x-www-form-urlencoded" -F "grant_type=client_credentials" -v "http://localhost:8081/default/token" +``` \ No newline at end of file diff --git a/docker/mocks/config/mock-oauth2-server/config.json b/docker/mocks/config/mock-oauth2-server/config.json new file mode 100644 index 00000000..7fe7b6e2 --- /dev/null +++ b/docker/mocks/config/mock-oauth2-server/config.json @@ -0,0 +1,35 @@ +{ + "interactiveLogin": true, + "httpServer": "NettyWrapper", + "tokenCallbacks": [ + { + "issuerId": "default", + "tokenExpiry": 3600, + "requestMappings": [ + { + "requestParam": "scope", + "match": "openid", + "claims": { + "sub": "admin@paw-microfrontend-toggler", + "aud": [ + "paw-microfrontend-toggler" + ], + "acr": "idporten-loa-high" + } + }, + { + "requestParam": "scope", + "match": "openid pid", + "claims": { + "sub": "admin@paw-microfrontend-toggler", + "aud": [ + "paw-microfrontend-toggler" + ], + "pid": "01017012345", + "acr": "idporten-loa-high" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/docker/mocks/config/wiremock/kafka-keys.json b/docker/mocks/config/wiremock/kafka-keys.json new file mode 100644 index 00000000..5f191f17 --- /dev/null +++ b/docker/mocks/config/wiremock/kafka-keys.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/kafka-keys" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 1, + "key": 1 + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/docker/mocks/config/wiremock/pdl.json b/docker/mocks/config/wiremock/pdl.json new file mode 100644 index 00000000..01327b99 --- /dev/null +++ b/docker/mocks/config/wiremock/pdl.json @@ -0,0 +1,51 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/pdl" + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + "hentPerson": { + "foedsel": [ + { + "foedselsdato": "1986-11-26", + "foedselsaar": "1986" + } + ], + "opphold": [ + { + "oppholdFra": "2010-01-20", + "oppholdTil": "2022-01-20", + "type": "PERMANENT" + } + ], + "folkeregisterpersonstatus": [ + { + "forenkletStatus": "bosattEtterFolkeregisterloven" + } + ], + "bostedsadresse": [ + { + "angittFlyttedato": "2010-01-20", + "gyldigFraOgMed": "2010-01-20", + "gyldigTilOgMed": "2022-01-20", + "vegadresse": { + "kommunenummer": "0301" + }, + "matrikkeladresse": null, + "ukjentBosted" : null, + "utenlandskAdresse": null + } + ], + "innflyttingTilNorge": [], + "utflyttingFraNorge": [] + } + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/docker/mocks/config/wiremock/poao-tilgang.json b/docker/mocks/config/wiremock/poao-tilgang.json new file mode 100644 index 00000000..c8f2b67f --- /dev/null +++ b/docker/mocks/config/wiremock/poao-tilgang.json @@ -0,0 +1,22 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/poao-tilgang/api/v1/policy/evaluate" + }, + "response": { + "status": 200, + "jsonBody": { + "results": [ + { + "requestId": "073ee000-3e61-11ed-b878-0242ac120002", + "decision": { + "type": "PERMIT" + } + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/docker/mocks/docker-compose.yaml b/docker/mocks/docker-compose.yaml new file mode 100644 index 00000000..f4512ffa --- /dev/null +++ b/docker/mocks/docker-compose.yaml @@ -0,0 +1,29 @@ +### SERVICES ### +services: + wiremock: + image: wiremock/wiremock:3.4.2 + container_name: wiremock + ports: + - "8090:8080" + volumes: + - ./config/wiremock:/home/wiremock/mappings/ + networks: + - mocks + + mock-oauth2-server: + image: ghcr.io/navikt/mock-oauth2-server:2.1.5 + container_name: mock-oauth2-server + ports: + - "8081:8081" + environment: + SERVER_PORT: 8081 + JSON_CONFIG_PATH: /config.json + volumes: + - ./config/mock-oauth2-server/config.json:/config.json + networks: + - mocks + +### NETWORKS ### +networks: + mocks: + name: mocks diff --git a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/NaisEnv.kt b/lib/hoplite-config/src/main/kotlin/no/nav/paw/config/env/NaisEnv.kt similarity index 52% rename from lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/NaisEnv.kt rename to lib/hoplite-config/src/main/kotlin/no/nav/paw/config/env/NaisEnv.kt index d64c8dba..30aad9eb 100644 --- a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/NaisEnv.kt +++ b/lib/hoplite-config/src/main/kotlin/no/nav/paw/config/env/NaisEnv.kt @@ -1,4 +1,4 @@ -package no.nav.paw.kafkakeygenerator.auth +package no.nav.paw.config.env enum class NaisEnv(val clusterName: String) { Local("local"), @@ -13,3 +13,7 @@ val currentNaisEnv: NaisEnv NaisEnv.ProdGCP.clusterName -> NaisEnv.ProdGCP else -> NaisEnv.Local } + +val currentAppId: String? get() = System.getenv("NAIS_APP_IMAGE") // F.eks. europe-north1-docker.pkg.dev/nais-management-233d/paw/paw-arbeidssoekerregisteret-utgang-pdl:24.01.01.01-1 + +val currentAppName: String? get() = System.getenv("NAIS_APP_NAME") // F.eks. paw-arbeidssoekerregisteret-utgang-pdl diff --git a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt b/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt index c1c0dffc..c7277d74 100644 --- a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt +++ b/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt @@ -5,6 +5,7 @@ import com.nimbusds.jose.jwk.RSAKey import no.nav.common.token_client.builder.AzureAdTokenClientBuilder import no.nav.common.token_client.cache.CaffeineTokenCache import no.nav.common.token_client.client.AzureAdMachineToMachineTokenClient +import no.nav.paw.config.env.NaisEnv import java.security.KeyPairGenerator import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey diff --git a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt b/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt index 4b07037a..4591ca1b 100644 --- a/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt +++ b/lib/kafka-key-generator-client/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt @@ -1,13 +1,13 @@ package no.nav.paw.kafkakeygenerator.client -import io.ktor.client.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.jackson.* +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.jackson.jackson import no.nav.common.token_client.client.AzureAdMachineToMachineTokenClient +import no.nav.paw.config.env.currentNaisEnv import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient -import no.nav.paw.kafkakeygenerator.auth.currentNaisEnv fun createKafkaKeyGeneratorClient(m2mTokenClient: AzureAdMachineToMachineTokenClient? = null): KafkaKeysClient { @@ -20,6 +20,7 @@ fun createKafkaKeyGeneratorClient(m2mTokenClient: AzureAdMachineToMachineTokenCl m2mTC.createMachineToMachineToken(kafkaKeyConfig.scope) } } + fun kafkaKeysClient(konfigurasjon: KafkaKeyConfig, m2mTokenFactory: () -> String): KafkaKeysClient = when (konfigurasjon.url) { "MOCK" -> inMemoryKafkaKeysMock() diff --git a/settings.gradle.kts b/settings.gradle.kts index 19019b63..087b3937 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ include( "apps:utgang-formidlingsgruppe", "apps:hendelselogg-backup", "apps:utgang-pdl", + "apps:opplysninger-aggregering", "domain:rapportering-interne-hendelser", "domain:rapporteringsansvar-schema", "domain:rapporteringsmelding-schema", @@ -85,6 +86,7 @@ dependencyResolutionManagement { val micrometerVersion = "1.13.1" val otelTargetSdkVersion = "1.39.0" val otelInstrumentationVersion = "2.4.0" + val otelJavaagentVersion = "2.5.0" val coroutinesVersion = "1.8.1" val rapporteringsSchemaVersion = "24.05.15.2-1" val postgresDriverVersion = "42.7.3" @@ -110,6 +112,7 @@ dependencyResolutionManagement { ) } create("ktorClient") { + bundle("withCio", listOf("core", "cio")) ktorLibs( "ktor-client-content-negotiation" alias "contentNegotiation", "ktor-client-core" alias "core", @@ -158,6 +161,7 @@ dependencyResolutionManagement { "io.opentelemetry.instrumentation", "opentelemetry-instrumentation-annotations" ).version(otelInstrumentationVersion) + library("javaagent", "io.opentelemetry.javaagent", "opentelemetry-javaagent").version(otelJavaagentVersion) } create("micrometer") { library("core", "io.micrometer", "micrometer-core").version(micrometerVersion)