feat: Health-Check-Ports und Service-URLs konsolidiert, Consul-Best-Practices umgesetzt

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-16 21:26:54 +02:00
parent dd76ad6d14
commit 26b3b193ca
19 changed files with 130 additions and 43 deletions

View File

@ -1,6 +1,5 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.cloud.gateway.route.builder.filters
@ -9,15 +8,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class GatewayConfig(
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
@Value("\${masterdata.service.url:http://localhost:8086}") private val masterdataServiceUrl: String,
@Value("\${events.service.url:http://localhost:8085}") private val eventsServiceUrl: String,
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String,
@Value("\${results.service.url:http://localhost:8088}") private val resultsServiceUrl: String,
@Value("\${series.service.url:http://localhost:8089}") private val seriesServiceUrl: String,
@Value("\${billing.service.url:http://localhost:8087}") private val billingServiceUrl: String
) {
class GatewayConfig {
@Bean
fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
@ -31,31 +22,31 @@ class GatewayConfig(
it.fallbackUri = java.net.URI.create("forward:/fallback/ping")
}
}
uri(pingServiceUrl)
uri("lb://ping-service")
}
route(id = "masterdata-service") {
path("/api/v1/masterdata/**")
uri(masterdataServiceUrl)
uri("lb://masterdata-service")
}
route(id = "events-service") {
path("/api/v1/events/**")
uri(eventsServiceUrl)
uri("lb://events-service")
}
route(id = "zns-import-service") {
path("/api/v1/import/zns/**", "/api/v1/import/zns")
uri(znsImportServiceUrl)
uri("lb://zns-import-service")
}
route(id = "results-service") {
path("/api/v1/results/**")
uri(resultsServiceUrl)
uri("lb://results-service")
}
route(id = "series-service") {
path("/api/v1/series/**")
uri(seriesServiceUrl)
uri("lb://series-service")
}
route(id = "billing-service") {
path("/api/v1/billing/**")
uri(billingServiceUrl)
uri("lb://billing-service")
}
}
}

View File

@ -25,13 +25,13 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8081
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
gateway:
httpclient: { }
# Routen sind in GatewayConfig.kt definiert
# Routen sind in GatewayConfig.kt via Service-Discovery (lb://) definiert
# --- SECURITY (OAuth2 Resource Server) ---
security:
@ -66,9 +66,3 @@ management:
# Lokal: Zipkin auf Port 9411. In Docker via ENV MANAGEMENT_ZIPKIN_TRACING_ENDPOINT überschrieben.
endpoint: ${MANAGEMENT_ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
# --- Custom Service URLs ---
# Default: Localhost (für Entwicklung ohne Docker)
# Im Docker-Compose überschreiben wir das mit dem Service-Namen
ping:
service:
url: ${PING_SERVICE_URL:http://localhost:8082}

View File

@ -15,12 +15,12 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8089
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
server:
port: ${SERVER_PORT:${BILLING_SERVICE_PORT:8087}}
port: 8089
management:
endpoints:

View File

@ -21,7 +21,7 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8083
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}

View File

@ -22,7 +22,7 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8085
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}

View File

@ -1,5 +1,5 @@
server:
port: ${SERVER_PORT:${IDENTITY_SERVICE_PORT:8088}}
port: 8087 # identity-service port
spring:
application:
@ -18,7 +18,7 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8087
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
security:

View File

@ -32,12 +32,12 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8092
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
server:
port: 8085
port: 8092
management:
endpoints:

View File

@ -22,10 +22,10 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port} # Tomcat Port für Health Checks
health-check-port: 8086 # Spring Boot Management Port (Actuator)
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
port: ${masterdata.http.port} # Ktor API Port registrieren
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)
server:
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)

View File

@ -50,7 +50,7 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8082
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}

View File

@ -22,12 +22,12 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8088
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
server:
port: ${SERVER_PORT:${RESULTS_SERVICE_PORT:8084}}
port: 8088
management:
endpoints:

View File

@ -16,12 +16,12 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8094
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
server:
port: 8089
port: 8094
management:
endpoints:

View File

@ -22,12 +22,12 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: ${server.port}
health-check-port: 8093
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
server:
port: 8090
port: 8093
management:
endpoints:

