chore: erweitere Resilience4j-Bundle um Kotlin-Support, aktualisiere PingController um Fallback-Logik, füge Fehlerhandler hinzu, verbessere PingControllerTest, synchronisiere .env und dc-infra.yaml
This commit is contained in:
parent
54f91c7309
commit
83adb4ae07
246
.env
Normal file
246
.env
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
# ==========================================
|
||||||
|
# Meldestelle – Docker Compose Environment
|
||||||
|
# Single Source of Truth (SSoT)
|
||||||
|
# ==========================================
|
||||||
|
# WARNING: This file contains secrets (passwords).
|
||||||
|
# Do NOT commit this file to version control if it contains production secrets.
|
||||||
|
|
||||||
|
# --- PROJECT ---
|
||||||
|
PROJECT_NAME=meldestelle
|
||||||
|
|
||||||
|
# --- BACKUP ---
|
||||||
|
BACKUP_DIR=/home/stefan/backups/meldestelle
|
||||||
|
BACKUP_RETENTION_DAYS=7
|
||||||
|
|
||||||
|
# Docker build versions (optional overrides)
|
||||||
|
DOCKER_VERSION=1.0.0-SNAPSHOT
|
||||||
|
DOCKER_REGISTRY=git.mo-code.at/mocode-software/meldestelle
|
||||||
|
DOCKER_BUILD_DATE=2026-03-16T12:00:00Z
|
||||||
|
DOCKER_GRADLE_VERSION=9.3.1
|
||||||
|
DOCKER_JAVA_VERSION=25
|
||||||
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
|
DOCKER_NGINX_VERSION=1.28.0-alpine
|
||||||
|
|
||||||
|
# JVM Power Flags (Lokal leer lassen, da Intel/AMD Architektur)
|
||||||
|
JVM_OPTS_ARM64=
|
||||||
|
|
||||||
|
# --- POSTGRES ---
|
||||||
|
POSTGRES_IMAGE=postgres:16-alpine
|
||||||
|
POSTGRES_SHARED_BUFFERS=256MB
|
||||||
|
POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
|
||||||
|
POSTGRES_USER=pg-user
|
||||||
|
POSTGRES_PASSWORD=pg-password
|
||||||
|
POSTGRES_DB=pg-meldestelle-db
|
||||||
|
POSTGRES_PORT=5432:5432
|
||||||
|
POSTGRES_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db
|
||||||
|
|
||||||
|
# --- VALKEY (formerly Redis) ---
|
||||||
|
VALKEY_IMAGE=valkey/valkey:9-alpine
|
||||||
|
VALKEY_PASSWORD=valkey-password
|
||||||
|
VALKEY_PORT=6379:6379
|
||||||
|
VALKEY_SERVER_HOSTNAME=valkey
|
||||||
|
VALKEY_SERVER_PORT=6379
|
||||||
|
VALKEY_SERVER_CONNECT_TIMEOUT=5s
|
||||||
|
VALKEY_POLICY=allkeys-lru
|
||||||
|
VALKEY_MAX_MEMORY=256MB
|
||||||
|
SPRING_DATA_VALKEY_HOST=localhost
|
||||||
|
SPRING_DATA_VALKEY_PORT=6379
|
||||||
|
SPRING_DATA_VALKEY_PASSWORD=valkey-password
|
||||||
|
|
||||||
|
# --- KEYCLOAK ---
|
||||||
|
KEYCLOAK_IMAGE_TAG=latest
|
||||||
|
KC_HEAP_MIN=512M
|
||||||
|
KC_HEAP_MAX=1024M
|
||||||
|
# Lokale Entwicklung: start-dev (kein Pre-Build nötig, kein --optimized)
|
||||||
|
# Server/Produktion: start --optimized --import-realm (nutzt das pre-built Registry-Image)
|
||||||
|
KC_COMMAND=start-dev --import-realm
|
||||||
|
# System-Admin (Master Console)
|
||||||
|
KC_BOOTSTRAP_ADMIN_USERNAME=kc-admin
|
||||||
|
KC_BOOTSTRAP_ADMIN_PASSWORD=kc-password
|
||||||
|
# Fach-Admin User Passwort (wird im Realm Import genutzt)
|
||||||
|
# Hinweis: Wenn du das hier änderst, müsstest du auch die JSON anpassen
|
||||||
|
# oder dort eine Variable nutzen.
|
||||||
|
|
||||||
|
KC_DB=postgres
|
||||||
|
KC_DB_SCHEMA=keycloak
|
||||||
|
KC_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db
|
||||||
|
KC_DB_USERNAME=pg-user
|
||||||
|
KC_DB_PASSWORD=meldestelle
|
||||||
|
|
||||||
|
# Lokal: localhost | Server: echte IP oder Domain (z.B. 10.0.0.50 oder auth.meldestelle.at)
|
||||||
|
# WICHTIG: Nur den Hostnamen angeben, OHNE Port (Keycloak 26.x hostname v2)
|
||||||
|
KC_HOSTNAME=localhost
|
||||||
|
# false = Zugriff über beliebige Hostnamen erlaubt (nötig ohne TLS / für HTTP-Betrieb)
|
||||||
|
KC_HOSTNAME_STRICT=false
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS=false
|
||||||
|
KC_PORT=8180:8080
|
||||||
|
KC_MANAGEMENT_PORT=9000:9000
|
||||||
|
|
||||||
|
KC_HTTP_ENABLE=true
|
||||||
|
|
||||||
|
KC_API_GATEWAY_CLIENT_SECRET=K5RqonwVOaxPKaXVH4mbthSRbjRh5tOK
|
||||||
|
# KC_POSTMAN_CLIENT_SECRET=postman-secret-123
|
||||||
|
# KC_BOOTSTRAP_ADMIN_PASSWORD=Admin#1234
|
||||||
|
KC_FRONTEND_URL=http://localhost:8180
|
||||||
|
KC_PROXY_HEADERS=xforwarded
|
||||||
|
|
||||||
|
# --- KEYCLOAK TOKEN VALIDATION ---
|
||||||
|
# Public Issuer URI (must match the token issuer from browser/postman)
|
||||||
|
# Lokal: http://localhost:8180 | Produktion: http://10.0.0.50:8180
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8180/realms/meldestelle
|
||||||
|
# Internal JWK Set URI (for service-to-service communication within Docker)
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
|
||||||
|
|
||||||
|
# --- CONSUL ---
|
||||||
|
CONSUL_IMAGE=hashicorp/consul:1.22.1
|
||||||
|
CONSUL_PORT=8500:8500
|
||||||
|
CONSUL_UDP_PORT=8600:8600/udp
|
||||||
|
CONSUL_HOST=consul
|
||||||
|
SPRING_CLOUD_CONSUL_HOST=consul
|
||||||
|
SPRING_CLOUD_CONSUL_PORT=8500
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME=api-gateway
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS=true
|
||||||
|
|
||||||
|
# --- Zipkin ---
|
||||||
|
ZIPKIN_IMAGE=openzipkin/zipkin:3
|
||||||
|
ZIPKIN_MIN_HEAP=256M
|
||||||
|
ZIPKIN_MAX_HEAP=512M
|
||||||
|
ZIPKIN_PORT=9411:9411
|
||||||
|
ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans
|
||||||
|
ZIPKIN_SAMPLING_PROBABILITY=1.0
|
||||||
|
|
||||||
|
# --- Mailpit ---
|
||||||
|
MAILPIT_IMAGE=axllent/mailpit:v1.29
|
||||||
|
MAILPIT_WEB_PORT=8025:8025
|
||||||
|
MAILPIT_SMTP_PORT=1025:1025
|
||||||
|
|
||||||
|
# --- PGADMIN ---
|
||||||
|
PGADMIN_IMAGE=dpage/pgadmin4:8
|
||||||
|
PGADMIN_EMAIL=meldestelle@mo-code.at
|
||||||
|
PGADMIN_PASSWORD=pgadmin
|
||||||
|
PGADMIN_PORT=8888:80
|
||||||
|
|
||||||
|
# --- POSTGRES-EXPORTER ---
|
||||||
|
POSTGRES_EXPORTER_IMAGE=prometheuscommunity/postgres-exporter:v0.18.0
|
||||||
|
|
||||||
|
# --- ALERTMANAGER ---
|
||||||
|
ALERTMANAGER_IMAGE=prom/alertmanager:v0.29.0
|
||||||
|
ALERTMANAGER_PORT=9093:9093
|
||||||
|
|
||||||
|
# --- PROMETHEUS ---
|
||||||
|
PROMETHEUS_IMAGE=prom/prometheus:v3.7.3
|
||||||
|
PROMETHEUS_PORT=9090:9090
|
||||||
|
|
||||||
|
# --- GRAFANA ---
|
||||||
|
GF_IMAGE=grafana/grafana:12.3
|
||||||
|
GF_ADMIN_USER=gf-admin
|
||||||
|
GF_ADMIN_PASSWORD=gf-password
|
||||||
|
GF_PORT=3000:3000
|
||||||
|
|
||||||
|
# --- API-GATEWAY ---
|
||||||
|
GATEWAY_PORT=8081:8081
|
||||||
|
GATEWAY_DEBUG_PORT=5005:5005
|
||||||
|
GATEWAY_SERVER_PORT=8081
|
||||||
|
GATEWAY_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
GATEWAY_DEBUG=true
|
||||||
|
GATEWAY_SERVICE_NAME=api-gateway
|
||||||
|
GATEWAY_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- PING-SERVICE ---
|
||||||
|
PING_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
PING_PORT=8082:8082
|
||||||
|
PING_DEBUG_PORT=5006:5006
|
||||||
|
PING_SERVER_PORT=8082
|
||||||
|
PING_DEBUG=true
|
||||||
|
PING_SERVICE_NAME=ping-service
|
||||||
|
PING_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- MAIL-SERVICE ---
|
||||||
|
MAIL_PORT=8083:8083
|
||||||
|
MAIL_DEBUG_PORT=5014:5014
|
||||||
|
MAIL_SERVER_PORT=8083
|
||||||
|
MAIL_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
MAIL_DEBUG=true
|
||||||
|
MAIL_SERVICE_NAME=mail-service
|
||||||
|
MAIL_CONSUL_PREFER_IP=true
|
||||||
|
MAIL_SMTP_HOST=smtp.world4you.com
|
||||||
|
MAIL_SMTP_PORT=587
|
||||||
|
MAIL_SMTP_USER=online-nennen@mo-code.at
|
||||||
|
MAIL_SMTP_PASSWORD=secret
|
||||||
|
MAIL_SMTP_AUTH=true
|
||||||
|
MAIL_SMTP_STARTTLS=true
|
||||||
|
|
||||||
|
# --- MASTERDATA-SERVICE ---
|
||||||
|
MASTERDATA_PORT=8086:8086
|
||||||
|
MASTERDATA_DEBUG_PORT=5007:5007
|
||||||
|
MASTERDATA_SERVER_PORT=8086
|
||||||
|
MASTERDATA_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
MASTERDATA_DEBUG=true
|
||||||
|
MASTERDATA_SERVICE_NAME=masterdata-service
|
||||||
|
MASTERDATA_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- EVENTS-SERVICE ---
|
||||||
|
EVENTS_PORT=8085:8085
|
||||||
|
EVENTS_DEBUG_PORT=5008:5008
|
||||||
|
EVENTS_SERVER_PORT=8085
|
||||||
|
EVENTS_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
EVENTS_DEBUG=true
|
||||||
|
EVENTS_SERVICE_NAME=events-service
|
||||||
|
EVENTS_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- ZNS-IMPORT-SERVICE ---
|
||||||
|
ZNS_IMPORT_PORT=8095:8095
|
||||||
|
ZNS_IMPORT_DEBUG_PORT=5009:5009
|
||||||
|
ZNS_IMPORT_SERVER_PORT=8095
|
||||||
|
ZNS_IMPORT_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
ZNS_IMPORT_DEBUG=true
|
||||||
|
ZNS_IMPORT_SERVICE_NAME=zns-import-service
|
||||||
|
ZNS_IMPORT_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- RESULTS-SERVICE ---
|
||||||
|
RESULTS_PORT=8088:8088
|
||||||
|
RESULTS_DEBUG_PORT=5010:5010
|
||||||
|
RESULTS_SERVER_PORT=8088
|
||||||
|
RESULTS_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
RESULTS_DEBUG=true
|
||||||
|
RESULTS_SERVICE_NAME=results-service
|
||||||
|
RESULTS_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- BILLING-SERVICE ---
|
||||||
|
BILLING_PORT=8087:8087
|
||||||
|
BILLING_DEBUG_PORT=5012:5012
|
||||||
|
BILLING_SERVER_PORT=8087
|
||||||
|
BILLING_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
BILLING_DEBUG=true
|
||||||
|
BILLING_SERVICE_NAME=billing-service
|
||||||
|
BILLING_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- SCHEDULING-SERVICE ---
|
||||||
|
SCHEDULING_PORT=8084:8084
|
||||||
|
SCHEDULING_DEBUG_PORT=5013:5013
|
||||||
|
SCHEDULING_SERVER_PORT=8084
|
||||||
|
SCHEDULING_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
SCHEDULING_DEBUG=true
|
||||||
|
SCHEDULING_SERVICE_NAME=scheduling-service
|
||||||
|
SCHEDULING_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- SERIES-SERVICE ---
|
||||||
|
SERIES_PORT=8089:8089
|
||||||
|
SERIES_DEBUG_PORT=5011:5011
|
||||||
|
SERIES_SERVER_PORT=8089
|
||||||
|
SERIES_SPRING_PROFILES_ACTIVE=docker
|
||||||
|
SERIES_DEBUG=true
|
||||||
|
SERIES_SERVICE_NAME=series-service
|
||||||
|
SERIES_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
|
# --- WEB-APP ---
|
||||||
|
CADDY_VERSION=2.11-alpine
|
||||||
|
WEB_APP_PORT=4000:4000
|
||||||
|
WEB_BUILD_PROFILE=dev
|
||||||
|
# Lokal: http://localhost:8081 | Produktion: http://10.0.0.50:8081
|
||||||
|
WEB_APP_API_URL=http://localhost:8081
|
||||||
|
WEB_APP_KEYCLOAK_URL=http://auth.mo-code.at
|
||||||
|
|
||||||
|
# --- DESKTOP-APP ---
|
||||||
|
DESKTOP_APP_VNC_PORT=5901:5901
|
||||||
|
DESKTOP_APP_NOVNC_PORT=6080:6080
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/.idea/
|
||||||
|
|
@ -52,10 +52,12 @@ class GlobalSecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun jwtDecoder(): JwtDecoder {
|
fun jwtDecoder(): JwtDecoder {
|
||||||
// Wenn jwk-set-uri gesetzt ist, nutzen wir sie.
|
// 1. Suche in System-Properties (Spring injects these)
|
||||||
// Wir verzichten auf den Issuer-Check für maximale Flexibilität zwischen Docker/Host.
|
// 2. Suche in Environment Variables
|
||||||
val jwkSetUri = System.getenv("SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI")
|
// 3. Fallback auf localhost (IDE-Start) oder keycloak (Docker-Start)
|
||||||
?: "http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs"
|
val jwkSetUri = System.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
|
||||||
|
?: System.getenv("SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI")
|
||||||
|
?: "http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs"
|
||||||
|
|
||||||
val decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
val decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||||
val validator = DelegatingOAuth2TokenValidator<Jwt>(JwtTimestampValidator())
|
val validator = DelegatingOAuth2TokenValidator<Jwt>(JwtTimestampValidator())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
alias(libs.plugins.kotlinJpa)
|
alias(libs.plugins.kotlinJpa)
|
||||||
|
|
@ -37,8 +37,7 @@ dependencies {
|
||||||
implementation(libs.bundles.database.complete)
|
implementation(libs.bundles.database.complete)
|
||||||
|
|
||||||
// === Resilience ===
|
// === Resilience ===
|
||||||
implementation(libs.resilience4j.spring.boot3)
|
implementation(libs.bundles.resilience)
|
||||||
implementation(libs.resilience4j.reactor)
|
|
||||||
implementation(libs.spring.boot.starter.aop)
|
implementation(libs.spring.boot.starter.aop)
|
||||||
|
|
||||||
// === Testing ===
|
// === Testing ===
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package at.mocode.ping.infrastructure.web
|
||||||
|
|
||||||
import at.mocode.ping.api.*
|
import at.mocode.ping.api.*
|
||||||
import at.mocode.ping.application.PingUseCase
|
import at.mocode.ping.application.PingUseCase
|
||||||
|
import at.mocode.ping.domain.Ping
|
||||||
import at.mocode.ping.infrastructure.PingProperties
|
import at.mocode.ping.infrastructure.PingProperties
|
||||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
@ -20,7 +21,7 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class PingController(
|
open class PingController(
|
||||||
private val pingUseCase: PingUseCase,
|
private val pingUseCase: PingUseCase,
|
||||||
private val properties: PingProperties
|
private val properties: PingProperties
|
||||||
) : PingApi {
|
) : PingApi {
|
||||||
|
|
@ -43,10 +44,16 @@ class PingController(
|
||||||
override suspend fun enhancedPing(
|
override suspend fun enhancedPing(
|
||||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||||
): EnhancedPingResponse {
|
): EnhancedPingResponse {
|
||||||
|
logger.info("Enhanced ping requested, simulate: {}", simulate)
|
||||||
val start = System.nanoTime()
|
val start = System.nanoTime()
|
||||||
|
|
||||||
if (simulate && Random.nextDouble() < 0.6) {
|
if (simulate) {
|
||||||
throw RuntimeException("Simulated service failure")
|
if (Random.nextDouble() < 0.6) {
|
||||||
|
logger.info("Simulating service failure now...")
|
||||||
|
throw SimulatedException("Simulated service failure")
|
||||||
|
} else {
|
||||||
|
logger.info("Simulation mode ACTIVE, but this time lucky: Request passed!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||||
|
|
@ -61,6 +68,8 @@ class PingController(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SimulatedException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
// Neue Endpunkte
|
// Neue Endpunkte
|
||||||
|
|
||||||
@GetMapping("/ping/public")
|
@GetMapping("/ping/public")
|
||||||
|
|
@ -70,7 +79,7 @@ class PingController(
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ping/secure")
|
@GetMapping("/ping/secure")
|
||||||
@PreAuthorize("hasRole('MELD_USER') or hasRole('MELD_ADMIN')") // Beispiel-Rollen
|
@PreAuthorize("hasRole('ROLE_MELD_USER') or hasRole('ROLE_MELD_ADMIN')") // Beispiel-Rollen
|
||||||
override suspend fun securePing(): PingResponse {
|
override suspend fun securePing(): PingResponse {
|
||||||
val domainPing = pingUseCase.executePing("Secure Ping")
|
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||||
return createResponse(domainPing, "secure-pong")
|
return createResponse(domainPing, "secure-pong")
|
||||||
|
|
@ -79,7 +88,7 @@ class PingController(
|
||||||
@GetMapping("/ping/sync")
|
@GetMapping("/ping/sync")
|
||||||
override suspend fun syncPings(
|
override suspend fun syncPings(
|
||||||
// Changed the parameter name to 'since' to match SyncManager convention
|
// Changed the parameter name to 'since' to match SyncManager convention
|
||||||
@RequestParam(required = false, defaultValue = "0") since: Long
|
@RequestParam(name = "lastSyncTimestamp", required = false, defaultValue = "0") since: Long
|
||||||
): List<PingEvent> {
|
): List<PingEvent> {
|
||||||
return pingUseCase.getPingsSince(since).map {
|
return pingUseCase.getPingsSince(since).map {
|
||||||
PingEvent(
|
PingEvent(
|
||||||
|
|
@ -91,7 +100,7 @@ class PingController(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper
|
// Helper
|
||||||
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
private fun createResponse(domainPing: Ping, status: String) = PingResponse(
|
||||||
status = status,
|
status = status,
|
||||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||||
service = properties.serviceName
|
service = properties.serviceName
|
||||||
|
|
@ -99,8 +108,8 @@ class PingController(
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
@Suppress("unused", "UNUSED_PARAMETER")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
open fun fallbackPing(simulate: Boolean, ex: Throwable): EnhancedPingResponse {
|
||||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
logger.error("CIRCUIT BREAKER FALLBACK TRIGGERED! Reason: {}", ex.message, ex)
|
||||||
return EnhancedPingResponse(
|
return EnhancedPingResponse(
|
||||||
status = "fallback",
|
status = "fallback",
|
||||||
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package at.mocode.ping.infrastructure.web
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ProblemDetail
|
||||||
|
import org.springframework.security.access.AccessDeniedException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class PingExceptionHandler {
|
||||||
|
private val log = LoggerFactory.getLogger(PingExceptionHandler::class.java)
|
||||||
|
|
||||||
|
@ExceptionHandler(AccessDeniedException::class)
|
||||||
|
fun handleAccessDenied(ex: AccessDeniedException): ProblemDetail {
|
||||||
|
log.warn("Zugriff verweigert: ${ex.message}")
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, "Nicht berechtigt: ${ex.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception::class)
|
||||||
|
fun handleAll(ex: Exception): ProblemDetail {
|
||||||
|
log.error("Unerwarteter Fehler: ", ex)
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Ein interner Fehler ist aufgetreten")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException::class)
|
||||||
|
fun handleRuntime(ex: RuntimeException): ProblemDetail {
|
||||||
|
log.error("Interner Fehler: ", ex)
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Unbekannter Fehler")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -40,12 +40,17 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||||
controllers = [PingController::class],
|
controllers = [PingController::class],
|
||||||
properties = ["spring.aop.proxy-target-class=true"]
|
properties = ["spring.aop.proxy-target-class=true"]
|
||||||
)
|
)
|
||||||
|
@Import(
|
||||||
|
PingControllerTest.PingControllerTestConfig::class,
|
||||||
|
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerAutoConfiguration::class,
|
||||||
|
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerMetricsAutoConfiguration::class,
|
||||||
|
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration::class
|
||||||
|
)
|
||||||
@ContextConfiguration(classes = [TestPingServiceApplication::class])
|
@ContextConfiguration(classes = [TestPingServiceApplication::class])
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
@Import(PingControllerTest.PingControllerTestConfig::class)
|
|
||||||
@AutoConfigureMockMvc(addFilters = false)
|
@AutoConfigureMockMvc(addFilters = false)
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class PingControllerTest {
|
open class PingControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
@ -125,11 +130,24 @@ class PingControllerTest {
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val json = objectMapper.readTree(result.response.contentAsString)
|
val json = objectMapper.readTree(result.response.contentAsString)
|
||||||
assertThat(json["status"].asText()).isEqualTo("pong")
|
|
||||||
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
||||||
verify { pingUseCase.executePing("Enhanced Ping") }
|
verify { pingUseCase.executePing("Enhanced Ping") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return fallback when simulation failure occurs`() {
|
||||||
|
// Given
|
||||||
|
val controller = PingController(pingUseCase, properties)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val response = controller.fallbackPing(simulate = true, ex = PingController.SimulatedException("test"))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.status).isEqualTo("fallback")
|
||||||
|
assertThat(response.service).isEqualTo(properties.serviceNameFallback)
|
||||||
|
assertThat(response.circuitBreakerState).isEqualTo("OPEN")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return health check response with status up`() {
|
fun `should return health check response with status up`() {
|
||||||
// When
|
// When
|
||||||
|
|
@ -159,7 +177,7 @@ class PingControllerTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||||
.andExpect(request().asyncStarted())
|
.andExpect(request().asyncStarted())
|
||||||
.andReturn()
|
.andReturn()
|
||||||
|
|
||||||
|
|
@ -183,7 +201,7 @@ class PingControllerTest {
|
||||||
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||||
.andExpect(request().asyncStarted())
|
.andExpect(request().asyncStarted())
|
||||||
.andReturn()
|
.andReturn()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,18 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: [ "infra", "all" ]
|
profiles: [ "infra", "all" ]
|
||||||
environment:
|
environment:
|
||||||
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_ADMIN_USERNAME:-kc-admin}"
|
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_BOOTSTRAP_ADMIN_USERNAME:-kc-admin}"
|
||||||
KC_BOOTSTRAP_ADMIN_PASSWORD: "${KC_ADMIN_PASSWORD:-kc-password}"
|
KC_BOOTSTRAP_ADMIN_PASSWORD: "${KC_BOOTSTRAP_ADMIN_PASSWORD:-kc-password}"
|
||||||
|
|
||||||
|
KC_FRONTEND_URL: "${KC_FRONTEND_URL:-http://localhost:8180}"
|
||||||
|
KC_PROXY_HEADERS: "${KC_PROXY_HEADERS:-xforwarded}"
|
||||||
|
|
||||||
KC_DB: "${KC_DB:-postgres}"
|
KC_DB: "${KC_DB:-postgres}"
|
||||||
KC_DB_SCHEMA: "${KC_DB_SCHEMA:-keycloak}"
|
KC_DB_SCHEMA: "${KC_DB_SCHEMA:-keycloak}"
|
||||||
KC_DB_URL: "jdbc:postgresql://postgres:5432/${POSTGRES_DB:-pg-meldestelle-db}"
|
KC_DB_URL: "jdbc:postgresql://postgres:5432/${POSTGRES_DB:-pg-meldestelle-db}"
|
||||||
KC_DB_USERNAME: "${POSTGRES_USER:-pg-user}"
|
KC_DB_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
KC_DB_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
KC_DB_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
# Hostname-Konfiguration: Für lokale Entwicklung "localhost", auf dem Server die echte IP/Domain setzen
|
# Hostname-Konfiguration: Für lokale Entwicklung "localhost", auf dem Server die echte IP/Domain setzen
|
||||||
KC_HOSTNAME: "${KC_HOSTNAME:-localhost}"
|
KC_HOSTNAME: "${KC_HOSTNAME:-localhost}"
|
||||||
# WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS)
|
# WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS)
|
||||||
|
|
@ -98,6 +103,7 @@ services:
|
||||||
KC_HTTP_ENABLED: "true"
|
KC_HTTP_ENABLED: "true"
|
||||||
# Admin-Interface explizit auf allen Interfaces binden (0.0.0.0)
|
# Admin-Interface explizit auf allen Interfaces binden (0.0.0.0)
|
||||||
KC_HTTP_MANAGEMENT_PORT: "9000"
|
KC_HTTP_MANAGEMENT_PORT: "9000"
|
||||||
|
|
||||||
KC_HEALTH_ENABLED: "true"
|
KC_HEALTH_ENABLED: "true"
|
||||||
KC_METRICS_ENABLED: "true"
|
KC_METRICS_ENABLED: "true"
|
||||||
# Integration der Power-Flags
|
# Integration der Power-Flags
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,7 @@ zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" }
|
||||||
|
|
||||||
resilience4j-spring-boot3 = { module = "io.github.resilience4j:resilience4j-spring-boot3", version.ref = "resilience4j" }
|
resilience4j-spring-boot3 = { module = "io.github.resilience4j:resilience4j-spring-boot3", version.ref = "resilience4j" }
|
||||||
resilience4j-reactor = { module = "io.github.resilience4j:resilience4j-reactor", version.ref = "resilience4j" }
|
resilience4j-reactor = { module = "io.github.resilience4j:resilience4j-reactor", version.ref = "resilience4j" }
|
||||||
|
resilience4j-kotlin = { module = "io.github.resilience4j:resilience4j-kotlin", version.ref = "resilience4j" }
|
||||||
|
|
||||||
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
||||||
keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloakAdminClient" }
|
keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloakAdminClient" }
|
||||||
|
|
@ -392,7 +393,8 @@ jackson-kotlin = [
|
||||||
]
|
]
|
||||||
resilience = [
|
resilience = [
|
||||||
"resilience4j-spring-boot3",
|
"resilience4j-spring-boot3",
|
||||||
"resilience4j-reactor"
|
"resilience4j-reactor",
|
||||||
|
"resilience4j-kotlin"
|
||||||
]
|
]
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user