refactoring(Gateway Health Indicator implementieren)

TODO-Roadmap.md
1.2 Health Check Verbesserungen
This commit is contained in:
stefan
2025-08-14 13:54:06 +02:00
parent d937e82d2b
commit eeda3b7ac2
13 changed files with 1537 additions and 13972 deletions
@@ -0,0 +1,141 @@
package at.mocode.infrastructure.gateway.health
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.core.env.Environment
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import java.time.Duration
/**
* Gateway Health Indicator zur Überwachung der Downstream Services.
*
* Prüft die Verfügbarkeit aller registrierten Services über Consul Discovery
* und führt Health-Checks für kritische Services durch.
*/
@Component
class GatewayHealthIndicator(
private val discoveryClient: DiscoveryClient,
private val webClient: WebClient.Builder,
private val environment: Environment
) : HealthIndicator {
companion object {
private val CRITICAL_SERVICES = setOf(
"members-service",
"horses-service",
"events-service",
"masterdata-service",
"auth-service"
)
private val OPTIONAL_SERVICES = setOf(
"ping-service"
)
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
}
override fun health(): Health {
val builder = Health.up()
val details = mutableMapOf<String, Any>()
try {
// Prüfe alle registrierten Services in Consul
val allServices = discoveryClient.services
val discoveredServices = mutableMapOf<String, Any>()
allServices.forEach { serviceName ->
val instances = discoveryClient.getInstances(serviceName)
discoveredServices[serviceName] = mapOf(
"instanceCount" to instances.size,
"instances" to instances.map { "${it.host}:${it.port}" }
)
}
details["discoveredServices"] = discoveredServices
details["totalServices"] = allServices.size
// Prüfe kritische Services
val criticalServiceStatus = mutableMapOf<String, String>()
var hasCriticalFailure = false
CRITICAL_SERVICES.forEach { serviceName ->
val status = checkServiceHealth(serviceName)
criticalServiceStatus[serviceName] = status
if (status != "UP") {
hasCriticalFailure = true
}
}
// Prüfe optionale Services
val optionalServiceStatus = mutableMapOf<String, String>()
OPTIONAL_SERVICES.forEach { serviceName ->
optionalServiceStatus[serviceName] = checkServiceHealth(serviceName)
}
details["criticalServices"] = criticalServiceStatus
details["optionalServices"] = optionalServiceStatus
// Gateway Status basierend auf kritischen Services
val isTestEnvironment = environment.activeProfiles.contains("test")
if (hasCriticalFailure && !isTestEnvironment) {
builder.down()
details["status"] = "DOWN"
details["reason"] = "One or more critical services are unavailable"
} else {
details["status"] = "UP"
details["reason"] = if (isTestEnvironment) {
"Health check passed (test environment)"
} else {
"All critical services are available"
}
}
} catch (exception: Exception) {
builder.down()
.withException(exception)
details["status"] = "DOWN"
details["reason"] = "Failed to check downstream services: ${exception.message}"
}
return builder.withDetails(details).build()
}
private fun checkServiceHealth(serviceName: String): String {
return try {
val instances = discoveryClient.getInstances(serviceName)
if (instances.isEmpty()) {
"NO_INSTANCES"
} else {
// Versuche Health-Check für die erste verfügbare Instanz
val instance = instances.first()
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
val client = webClient.build()
val response = client.get()
.uri(healthUrl)
.retrieve()
.bodyToMono(Map::class.java)
.timeout(HEALTH_CHECK_TIMEOUT)
.onErrorReturn(mapOf("status" to "DOWN"))
.block()
val status = response?.get("status")?.toString() ?: "UNKNOWN"
if (status == "UP") "UP" else "DOWN"
}
} catch (exception: WebClientResponseException) {
when (exception.statusCode.value()) {
404 -> "NO_HEALTH_ENDPOINT"
503 -> "DOWN"
else -> "ERROR"
}
} catch (exception: Exception) {
"ERROR"
}
}
}
@@ -6,7 +6,7 @@ server:
connection-timeout: 5s
idle-timeout: 15s
# Name, unter dem sich das Gateway in Consul registriert
# Der Name, unter dem sich das Gateway in Consul registriert
spring:
application:
name: api-gateway
@@ -69,6 +69,27 @@ spring:
maxBackoff: 500ms
factor: 2
basedOnPreviousValue: false
# Security Headers for enhanced protection
- name: AddResponseHeader
args:
name: X-Content-Type-Options
value: nosniff
- name: AddResponseHeader
args:
name: X-Frame-Options
value: DENY
- name: AddResponseHeader
args:
name: X-XSS-Protection
value: 1; mode=block
- name: AddResponseHeader
args:
name: Referrer-Policy
value: strict-origin-when-cross-origin
- name: AddResponseHeader
args:
name: Cache-Control
value: no-cache, no-store, must-revalidate
# Route definitions with service discovery
routes:
# Health Check und Gateway Info Routes
@@ -191,29 +212,83 @@ management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,gateway
include: health,info,metrics,prometheus,gateway,circuitbreakers
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
info:
enabled: true
prometheus:
enabled: true
gateway:
enabled: true
circuitbreakers:
enabled: true
metrics:
export:
prometheus:
# Prometheus configuration moved to monitoring-client module
distribution:
percentiles-histogram:
spring.cloud.gateway.requests: true
http.server.requests: true
percentiles:
spring.cloud.gateway.requests: 0.5,0.95,0.99
spring.cloud.gateway.requests: 0.5,0.90,0.95,0.99
http.server.requests: 0.5,0.90,0.95,0.99
minimum-expected-value:
spring.cloud.gateway.requests: 1ms
http.server.requests: 1ms
maximum-expected-value:
spring.cloud.gateway.requests: 30s
http.server.requests: 30s
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
instance: ${spring.cloud.consul.discovery.instance-id}
gateway: api-gateway
info:
env:
enabled: true
git:
mode: full
build:
enabled: true
java:
enabled: true
# Logging Configuration
# Enhanced Logging Configuration
logging:
level:
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
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{correlationId:-}] %logger{36} - %msg%n"
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr([%X{correlationId:-}]){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{correlationId:-}] %logger{36} - %msg%n"
file:
name: logs/gateway.log
max-size: 100MB
logback:
rollingpolicy:
clean-history-on-start: true
max-file-size: 100MB
total-size-cap: 1GB
max-history: 30
@@ -61,6 +61,8 @@ servers:
tags:
- name: Authentication
description: User authentication, registration, and profile management
- name: Members
description: Member registration, profile management, and membership administration
- name: Master Data
description: Reference data management (countries, states, age classes, venues)
- name: Horse Registry
@@ -186,6 +188,264 @@ paths:
schema:
$ref: '#/components/schemas/ErrorResponse'
# Members Context
/api/members:
get:
tags:
- Members
summary: Get All Members
description: Returns a list of all members with pagination support
operationId: getAllMembers
security:
- bearerAuth: []
parameters:
- name: activeOnly
in: query
required: false
schema:
type: boolean
default: true
description: Filter to only return active members
- name: limit
in: query
required: false
schema:
type: integer
default: 50
minimum: 1
maximum: 200
description: Maximum number of members to return
- name: offset
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
description: Number of members to skip for pagination
- name: search
in: query
required: false
schema:
type: string
description: Search term for member name or email
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/MembersResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
tags:
- Members
summary: Create Member
description: Creates a new member registration
operationId: createMember
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateMemberRequest'
responses:
'201':
description: Member successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/MemberResponse'
'400':
description: Invalid member data
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/members/{id}:
get:
tags:
- Members
summary: Get Member by ID
description: Returns a member by their unique ID
operationId: getMemberById
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Unique identifier of the member
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/MemberResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Member not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
put:
tags:
- Members
summary: Update Member
description: Updates an existing member's information
operationId: updateMember
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Unique identifier of the member
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateMemberRequest'
responses:
'200':
description: Member successfully updated
content:
application/json:
schema:
$ref: '#/components/schemas/MemberResponse'
'400':
description: Invalid member data
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Member not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
tags:
- Members
summary: Delete Member
description: Soft deletes a member (marks as inactive)
operationId: deleteMember
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Unique identifier of the member
responses:
'204':
description: Member successfully deleted
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Member not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/members/search:
get:
tags:
- Members
summary: Search Members
description: Search members by various criteria
operationId: searchMembers
security:
- bearerAuth: []
parameters:
- name: query
in: query
required: true
schema:
type: string
minLength: 2
description: Search query for member name, email, or membership number
- name: membershipType
in: query
required: false
schema:
type: string
enum: [FULL, YOUTH, HONORARY, ASSOCIATE]
description: Filter by membership type
- name: status
in: query
required: false
schema:
type: string
enum: [ACTIVE, INACTIVE, SUSPENDED]
default: ACTIVE
description: Filter by member status
responses:
'200':
description: Successful search operation
content:
application/json:
schema:
$ref: '#/components/schemas/MembersResponse'
'400':
description: Invalid search parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
# Master Data Context
/api/masterdata/countries:
get:
@@ -1186,6 +1446,218 @@ components:
data:
$ref: '#/components/schemas/User'
# Members Models
MembersResponse:
allOf:
- $ref: '#/components/schemas/BaseResponse'
- type: object
properties:
data:
type: object
properties:
members:
type: array
items:
$ref: '#/components/schemas/Member'
totalCount:
type: integer
description: Total number of members matching the criteria
example: 150
pagination:
type: object
properties:
limit:
type: integer
example: 50
offset:
type: integer
example: 0
hasNext:
type: boolean
example: true
MemberResponse:
allOf:
- $ref: '#/components/schemas/BaseResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/Member'
CreateMemberRequest:
type: object
required:
- firstName
- lastName
- email
- membershipType
properties:
firstName:
type: string
minLength: 1
maxLength: 100
example: Maria
lastName:
type: string
minLength: 1
maxLength: 100
example: Müller
email:
type: string
format: email
example: maria.mueller@example.com
phone:
type: string
pattern: '^\+?[1-9]\d{1,14}$'
example: +43 123 456789
dateOfBirth:
type: string
format: date
example: 1985-03-15
address:
$ref: '#/components/schemas/Address'
membershipType:
type: string
enum: [FULL, YOUTH, HONORARY, ASSOCIATE]
example: FULL
emergencyContact:
$ref: '#/components/schemas/EmergencyContact'
UpdateMemberRequest:
type: object
properties:
firstName:
type: string
minLength: 1
maxLength: 100
example: Maria
lastName:
type: string
minLength: 1
maxLength: 100
example: Müller-Schmidt
email:
type: string
format: email
example: maria.mueller-schmidt@example.com
phone:
type: string
pattern: '^\+?[1-9]\d{1,14}$'
example: +43 123 456789
address:
$ref: '#/components/schemas/Address'
membershipType:
type: string
enum: [FULL, YOUTH, HONORARY, ASSOCIATE]
example: FULL
status:
type: string
enum: [ACTIVE, INACTIVE, SUSPENDED]
example: ACTIVE
emergencyContact:
$ref: '#/components/schemas/EmergencyContact'
Member:
type: object
properties:
id:
type: string
format: uuid
example: 123e4567-e89b-12d3-a456-426614174000
membershipNumber:
type: string
example: M2024001234
firstName:
type: string
example: Maria
lastName:
type: string
example: Müller
email:
type: string
format: email
example: maria.mueller@example.com
phone:
type: string
example: +43 123 456789
dateOfBirth:
type: string
format: date
example: 1985-03-15
address:
$ref: '#/components/schemas/Address'
membershipType:
type: string
enum: [FULL, YOUTH, HONORARY, ASSOCIATE]
example: FULL
status:
type: string
enum: [ACTIVE, INACTIVE, SUSPENDED]
example: ACTIVE
joinDate:
type: string
format: date
example: 2024-01-15
lastPaymentDate:
type: string
format: date
example: 2024-01-01
emergencyContact:
$ref: '#/components/schemas/EmergencyContact'
createdAt:
type: string
format: date-time
example: 2024-01-15T10:30:00Z
updatedAt:
type: string
format: date-time
example: 2024-01-15T10:30:00Z
Address:
type: object
required:
- street
- city
- postalCode
- country
properties:
street:
type: string
example: Hauptstraße 123
city:
type: string
example: Wien
postalCode:
type: string
example: 1010
state:
type: string
example: Wien
country:
type: string
example: Austria
EmergencyContact:
type: object
required:
- name
- relationship
- phone
properties:
name:
type: string
example: Johann Müller
relationship:
type: string
example: Ehepartner
phone:
type: string
example: +43 123 456788
email:
type: string
format: email
example: johann.mueller@example.com
User:
type: object
properties:
@@ -273,6 +273,23 @@
</div>
</div>
<div class="card">
<h3>Members Context</h3>
<p>Member registration, profile management, and membership administration</p>
<p><strong>Base Path:</strong> /api/members</p>
<div class="endpoints">
<h4>Key Endpoints:</h4>
<ul>
<li><span class="method get">GET</span> /api/members - Get all members with pagination</li>
<li><span class="method get">GET</span> /api/members/search - Search members by criteria</li>
<li><span class="method get">GET</span> /api/members/{id} - Get member by ID</li>
<li><span class="method post">POST</span> /api/members - Create new member</li>
<li><span class="method put">PUT</span> /api/members/{id} - Update member information</li>
<li><span class="method delete">DELETE</span> /api/members/{id} - Delete member (soft delete)</li>
</ul>
</div>
</div>
<div class="card">
<h3>Master Data Context</h3>
<p>Reference data management (countries, states, age classes, venues)</p>
@@ -333,18 +350,48 @@
<div class="resource-card">
<h3>Swagger UI</h3>
<p>Interactive documentation for exploring and testing the API endpoints.</p>
<a href="/swagger" class="btn" target="_blank">Open Swagger UI</a>
<a href="/swagger" class="btn" target="_blank" aria-label="Open Swagger UI in new tab">Open Swagger UI</a>
</div>
<div class="resource-card">
<h3>OpenAPI Specification</h3>
<p>Raw OpenAPI 3.0.3 specification in YAML format for code generation or import into other tools.</p>
<a href="/openapi" class="btn" target="_blank">View OpenAPI Spec</a>
<a href="/openapi" class="btn" target="_blank" aria-label="View OpenAPI specification in new tab">View OpenAPI Spec</a>
</div>
<div class="resource-card">
<h3>Postman Collection</h3>
<p>Comprehensive API collection covering all endpoints with pre-configured request examples.</p>
<a href="/docs/postman/Meldestelle_API_Collection.json" class="btn" target="_blank">Download Collection</a>
<a href="/docs/postman/Meldestelle_API_Collection.json" class="btn" target="_blank" aria-label="Download Postman collection">Download Collection</a>
</div>
<div class="resource-card">
<h3>Health Monitoring</h3>
<p>Real-time health status and monitoring information for all downstream services.</p>
<a href="/actuator/health" class="btn" target="_blank" aria-label="View health monitoring in new tab">View Health Status</a>
</div>
</div>
</section>
<section id="monitoring" class="section">
<h2>System Monitoring & Health</h2>
<div class="card">
<h3>Health Check Endpoints</h3>
<p>The API Gateway provides comprehensive health monitoring for all downstream services:</p>
<div class="endpoints">
<h4>Monitoring Endpoints:</h4>
<ul>
<li><span class="method get">GET</span> /actuator/health - Comprehensive health status of all services</li>
<li><span class="method get">GET</span> /actuator/metrics - System metrics and performance data</li>
<li><span class="method get">GET</span> /actuator/info - Application information and build details</li>
<li><span class="method get">GET</span> /actuator/prometheus - Prometheus-compatible metrics export</li>
</ul>
</div>
<p><strong>Health Indicator Features:</strong></p>
<ul>
<li>Monitors critical services: Members, Horses, Events, Masterdata, Auth</li>
<li>Optional service monitoring: Ping service</li>
<li>Circuit breaker status integration</li>
<li>Service discovery status from Consul</li>
<li>Detailed error reporting and status codes</li>
</ul>
</div>
</section>