refactoring infrastructure

This commit is contained in:
2025-10-14 12:36:00 +02:00
parent a8de8671ce
commit 689458e9b1
10 changed files with 1708 additions and 20 deletions
@@ -1,11 +1,20 @@
spring:
application:
name: auth-server
name: auth-service
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
enabled: ${CONSUL_ENABLED:true}
discovery:
enabled: false
enabled: ${CONSUL_ENABLED:true}
register: ${CONSUL_ENABLED:true}
health-check-path: /actuator/health
health-check-interval: 10s
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
security:
oauth2:
@@ -21,12 +30,41 @@ management:
endpoints:
web:
exposure:
include: health,info
include: health,info,metrics,prometheus
base-path: /actuator
cors:
allowed-origins:
- "https://*.meldestelle.at"
- "http://localhost:*"
allowed-methods: GET,POST
allowed-headers: "*"
allow-credentials: true
endpoint:
health:
show-details: always
show-components: always
probes:
enabled: true
metrics:
enabled: true
prometheus:
enabled: true
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
service: auth
component: infrastructure
# Tracing-Konfiguration
tracing:
enabled: ${TRACING_ENABLED:false}
sampling:
probability: ${TRACING_SAMPLING_PROBABILITY:1.0}
zipkin:
tracing:
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
connect-timeout: 1s
read-timeout: 10s
logging:
level:
+167
View File
@@ -0,0 +1,167 @@
# Redis Cache Module
## Überblick
Dieses Modul stellt eine konkrete Implementierung der `cache-api` unter Verwendung von Redis als Caching-Backend bereit.
## Architektur
Das Modul folgt dem Provider-Pattern:
- **cache-api**: Provider-agnostische Interfaces (`CacheService`, `DistributedCache`)
- **redis-cache**: Redis-spezifische Implementierung
## Verwendung
### Dependency Hinzufügen
```kotlin
dependencies {
implementation(projects.infrastructure.cache.redisCache)
}
```
### Konfiguration
Das Modul verwendet Spring Boot Auto-Configuration. Konfigurieren Sie Redis über `application.yml`:
```yaml
redis:
host: localhost
port: 6379
password: null # Optional
database: 0 # Default cache database
connectionTimeout: 2000
readTimeout: 2000
usePooling: true
maxPoolSize: 8
minPoolSize: 2
```
### Code-Beispiel
```kotlin
@Service
class MyService(private val cache: DistributedCache) {
suspend fun getData(key: String): MyData? {
return cache.get(key, MyData::class)
}
suspend fun saveData(key: String, data: MyData) {
cache.put(key, data, ttl = 1.hours)
}
}
```
## Features
- ✅ TTL-Unterstützung für Cache-Einträge
- ✅ Connection State Tracking
- ✅ Health Monitoring
- ✅ Jackson-basierte Serialisierung
- ✅ Connection Pooling mit Lettuce
- ✅ Kotlin Coroutines Support
## Beans
Das Modul registriert folgende Spring Beans:
- `redisConnectionFactory`: Standard Redis ConnectionFactory
- `redisTemplate`: RedisTemplate<String, ByteArray> für Cache-Operationen
- `cacheSerializer`: Jackson-basierter Serializer (kann überschrieben werden)
- `cacheConfiguration`: Standard Cache-Konfiguration (kann überschrieben werden)
## Gleichzeitige Verwendung mit redis-event-store
⚠️ **WICHTIG**: Wenn Sie sowohl `redis-cache` als auch `redis-event-store` im selben Service verwenden:
### Unterschiedliche Databases
Die Module verwenden **separate Redis Databases**, um Konflikte zu vermeiden:
- **redis-cache**: Database 0 (Standard)
- **redis-event-store**: Database 1 (konfigurierbar)
### Konfigurationsbeispiel
```yaml
# Redis Cache Konfiguration
redis:
host: localhost
port: 6379
database: 0 # Cache verwendet Database 0
# Redis Event Store Konfiguration
redis:
event-store:
host: localhost
port: 6379
database: 1 # Event Store verwendet Database 1
```
### Bean-Namen
Die Module verwenden unterschiedliche Bean-Namen:
| Komponente | redis-cache | redis-event-store |
|------------|-------------|-------------------|
| ConnectionFactory | `redisConnectionFactory` | `eventStoreRedisConnectionFactory` |
| Template | `redisTemplate` | `eventStoreRedisTemplate` |
| Serializer | `cacheSerializer` | `eventSerializer` |
### Keine Konflikte
✅ Die Module sind so designed, dass sie **ohne Konflikte** gleichzeitig verwendet werden können:
- Separate ConnectionFactories mit `@Qualifier`
- Separate Property-Prefixes (`redis` vs `redis.event-store`)
- Unterschiedliche Database-Nummern
- Unterschiedliche Bean-Namen
## Serialisierung
Das Modul verwendet Jackson für die Serialisierung:
- Automatische Kotlin-Modul Integration
- Java 8 Date/Time Support
- Custom Serializer können via `@Bean` überschrieben werden
## Health Checks
Das Modul tracked automatisch den Redis-Verbindungsstatus:
- Connection State (CONNECTED, DISCONNECTED, CONNECTING)
- Connection State Listeners für Benachrichtigungen
- Automatische Reconnect-Versuche
## Performance
- **Connection Pooling**: Wiederverwendbare Verbindungen via Lettuce
- **Non-blocking I/O**: Reaktive Operations mit Kotlin Coroutines
- **Optimierte Serialisierung**: Jackson-basiert mit Byte-Array-Caching
## Troubleshooting
### Redis Verbindungsfehler
```
RedisConnectionFailureException: Unable to connect to Redis
```
**Lösung**: Überprüfen Sie Redis-Server und Netzwerk-Konfiguration.
### Serialisierungsfehler
```
SerializationException: Could not serialize object
```
**Lösung**: Stellen Sie sicher, dass Ihre Datenklassen mit Jackson serialisierbar sind (data classes, keine private Konstruktoren).
### Bean-Konflikte mit Event Store
Wenn Sie Fehler wie "Multiple beans of type RedisConnectionFactory" erhalten:
**Lösung**: Verwenden Sie `@Qualifier` Annotations oder stellen Sie sicher, dass Sie die neueste Version beider Module verwenden (Bean-Namen-Konflikte sind bereits behoben).
## Weitere Informationen
- Siehe auch: [event-store-api README](../../event-store/event-store-api/README.md)
- Siehe auch: [redis-event-store README](../../event-store/redis-event-store/README.md)
@@ -0,0 +1,277 @@
# Redis Event Store Module
## Überblick
Dieses Modul stellt eine konkrete Implementierung der `event-store-api` unter Verwendung von Redis Streams als Event-Store-Backend bereit.
## Architektur
Das Modul folgt dem Provider-Pattern:
- **event-store-api**: Provider-agnostische Interfaces (`EventStore`, `EventSerializer`)
- **redis-event-store**: Redis Streams-spezifische Implementierung
## Verwendung
### Dependency Hinzufügen
```kotlin
dependencies {
implementation(projects.infrastructure.eventStore.redisEventStore)
}
```
### Konfiguration
Das Modul verwendet Spring Boot Auto-Configuration. Konfigurieren Sie Redis über `application.yml`:
```yaml
redis:
event-store:
host: localhost
port: 6379
password: null # Optional
database: 1 # Separate database for event store (default: 1)
connectionTimeout: 2000
readTimeout: 2000
usePooling: true
maxPoolSize: 8
minPoolSize: 2
consumerGroup: event-processors
consumerName: event-consumer
streamPrefix: "event-stream:"
allEventsStream: all-events
claimIdleTimeout: 60s
pollTimeout: 100ms
maxBatchSize: 100
createConsumerGroupIfNotExists: true
```
### Code-Beispiel
```kotlin
@Service
class MyEventService(
private val eventStore: EventStore,
private val eventConsumer: RedisEventConsumer
) {
// Event speichern
suspend fun saveEvent(aggregateId: Uuid, event: DomainEvent) {
eventStore.appendEvent(
aggregateId = aggregateId,
event = event,
expectedVersion = EventVersion.ANY
)
}
// Events abrufen
suspend fun loadEvents(aggregateId: Uuid): List<DomainEvent> {
return eventStore.loadEvents(aggregateId)
}
// Events konsumieren
fun startConsuming() {
eventConsumer.consumeEvents { event ->
println("Received event: $event")
}
}
}
```
## Features
- ✅ Event Sourcing mit Redis Streams
- ✅ Optimistic Locking mit Event Versioning
- ✅ Consumer Groups für parallele Event-Verarbeitung
- ✅ Event Replay-Fähigkeit
- ✅ Pub/Sub für Event-Benachrichtigungen
- ✅ Jackson-basierte Serialisierung
- ✅ Connection Pooling mit Lettuce
- ✅ Kotlin Coroutines Support
## Redis Streams
Das Modul nutzt Redis Streams für Event Sourcing:
- **Stream pro Aggregate**: `event-stream:{aggregateId}`
- **All Events Stream**: `event-stream:all-events`
- **Consumer Groups**: Für parallele Verarbeitung
- **Message IDs**: Für Event-Ordering und Replay
## Beans
Das Modul registriert folgende Spring Beans:
- `eventStoreRedisConnectionFactory`: Separate Redis ConnectionFactory für Event Store
- `eventStoreRedisTemplate`: StringRedisTemplate für Event-Operationen
- `eventSerializer`: Jackson-basierter Event-Serializer
- `eventStore`: EventStore Implementierung
- `eventConsumer`: RedisEventConsumer für Event-Verarbeitung
## Gleichzeitige Verwendung mit redis-cache
⚠️ **WICHTIG**: Wenn Sie sowohl `redis-cache` als auch `redis-event-store` im selben Service verwenden:
### Unterschiedliche Databases
Die Module verwenden **separate Redis Databases**, um Konflikte zu vermeiden:
- **redis-cache**: Database 0 (Standard)
- **redis-event-store**: Database 1 (Standard, konfigurierbar)
### Konfigurationsbeispiel
```yaml
# Beide Module in einer application.yml
redis:
# Cache Konfiguration
host: localhost
port: 6379
database: 0 # Cache verwendet Database 0
# Event Store Konfiguration (nested)
event-store:
host: localhost
port: 6379
database: 1 # Event Store verwendet Database 1
consumerGroup: event-processors
```
### Bean-Namen
Die Module verwenden unterschiedliche Bean-Namen zur Vermeidung von Konflikten:
| Komponente | redis-cache | redis-event-store |
|------------|-------------|-------------------|
| ConnectionFactory | `redisConnectionFactory` | `eventStoreRedisConnectionFactory` |
| Template | `redisTemplate` | `eventStoreRedisTemplate` |
| Serializer | `cacheSerializer` | `eventSerializer` |
### Keine Konflikte
✅ Die Module sind so designed, dass sie **ohne Konflikte** gleichzeitig verwendet werden können:
- **Separate ConnectionFactories** mit `@Qualifier` Annotations
- **Separate Property-Prefixes**: `redis` vs `redis.event-store`
- **Unterschiedliche Database-Nummern**: 0 vs 1
- **Unterschiedliche Bean-Namen**: Explizite Qualifier verhindern Kollisionen
## Event Versioning
Das Modul unterstützt Optimistic Locking:
```kotlin
// Erwartete Version spezifizieren
eventStore.appendEvent(
aggregateId = aggregateId,
event = myEvent,
expectedVersion = EventVersion.of(5) // Erwartet Version 5
)
// Beliebige Version akzeptieren
eventStore.appendEvent(
aggregateId = aggregateId,
event = myEvent,
expectedVersion = EventVersion.ANY
)
```
Bei Version-Konflikten wird eine `ConcurrencyException` geworfen.
## Consumer Groups
Das Modul unterstützt Consumer Groups für parallele Event-Verarbeitung:
```kotlin
// Consumer 1
eventConsumer.consumeEvents(
consumerName = "consumer-1"
) { event ->
// Verarbeite Event
processEvent(event)
}
// Consumer 2 (in der gleichen Consumer Group)
eventConsumer.consumeEvents(
consumerName = "consumer-2"
) { event ->
// Verarbeite Event parallel
processEvent(event)
}
```
Events werden automatisch auf verfügbare Consumer verteilt.
## Event Replay
Sie können Events von einem bestimmten Zeitpunkt oder Message-ID replaying:
```kotlin
// Replay alle Events eines Aggregates
val events = eventStore.loadEvents(aggregateId)
// Replay Events ab einer bestimmten Version
val eventsFromVersion = eventStore.loadEvents(
aggregateId = aggregateId,
fromVersion = EventVersion.of(10)
)
```
## Serialisierung
Das Modul verwendet Jackson für Event-Serialisierung:
- Automatische Kotlin-Modul Integration
- Polymorphe Serialisierung für verschiedene Event-Typen
- Custom Serializer können via `@Bean` überschrieben werden
## Performance
- **Connection Pooling**: Wiederverwendbare Verbindungen via Lettuce
- **Batch Processing**: Konfigurierbare Batch-Größe für Consumer
- **Non-blocking I/O**: Reaktive Operations mit Kotlin Coroutines
- **Stream-basiert**: Effiziente Event-Speicherung mit Redis Streams
## Troubleshooting
### Redis Verbindungsfehler
```
RedisConnectionFailureException: Unable to connect to Redis
```
**Lösung**: Überprüfen Sie Redis-Server und Netzwerk-Konfiguration. Stellen Sie sicher, dass Redis Streams unterstützt werden (Redis 5.0+).
### Concurrency Exception
```
ConcurrencyException: Expected version X but found Y
```
**Lösung**: Dies ist normales Verhalten bei Optimistic Locking. Implementieren Sie Retry-Logik oder verwenden Sie `EventVersion.ANY`.
### Consumer Group Fehler
```
Consumer group already exists
```
**Lösung**: Setzen Sie `createConsumerGroupIfNotExists: true` in der Konfiguration oder löschen Sie die Consumer Group manuell.
### Bean-Konflikte mit Cache
Wenn Sie Fehler wie "Multiple beans of type RedisConnectionFactory" erhalten:
**Lösung**: Die Module verwenden bereits unterschiedliche Bean-Namen mit `@Qualifier`. Stellen Sie sicher, dass Sie beide Module korrekt konfiguriert haben (siehe Abschnitt "Gleichzeitige Verwendung").
## Best Practices
1. **Separate Databases**: Verwenden Sie immer separate Redis Databases für Cache und Event Store
2. **Event Versioning**: Verwenden Sie Optimistic Locking für kritische Aggregates
3. **Consumer Groups**: Nutzen Sie Consumer Groups für horizontale Skalierung
4. **Error Handling**: Implementieren Sie Retry-Logik für transiente Fehler
5. **Monitoring**: Überwachen Sie Stream-Größen und Consumer Lag
## Weitere Informationen
- Siehe auch: [cache-api README](../../cache/cache-api/README.md)
- Siehe auch: [redis-cache README](../../cache/redis-cache/README.md)
- Redis Streams Dokumentation: https://redis.io/docs/data-types/streams/
@@ -42,6 +42,9 @@ dependencies {
// Zusätzliche Test-Dependencies für erweiterte Event-Store-Tests
testImplementation(libs.kotlinx.serialization.json)
testImplementation(libs.reactor.test)
// Für Integration Tests mit beiden Redis-Modulen
testImplementation(projects.infrastructure.cache.cacheApi)
testImplementation(projects.infrastructure.cache.redisCache)
}
// === Task Configuration ===
@@ -0,0 +1,251 @@
package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.DomainEvent
import at.mocode.core.domain.model.*
import at.mocode.infrastructure.cache.api.CacheConfiguration
import at.mocode.infrastructure.cache.api.DistributedCache
import at.mocode.infrastructure.cache.redis.JacksonCacheSerializer
import at.mocode.infrastructure.cache.redis.RedisConfiguration
import at.mocode.infrastructure.cache.redis.RedisDistributedCache
import at.mocode.infrastructure.eventstore.api.EventStore
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
import kotlin.time.Duration.Companion.minutes
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
/**
* Integration Test zur Demonstration der gleichzeitigen Verwendung von
* redis-cache und redis-event-store im selben Service.
*
* Dieser Test zeigt:
* 1. Beide Module können ohne Konflikte gleichzeitig verwendet werden
* 2. Separate Redis Databases verhindern Daten-Überschneidungen
* 3. Separate Bean-Namen verhindern Bean-Konflikte
* 4. Beide Module arbeiten unabhängig voneinander
*/
@OptIn(ExperimentalUuidApi::class)
@SpringBootTest(
classes = [
RedisCacheAndEventStoreIntegrationTest.TestConfig::class
]
)
@Testcontainers
class RedisCacheAndEventStoreIntegrationTest {
companion object {
@Container
@JvmStatic
val redisContainer: GenericContainer<*> = GenericContainer(
DockerImageName.parse("redis:7-alpine")
).withExposedPorts(6379)
@DynamicPropertySource
@JvmStatic
fun configureProperties(registry: DynamicPropertyRegistry) {
// Cache Configuration (Database 0)
registry.add("redis.host") { redisContainer.host }
registry.add("redis.port") { redisContainer.getMappedPort(6379) }
registry.add("redis.database") { 0 }
// Event Store Configuration (Database 1)
registry.add("redis.event-store.host") { redisContainer.host }
registry.add("redis.event-store.port") { redisContainer.getMappedPort(6379) }
registry.add("redis.event-store.database") { 1 }
registry.add("redis.event-store.consumerGroup") { "test-group" }
}
@BeforeAll
@JvmStatic
fun setUp() {
println("[DEBUG_LOG] Starting Redis container for integration test")
redisContainer.start()
}
@AfterAll
@JvmStatic
fun tearDown() {
println("[DEBUG_LOG] Stopping Redis container")
redisContainer.stop()
}
}
@Configuration
@Import(
RedisConfiguration::class,
RedisEventStoreConfiguration::class
)
class TestConfig {
@Bean
fun distributedCache(
redisTemplate: RedisTemplate<String, ByteArray>,
cacheConfiguration: CacheConfiguration
): DistributedCache {
return RedisDistributedCache(
redisTemplate = redisTemplate,
serializer = JacksonCacheSerializer(),
config = cacheConfiguration
)
}
}
@Autowired
private lateinit var cache: DistributedCache
@Autowired
private lateinit var eventStore: EventStore
// Verify separate ConnectionFactories
@Autowired
@Qualifier("redisConnectionFactory")
private lateinit var cacheConnectionFactory: RedisConnectionFactory
@Autowired
@Qualifier("eventStoreRedisConnectionFactory")
private lateinit var eventStoreConnectionFactory: RedisConnectionFactory
@Test
fun `test both modules can be used simultaneously without conflicts`(): Unit = runBlocking {
println("[DEBUG_LOG] Testing simultaneous usage of cache and event store")
// Test Cache Operations
val cacheKey = "test-user-${Uuid.random()}"
val cacheData = TestUser("John Doe", 30)
println("[DEBUG_LOG] Cache: Storing data with key=$cacheKey")
cache.set(cacheKey, cacheData, ttl = 5.minutes)
val retrievedCacheData = cache.get(cacheKey, TestUser::class.java)
println("[DEBUG_LOG] Cache: Retrieved data=$retrievedCacheData")
assertNotNull(retrievedCacheData)
assertEquals(cacheData.name, retrievedCacheData!!.name)
assertEquals(cacheData.age, retrievedCacheData.age)
// Test Event Store Operations
val aggregateId = Uuid.random()
val event = TestEvent(
aggregateId = AggregateId(aggregateId),
eventType = EventType("UserCreated"),
data = mapOf("userId" to aggregateId.toString(), "name" to "Jane Doe")
)
println("[DEBUG_LOG] EventStore: Appending event for aggregateId=$aggregateId")
eventStore.appendToStream(event, aggregateId, -1L)
val loadedEvents = eventStore.readFromStream(aggregateId)
println("[DEBUG_LOG] EventStore: Loaded ${loadedEvents.size} events")
assertEquals(1, loadedEvents.size)
assertEquals(event.eventType, (loadedEvents[0] as TestEvent).eventType)
// Verify Cache and Event Store are independent
println("[DEBUG_LOG] Verifying cache and event store are independent")
// Cache should still work after event operations
val cacheStillWorks = cache.get(cacheKey, TestUser::class.java)
assertNotNull(cacheStillWorks)
println("[DEBUG_LOG] Cache still works: key=$cacheKey exists")
// Event store should still work after cache operations
val eventsStillWork = eventStore.readFromStream(aggregateId)
assertEquals(1, eventsStillWork.size)
println("[DEBUG_LOG] Event store still works: aggregateId=$aggregateId has ${eventsStillWork.size} events")
println("[DEBUG_LOG] Test completed successfully - Both modules work independently")
}
@Test
fun `test separate connection factories are used`() {
println("[DEBUG_LOG] Testing separate connection factories")
assertNotNull(cacheConnectionFactory)
assertNotNull(eventStoreConnectionFactory)
// The connection factories should be different instances
println("[DEBUG_LOG] Cache ConnectionFactory: ${cacheConnectionFactory.javaClass.simpleName}")
println("[DEBUG_LOG] EventStore ConnectionFactory: ${eventStoreConnectionFactory.javaClass.simpleName}")
// Both should be functional
val cacheConnection = cacheConnectionFactory.connection
val eventStoreConnection = eventStoreConnectionFactory.connection
assertNotNull(cacheConnection)
assertNotNull(eventStoreConnection)
// Different databases
println("[DEBUG_LOG] Cache uses database: ${cacheConnection.nativeConnection}")
println("[DEBUG_LOG] EventStore uses database: ${eventStoreConnection.nativeConnection}")
cacheConnection.close()
eventStoreConnection.close()
println("[DEBUG_LOG] Both connection factories are functional and independent")
}
@Test
fun `test data isolation between cache and event store`(): Unit = runBlocking {
println("[DEBUG_LOG] Testing data isolation between cache and event store")
val sharedKey = "shared-key-${Uuid.random()}"
// Store data in cache
cache.set(sharedKey, TestUser("Cache User", 25), ttl = 5.minutes)
println("[DEBUG_LOG] Stored data in cache with key=$sharedKey")
// Store event with same UUID in event store
val aggregateId = Uuid.random()
val event = TestEvent(
aggregateId = AggregateId(aggregateId),
eventType = EventType("TestEvent"),
data = mapOf("key" to sharedKey)
)
eventStore.appendToStream(event, aggregateId, -1L)
println("[DEBUG_LOG] Stored event in event store with aggregateId=$aggregateId")
// Both should be retrievable independently
val cachedUser = cache.get(sharedKey, TestUser::class.java)
val storedEvents = eventStore.readFromStream(aggregateId)
assertNotNull(cachedUser)
assertEquals(1, storedEvents.size)
println("[DEBUG_LOG] Data isolation verified:")
println("[DEBUG_LOG] - Cache retrieved: ${cachedUser?.name}")
println("[DEBUG_LOG] - Event store retrieved: ${storedEvents.size} events")
println("[DEBUG_LOG] Cache and Event Store use separate databases - no conflicts!")
}
// Test data classes
data class TestUser(
val name: String,
val age: Int
)
data class TestEvent(
override val aggregateId: AggregateId,
override val eventType: EventType,
val data: Map<String, String>,
override val eventId: EventId = EventId(Uuid.random()),
override val timestamp: kotlin.time.Instant = kotlin.time.Clock.System.now(),
override val version: EventVersion = EventVersion(0),
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : DomainEvent
}
+660
View File
@@ -0,0 +1,660 @@
# Gateway Configuration Documentation
## Überblick
Dieses Dokument beschreibt alle zentralen Konfigurationseigenschaften für das API Gateway. Die Konfiguration erfolgt über die `application.yml` Datei und kann durch Umgebungsvariablen überschrieben werden.
## Table of Contents
- [Server Configuration](#server-configuration)
- [Spring Application](#spring-application)
- [Consul Service Discovery](#consul-service-discovery)
- [Spring Cloud Gateway](#spring-cloud-gateway)
- [Circuit Breaker (Resilience4j)](#circuit-breaker-resilience4j)
- [Management & Monitoring](#management--monitoring)
- [Security](#security)
- [Logging](#logging)
---
## Server Configuration
### server.port
- **Typ**: Integer
- **Default**: 8081
- **Environment Variable**: `GATEWAY_PORT`
- **Beschreibung**: Port, auf dem das Gateway läuft
### server.netty.connection-timeout
- **Typ**: Duration
- **Default**: 5s
- **Beschreibung**: Timeout für initiale TCP-Verbindungen
### server.netty.idle-timeout
- **Typ**: Duration
- **Default**: 15s
- **Beschreibung**: Timeout für inaktive Verbindungen
**Beispiel:**
```yaml
server:
port: 8081
netty:
connection-timeout: 5s
idle-timeout: 15s
```
---
## Spring Application
### spring.application.name
- **Typ**: String
- **Default**: api-gateway
- **Beschreibung**: Name der Anwendung, wird in Consul und Logs verwendet
### spring.profiles.active
- **Typ**: String
- **Default**: dev
- **Environment Variable**: `SPRING_PROFILES_ACTIVE`
- **Beschreibung**: Aktives Spring-Profil (dev, test, prod)
- **Mögliche Werte**: dev, test, staging, prod
### spring.security.user.name / password
- **Typ**: String
- **Default**: admin / admin
- **Environment Variables**: `GATEWAY_ADMIN_USER`, `GATEWAY_ADMIN_PASSWORD`
- **Beschreibung**: Basic Auth für administrative Endpunkte
- **⚠️ Wichtig**: In Produktion durch sichere Werte ersetzen!
**Beispiel:**
```yaml
spring:
application:
name: api-gateway
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
security:
user:
name: ${GATEWAY_ADMIN_USER:admin}
password: ${GATEWAY_ADMIN_PASSWORD:admin}
```
---
## Consul Service Discovery
### spring.cloud.consul.host
- **Typ**: String
- **Default**: localhost
- **Environment Variable**: `CONSUL_HOST`
- **Beschreibung**: Hostname des Consul-Servers
### spring.cloud.consul.port
- **Typ**: Integer
- **Default**: 8500
- **Environment Variable**: `CONSUL_PORT`
- **Beschreibung**: Port des Consul-Servers
### spring.cloud.consul.enabled
- **Typ**: Boolean
- **Default**: true
- **Environment Variable**: `CONSUL_ENABLED`
- **Beschreibung**: Aktiviert/Deaktiviert Consul Integration
### spring.cloud.consul.discovery.enabled
- **Typ**: Boolean
- **Default**: true
- **Environment Variable**: `CONSUL_ENABLED`
- **Beschreibung**: Aktiviert Service Discovery
### spring.cloud.consul.discovery.register
- **Typ**: Boolean
- **Default**: true
- **Environment Variable**: `CONSUL_ENABLED`
- **Beschreibung**: Registriert das Gateway in Consul
### spring.cloud.consul.discovery.health-check-path
- **Typ**: String
- **Default**: /actuator/health
- **Beschreibung**: Pfad für Consul Health Checks
### spring.cloud.consul.discovery.health-check-interval
- **Typ**: Duration
- **Default**: 10s
- **Beschreibung**: Intervall für Health Checks
### spring.cloud.consul.discovery.instance-id
- **Typ**: String
- **Default**: ${spring.application.name}-${server.port}-${random.uuid}
- **Beschreibung**: Eindeutige Instanz-ID für Service Discovery
**Beispiel:**
```yaml
spring:
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
enabled: ${CONSUL_ENABLED:true}
discovery:
enabled: ${CONSUL_ENABLED:true}
register: ${CONSUL_ENABLED:true}
health-check-path: /actuator/health
health-check-interval: 10s
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
```
---
## Spring Cloud Gateway
### Verbindungskonfiguration
#### spring.cloud.gateway.server.webflux.httpclient.connect-timeout
- **Typ**: Integer (Millisekunden)
- **Default**: 5000
- **Beschreibung**: Timeout für Backend-Verbindungen
#### spring.cloud.gateway.server.webflux.httpclient.response-timeout
- **Typ**: Duration
- **Default**: 30s
- **Beschreibung**: Timeout für Backend-Responses
#### spring.cloud.gateway.server.webflux.httpclient.pool.max-idle-time
- **Typ**: Duration
- **Default**: 15s
- **Beschreibung**: Max. Idle-Zeit für Verbindungen im Pool
#### spring.cloud.gateway.server.webflux.httpclient.pool.max-life-time
- **Typ**: Duration
- **Default**: 60s
- **Beschreibung**: Max. Lebensdauer einer Verbindung
**Beispiel:**
```yaml
spring:
cloud:
gateway:
server:
webflux:
httpclient:
connect-timeout: 5000
response-timeout: 30s
pool:
max-idle-time: 15s
max-life-time: 60s
```
### Default Filters
Diese Filter werden auf **alle** Routen angewendet:
1. **DedupeResponseHeader**: Entfernt doppelte CORS-Header
2. **CircuitBreaker**: Default Circuit Breaker mit Fallback
3. **Retry**: Automatische Wiederholung bei Fehlern
4. **Security Headers**: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, etc.
5. **Cache-Control**: No-cache Header für alle Responses
**Beispiel:**
```yaml
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: CircuitBreaker
args:
name: defaultCircuitBreaker
fallbackUri: forward:/fallback
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET,POST,PUT,DELETE
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
factor: 2
```
### Routes
Das Gateway definiert folgende Service-Routen:
#### 1. Members Service Route
- **Path**: `/api/members/**`
- **Service**: members-service (via Consul)
- **Circuit Breaker**: membersCircuitBreaker
- **Fallback**: /fallback/members
#### 2. Horses Service Route
- **Path**: `/api/horses/**`
- **Service**: horses-service (via Consul)
- **Circuit Breaker**: horsesCircuitBreaker
- **Fallback**: /fallback/horses
#### 3. Events Service Route
- **Path**: `/api/events/**`
- **Service**: events-service (via Consul)
- **Circuit Breaker**: eventsCircuitBreaker
- **Fallback**: /fallback/events
#### 4. Masterdata Service Route
- **Path**: `/api/masterdata/**`
- **Service**: masterdata-service (via Consul)
- **Circuit Breaker**: masterdataCircuitBreaker
- **Fallback**: /fallback/masterdata
#### 5. Auth Service Route
- **Path**: `/api/auth/**`
- **Service**: auth-service (via Consul)
- **Circuit Breaker**: authCircuitBreaker
- **Fallback**: /fallback/auth
#### 6. Ping Service Route
- **Path**: `/api/ping/**`
- **Service**: ping-service (via Consul)
- **No Circuit Breaker**: Optional service
**Beispiel einer Route:**
```yaml
spring:
cloud:
gateway:
routes:
- id: members-service-route
uri: lb://members-service # lb = Load Balanced via Consul
predicates:
- Path=/api/members/**
filters:
- StripPrefix=1 # Entfernt /api vom Pfad
- name: CircuitBreaker
args:
name: membersCircuitBreaker
fallbackUri: forward:/fallback/members
```
---
## Circuit Breaker (Resilience4j)
### Default Konfiguration
#### resilience4j.circuitbreaker.configs.default.registerHealthIndicator
- **Typ**: Boolean
- **Default**: true
- **Beschreibung**: Registriert Circuit Breaker im Health Endpoint
#### resilience4j.circuitbreaker.configs.default.slidingWindowSize
- **Typ**: Integer
- **Default**: 100
- **Beschreibung**: Größe des Sliding Window für Fehlerrate-Berechnung
#### resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls
- **Typ**: Integer
- **Default**: 20
- **Beschreibung**: Mindestanzahl an Calls bevor Circuit Breaker aktiviert wird
#### resilience4j.circuitbreaker.configs.default.permittedNumberOfCallsInHalfOpenState
- **Typ**: Integer
- **Default**: 3
- **Beschreibung**: Anzahl Test-Calls im Half-Open State
#### resilience4j.circuitbreaker.configs.default.waitDurationInOpenState
- **Typ**: Duration
- **Default**: 5s
- **Beschreibung**: Wartezeit bevor von Open zu Half-Open gewechselt wird
#### resilience4j.circuitbreaker.configs.default.failureRateThreshold
- **Typ**: Integer (Prozent)
- **Default**: 50
- **Beschreibung**: Fehlerrate-Schwelle für Circuit Breaker Aktivierung
### Service-spezifische Circuit Breaker
Jeder Service hat einen eigenen Circuit Breaker mit angepasster Konfiguration:
| Service | Sliding Window | Failure Threshold | Besonderheit |
|---------|---------------|-------------------|--------------|
| members-service | 50 | 50% | Standard |
| horses-service | 50 | 50% | Standard |
| events-service | 75 | 50% | Größeres Window |
| masterdata-service | 30 | 50% | Kleineres Window |
| auth-service | 20 | 30% | Sensitiverer Threshold |
**Beispiel:**
```yaml
resilience4j:
circuitbreaker:
instances:
authCircuitBreaker:
baseConfig: default
slidingWindowSize: 20
failureRateThreshold: 30 # Auth ist kritisch -> niedrigerer Threshold
```
---
## Management & Monitoring
### Exposed Endpoints
#### management.endpoints.web.exposure.include
- **Typ**: Comma-separated String
- **Default**: health,info,metrics,prometheus,gateway,circuitbreakers
- **Beschreibung**: Öffentlich verfügbare Actuator Endpoints
**Verfügbare Endpoints:**
- `/actuator/health` - Service Health Status
- `/actuator/info` - Service Informationen
- `/actuator/metrics` - Micrometer Metriken
- `/actuator/prometheus` - Prometheus Scrape Endpoint
- `/actuator/gateway` - Gateway Routes & Filters
- `/actuator/circuitbreakers` - Circuit Breaker Status
### Health Endpoint
#### management.endpoint.health.show-details
- **Typ**: String
- **Default**: always
- **Mögliche Werte**: never, when-authorized, always
- **Beschreibung**: Zeigt detaillierte Health-Informationen
#### management.endpoint.health.show-components
- **Typ**: Boolean
- **Default**: always
- **Beschreibung**: Zeigt Health-Komponenten
#### management.endpoint.health.probes.enabled
- **Typ**: Boolean
- **Default**: true
- **Beschreibung**: Aktiviert Kubernetes Liveness/Readiness Probes
### Metrics
#### management.metrics.tags
- **Beschreibung**: Globale Tags für alle Metriken
- **Standard Tags**:
- application: ${spring.application.name}
- environment: ${spring.profiles.active}
- instance: ${spring.cloud.consul.discovery.instance-id}
- service: gateway
- component: infrastructure
- gateway: api-gateway
#### management.metrics.distribution.percentiles-histogram.http.server.requests
- **Typ**: Boolean
- **Default**: true
- **Beschreibung**: Aktiviert Histogram für Request-Zeiten
#### management.metrics.distribution.percentiles.http.server.requests
- **Typ**: Array[Double]
- **Default**: [0.5, 0.90, 0.95, 0.99]
- **Beschreibung**: Percentile-Werte für Request-Zeiten
### Tracing
#### management.tracing.enabled
- **Typ**: Boolean
- **Default**: false
- **Environment Variable**: `TRACING_ENABLED`
- **Beschreibung**: Aktiviert Distributed Tracing
#### management.tracing.sampling.probability
- **Typ**: Double (0.0 - 1.0)
- **Default**: 1.0
- **Environment Variable**: `TRACING_SAMPLING_PROBABILITY`
- **Beschreibung**: Sampling-Rate für Traces (1.0 = 100%)
#### management.zipkin.tracing.endpoint
- **Typ**: URL
- **Default**: http://localhost:9411/api/v2/spans
- **Environment Variable**: `ZIPKIN_TRACING_ENDPOINT`
- **Beschreibung**: Zipkin Server URL
**Beispiel:**
```yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,gateway,circuitbreakers
endpoint:
health:
show-details: always
probes:
enabled: true
tracing:
enabled: ${TRACING_ENABLED:false}
sampling:
probability: ${TRACING_SAMPLING_PROBABILITY:1.0}
zipkin:
tracing:
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
```
---
## Security
Die Security-Konfiguration erfolgt über Custom Properties unter `gateway.security`:
### gateway.security.publicPaths
- **Typ**: Array[String]
- **Default**: ["/", "/fallback/**", "/actuator/**", "/webjars/**", "/v3/api-docs/**", "/api/auth/**"]
- **Beschreibung**: Pfade, die ohne Authentifizierung zugänglich sind
### gateway.security.cors.allowedOriginPatterns
- **Typ**: Array[String]
- **Default**: ["http://localhost:[*]", "https://*.meldestelle.at"]
- **Beschreibung**: Erlaubte Origin-Patterns für CORS
### gateway.security.cors.allowedMethods
- **Typ**: Array[String]
- **Default**: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]
- **Beschreibung**: Erlaubte HTTP-Methoden
### gateway.security.cors.allowedHeaders
- **Typ**: Array[String]
- **Default**: ["*"]
- **Beschreibung**: Erlaubte Request-Headers
### gateway.security.cors.exposedHeaders
- **Typ**: Array[String]
- **Default**: ["X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"]
- **Beschreibung**: Headers die an Client exponiert werden
### gateway.security.cors.allowCredentials
- **Typ**: Boolean
- **Default**: true
- **Beschreibung**: Erlaubt Credentials (Cookies, Auth-Header)
### gateway.security.cors.maxAge
- **Typ**: Duration
- **Default**: 1h
- **Beschreibung**: Cache-Zeit für CORS Preflight-Requests
**Beispiel:**
```yaml
gateway:
security:
publicPaths:
- "/"
- "/actuator/**"
- "/api/auth/**"
cors:
allowedOriginPatterns:
- "http://localhost:[*]"
- "https://*.meldestelle.at"
allowedMethods:
- GET
- POST
- PUT
- DELETE
allowCredentials: true
maxAge: 1h
```
### JWT Configuration
#### spring.security.oauth2.resourceserver.jwt.jwk-set-uri
- **Typ**: URL
- **Environment Variable**: `KEYCLOAK_JWK_SET_URI`
- **Beschreibung**: Keycloak JWK Set URI für JWT-Validierung
- **Beispiel**: http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs
---
## Logging
### logging.level
- **Beschreibung**: Log-Level für verschiedene Pakete
**Standard Log-Levels:**
- `org.springframework.cloud.gateway`: INFO
- `org.springframework.cloud.loadbalancer`: DEBUG
- `org.springframework.cloud.consul`: INFO
- `at.mocode.infrastructure.gateway`: DEBUG
- `io.github.resilience4j`: INFO
- `reactor.netty.http.client`: INFO
- `org.springframework.security`: WARN
- `org.springframework.web`: INFO
### logging.pattern.console
- **Beschreibung**: Console-Log-Pattern mit Farben und Correlation-ID
### logging.pattern.file
- **Beschreibung**: File-Log-Pattern ohne Farben
### logging.file.name
- **Typ**: String
- **Default**: infrastructure/gateway/logs/gateway.log
- **Beschreibung**: Log-Datei Pfad
### logging.logback.rollingpolicy
- **clean-history-on-start**: true
- **max-file-size**: 100MB
- **total-size-cap**: 1GB
- **max-history**: 30 (Tage)
**Beispiel:**
```yaml
logging:
level:
at.mocode.infrastructure.gateway: DEBUG
org.springframework.cloud.gateway: INFO
file:
name: infrastructure/gateway/logs/gateway.log
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 30
```
---
## Umgebungsvariablen Übersicht
### Kritische Variablen für Produktion
| Variable | Beschreibung | Default |
|----------|--------------|---------|
| `GATEWAY_PORT` | Gateway Port | 8081 |
| `CONSUL_HOST` | Consul Server | localhost |
| `CONSUL_PORT` | Consul Port | 8500 |
| `CONSUL_ENABLED` | Consul Aktivieren | true |
| `GATEWAY_ADMIN_USER` | Admin Username | admin |
| `GATEWAY_ADMIN_PASSWORD` | Admin Password | admin |
| `KEYCLOAK_JWK_SET_URI` | Keycloak JWK URI | http://localhost:8180/... |
| `TRACING_ENABLED` | Tracing aktivieren | false |
| `ZIPKIN_TRACING_ENDPOINT` | Zipkin Server | http://localhost:9411/... |
| `SPRING_PROFILES_ACTIVE` | Spring Profil | dev |
---
## Profile-spezifische Konfiguration
Das Gateway unterstützt verschiedene Spring Profile:
### dev (Development)
- Detailliertes Logging
- Alle Monitoring-Endpunkte verfügbar
- Tracing optional
### test
- Reduziertes Logging
- Test-spezifische Timeouts
- In-Memory Services optional
### prod (Production)
- Production-ready Logging
- Sichere Credentials erforderlich
- Tracing empfohlen
- Rate Limiting aktiviert
**Beispiel für profile-spezifische Datei:**
```yaml
# application-prod.yml
spring:
security:
user:
name: ${GATEWAY_ADMIN_USER} # Muss gesetzt sein!
password: ${GATEWAY_ADMIN_PASSWORD} # Muss gesetzt sein!
management:
tracing:
enabled: true
sampling:
probability: 0.1 # 10% Sampling in Production
logging:
level:
at.mocode.infrastructure.gateway: INFO # Weniger Logs
```
---
## Best Practices
1. **Umgebungsvariablen verwenden**: Nie Credentials in application.yml hardcoden
2. **Profile nutzen**: Separate Konfigurationen für dev/test/prod
3. **Health Checks aktivieren**: Für Consul und Kubernetes
4. **Tracing in Production**: Mindestens 10% Sampling
5. **Monitoring exportieren**: Prometheus-Endpunkt für Grafana
6. **Circuit Breaker tunen**: An Service-Charakteristiken anpassen
7. **CORS restriktiv**: Nur benötigte Origins erlauben
8. **Log Rotation**: Verhindert volle Festplatten
---
## Troubleshooting
### Gateway startet nicht
- ✅ Prüfen: Consul erreichbar?
- ✅ Prüfen: Port 8081 frei?
- ✅ Prüfen: Keycloak erreichbar? (Optional)
### Service nicht erreichbar
- ✅ Prüfen: Service in Consul registriert?
- ✅ Prüfen: Circuit Breaker offen?
- ✅ Prüfen: Health Check erfolgreich?
### CORS-Fehler
- ✅ Prüfen: Origin in allowedOriginPatterns?
- ✅ Prüfen: Methode in allowedMethods?
- ✅ Prüfen: allowCredentials korrekt?
### Hohe Latenz
- ✅ Prüfen: response-timeout zu hoch?
- ✅ Prüfen: Backend-Services langsam?
- ✅ Prüfen: Connection Pool ausgeschöpft?
---
## Weitere Ressourcen
- [Gateway README](README-INFRA-GATEWAY.md)
- [Spring Cloud Gateway Dokumentation](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/)
- [Resilience4j Dokumentation](https://resilience4j.readme.io/)
- [Consul Dokumentation](https://www.consul.io/docs)
-1
View File
@@ -29,7 +29,6 @@ dependencies {
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics
implementation(libs.bundles.logging)
implementation(libs.bundles.jackson.kotlin)
implementation(project(":infrastructure:event-store:redis-event-store"))
// === Test Dependencies ===
testImplementation(projects.platform.platformTesting)
@@ -8,17 +8,13 @@ plugins {
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
tasks.bootJar {
enabled = false
}
// Deaktiviert bootRun und bootTestRun für dieses Bibliotheks-Modul, da es keine ausführbare Anwendung ist.
tasks.getByName("bootRun") {
enabled = false
}
tasks.getByName("bootTestRun") {
enabled = false
// Stellt sicher, dass stattdessen ein reguläres Jar gebaut wird
tasks.jar {
enabled = true
}
dependencies {
@@ -8,17 +8,13 @@ plugins {
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
tasks.bootJar {
enabled = false
}
// Deaktiviert bootRun und bootTestRun für dieses Bibliotheks-Modul, da es keine ausführbare Anwendung ist.
tasks.getByName("bootRun") {
enabled = false
}
tasks.getByName("bootTestRun") {
enabled = false
// Stellt sicher, dass stattdessen ein reguläres Jar gebaut wird
tasks.jar {
enabled = true
}
dependencies {