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
+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.
}
}