Skip to content

Commit c9754cb

Browse files
committed
Erstattet fuel http klient med ktor http klient.
1 parent e4c3f62 commit c9754cb

File tree

9 files changed

+146
-137
lines changed

9 files changed

+146
-137
lines changed

dp-inntekt-api/build.gradle.kts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ dependencies {
4545
implementation("com.expediagroup:graphql-kotlin-ktor-client:$expediaGraphqlVersion")
4646
implementation("com.expediagroup:graphql-kotlin-client-jackson:$expediaGraphqlVersion")
4747

48-
implementation(libs.ktor.client.logging.jvm)
48+
implementation(libs.bundles.ktor.client)
4949
implementation("io.ktor:ktor-client-apache:${libs.versions.ktor.get()}")
5050

5151
implementation(libs.bundles.jackson)
@@ -58,10 +58,6 @@ dependencies {
5858

5959
implementation(libs.kotlin.logging)
6060

61-
implementation("com.github.kittinunf.fuel:fuel:2.2.1")
62-
implementation("com.github.kittinunf.fuel:fuel-moshi:2.2.1")
63-
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.2.1")
64-
6561
implementation("org.apache.logging.log4j:log4j-api:$log4j2Version")
6662
implementation("org.apache.logging.log4j:log4j-core:$log4j2Version")
6763
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version")

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/Application.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import no.nav.dagpenger.inntekt.db.KronetilleggUttrekk
1111
import no.nav.dagpenger.inntekt.db.PostgresInntektStore
1212
import no.nav.dagpenger.inntekt.db.dataSourceFrom
1313
import no.nav.dagpenger.inntekt.db.migrate
14-
import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektskomponentHttpClient
14+
import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentKtorClient
1515
import no.nav.dagpenger.inntekt.oppslag.enhetsregister.EnhetsregisterClient
1616
import no.nav.dagpenger.inntekt.oppslag.enhetsregister.httpClient
1717
import no.nav.dagpenger.inntekt.oppslag.pdl.PdlGraphQLRepository
@@ -57,7 +57,7 @@ fun main() {
5757
httpClient = httpClient(),
5858
)
5959
val inntektskomponentHttpClient =
60-
InntektskomponentHttpClient(
60+
InntektkomponentKtorClient(
6161
config.application.hentinntektListeUrl,
6262
stsOidcClient,
6363
)

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.ktor.http.ContentType
88
import io.ktor.http.HttpHeaders
99
import io.ktor.http.HttpStatusCode
1010
import io.ktor.http.isSuccess
11+
import io.ktor.http.withCharset
1112
import io.ktor.serialization.jackson.JacksonConverter
1213
import io.ktor.server.application.Application
1314
import io.ktor.server.application.install
@@ -227,7 +228,7 @@ internal fun Application.inntektApi(
227228
}
228229

229230
install(ContentNegotiation) {
230-
register(ContentType.Application.Json, JacksonConverter(jacksonObjectMapper))
231+
register(ContentType.Application.Json.withCharset(Charsets.UTF_8), JacksonConverter(jacksonObjectMapper))
231232
}
232233

233234
routing {

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/MoshiAdapters.kt

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package no.nav.dagpenger.inntekt
22

3-
import com.github.kittinunf.fuel.core.Request
4-
import com.github.kittinunf.fuel.core.ResponseDeserializable
5-
import com.github.kittinunf.fuel.core.response
63
import com.squareup.moshi.FromJson
74
import com.squareup.moshi.JsonAdapter
85
import com.squareup.moshi.Moshi
@@ -100,15 +97,3 @@ class URIJsonAdapter {
10097
return URI.create(json)
10198
}
10299
}
103-
104-
internal fun <T : Any> moshiDeserializerOf(clazz: Class<T>) =
105-
object : ResponseDeserializable<T> {
106-
override fun deserialize(content: String): T? =
107-
Moshi.Builder()
108-
.add(KotlinJsonAdapterFactory())
109-
.build()
110-
.adapter(clazz)
111-
.fromJson(content)
112-
}
113-
114-
internal inline fun <reified T : Any> Request.responseObject() = response(moshiDeserializerOf(T::class.java))
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package no.nav.dagpenger.inntekt.inntektskomponenten.v1
22

33
import java.time.Duration
4+
import java.time.YearMonth
45

56
interface InntektskomponentClient {
67
suspend fun getInntekt(
78
request: InntektkomponentRequest,
8-
timeouts: ConnectionTimeout = ConnectionTimeout(),
99
callId: String? = null,
1010
): InntektkomponentResponse
1111

@@ -14,3 +14,17 @@ interface InntektskomponentClient {
1414
val readTimeout: Duration = Duration.ofSeconds(15),
1515
)
1616
}
17+
18+
data class HentInntektListeRequest(
19+
val ainntektsfilter: String,
20+
val formaal: String,
21+
val ident: Aktoer,
22+
val maanedFom: YearMonth,
23+
val maanedTom: YearMonth,
24+
)
25+
26+
class InntektskomponentenHttpClientException(
27+
val status: Int,
28+
override val message: String,
29+
val detail: String? = null,
30+
) : RuntimeException(message)
Lines changed: 94 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
package no.nav.dagpenger.inntekt.inntektskomponenten.v1
22

3-
import com.github.kittinunf.fuel.core.awaitResponseResult
4-
import com.github.kittinunf.fuel.core.extensions.authentication
5-
import com.github.kittinunf.fuel.httpPost
6-
import com.github.kittinunf.fuel.moshi.moshiDeserializerOf
73
import de.huxhorn.sulky.ulid.ULID
4+
import io.ktor.client.HttpClient
5+
import io.ktor.client.call.body
6+
import io.ktor.client.engine.HttpClientEngine
7+
import io.ktor.client.engine.cio.CIO
8+
import io.ktor.client.plugins.HttpRequestTimeoutException
9+
import io.ktor.client.plugins.HttpTimeout
10+
import io.ktor.client.plugins.ServerResponseException
11+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
12+
import io.ktor.client.plugins.defaultRequest
13+
import io.ktor.client.plugins.logging.LogLevel
14+
import io.ktor.client.plugins.logging.Logging
15+
import io.ktor.client.request.header
16+
import io.ktor.client.request.post
17+
import io.ktor.client.request.setBody
18+
import io.ktor.client.statement.bodyAsText
19+
import io.ktor.http.ContentType
20+
import io.ktor.http.HttpHeaders
21+
import io.ktor.serialization.jackson.JacksonConverter
822
import io.prometheus.client.Counter
923
import io.prometheus.client.Summary
1024
import mu.KotlinLogging
1125
import mu.withLoggingContext
12-
import no.nav.dagpenger.inntekt.moshiInstance
26+
import no.nav.dagpenger.inntekt.serder.jacksonObjectMapper
1327
import no.nav.dagpenger.oidc.OidcClient
14-
import java.time.YearMonth
28+
import kotlin.time.Duration.Companion.seconds
1529

1630
private val logg = KotlinLogging.logger {}
1731
private val sikkerLogg = KotlinLogging.logger("tjenestekall")
18-
private val jsonResponseAdapter = moshiInstance.adapter(InntektkomponentResponse::class.java)
19-
private val jsonRequestRequestAdapter = moshiInstance.adapter(HentInntektListeRequest::class.java)
20-
private val jsonMapAdapter = moshiInstance.adapter(Map::class.java)
2132
private val ulid = ULID()
2233
const val INNTEKTSKOMPONENT_CLIENT_SECONDS_METRICNAME = "inntektskomponent_client_seconds"
2334
private val clientLatencyStats: Summary =
@@ -42,89 +53,90 @@ private val inntektskomponentStatusCodesCounter =
4253
.labelNames("status_code")
4354
.register()
4455

45-
class InntektskomponentHttpClient(
56+
internal class InntektkomponentKtorClient(
4657
private val hentInntektlisteUrl: String,
4758
private val oidcClient: OidcClient,
59+
private val timeouts: InntektskomponentClient.ConnectionTimeout = InntektskomponentClient.ConnectionTimeout(),
60+
engine: HttpClientEngine =
61+
CIO.create {
62+
requestTimeout = 30.seconds.inWholeMilliseconds
63+
},
4864
) : InntektskomponentClient {
65+
private val httpClient =
66+
HttpClient(engine) {
67+
expectSuccess = true
68+
install(Logging) {
69+
level = LogLevel.INFO
70+
}
71+
install(HttpTimeout) {
72+
connectTimeoutMillis = timeouts.connectionTimeout.toMillis()
73+
requestTimeoutMillis = timeouts.readTimeout.toMillis()
74+
socketTimeoutMillis = timeouts.connectionTimeout.toMillis()
75+
}
76+
install(ContentNegotiation) {
77+
register(ContentType.Application.Json, JacksonConverter(jacksonObjectMapper))
78+
}
79+
defaultRequest {
80+
header("Nav-Consumer-Id", "dp-inntekt-api")
81+
}
82+
}
83+
4984
override suspend fun getInntekt(
5085
request: InntektkomponentRequest,
51-
timeouts: InntektskomponentClient.ConnectionTimeout,
5286
callId: String?,
5387
): InntektkomponentResponse {
54-
val requestBody =
55-
HentInntektListeRequest(
56-
"DagpengerGrunnlagA-Inntekt",
57-
"Dagpenger",
58-
Aktoer(AktoerType.AKTOER_ID, request.aktørId),
59-
request.månedFom,
60-
request.månedTom,
61-
)
62-
val jsonBody = jsonRequestRequestAdapter.toJson(requestBody)
63-
val timer = clientLatencyStats.startTimer()
64-
val externalCallId = callId ?: ulid.nextULID()
65-
withLoggingContext(
66-
"callId" to externalCallId,
67-
) {
68-
logg.info("Fetching new inntekt for ${request.copy(fødselsnummer = "<REDACTED>")}")
88+
val requestBody = request.tilInntektListeRequest()
6989

70-
try {
71-
val (_, response, result) =
72-
with(hentInntektlisteUrl.httpPost()) {
73-
timeout(timeouts.connectionTimeout.toMillis().toInt())
74-
timeoutRead(timeouts.readTimeout.toMillis().toInt())
75-
76-
authentication().bearer(oidcClient.oidcToken().access_token)
77-
header("Nav-Consumer-Id" to "dp-inntekt-api")
78-
header("Nav-Call-Id" to externalCallId)
79-
body(jsonBody)
80-
awaitResponseResult(moshiDeserializerOf(jsonResponseAdapter))
90+
val externalCallId = callId ?: ulid.nextULID()
91+
withLoggingContext(mapOf("callId" to externalCallId)) {
92+
val timer = clientLatencyStats.startTimer()
93+
val response =
94+
try {
95+
httpClient.post(urlString = hentInntektlisteUrl) {
96+
header("Nav-Call-Id", externalCallId)
97+
header(HttpHeaders.ContentType, ContentType.Application.Json)
98+
header(HttpHeaders.Authorization, "Bearer ${oidcClient.oidcToken().access_token}")
99+
setBody(requestBody)
81100
}
101+
} catch (error: ServerResponseException) {
102+
val statusKode = error.response.status.value
103+
inntektskomponentStatusCodesCounter.labels(statusKode.toString()).inc()
104+
clientFetchErrors.inc()
105+
val feilmelding =
106+
kotlin.runCatching { jacksonObjectMapper.readTree(error.response.bodyAsText()).get("message").asText() }
107+
.getOrElse { error.message }
108+
throw InntektskomponentenHttpClientException(
109+
statusKode,
110+
"Failed to fetch inntekt. Problem message: $feilmelding",
111+
feilmelding,
112+
).also {
113+
logg.error(it) { it }
114+
sikkerLogg.error(it) { "Oppslag mot inntektskomponenten feilet. Request=$requestBody" }
115+
}
116+
} catch (timeout: HttpRequestTimeoutException) {
117+
val detail = "Tidsavbrudd mot inntektskomponenten. Brukte ${timer.observeDuration().seconds}"
118+
clientFetchErrors.inc()
119+
logg.error(timeout) { detail }
120+
throw InntektskomponentenHttpClientException(
121+
500,
122+
"Tidsavbrudd mot inntektskomponenten.",
123+
detail,
124+
)
125+
} finally {
126+
timer.observeDuration()
127+
}
128+
inntektskomponentStatusCodesCounter.labels(response.status.value.toString()).inc()
82129

83-
inntektskomponentStatusCodesCounter.labels(response.statusCode.toString()).inc()
84-
85-
return result.fold(
86-
{
87-
it
88-
},
89-
{ error ->
90-
val resp = error.response.body().asString("application/json")
91-
val detail =
92-
runCatching {
93-
jsonMapAdapter.fromJson(resp)
94-
}.let {
95-
it.getOrNull()?.get("message")?.toString() ?: error.message
96-
}
97-
98-
clientFetchErrors.inc()
99-
100-
throw InntektskomponentenHttpClientException(
101-
if (response.statusCode == -1) 500 else response.statusCode,
102-
@Suppress("ktlint:standard:max-line-length")
103-
"Failed to fetch inntekt. Status code ${response.statusCode}. Response message: ${response.responseMessage}. Problem message: $detail",
104-
detail,
105-
).also {
106-
logg.error(it) { it }
107-
sikkerLogg.error(it) { "Oppslag mot inntektskomponenten feilet. Request=$requestBody" }
108-
}
109-
},
110-
)
111-
} finally {
112-
timer.observeDuration()
113-
}
130+
return response.body()
114131
}
115132
}
116-
}
117133

118-
data class HentInntektListeRequest(
119-
val ainntektsfilter: String,
120-
val formaal: String,
121-
val ident: Aktoer,
122-
val maanedFom: YearMonth,
123-
val maanedTom: YearMonth,
124-
)
125-
126-
class InntektskomponentenHttpClientException(
127-
val status: Int,
128-
override val message: String,
129-
val detail: String? = null,
130-
) : RuntimeException(message)
134+
private fun InntektkomponentRequest.tilInntektListeRequest() =
135+
HentInntektListeRequest(
136+
"DagpengerGrunnlagA-Inntekt",
137+
"Dagpenger",
138+
Aktoer(AktoerType.AKTOER_ID, this.aktørId),
139+
this.månedFom,
140+
this.månedTom,
141+
)
142+
}

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRoute.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package no.nav.dagpenger.inntekt.v1
22

33
import com.auth0.jwt.exceptions.JWTDecodeException
4-
import io.ktor.http.ContentType
54
import io.ktor.http.HttpStatusCode
6-
import io.ktor.http.withCharset
75
import io.ktor.server.application.ApplicationCall
86
import io.ktor.server.application.call
97
import io.ktor.server.auth.authenticate
@@ -12,7 +10,6 @@ import io.ktor.server.auth.jwt.JWTPrincipal
1210
import io.ktor.server.plugins.callid.callId
1311
import io.ktor.server.request.receive
1412
import io.ktor.server.response.respond
15-
import io.ktor.server.response.respondText
1613
import io.ktor.server.routing.Route
1714
import io.ktor.server.routing.get
1815
import io.ktor.server.routing.post
@@ -29,7 +26,6 @@ import no.nav.dagpenger.inntekt.db.Inntektparametre
2926
import no.nav.dagpenger.inntekt.db.ManueltRedigert
3027
import no.nav.dagpenger.inntekt.db.RegelKontekst
3128
import no.nav.dagpenger.inntekt.db.StoreInntektCommand
32-
import no.nav.dagpenger.inntekt.inntektKlassifiseringsKoderJsonAdapter
3329
import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentRequest
3430
import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektskomponentClient
3531
import no.nav.dagpenger.inntekt.mapping.GUIInntekt
@@ -191,11 +187,12 @@ fun Route.uklassifisertInntekt(
191187
route("/verdikoder") {
192188
get {
193189
withContext(Dispatchers.IO) {
194-
call.respondText(
195-
inntektKlassifiseringsKoderJsonAdapter.toJson(dataGrunnlagKlassifiseringToVerdikode.values),
196-
ContentType.Application.Json.withCharset(Charsets.UTF_8),
197-
HttpStatusCode.OK,
198-
)
190+
call.respond(HttpStatusCode.OK, dataGrunnlagKlassifiseringToVerdikode.values)
191+
// call.respondText(
192+
// inntektKlassifiseringsKoderJsonAdapter.toJson(dataGrunnlagKlassifiseringToVerdikode.values),
193+
// ContentType.Application.Json.withCharset(Charsets.UTF_8),
194+
// HttpStatusCode.OK,
195+
// )
199196
}
200197
}
201198
}

dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/NaisChecksTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class NaisChecksTest {
2222
private val inntektStoreMock: InntektStore =
2323
mockk(
2424
relaxed = true,
25-
moreInterfaces = *arrayOf(HealthCheck::class),
25+
moreInterfaces = arrayOf(HealthCheck::class),
2626
)
2727
private val inntektStoreMockHealthCheck = inntektStoreMock as HealthCheck
2828

0 commit comments

Comments
 (0)