1
1
package no.nav.dagpenger.inntekt.inntektskomponenten.v1
2
2
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
7
3
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
8
22
import io.prometheus.client.Counter
9
23
import io.prometheus.client.Summary
10
24
import mu.KotlinLogging
11
25
import mu.withLoggingContext
12
- import no.nav.dagpenger.inntekt.moshiInstance
26
+ import no.nav.dagpenger.inntekt.serder.jacksonObjectMapper
13
27
import no.nav.dagpenger.oidc.OidcClient
14
- import java .time.YearMonth
28
+ import kotlin .time.Duration.Companion.seconds
15
29
16
30
private val logg = KotlinLogging .logger {}
17
31
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)
21
32
private val ulid = ULID ()
22
33
const val INNTEKTSKOMPONENT_CLIENT_SECONDS_METRICNAME = " inntektskomponent_client_seconds"
23
34
private val clientLatencyStats: Summary =
@@ -42,89 +53,90 @@ private val inntektskomponentStatusCodesCounter =
42
53
.labelNames(" status_code" )
43
54
.register()
44
55
45
- class InntektskomponentHttpClient (
56
+ internal class InntektkomponentKtorClient (
46
57
private val hentInntektlisteUrl : String ,
47
58
private val oidcClient : OidcClient ,
59
+ private val timeouts : InntektskomponentClient .ConnectionTimeout = InntektskomponentClient .ConnectionTimeout (),
60
+ engine : HttpClientEngine =
61
+ CIO .create {
62
+ requestTimeout = 30 .seconds.inWholeMilliseconds
63
+ },
48
64
) : 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
+
49
84
override suspend fun getInntekt (
50
85
request : InntektkomponentRequest ,
51
- timeouts : InntektskomponentClient .ConnectionTimeout ,
52
86
callId : String? ,
53
87
): 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()
69
89
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)
81
100
}
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()
82
129
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()
114
131
}
115
132
}
116
- }
117
133
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
+ }
0 commit comments