diff --git a/build.gradle.kts b/build.gradle.kts index 2fb9dc4d3..1227fae67 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -189,6 +189,10 @@ dependencies { // Hvis det ønskes swagger doc, foreslås å bruke springdoc (springdoc-openapi-starter-webmvc-ui - se no.nav.fo.veilarbdialog.rest.SwaggerConfig for eksempelconfig) implementation("io.swagger.core.v3:swagger-annotations:2.2.8") + // BigQuery + implementation(platform("com.google.cloud:libraries-bom:26.45.0")) + implementation("com.google.cloud:google-cloud-bigquery") + implementation("io.getunleash:unleash-client-java:8.2.1") runtimeOnly("org.springframework.boot:spring-boot-devtools") diff --git a/nais/nais-dev-gcp.yaml b/nais/nais-dev-gcp.yaml index 798b02e7b..4c3ca7a60 100644 --- a/nais/nais-dev-gcp.yaml +++ b/nais/nais-dev-gcp.yaml @@ -56,6 +56,7 @@ spec: autoInstrumentation: enabled: true runtime: java + accessPolicy: inbound: rules: @@ -107,6 +108,10 @@ spec: envVarPrefix: DB users: - name: datastream + bigQueryDatasets: + - description: Contains big data, supporting big queries, for use in big ideas. + name: aktivitet_metrikker + permission: READWRITE env: - name: POAO_TILGANG_SCOPE value: api://dev-gcp.poao.poao-tilgang/.default diff --git a/nais/nais-prod-gcp.yaml b/nais/nais-prod-gcp.yaml index 36ac5724f..af3fc1799 100644 --- a/nais/nais-prod-gcp.yaml +++ b/nais/nais-prod-gcp.yaml @@ -105,6 +105,10 @@ spec: envVarPrefix: DB users: - name: datastream + bigQueryDatasets: + - description: Funksjonelle metrikker for aktivitetsplan + name: aktivitet_metrikker + permission: READWRITE env: - name: POAO_TILGANG_SCOPE value: api://prod-gcp.poao.poao-tilgang/.default diff --git a/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetAppService.java b/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetAppService.java index 2dd93454f..53bedf207 100644 --- a/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetAppService.java +++ b/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetAppService.java @@ -8,6 +8,8 @@ import no.nav.veilarbaktivitet.aktivitet.domain.AktivitetTypeData; import no.nav.veilarbaktivitet.aktivitet.feil.EndringAvFerdigAktivitetException; import no.nav.veilarbaktivitet.aktivitet.feil.EndringAvHistoriskAktivitetException; +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClient; +import no.nav.veilarbaktivitet.eventsLogger.EventType; import no.nav.veilarbaktivitet.person.Person; import no.nav.veilarbaktivitet.person.PersonService; import org.slf4j.Logger; @@ -29,6 +31,7 @@ public class AktivitetAppService { private final AktivitetService aktivitetService; private final MetricService metricService; private final PersonService personService; + private final BigQueryClient bigQueryClient; private static final Set TYPER_SOM_KAN_ENDRES_EKSTERNT = new HashSet<>(Arrays.asList( AktivitetTypeData.EGENAKTIVITET, @@ -111,6 +114,9 @@ public AktivitetData opprettNyAktivitet(AktivitetData aktivitetData) { } AktivitetData nyAktivitet = aktivitetService.opprettAktivitet(aktivitetData); + if (nyAktivitet.getAktivitetType() == AktivitetTypeData.SAMTALEREFERAT || nyAktivitet.getAktivitetType() == AktivitetTypeData.MOTE) { + bigQueryClient.logEvent(nyAktivitet, EventType.SAMTALEREFERAT_OPPRETTET); + } // dette er gjort på grunn av KVP return authService.erSystemBruker() ? nyAktivitet.withKontorsperreEnhetId(null) : nyAktivitet; @@ -224,12 +230,21 @@ public AktivitetData oppdaterReferat(AktivitetData aktivitet) { val originalAktivitet = hentAktivitet(aktivitet.getId()); kanEndreAktivitetGuard(originalAktivitet, aktivitet.getVersjon(), aktivitet.getAktorId()); - aktivitetService.oppdaterReferat( + var oppdatertAktivtiet = aktivitetService.oppdaterReferat( originalAktivitet, aktivitet ); - return hentAktivitet(aktivitet.getId()); + if(!originalAktivitet.getMoteData().isReferatPublisert() && oppdatertAktivtiet.getMoteData().isReferatPublisert()) { + bigQueryClient.logEvent(oppdatertAktivtiet, EventType.SAMTALEREFERAT_DELT_MED_BRUKER); + } + var forrigeReferat = originalAktivitet.getMoteData().getReferat(); + var nesteReferat = oppdatertAktivtiet.getMoteData().getReferat(); + if (forrigeReferat.isEmpty() && !nesteReferat.isEmpty() && oppdatertAktivtiet.getAktivitetType() == AktivitetTypeData.MOTE ) { + bigQueryClient.logEvent(oppdatertAktivtiet, EventType.SAMTALEREFERAT_FIKK_INNHOLD); + } + + return oppdatertAktivtiet; } } diff --git a/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetService.java b/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetService.java index ec667a8c5..fc95eb7b8 100644 --- a/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetService.java +++ b/src/main/java/no/nav/veilarbaktivitet/aktivitet/AktivitetService.java @@ -165,14 +165,14 @@ public void oppdaterMoteTidStedOgKanal(AktivitetData originalAktivitet, Aktivite aktivitetDAO.oppdaterAktivitet(oppdatertAktivitetMedNyFrist); } - public void oppdaterReferat( + public AktivitetData oppdaterReferat( AktivitetData originalAktivitet, AktivitetData aktivitetData ) { val transaksjon = getReferatTransakjsonType(originalAktivitet, aktivitetData); val merger = MappingUtils.merge(originalAktivitet, aktivitetData); - aktivitetDAO.oppdaterAktivitet(originalAktivitet + return aktivitetDAO.oppdaterAktivitet(originalAktivitet .withEndretDato(aktivitetData.getEndretDato()) .withEndretAv(aktivitetData.getEndretAv()) .withEndretAvType(Innsender.NAV) // Bare NAV kan endre referat diff --git a/src/main/java/no/nav/veilarbaktivitet/config/ApplicationContext.java b/src/main/java/no/nav/veilarbaktivitet/config/ApplicationContext.java index 640ca0ca8..f78ceae93 100644 --- a/src/main/java/no/nav/veilarbaktivitet/config/ApplicationContext.java +++ b/src/main/java/no/nav/veilarbaktivitet/config/ApplicationContext.java @@ -6,6 +6,8 @@ import no.nav.common.auth.context.AuthContextHolderThreadLocal; import no.nav.common.metrics.Event; import no.nav.common.metrics.MetricsClient; +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClient; +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClientImplementation; import no.nav.veilarbaktivitet.unleash.UnleashConfig; import no.nav.veilarbaktivitet.unleash.strategies.ByEnhetStrategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -31,6 +33,11 @@ public AuthContextHolder authContextHolder() { return AuthContextHolderThreadLocal.instance(); } + @Bean + public BigQueryClient bigQueryClient(BigQueryClientImplementation bigQueryClient) { + return bigQueryClient; + } + @Bean public MetricsClient metricsClient() { return new MetricsClient() { diff --git a/src/main/kotlin/no/nav/veilarbaktivitet/aktivitet/AktivitetsplanController.kt b/src/main/kotlin/no/nav/veilarbaktivitet/aktivitet/AktivitetsplanController.kt index cf832667a..69611046d 100644 --- a/src/main/kotlin/no/nav/veilarbaktivitet/aktivitet/AktivitetsplanController.kt +++ b/src/main/kotlin/no/nav/veilarbaktivitet/aktivitet/AktivitetsplanController.kt @@ -15,6 +15,7 @@ import no.nav.veilarbaktivitet.aktivitet.mappers.AktivitetDataMapperService import no.nav.veilarbaktivitet.aktivitetskort.MigreringService import no.nav.veilarbaktivitet.config.AktivitetResource import no.nav.veilarbaktivitet.config.OppfolgingsperiodeResource +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClient import no.nav.veilarbaktivitet.person.UserInContext import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* @@ -29,7 +30,8 @@ class AktivitetsplanController( private val appService: AktivitetAppService, private val aktivitetDataMapperService: AktivitetDataMapperService, private val userInContext: UserInContext, - private val migreringService: MigreringService + private val migreringService: MigreringService, + private val bigQueryClient: BigQueryClient ) { @Deprecated("Bruk graphql endepunkt") @GetMapping diff --git a/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryClient.kt b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryClient.kt new file mode 100644 index 000000000..e166d1728 --- /dev/null +++ b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryClient.kt @@ -0,0 +1,81 @@ +package no.nav.veilarbaktivitet.eventsLogger + +import com.google.cloud.bigquery.BigQueryOptions +import com.google.cloud.bigquery.InsertAllRequest +import com.google.cloud.bigquery.TableId +import no.nav.veilarbaktivitet.aktivitet.domain.AktivitetData +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.time.ZoneOffset +import java.time.ZonedDateTime + +enum class EventType { + SAMTALEREFERAT_OPPRETTET, + SAMTALEREFERAT_FIKK_INNHOLD, + SAMTALEREFERAT_DELT_MED_BRUKER, +} + +data class SamtalereferatPublisertFordeling( + val antallPublisert: Int, + val antallIkkePublisert: Int, +) + +interface BigQueryClient { + fun logEvent(aktivitetData: AktivitetData, eventType: EventType) + fun logSamtalereferatFordeling(samtalereferatPublisertFordeling: SamtalereferatPublisertFordeling) +} + +@Service +class BigQueryClientImplementation(@Value("\${gcp.projectId}") val projectId: String): BigQueryClient { + val SAMTALEREFERAT_EVENTS = "SAMTALEREFERAT_EVENTS" + val SAMTALEREFERAT_FORDELING = "SAMTALEREFERAT_FORDELING" + val DATASET_NAME = "aktivitet_metrikker" + val moteEventsTable = TableId.of(DATASET_NAME, SAMTALEREFERAT_EVENTS) + val samtalereferatFordelingTable = TableId.of(DATASET_NAME, SAMTALEREFERAT_FORDELING) + + fun TableId.insertRequest(row: Map): InsertAllRequest { + return InsertAllRequest.newBuilder(this).addRow(row).build() + } + + val bigQuery = BigQueryOptions.newBuilder().setProjectId(projectId).build().service + val log = LoggerFactory.getLogger(BigQueryClient::class.java) + + override fun logEvent(aktivitetData: AktivitetData, eventType: EventType) { + val moteRow = mapOf( + "aktivitetId" to aktivitetData.id, + "event" to eventType.name, + "versjon" to aktivitetData.versjon, + "endretDato" to ZonedDateTime.ofInstant(aktivitetData.endretDato.toInstant(), ZoneOffset.systemDefault()).toOffsetDateTime().toString(), + "erPublisert" to aktivitetData.moteData.isReferatPublisert, + "opprettet" to ZonedDateTime.ofInstant(aktivitetData.opprettetDato.toInstant(), ZoneOffset.systemDefault()).toOffsetDateTime().toString(), + "aktivitetsType" to aktivitetData.aktivitetType.name, + "referatLengde" to aktivitetData.moteData.referat.length + ) + val moteEvent = moteEventsTable.insertRequest(moteRow) + insertWhileToleratingErrors(moteEvent) + } + + override fun logSamtalereferatFordeling(samtalereferatPublisertFordeling: SamtalereferatPublisertFordeling) { + val fordelingRow = mapOf( + "antallPublisert" to samtalereferatPublisertFordeling.antallPublisert, + "antallIkkePublisert" to samtalereferatPublisertFordeling.antallIkkePublisert, + "timestamp" to ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC).toOffsetDateTime().toString(), + ) + val fordelingInsertRequest = samtalereferatFordelingTable.insertRequest(fordelingRow) + insertWhileToleratingErrors(fordelingInsertRequest) + } + + private fun insertWhileToleratingErrors(insertRequest: InsertAllRequest) { + runCatching { + val response = bigQuery.insertAll(insertRequest) + val errors = response.insertErrors + if (errors.isNotEmpty()) { + log.error("Error inserting bigquery rows", errors) + } + }.onFailure { + log.error("BigQuery error", it) + } + } + +} diff --git a/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryMetrikkCron.kt b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryMetrikkCron.kt new file mode 100644 index 000000000..833cba65d --- /dev/null +++ b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/BigQueryMetrikkCron.kt @@ -0,0 +1,22 @@ +package no.nav.veilarbaktivitet.eventsLogger + +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +@EnableScheduling +open class BigQueryMetrikkCron( + val bigQueryClient: BigQueryClient, + val isPublisertDAO: IsPublisertDAO +) { + + @Scheduled(cron = "@midnight") + @SchedulerLock(name = "aktiviteter_bigquery_metrikker", lockAtMostFor = "PT2M") + open fun hentPublisertCron() { + val fordeling = isPublisertDAO.hentIsPublisertFordeling() + bigQueryClient.logSamtalereferatFordeling(fordeling) + } + +} diff --git a/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/IsPublisertDAO.kt b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/IsPublisertDAO.kt new file mode 100644 index 000000000..df139e95e --- /dev/null +++ b/src/main/kotlin/no/nav/veilarbaktivitet/eventsLogger/IsPublisertDAO.kt @@ -0,0 +1,33 @@ +package no.nav.veilarbaktivitet.eventsLogger + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Repository + +@Repository +open class IsPublisertDAO( + val template: JdbcTemplate +) { + open fun hentIsPublisertFordeling(): SamtalereferatPublisertFordeling { + val sql = """ + select count(*) as antall, mote.referat_publisert as erPublisert from mote + join veilarbaktivitet.aktivitet a + on mote.aktivitet_id = a.aktivitet_id and mote.versjon = a.versjon + where mote.referat is not null + and a.til_dato < NOW() + and a.gjeldende = 1 -- Bare siste versjon + and a.historisk_dato is null -- Ikke ta med referat i avsluttede perioder + group by mote.referat_publisert + """.trimIndent() + return template.query(sql) { result, _ -> + val erPublisert = result.getInt("erPublisert") + val antall = result.getInt("antall") + if (erPublisert == 1) SamtalereferatPublisertFordeling(antallPublisert = antall, antallIkkePublisert = 0) + else SamtalereferatPublisertFordeling(antallPublisert = 0, antallIkkePublisert = antall) + }.let { fordelinger -> + SamtalereferatPublisertFordeling( + antallPublisert = fordelinger.sumOf { it.antallPublisert }, + antallIkkePublisert = fordelinger.sumOf { it.antallIkkePublisert }, + ) + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5129ded4b..add0075c3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -50,6 +50,7 @@ axsys.url=${AXSYS_URL} veilarbaktivitet-fss.url=${VEILARBAKTIVITET_FSS_URL} pdl.url=${PDL_URL} pdl.scope=${PDL_SCOPE} +gcp.projectId=${GCP_TEAM_PROJECT_ID} unleash.appName=${NAIS_APP_NAME} unleash.url=${UNLEASH_SERVER_API_URL} diff --git a/src/main/resources/db/migration/V8__referat_publisert_index.sql b/src/main/resources/db/migration/V8__referat_publisert_index.sql new file mode 100644 index 000000000..1b4805d19 --- /dev/null +++ b/src/main/resources/db/migration/V8__referat_publisert_index.sql @@ -0,0 +1,2 @@ +create index if not exists mote_publisert_index + on mote (referat_publisert); diff --git a/src/test/java/no/nav/veilarbaktivitet/config/ApplicationTestConfig.java b/src/test/java/no/nav/veilarbaktivitet/config/ApplicationTestConfig.java index 54fd5cb74..bed39c6af 100644 --- a/src/test/java/no/nav/veilarbaktivitet/config/ApplicationTestConfig.java +++ b/src/test/java/no/nav/veilarbaktivitet/config/ApplicationTestConfig.java @@ -12,6 +12,7 @@ import no.nav.common.token_client.client.TokenXOnBehalfOfTokenClient; import no.nav.common.utils.Credentials; import no.nav.veilarbaktivitet.db.DbTestUtils; +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClient; import no.nav.veilarbaktivitet.mock.MetricsClientMock; import okhttp3.EventListener; import org.mockito.Mockito; @@ -30,6 +31,11 @@ @EnableConfigurationProperties({EnvironmentProperties.class}) public class ApplicationTestConfig { + @Bean + public BigQueryClient bigQueryClient() { + return mock(BigQueryClient.class); + } + @Bean public AzureAdMachineToMachineTokenClient tokenClient() { AzureAdMachineToMachineTokenClient tokenClient = mock(AzureAdMachineToMachineTokenClient.class); diff --git a/src/test/java/no/nav/veilarbaktivitet/service/AktivitetAppServiceTest.java b/src/test/java/no/nav/veilarbaktivitet/service/AktivitetAppServiceTest.java index dc4e4cd7c..3d1999de5 100644 --- a/src/test/java/no/nav/veilarbaktivitet/service/AktivitetAppServiceTest.java +++ b/src/test/java/no/nav/veilarbaktivitet/service/AktivitetAppServiceTest.java @@ -10,6 +10,7 @@ import no.nav.veilarbaktivitet.aktivitet.domain.BehandlingAktivitetData; import no.nav.veilarbaktivitet.aktivitet.feil.EndringAvFerdigAktivitetException; import no.nav.veilarbaktivitet.aktivitet.feil.EndringAvHistoriskAktivitetException; +import no.nav.veilarbaktivitet.eventsLogger.BigQueryClient; import no.nav.veilarbaktivitet.person.Innsender; import no.nav.veilarbaktivitet.person.Person; import no.nav.veilarbaktivitet.person.PersonService; @@ -55,6 +56,9 @@ public class AktivitetAppServiceTest { @InjectMocks private AktivitetAppService appService; + @Mock + BigQueryClient bigQueryClient; + @Mock @SuppressWarnings("unused") // Blir faktisk brukt private MetricService metricService; diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 7d036ef81..62c45ca35 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -93,5 +93,7 @@ pdl.scope=api://dev-fss.pdl.pdl-api/.default spring.cloud.gateway.mvc.enabled=false +gcp.projectId=test + app.env.kassering.godkjenteIdenter=Z999999 shedlock.lockAtLeastFor=PT0S \ No newline at end of file