Changed to Kotlin
This commit is contained in:
parent
9bade2780c
commit
d377cbd886
|
@ -1,15 +0,0 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MainApplication" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="de.twomartens.template.MainApplication" />
|
||||
<module name="template.server.main" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="de.twomartens.template.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,21 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MainApplicationKt" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" value="dev" />
|
||||
<module name="timetable.server.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="de.twomartens.timetable.MainApplicationKt" />
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="true" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
<ENTRY IS_ENABLED="true" PARSER="env" IS_EXECUTABLE="false" PATH=".env" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
13
build.gradle
13
build.gradle
|
@ -1,13 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.versions'
|
||||
id 'twomartens.nebula-release'
|
||||
}
|
||||
|
||||
versionCatalogUpdate {
|
||||
sortByKey = false
|
||||
keep {
|
||||
keepUnusedVersions = true
|
||||
keepUnusedLibraries = true
|
||||
keepUnusedPlugins = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
id("twomartens.versions")
|
||||
id("twomartens.nebula-release")
|
||||
}
|
||||
|
||||
nebulaRelease {
|
||||
addReleaseBranchPattern("/main/")
|
||||
}
|
||||
|
||||
versionCatalogUpdate {
|
||||
sortByKey.set(false)
|
||||
keep {
|
||||
keepUnusedVersions.set(true)
|
||||
keepUnusedLibraries.set(true)
|
||||
keepUnusedPlugins.set(true)
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
plugins {
|
||||
id 'groovy-gradle-plugin'
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.plugin.springboot
|
||||
implementation libs.plugin.lombok
|
||||
implementation libs.plugin.nebula.release
|
||||
implementation libs.plugin.gradle.versions
|
||||
implementation libs.plugin.version.catalog
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
implementation(libs.plugin.kotlin.gradle)
|
||||
implementation(libs.plugin.springboot)
|
||||
implementation(libs.plugin.lombok)
|
||||
implementation(libs.plugin.nebula.release)
|
||||
implementation(libs.plugin.gradle.versions)
|
||||
implementation(libs.plugin.version.catalog)
|
||||
implementation(libs.plugin.jib)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
rootProject.name = 'twomartens.config'
|
||||
rootProject.name = "twomartens.config"
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
|
@ -1,6 +0,0 @@
|
|||
plugins {
|
||||
id 'idea'
|
||||
id 'eclipse'
|
||||
}
|
||||
|
||||
group = projectgroup
|
|
@ -1,23 +0,0 @@
|
|||
plugins {
|
||||
id 'checkstyle'
|
||||
id 'twomartens.java-base'
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion '10.0'
|
||||
ignoreFailures = false
|
||||
maxWarnings = 0
|
||||
|
||||
configFile rootProject.file('config/checkstyle/checkstyle.xml')
|
||||
|
||||
configProperties = ['org.checkstyle.google.suppressionfilter.config': "${project.rootDir}/config/checkstyle/checkstyle-suppressions.xml"]
|
||||
|
||||
tasks.withType(Checkstyle).tap {
|
||||
configureEach {
|
||||
reports {
|
||||
xml.required = true
|
||||
html.required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id 'twomartens.base'
|
||||
}
|
||||
|
||||
sourceCompatibility = projectSourceCompatibility
|
||||
targetCompatibility = projectSourceCompatibility
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
tasks.register('buildAll') {
|
||||
group 'build'
|
||||
dependsOn(build)
|
||||
dependsOn(test)
|
||||
}
|
||||
|
||||
clean {
|
||||
delete 'out'
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.java-base'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += "--enable-preview"
|
||||
}
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
jvmArgs += "--enable-preview"
|
||||
}
|
||||
|
||||
tasks.withType(JavaExec).configureEach {
|
||||
jvmArgs += '--enable-preview'
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import java.text.SimpleDateFormat
|
||||
|
||||
plugins {
|
||||
id 'jacoco'
|
||||
id 'io.freefair.lombok'
|
||||
id 'twomartens.java-base'
|
||||
id 'twomartens.checkstyle'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
constraints.implementation libs.bundles.logging
|
||||
|
||||
implementation libs.slf4j.api
|
||||
runtimeOnly libs.bundles.logging
|
||||
|
||||
testImplementation libs.bundles.test
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude group: 'junit', module: 'junit'
|
||||
// we are using log4j-slf4j2-impl, so we need to suppress spring include of log4j-slf4j-impl
|
||||
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
systemProperty 'junit.jupiter.execution.parallel.enabled', true
|
||||
systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent"
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
workingDir = rootProject.projectDir
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn(test)
|
||||
reports {
|
||||
xml.required = true
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
doFirst {
|
||||
manifest {
|
||||
attributes 'Implementation-Title': rootProject.name,
|
||||
'Implementation-Version': archiveVersion.get(),
|
||||
'Implementation-Vendor': "Jim Martens",
|
||||
'Build-Timestamp': new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
|
||||
'Created-By': "Gradle ${gradle.gradleVersion}",
|
||||
'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
|
||||
'Build-OS': "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalization.runtimeClasspath.metaInf {
|
||||
ignoreAttribute("Build-Timestamp")
|
||||
}
|
||||
|
||||
tasks.register('cleanLibs') {
|
||||
delete("${buildDir}/libs")
|
||||
}
|
||||
|
||||
tasks.build.dependsOn("cleanLibs")
|
|
@ -1,16 +0,0 @@
|
|||
plugins {
|
||||
id 'com.netflix.nebula.release'
|
||||
id 'twomartens.base'
|
||||
}
|
||||
|
||||
nebulaRelease {
|
||||
addReleaseBranchPattern(/main/)
|
||||
}
|
||||
|
||||
task writeVersionProperties() {
|
||||
group 'version'
|
||||
buildDir.mkdirs()
|
||||
file("$buildDir/version.properties").text = "VERSION=${project.version.toString()}\n"
|
||||
mustRunAfter("release")
|
||||
outputs.file("$buildDir/version.properties")
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.spring-boot'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(libs.spring.cloud)
|
||||
implementation libs.bundles.spring.boot.server
|
||||
implementation libs.spring.openapi
|
||||
|
||||
implementation libs.httpclient
|
||||
implementation libs.prometheus
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
"server-test" {
|
||||
java {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/server-test/java')
|
||||
}
|
||||
resources.srcDir file('src/server-test/resources')
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
serverTestImplementation.extendsFrom testImplementation
|
||||
}
|
||||
|
||||
tasks.register('serverTest', Test) {
|
||||
outputs.upToDateWhen { false }
|
||||
systemProperty 'junit.jupiter.execution.parallel.enabled', true
|
||||
systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent"
|
||||
systemProperty 'junit.jupiter.execution.parallel.mode.classes.default', "concurrent"
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
group = 'verification'
|
||||
workingDir = rootProject.projectDir
|
||||
testClassesDirs = sourceSets."server-test".output.classesDirs
|
||||
classpath = sourceSets."server-test".runtimeClasspath
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
plugins {
|
||||
id 'org.springframework.boot'
|
||||
id 'twomartens.java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(libs.spring.boot)
|
||||
|
||||
implementation libs.bundles.spring.boot
|
||||
testImplementation libs.spring.boot.test
|
||||
annotationProcessor libs.spring.boot.config
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
"integration-test" {
|
||||
java {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/java')
|
||||
}
|
||||
resources.srcDir file('src/integration-test/resources')
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
|
||||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||
}
|
||||
integrationTestImplementation.extendsFrom testImplementation
|
||||
}
|
||||
|
||||
tasks.register('integrationTest', Test) {
|
||||
systemProperty 'junit.jupiter.execution.parallel.enabled', true
|
||||
systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent"
|
||||
systemProperty 'junit.jupiter.execution.parallel.mode.classes.default', "concurrent"
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
group = 'verification'
|
||||
workingDir = rootProject.projectDir
|
||||
testClassesDirs = sourceSets."integration-test".output.classesDirs
|
||||
classpath = sourceSets."integration-test".runtimeClasspath
|
||||
}
|
||||
|
||||
tasks.named("buildAll").configure() {
|
||||
dependsOn(integrationTest)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
buildInfo()
|
||||
}
|
||||
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = true
|
||||
archiveClassifier.set("")
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
plugins {
|
||||
id "com.github.ben-manes.versions"
|
||||
id "nl.littlerobots.version-catalog-update"
|
||||
}
|
||||
|
||||
dependencyUpdates {
|
||||
revision = "release"
|
||||
gradleReleaseChannel = "current"
|
||||
}
|
||||
|
||||
def isNonStable = { String version ->
|
||||
def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) }
|
||||
def regex = /^[0-9,.v-]+(-r)?$/
|
||||
return !stableKeyword && !(version ==~ regex)
|
||||
}
|
||||
|
||||
tasks.named("dependencyUpdates").configure {
|
||||
rejectVersionIf {
|
||||
isNonStable(it.candidate.version)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("versionCatalogUpdate").configure {
|
||||
group 'version'
|
||||
}
|
||||
|
||||
tasks.named("dependencyUpdates").configure {
|
||||
group 'version'
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
plugins {
|
||||
idea
|
||||
}
|
||||
|
||||
val projectgroup: String = providers.gradleProperty("projectgroup").get()
|
||||
group = projectgroup
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
checkstyle
|
||||
id("twomartens.java-base")
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.0"
|
||||
isIgnoreFailures = false
|
||||
maxWarnings = 0
|
||||
|
||||
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
|
||||
|
||||
configProperties = mapOf<String, String>(
|
||||
"org.checkstyle.google.suppressionfilter.config" to
|
||||
"${project.rootDir}/config/checkstyle/checkstyle-suppressions.xml")
|
||||
|
||||
}
|
||||
tasks.withType<Checkstyle>().configureEach {
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
html.required.set(true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
plugins {
|
||||
java
|
||||
`java-library`
|
||||
id("twomartens.base")
|
||||
application
|
||||
}
|
||||
|
||||
val projectSourceCompatibility: String = rootProject.properties["projectSourceCompatibility"].toString()
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(projectSourceCompatibility))
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
tasks.register("buildAll") {
|
||||
group = "build"
|
||||
dependsOn("build")
|
||||
dependsOn("test")
|
||||
}
|
||||
|
||||
tasks.clean {
|
||||
doFirst {
|
||||
delete("out")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
id("twomartens.java-base")
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.plusAssign("--enable-preview")
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
jvmArgs.plusAssign("--enable-preview")
|
||||
}
|
||||
|
||||
tasks.withType<JavaExec>().configureEach {
|
||||
jvmArgs.plusAssign("--enable-preview")
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
plugins {
|
||||
jacoco
|
||||
id("io.freefair.lombok")
|
||||
id("twomartens.java-base")
|
||||
id("twomartens.checkstyle")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
constraints.implementation(libs.bundles.logging)
|
||||
|
||||
implementation(libs.slf4j.api)
|
||||
runtimeOnly(libs.bundles.logging)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.launcher)
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude(group="junit", module="junit")
|
||||
// we are using log4j-slf4j2-impl, so we need to suppress spring include of log4j-slf4j-impl
|
||||
exclude(group="org.apache.logging.log4j", module="log4j-slf4j-impl")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
systemProperty("junit.jupiter.execution.parallel.enabled", true)
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
workingDir = rootProject.projectDir
|
||||
finalizedBy(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test)
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
normalization.runtimeClasspath.metaInf {
|
||||
ignoreAttribute("Build-Timestamp")
|
||||
}
|
||||
|
||||
tasks.register("cleanLibs") {
|
||||
delete("${buildDir}/libs")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn("cleanLibs")
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
id("com.google.cloud.tools.jib")
|
||||
id("twomartens.java-base")
|
||||
}
|
||||
|
||||
tasks.named("jib") {
|
||||
dependsOn("build")
|
||||
}
|
||||
|
||||
tasks.named("jibDockerBuild") {
|
||||
dependsOn("build")
|
||||
}
|
||||
|
||||
tasks.named("build") {
|
||||
dependsOn("cleanCache")
|
||||
}
|
||||
|
||||
tasks.register("cleanCache") {
|
||||
delete("${buildDir}/jib-cache")
|
||||
delete("${buildDir}/libs")
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.logging)
|
||||
implementation(kotlin("reflect"))
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xjvm-default=all")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import java.nio.file.Files
|
||||
|
||||
plugins {
|
||||
id("twomartens.base")
|
||||
}
|
||||
|
||||
apply(plugin="com.netflix.nebula.release")
|
||||
|
||||
tasks.register("writeVersionProperties") {
|
||||
group = "version"
|
||||
mustRunAfter("release")
|
||||
outputs.file("$buildDir/version.properties")
|
||||
val directory = buildDir
|
||||
doLast {
|
||||
Files.createDirectories(directory.toPath())
|
||||
File("$buildDir/version.properties").writeText("VERSION=${project.version}\n")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
plugins {
|
||||
id("twomartens.spring-boot")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.cloud))
|
||||
implementation(libs.bundles.spring.boot.server)
|
||||
implementation(libs.spring.openapi)
|
||||
|
||||
implementation(libs.httpclient)
|
||||
implementation(libs.prometheus)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
create("server-test") {
|
||||
java {
|
||||
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
|
||||
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
|
||||
setSrcDirs(listOf("src/server-test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
testSources.from(sourceSets["server-test"].java.srcDirs)
|
||||
}
|
||||
}
|
||||
|
||||
val serverTestImplementation: Configuration by configurations.getting {
|
||||
extendsFrom(configurations["testImplementation"])
|
||||
}
|
||||
|
||||
tasks.register<Test>("serverTest") {
|
||||
outputs.upToDateWhen { false }
|
||||
systemProperty("junit.jupiter.execution.parallel.enabled", true)
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
group = "verification"
|
||||
workingDir = rootProject.projectDir
|
||||
testClassesDirs = sourceSets["server-test"].output.classesDirs
|
||||
classpath = sourceSets["server-test"].runtimeClasspath
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatter.ofPattern
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot")
|
||||
id("twomartens.java")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.boot))
|
||||
|
||||
implementation(libs.bundles.spring.boot)
|
||||
testImplementation(libs.spring.boot.test)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
create("integration-test") {
|
||||
java {
|
||||
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
|
||||
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
|
||||
setSrcDirs(listOf("src/integration-test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
testSources.from(sourceSets["integration-test"].java.srcDirs)
|
||||
}
|
||||
}
|
||||
|
||||
val integrationTestImplementation: Configuration by configurations.getting {
|
||||
extendsFrom(configurations.testImplementation.get())
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Test>("integrationTest") {
|
||||
systemProperty("junit.jupiter.execution.parallel.enabled", true)
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
group = "verification"
|
||||
workingDir = rootProject.projectDir
|
||||
testClassesDirs = sourceSets["integration-test"].output.classesDirs
|
||||
classpath = sourceSets["integration-test"].runtimeClasspath
|
||||
}
|
||||
|
||||
tasks.named("buildAll") {
|
||||
dependsOn("integrationTest")
|
||||
}
|
||||
|
||||
val formatter: DateTimeFormatter = ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
|
||||
|
||||
tasks.jar {
|
||||
manifest {
|
||||
attributes["Implementation-Title"] = rootProject.name
|
||||
attributes["Implementation-Version"] = archiveVersion.get()
|
||||
attributes["Implementation-Vendor"] = "Jim Martens"
|
||||
attributes["Build-Timestamp"] = ZonedDateTime.now().format(formatter)
|
||||
attributes["Created-By"] = "Gradle ${gradle.gradleVersion}"
|
||||
attributes["Build-Jdk"] = "${providers.systemProperty("java.version").get()} (${providers.systemProperty("java.vendor").get()} ${providers.systemProperty("java.vm.version").get()})"
|
||||
attributes["Build-OS"] = "${providers.systemProperty("os.name").get()} ${providers.systemProperty("os.arch").get()} ${providers.systemProperty("os.version").get()}"
|
||||
}
|
||||
}
|
||||
|
||||
springBoot {
|
||||
buildInfo()
|
||||
mainClass.set("de.twomartens.timetable.MainApplicationKt")
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
|
||||
plugins {
|
||||
id("com.github.ben-manes.versions")
|
||||
id("nl.littlerobots.version-catalog-update")
|
||||
}
|
||||
|
||||
tasks.withType<DependencyUpdatesTask>().configureEach {
|
||||
revision = "release"
|
||||
gradleReleaseChannel = "current"
|
||||
}
|
||||
|
||||
fun String.isNonStable(): Boolean {
|
||||
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { uppercase().contains(it) }
|
||||
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
|
||||
val isStable = stableKeyword || regex.matches(this)
|
||||
return isStable.not()
|
||||
}
|
||||
|
||||
tasks.withType<DependencyUpdatesTask> {
|
||||
rejectVersionIf {
|
||||
candidate.version.isNonStable()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("versionCatalogUpdate").configure {
|
||||
group = "version"
|
||||
}
|
||||
|
||||
tasks.named("dependencyUpdates").configure {
|
||||
group = "version"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
projectname=template
|
||||
projectname=timetable
|
||||
projectgroup=de.2martens
|
||||
projectSourceCompatibility=17
|
||||
file.encoding=utf-8
|
||||
|
@ -6,4 +6,5 @@ org.gradle.parallel=true
|
|||
org.gradle.daemon=true
|
||||
org.gradle.welcome=never
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
kapt.include.compile.classpath=false
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
[versions]
|
||||
spring-boot = "3.0.5"
|
||||
spring-boot = "3.1.2"
|
||||
spring-doc = "2.1.0"
|
||||
spring-cloud = "2022.0.2"
|
||||
spring-cloud = "2022.0.4"
|
||||
spring-grpc = "2.14.0.RELEASE"
|
||||
grpc = "1.54.0"
|
||||
grpc = "1.57.0"
|
||||
tomcat-annotations = "6.0.53"
|
||||
httpclient = "5.2.1"
|
||||
jaxb = "4.0.3"
|
||||
jakarta-xml = "4.0.1"
|
||||
slf4j = "2.0.7"
|
||||
log4j = "2.20.0"
|
||||
log4j-ecs = "1.5.0"
|
||||
mapstruct = "1.5.3.Final"
|
||||
junit = "5.9.2"
|
||||
mapstruct = "1.5.5.Final"
|
||||
junit = "5.10.0"
|
||||
assertj = "3.24.2"
|
||||
mockito = "5.3.0"
|
||||
plugin-nebula-release = "17.1.0"
|
||||
mockito = "5.4.0"
|
||||
keycloak = "22.0.1"
|
||||
kotlin-logging = "3.0.5"
|
||||
kotlin-reflect = "1.9.0"
|
||||
kotlin-lombok = "1.9.0"
|
||||
plugin-nebula-release = "17.2.2"
|
||||
plugin-lombok = "8.0.1"
|
||||
plugin-gradle-versions = "0.46.0"
|
||||
plugin-version-catalog = "0.8.0"
|
||||
plugin-kotlin-gradle = "1.9.0"
|
||||
plugin-jib = "3.3.2"
|
||||
|
||||
[libraries]
|
||||
spring-boot = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }
|
||||
|
@ -27,14 +35,23 @@ spring-boot-thymeleaf = { module = "org.springframework.boot:spring-boot-starter
|
|||
spring-boot-mongo = { module = "org.springframework.boot:spring-boot-starter-data-mongodb" }
|
||||
spring-boot-validation = { module = "org.springframework.boot:spring-boot-starter-validation" }
|
||||
spring-boot-test = { module = "org.springframework.boot:spring-boot-starter-test" }
|
||||
spring-boot-security = { module = "org.springframework.boot:spring-boot-starter-security" }
|
||||
spring-boot-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" }
|
||||
spring-boot-config = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot" }
|
||||
spring-cloud = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" }
|
||||
spring-cloud-starter = { module = "org.springframework.cloud:spring-cloud-starter" }
|
||||
spring-cloud-starter-bus-kafka = { module = "org.springframework.cloud:spring-cloud-starter-bus-kafka" }
|
||||
spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config" }
|
||||
spring-cloud-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" }
|
||||
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" }
|
||||
spring-grpc = { module = "net.devh:grpc-spring-boot-starter", version.ref = "spring-grpc" }
|
||||
spring-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "spring-doc" }
|
||||
spring-sec = { module = "org.springdoc:springdoc-openapi-starter-common", version.ref = "spring-doc" }
|
||||
spring-openapi = { module = "org.springdoc:springdoc-openapi-starter-webmvc-api", version.ref = "spring-doc" }
|
||||
jaxb-impl = { module = "com.sun.xml.bind:jaxb-impl", version.ref="jaxb" }
|
||||
jakarta-xml-binding = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version.ref = "jakarta-xml" }
|
||||
keycloak-core = { module = "org.keycloak:keycloak-core", version.ref = "keycloak" }
|
||||
keycloak-policy-enforcer = { module = "org.keycloak:keycloak-policy-enforcer", version.ref = "keycloak" }
|
||||
grpc-api = { module = "io.grpc:grpc-api", version.ref = "grpc" }
|
||||
grpc-context = { module = "io.grpc:grpc-context", version.ref = "grpc" }
|
||||
grpc-core = { module = "io.grpc:grpc-core", version.ref = "grpc" }
|
||||
|
@ -58,15 +75,20 @@ log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4
|
|||
log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
||||
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
|
||||
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
mockito-inline = "org.mockito:mockito-inline:5.2.0"
|
||||
mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
||||
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||
plugin-nebula-release = { module = "com.netflix.nebula:nebula-release-plugin", version.ref = "plugin-nebula-release" }
|
||||
plugin-springboot = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" }
|
||||
plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "plugin-lombok" }
|
||||
plugin-gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "plugin-gradle-versions" }
|
||||
plugin-version-catalog = { module = "nl.littlerobots.vcu:plugin", version.ref = "plugin-version-catalog" }
|
||||
plugin-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "plugin-kotlin-gradle" }
|
||||
plugin-jib = { module = "com.google.cloud.tools:jib-gradle-plugin", version.ref = "plugin-jib" }
|
||||
|
||||
[bundles]
|
||||
logging = [
|
||||
|
@ -88,10 +110,9 @@ grpc = [
|
|||
"grpc-stub",
|
||||
]
|
||||
spring-boot = [
|
||||
"spring-boot-starter",
|
||||
"spring-boot-log4j",
|
||||
"spring-boot-starter",
|
||||
]
|
||||
|
||||
spring-boot-server = [
|
||||
"spring-boot-actuator",
|
||||
"spring-boot-log4j",
|
||||
|
@ -103,6 +124,12 @@ spring-boot-server = [
|
|||
"spring-sec",
|
||||
"spring-ui",
|
||||
]
|
||||
spring-boot-security = [
|
||||
"keycloak-core",
|
||||
"keycloak-policy-enforcer",
|
||||
"spring-boot-oauth2-resource-server",
|
||||
"spring-boot-security",
|
||||
]
|
||||
test = [
|
||||
"assertj",
|
||||
"junit-jupiter",
|
||||
|
@ -111,3 +138,6 @@ test = [
|
|||
"mockito-inline",
|
||||
"mockito-junit",
|
||||
]
|
||||
|
||||
[plugins]
|
||||
kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin-lombok" }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
plugins {
|
||||
id("twomartens.jib")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":server"))
|
||||
}
|
||||
|
||||
jib {
|
||||
from {
|
||||
image = "amazoncorretto:" + properties["projectSourceCompatibility"] + "-alpine"
|
||||
platforms {
|
||||
platform {
|
||||
architecture = "amd64"
|
||||
os = "linux"
|
||||
}
|
||||
platform {
|
||||
architecture = "arm64"
|
||||
os = "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
to {
|
||||
image = "2martens/timetable"
|
||||
tags = setOf(
|
||||
"latest",
|
||||
properties["version"].toString().replace("+", "-"))
|
||||
auth {
|
||||
username = System.getenv("USERNAME")
|
||||
password = System.getenv("PASSWORD")
|
||||
}
|
||||
}
|
||||
container {
|
||||
mainClass = "de.twomartens.timetable.MainApplicationKt"
|
||||
jvmFlags = listOf("-XX:+UseContainerSupport",
|
||||
"-XX:MaxRAMPercentage=75.0")
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package de.twomartens.template.model;
|
||||
|
||||
public record Name(String value) {
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib')
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package de.twomartens.template;
|
||||
|
||||
import de.twomartens.template.model.Name;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Name name = new Name("World");
|
||||
log.info("Hello %s!".formatted(name));
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Configuration status="warn" monitorInterval="30">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout alwaysWriteExceptions="true"
|
||||
pattern="%5p %d{HH:mm:ss.SSS} (%F:%L) %notEmpty{[%X{REQTYPE}] }%notEmpty{[%marker] }%K{event.end}%m%n%xEx{full}"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
|
@ -1,9 +0,0 @@
|
|||
plugins {
|
||||
id 'twomartens.spring-boot-cloud'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.mapstruct.base
|
||||
annotationProcessor libs.mapstruct.processor
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
id("twomartens.spring-boot-cloud")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.mapstruct.base)
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(libs.jaxb.impl)
|
||||
implementation(libs.jakarta.xml.binding)
|
||||
annotationProcessor(libs.mapstruct.processor)
|
||||
kapt(libs.mapstruct.processor)
|
||||
implementation(libs.spring.cloud.starter.config)
|
||||
// implementation(libs.spring.cloud.starter.bus.kafka)
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package de.twomartens.template;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
public class MainApplication extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
|
||||
return builder.sources(MainApplication.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MainApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI(@Value("${openapi.description}") String apiDesciption,
|
||||
@Value("${openapi.version}") String apiVersion, @Value("${openapi.title}") String apiTitle) {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title(apiTitle)
|
||||
.version(apiVersion)
|
||||
.description(apiDesciption));
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import de.twomartens.template.property.ServiceProperties;
|
||||
import java.time.Clock;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class ClockConfiguration {
|
||||
|
||||
private final ServiceProperties serviceProperties;
|
||||
|
||||
@Bean
|
||||
public Clock clock() {
|
||||
return Clock.system(serviceProperties.getDefaultTimeZone());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import de.twomartens.template.interceptor.HeaderInterceptorRest;
|
||||
import de.twomartens.template.interceptor.LoggingInterceptorRest;
|
||||
import java.time.Clock;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class InterceptorConfiguration {
|
||||
|
||||
@Bean
|
||||
public LoggingInterceptorRest loggingInterceptorRest(Clock clock) {
|
||||
return new LoggingInterceptorRest(clock);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HeaderInterceptorRest headerInterceptorRest() {
|
||||
return new HeaderInterceptorRest();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfiguration {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi swaggerApi10() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("1.0")
|
||||
.pathsToMatch("/template/v1/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import de.twomartens.template.interceptor.HeaderInterceptorRest;
|
||||
import de.twomartens.template.interceptor.LoggingInterceptorRest;
|
||||
import de.twomartens.template.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
|
||||
public class RestTemplateConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(HeaderInterceptorRest headerInterceptorRest,
|
||||
LoggingInterceptorRest loggingInterceptor,
|
||||
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
|
||||
return new RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestTemplateTimeoutInMillis())
|
||||
.setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestTemplateInMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplateRestHealthIndicator(HeaderInterceptorRest headerInterceptorRest,
|
||||
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
|
||||
return new RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestHealthIndicatorTimeoutInMillis())
|
||||
.setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestHealthIndicatorInMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import de.twomartens.template.monitoring.statusprobe.CountBasedStatusProbe;
|
||||
import de.twomartens.template.monitoring.statusprobe.StatusProbe;
|
||||
import de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality;
|
||||
import de.twomartens.template.monitoring.statusprobe.StatusProbeLogger;
|
||||
import java.time.Clock;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class StatusProbeConfiguration {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
@Bean
|
||||
public StatusProbeLogger statusProbeLogger() {
|
||||
return new StatusProbeLogger(clock);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StatusProbe testStatusProbe(StatusProbeLogger statusProbeLogger) {
|
||||
return new CountBasedStatusProbe(1,
|
||||
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package de.twomartens.template.configuration;
|
||||
|
||||
import de.twomartens.template.interceptor.HeaderInterceptorRest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private final HeaderInterceptorRest headerInterceptorRest;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(headerInterceptorRest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
CorsRegistration registration = registry.addMapping("/**");
|
||||
registration.allowedOrigins("*");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package de.twomartens.template.controller;
|
||||
|
||||
import de.twomartens.template.exception.HttpStatusException;
|
||||
import de.twomartens.template.model.dto.ErrorMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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)
|
||||
@Slf4j
|
||||
public class ExceptionController extends ResponseEntityExceptionHandler {
|
||||
|
||||
@ExceptionHandler(HttpStatusException.class)
|
||||
public ResponseEntity<ErrorMessage> handleException(HttpStatusException e) {
|
||||
if (e.getCause() != null) {
|
||||
log.info(e.getCause().toString(), e.getCause());
|
||||
} else {
|
||||
log.info(e.toString());
|
||||
}
|
||||
return ResponseEntity.status(e.getStatus()).body(ErrorMessage.builder()
|
||||
.message(e.getMessage())
|
||||
.build());
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<ErrorMessage> handleRuntimeException(RuntimeException e) {
|
||||
log.error("unexpected exception occurred", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorMessage.builder()
|
||||
.message(e.getMessage())
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package de.twomartens.template.controller;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.zip.ZipEntry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
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;
|
||||
|
||||
/**
|
||||
* used to show version and title information on html pages
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = "/vorlage")
|
||||
public class VersionHtmlController {
|
||||
|
||||
@GetMapping(path = "/html/version.html")
|
||||
public String version() {
|
||||
return "version";
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
public static class VersionControllerAdvice {
|
||||
|
||||
@ModelAttribute("version")
|
||||
public String getApplicationVersion() {
|
||||
return getTitle() + " " + getVersion();
|
||||
}
|
||||
|
||||
@ModelAttribute("footerString")
|
||||
public String getApplicationVersion(@RequestHeader("host") String hostName) {
|
||||
return getTitle() + " " + getVersion() + " - " + hostName;
|
||||
}
|
||||
|
||||
private String getTitle() {
|
||||
return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationTitle())
|
||||
.filter(s -> !s.isBlank())
|
||||
.orElse("application");
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationVersion())
|
||||
.filter(s -> !s.isBlank())
|
||||
.orElse("DEVELOPER");
|
||||
}
|
||||
|
||||
@ModelAttribute("hostname")
|
||||
public String getHostname() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
} catch (UnknownHostException e) {
|
||||
log.warn(e.toString(), e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@ModelAttribute("manifest")
|
||||
private Collection<String> getManifest() {
|
||||
try {
|
||||
URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
|
||||
String jarFileName = Paths.get(location.toURI()).toString();
|
||||
try (JarFile jarFile = new JarFile(jarFileName)) {
|
||||
ZipEntry entry = jarFile.getEntry(JarFile.MANIFEST_NAME);
|
||||
try (InputStream in = jarFile.getInputStream(entry)) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().toList();
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// do nothing if manifest file is not available
|
||||
} catch (Exception e) {
|
||||
log.info(e.toString(), e);
|
||||
}
|
||||
return List.of(getTitle() + " " + getVersion());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package de.twomartens.template.controller.v1;
|
||||
|
||||
import de.twomartens.template.mapper.v1.GreetingMapper;
|
||||
import de.twomartens.template.model.dto.v1.Greeting;
|
||||
import de.twomartens.template.service.GreetingService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = "/template/v1")
|
||||
@Tag(name = "Greeting Example", description = "all requests relating to greetings")
|
||||
public class GreetingRestController {
|
||||
|
||||
private final GreetingMapper mapper = Mappers.getMapper(GreetingMapper.class);
|
||||
|
||||
private final GreetingService service;
|
||||
|
||||
@Operation(
|
||||
summary = "Returns a greeting message",
|
||||
responses = {@ApiResponse(
|
||||
responseCode = "200")
|
||||
}
|
||||
)
|
||||
@GetMapping("/greeting")
|
||||
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
|
||||
return mapper.map(service.createGreeting(name));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Posts a greeting message to db",
|
||||
responses = {@ApiResponse(
|
||||
responseCode = "200")
|
||||
}
|
||||
)
|
||||
@PostMapping("/greeting")
|
||||
public void postGreeting(@RequestBody Greeting greeting) {
|
||||
service.postGreeting(mapper.map(greeting));
|
||||
}
|
||||
|
||||
@Hidden
|
||||
@GetMapping("/healthCheck")
|
||||
public String checkHealth(@RequestParam(value = "message") String message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package de.twomartens.template.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public class HttpStatusException extends RuntimeException {
|
||||
|
||||
private final HttpStatus status;
|
||||
|
||||
public HttpStatusException(HttpStatus status, String message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public HttpStatusException(HttpStatus status, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.twomartens.template.interceptor;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Simple implementation of {@link ClientHttpResponse} that reads the response's body into memory, thus allowing for
|
||||
* multiple invocations of {@link #getBody()}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
|
||||
|
||||
private final ClientHttpResponse response;
|
||||
|
||||
@Nullable
|
||||
private byte[] body;
|
||||
|
||||
|
||||
public BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public HttpStatusCode getStatusCode() throws IOException {
|
||||
return this.response.getStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public int getRawStatusCode() throws IOException {
|
||||
return this.response.getRawStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatusText() throws IOException {
|
||||
return this.response.getStatusText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.response.getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() throws IOException {
|
||||
if (this.body == null) {
|
||||
this.body = StreamUtils.copyToByteArray(this.response.getBody());
|
||||
}
|
||||
return new ByteArrayInputStream(this.body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.response.close();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package de.twomartens.template.interceptor;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
public abstract class HeaderInterceptor {
|
||||
|
||||
public static final String LOGGER_TRACE_ID = "trace.id";
|
||||
public static final String LOGGER_REQTYPE_ID = "REQTYPE";
|
||||
|
||||
public static final String HEADER_FIELD_TRACE_ID = "X-TraceId";
|
||||
public static final String HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid";
|
||||
public static final String HEADER_FIELD_TYPE_ID = "x-type";
|
||||
|
||||
public static final String REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK";
|
||||
public static final String REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST";
|
||||
public static final String REQ_TYPE_SERVER_TEST = "SERVER_TEST";
|
||||
public static final String REQ_TYPE_WARMUP = "WARMUP";
|
||||
|
||||
public String getTraceId() {
|
||||
return Optional.ofNullable(MDC.get(LOGGER_TRACE_ID))
|
||||
.filter(s -> !s.isBlank())
|
||||
.orElse(createNewTraceId());
|
||||
}
|
||||
|
||||
public Optional<String> getRequestType() {
|
||||
return Optional.ofNullable(MDC.get(LOGGER_REQTYPE_ID))
|
||||
.filter(requestType -> !requestType.isBlank());
|
||||
}
|
||||
|
||||
public InterceptorCloseables setTraceId(String traceId) {
|
||||
return new InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId));
|
||||
}
|
||||
|
||||
public InterceptorCloseables mark(String requestType) {
|
||||
return new InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType));
|
||||
}
|
||||
|
||||
public InterceptorCloseables set(String traceId, String requestType) {
|
||||
return requestType != null
|
||||
? new InterceptorCloseables(setTraceId(traceId), mark(requestType))
|
||||
: setTraceId(traceId);
|
||||
}
|
||||
|
||||
public InterceptorCloseables markAsHealthCheck() {
|
||||
return new InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()));
|
||||
}
|
||||
|
||||
public InterceptorCloseables markAsIntegrationTest() {
|
||||
return new InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()));
|
||||
}
|
||||
|
||||
public InterceptorCloseables markAsServerTest() {
|
||||
return new InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()));
|
||||
}
|
||||
|
||||
public InterceptorCloseables markAsWarmup() {
|
||||
return new InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()));
|
||||
}
|
||||
|
||||
private static String createNewTraceId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
public static class InterceptorCloseables implements Closeable {
|
||||
|
||||
private final Closeable[] closeables;
|
||||
|
||||
private InterceptorCloseables(Closeable... closeables) {
|
||||
this.closeables = closeables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (Closeable closeable : closeables) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception ignored) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package de.twomartens.template.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
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.lang.NonNull;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
public class HeaderInterceptorRest extends HeaderInterceptor
|
||||
implements HandlerInterceptor, ClientHttpRequestInterceptor {
|
||||
|
||||
public static final String CLASS_NAME = HeaderInterceptorRest.class.getName();
|
||||
|
||||
// ClientHttpRequestInterceptor
|
||||
@Override
|
||||
@NonNull
|
||||
public ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
request.getHeaders().add(HEADER_FIELD_TRACE_ID, getTraceId());
|
||||
getRequestType().ifPresent(requestType -> request.getHeaders().add(HEADER_FIELD_TYPE_ID, requestType));
|
||||
try {
|
||||
return execution.execute(request, body);
|
||||
} finally {
|
||||
request.getHeaders().remove(HEADER_FIELD_TRACE_ID);
|
||||
request.getHeaders().remove(HEADER_FIELD_TYPE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
||||
@NonNull Object handler) {
|
||||
String traceId = extractTraceId(request);
|
||||
String requestType = extractRequestType(request);
|
||||
InterceptorCloseables closeable = set(traceId, requestType);
|
||||
request.setAttribute(CLASS_NAME, closeable);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String extractTraceId(HttpServletRequest request) {
|
||||
return Optional.ofNullable(request.getHeader(HEADER_FIELD_TRACE_ID)).filter(s -> !s.isBlank())
|
||||
.or(() -> Optional.ofNullable(request.getHeader(HEADER_FIELD_B3_TRACE_ID)).filter(s -> !s.isBlank()))
|
||||
.orElse(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public static String extractRequestType(HttpServletRequest request) {
|
||||
return Optional.ofNullable(request.getHeader(HEADER_FIELD_TYPE_ID))
|
||||
.filter(reqType -> !reqType.isBlank())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
@Override
|
||||
public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
||||
@NonNull Object handler, ModelAndView modelAndView) {
|
||||
Optional.ofNullable(request.getAttribute(CLASS_NAME))
|
||||
.filter(InterceptorCloseables.class::isInstance)
|
||||
.map(InterceptorCloseables.class::cast)
|
||||
.ifPresent(InterceptorCloseables::close);
|
||||
}
|
||||
}
|
|
@ -1,419 +0,0 @@
|
|||
package de.twomartens.template.interceptor;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
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.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.Builder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.MarkerManager.Log4jMarker;
|
||||
import org.apache.logging.log4j.message.StringMapMessage;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
@Log4j2
|
||||
@RequiredArgsConstructor
|
||||
public class LoggingInterceptorRest implements Filter, ClientHttpRequestInterceptor {
|
||||
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
public static final Marker MARKER = new Log4jMarker("communication");
|
||||
|
||||
private static final int MAX_LOG_SIZE = 20480; // 20 KB - logging could fail with bigger logmessages
|
||||
|
||||
public static final String DIRECTION_IN = "inbound";
|
||||
public static final String DIRECTION_OUT = "outbound";
|
||||
public static final String PROTOCOL_NAME = "http";
|
||||
|
||||
public static final String PARAM_URL_FULL = "url.full";
|
||||
public static final String PARAM_URL_DOMAIN = "url.domain";
|
||||
public static final String PARAM_URL_EXTENSION = "url.extension";
|
||||
public static final String PARAM_URL_PATH = "url.path";
|
||||
public static final String PARAM_URL_PORT = "url.port";
|
||||
public static final String PARAM_URL_SCHEME = "url.scheme";
|
||||
public static final String PARAM_URL_QUERY = "url.query";
|
||||
public static final String PARAM_BUSINESS_TYPE = "http.request.type";
|
||||
public static final String PARAM_REQUEST_BODY = "http.request.body.content";
|
||||
public static final String PARAM_RESPONSE_BODY = "http.response.body.content";
|
||||
public static final String PARAM_RESPONSE_STATUS = "http.response.status_code";
|
||||
public static final String PARAM_REQUEST_HEADERS = "http.request.headers";
|
||||
public static final String PARAM_RESPONSE_HEADERS = "http.response.headers";
|
||||
public static final String PARAM_REQUEST_BYTES = "http.request.body.bytes";
|
||||
public static final String PARAM_RESPONSE_BYTES = "http.response.body.bytes";
|
||||
public static final String PARAM_REQUEST_MIMETYPE = "http.request.mime_type";
|
||||
public static final String PARAM_RESPONSE_MIMETYPE = "http.response.mime_type";
|
||||
public static final String PARAM_REQUEST_METHOD = "http.request.method";
|
||||
public static final String PARAM_REQUEST_REFERER = "http.request.referrer";
|
||||
public static final String PARAM_REQUEST_TIME = "event.start";
|
||||
public static final String PARAM_RESPONSE_TIME = "event.end";
|
||||
public static final String PARAM_DURATION = "event.duration";
|
||||
public static final String PARAM_USER_AGENT = "user_agent.original";
|
||||
public static final String PARAM_DIRECTION = "network.direction";
|
||||
public static final String PARAM_PROTOCOL = "network.protocol";
|
||||
|
||||
private final FieldLogBehaviour requestLogBehaviour;
|
||||
private final FieldLogBehaviour responseLogBehaviour;
|
||||
private final Clock clock;
|
||||
|
||||
public LoggingInterceptorRest(Clock clock) {
|
||||
this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
ZonedDateTime requestTime = ZonedDateTime.now(clock);
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(
|
||||
(HttpServletResponse) response);
|
||||
|
||||
byte[] fullResponseBytes = null;
|
||||
Throwable throwable = null;
|
||||
String responseBody = null;
|
||||
int httpStatusCode = -1;
|
||||
|
||||
try {
|
||||
try {
|
||||
chain.doFilter(
|
||||
requestLogBehaviour != FieldLogBehaviour.NEVER ? requestWrapper : httpRequest,
|
||||
responseWrapper);
|
||||
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
|
||||
fullResponseBytes = responseWrapper.getContentAsByteArray();
|
||||
}
|
||||
httpStatusCode = responseWrapper.getStatus();
|
||||
} finally {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throwable = e;
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
int responseSize = responseWrapper.getContentSize();
|
||||
Map<String, Collection<String>> responseHeaders = extractHeaders(
|
||||
responseWrapper.getHeaderNames().iterator(),
|
||||
s -> responseWrapper.getHeaders(s).iterator());
|
||||
if ((responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS) && fullResponseBytes != null) {
|
||||
responseBody = new String(fullResponseBytes,
|
||||
determineResponseEncoding(responseHeaders, fullResponseBytes));
|
||||
}
|
||||
URL requestUrl = new URL(Optional.ofNullable(httpRequest.getQueryString())
|
||||
.map(qs -> httpRequest.getRequestURL().toString() + "?" + qs)
|
||||
.orElse(httpRequest.getRequestURL().toString()));
|
||||
Map<String, Collection<String>> requestHeaders = extractHeaders(
|
||||
httpRequest.getHeaderNames().asIterator(),
|
||||
s -> httpRequest.getHeaders(s).asIterator());
|
||||
String requestBody = null;
|
||||
String businessType = null;
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
||||
byte[] fullRequestBytes = requestWrapper.getContentAsByteArray();
|
||||
if (fullRequestBytes != null) {
|
||||
requestBody = new String(fullRequestBytes,
|
||||
determineRequestEncoding(requestHeaders, fullRequestBytes));
|
||||
}
|
||||
businessType = determineBusinessType(requestUrl, requestBody);
|
||||
}
|
||||
log(LogMessage.builder()
|
||||
.requestHeaders(requestHeaders)
|
||||
.responseHeaders(responseHeaders)
|
||||
.url(requestUrl)
|
||||
.method(httpRequest.getMethod())
|
||||
.requestMimeType(typeToString(request.getContentType()))
|
||||
.responseMimeType(typeToString(response.getContentType()))
|
||||
.requestBody(requestBody)
|
||||
.responseBody(responseBody)
|
||||
.requestSize(httpRequest.getContentLength())
|
||||
.responseSize(responseSize)
|
||||
.httpStatus(httpStatusCode)
|
||||
.direction(DIRECTION_IN)
|
||||
.requestTime(requestTime)
|
||||
.responseTime(ZonedDateTime.now(clock))
|
||||
.traceId(HeaderInterceptorRest.extractTraceId(httpRequest))
|
||||
.requestType(HeaderInterceptorRest.extractRequestType(httpRequest))
|
||||
.businessType(businessType)
|
||||
.throwable(throwable)
|
||||
.build());
|
||||
} catch (RuntimeException e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isError(int httpStatusCode) {
|
||||
return httpStatusCode >= 400 && httpStatusCode < 600;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ClientHttpResponse intercept(@NonNull HttpRequest request,
|
||||
@NonNull byte[] requestBytes, @NonNull ClientHttpRequestExecution execution)
|
||||
throws IOException {
|
||||
|
||||
ZonedDateTime requestTime = ZonedDateTime.now(clock);
|
||||
int responseSize = 0;
|
||||
Map<String, Collection<String>> responseHeaders = Collections.emptyMap();
|
||||
MediaType responseMediaType = null;
|
||||
int httpStatusCode = -1;
|
||||
Throwable throwable = null;
|
||||
String requestBody = null;
|
||||
String responseBody = null;
|
||||
String businessType = null;
|
||||
|
||||
try {
|
||||
BufferingClientHttpResponseWrapper result = new BufferingClientHttpResponseWrapper(
|
||||
execution.execute(request, requestBytes));
|
||||
byte[] responseBytes = StreamUtils.copyToByteArray(result.getBody());
|
||||
responseSize = responseBytes.length;
|
||||
responseHeaders = extractHeaders(result.getHeaders());
|
||||
responseMediaType = result.getHeaders().getContentType();
|
||||
httpStatusCode = result.getStatusCode().value();
|
||||
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
||||
responseBody = new String(responseBytes,
|
||||
determineRequestEncoding(responseHeaders, responseBytes));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throwable = e;
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
URL url = request.getURI().toURL();
|
||||
Map<String, Collection<String>> requestHeaders = extractHeaders(request.getHeaders());
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
||||
requestBody = new String(requestBytes,
|
||||
determineRequestEncoding(requestHeaders, requestBytes));
|
||||
businessType = determineBusinessType(url, requestBody);
|
||||
}
|
||||
log(LogMessage.builder()
|
||||
.requestHeaders(requestHeaders)
|
||||
.responseHeaders(responseHeaders)
|
||||
.url(url)
|
||||
.method(Objects.requireNonNull(request.getMethod()).toString())
|
||||
.requestMimeType(typeToString(request.getHeaders().getContentType()))
|
||||
.requestBody(requestBody)
|
||||
.responseBody(responseBody)
|
||||
.responseMimeType(typeToString(responseMediaType))
|
||||
.requestSize(requestBytes.length)
|
||||
.responseSize(responseSize)
|
||||
.httpStatus(httpStatusCode)
|
||||
.direction(DIRECTION_OUT)
|
||||
.requestTime(requestTime)
|
||||
.responseTime(ZonedDateTime.now(clock))
|
||||
.businessType(businessType)
|
||||
.throwable(throwable)
|
||||
.build());
|
||||
} catch (RuntimeException e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void log(LogMessage logMessage) {
|
||||
StringMapMessage stringMapMessage = new 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().getHost());
|
||||
addLogString(stringMapMessage, PARAM_URL_EXTENSION,
|
||||
extractExtension(logMessage.url().getPath()));
|
||||
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url().getPath());
|
||||
addLogString(stringMapMessage, PARAM_URL_PORT, Integer.toString(logMessage.url().getPort()));
|
||||
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url().getProtocol());
|
||||
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url().getQuery());
|
||||
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, Integer.toString(logMessage.requestSize()));
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES,
|
||||
Integer.toString(logMessage.responseSize()));
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS,
|
||||
Integer.toString(logMessage.httpStatus()));
|
||||
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,
|
||||
Long.toString(getDurationBetweenRequestAndResponseTime(logMessage).toNanos()));
|
||||
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, logMessage.throwable());
|
||||
}
|
||||
|
||||
private Duration getDurationBetweenRequestAndResponseTime(LogMessage logMessage) {
|
||||
return Duration.between(logMessage.requestTime(), logMessage.responseTime());
|
||||
}
|
||||
|
||||
private String getHeader(Map<String, Collection<String>> headers, String headerKey) {
|
||||
return headers.entrySet().stream()
|
||||
.filter(e -> e.getKey().equalsIgnoreCase(headerKey))
|
||||
.flatMap(e -> e.getValue().stream())
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void addLogString(StringMapMessage stringMapMessage, String key, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
stringMapMessage.with(key, value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Collection<String>> extractHeaders(Iterator<String> headerNames,
|
||||
Function<String, Iterator<String>> headerValuesSupplier) {
|
||||
Map<String, Collection<String>> requestHeaders = new HashMap<>();
|
||||
while (headerNames.hasNext()) {
|
||||
String name = headerNames.next();
|
||||
Collection<String> values = requestHeaders.computeIfAbsent(name, n -> new TreeSet<>());
|
||||
Iterator<String> headerValues = headerValuesSupplier.apply(name);
|
||||
while (headerValues.hasNext()) {
|
||||
values.add(headerValues.next());
|
||||
}
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
private static Map<String, Collection<String>> extractHeaders(HttpHeaders headers) {
|
||||
Map<String, Collection<String>> result = new HashMap<>();
|
||||
for (Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String toHeaderString(Map<String, Collection<String>> headerMap) {
|
||||
return headerMap.entrySet().stream()
|
||||
.flatMap(es -> es.getValue().stream().map(v -> new KeyValuePair(es.getKey(), v)))
|
||||
.map(kv -> kv.key() + "=" + kv.value())
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static String typeToString(String contentType) {
|
||||
try {
|
||||
return Optional.ofNullable(contentType)
|
||||
.map(MediaType::parseMediaType)
|
||||
.map(LoggingInterceptorRest::typeToString)
|
||||
.orElse(null);
|
||||
} catch (RuntimeException e) {
|
||||
log.info(e.toString(), e);
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static String typeToString(MediaType mediaType) {
|
||||
try {
|
||||
return Optional.ofNullable(mediaType)
|
||||
.map(m -> m.getType() + "/" + m.getSubtype())
|
||||
.orElse(null);
|
||||
} catch (RuntimeException e) {
|
||||
log.info(e.toString(), e);
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractExtension(String fileName) {
|
||||
return Optional.ofNullable(fileName)
|
||||
.filter(name -> name.contains("."))
|
||||
.map(name -> name.substring(name.lastIndexOf('.') + 1))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static String cutToMaxLength(String string) {
|
||||
if (string != null && string.length() > MAX_LOG_SIZE) {
|
||||
return string.substring(0, MAX_LOG_SIZE);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns null, but can be overridden to implement more complex logic
|
||||
*/
|
||||
public String determineBusinessType(URL requestUrl, String requestBody) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
public Charset determineRequestEncoding(Map<String, Collection<String>> requestHeaders,
|
||||
byte[] fullRequest) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
public Charset determineResponseEncoding(Map<String, Collection<String>> responseHeaders,
|
||||
byte[] fullResponse) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
@Builder
|
||||
private record LogMessage(Map<String, Collection<String>> requestHeaders,
|
||||
Map<String, Collection<String>> responseHeaders, URL url, String method,
|
||||
String requestMimeType, String responseMimeType, String requestBody,
|
||||
String responseBody,
|
||||
int requestSize, int responseSize, int httpStatus, String direction,
|
||||
ZonedDateTime requestTime, ZonedDateTime responseTime, String traceId,
|
||||
String requestType,
|
||||
String businessType, Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
private record KeyValuePair(String key, String value) {
|
||||
|
||||
}
|
||||
|
||||
public enum FieldLogBehaviour {
|
||||
NEVER, ONLY_ON_ERROR, ALWAYS
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package de.twomartens.template.mapper.v1;
|
||||
|
||||
import de.twomartens.template.model.db.Greeting;
|
||||
import org.mapstruct.CollectionMappingStrategy;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.NullValueCheckStrategy;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface GreetingMapper {
|
||||
|
||||
de.twomartens.template.model.dto.v1.Greeting map(Greeting greeting);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
Greeting map(de.twomartens.template.model.dto.v1.Greeting greeting);
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package de.twomartens.template.model.db;
|
||||
|
||||
import java.util.Date;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Document
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Greeting {
|
||||
|
||||
@Id
|
||||
ObjectId id;
|
||||
|
||||
@CreatedDate
|
||||
Date created;
|
||||
|
||||
@LastModifiedDate
|
||||
Date lastModified;
|
||||
|
||||
String message;
|
||||
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package de.twomartens.template.model.dto;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ErrorMessage {
|
||||
|
||||
String message;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package de.twomartens.template.model.dto.v1;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Schema(description = "Data container for a greeting message")
|
||||
public class Greeting {
|
||||
|
||||
@NonNull
|
||||
@Schema(description = "Data container for a greeting message", example = "Hello Helmut!",
|
||||
defaultValue = "Hello World!")
|
||||
private final String message;
|
||||
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package de.twomartens.template.monitoring.actuator;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.slf4j.MDC.MDCCloseable;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public abstract class AbstractHealthIndicator implements HealthIndicator {
|
||||
|
||||
public static final String HOST = "localhost";
|
||||
public static final String HTTP_PREFIX = "http://";
|
||||
public static final String HOST_PORT_SEPERATOR = ":";
|
||||
public static final String PATH_SEPERATOR = "/";
|
||||
public static final String PARAMETER_SEPERATOR = "?";
|
||||
public static final String DETAIL_ENDPOINT_KEY = "endpoint";
|
||||
|
||||
private final String logStatusDownMessage = String.format("health indicator '%s' invoked with status '%s'",
|
||||
indicatorName(), Status.DOWN.getCode());
|
||||
private final String logStatusUpMessage = String.format("health indicator '%s' invoked with status '%s'",
|
||||
indicatorName(), Status.UP.getCode());
|
||||
private final Clock clock;
|
||||
private final Preparable preparable;
|
||||
private boolean firstTime = true;
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
protected abstract Health determineHealth();
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
try (Closeable ignored = preparable.prepare()) {
|
||||
Health result = null;
|
||||
Exception exception = null;
|
||||
long start = clock.millis();
|
||||
try {
|
||||
result = determineHealth();
|
||||
} catch (RuntimeException e) {
|
||||
exception = e;
|
||||
result = Health.down().withException(e).build();
|
||||
} finally {
|
||||
logInvocation(result, exception, start, clock.millis());
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
log.error("unexpected exception occurred", e);
|
||||
return Health.down(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
private void logInvocation(Health health, Exception exception, long start, long end) {
|
||||
Duration duration = Duration.ofMillis(end - start);
|
||||
try (MDCCloseable ignored = MDC.putCloseable("event.duration", Long.toString(duration.toNanos()))) {
|
||||
if (exception != null || health == null) {
|
||||
log.error(logStatusDownMessage, exception);
|
||||
firstTime = true;
|
||||
} else if (health.getStatus() == Status.DOWN) {
|
||||
log.warn(logStatusDownMessage);
|
||||
firstTime = true;
|
||||
} else if (firstTime) {
|
||||
log.info(logStatusUpMessage);
|
||||
firstTime = false;
|
||||
} else {
|
||||
log.trace(logStatusUpMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String indicatorName() {
|
||||
return this.getClass().getSimpleName()
|
||||
.replace("HealthIndicator", "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package de.twomartens.template.monitoring.actuator;
|
||||
|
||||
import de.twomartens.template.monitoring.statusprobe.StatusProbe;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Health.Builder;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractStatusProbeHealthIndicator extends AbstractHealthIndicator
|
||||
implements HealthIndicator {
|
||||
|
||||
public static final String MESSAGE_KEY = "message";
|
||||
public static final String LAST_STATUS_CHANGE_KEY = "lastStatusChange";
|
||||
|
||||
private final StatusProbe statusProbe;
|
||||
|
||||
public AbstractStatusProbeHealthIndicator(Clock timeProvider, Preparable headerInterceptor,
|
||||
StatusProbe statusProbe) {
|
||||
super(timeProvider, headerInterceptor);
|
||||
this.statusProbe = statusProbe;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Health determineHealth() {
|
||||
Builder healthBuilder = Health.status(statusProbe.getStatus());
|
||||
Optional.ofNullable(statusProbe.getLastStatusChange())
|
||||
.ifPresent(l -> healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, l));
|
||||
Optional.ofNullable(statusProbe.getThrowable()).ifPresent(healthBuilder::withException);
|
||||
Optional.ofNullable(statusProbe.getMessage())
|
||||
.ifPresent(m -> healthBuilder.withDetail(MESSAGE_KEY, m));
|
||||
return healthBuilder.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package de.twomartens.template.monitoring.actuator;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Preparable {
|
||||
|
||||
Closeable prepare();
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package de.twomartens.template.monitoring.actuator;
|
||||
|
||||
import de.twomartens.template.interceptor.HeaderInterceptorRest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* A Health check which checks if the rest services are working.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* There should be a health check which is ok not before all data is loaded.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RestHealthIndicator extends AbstractHealthIndicator implements HealthIndicator {
|
||||
|
||||
private static final String URL_PATH = "/template/v1/healthCheck";
|
||||
private static final String GET_PARAMETER = "message=";
|
||||
|
||||
private final SecureRandom randomizer = new SecureRandom();
|
||||
private final RestTemplate restTemplateRestHealthIndicator;
|
||||
private final String urlPrefix;
|
||||
|
||||
public RestHealthIndicator(Clock clock, HeaderInterceptorRest interceptor,
|
||||
ServerProperties serverProperties, RestTemplate restTemplateRestHealthIndicator) {
|
||||
super(clock, interceptor::markAsHealthCheck);
|
||||
this.restTemplateRestHealthIndicator = restTemplateRestHealthIndicator;
|
||||
urlPrefix = HTTP_PREFIX + HOST + HOST_PORT_SEPERATOR + serverProperties.getPort()
|
||||
+ URL_PATH + PARAMETER_SEPERATOR + GET_PARAMETER;
|
||||
}
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
@Override
|
||||
protected Health determineHealth() {
|
||||
String random = Integer.toString(randomizer.nextInt(100000, 999999));
|
||||
String url = urlPrefix + "{random}";
|
||||
ResponseEntity<String> response = restTemplateRestHealthIndicator.getForEntity(url, String.class, random);
|
||||
Status status = Optional.ofNullable(response.getBody())
|
||||
.filter(random::equals)
|
||||
.map(m -> Status.UP)
|
||||
.orElse(Status.DOWN);
|
||||
return Health.status(status)
|
||||
.withDetail(DETAIL_ENDPOINT_KEY, url)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
public class CountBasedStatusProbe extends StatusProbe {
|
||||
|
||||
private final AtomicInteger failureCount = new AtomicInteger(0);
|
||||
|
||||
private final int maxFailureCount;
|
||||
|
||||
public CountBasedStatusProbe(int maxFailureCount, Clock clock, StatusProbeCriticality criticality, String name,
|
||||
StatusProbeLogger statusProbeLogger) {
|
||||
super(clock, criticality, name, statusProbeLogger);
|
||||
this.maxFailureCount = maxFailureCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
|
||||
if (status == Status.DOWN) {
|
||||
int failureCount = this.failureCount.incrementAndGet();
|
||||
if (failureCount > maxFailureCount) {
|
||||
super.setStatus(status, throwable, message);
|
||||
}
|
||||
} else if (status == Status.UP) {
|
||||
this.failureCount.set(0);
|
||||
super.setStatus(status, throwable, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class PercentageBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe {
|
||||
|
||||
private final int maxFailurePercent;
|
||||
|
||||
private int requestCount = 0;
|
||||
private int downCount = 0;
|
||||
private Throwable throwable;
|
||||
private String message;
|
||||
|
||||
public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock,
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name,
|
||||
StatusProbeLogger statusProbeLogger) {
|
||||
this(maxFailurePercent, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name,
|
||||
statusProbeLogger);
|
||||
}
|
||||
|
||||
public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock,
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality,
|
||||
String name, StatusProbeLogger statusProbeLogger) {
|
||||
super(clock, criticality, name, statusProbeLogger);
|
||||
this.maxFailurePercent = maxFailurePercent;
|
||||
scheduleTask(threadPoolTaskScheduler, schedulePeriod);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
|
||||
if (status == Status.DOWN) {
|
||||
downCount++;
|
||||
this.throwable = throwable;
|
||||
this.message = message;
|
||||
}
|
||||
requestCount++;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
requestCount = 0;
|
||||
downCount = 0;
|
||||
throwable = null;
|
||||
message = null;
|
||||
}
|
||||
|
||||
public synchronized void runScheduledTask() {
|
||||
if (requestCount > 0 && (downCount * 100.0 / requestCount) > maxFailurePercent) {
|
||||
super.setStatus(Status.DOWN, throwable, message);
|
||||
} else {
|
||||
super.setStatus(Status.UP, null, null);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
|
||||
public interface ScheduledStatusProbe {
|
||||
|
||||
void runScheduledTask();
|
||||
|
||||
default void scheduleTask(ThreadPoolTaskScheduler threadPoolTaskScheduler,
|
||||
Duration schedulePeriod) {
|
||||
PeriodicTrigger periodicTrigger = new PeriodicTrigger(
|
||||
Duration.ofSeconds(schedulePeriod.toSeconds()));
|
||||
threadPoolTaskScheduler.schedule(this::runScheduledTask, periodicTrigger);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
@Getter
|
||||
public class StatusProbe {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private Status status = Status.UP;
|
||||
private Throwable throwable = null;
|
||||
private String message = null;
|
||||
private ZonedDateTime lastStatusChange;
|
||||
private final StatusProbeLogger statusProbeLogger;
|
||||
private final String name;
|
||||
|
||||
public StatusProbe(Clock clock, StatusProbeCriticality criticality, String name,
|
||||
StatusProbeLogger statusProbeLogger) {
|
||||
this.clock = clock;
|
||||
this.name = name;
|
||||
this.statusProbeLogger = statusProbeLogger;
|
||||
statusProbeLogger.registerStatusProbe(name, criticality);
|
||||
}
|
||||
|
||||
protected void setStatus(Status status, Throwable throwable, String message) {
|
||||
if (status != this.status) {
|
||||
lastStatusChange = ZonedDateTime.now(clock);
|
||||
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable);
|
||||
}
|
||||
this.status = status;
|
||||
this.throwable = throwable;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public void up() {
|
||||
setStatus(Status.UP, null, null);
|
||||
}
|
||||
|
||||
public void up(String message) {
|
||||
setStatus(Status.UP, null, message);
|
||||
}
|
||||
|
||||
public void down() {
|
||||
setStatus(Status.DOWN, null, null);
|
||||
}
|
||||
|
||||
public void down(Throwable throwable) {
|
||||
setStatus(Status.DOWN, throwable, null);
|
||||
}
|
||||
|
||||
public void down(String message) {
|
||||
setStatus(Status.DOWN, null, message);
|
||||
}
|
||||
|
||||
protected void down(Throwable throwable, String message) {
|
||||
setStatus(Status.DOWN, throwable, message);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
|
||||
public enum StatusProbeCriticality {
|
||||
K1, K2, K3
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K1;
|
||||
import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K2;
|
||||
import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K3;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.MarkerManager;
|
||||
import org.apache.logging.log4j.message.StringMapMessage;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
public class StatusProbeLogger {
|
||||
|
||||
private static final Marker MARKER = MarkerManager.getMarker("statusprobe");
|
||||
|
||||
private static final String LABEL_CRITICALITY = "label.status.criticality";
|
||||
private static final String LABEL_STATUS = "label.status.status";
|
||||
private static final String LABEL_REASON = "label.status.reason";
|
||||
private static final String LABEL_MESSAGE = "label.status.description";
|
||||
private static final String LABEL_LAST_STATUS_CHANGE = "label.status.last_change";
|
||||
|
||||
private final Clock clock;
|
||||
private final Logger commLog;
|
||||
private final Map<ProbeIdent, Status> statusProbeToStatus = new HashMap<>();
|
||||
|
||||
public StatusProbeLogger(Clock clock) {
|
||||
this(clock, LogManager.getLogger("statusprobe"));
|
||||
}
|
||||
|
||||
/**
|
||||
* only for testing purposes
|
||||
*/
|
||||
StatusProbeLogger(Clock clock, Logger commLog) {
|
||||
this.clock = clock;
|
||||
this.commLog = commLog;
|
||||
}
|
||||
|
||||
public void registerStatusProbe(String name, StatusProbeCriticality criticality) {
|
||||
statusProbeToStatus.put(new ProbeIdent(name, criticality), Status.UP);
|
||||
logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null);
|
||||
}
|
||||
|
||||
public void logStatusChange(String name, String message, Status status, ZonedDateTime lastStatusChange,
|
||||
Throwable throwable) {
|
||||
ProbeIdent probeIdent = getProbeIdent(name);
|
||||
if (probeIdent == null) {
|
||||
probeIdent = new ProbeIdent(name, K1);
|
||||
}
|
||||
statusProbeToStatus.put(probeIdent, status);
|
||||
createLog(message, lastStatusChange, throwable);
|
||||
}
|
||||
|
||||
private ProbeIdent getProbeIdent(String name) {
|
||||
return statusProbeToStatus.keySet().stream().filter(key -> key.name.equals(name)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private void createLog(String message, ZonedDateTime lastStatusChange, Throwable throwable) {
|
||||
Status overallStatus = getOverallStatus();
|
||||
StatusProbeCriticality criticality = getOverallCriticality();
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
if (Status.UP.equals(overallStatus)) {
|
||||
commLog.info(MARKER, new StringMapMessage()
|
||||
.with(LABEL_CRITICALITY, criticality)
|
||||
.with(LABEL_STATUS, overallStatus)
|
||||
.with(LABEL_MESSAGE, message)
|
||||
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange));
|
||||
} else {
|
||||
commLog.error(MARKER, new StringMapMessage()
|
||||
.with(LABEL_CRITICALITY, criticality)
|
||||
.with(LABEL_STATUS, overallStatus)
|
||||
.with(LABEL_MESSAGE, message)
|
||||
.with(LABEL_REASON, getReason())
|
||||
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private StatusProbeCriticality getOverallCriticality() {
|
||||
List<StatusProbeCriticality> crits = statusProbeToStatus.keySet().stream().map(key -> key.criticality).toList();
|
||||
return crits.contains(K1) ? K1 : crits.contains(K2) ? K2 : K3;
|
||||
}
|
||||
|
||||
private Status getOverallStatus() {
|
||||
if (statusProbeToStatus.containsValue(Status.DOWN)) {
|
||||
return Status.DOWN;
|
||||
}
|
||||
return Status.UP;
|
||||
}
|
||||
|
||||
private String getReason() {
|
||||
|
||||
List<ProbeIdent> probesDown = statusProbeToStatus.entrySet().stream()
|
||||
.filter(entry -> Status.DOWN.equals(entry.getValue()))
|
||||
.map(Map.Entry::getKey)
|
||||
.toList();
|
||||
|
||||
String reasonK1 = getDownStatusProbes(probesDown, K1);
|
||||
String reasonK2 = getDownStatusProbes(probesDown, K2);
|
||||
String reasonK3 = getDownStatusProbes(probesDown, K3);
|
||||
|
||||
return "%s%s%s".formatted(reasonK1, reasonK2, reasonK3).trim();
|
||||
}
|
||||
|
||||
private String getDownStatusProbes(List<ProbeIdent> probesDown, StatusProbeCriticality criticality) {
|
||||
List<String> downProbeNames = probesDown.stream().filter(probe -> probe.criticality.equals(criticality))
|
||||
.map(probe -> probe.name).toList();
|
||||
if (downProbeNames.size() > 0) {
|
||||
return criticality + " failed: " + String.join(",", downProbeNames) + "\n";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public record ProbeIdent(String name, StatusProbeCriticality criticality) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package de.twomartens.template.monitoring.statusprobe;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
public class TimeBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private final Duration maxFailureDuration;
|
||||
|
||||
private ZonedDateTime lastSuccess;
|
||||
|
||||
private Throwable throwable = null;
|
||||
|
||||
private String message = null;
|
||||
|
||||
public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock,
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name,
|
||||
StatusProbeLogger statusProbeLogger) {
|
||||
this(maxFailureDuration, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name,
|
||||
statusProbeLogger);
|
||||
}
|
||||
|
||||
public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock,
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality,
|
||||
String name, StatusProbeLogger statusProbeLogger) {
|
||||
super(clock, criticality, name, statusProbeLogger);
|
||||
this.clock = clock;
|
||||
this.maxFailureDuration = maxFailureDuration;
|
||||
this.lastSuccess = null;
|
||||
scheduleTask(threadPoolTaskScheduler, schedulePeriod);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
|
||||
if (status == Status.DOWN) {
|
||||
this.throwable = throwable;
|
||||
this.message = message;
|
||||
} else if (status == Status.UP) {
|
||||
lastSuccess = ZonedDateTime.now(clock);
|
||||
super.setStatus(status, throwable, message);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOverdue() {
|
||||
if (lastSuccess == null) {
|
||||
return false;
|
||||
}
|
||||
Duration timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock));
|
||||
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative();
|
||||
}
|
||||
|
||||
public synchronized void runScheduledTask() {
|
||||
if (isOverdue()) {
|
||||
super.setStatus(Status.DOWN, throwable, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package de.twomartens.template.property;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ConfigurationProperties(prefix = "resttemplate.timeout")
|
||||
@Component
|
||||
public class RestTemplateTimeoutProperties {
|
||||
|
||||
@DurationUnit(ChronoUnit.MILLIS)
|
||||
private Duration readTimeoutRestHealthIndicatorInMillis;
|
||||
@DurationUnit(ChronoUnit.MILLIS)
|
||||
private Duration connectionRestHealthIndicatorTimeoutInMillis;
|
||||
@DurationUnit(ChronoUnit.MILLIS)
|
||||
private Duration readTimeoutRestTemplateInMillis;
|
||||
@DurationUnit(ChronoUnit.MILLIS)
|
||||
private Duration connectionRestTemplateTimeoutInMillis;
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package de.twomartens.template.property;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.ZoneId;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "de.twomartens.template")
|
||||
@Schema(description = "Properties, to configure this Application")
|
||||
public class ServiceProperties {
|
||||
|
||||
private ZoneId defaultTimeZone;
|
||||
|
||||
private String greeting;
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package de.twomartens.template.property;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "de.twomartens.template.statusprobe")
|
||||
@Schema(description = "Properties, to configure this Application")
|
||||
public class StatusProbeProperties {
|
||||
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration scheduleDuration;
|
||||
|
||||
@DurationUnit(ChronoUnit.MINUTES)
|
||||
private Duration maxKafkaFailureDuration;
|
||||
|
||||
private int maxBlobFailureCount;
|
||||
|
||||
private int maxFailurePercent;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package de.twomartens.template.repository;
|
||||
|
||||
import de.twomartens.template.model.db.Greeting;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
public interface GreetingRepository extends MongoRepository<Greeting, String> {
|
||||
|
||||
Optional<Greeting> findByMessageIgnoreCase(String message);
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package de.twomartens.template.service;
|
||||
|
||||
import de.twomartens.template.model.db.Greeting;
|
||||
import de.twomartens.template.property.ServiceProperties;
|
||||
import de.twomartens.template.repository.GreetingRepository;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GreetingService {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final ServiceProperties serviceProperties;
|
||||
private final Counter counter;
|
||||
|
||||
private final GreetingRepository greetingRepository;
|
||||
|
||||
public GreetingService(MeterRegistry meterRegistry, ServiceProperties serviceProperties,
|
||||
GreetingRepository greetingRepository) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.greetingRepository = greetingRepository;
|
||||
counter = meterRegistry.counter("template.callCounter");
|
||||
}
|
||||
|
||||
public Greeting createGreeting(String name) {
|
||||
log.info("Create greeting for '{}'", name);
|
||||
counter.increment();
|
||||
meterRegistry.gauge("template.nameLength", name.length());
|
||||
String greeting = serviceProperties.getGreeting();
|
||||
return Greeting.builder().message(String.format(greeting, name)).build();
|
||||
}
|
||||
|
||||
public void postGreeting(Greeting greeting) {
|
||||
greetingRepository.save(greeting);
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package de.twomartens.template.service;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.apache.logging.log4j.message.StringMapMessage;
|
||||
import org.springframework.cloud.context.refresh.ContextRefresher;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* automatically reloads application*.yaml. reload can also be triggered manually by doing a post
|
||||
* request on http://localhost:12001/actuator/refresh
|
||||
*/
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PropertyReloadService {
|
||||
|
||||
public static final int REFRESH_SECONDS = 60;
|
||||
public static final String PARAM_MESSAGE = "message";
|
||||
public static final String PARAM_PROPERTIES = "labels.properties";
|
||||
|
||||
private final ContextRefresher contextRefresher;
|
||||
|
||||
@Scheduled(fixedDelay = REFRESH_SECONDS, initialDelay = REFRESH_SECONDS, timeUnit = TimeUnit.SECONDS)
|
||||
public void refresh() {
|
||||
Set<String> properties = contextRefresher.refresh();
|
||||
if (!properties.isEmpty()) {
|
||||
log.info(new StringMapMessage()
|
||||
.with(PARAM_MESSAGE, "properties changed")
|
||||
.with(PARAM_PROPERTIES, String.join("\n", properties)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package de.twomartens.timetable
|
||||
|
||||
import de.twomartens.timetable.bahnApi.service.BahnApiService
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.data.mongodb.config.EnableMongoAuditing
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.Month
|
||||
|
||||
@EnableMongoAuditing
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
open class MainApplication(
|
||||
private val bahnApiService: BahnApiService
|
||||
) {
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun ready() {
|
||||
val result = bahnApiService.fetchStations("Köln Hbf")
|
||||
val koeln = result[0]
|
||||
val stops = bahnApiService.fetchTimetable(koeln.eva,
|
||||
LocalDate.of(2023, Month.SEPTEMBER, 23),
|
||||
LocalTime.of(17, 0))
|
||||
log.info("stations: $result")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MainApplication>(*args)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.twomartens.timetable.configuration
|
||||
|
||||
import de.twomartens.timetable.bahnApi.property.BahnApiProperties
|
||||
import de.twomartens.timetable.property.RestTemplateTimeoutProperties
|
||||
import de.twomartens.timetable.property.ServiceProperties
|
||||
import de.twomartens.timetable.property.StatusProbeProperties
|
||||
import de.twomartens.timetable.property.TimeProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(RestTemplateTimeoutProperties::class, ServiceProperties::class,
|
||||
StatusProbeProperties::class, TimeProperties::class, BahnApiProperties::class)
|
||||
open class PropertyConfiguration
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.property
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "de.twomartens.timetable")
|
||||
@Schema(description = "Properties, to configure this Application")
|
||||
class ServiceProperties {
|
||||
lateinit var greeting: String
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package de.twomartens.timetable.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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.timetable.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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
|
||||
import io.swagger.v3.oas.annotations.security.OAuthFlow
|
||||
import io.swagger.v3.oas.annotations.security.OAuthFlows
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme
|
||||
import io.swagger.v3.oas.models.OpenAPI
|
||||
import io.swagger.v3.oas.models.info.Info
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT")
|
||||
@SecurityScheme(
|
||||
name = "oauth2",
|
||||
type = SecuritySchemeType.OAUTH2,
|
||||
flows = OAuthFlows(
|
||||
implicit = OAuthFlow(
|
||||
authorizationUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/auth",
|
||||
tokenUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/token"
|
||||
)
|
||||
)
|
||||
)
|
||||
@Configuration
|
||||
open class OpenApiConfiguration {
|
||||
@Bean
|
||||
open fun customOpenAPI(
|
||||
@Value("\${openapi.description}") apiDescription: String,
|
||||
@Value("\${openapi.version}") apiVersion: String, @Value("\${openapi.title}") apiTitle: String
|
||||
): OpenAPI {
|
||||
return OpenAPI()
|
||||
.info(
|
||||
Info()
|
||||
.title(apiTitle)
|
||||
.version(apiVersion)
|
||||
.description(apiDescription)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.timetable.support.interceptor.LoggingInterceptorRest
|
||||
import de.twomartens.timetable.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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.CountBasedStatusProbe
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbe
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbeCriticality
|
||||
import de.twomartens.timetable.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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Configuration
|
||||
open class WebConfiguration(private val headerInterceptorRest: HeaderInterceptorRest) : WebMvcConfigurer {
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
registry.addInterceptor(headerInterceptorRest)
|
||||
}
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
val registration = registry.addMapping("/**")
|
||||
registration.allowedMethods(
|
||||
HttpMethod.GET.name(), HttpMethod.POST.name(),
|
||||
HttpMethod.PUT.name(), HttpMethod.HEAD.name()
|
||||
)
|
||||
registration.allowCredentials(true)
|
||||
registration.allowedOrigins(
|
||||
"http://localhost:4200",
|
||||
"https://timetable.2martens.de"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.security.SpringPolicyEnforcerFilter
|
||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
|
||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig
|
||||
import org.keycloak.util.JsonSerialization
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.Customizer
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import java.io.IOException
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfiguration {
|
||||
@Value("\${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
|
||||
private lateinit var jwkSetUri: String
|
||||
|
||||
@Value("#{environment.CLIENT_SECRET}")
|
||||
private lateinit var clientSecret: String
|
||||
|
||||
@Bean
|
||||
@Throws(Exception::class)
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.csrf { it.disable() }
|
||||
.authorizeHttpRequests { it.requestMatchers(*PERMITTED_PATHS.toTypedArray<String>()).permitAll() }
|
||||
.authorizeHttpRequests { it.requestMatchers(HttpMethod.OPTIONS).permitAll() }
|
||||
.authorizeHttpRequests { it.anyRequest().authenticated() }
|
||||
.oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer<HttpSecurity?> -> obj.jwt(Customizer.withDefaults()) }
|
||||
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter::class.java)
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
}
|
||||
|
||||
private fun createPolicyEnforcerFilter(): SpringPolicyEnforcerFilter {
|
||||
return SpringPolicyEnforcerFilter(object : ConfigurationResolver {
|
||||
override fun resolve(request: HttpRequest): PolicyEnforcerConfig {
|
||||
return try {
|
||||
val policyEnforcerConfig = JsonSerialization.readValue(
|
||||
javaClass.getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig::class.java
|
||||
)
|
||||
policyEnforcerConfig.credentials = mapOf(Pair("secret", clientSecret))
|
||||
if (request.method == HttpMethod.OPTIONS.name()) {
|
||||
// always allow options request
|
||||
policyEnforcerConfig.enforcementMode = EnforcementMode.DISABLED
|
||||
} else {
|
||||
policyEnforcerConfig.paths = PATHS
|
||||
}
|
||||
policyEnforcerConfig
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PERMITTED_PATHS: Collection<String> = listOf(
|
||||
"/timetable/v1/healthCheck",
|
||||
"/actuator/**",
|
||||
"/timetable/v1/doc/**",
|
||||
"/timetable/v1/api-docs/**",
|
||||
"/error",
|
||||
"/timetable/version",
|
||||
)
|
||||
private val PATHS = buildPathConfigs()
|
||||
|
||||
private fun buildPathConfigs(): List<PathConfig> {
|
||||
val paths: MutableList<PathConfig> = mutableListOf()
|
||||
for (path in PERMITTED_PATHS) {
|
||||
val pathConfig = PathConfig()
|
||||
pathConfig.path = path.replace("**", "*")
|
||||
pathConfig.enforcementMode = EnforcementMode.DISABLED
|
||||
paths.add(pathConfig)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package de.twomartens.timetable.support.controller
|
||||
|
||||
import de.twomartens.timetable.support.exception.HttpStatusException
|
||||
import de.twomartens.timetable.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 {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package de.twomartens.timetable.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.timetable.support.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
class HttpStatusException(val status: HttpStatus, message: String, cause: Throwable?):
|
||||
RuntimeException(message, cause)
|
|
@ -0,0 +1,45 @@
|
|||
package de.twomartens.timetable.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
|
||||
}
|
||||
|
||||
@Deprecated("Replace calls of getRawStatusCode with getStatusCode", ReplaceWith("getStatusCode()"))
|
||||
@Throws(IOException::class)
|
||||
override fun getRawStatusCode(): Int {
|
||||
return statusCode.value()
|
||||
}
|
||||
|
||||
@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.timetable.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package de.twomartens.timetable.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()
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,438 @@
|
|||
package de.twomartens.timetable.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.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 = URL("${httpRequest.requestURL}$query")
|
||||
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,3 @@
|
|||
package de.twomartens.timetable.support.model.dto
|
||||
|
||||
data class ErrorMessage(val message: String)
|
|
@ -0,0 +1,83 @@
|
|||
package de.twomartens.timetable.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 {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.timetable.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.timetable.support.monitoring.actuator
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
fun interface Preparable {
|
||||
fun prepare(): Closeable
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.timetable.property.ServiceProperties
|
||||
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: ServiceProperties
|
||||
) : 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/v1/healthCheck"
|
||||
private const val GET_PARAMETER = "message="
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package de.twomartens.timetable.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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package de.twomartens.timetable.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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package de.twomartens.timetable.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)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue