mirror of
https://github.com/2martens/tsw-timetable.git
synced 2026-10-17 16:06:51 +02:00
feat: Support CRUD functionality (#7)
The station search implementation only works via DB and is very slow. For a simple query, the answer takes about 1s or longer. That is 100 times slower than it should be. For now, however, this solution is adequate to achieve a first prototype that includes the core functionality. feat: Add station search
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import de.twomartens.support.property.TimeProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class ClockConfiguration(private val properties: TimeProperties) {
|
||||
|
||||
@Bean
|
||||
open fun clock(): Clock {
|
||||
return Clock.system(properties.defaultTimeZone)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter
|
||||
|
||||
@Configuration
|
||||
open class FilterConfiguration {
|
||||
@Bean
|
||||
open fun forwardedFilter(): ForwardedHeaderFilter {
|
||||
return ForwardedHeaderFilter()
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import de.twomartens.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.support.interceptor.LoggingInterceptorRest
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class InterceptorConfiguration {
|
||||
@Bean
|
||||
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
|
||||
return LoggingInterceptorRest(clock)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun headerInterceptorRest(): HeaderInterceptorRest {
|
||||
return HeaderInterceptorRest()
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import de.twomartens.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.support.interceptor.LoggingInterceptorRest
|
||||
import de.twomartens.support.property.RestTemplateTimeoutProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter
|
||||
import org.springframework.web.client.RestClient
|
||||
|
||||
@Configuration
|
||||
open class RestClientConfiguration {
|
||||
@Bean("restClient")
|
||||
open fun restClient(
|
||||
headerInterceptorRest: HeaderInterceptorRest,
|
||||
loggingInterceptor: LoggingInterceptorRest,
|
||||
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||
): RestClient {
|
||||
return RestClient.builder()
|
||||
.messageConverters { it.add(Jaxb2RootElementHttpMessageConverter()) }
|
||||
.requestInterceptors { listOf(headerInterceptorRest, loggingInterceptor) }
|
||||
.build()
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import de.twomartens.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.support.interceptor.LoggingInterceptorRest
|
||||
import de.twomartens.support.property.RestTemplateTimeoutProperties
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.client.RestTemplate
|
||||
|
||||
@Configuration
|
||||
open class RestTemplateConfiguration {
|
||||
@Bean("restTemplate")
|
||||
open fun restTemplate(
|
||||
headerInterceptorRest: HeaderInterceptorRest,
|
||||
loggingInterceptor: LoggingInterceptorRest,
|
||||
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||
): RestTemplate {
|
||||
return RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestTemplateTimeoutInMillis)
|
||||
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestTemplateInMillis)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean("restTemplateRestHealthIndicator")
|
||||
open fun restTemplateRestHealthIndicator(
|
||||
headerInterceptorRest: HeaderInterceptorRest,
|
||||
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||
): RestTemplate {
|
||||
return RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestHealthIndicatorTimeoutInMillis)
|
||||
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestHealthIndicatorInMillis)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package de.twomartens.support.configuration
|
||||
|
||||
import de.twomartens.support.monitoring.statusprobe.CountBasedStatusProbe
|
||||
import de.twomartens.support.monitoring.statusprobe.StatusProbe
|
||||
import de.twomartens.support.monitoring.statusprobe.StatusProbeCriticality
|
||||
import de.twomartens.support.monitoring.statusprobe.StatusProbeLogger
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class StatusProbeConfiguration(private val clock: Clock) {
|
||||
@Bean
|
||||
open fun statusProbeLogger(): StatusProbeLogger {
|
||||
return StatusProbeLogger(clock)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
|
||||
return CountBasedStatusProbe(
|
||||
1,
|
||||
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package de.twomartens.support.controller
|
||||
|
||||
import de.twomartens.support.exception.HttpStatusException
|
||||
import de.twomartens.support.model.dto.ErrorMessage
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
|
||||
|
||||
@ControllerAdvice(annotations = [RestController::class])
|
||||
class ExceptionController : ResponseEntityExceptionHandler() {
|
||||
@ExceptionHandler(HttpStatusException::class)
|
||||
fun handleException(e: HttpStatusException): ResponseEntity<ErrorMessage> {
|
||||
if (e.cause != null) {
|
||||
log.info(e.cause.toString(), e.cause)
|
||||
} else {
|
||||
log.info(e.toString())
|
||||
}
|
||||
return ResponseEntity.status(e.status)
|
||||
.body(ErrorMessage(e.message!!))
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException::class)
|
||||
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
|
||||
log.error("unexpected exception occurred", e)
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ErrorMessage(message = e.message!!))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package de.twomartens.support.controller
|
||||
|
||||
import de.twomartens.support.property.HealthCheckProperties
|
||||
import io.swagger.v3.oas.annotations.Hidden
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/timetable"])
|
||||
class HealthCheckController(private val properties: HealthCheckProperties) {
|
||||
|
||||
|
||||
@Hidden
|
||||
@GetMapping("/healthCheck")
|
||||
fun healthCheck(message: String): String {
|
||||
return properties.greeting.format(message)
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package de.twomartens.support.controller
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.ModelAttribute
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Paths
|
||||
import java.util.jar.JarFile
|
||||
|
||||
@Controller
|
||||
@RequestMapping(value = ["/timetable"])
|
||||
class VersionHtmlController {
|
||||
@GetMapping(path = ["/version"])
|
||||
fun version(): String {
|
||||
return "version"
|
||||
}
|
||||
|
||||
@ModelAttribute("version")
|
||||
private fun getApplicationVersion(): String {
|
||||
return "${getTitle()} ${getVersion()}"
|
||||
}
|
||||
|
||||
@ModelAttribute("footerString")
|
||||
private fun getApplicationVersion(@RequestHeader("host") hostName: String): String {
|
||||
return "${getTitle()} ${getVersion()} - $hostName"
|
||||
}
|
||||
|
||||
private fun getTitle(): String {
|
||||
val title = VersionHtmlController::class.java.`package`.implementationTitle
|
||||
?: return "application"
|
||||
return title.ifBlank {
|
||||
"application"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVersion(): String {
|
||||
val version = VersionHtmlController::class.java.`package`.implementationVersion
|
||||
?: return "DEVELOPER"
|
||||
return version.ifBlank {
|
||||
"DEVELOPER"
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute("hostname")
|
||||
private fun getHostname(): String {
|
||||
try {
|
||||
return InetAddress.getLocalHost().hostName
|
||||
} catch (e: UnknownHostException) {
|
||||
log.warn(e.toString(), e)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ModelAttribute("manifest")
|
||||
private fun getManifest(): Collection<String> {
|
||||
try {
|
||||
val location = javaClass.getProtectionDomain().codeSource.location
|
||||
val jarFileName = Paths.get(location.toURI()).toString()
|
||||
JarFile(jarFileName).use { jarFile ->
|
||||
val entry = jarFile.getEntry(JarFile.MANIFEST_NAME)
|
||||
jarFile.getInputStream(entry).use { `in` ->
|
||||
return String(`in`.readAllBytes(), StandardCharsets.UTF_8).lines().toList()
|
||||
}
|
||||
}
|
||||
} catch (ignored: FileNotFoundException) {
|
||||
// do nothing if manifest file is not available
|
||||
} catch (e: Exception) {
|
||||
log.info(e.toString(), e)
|
||||
}
|
||||
return listOf("${getTitle()} ${getVersion()}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.twomartens.support.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
class HttpStatusException(val status: HttpStatus, message: String, cause: Throwable?) :
|
||||
RuntimeException(message, cause)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package de.twomartens.support.interceptor
|
||||
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatusCode
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.util.StreamUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class BufferingClientHttpResponseWrapper(private val response: ClientHttpResponse) : ClientHttpResponse {
|
||||
private var body: ByteArray? = null
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getStatusCode(): HttpStatusCode {
|
||||
return response.statusCode
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getStatusText(): String {
|
||||
return response.statusText
|
||||
}
|
||||
|
||||
override fun getHeaders(): HttpHeaders {
|
||||
return response.headers
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getBody(): InputStream {
|
||||
if (body == null) {
|
||||
body = StreamUtils.copyToByteArray(response.body)
|
||||
}
|
||||
return ByteArrayInputStream(body)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.twomartens.support.interceptor
|
||||
|
||||
import org.slf4j.MDC
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
|
||||
abstract class HeaderInterceptor {
|
||||
companion object {
|
||||
const val LOGGER_TRACE_ID = "trace.id"
|
||||
const val LOGGER_REQTYPE_ID = "REQTYPE"
|
||||
|
||||
const val HEADER_FIELD_TRACE_ID = "X-TraceId"
|
||||
const val HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid"
|
||||
const val HEADER_FIELD_TYPE_ID = "x-type"
|
||||
|
||||
const val REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK"
|
||||
const val REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST"
|
||||
const val REQ_TYPE_SERVER_TEST = "SERVER_TEST"
|
||||
const val REQ_TYPE_WARMUP = "WARMUP"
|
||||
|
||||
fun createNewTraceId(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
fun getTraceId(): String {
|
||||
val traceId = MDC.get(LOGGER_TRACE_ID)
|
||||
return if (traceId.isNullOrBlank()) createNewTraceId() else traceId
|
||||
}
|
||||
|
||||
fun getRequestType(): String? {
|
||||
val type = MDC.get(LOGGER_REQTYPE_ID)
|
||||
return if (type.isNullOrBlank()) null else type
|
||||
}
|
||||
|
||||
private fun setTraceId(traceId: String): InterceptorCloseables {
|
||||
return InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId))
|
||||
}
|
||||
|
||||
private fun mark(requestType: String?): InterceptorCloseables {
|
||||
return InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType))
|
||||
}
|
||||
|
||||
fun set(traceId: String, requestType: String?): InterceptorCloseables {
|
||||
return if (requestType != null) {
|
||||
InterceptorCloseables(setTraceId(traceId), mark(requestType))
|
||||
} else setTraceId(traceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun markAsHealthCheck(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsIntegrationTest(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsServerTest(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsWarmup(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
class InterceptorCloseables(vararg closeables: Closeable) : Closeable {
|
||||
private val closeables: Array<out Closeable>
|
||||
|
||||
init {
|
||||
this.closeables = closeables
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closeables.forEach {
|
||||
try {
|
||||
it.close()
|
||||
} catch (ignored: Exception) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package de.twomartens.support.interceptor
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class HeaderInterceptorRest : HeaderInterceptor(), HandlerInterceptor, ClientHttpRequestInterceptor {
|
||||
companion object {
|
||||
val CLASS_NAME: String = HeaderInterceptorRest::class.java.getName()
|
||||
|
||||
fun extractTraceId(request: HttpServletRequest): String {
|
||||
var traceId = request.getHeader(HEADER_FIELD_TRACE_ID)
|
||||
if (traceId.isNullOrBlank()) traceId = request.getHeader(HEADER_FIELD_B3_TRACE_ID)
|
||||
if (traceId.isNullOrBlank()) return createNewTraceId()
|
||||
return traceId
|
||||
}
|
||||
|
||||
fun extractTraceId(request: HttpRequest): String {
|
||||
var traceId = request.headers[HEADER_FIELD_TRACE_ID]?.first()
|
||||
if (traceId.isNullOrBlank()) traceId = request.headers[HEADER_FIELD_B3_TRACE_ID]?.first()
|
||||
if (traceId.isNullOrBlank()) return UUID.randomUUID().toString()
|
||||
return traceId
|
||||
}
|
||||
|
||||
fun extractRequestType(request: HttpServletRequest): String? {
|
||||
val type = request.getHeader(HEADER_FIELD_TYPE_ID)
|
||||
if (type.isNullOrBlank()) return null
|
||||
return type
|
||||
}
|
||||
|
||||
fun extractRequestType(request: HttpRequest): String? {
|
||||
val type = request.headers[HEADER_FIELD_TYPE_ID]?.first()
|
||||
if (type.isNullOrBlank()) return null
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
// ClientHttpRequestInterceptor
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(
|
||||
request: HttpRequest, body: ByteArray,
|
||||
execution: ClientHttpRequestExecution
|
||||
): ClientHttpResponse {
|
||||
request.headers.add(HEADER_FIELD_TRACE_ID, getTraceId())
|
||||
val requestType = getRequestType()
|
||||
if (requestType != null) {
|
||||
request.headers.add(HEADER_FIELD_TYPE_ID, requestType)
|
||||
}
|
||||
|
||||
return try {
|
||||
execution.execute(request, body)
|
||||
} finally {
|
||||
request.headers.remove(HEADER_FIELD_TRACE_ID)
|
||||
request.headers.remove(HEADER_FIELD_TYPE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
override fun preHandle(
|
||||
request: HttpServletRequest, response: HttpServletResponse,
|
||||
handler: Any
|
||||
): Boolean {
|
||||
val traceId = extractTraceId(request)
|
||||
val requestType = extractRequestType(request)
|
||||
val closeable = set(traceId, requestType)
|
||||
request.setAttribute(CLASS_NAME, closeable)
|
||||
return true
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
// override fun postHandle(
|
||||
// request: HttpServletRequest,
|
||||
// response: HttpServletResponse,
|
||||
// handler: Any,
|
||||
// modelAndView: ModelAndView?
|
||||
// ) {
|
||||
// val obj = request.getAttribute(CLASS_NAME)
|
||||
// if (obj != null && obj is InterceptorCloseables) {
|
||||
// obj.close()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
package de.twomartens.support.interceptor
|
||||
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import mu.KotlinLogging
|
||||
import org.apache.logging.log4j.message.StringMapMessage
|
||||
import org.slf4j.Marker
|
||||
import org.slf4j.MarkerFactory
|
||||
import org.springframework.http.*
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.util.StreamUtils
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
|
||||
class LoggingInterceptorRest(
|
||||
private val requestLogBehaviour: FieldLogBehaviour,
|
||||
private val responseLogBehaviour: FieldLogBehaviour,
|
||||
val clock: Clock
|
||||
) : Filter, ClientHttpRequestInterceptor {
|
||||
|
||||
constructor(clock: Clock) : this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock)
|
||||
|
||||
@Throws(IOException::class, ServletException::class)
|
||||
override fun doFilter(
|
||||
request: ServletRequest, response: ServletResponse,
|
||||
chain: FilterChain
|
||||
) {
|
||||
val requestTime = ZonedDateTime.now(clock)
|
||||
val httpRequest = request as HttpServletRequest
|
||||
val requestWrapper = ContentCachingRequestWrapper(httpRequest)
|
||||
val responseWrapper = ContentCachingResponseWrapper(
|
||||
(response as HttpServletResponse)
|
||||
)
|
||||
var fullResponseBytes: ByteArray? = null
|
||||
var throwable: Throwable? = null
|
||||
var responseBody: String? = null
|
||||
var httpStatusCode = -1
|
||||
try {
|
||||
try {
|
||||
chain.doFilter(
|
||||
if (requestLogBehaviour != FieldLogBehaviour.NEVER) requestWrapper else httpRequest,
|
||||
responseWrapper
|
||||
)
|
||||
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
|
||||
fullResponseBytes = responseWrapper.contentAsByteArray
|
||||
}
|
||||
httpStatusCode = responseWrapper.status
|
||||
} finally {
|
||||
responseWrapper.copyBodyToResponse()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throwable = e
|
||||
throw e
|
||||
} finally {
|
||||
try {
|
||||
val responseSize = responseWrapper.contentSize
|
||||
val responseHeaders = extractHeaders(
|
||||
headerNames = responseWrapper.headerNames.iterator()
|
||||
) { responseWrapper.getHeaders(it).iterator() }
|
||||
if (
|
||||
(responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS)
|
||||
&& fullResponseBytes != null
|
||||
) {
|
||||
responseBody = String(
|
||||
fullResponseBytes,
|
||||
determineResponseEncoding()
|
||||
)
|
||||
}
|
||||
val query = if (httpRequest.queryString != null) "?${httpRequest.queryString}" else ""
|
||||
val requestUrl = URI.create("${httpRequest.requestURL}$query").toURL()
|
||||
val requestHeaders = extractHeaders(
|
||||
headerNames = httpRequest.headerNames.asIterator(),
|
||||
ignoreList = listOf("authorization")) {
|
||||
httpRequest.getHeaders(it).asIterator()
|
||||
}
|
||||
var requestBody: String? = null
|
||||
var businessType: String? = null
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
val fullRequestBytes = requestWrapper.contentAsByteArray
|
||||
requestBody = String(fullRequestBytes, determineRequestEncoding())
|
||||
businessType = determineBusinessType()
|
||||
}
|
||||
log(
|
||||
LogMessage(
|
||||
requestHeaders = requestHeaders,
|
||||
responseHeaders = responseHeaders,
|
||||
url = requestUrl,
|
||||
method = httpRequest.method,
|
||||
requestMimeType = typeToString(request.getContentType()),
|
||||
responseMimeType = typeToString(response.getContentType()),
|
||||
requestBody = requestBody,
|
||||
responseBody = responseBody,
|
||||
requestSize = httpRequest.contentLength,
|
||||
responseSize = responseSize,
|
||||
httpStatus = httpStatusCode,
|
||||
direction = DIRECTION_IN,
|
||||
requestTime = requestTime,
|
||||
responseTime = ZonedDateTime.now(clock),
|
||||
traceId = HeaderInterceptor.getTraceId(),
|
||||
requestType = HeaderInterceptor.getRequestType(),
|
||||
businessType = businessType,
|
||||
throwable = throwable
|
||||
)
|
||||
)
|
||||
val interceptorCloseables =
|
||||
request.getAttribute(HeaderInterceptorRest.CLASS_NAME) as HeaderInterceptor.InterceptorCloseables
|
||||
interceptorCloseables.close()
|
||||
} catch (e: java.lang.RuntimeException) {
|
||||
log.error(e.toString(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isError(httpStatusCode: Int): Boolean {
|
||||
return httpStatusCode in 400..599
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(
|
||||
request: HttpRequest,
|
||||
requestBytes: ByteArray, execution: ClientHttpRequestExecution
|
||||
): ClientHttpResponse {
|
||||
val requestTime = ZonedDateTime.now(clock)
|
||||
var responseSize = 0
|
||||
var responseHeaders: Map<String, Collection<String>> = emptyMap()
|
||||
var responseMediaType: MediaType? = null
|
||||
var httpStatusCode = -1
|
||||
var throwable: Throwable? = null
|
||||
var requestBody: String? = null
|
||||
var responseBody: String? = null
|
||||
var businessType: String? = null
|
||||
return try {
|
||||
val result = BufferingClientHttpResponseWrapper(
|
||||
execution.execute(request, requestBytes)
|
||||
)
|
||||
val responseBytes = StreamUtils.copyToByteArray(result.getBody())
|
||||
responseSize = responseBytes.size
|
||||
responseHeaders = extractHeaders(result.headers)
|
||||
responseMediaType = result.headers.getContentType()
|
||||
httpStatusCode = result.statusCode.value()
|
||||
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
responseBody = String(
|
||||
responseBytes,
|
||||
determineRequestEncoding()
|
||||
)
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
throwable = e
|
||||
throw e
|
||||
} finally {
|
||||
try {
|
||||
val url = request.uri.toURL()
|
||||
val requestHeaders: Map<String, Collection<String>> = extractHeaders(request.headers)
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
requestBody = String(
|
||||
requestBytes,
|
||||
determineRequestEncoding()
|
||||
)
|
||||
businessType = determineBusinessType()
|
||||
}
|
||||
log(
|
||||
LogMessage(
|
||||
requestHeaders = requestHeaders,
|
||||
responseHeaders = responseHeaders,
|
||||
url = url,
|
||||
method = request.method.name(),
|
||||
requestMimeType = typeToString(request.headers.getContentType()),
|
||||
requestBody = requestBody,
|
||||
responseBody = responseBody,
|
||||
responseMimeType = typeToString(responseMediaType!!),
|
||||
requestSize = requestBytes.size,
|
||||
responseSize = responseSize,
|
||||
httpStatus = httpStatusCode,
|
||||
direction = DIRECTION_OUT,
|
||||
requestTime = requestTime,
|
||||
responseTime = ZonedDateTime.now(clock),
|
||||
businessType = businessType,
|
||||
throwable = throwable,
|
||||
traceId = HeaderInterceptorRest.extractTraceId(request),
|
||||
requestType = HeaderInterceptorRest.extractRequestType(request),
|
||||
)
|
||||
)
|
||||
} catch (e: java.lang.RuntimeException) {
|
||||
log.error(e.toString(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(logMessage: LogMessage) {
|
||||
val stringMapMessage = StringMapMessage()
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_HEADERS,
|
||||
toHeaderString(logMessage.requestHeaders)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_RESPONSE_HEADERS,
|
||||
toHeaderString(logMessage.responseHeaders)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_URL_FULL, logMessage.url.toString())
|
||||
addLogString(stringMapMessage, PARAM_URL_DOMAIN, logMessage.url.host)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_URL_EXTENSION,
|
||||
extractExtension(logMessage.url.path)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url.path)
|
||||
addLogString(stringMapMessage, PARAM_URL_PORT, logMessage.url.port.toString())
|
||||
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url.protocol)
|
||||
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url.query)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_METHOD, logMessage.method)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_REFERER,
|
||||
getHeader(logMessage.requestHeaders, HttpHeaders.REFERER)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_MIMETYPE, logMessage.requestMimeType)
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_MIMETYPE, logMessage.responseMimeType)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_BYTES, logMessage.requestSize.toString())
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES, logMessage.responseSize.toString())
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS, logMessage.httpStatus.toString())
|
||||
addLogString(stringMapMessage, PARAM_DIRECTION, logMessage.direction)
|
||||
addLogString(stringMapMessage, PARAM_PROTOCOL, PROTOCOL_NAME)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_TIME,
|
||||
logMessage.requestTime.format(DATE_TIME_FORMATTER)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_RESPONSE_TIME,
|
||||
logMessage.responseTime.format(DATE_TIME_FORMATTER)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage,
|
||||
PARAM_DURATION,
|
||||
getDurationBetweenRequestAndResponseTime(logMessage).toNanos().toString()
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_USER_AGENT,
|
||||
getHeader(logMessage.requestHeaders, HttpHeaders.USER_AGENT)
|
||||
)
|
||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_TRACE_ID, logMessage.traceId)
|
||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_REQTYPE_ID, logMessage.requestType)
|
||||
addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody))
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_BODY, cutToMaxLength(logMessage.responseBody))
|
||||
log.debug(MARKER, stringMapMessage.asString(), logMessage.throwable)
|
||||
}
|
||||
|
||||
private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration {
|
||||
return Duration.between(logMessage.requestTime, logMessage.responseTime)
|
||||
}
|
||||
|
||||
private fun getHeader(headers: Map<String, Collection<String?>>, headerKey: String): String? {
|
||||
return headers.entries.asSequence()
|
||||
.filter { it.key.equals(headerKey, ignoreCase = true) }
|
||||
.flatMap { it.value.asSequence() }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun addLogString(stringMapMessage: StringMapMessage, key: String, value: String?) {
|
||||
if (!value.isNullOrBlank()) {
|
||||
stringMapMessage.with(key, value.trim { it <= ' ' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns null, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineBusinessType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineRequestEncoding(): Charset {
|
||||
return StandardCharsets.UTF_8
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineResponseEncoding(): Charset {
|
||||
return StandardCharsets.UTF_8
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
private const val MAX_LOG_SIZE = 20480 // 20 KB - logging could fail with bigger logmessages
|
||||
|
||||
val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||
val MARKER: Marker = MarkerFactory.getMarker("communication")
|
||||
|
||||
const val DIRECTION_IN = "inbound"
|
||||
const val DIRECTION_OUT = "outbound"
|
||||
const val PROTOCOL_NAME = "http"
|
||||
|
||||
const val PARAM_URL_FULL = "url.full"
|
||||
const val PARAM_URL_DOMAIN = "url.domain"
|
||||
const val PARAM_URL_EXTENSION = "url.extension"
|
||||
const val PARAM_URL_PATH = "url.path"
|
||||
const val PARAM_URL_PORT = "url.port"
|
||||
const val PARAM_URL_SCHEME = "url.scheme"
|
||||
const val PARAM_URL_QUERY = "url.query"
|
||||
const val PARAM_BUSINESS_TYPE = "http.request.type"
|
||||
const val PARAM_REQUEST_BODY = "http.request.body.content"
|
||||
const val PARAM_RESPONSE_BODY = "http.response.body.content"
|
||||
const val PARAM_RESPONSE_STATUS = "http.response.status_code"
|
||||
const val PARAM_REQUEST_HEADERS = "http.request.headers"
|
||||
const val PARAM_RESPONSE_HEADERS = "http.response.headers"
|
||||
const val PARAM_REQUEST_BYTES = "http.request.body.bytes"
|
||||
const val PARAM_RESPONSE_BYTES = "http.response.body.bytes"
|
||||
const val PARAM_REQUEST_MIMETYPE = "http.request.mime_type"
|
||||
const val PARAM_RESPONSE_MIMETYPE = "http.response.mime_type"
|
||||
const val PARAM_REQUEST_METHOD = "http.request.method"
|
||||
const val PARAM_REQUEST_REFERER = "http.request.referrer"
|
||||
const val PARAM_REQUEST_TIME = "event.start"
|
||||
const val PARAM_RESPONSE_TIME = "event.end"
|
||||
const val PARAM_DURATION = "event.duration"
|
||||
const val PARAM_USER_AGENT = "user_agent.original"
|
||||
const val PARAM_DIRECTION = "network.direction"
|
||||
const val PARAM_PROTOCOL = "network.protocol"
|
||||
|
||||
private fun extractHeaders(
|
||||
headerNames: Iterator<String>,
|
||||
ignoreList: List<String> = emptyList(),
|
||||
headerValuesSupplier: Function<String, Iterator<String>>
|
||||
): Map<String, MutableCollection<String>> {
|
||||
val requestHeaders: MutableMap<String, MutableCollection<String>> = mutableMapOf()
|
||||
while (headerNames.hasNext()) {
|
||||
val name = headerNames.next()
|
||||
if (name in ignoreList) continue
|
||||
val values = requestHeaders.computeIfAbsent(name) { mutableSetOf() }
|
||||
val headerValues = headerValuesSupplier.apply(name)
|
||||
while (headerValues.hasNext()) {
|
||||
values.add(headerValues.next())
|
||||
}
|
||||
}
|
||||
return requestHeaders
|
||||
}
|
||||
|
||||
private fun extractHeaders(headers: HttpHeaders): Map<String, Collection<String>> {
|
||||
val result: MutableMap<String, Collection<String>> = mutableMapOf()
|
||||
for ((key, value) in headers) {
|
||||
result[key] = value.toList()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun toHeaderString(headerMap: Map<String, Collection<String>>): String {
|
||||
return headerMap.entries.asSequence()
|
||||
.flatMap { entry ->
|
||||
val key = entry.key
|
||||
entry.value.asSequence().map { Pair(key, it) }
|
||||
}
|
||||
.map { "${it.first}=${it.second}" }
|
||||
.joinToString(separator = ",")
|
||||
}
|
||||
|
||||
private fun typeToString(contentType: String?): String? {
|
||||
return try {
|
||||
if (contentType == null) return null
|
||||
val mediaType = MediaType.parseMediaType(contentType)
|
||||
return typeToString(mediaType)
|
||||
} catch (e: InvalidMediaTypeException) {
|
||||
log.info(e.toString(), e)
|
||||
e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun typeToString(mediaType: MediaType?): String? {
|
||||
return try {
|
||||
if (mediaType == null) return null
|
||||
"${mediaType.type}/${mediaType.subtype}"
|
||||
} catch (e: RuntimeException) {
|
||||
log.info(e.toString(), e)
|
||||
e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractExtension(fileName: String?): String? {
|
||||
if (fileName == null || !fileName.contains(".")) return null
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||
}
|
||||
|
||||
private fun cutToMaxLength(string: String?): String? {
|
||||
return if (string != null && string.length > MAX_LOG_SIZE) {
|
||||
string.substring(0, MAX_LOG_SIZE)
|
||||
} else string
|
||||
}
|
||||
|
||||
private data class LogMessage(
|
||||
val requestHeaders: Map<String, Collection<String>>,
|
||||
val responseHeaders: Map<String, Collection<String>>,
|
||||
val url: URL,
|
||||
val method: String,
|
||||
val requestMimeType: String?,
|
||||
val responseMimeType: String?,
|
||||
val requestBody: String?,
|
||||
val responseBody: String?,
|
||||
val requestSize: Int,
|
||||
val responseSize: Int,
|
||||
val httpStatus: Int,
|
||||
val direction: String,
|
||||
val requestTime: ZonedDateTime,
|
||||
val responseTime: ZonedDateTime,
|
||||
val traceId: String?,
|
||||
val requestType: String?,
|
||||
val businessType: String?,
|
||||
val throwable: Throwable?
|
||||
)
|
||||
|
||||
enum class FieldLogBehaviour {
|
||||
NEVER,
|
||||
ONLY_ON_ERROR,
|
||||
ALWAYS
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.twomartens.support.model
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class LeadershipStatus {
|
||||
var isLeader = false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.twomartens.support.model.dto
|
||||
|
||||
data class ErrorMessage(val message: String)
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package de.twomartens.support.monitoring.actuator
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import java.io.IOException
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
abstract class AbstractHealthIndicator(
|
||||
private val clock: Clock,
|
||||
private val preparable: Preparable
|
||||
) : HealthIndicator {
|
||||
|
||||
private val logStatusDownMessage = "health indicator '${indicatorName()}' invoked with status '${Status.DOWN.code}'"
|
||||
private val logStatusUpMessage = "health indicator '${indicatorName()}' invoked with status '${Status.UP.code}'"
|
||||
|
||||
private var firstTime = true
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
protected abstract fun determineHealth(): Health
|
||||
|
||||
override fun health(): Health {
|
||||
try {
|
||||
preparable.prepare().use {
|
||||
var result: Health? = null
|
||||
var exception: Exception? = null
|
||||
val start = clock.millis()
|
||||
try {
|
||||
result = determineHealth()
|
||||
} catch (e: RuntimeException) {
|
||||
exception = e
|
||||
result = Health.down().withException(e).build()
|
||||
} finally {
|
||||
logInvocation(result, exception, start, clock.millis())
|
||||
}
|
||||
return result!!
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("unexpected exception occurred", e)
|
||||
return Health.down(e).build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logInvocation(health: Health?, exception: Exception?, start: Long, end: Long) {
|
||||
val duration = Duration.ofMillis(end - start)
|
||||
MDC.putCloseable("event.duration", duration.toNanos().toString()).use {
|
||||
if (exception != null || health == null) {
|
||||
log.error(logStatusDownMessage, exception)
|
||||
firstTime = true
|
||||
} else if (health.status === Status.DOWN) {
|
||||
log.warn(logStatusDownMessage)
|
||||
firstTime = true
|
||||
} else if (firstTime) {
|
||||
log.info(logStatusUpMessage)
|
||||
firstTime = false
|
||||
} else {
|
||||
log.trace(logStatusUpMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indicatorName(): String {
|
||||
return this.javaClass.getSimpleName()
|
||||
.replace("HealthIndicator", "")
|
||||
.lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HOST = "localhost"
|
||||
const val HTTP_PREFIX = "http://"
|
||||
const val HOST_PORT_SEPARATOR = ":"
|
||||
const val PATH_SEPARATOR = "/"
|
||||
const val PARAMETER_SEPARATOR = "?"
|
||||
const val DETAIL_ENDPOINT_KEY = "endpoint"
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package de.twomartens.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.support.monitoring.statusprobe.StatusProbe
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import java.time.Clock
|
||||
|
||||
abstract class AbstractStatusProbeHealthIndicator(
|
||||
timeProvider: Clock, headerInterceptor: Preparable,
|
||||
private val statusProbe: StatusProbe
|
||||
) : AbstractHealthIndicator(timeProvider, headerInterceptor), HealthIndicator {
|
||||
override fun determineHealth(): Health {
|
||||
val healthBuilder = Health.status(statusProbe.status)
|
||||
if (statusProbe.lastStatusChange != null) {
|
||||
healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, statusProbe.lastStatusChange)
|
||||
}
|
||||
if (statusProbe.throwable != null) {
|
||||
healthBuilder.withException(statusProbe.throwable)
|
||||
}
|
||||
if (statusProbe.message != null) {
|
||||
healthBuilder.withDetail(MESSAGE_KEY, statusProbe.message)
|
||||
}
|
||||
return healthBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MESSAGE_KEY = "message"
|
||||
const val LAST_STATUS_CHANGE_KEY = "lastStatusChange"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.twomartens.support.monitoring.actuator
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
fun interface Preparable {
|
||||
fun prepare(): Closeable
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.twomartens.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.support.property.HealthCheckProperties
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import java.security.SecureRandom
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* A Health check which checks if the rest services are working.
|
||||
*
|
||||
*
|
||||
* If you have a complex service, you should think about an easy greeting or echo service, which
|
||||
* only tests the
|
||||
* network/service stack and not the full application.
|
||||
*
|
||||
*
|
||||
* The health check will be called by kubernetes to check if the container/pod should be in load
|
||||
* balancing. It is possible
|
||||
* to have as much health checks as you like.
|
||||
*
|
||||
*
|
||||
* There should be a health check which is ok not before all data is loaded.
|
||||
*/
|
||||
@Component
|
||||
class RestHealthIndicator(
|
||||
clock: Clock, interceptor: HeaderInterceptorRest,
|
||||
serverProperties: ServerProperties,
|
||||
private val restTemplateRestHealthIndicator: RestTemplate,
|
||||
private val serviceProperties: HealthCheckProperties
|
||||
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
|
||||
private val randomizer = SecureRandom()
|
||||
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
|
||||
+ serverProperties.port
|
||||
+ URL_PATH + PARAMETER_SEPARATOR + GET_PARAMETER)
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
override fun determineHealth(): Health {
|
||||
val random = randomizer.nextInt(100000, 999999).toString()
|
||||
val url = "$urlPrefix{random}"
|
||||
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
|
||||
val status = if (response.body == serviceProperties.greeting.format(random)) Status.UP else Status.DOWN
|
||||
return Health.status(status)
|
||||
.withDetail(DETAIL_ENDPOINT_KEY, url)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val URL_PATH = "/timetable/healthCheck"
|
||||
private const val GET_PARAMETER = "message="
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class CountBasedStatusProbe(
|
||||
private val maxFailureCount: Int, clock: Clock, criticality: StatusProbeCriticality, name: String,
|
||||
statusProbeLogger: StatusProbeLogger
|
||||
) : StatusProbe(clock, criticality, name, statusProbeLogger) {
|
||||
private val failureCount = AtomicInteger(0)
|
||||
|
||||
@Synchronized
|
||||
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
|
||||
if (status === Status.DOWN) {
|
||||
val failureCount = failureCount.incrementAndGet()
|
||||
if (failureCount > maxFailureCount) {
|
||||
super.setStatus(status, throwable, message)
|
||||
}
|
||||
} else if (status === Status.UP) {
|
||||
failureCount.set(0)
|
||||
super.setStatus(status, throwable, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* uses the percentage of down statuses within a given period (default: 1 min) to determine if status of probe is down.
|
||||
* this is meant to be used to avoid flickering status probes on services that have lots of status updates. When there
|
||||
* is no significant amount of requests during one scheduling period, the behavior may be arbitrary.
|
||||
*/
|
||||
class PercentageBasedStatusProbe(
|
||||
private val maxFailurePercent: Int, clock: Clock,
|
||||
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
|
||||
name: String, statusProbeLogger: StatusProbeLogger
|
||||
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
|
||||
private var requestCount = 0
|
||||
private var downCount = 0
|
||||
private var temporaryMessage: String? = null
|
||||
private var temporaryThrowable: Throwable? = null
|
||||
|
||||
init {
|
||||
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
|
||||
if (status === Status.DOWN) {
|
||||
downCount++
|
||||
this.temporaryThrowable = throwable
|
||||
this.temporaryMessage = message
|
||||
}
|
||||
requestCount++
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
requestCount = 0
|
||||
downCount = 0
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun runScheduledTask() {
|
||||
if (requestCount > 0 && downCount * 100.0 / requestCount > maxFailurePercent) {
|
||||
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
|
||||
} else {
|
||||
super.setStatus(Status.UP, null, null)
|
||||
}
|
||||
reset()
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
|
||||
import org.springframework.scheduling.support.PeriodicTrigger
|
||||
import java.time.Duration
|
||||
|
||||
interface ScheduledStatusProbe {
|
||||
fun runScheduledTask()
|
||||
fun scheduleTask(
|
||||
threadPoolTaskScheduler: ThreadPoolTaskScheduler,
|
||||
schedulePeriod: Duration
|
||||
) {
|
||||
val periodicTrigger = PeriodicTrigger(
|
||||
Duration.ofSeconds(schedulePeriod.toSeconds())
|
||||
)
|
||||
threadPoolTaskScheduler.schedule(periodicTrigger) { runScheduledTask() }
|
||||
}
|
||||
}
|
||||
|
||||
fun ThreadPoolTaskScheduler.schedule(periodicTrigger: PeriodicTrigger, task: Runnable) {
|
||||
this.schedule(task, periodicTrigger)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import lombok.Getter
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import java.time.Clock
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Getter
|
||||
open class StatusProbe(
|
||||
private val clock: Clock, criticality: StatusProbeCriticality, private val name: String,
|
||||
private val statusProbeLogger: StatusProbeLogger
|
||||
) {
|
||||
var status: Status = Status.UP
|
||||
private set
|
||||
var throwable: Throwable? = null
|
||||
private set
|
||||
var message: String? = null
|
||||
private set
|
||||
var lastStatusChange: ZonedDateTime? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
statusProbeLogger.registerStatusProbe(name, criticality)
|
||||
}
|
||||
|
||||
protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) {
|
||||
if (status !== this.status) {
|
||||
lastStatusChange = ZonedDateTime.now(clock)
|
||||
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable)
|
||||
}
|
||||
this.status = status
|
||||
this.throwable = throwable
|
||||
this.message = message
|
||||
}
|
||||
|
||||
fun up() {
|
||||
setStatus(Status.UP, null, null)
|
||||
}
|
||||
|
||||
fun up(message: String) {
|
||||
setStatus(Status.UP, null, message)
|
||||
}
|
||||
|
||||
fun down() {
|
||||
setStatus(Status.DOWN, null, null)
|
||||
}
|
||||
|
||||
fun down(throwable: Throwable) {
|
||||
setStatus(Status.DOWN, throwable, null)
|
||||
}
|
||||
|
||||
fun down(message: String) {
|
||||
setStatus(Status.DOWN, null, message)
|
||||
}
|
||||
|
||||
protected fun down(throwable: Throwable, message: String) {
|
||||
setStatus(Status.DOWN, throwable, message)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
enum class StatusProbeCriticality {
|
||||
K1,
|
||||
K2,
|
||||
K3
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.apache.logging.log4j.MarkerManager
|
||||
import org.apache.logging.log4j.message.StringMapMessage
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import java.time.Clock
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class StatusProbeLogger internal constructor(private val clock: Clock, private val commLog: Logger) {
|
||||
private val statusProbeToStatus: MutableMap<ProbeIdent, Status> = mutableMapOf()
|
||||
|
||||
constructor(clock: Clock) : this(clock, LogManager.getLogger("statusprobe"))
|
||||
|
||||
fun registerStatusProbe(name: String, criticality: StatusProbeCriticality) {
|
||||
statusProbeToStatus[ProbeIdent(name, criticality)] = Status.UP
|
||||
logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null)
|
||||
}
|
||||
|
||||
fun logStatusChange(
|
||||
name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?,
|
||||
throwable: Throwable?
|
||||
) {
|
||||
var probeIdent = getProbeIdent(name)
|
||||
if (probeIdent == null) {
|
||||
probeIdent = ProbeIdent(name, StatusProbeCriticality.K1)
|
||||
}
|
||||
statusProbeToStatus[probeIdent] = status
|
||||
createLog(message, lastStatusChange, throwable)
|
||||
}
|
||||
|
||||
private fun getProbeIdent(name: String): ProbeIdent? {
|
||||
return statusProbeToStatus.keys.asSequence()
|
||||
.filter { it.name == name }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) {
|
||||
val cleanedMessage = message ?: ""
|
||||
val overallStatus = overallStatus
|
||||
val criticality = overallCriticality
|
||||
if (Status.UP == overallStatus) {
|
||||
commLog.info(
|
||||
MARKER, StringMapMessage()
|
||||
.with(LABEL_CRITICALITY, criticality)
|
||||
.with(LABEL_STATUS, overallStatus)
|
||||
.with(LABEL_MESSAGE, cleanedMessage)
|
||||
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)
|
||||
)
|
||||
} else {
|
||||
commLog.error(
|
||||
MARKER, StringMapMessage()
|
||||
.with(LABEL_CRITICALITY, criticality)
|
||||
.with(LABEL_STATUS, overallStatus)
|
||||
.with(LABEL_MESSAGE, cleanedMessage)
|
||||
.with(LABEL_REASON, reason)
|
||||
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val overallCriticality: StatusProbeCriticality
|
||||
get() {
|
||||
val crits = statusProbeToStatus.keys.asSequence()
|
||||
.map { it.criticality }
|
||||
.toList()
|
||||
return if (crits.contains(StatusProbeCriticality.K1)) StatusProbeCriticality.K1
|
||||
else if (crits.contains(StatusProbeCriticality.K2)) StatusProbeCriticality.K2
|
||||
else StatusProbeCriticality.K3
|
||||
}
|
||||
|
||||
private val overallStatus: Status
|
||||
get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN
|
||||
else Status.UP
|
||||
|
||||
private val reason: String
|
||||
get() {
|
||||
val probesDown = statusProbeToStatus.entries.asSequence()
|
||||
.filter { it.value == Status.DOWN }
|
||||
.map { it.key }
|
||||
.toList()
|
||||
val reasonK1 = getDownStatusProbes(probesDown, StatusProbeCriticality.K1)
|
||||
val reasonK2 = getDownStatusProbes(probesDown, StatusProbeCriticality.K2)
|
||||
val reasonK3 = getDownStatusProbes(probesDown, StatusProbeCriticality.K3)
|
||||
return "$reasonK1$reasonK2$reasonK3".trim { it <= ' ' }
|
||||
}
|
||||
|
||||
private fun getDownStatusProbes(probesDown: List<ProbeIdent>, criticality: StatusProbeCriticality): String {
|
||||
val downProbeNames = probesDown.asSequence()
|
||||
.filter { it.criticality == criticality }
|
||||
.map { it.name }
|
||||
.toList()
|
||||
return if (downProbeNames.isNotEmpty()) {
|
||||
"$criticality failed: ${downProbeNames.joinToString(separator = ",")}\n"
|
||||
} else ""
|
||||
}
|
||||
|
||||
data class ProbeIdent(val name: String, val criticality: StatusProbeCriticality)
|
||||
|
||||
companion object {
|
||||
private val MARKER = MarkerManager.getMarker("statusprobe")
|
||||
private const val LABEL_CRITICALITY = "label.status.criticality"
|
||||
private const val LABEL_STATUS = "label.status.status"
|
||||
private const val LABEL_REASON = "label.status.reason"
|
||||
private const val LABEL_MESSAGE = "label.status.description"
|
||||
private const val LABEL_LAST_STATUS_CHANGE = "label.status.last_change"
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package de.twomartens.support.monitoring.statusprobe
|
||||
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class TimeBasedStatusProbe(
|
||||
private val maxFailureDuration: Duration, private val clock: Clock,
|
||||
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
|
||||
name: String, statusProbeLogger: StatusProbeLogger
|
||||
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
|
||||
private var lastSuccess: ZonedDateTime? = null
|
||||
private var temporaryThrowable: Throwable? = null
|
||||
private var temporaryMessage: String? = null
|
||||
|
||||
init {
|
||||
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
|
||||
if (status === Status.DOWN) {
|
||||
this.temporaryThrowable = throwable
|
||||
this.temporaryMessage = message
|
||||
} else if (status === Status.UP) {
|
||||
lastSuccess = ZonedDateTime.now(clock)
|
||||
super.setStatus(status, throwable, message)
|
||||
}
|
||||
}
|
||||
|
||||
private val isOverdue: Boolean
|
||||
get() {
|
||||
if (lastSuccess == null) {
|
||||
return false
|
||||
}
|
||||
val timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock))
|
||||
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun runScheduledTask() {
|
||||
if (isOverdue) {
|
||||
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package de.twomartens.support.property
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties(prefix = "de.twomartens.support.health")
|
||||
@Schema(description = "Properties, to configure health check")
|
||||
class HealthCheckProperties {
|
||||
lateinit var greeting: String
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package de.twomartens.support.property
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding
|
||||
import org.springframework.boot.convert.DurationUnit
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@ConfigurationProperties(prefix = "resttemplate.timeout")
|
||||
data class RestTemplateTimeoutProperties @ConstructorBinding constructor(
|
||||
@DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestHealthIndicatorInMillis: Duration,
|
||||
@DurationUnit(ChronoUnit.MILLIS) val connectionRestHealthIndicatorTimeoutInMillis: Duration,
|
||||
@DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestTemplateInMillis: Duration,
|
||||
@DurationUnit(ChronoUnit.MILLIS) val connectionRestTemplateTimeoutInMillis: Duration
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package de.twomartens.support.property
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding
|
||||
import org.springframework.boot.convert.DurationUnit
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "de.twomartens.timetable.statusprobe")
|
||||
@Schema(description = "Properties, to configure this Application")
|
||||
data class StatusProbeProperties @ConstructorBinding constructor(
|
||||
@DurationUnit(ChronoUnit.SECONDS) val scheduleDuration: Duration,
|
||||
@DurationUnit(ChronoUnit.MINUTES) val maxKafkaFailureDuration: Duration,
|
||||
val maxBlobFailureCount: Int,
|
||||
val maxFailurePercent: Int
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.twomartens.support.property
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
import java.time.ZoneId
|
||||
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "time")
|
||||
@Schema(description = "Properties to configure time")
|
||||
data class TimeProperties @ConstructorBinding constructor(
|
||||
val defaultTimeZone: ZoneId
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.twomartens.support.security
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.keycloak.AuthorizationContext
|
||||
import org.keycloak.adapters.authorization.PolicyEnforcer
|
||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
||||
import org.keycloak.adapters.authorization.spi.HttpResponse
|
||||
import org.keycloak.authorization.client.ClientAuthorizationContext
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
|
||||
import org.keycloak.representations.idm.authorization.Permission
|
||||
|
||||
class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer,
|
||||
private val policyEnforcerConfig: PolicyEnforcerConfig) {
|
||||
|
||||
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri)
|
||||
}
|
||||
val context = authorize(request, response)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED")
|
||||
log.debug("Returning authorization context with permissions:")
|
||||
for (permission in context.permissions) {
|
||||
log.debug(permission.toString())
|
||||
}
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
|
||||
val enforcementMode = policyEnforcerConfig.enforcementMode
|
||||
return if (EnforcementMode.DISABLED == enforcementMode) {
|
||||
createAuthorizedContext()
|
||||
} else policyEnforcer.enforce(request, response)
|
||||
}
|
||||
|
||||
private fun createAuthorizedContext(): AuthorizationContext {
|
||||
return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
|
||||
override fun hasPermission(resourceName: String, scopeName: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hasResourcePermission(resourceName: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hasScopePermission(scopeName: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getPermissions(): List<Permission> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun isGranted(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package de.twomartens.support.security
|
||||
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import mu.KotlinLogging
|
||||
import org.keycloak.AuthorizationContext
|
||||
import org.keycloak.adapters.authorization.PolicyEnforcer
|
||||
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest
|
||||
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse
|
||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
|
||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
||||
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
|
||||
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = ConcurrentHashMap()
|
||||
|
||||
@Throws(IOException::class, ServletException::class)
|
||||
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
|
||||
val request = servletRequest as HttpServletRequest
|
||||
val response = servletResponse as HttpServletResponse?
|
||||
val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
|
||||
val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest)
|
||||
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
|
||||
request.setAttribute(AuthorizationContext::class.java.name, authzContext)
|
||||
if (authzContext.isGranted) {
|
||||
log.debug("Request authorized, continuing the filter chain")
|
||||
filterChain.doFilter(servletRequest, servletResponse)
|
||||
} else {
|
||||
log.debug("Unauthorized request to path [{}], aborting the filter chain", request.requestURI)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBearerToken(request: HttpServletRequest): String? {
|
||||
val authorizationHeaderValues = request.getHeaders("Authorization")
|
||||
while (authorizationHeaderValues.hasMoreElements()) {
|
||||
val value = authorizationHeaderValues.nextElement()
|
||||
val parts = value.trim()
|
||||
.split("\\s+".toRegex())
|
||||
.dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
if (parts.size != 2) {
|
||||
continue
|
||||
}
|
||||
val bearer = parts[0]
|
||||
if (bearer.equals("Bearer", ignoreCase = true)) {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
|
||||
return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) }
|
||||
}
|
||||
|
||||
private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer {
|
||||
val authServerUrl = enforcerConfig.authServerUrl
|
||||
return SpringPolicyEnforcer(PolicyEnforcer.builder()
|
||||
.authServerUrl(authServerUrl)
|
||||
.realm(enforcerConfig.realm)
|
||||
.clientId(enforcerConfig.resource)
|
||||
.credentials(enforcerConfig.credentials)
|
||||
.bearerOnly(false)
|
||||
.enforcerConfig(enforcerConfig).build(), enforcerConfig)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.twomartens.support.service
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.cloud.bus.BusBridge
|
||||
import org.springframework.cloud.bus.event.RemoteApplicationEvent
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class BusService(
|
||||
private val busBridge: ObjectProvider<BusBridge>
|
||||
) {
|
||||
fun publishEvent(event: RemoteApplicationEvent) {
|
||||
busBridge.ifAvailable {
|
||||
it.send(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id("twomartens.spring-boot-cloud-base")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.spring.boot.actuator)
|
||||
implementation(libs.spring.boot.web)
|
||||
implementation(libs.spring.openapi)
|
||||
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(libs.spring.cloud.leader.election)
|
||||
implementation(libs.spring.cloud.starter.bus.kafka)
|
||||
}
|
||||
Reference in New Issue
Block a user