Skip to content

Commit 34bc1e7

Browse files
committed
Opprettet felles modul for failhåndtering
1 parent 92d496c commit 34bc1e7

14 files changed

+353
-10
lines changed

lib/error-handling/build.gradle.kts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
kotlin("jvm")
3+
}
4+
5+
dependencies {
6+
compileOnly(ktorServer.core)
7+
compileOnly(orgApacheKafka.kafkaStreams)
8+
compileOnly(loggingLibs.logbackClassic)
9+
10+
// Test
11+
testImplementation(testLibs.bundles.withUnitTesting)
12+
}
13+
14+
tasks.withType<Test>().configureEach {
15+
useJUnitPlatform()
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package no.nav.paw.error.exception
2+
3+
import io.ktor.http.HttpStatusCode
4+
5+
open class ClientResponseException(
6+
val status: HttpStatusCode,
7+
override val code: String,
8+
override val message: String,
9+
override val cause: Throwable?
10+
) : ErrorCodeAwareException(code, message, cause)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package no.nav.paw.error.exception
2+
3+
open class ErrorCodeAwareException(open val code: String, override val message: String, override val cause: Throwable?) :
4+
Exception(message, cause) {
5+
6+
constructor(code: String, message: String) : this(code, message, null)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package no.nav.paw.error.exception
2+
3+
import io.ktor.http.HttpStatusCode
4+
5+
open class ServerResponseException(
6+
val status: HttpStatusCode,
7+
override val code: String,
8+
override val message: String,
9+
override val cause: Throwable?
10+
) : ErrorCodeAwareException(code, message, cause)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package no.nav.paw.error.handler
2+
3+
import io.ktor.server.application.ApplicationCall
4+
import io.ktor.server.plugins.BadRequestException
5+
import io.ktor.server.plugins.ContentTransformationException
6+
import io.ktor.server.request.RequestAlreadyConsumedException
7+
import io.ktor.server.request.uri
8+
import io.ktor.server.response.respond
9+
import no.nav.paw.error.exception.ClientResponseException
10+
import no.nav.paw.error.exception.ServerResponseException
11+
import no.nav.paw.error.model.build400Error
12+
import no.nav.paw.error.model.build500Error
13+
import no.nav.paw.error.model.buildError
14+
import org.slf4j.Logger
15+
import org.slf4j.LoggerFactory
16+
17+
private const val ERROR_TYPE_PREFIX = "PAW_"
18+
private val logger: Logger = LoggerFactory.getLogger("paw.application.error.http")
19+
20+
suspend fun <T : Throwable> ApplicationCall.handleException(throwable: T) {
21+
when (throwable) {
22+
is ContentTransformationException -> {
23+
val error = build400Error(
24+
"${ERROR_TYPE_PREFIX}KUNNE_IKKE_TOLKE_INNHOLD",
25+
"Kunne ikke tolke innhold i kall",
26+
this.request.uri
27+
)
28+
logger.debug(error.detail, throwable)
29+
this.respond(error.status, error)
30+
}
31+
32+
is ClientResponseException -> {
33+
val error = buildError(
34+
"${ERROR_TYPE_PREFIX}${throwable.code}",
35+
throwable.message,
36+
throwable.status,
37+
this.request.uri
38+
)
39+
logger.warn(error.detail, throwable)
40+
this.respond(error.status, error)
41+
}
42+
43+
is ServerResponseException -> {
44+
val error = buildError(
45+
"${ERROR_TYPE_PREFIX}${throwable.code}",
46+
throwable.message,
47+
throwable.status,
48+
this.request.uri
49+
)
50+
logger.error(error.detail, throwable)
51+
this.respond(error.status, error)
52+
}
53+
54+
is BadRequestException -> {
55+
val error =
56+
build400Error(
57+
"${ERROR_TYPE_PREFIX}ULOVLIG_FORESPOERSEL",
58+
"Kunne ikke tolke innhold i forespørsel",
59+
this.request.uri
60+
)
61+
logger.error(error.detail, throwable)
62+
this.respond(error.status, error)
63+
}
64+
65+
is RequestAlreadyConsumedException -> {
66+
val error = build500Error(
67+
"${ERROR_TYPE_PREFIX}FORESPOERSEL_ALLEREDE_MOTTATT",
68+
"Forespørsel er allerede mottatt. Dette er en kodefeil",
69+
this.request.uri
70+
)
71+
logger.error(error.detail, throwable)
72+
this.respond(error.status, error)
73+
}
74+
75+
else -> {
76+
val error = build500Error(
77+
"${ERROR_TYPE_PREFIX}UKJENT_FEIL",
78+
"Forespørsel feilet med ukjent feil",
79+
this.request.uri
80+
)
81+
logger.error(error.detail, throwable)
82+
this.respond(error.status, error)
83+
}
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package no.nav.paw.error.handler
2+
3+
import org.apache.kafka.streams.KafkaStreams
4+
import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler
5+
import org.slf4j.Logger
6+
import org.slf4j.LoggerFactory
7+
8+
private val logger: Logger = LoggerFactory.getLogger("paw.application.error.kafka")
9+
10+
fun KafkaStreams.withApplicationTerminatingExceptionHandler() = StreamsUncaughtExceptionHandler { throwable ->
11+
logger.error("Kafka Streams opplevde en uventet feil", throwable)
12+
StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_APPLICATION
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package no.nav.paw.error.model
2+
3+
import io.ktor.http.HttpStatusCode
4+
5+
/**
6+
* Object som inneholder detaljer om en oppstått feilsituasjon, basert på RFC 7807.
7+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7807">IETF RFC 7807</a>
8+
*/
9+
data class ProblemDetails(
10+
val type: String,
11+
val title: String,
12+
val status: HttpStatusCode,
13+
val detail: String,
14+
val instance: String
15+
) {
16+
constructor(
17+
title: String,
18+
status: HttpStatusCode,
19+
detail: String,
20+
instance: String
21+
) : this("about:blank", title, status, detail, instance)
22+
}
23+
24+
fun build400Error(type: String, detail: String, instance: String) =
25+
buildError(type, detail, HttpStatusCode.BadRequest, instance)
26+
27+
fun build403Error(type: String, detail: String, instance: String) =
28+
buildError(type, detail, HttpStatusCode.Forbidden, instance)
29+
30+
fun build500Error(type: String, detail: String, instance: String) =
31+
buildError(type, detail, HttpStatusCode.InternalServerError, instance)
32+
33+
fun buildError(type: String, detail: String, status: HttpStatusCode, instance: String) = ProblemDetails(
34+
type = type,
35+
title = status.description,
36+
status = status,
37+
detail = detail,
38+
instance = instance
39+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package no.nav.paw.health.listener
2+
3+
import no.nav.paw.health.model.HealthIndicator
4+
import org.apache.kafka.streams.KafkaStreams
5+
import org.slf4j.LoggerFactory
6+
7+
private val logger = LoggerFactory.getLogger("paw.application.health.kafka")
8+
9+
fun KafkaStreams.withHealthIndicatorStateListener(
10+
livenessHealthIndicator: HealthIndicator,
11+
readinessHealthIndicator: HealthIndicator
12+
) = KafkaStreams.StateListener { newState, previousState ->
13+
when (newState) {
14+
KafkaStreams.State.RUNNING -> {
15+
readinessHealthIndicator.setHealthy()
16+
}
17+
18+
KafkaStreams.State.REBALANCING -> {
19+
readinessHealthIndicator.setHealthy()
20+
}
21+
22+
KafkaStreams.State.PENDING_ERROR -> {
23+
readinessHealthIndicator.setUnhealthy()
24+
}
25+
26+
KafkaStreams.State.PENDING_SHUTDOWN -> {
27+
readinessHealthIndicator.setUnhealthy()
28+
}
29+
30+
KafkaStreams.State.ERROR -> {
31+
readinessHealthIndicator.setUnhealthy()
32+
livenessHealthIndicator.setUnhealthy()
33+
}
34+
35+
else -> {
36+
readinessHealthIndicator.setUnknown()
37+
}
38+
}
39+
40+
logger.debug("Kafka Streams state endret seg ${previousState.name} -> ${newState.name}")
41+
logger.info("Kafka Streams liveness er ${livenessHealthIndicator.getStatus().value}")
42+
logger.info("Kafka Streams readiness er ${readinessHealthIndicator.getStatus().value}")
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package no.nav.paw.health.model
2+
3+
import java.util.concurrent.atomic.AtomicReference
4+
5+
enum class HealthStatus(val value: String) {
6+
UNKNOWN("UNKNOWN"),
7+
HEALTHY("HEALTHY"),
8+
UNHEALTHY("UNHEALTHY"),
9+
}
10+
11+
interface HealthIndicator {
12+
fun setUnknown()
13+
fun setHealthy()
14+
fun setUnhealthy()
15+
fun getStatus(): HealthStatus
16+
}
17+
18+
class StandardHealthIndicator(initialStatus: HealthStatus) : HealthIndicator {
19+
20+
private val status = AtomicReference(initialStatus)
21+
22+
override fun setUnknown() {
23+
status.set(HealthStatus.UNKNOWN)
24+
}
25+
26+
override fun setHealthy() {
27+
status.set(HealthStatus.HEALTHY)
28+
}
29+
30+
override fun setUnhealthy() {
31+
status.set(HealthStatus.UNHEALTHY)
32+
}
33+
34+
override fun getStatus(): HealthStatus {
35+
return status.get()
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package no.nav.paw.health.route
2+
3+
import io.ktor.http.ContentType
4+
import io.ktor.http.HttpStatusCode
5+
import io.ktor.server.application.call
6+
import io.ktor.server.response.respondText
7+
import io.ktor.server.routing.Route
8+
import io.ktor.server.routing.get
9+
import no.nav.paw.health.model.HealthStatus
10+
import no.nav.paw.health.service.HealthIndicatorService
11+
12+
fun Route.healthRoutes(
13+
healthIndicatorService: HealthIndicatorService,
14+
) {
15+
16+
get("/internal/isAlive") {
17+
when (val status = healthIndicatorService.getLivenessStatus()) {
18+
HealthStatus.HEALTHY -> call.respondText(
19+
ContentType.Text.Plain,
20+
HttpStatusCode.OK
21+
) { status.value }
22+
23+
else -> call.respondText(
24+
ContentType.Text.Plain,
25+
HttpStatusCode.ServiceUnavailable
26+
) { status.value }
27+
}
28+
}
29+
30+
get("/internal/isReady") {
31+
when (val status = healthIndicatorService.getReadinessStatus()) {
32+
HealthStatus.HEALTHY -> call.respondText(
33+
ContentType.Text.Plain,
34+
HttpStatusCode.OK
35+
) { status.value }
36+
37+
else -> call.respondText(
38+
ContentType.Text.Plain,
39+
HttpStatusCode.ServiceUnavailable
40+
) { status.value }
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package no.nav.paw.health.service
2+
3+
import no.nav.paw.health.model.HealthIndicator
4+
import no.nav.paw.health.model.HealthStatus
5+
import no.nav.paw.health.model.StandardHealthIndicator
6+
7+
class HealthIndicatorService {
8+
9+
private val readinessIndicators = mutableListOf<HealthIndicator>()
10+
private val livenessIndicators = mutableListOf<HealthIndicator>()
11+
12+
fun addReadinessIndicator(): HealthIndicator {
13+
val healthIndicator = StandardHealthIndicator(HealthStatus.UNKNOWN)
14+
readinessIndicators.add(healthIndicator)
15+
return healthIndicator
16+
}
17+
18+
fun addLivenessIndicator(): HealthIndicator {
19+
val healthIndicator = StandardHealthIndicator(HealthStatus.HEALTHY)
20+
livenessIndicators.add(healthIndicator)
21+
return healthIndicator
22+
}
23+
24+
fun getReadinessStatus(): HealthStatus {
25+
return if (readinessIndicators.all { it.getStatus() == HealthStatus.HEALTHY }) {
26+
HealthStatus.HEALTHY
27+
} else if (readinessIndicators.any { it.getStatus() == HealthStatus.UNHEALTHY }) {
28+
HealthStatus.UNHEALTHY
29+
} else {
30+
HealthStatus.UNKNOWN
31+
}
32+
}
33+
34+
fun getLivenessStatus(): HealthStatus {
35+
return if (livenessIndicators.all { it.getStatus() == HealthStatus.HEALTHY }) {
36+
HealthStatus.HEALTHY
37+
} else if (livenessIndicators.any { it.getStatus() == HealthStatus.UNHEALTHY }) {
38+
HealthStatus.UNHEALTHY
39+
} else {
40+
HealthStatus.UNKNOWN
41+
}
42+
}
43+
}

lib/kafka-streams/build.gradle.kts

+4-7
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ plugins {
22
kotlin("jvm")
33
}
44

5-
val koTestVersion = "5.7.2"
6-
75
dependencies {
86
api(project(":lib:kafka"))
9-
implementation("org.apache.kafka:kafka-clients:3.6.0")
10-
implementation("org.apache.kafka:kafka-streams:3.6.0")
11-
implementation("io.confluent:kafka-streams-avro-serde:7.4.0")
7+
implementation(orgApacheKafka.kafkaClients)
8+
implementation(orgApacheKafka.kafkaStreams)
9+
implementation(apacheAvro.kafkaStreamsAvroSerde)
1210

1311
// Test
14-
testImplementation("io.kotest:kotest-runner-junit5:$koTestVersion")
15-
testImplementation("io.kotest:kotest-assertions-core:$koTestVersion")
12+
testImplementation(testLibs.bundles.withUnitTesting)
1613
}
1714

1815
tasks.withType<Test>().configureEach {

0 commit comments

Comments
 (0)