chore(cleanup): remove unused FallbackController and outdated GatewayDependencies.txt
- Deleted `FallbackController` as it is no longer required, with alternatives already in place. - Removed `GatewayDependencies.txt` to clean up outdated and redundant dependency tracking files.
This commit is contained in:
@@ -25,7 +25,7 @@ dependencies {
|
||||
|
||||
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
||||
// Die WebFlux-Abhängigkeit wird jetzt korrekt durch das BOM bereitgestellt.
|
||||
// implementation(libs.spring.boot.starter.webflux)
|
||||
implementation(libs.spring.boot.starter.webflux)
|
||||
|
||||
// Kern-Gateway inkl. Security, Actuator, CircuitBreaker, Discovery
|
||||
implementation(libs.bundles.gateway.core)
|
||||
@@ -34,6 +34,12 @@ dependencies {
|
||||
// Redis-Unterstützung für verteiltes Rate Limiting (RequestRateLimiter)
|
||||
implementation(libs.bundles.gateway.redis)
|
||||
|
||||
// === Tracing Dependencies (Micrometer Tracing) ===
|
||||
// Ermöglicht verteiltes Tracing über Thread-Grenzen hinweg (ersetzt manuellen MDC-Filter)
|
||||
implementation(libs.micrometer.tracing.bridge.brave)
|
||||
// Optional: Zipkin Reporter, falls du Traces an Zipkin senden willst (bereits im monitoringClient enthalten, aber hier explizit schadet nicht)
|
||||
// implementation(libs.zipkin.reporter.brave)
|
||||
|
||||
// === Test Dependencies ===
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package at.mocode.infrastructure.gateway
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.core.env.Environment
|
||||
@@ -11,7 +12,7 @@ class GatewayApplication
|
||||
fun main(args: Array<String>) {
|
||||
val context = runApplication<GatewayApplication>(*args)
|
||||
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
|
||||
val env = context.getBean(Environment::class.java)
|
||||
val env = context.getBean<Environment>()
|
||||
val port = env.getProperty("server.port") ?: "8081"
|
||||
|
||||
logger.info("""
|
||||
|
||||
+19
-19
@@ -1,5 +1,6 @@
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import io.micrometer.tracing.Tracer
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter
|
||||
@@ -7,17 +8,21 @@ import org.springframework.core.Ordered
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Global Filter für Correlations-IDs zur Request-Verfolgung.
|
||||
* Globaler Filter, der sicherstellt, dass die Trace-ID (von Micrometer Tracing)
|
||||
* auch als "X-Correlation-ID" im Response-Header zurückgegeben wird.
|
||||
*
|
||||
* Hinweis: Micrometer Tracing kümmert sich bereits automatisch um die Propagation
|
||||
* der Trace-ID (b3 oder w3c) an nachgelagerte Services. Dieser Filter dient nur
|
||||
* der Bequemlichkeit für Clients (z. B. Frontend), um die ID einfach auslesen zu können.
|
||||
*/
|
||||
@Component
|
||||
class CorrelationIdFilter : GlobalFilter, Ordered {
|
||||
class CorrelationIdFilter(private val tracer: Tracer) : GlobalFilter, Ordered {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(CorrelationIdFilter::class.java)
|
||||
|
||||
@@ -26,26 +31,21 @@ class CorrelationIdFilter : GlobalFilter, Ordered {
|
||||
}
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
||||
val request = exchange.request
|
||||
val correlationId = request.headers.getFirst(CORRELATION_ID_HEADER)
|
||||
?: UUID.randomUUID().toString()
|
||||
// Die aktuelle Trace-ID aus dem Micrometer Tracer holen
|
||||
val currentSpan = tracer.currentSpan()
|
||||
val traceId = currentSpan?.context()?.traceId()
|
||||
|
||||
val mutatedRequest = request.mutate()
|
||||
.header(CORRELATION_ID_HEADER, correlationId)
|
||||
.build()
|
||||
if (traceId != null) {
|
||||
// Trace-ID als Response-Header hinzufügen
|
||||
exchange.response.headers.add(CORRELATION_ID_HEADER, traceId)
|
||||
}
|
||||
|
||||
val mutatedExchange = exchange.mutate()
|
||||
.request(mutatedRequest)
|
||||
.build()
|
||||
|
||||
// Response-Header nach der Verarbeitung hinzufügen
|
||||
mutatedExchange.response.headers.add(CORRELATION_ID_HEADER, correlationId)
|
||||
|
||||
return chain.filter(mutatedExchange)
|
||||
return chain.filter(exchange)
|
||||
.doOnError { ex ->
|
||||
logger.error("Error in CorrelationIdFilter for request {}: {}", request.uri, ex.message)
|
||||
logger.error("Error processing request {}: {}", exchange.request.uri, ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
|
||||
// Niedrige Priorität, damit Tracing-Kontext bereits initialisiert ist
|
||||
override fun getOrder(): Int = Ordered.LOWEST_PRECEDENCE
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Primary
|
||||
import reactor.core.publisher.Mono
|
||||
import java.security.Principal
|
||||
|
||||
@Configuration
|
||||
class RateLimitConfig {
|
||||
|
||||
/**
|
||||
* Standard KeyResolver: IP-basiert.
|
||||
* Nutzt X-Forwarded-For (für Proxies/LoadBalancer), wenn vorhanden, sonst die Remote-Adresse.
|
||||
* Wird verwendet, wenn kein anderer KeyResolver explizit angefordert wird oder aktiv ist.
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
fun ipAddressKeyResolver(): KeyResolver = KeyResolver { exchange ->
|
||||
val forwardedFor = exchange.request.headers.getFirst("X-Forwarded-For")
|
||||
?.split(',')?.firstOrNull()?.trim()
|
||||
val ip = forwardedFor
|
||||
?: exchange.request.remoteAddress?.address?.hostAddress
|
||||
?: "unknown"
|
||||
Mono.just(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterter KeyResolver: Principal-basiert (User-ID).
|
||||
* Versucht, den authentifizierten User (Principal) zu nutzen.
|
||||
* Fallback auf IP-Adresse, falls der User nicht eingeloggt ist.
|
||||
*
|
||||
* Aktivierung über Property: gateway.ratelimit.principal-key-resolver.enabled=true
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "gateway.ratelimit.principal-key-resolver", name = ["enabled"], havingValue = "true", matchIfMissing = false)
|
||||
fun principalNameKeyResolver(): KeyResolver = KeyResolver { exchange ->
|
||||
exchange.getPrincipal<Principal>()
|
||||
.map { it.name }
|
||||
.switchIfEmpty(
|
||||
Mono.just(
|
||||
exchange.request.headers.getFirst("X-Forwarded-For")?.split(",")?.first()?.trim()
|
||||
?: exchange.request.headers.getFirst("X-Real-IP")
|
||||
?: exchange.request.remoteAddress?.address?.hostAddress
|
||||
?: "unknown"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import reactor.core.publisher.Mono
|
||||
import java.security.Principal
|
||||
|
||||
@Configuration
|
||||
class RateLimiterConfig {
|
||||
|
||||
/**
|
||||
* KeyResolver basierend auf authentifiziertem Principal; Fallback auf Client-IP.
|
||||
* Funktioniert out-of-the-box mit Keycloak (Resource Server), sofern Security aktiv ist.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "gateway.ratelimit.principal-key-resolver", name = ["enabled"], havingValue = "true", matchIfMissing = false)
|
||||
fun principalNameKeyResolver(): KeyResolver = KeyResolver { exchange ->
|
||||
exchange.getPrincipal<Principal>()
|
||||
.map { it.name }
|
||||
.switchIfEmpty(
|
||||
Mono.just(
|
||||
exchange.request.headers.getFirst("X-Forwarded-For")?.split(",")?.first()?.trim()
|
||||
?: exchange.request.headers.getFirst("X-Real-IP")
|
||||
?: exchange.request.remoteAddress?.address?.hostAddress
|
||||
?: "unknown"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Configuration
|
||||
class RateLimitingConfig {
|
||||
|
||||
/**
|
||||
* Einfache IP-basierte KeyResolver-Implementierung für das RequestRateLimiter-Filter.
|
||||
* Nutzt X-Forwarded-For, wenn vorhanden, sonst die Remote-Adresse.
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
fun ipAddressKeyResolver(): KeyResolver = KeyResolver { exchange ->
|
||||
val forwardedFor = exchange.request.headers.getFirst("X-Forwarded-For")
|
||||
?.split(',')?.firstOrNull()?.trim()
|
||||
val ip = forwardedFor
|
||||
?: exchange.request.remoteAddress?.address?.hostAddress
|
||||
?: "unknown"
|
||||
Mono.just(ip)
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package at.mocode.infrastructure.gateway.fallback
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Alternative FallbackController (deaktiviert per Default), nur aktivierbar über
|
||||
* property `gateway.customFallback.enabled=true`. Standardmäßig existiert bereits
|
||||
* ein FallbackController unter `...gateway.controller.FallbackController`.
|
||||
*/
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "gateway.customFallback", name = ["enabled"], havingValue = "true", matchIfMissing = false)
|
||||
class FallbackController {
|
||||
|
||||
@RequestMapping("/fallback/ping")
|
||||
fun pingFallback(): ResponseEntity<Map<String, Any>> =
|
||||
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(
|
||||
mapOf(
|
||||
"message" to "Ping service unavailable",
|
||||
"timestamp" to Instant.now().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
+58
-54
@@ -1,7 +1,8 @@
|
||||
package at.mocode.infrastructure.gateway.health
|
||||
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.ReactiveHealthIndicator
|
||||
import org.springframework.boot.health.contributor.Health
|
||||
import org.springframework.boot.health.contributor.ReactiveHealthIndicator
|
||||
import org.springframework.cloud.client.ServiceInstance
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.stereotype.Component
|
||||
@@ -9,7 +10,9 @@ import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.core.scheduler.Schedulers
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
/**
|
||||
* Gateway Health Indicator zur Überwachung der Downstream Services.
|
||||
@@ -40,46 +43,47 @@ class GatewayHealthIndicator(
|
||||
)
|
||||
|
||||
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
|
||||
private const val PARALLELISM = 8
|
||||
}
|
||||
|
||||
// KORREKTUR für Spring Boot 4: Die `health()`-Methode und ihr Rückgabetyp `Mono<Health>`
|
||||
// erlauben keine Null-Werte mehr. Die Fragezeichen (?) wurden entfernt.
|
||||
override fun health(): Mono<Health> {
|
||||
val builder = Health.up()
|
||||
val details = mutableMapOf<String, Any>()
|
||||
|
||||
return Mono.fromCallable { discoveryClient.services }
|
||||
.flatMapMany { services ->
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMapMany { services: List<String> ->
|
||||
details["totalServices"] = services.size
|
||||
Flux.fromIterable(services)
|
||||
}
|
||||
.flatMap({ serviceName ->
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
val instanceDetails = mapOf(
|
||||
"instanceCount" to instances.size,
|
||||
"instances" to instances.map { "${it.host}:${it.port}" }
|
||||
)
|
||||
// Für Health-Check nur auf definierte Services gehen
|
||||
val checkMono = when {
|
||||
CRITICAL_SERVICES.contains(serviceName) || OPTIONAL_SERVICES.contains(serviceName) ->
|
||||
checkServiceHealthReactive(serviceName)
|
||||
else -> Mono.just("SKIPPED")
|
||||
}
|
||||
checkMono
|
||||
.map { status -> Triple(serviceName, status, instanceDetails) }
|
||||
}, 8) // begrenze Parallelität
|
||||
.flatMap({ serviceName: String ->
|
||||
Mono.fromCallable { discoveryClient.getInstances(serviceName) }
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap { instances: List<ServiceInstance> ->
|
||||
val instanceDetails = mapOf(
|
||||
"instanceCount" to instances.size,
|
||||
"instances" to instances.map { "${it.host}:${it.port}" }
|
||||
)
|
||||
|
||||
val checkMono: Mono<String> = when {
|
||||
CRITICAL_SERVICES.contains(serviceName) || OPTIONAL_SERVICES.contains(serviceName) ->
|
||||
checkServiceHealthReactive(serviceName, instances)
|
||||
else -> Mono.just("SKIPPED")
|
||||
}
|
||||
checkMono.map { status -> Triple(serviceName, status, instanceDetails) }
|
||||
}
|
||||
}, PARALLELISM)
|
||||
.collectList()
|
||||
.map { results ->
|
||||
.flatMap { results: List<Triple<String, String, Map<String, Any>>> ->
|
||||
val discoveredServices = mutableMapOf<String, Any>()
|
||||
val criticalServiceStatus = mutableMapOf<String, String>()
|
||||
val optionalServiceStatus = mutableMapOf<String, String>()
|
||||
|
||||
results.forEach { (serviceName, status, instanceDetails) ->
|
||||
discoveredServices[serviceName] = instanceDetails
|
||||
if (CRITICAL_SERVICES.contains(serviceName)) {
|
||||
criticalServiceStatus[serviceName] = status
|
||||
} else if (OPTIONAL_SERVICES.contains(serviceName)) {
|
||||
optionalServiceStatus[serviceName] = status
|
||||
when {
|
||||
CRITICAL_SERVICES.contains(serviceName) -> criticalServiceStatus[serviceName] = status
|
||||
OPTIONAL_SERVICES.contains(serviceName) -> optionalServiceStatus[serviceName] = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,48 +103,48 @@ class GatewayHealthIndicator(
|
||||
details["status"] = "UP"
|
||||
details["reason"] = when {
|
||||
isTestEnvironment -> "Gesundheitsprüfung erfolgreich (Testumgebung)"
|
||||
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung - nicht alle Services erforderlich)"
|
||||
else -> "Alle kritischen Services sind verfügbar oder optional"
|
||||
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung)"
|
||||
else -> "Alle kritischen Services sind verfügbar"
|
||||
}
|
||||
}
|
||||
|
||||
builder.withDetails(details).build()
|
||||
Mono.just(builder.withDetails(details).build())
|
||||
}
|
||||
.onErrorResume { ex ->
|
||||
Mono.just(
|
||||
Health.down(ex)
|
||||
.withDetail("status", "DOWN")
|
||||
.withDetail("reason", "Fehler beim Prüfen der nachgelagerten Services: ${ex.message}")
|
||||
.withDetail("reason", "Fehler bei der Service-Prüfung: ${ex.message}")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkServiceHealthReactive(serviceName: String): Mono<String> {
|
||||
return Mono.fromCallable { discoveryClient.getInstances(serviceName) }
|
||||
.flatMap { instances ->
|
||||
if (instances.isEmpty()) {
|
||||
Mono.just("NO_INSTANCES")
|
||||
} else {
|
||||
val instance = instances.first()
|
||||
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
|
||||
webClient.get()
|
||||
.uri(healthUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Map::class.java)
|
||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
||||
.map { it["status"]?.toString() ?: "UNKNOWN" }
|
||||
.map { status -> if (status == "UP") "UP" else "DOWN" }
|
||||
.onErrorResume { ex ->
|
||||
when (ex) {
|
||||
is WebClientResponseException -> when (ex.statusCode.value()) {
|
||||
404 -> Mono.just("NO_HEALTH_ENDPOINT")
|
||||
503 -> Mono.just("DOWN")
|
||||
else -> Mono.just("ERROR")
|
||||
}
|
||||
else -> Mono.just("ERROR")
|
||||
}
|
||||
}
|
||||
private fun checkServiceHealthReactive(serviceName: String, instances: List<ServiceInstance>): Mono<String> {
|
||||
if (instances.isEmpty()) {
|
||||
return Mono.just("NO_INSTANCES")
|
||||
}
|
||||
|
||||
// Wir prüfen exemplarisch die erste Instanz
|
||||
val instance = instances.first()
|
||||
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
|
||||
|
||||
return webClient.get()
|
||||
.uri(healthUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Map::class.java)
|
||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
||||
.map { it["status"]?.toString() ?: "UNKNOWN" }
|
||||
.map { status -> if (status.equals("UP", ignoreCase = true)) "UP" else "DOWN" }
|
||||
.onErrorResume { ex ->
|
||||
when (ex) {
|
||||
is WebClientResponseException -> when (ex.statusCode.value()) {
|
||||
404 -> Mono.just("NO_HEALTH_ENDPOINT")
|
||||
503 -> Mono.just("DOWN")
|
||||
else -> Mono.just("ERROR")
|
||||
}
|
||||
is TimeoutException -> Mono.just("TIMEOUT")
|
||||
else -> Mono.just("ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-20
@@ -16,6 +16,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
|
||||
@Configuration
|
||||
@@ -69,35 +70,53 @@ class SecurityConfig(
|
||||
* Erstellt einen ReactiveJwtDecoder für die JWT-Validierung.
|
||||
*
|
||||
* Verwendet die JWK Set URI aus der Konfiguration, um die öffentlichen Schlüssel
|
||||
* von Keycloak zu laden. Falls die URI nicht konfiguriert ist oder Keycloak
|
||||
* nicht erreichbar ist, wird trotzdem ein Bean erstellt, um Startfehler zu vermeiden.
|
||||
* von Keycloak zu laden.
|
||||
*
|
||||
* Resilience-Optimierung:
|
||||
* Anstatt beim Start zu failen oder einen statischen NoOp-Decoder zu nutzen,
|
||||
* verwenden wir einen delegierenden Decoder. Dieser versucht bei jedem Request,
|
||||
* den echten Decoder (lazy) zu initialisieren, falls er noch nicht bereit ist.
|
||||
* So kann Keycloak auch NACH dem Gateway starten.
|
||||
*/
|
||||
@Bean
|
||||
fun reactiveJwtDecoder(
|
||||
@Value($$"${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}") jwkSetUri: String
|
||||
): ReactiveJwtDecoder {
|
||||
return if (jwkSetUri.isNotBlank()) {
|
||||
try {
|
||||
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
} catch (e: Exception) {
|
||||
// Log warning and return a no-op decoder to allow startup
|
||||
logger.warn("Failed to configure JWT decoder with JWK Set URI: {} - {}", jwkSetUri, e.message)
|
||||
logger.warn("JWT authentication will not work until Keycloak is available")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
} else {
|
||||
logger.info("No JWK Set URI configured, using no-op JWT decoder")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
return ResilienceReactiveJwtDecoder(jwkSetUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen No-Op JWT Decoder für Fälle, in denen Keycloak nicht verfügbar ist.
|
||||
* Dieser Decoder lehnt alle Token ab, erlaubt aber den Anwendungsstart.
|
||||
* Ein Wrapper um den NimbusReactiveJwtDecoder, der Initialisierungsfehler abfängt
|
||||
* und erst zur Laufzeit (lazy) versucht, die JWKs zu laden.
|
||||
*/
|
||||
private fun createNoOpJwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoder { token ->
|
||||
throw IllegalStateException("JWT validation is not available - Keycloak may not be running")
|
||||
class ResilienceReactiveJwtDecoder(private val jwkSetUri: String) : ReactiveJwtDecoder {
|
||||
private val logger = LoggerFactory.getLogger(ResilienceReactiveJwtDecoder::class.java)
|
||||
private var delegate: ReactiveJwtDecoder? = null
|
||||
|
||||
override fun decode(token: String): Mono<org.springframework.security.oauth2.jwt.Jwt> {
|
||||
if (delegate == null) {
|
||||
synchronized(this) {
|
||||
if (delegate == null) {
|
||||
try {
|
||||
if (jwkSetUri.isBlank()) {
|
||||
throw IllegalArgumentException("JWK Set URI is missing")
|
||||
}
|
||||
logger.info("Attempting to initialize JWT Decoder with URI: {}", jwkSetUri)
|
||||
delegate = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
logger.info("JWT Decoder successfully initialized.")
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not initialize JWT Decoder (Keycloak might be down): {}", e.message)
|
||||
return Mono.error(IllegalStateException("Identity Provider currently unavailable. Please try again later."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return delegate!!.decode(token)
|
||||
.onErrorResume { e ->
|
||||
// Falls der Decoder zwar da ist, aber z.B. Netzwerkfehler auftreten, loggen wir das
|
||||
logger.debug("JWT decoding failed: {}", e.message)
|
||||
Mono.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ management:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,prometheus
|
||||
tracing:
|
||||
sampling:
|
||||
probability: 1.0 # 100% der Requests tracen (für Dev/Test sinnvoll, in Prod reduzieren)
|
||||
propagation:
|
||||
type: w3c # Standard W3C Trace Context (kompatibel mit OpenTelemetry)
|
||||
|
||||
# Gateway-spezifische Einstellungen
|
||||
gateway:
|
||||
|
||||
@@ -1,61 +1,38 @@
|
||||
// Optimized Spring Boot ping service for testing microservice architecture
|
||||
// This service demonstrates circuit breaker patterns, service discovery, and monitoring
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
// FINALE BEREINIGUNG: Das `dependencyManagement`-Plugin wird entfernt.
|
||||
// alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
}
|
||||
|
||||
// Configure the main class for the executable JAR
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.ping.service.PingServiceApplicationKt")
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
// Aktiviert die experimentelle UUID API von Kotlin 2.3.0
|
||||
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Die `platform`-Deklaration ist der einzig korrekte Weg.
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
// === Project Dependencies ===
|
||||
implementation(projects.backend.services.ping.pingApi)
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
// Platform und Core Dependencies
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.ping.pingApi)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
// === Spring Boot & Cloud ===
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
// WICHTIG: Da wir JPA (blockierend) nutzen, brauchen wir Spring MVC (nicht WebFlux)
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.bundles.spring.cloud.gateway) // Für Discovery Client
|
||||
|
||||
// Spring Boot Service Complete Bundle
|
||||
// Provides: web, validation, actuator, security, oauth2-client, oauth2-resource-server,
|
||||
// data-jpa, data-redis, micrometer-prometheus, tracing, zipkin
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
// === Database & Persistence ===
|
||||
implementation(libs.bundles.database.complete)
|
||||
|
||||
// Datenbank (PostgresQL) Driver
|
||||
implementation(libs.postgresql.driver)
|
||||
// === Resilience ===
|
||||
implementation(libs.bundles.resilience)
|
||||
|
||||
// Web-Server (Tomcat) explizit hinzufügen!
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
// Jackson Kotlin Support Bundle
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
|
||||
// Kotlin Reflection (now from version catalog)
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
// Service Discovery
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
|
||||
// Caching (Caffeine for Spring Cloud LoadBalancer)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.spring.web) // Provides spring-context-support
|
||||
|
||||
// Resilience4j Bundle (Circuit Breaker, Reactor, AOP)
|
||||
implementation(libs.bundles.resilience)
|
||||
|
||||
// OpenAPI Documentation
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// Test Dependencies
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.spring.boot.starter.web)
|
||||
// === Testing ===
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package at.mocode.ping.service
|
||||
package at.mocode.ping
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.reactive.config.CorsRegistry
|
||||
|
||||
@SpringBootApplication
|
||||
// Scannt explizit alle Sub-Packages (infrastructure, application, domain)
|
||||
@EnableAspectJAutoProxy
|
||||
class PingServiceApplication {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package at.mocode.ping.application
|
||||
|
||||
import at.mocode.ping.domain.Ping
|
||||
import at.mocode.ping.domain.PingRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Application Service.
|
||||
* Implementiert den Use Case und orchestriert Domain & Repository.
|
||||
* Hier darf Spring (@Service, @Transactional) verwendet werden, da es "Application Logic" ist.
|
||||
*/
|
||||
@Service
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingService(
|
||||
private val repository: PingRepository
|
||||
) : PingUseCase {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PingService::class.java)
|
||||
|
||||
@Transactional
|
||||
override fun executePing(message: String): Ping {
|
||||
logger.info("Executing ping with message: {}", message)
|
||||
|
||||
// Domain Logic: Erstelle neue Entity (generiert UUID v7 automatisch)
|
||||
val ping = Ping(message = message)
|
||||
|
||||
// Persistence
|
||||
return repository.save(ping)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun getPingHistory(): List<Ping> {
|
||||
return repository.findAll()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun getPing(id: Uuid): Ping? {
|
||||
return repository.findById(id)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package at.mocode.ping.application
|
||||
|
||||
import at.mocode.ping.domain.Ping
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Primary Port (Inbound Port).
|
||||
* Definiert die fachlichen Operationen, die von außen (Controller) aufgerufen werden können.
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
interface PingUseCase {
|
||||
fun executePing(message: String): Ping
|
||||
fun getPingHistory(): List<Ping>
|
||||
fun getPing(id: Uuid): Ping?
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package at.mocode.ping.domain
|
||||
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Domain Entity für einen Ping.
|
||||
* Unabhängig von Frameworks (Pure Kotlin).
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
data class Ping(
|
||||
val id: Uuid = Uuid.generateV7(), // Kotlin 2.3.0 UUID v7
|
||||
val message: String,
|
||||
val timestamp: Instant = Instant.now()
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package at.mocode.ping.domain
|
||||
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Secondary Port (Outbound Port).
|
||||
* Definiert, wie Pings gespeichert werden, ohne die Technologie (DB) zu kennen.
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
interface PingRepository {
|
||||
fun save(ping: Ping): Ping
|
||||
fun findAll(): List<Ping>
|
||||
fun findById(id: Uuid): Ping?
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package at.mocode.ping.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* JPA Entity (Infrastructure Detail).
|
||||
* Spiegelt die Datenbank-Tabelle wider.
|
||||
* Nutzt java.util.UUID für JPA-Kompatibilität (bis Hibernate kotlin.uuid nativ unterstützt).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "pings")
|
||||
class PingJpaEntity(
|
||||
@Id
|
||||
val id: UUID,
|
||||
val message: String,
|
||||
val timestamp: Instant
|
||||
) {
|
||||
// Default constructor for JPA
|
||||
protected constructor() : this(UUID.randomUUID(), "", Instant.now())
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package at.mocode.ping.infrastructure.persistence
|
||||
|
||||
import at.mocode.ping.domain.Ping
|
||||
import at.mocode.ping.domain.PingRepository
|
||||
import org.springframework.stereotype.Component
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Driven Adapter.
|
||||
* Implementiert den Domain-Port `PingRepository` mithilfe von Spring Data JPA.
|
||||
* Mappt zwischen Domain-Entity und JPA-Entity.
|
||||
*/
|
||||
@Component
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingRepositoryAdapter(
|
||||
private val jpaRepository: SpringDataPingRepository
|
||||
) : PingRepository {
|
||||
|
||||
override fun save(ping: Ping): Ping {
|
||||
val jpaEntity = PingJpaEntity(
|
||||
id = ping.id.toJavaUuid(),
|
||||
message = ping.message,
|
||||
timestamp = ping.timestamp
|
||||
)
|
||||
val saved = jpaRepository.save(jpaEntity)
|
||||
return mapToDomain(saved)
|
||||
}
|
||||
|
||||
override fun findAll(): List<Ping> {
|
||||
return jpaRepository.findAll().map { mapToDomain(it) }
|
||||
}
|
||||
|
||||
override fun findById(id: Uuid): Ping? {
|
||||
return jpaRepository.findById(id.toJavaUuid())
|
||||
.map { mapToDomain(it) }
|
||||
.orElse(null)
|
||||
}
|
||||
|
||||
private fun mapToDomain(entity: PingJpaEntity): Ping {
|
||||
return Ping(
|
||||
id = entity.id.toKotlinUuid(),
|
||||
message = entity.message,
|
||||
timestamp = entity.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package at.mocode.ping.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface SpringDataPingRepository : JpaRepository<PingJpaEntity, UUID>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package at.mocode.ping.infrastructure.web
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.application.PingUseCase
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Driving Adapter (REST Controller).
|
||||
* Nutzt den Application Port (PingUseCase).
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin(allowedHeaders = ["*"], allowCredentials = "true")
|
||||
class PingController(
|
||||
private val pingUseCase: PingUseCase
|
||||
) : PingApi {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PingController::class.java)
|
||||
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||
|
||||
companion object {
|
||||
const val PING_CIRCUIT_BREAKER = "pingCircuitBreaker"
|
||||
}
|
||||
|
||||
@GetMapping("/ping/simple")
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
// Ruft Use Case auf -> Speichert in DB
|
||||
val domainPing = pingUseCase.executePing("Simple Ping")
|
||||
|
||||
return PingResponse(
|
||||
status = "pong",
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = "ping-service"
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/ping/enhanced")
|
||||
@CircuitBreaker(name = PING_CIRCUIT_BREAKER, fallbackMethod = "fallbackPing")
|
||||
override suspend fun enhancedPing(
|
||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||
): EnhancedPingResponse {
|
||||
val start = System.nanoTime()
|
||||
|
||||
if (simulate && Random.nextDouble() < 0.6) {
|
||||
throw RuntimeException("Simulated service failure")
|
||||
}
|
||||
|
||||
// Use Case Aufruf
|
||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
|
||||
return EnhancedPingResponse(
|
||||
status = "pong",
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = "ping-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = elapsedMs
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback muss public sein für Resilience4j Proxy
|
||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
||||
return EnhancedPingResponse(
|
||||
status = "fallback",
|
||||
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
||||
service = "ping-service-fallback",
|
||||
circuitBreakerState = "OPEN",
|
||||
responseTime = 0
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/ping/health")
|
||||
override suspend fun healthCheck(): HealthResponse {
|
||||
return HealthResponse(
|
||||
status = "up",
|
||||
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
||||
service = "ping-service",
|
||||
healthy = true
|
||||
)
|
||||
}
|
||||
|
||||
// Zusätzlicher Endpunkt um die DB zu prüfen (History)
|
||||
@GetMapping("/ping/history")
|
||||
fun getHistory() = pingUseCase.getPingHistory().map {
|
||||
mapOf("id" to it.id.toString(), "message" to it.message, "time" to it.timestamp.toString())
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package at.mocode.ping.service
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@RestController
|
||||
@CrossOrigin(
|
||||
origins = ["http://localhost:8080", "http://localhost:8083", "http://localhost:4000"],
|
||||
methods = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS],
|
||||
allowedHeaders = ["*"],
|
||||
allowCredentials = "true"
|
||||
)
|
||||
class PingController(
|
||||
private val pingService: PingServiceCircuitBreaker
|
||||
) : PingApi {
|
||||
|
||||
// Contract endpoints
|
||||
@GetMapping("/ping/simple")
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
val now = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
return PingResponse(
|
||||
status = "pong",
|
||||
timestamp = now,
|
||||
service = "ping-service"
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/ping/enhanced")
|
||||
override suspend fun enhancedPing(
|
||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||
): EnhancedPingResponse = pingService.ping(simulate)
|
||||
|
||||
@GetMapping("/ping/health")
|
||||
override suspend fun healthCheck(): HealthResponse = pingService.healthCheck()
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
package at.mocode.ping.service
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Service demonstrating a Circuit Breaker pattern with Resilience
|
||||
*
|
||||
* This service simulates potential failures and uses circuit breaker
|
||||
* to handle service degradation gracefully with fallback responses.
|
||||
*/
|
||||
@Service
|
||||
class PingServiceCircuitBreaker {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PingServiceCircuitBreaker::class.java)
|
||||
|
||||
companion object {
|
||||
const val PING_CIRCUIT_BREAKER = "pingCircuitBreaker"
|
||||
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME //.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary ping method with circuit breaker protection returning DTO directly
|
||||
*
|
||||
* @param simulateFailure - if true, randomly throws exceptions to test circuit breaker
|
||||
*/
|
||||
@CircuitBreaker(name = PING_CIRCUIT_BREAKER, fallbackMethod = "fallbackPing")
|
||||
fun ping(simulateFailure: Boolean = false): EnhancedPingResponse {
|
||||
val start = System.nanoTime()
|
||||
logger.info("Executing ping service call...")
|
||||
|
||||
if (simulateFailure && Random.nextDouble() < 0.6) {
|
||||
logger.warn("Simulating service failure for circuit breaker testing")
|
||||
throw RuntimeException("Simulated service failure")
|
||||
}
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(java.time.ZoneOffset.UTC).format(formatter)
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
logger.info("Ping service call successful")
|
||||
|
||||
return EnhancedPingResponse(
|
||||
status = "pong",
|
||||
timestamp = currentTime,
|
||||
service = "ping-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = elapsedMs
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method called when circuit breaker is OPEN
|
||||
*
|
||||
* @param simulateFailure - original parameter (ignored in fallback)
|
||||
* @param exception - the exception that triggered the fallback
|
||||
*/
|
||||
fun fallbackPing(simulateFailure: Boolean = false, exception: Exception): EnhancedPingResponse {
|
||||
val start = System.nanoTime()
|
||||
// Die volle Exception nur loggen, nicht an den Client weitergeben.
|
||||
logger.warn("Circuit breaker fallback triggered due to: {}", exception.toString())
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(java.time.ZoneOffset.UTC).format(formatter)
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
|
||||
return EnhancedPingResponse(
|
||||
status = "fallback",
|
||||
timestamp = currentTime,
|
||||
service = "ping-service-fallback",
|
||||
circuitBreakerState = "OPEN",
|
||||
responseTime = elapsedMs
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check method with circuit breaker protection returning DTO directly
|
||||
*/
|
||||
@CircuitBreaker(name = PING_CIRCUIT_BREAKER, fallbackMethod = "fallbackHealth")
|
||||
fun healthCheck(): HealthResponse {
|
||||
logger.info("Executing health check...")
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(java.time.ZoneOffset.UTC).format(formatter)
|
||||
return HealthResponse(
|
||||
status = "pong",
|
||||
timestamp = currentTime,
|
||||
service = "ping-service",
|
||||
healthy = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for health check returning DTO
|
||||
*/
|
||||
fun fallbackHealth(exception: Exception): HealthResponse {
|
||||
logger.warn("Health check fallback triggered: {}", exception.message)
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(java.time.ZoneOffset.UTC).format(formatter)
|
||||
return HealthResponse(
|
||||
status = "down",
|
||||
timestamp = currentTime,
|
||||
service = "ping-service",
|
||||
healthy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user