refactoring infrastructure
This commit is contained in:
@@ -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
@@ -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 ===
|
||||
|
||||
+251
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user