(fix) Umbau zu SCS

### API-Gateway erweitern
- Bestehenden API-Gateway-Service mit zusätzlichen Funktionen ausstatten:
    - Rate Limiting implementieren
    - Request/Response Logging verbessern
    - Cross-Service Tracing mit eindeutigen Request-IDs einführen
This commit is contained in:
stefan
2025-07-21 17:41:00 +02:00
parent f8eade8091
commit 834c92dcb2
2 changed files with 36 additions and 40 deletions
@@ -1,10 +1,13 @@
package at.mocode.gateway.config package at.mocode.gateway.config
import at.mocode.gateway.config.REQUEST_ID_KEY
import at.mocode.gateway.config.REQUEST_START_TIME_KEY
import at.mocode.shared.config.AppConfig import at.mocode.shared.config.AppConfig
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.calllogging.* import io.ktor.server.plugins.calllogging.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.* import io.ktor.util.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.event.Level import org.slf4j.event.Level
@@ -96,20 +99,9 @@ fun Application.configureLogSampling() {
} }
} }
// Modify the CallLogging plugin to respect the sampling decision // Instead of trying to modify CallLogging after installation,
environment.monitor.subscribe(CallLogging.LoggingConfig) { loggingConfig -> // we'll use the interceptor to decide if logging should happen
// Add a filter to the CallLogging plugin // The CallLogging plugin will be configured in MonitoringConfig.kt
loggingConfig.filter { call ->
// Check if the request should be logged based on sampling
val shouldLog = call.attributes.getOrNull(SHOULD_LOG_REQUEST_KEY) ?: true
// Apply the original filter as well (exclude paths)
val originalFilter = !AppConfig.logging.excludePaths.any { call.request.path().startsWith(it) }
// Only log if both filters pass
shouldLog && originalFilter
}
}
} }
/** /**
@@ -1,10 +1,12 @@
package at.mocode.gateway.config package at.mocode.gateway.config
import at.mocode.dto.base.ApiResponse import at.mocode.dto.base.ApiResponse
import at.mocode.gateway.config.REQUEST_ID_KEY
import at.mocode.gateway.config.REQUEST_START_TIME_KEY
import at.mocode.shared.config.AppConfig import at.mocode.shared.config.AppConfig
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.* import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.statuspages.* import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
@@ -125,13 +127,13 @@ fun Application.configureMonitoring() {
} }
// Filtere Pfade, die vom Logging ausgeschlossen werden sollen // Filtere Pfade, die vom Logging ausgeschlossen werden sollen
filter { call -> filter { call: ApplicationCall ->
val path = call.request.path() val path = call.request.path()
!loggingConfig.excludePaths.any { path.startsWith(it) } !loggingConfig.excludePaths.any { path.startsWith(it) }
} }
// Formatiere Log-Einträge mit erweitertem Format // Formatiere Log-Einträge mit erweitertem Format
format { call -> format { call: ApplicationCall ->
val status = call.response.status() val status = call.response.status()
val httpMethod = call.request.httpMethod.value val httpMethod = call.request.httpMethod.value
val path = call.request.path() val path = call.request.path()
@@ -140,7 +142,7 @@ fun Application.configureMonitoring() {
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) val timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// Get the request ID from the call attributes (set by RequestTracingConfig) // Get the request ID from the call attributes (set by RequestTracingConfig)
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
if (loggingConfig.useStructuredLogging) { if (loggingConfig.useStructuredLogging) {
// Strukturiertes Logging-Format // Strukturiertes Logging-Format
@@ -167,11 +169,11 @@ fun Application.configureMonitoring() {
// Log all headers if in debug mode, filtering sensitive data // Log all headers if in debug mode, filtering sensitive data
if (loggingConfig.level.equals("DEBUG", ignoreCase = true)) { if (loggingConfig.level.equals("DEBUG", ignoreCase = true)) {
append("headers={") append("headers={")
call.request.headers.entries().joinTo(this, ", ") { call.request.headers.entries().joinTo(this, ", ") { entry ->
if (isSensitiveHeader(it.key)) { if (isSensitiveHeader(entry.key)) {
"${it.key}=*****" "${entry.key}=*****"
} else { } else {
"${it.key}=${it.value.joinToString(",")}" "${entry.key}=${entry.value.joinToString(",")}"
} }
} }
append("} ") append("} ")
@@ -181,22 +183,24 @@ fun Application.configureMonitoring() {
// Log Query-Parameter wenn konfiguriert // Log Query-Parameter wenn konfiguriert
if (loggingConfig.logRequestParameters && call.request.queryParameters.entries().isNotEmpty()) { if (loggingConfig.logRequestParameters && call.request.queryParameters.entries().isNotEmpty()) {
append("params={") append("params={")
call.request.queryParameters.entries().joinTo(this, ", ") { call.request.queryParameters.entries().joinTo(this, ", ") { entry ->
if (isSensitiveParameter(it.key)) { if (isSensitiveParameter(entry.key)) {
"${it.key}=*****" "${entry.key}=*****"
} else { } else {
"${it.key}=${it.value.joinToString(",")}" "${entry.key}=${entry.value.joinToString(",")}"
} }
} }
append("} ") append("} ")
} }
if (userAgent != null) { if (userAgent != null) {
append("userAgent=\"${userAgent.replace("\"", "\\\"")}\" ") // Use a simpler approach to avoid escape sequence issues
val escapedUserAgent = userAgent.replace("\"", "\\\"")
append("userAgent=\"$escapedUserAgent\" ")
} }
// Log response time if available from RequestTracingConfig // Log response time if available from RequestTracingConfig
call.attributes.getOrNull(REQUEST_START_TIME_KEY)?.let { startTime -> call.attributes.getOrNull(REQUEST_START_TIME_KEY)?.let { startTime: Long ->
val duration = System.currentTimeMillis() - startTime val duration = System.currentTimeMillis() - startTime
append("duration=${duration}ms ") append("duration=${duration}ms ")
} }
@@ -215,8 +219,8 @@ fun Application.configureMonitoring() {
} }
} else { } else {
// Einfaches Logging-Format // Einfaches Logging-Format
val duration = call.attributes.getOrNull(REQUEST_START_TIME_KEY)?.let { val duration = call.attributes.getOrNull(REQUEST_START_TIME_KEY)?.let { startTime: Long ->
" - Duration: ${System.currentTimeMillis() - it}ms" " - Duration: ${System.currentTimeMillis() - startTime}ms"
} ?: "" } ?: ""
"$timestamp - $status: $httpMethod $path - RequestID: $requestId - $clientIp - $userAgent$duration" "$timestamp - $status: $httpMethod $path - RequestID: $requestId - $clientIp - $userAgent$duration"
@@ -235,9 +239,9 @@ fun Application.configureMonitoring() {
"propagateRequestId=${loggingConfig.propagateRequestId}") "propagateRequestId=${loggingConfig.propagateRequestId}")
install(StatusPages) { install(StatusPages) {
exception<Throwable> { call, cause -> exception<Throwable> { call: ApplicationCall, cause: Throwable ->
// Get the request ID for error logging // Get the request ID for error logging
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
call.application.log.error("Unhandled exception - RequestID: $requestId", cause) call.application.log.error("Unhandled exception - RequestID: $requestId", cause)
call.respond( call.respond(
@@ -246,9 +250,9 @@ fun Application.configureMonitoring() {
) )
} }
status(HttpStatusCode.NotFound) { call, status -> status(HttpStatusCode.NotFound) { call: ApplicationCall, status: HttpStatusCode ->
// Get the request ID for error logging // Get the request ID for error logging
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
call.application.log.warn("Not found - Path: ${call.request.path()} - RequestID: $requestId") call.application.log.warn("Not found - Path: ${call.request.path()} - RequestID: $requestId")
call.respond( call.respond(
@@ -257,9 +261,9 @@ fun Application.configureMonitoring() {
) )
} }
status(HttpStatusCode.Unauthorized) { call, status -> status(HttpStatusCode.Unauthorized) { call: ApplicationCall, status: HttpStatusCode ->
// Get the request ID for error logging // Get the request ID for error logging
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
call.application.log.warn("Unauthorized access - Path: ${call.request.path()} - RequestID: $requestId") call.application.log.warn("Unauthorized access - Path: ${call.request.path()} - RequestID: $requestId")
call.respond( call.respond(
@@ -268,9 +272,9 @@ fun Application.configureMonitoring() {
) )
} }
status(HttpStatusCode.Forbidden) { call, status -> status(HttpStatusCode.Forbidden) { call: ApplicationCall, status: HttpStatusCode ->
// Get the request ID for error logging // Get the request ID for error logging
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
call.application.log.warn("Forbidden access - Path: ${call.request.path()} - RequestID: $requestId") call.application.log.warn("Forbidden access - Path: ${call.request.path()} - RequestID: $requestId")
call.respond( call.respond(
@@ -280,9 +284,9 @@ fun Application.configureMonitoring() {
} }
// Rate limit exceeded // Rate limit exceeded
status(HttpStatusCode.TooManyRequests) { call, status -> status(HttpStatusCode.TooManyRequests) { call: ApplicationCall, status: HttpStatusCode ->
// Get the request ID for error logging // Get the request ID for error logging
val requestId = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id" val requestId: String = call.attributes.getOrNull(REQUEST_ID_KEY) ?: "no-request-id"
call.application.log.warn("Rate limit exceeded - Path: ${call.request.path()} - RequestID: $requestId") call.application.log.warn("Rate limit exceeded - Path: ${call.request.path()} - RequestID: $requestId")
call.respond( call.respond(