View File

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinSerialization)
}
springBoot {

View File

@ -4,11 +4,13 @@ import at.mocode.zns.import.service.job.ImportJob
import at.mocode.zns.import.service.job.ImportJobRegistry
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
import at.mocode.zns.importer.ZnsImportMode
import kotlinx.serialization.Serializable
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
@Serializable
data class ImportStartResponse(val jobId: String)
@RestController

View File

@ -1,12 +1,15 @@
package at.mocode.zns.import.service.job
import kotlinx.serialization.Serializable
import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentHashMap
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Serializable
enum class ImportJobStatus { AUSSTEHEND, ENTPACKEN, VERARBEITUNG, ABGESCHLOSSEN, FEHLER }
@Serializable
data class ImportJob(
val jobId: String,
var status: ImportJobStatus = ImportJobStatus.AUSSTEHEND,

View File

@ -31,7 +31,7 @@ spring:
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 15s
health-check-port: ${server.port}
health-check-port: 8095
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
management:

View File

@ -0,0 +1,45 @@
# 📓 Journal-Eintrag: 2026-04-16 - ZNS-Import Serialization Fix
## 🏗️ Status Quo
Nach dem Deployment der ZNS-First Strategie trat beim Testen der Desktop-App ein Serialisierungsfehler auf:
`Serializer for class 'ImportStartResponse' is not found`.
## 🚀 Analyse & Behebung
### 1. Das Problem
Die Kommunikation zwischen der Desktop-App (KMP/Compose) und dem `zns-import-service` (Spring Boot) nutzt
`kotlinx.serialization` auf der Client-Seite.
- Die DTO-Klassen im Backend (`ImportStartResponse`, `ImportJob`) waren nicht mit `@Serializable` annotiert.
- Das Gradle-Plugin `kotlin.plugin.serialization` fehlte in den relevanten Modulen (`zns-import-service` und
`zns-import-feature`).
- Dies führte dazu, dass der Ktor-Client im Frontend die JSON-Antwort des Backends nicht dekodieren konnte.
### 2. Die Lösung
- **Backend**: `@Serializable` zu `ImportStartResponse`, `ImportJob` und `ImportJobStatus` hinzugefügt.
- **Gradle**: Das `kotlinSerialization` Plugin in `backend/services/zns-import/zns-import-service/build.gradle.kts` und
`frontend/features/zns-import-feature/build.gradle.kts` aktiviert.
- **Frontend**: Konsistenzprüfung der `ImportStartResponse` und `JobStatusResponse` im `ZnsImportViewModel`.
## 🛠️ Technische Details
- **Module**:
- `backend:services:zns-import:zns-import-service`
- `frontend:features:zns-import-feature`
- **Änderungen**:
- `ImportJobRegistry.kt`: `ImportJob` & `ImportJobStatus` sind jetzt `@Serializable`.
- `ZnsImportController.kt`: `ImportStartResponse` ist jetzt `@Serializable`.
- `build.gradle.kts`: Plugins ergänzt.
## 🏁 Fazit
Der ZNS-Import-Workflow (Upload -> JobId -> Polling) ist nun technisch stabil und die Typen sind über die Systemgrenzen
hinweg serialisierbar.
---
**🧹 [Curator]**: Dokumentation des Serialization-Fixes abgeschlossen.
**👷 [Backend Developer]**: DTOs für API-Kommunikation stabilisiert.
**🏗️ [Lead Architect]**: Konsistente Nutzung von Kotlinx-Serialization in KMP-Context sichergestellt.

View File

@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
}
group = "at.mocode.clients"
version = "1.0.0"

View File

@ -0,0 +1,50 @@
# Journal-Eintrag: Consul Discovery Best Practice Fix
**Datum:** 16. April 2026
**Beteiligte:** 🏗️ [Lead Architect], 👷 [Backend Developer]
## 1. Problemstellung
Trotz vorheriger Versuche meldete Consul für den `masterdata-service` weiterhin den Status "critical" (404 auf
`/actuator/health`).
Die Ursache lag in der hybriden Architektur (Spring Boot + Ktor):
- Spring Boot (Management/Actuator) lief auf Port 8086.
- Ktor (API) lief auf Port 8091.
- Consul versuchte fälschlicherweise, den Healthcheck auf dem API-Port (oder einer fehlerhaften Kombination)
auszuführen, da die Service-Registrierung nicht präzise genug war.
## 2. Best Practice Lösung
Um die Stabilität und Wartbarkeit zu erhöhen, wurden folgende Maßnahmen ergriffen:
### A. Präzise Service-Registrierung (Masterdata-Service)
In der `application.yml` wurde die Consul-Discovery-Konfiguration geschärft:
- `health-check-port: 8086`: Der Healthcheck wird explizit an den Spring Boot Management Port gesendet.
- `port: ${masterdata.http.port:8091}`: Der Service wird explizit mit seinem API-Port (Ktor) im Service-Katalog
registriert.
- Damit weiß das Gateway: "Sende Traffic an 8091", und Consul weiß: "Prüfe Health auf 8086".
### B. Umstellung auf Load-Balancer Abstraktion (`lb://`)
Das API-Gateway nutzte bisher hartcodierte URLs oder Umgebungsvariablen für das Routing. Dies ist fehleranfällig und
widerspricht dem Prinzip der Service Discovery.
- **Änderung:** Alle Routen in `GatewayConfig.kt` wurden von `http://service-name:port` auf `lb://service-name`
umgestellt.
- **Vorteil:** Spring Cloud Gateway nutzt nun den Consul-Katalog direkt. Wenn ein Service (wie `masterdata-service`)
dort mit Port 8091 registriert ist, wird er automatisch korrekt angesteuert.
- Dies entfernt die Notwendigkeit für `*_SERVICE_URL` Umgebungsvariablen im Gateway.
## 3. Ergebnis
- Der `masterdata-service` wird nun korrekt als `healthy` markiert, da der Healthcheck den richtigen Port (8086)
erreicht.
- Das Routing ist nun dynamisch und robust gegenüber Port-Änderungen in den einzelnen Services.
- Alle Backend-Services folgen nun einem einheitlichen "Best Practice" Muster für die Service Discovery.
---
**🏗️ [Lead Architect]**: Architektur auf Standard Spring Cloud Discovery (lb://) gehärtet.
**👷 [Backend Developer]**: Hybride Port-Konfiguration im Masterdata-Service final korrigiert.