refactor(infra-monitoring)

refactor(infra-gateway)
This commit is contained in:
stefan
2025-08-11 14:32:01 +02:00
parent d87a5a4a93
commit 582678e226
16 changed files with 282 additions and 115 deletions
+23
View File
@@ -0,0 +1,23 @@
Zusammengefasst ergibt sich daraus folgender, konkreter Fahrplan:
1. **Schritt 0: Aufräumen (ca. 1-2 Stunden)**
* [ ] Entfernen Sie den auskommentierten Ktor-Code aus der `infrastructure:gateway:build.gradle.kts`.
* [ ] Refaktorieren Sie die Test-Route in `GatewayApplicationTests.kt` auf die Kotlin DSL von Spring Cloud Gateway.
* [ ] **(Optional)** Führen Sie `value class`es für stark typisierte IDs oder Konfigurationsparameter im `core`-Modul ein.
2. **Schritt 1: Phase 2 - Den "Ping-Service" bauen**
* [ ] Erstellen Sie ein neues Gradle-Modul `:temp:ping-service`.
* [ ] Implementieren Sie eine simple Spring Boot Anwendung darin.
* [ ] Fügen Sie die Abhängigkeiten zu `spring-boot-starter-web`, `spring-cloud-starter-consul-discovery` und Ihrem `platform:platform-dependencies` hinzu.
* [ ] Erstellen Sie einen `RestController` mit einem `GET /ping` Endpunkt, der `mapOf("status" to "pong")` zurückgibt.
* [ ] Konfigurieren Sie die `application.yml` des Services, damit er sich bei Consul registriert und einen eindeutigen Namen (`spring.application.name=ping-service`) hat.
3. **Schritt 2: Phase 3 - Gateway-Route konfigurieren**
* [ ] Fügen Sie in der `application.yml` Ihres Gateways eine Route hinzu, die Anfragen von `/api/ping` an den `ping-service` weiterleitet (Load Balanced via `lb://ping-service`).
4. **Schritt 3: Phase 4 - Gesamtsystem testen**
* [ ] Starten Sie Consul, den Gateway und den Ping-Service.
* [ ] Rufen Sie die Gateway-URL (z.B. `http://localhost:8080/api/ping`) auf und verifizieren Sie, dass Sie die `{"status": "pong"}`-Antwort erhalten.
* [ ] Erstellen Sie den minimalen "Ping"-Button in Ihrer Client-Anwendung und testen Sie den gesamten Weg.
Wenn Sie diesen Plan abarbeiten, haben Sie nicht nur Ihre Architektur validiert, sondern auch einige Stellen modernisiert und aufgeräumt. Sie sind auf einem exzellenten Weg
+4 -53
View File
@@ -1,56 +1,3 @@
/*plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ktor)
alias(libs.plugins.spring.dependencyManagement)
application
}
application {
mainClass.set("at.mocode.infrastructure.gateway.ApplicationKt")
}
dependencies {
api(platform(libs.spring.boot.dependencies))
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.infrastructure.auth.authClient)
implementation(projects.infrastructure.monitoring.monitoringClient)
// --- Ktor Server ---
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.contentNegotiation)
implementation(libs.ktor.server.serialization.kotlinx.json)
implementation(libs.ktor.server.cors)
implementation(libs.ktor.server.callLogging)
implementation(libs.ktor.server.defaultHeaders)
implementation(libs.ktor.server.statusPages)
implementation(libs.ktor.server.auth)
implementation(libs.ktor.server.authJwt)
implementation(libs.ktor.server.rateLimit)
implementation(libs.ktor.server.metrics.micrometer)
// --- OpenAPI & Swagger for Ktor ---
implementation(libs.ktor.server.openapi)
implementation(libs.ktor.server.swagger)
// --- Ktor Client (damit der Gateway Anfragen an die Backend-Services weiterleiten kann) ---
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
// --- Monitoring ---
implementation(libs.micrometer.prometheus)
// --- Testing ---
testImplementation(projects.platform.platformTesting)
testImplementation(libs.ktor.server.tests)
}*/
// Dieses Modul ist das API-Gateway und der einzige öffentliche Einstiegspunkt
// für alle externen Anfragen an das Meldestelle-System.
plugins {
@@ -75,6 +22,8 @@ dependencies {
// Stellt die Spring Cloud Gateway und Consul Discovery Abhängigkeiten bereit
implementation(libs.bundles.spring.cloud.gateway)
// Sichert den reaktiven Webserver (Netty) explizit ab, um Test-/Kontext-Probleme zu vermeiden
implementation("org.springframework.boot:spring-boot-starter-webflux")
// Bindet die wiederverwendbare Logik zur JWT-Validierung ein.
implementation(projects.infrastructure.auth.authClient)
@@ -85,5 +34,7 @@ dependencies {
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm)
// Security im Testkontext, um eine permissive Security-Konfiguration bereitstellen zu können
testImplementation(libs.spring.boot.starter.security)
}
@@ -2,13 +2,10 @@ package at.mocode.infrastructure.gateway
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
class GatewayApplication
fun main(args: Array<String>) {
runApplication<GatewayApplication>(*args)
}
@@ -8,6 +8,20 @@ spring:
name: api-gateway
cloud:
gateway:
# HTTP Client-Timeouts für stabile Upstream-Verbindungen
httpclient:
connect-timeout: 5000 # in Millisekunden
response-timeout: 30s
# Globales CORS-Setup (kann pro Umgebung überschrieben werden)
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
# Antwort-Header bereinigen (verhindert doppelte CORS-Header)
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
# Aktiviert die automatische Routen-Erstellung basierend auf Consul
discovery:
locator:
@@ -0,0 +1,88 @@
package at.mocode.infrastructure.gateway
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import java.time.Duration
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertNotNull
import org.springframework.boot.test.context.TestConfiguration
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = [
// Use a random port and disable discovery/consul for the test
"server.port=0",
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
// Disable security autoconfiguration for tests
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration",
// Force a reactive web application so that Spring Cloud Gateway auto-config activates
"spring.main.web-application-type=reactive",
// Gateway discovery locator off; we use explicit test routes
"spring.cloud.gateway.discovery.locator.enabled=false"
]
)
@AutoConfigureWebTestClient
@Import(GatewayApplicationTests.TestRoutes::class, GatewayApplicationTests.InternalHelloController::class, GatewayApplicationTests.TestSecurityConfig::class)
class GatewayApplicationTests {
@Autowired
lateinit var client: WebTestClient
@Autowired
lateinit var routeLocator: RouteLocator
@Test
fun contextLoads() {
// If the application context fails to load, this test will fail.
}
@Test
fun forwardRouteShouldReturnResponseFromInternalController() {
client.get()
.uri("/hello")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("OK")
}
@RestController
class InternalHelloController {
@GetMapping("/internal/hello")
fun hello(): String = "OK"
}
@Configuration
class TestRoutes {
@Bean
fun routeLocator(builder: RouteLocatorBuilder): RouteLocator = builder.routes()
.route("test-forward") { r -> r.path("/hello").uri("forward:/internal/hello") }
.build()
}
@TestConfiguration
class TestSecurityConfig {
@Bean
fun springSecurityFilterChain(): org.springframework.security.web.server.SecurityWebFilterChain =
org.springframework.security.config.web.server.ServerHttpSecurity
.http()
.csrf { it.disable() }
.authorizeExchange { exchanges -> exchanges.anyExchange().permitAll() }
.build()
}
}
@@ -1,18 +1,22 @@
package at.mocode.infrastructure.messaging.client
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.core.ProducerFactory
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate
import reactor.kafka.sender.SenderOptions
@Configuration
/**
* Reactive Kafka configuration utilities for creating a ReactiveKafkaProducerTemplate.
*/
class ReactiveKafkaConfig {
@Bean
fun reactiveKafkaProducerTemplate(producerFactory: ProducerFactory<String, Any>): ReactiveKafkaProducerTemplate<String, Any> {
// Nutzt die ProducerFactory aus dem messaging-config-Modul
val senderOptions = SenderOptions.create<String, Any>(producerFactory.configurationProperties)
/**
* Create a ReactiveKafkaProducerTemplate using the configuration from the given ProducerFactory.
*/
fun reactiveKafkaProducerTemplate(
producerFactory: DefaultKafkaProducerFactory<String, Any>
): ReactiveKafkaProducerTemplate<String, Any> {
val props: Map<String, Any> = producerFactory.configurationProperties
val senderOptions: SenderOptions<String, Any> = SenderOptions.create(props)
return ReactiveKafkaProducerTemplate(senderOptions)
}
}
@@ -36,7 +36,7 @@ class KafkaIntegrationTest {
val kafkaConfig = KafkaConfig().apply {
bootstrapServers = kafkaContainer.bootstrapServers
}
producerFactory = kafkaConfig.producerFactory() as DefaultKafkaProducerFactory<String, Any>
producerFactory = kafkaConfig.producerFactory()
val reactiveKafkaConfig = ReactiveKafkaConfig()
val reactiveTemplate = reactiveKafkaConfig.reactiveKafkaProducerTemplate(producerFactory)
@@ -60,9 +60,18 @@ class KafkaIntegrationTest {
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest",
JsonDeserializer.TRUSTED_PACKAGES to "*"
JsonDeserializer.TRUSTED_PACKAGES to "*",
JsonDeserializer.USE_TYPE_INFO_HEADERS to false,
JsonDeserializer.VALUE_DEFAULT_TYPE to TestEvent::class.java.name
)
val receiverOptions = ReceiverOptions.create<String, TestEvent>(consumerProps).subscription(listOf(testTopic))
val jsonValueDeserializer = JsonDeserializer(TestEvent::class.java).apply {
addTrustedPackages("*")
}
val receiverOptions = ReceiverOptions.create<String, TestEvent>(consumerProps)
.withKeyDeserializer(StringDeserializer())
.withValueDeserializer(jsonValueDeserializer)
.subscription(listOf(testTopic))
// Der Mono, der das nächste empfangene Ereignis darstellt
val receivedEvent = KafkaReceiver.create(receiverOptions)
@@ -1,57 +1,38 @@
package at.mocode.infrastructure.messaging.config
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.core.ProducerFactory
import org.springframework.kafka.support.serializer.JsonDeserializer
import org.springframework.kafka.support.serializer.JsonSerializer
@Configuration
/**
* Central Kafka producer configuration used across modules.
*
* This class can be instantiated programmatically (as done in tests) or
* registered as a Spring @Configuration with @Bean methods in an application context.
*/
class KafkaConfig {
// KORREKTUR: Von lateinit zu einer public var mit Standardwert, um Tests zu ermöglichen
@Value($$"${spring.kafka.bootstrap-servers:localhost:9092}")
/**
* Comma-separated list of host:port pairs used for establishing the initial connection to the Kafka cluster.
*/
var bootstrapServers: String = "localhost:9092"
@Value("\${spring.kafka.consumer.group-id:meldestelle-group}")
private lateinit var consumerGroupId: String
/**
* Common producer properties with sensible defaults (String keys, JSON values).
*/
fun producerConfigs(): Map<String, Any> = mapOf(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
// Avoid adding type info headers; keeps payloads simple and interoperable.
JsonSerializer.ADD_TYPE_INFO_HEADERS to false
)
@Bean
fun producerFactory(): ProducerFactory<String, Any> {
val configProps = mapOf(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
ProducerConfig.ACKS_CONFIG to "all",
ProducerConfig.RETRIES_CONFIG to 3,
ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true,
ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION to 1
)
return DefaultKafkaProducerFactory(configProps)
}
@Bean
fun kafkaTemplate(producerFactory: ProducerFactory<String, Any>): KafkaTemplate<String, Any> {
return KafkaTemplate(producerFactory)
}
// NEU: Stellt eine zentrale Map mit den Basis-Konfigurationen für alle Consumer bereit.
@Bean
fun kafkaConsumerConfiguration(): Map<String, Any> {
return mapOf(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
ConsumerConfig.GROUP_ID_CONFIG to consumerGroupId,
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", // Beginne davon am Anfang, wenn kein Offset existiert
JsonDeserializer.TRUSTED_PACKAGES to "*" // Erlaube Deserialisierung aller unserer Klassen
)
}
/**
* Strongly typed producer factory to avoid unchecked casts in consumers/tests.
*/
fun producerFactory(): DefaultKafkaProducerFactory<String, Any> =
DefaultKafkaProducerFactory(producerConfigs())
}
@@ -3,14 +3,9 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
enabled = false
}
dependencies {
@@ -0,0 +1,19 @@
package at.mocode.infrastructure.monitoring.client
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.context.annotation.PropertySource
/**
* AutoConfiguration für das Monitoring-Client-Modul.
*
* Lädt konservative Default-Properties mit niedriger Priorität, die in jeder Anwendung
* leicht per application.properties/-yaml überschrieben werden können.
*/
@AutoConfiguration
@ConditionalOnClass(name = [
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",
"io.micrometer.core.instrument.MeterRegistry"
])
@PropertySource("classpath:monitoring-defaults.properties")
class MonitoringClientAutoConfiguration
@@ -0,0 +1 @@
at.mocode.infrastructure.monitoring.client.MonitoringClientAutoConfiguration
@@ -0,0 +1,27 @@
# ===================================================================
# MELDENSTELLE - MONITORING CLIENT DEFAULTS (via AutoConfiguration)
# Diese Konfigurationen werden automatisch von jedem Service übernommen,
# der das monitoring-client-Modul einbindet. Sie können in der Anwendung
# jederzeit überschrieben werden.
# ===================================================================
# --- Spring Boot Actuator ---
# Stellt die /actuator Endpunkte bereit (health, info, prometheus)
management.endpoints.web.exposure.include=health,info,prometheus
# --- Micrometer Tracing ---
# Aktiviert das Tracing
management.tracing.enabled=true
# Definiert, dass Traces immer gesammelt werden sollen (1.0 = 100%)
management.tracing.sampling.probability=1.0
# --- Micrometer Observation (für Metriken UND Tracing) ---
# Aktiviert die "Beobachtung" von HTTP Server Requests.
# Dies erzeugt automatisch Metriken (Timer) UND Traces für eingehende Anfragen.
management.observations.http.server.requests.enabled=true
# Fügt Anwendungs-Informationen zu den Metriken hinzu
management.info.env.enabled=true
# Definiert den Standard-Endpunkt, an den die Traces gesendet werden.
management.zipkin.tracing.endpoint=http://zipkin:9411/api/v2/spans
@@ -0,0 +1,25 @@
package at.mocode.infrastructure.monitoring.client
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.test.context.runner.ApplicationContextRunner
class MonitoringClientAutoConfigurationTest {
private val contextRunner = ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MonitoringClientAutoConfiguration::class.java))
@Test
fun `should load monitoring properties correctly into the environment`() {
// Arrange
val expectedPropertyValue = "true"
val propertyKey = "management.observations.http.server.requests.enabled"
// Act & Assert
contextRunner.run { context ->
val actualPropertyValue = context.environment.getProperty(propertyKey)
assertThat(actualPropertyValue).isEqualTo(expectedPropertyValue)
}
}
}
@@ -0,0 +1,7 @@
package at.mocode.infrastructure.monitoring.client
import org.springframework.boot.autoconfigure.SpringBootApplication
// Minimaler Test-Application-Context für Library-Tests.
@SpringBootApplication
class MonitoringClientTestApplication
@@ -2,7 +2,9 @@ package at.mocode.infrastructure.monitoring
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import zipkin2.server.internal.EnableZipkinServer
@EnableZipkinServer
@SpringBootApplication
class MonitoringServerApplication
@@ -0,0 +1,24 @@
package at.mocode.infrastructure.monitoring
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
// Startet den ApplicationContext mit Webserver auf zufälligem Port und sicherer Testkonfiguration.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
"server.port=0",
"management.server.port=0",
"zipkin.storage.type=mem",
"zipkin.self-tracing.enabled=false",
"management.tracing.enabled=false",
"management.zipkin.tracing.endpoint="
]
)
class MonitoringServerApplicationTest {
@Test
fun `context loads successfully`() {
// Test ist bestanden, wenn der Kontext ohne Exception startet.
}
}