From d377cbd8862a6c3746f7e94d7df0bd716664834a Mon Sep 17 00:00:00 2001 From: Jim Martens Date: Sat, 23 Sep 2023 20:32:54 +0200 Subject: [PATCH] Changed to Kotlin --- .run/MainApplication.run.xml | 15 - .run/MainApplicationKt.run.xml | 21 + build.gradle | 13 - build.gradle.kts | 17 + buildSrc/build.gradle | 15 - buildSrc/build.gradle.kts | 19 + .../{settings.gradle => settings.gradle.kts} | 2 +- .../src/main/groovy/twomartens.base.gradle | 6 - .../main/groovy/twomartens.checkstyle.gradle | 23 - .../main/groovy/twomartens.java-base.gradle | 22 - .../groovy/twomartens.java-preview.gradle | 15 - .../src/main/groovy/twomartens.java.gradle | 71 --- .../groovy/twomartens.nebula-release.gradle | 16 - .../twomartens.spring-boot-cloud.gradle | 40 -- .../main/groovy/twomartens.spring-boot.gradle | 60 --- .../main/groovy/twomartens.versions.gradle | 29 -- .../main/kotlin/twomartens.base.gradle.kts | 6 + .../kotlin/twomartens.checkstyle.gradle.kts | 23 + .../kotlin/twomartens.java-base.gradle.kts | 30 ++ .../kotlin/twomartens.java-preview.gradle.kts | 15 + .../main/kotlin/twomartens.java.gradle.kts | 62 +++ .../src/main/kotlin/twomartens.jib.gradle.kts | 21 + .../main/kotlin/twomartens.kotlin.gradle.kts | 18 + .../twomartens.nebula-release.gradle.kts | 18 + .../twomartens.spring-boot-cloud.gradle.kts | 49 ++ .../kotlin/twomartens.spring-boot.gradle.kts | 80 ++++ .../kotlin/twomartens.versions.gradle.kts | 32 ++ gradle.properties | 5 +- gradle/libs.versions.toml | 48 +- gradle/wrapper/gradle-wrapper.properties | 2 +- module-deploy/deploy.gradle.kts | 38 ++ module-lib/lib.gradle | 7 - .../de/twomartens/template/model/Name.java | 5 - module-plain/plain.gradle | 7 - .../java/de/twomartens/template/Main.java | 13 - module-plain/src/main/resources/log4j2.xml | 14 - module-server/server.gradle | 9 - module-server/server.gradle.kts | 16 + .../twomartens/template/MainApplication.java | 35 -- .../configuration/ClockConfiguration.java | 20 - .../InterceptorConfiguration.java | 22 - .../configuration/OpenApiConfiguration.java | 18 - .../RestTemplateConfiguration.java | 34 -- .../StatusProbeConfiguration.java | 28 -- .../configuration/WebConfiguration.java | 28 -- .../controller/ExceptionController.java | 37 -- .../controller/VersionHtmlController.java | 95 ---- .../controller/v1/GreetingRestController.java | 59 --- .../exception/HttpStatusException.java | 20 - .../BufferingClientHttpResponseWrapper.java | 82 ---- .../interceptor/HeaderInterceptor.java | 87 ---- .../interceptor/HeaderInterceptorRest.java | 68 --- .../interceptor/LoggingInterceptorRest.java | 419 ----------------- .../template/mapper/v1/GreetingMapper.java | 21 - .../template/model/db/Greeting.java | 41 -- .../template/model/dto/ErrorMessage.java | 24 - .../template/model/dto/v1/Greeting.java | 31 -- .../actuator/AbstractHealthIndicator.java | 84 ---- .../AbstractStatusProbeHealthIndicator.java | 37 -- .../monitoring/actuator/Preparable.java | 10 - .../actuator/RestHealthIndicator.java | 65 --- .../statusprobe/CountBasedStatusProbe.java | 33 -- .../PercentageBasedStatusProbe.java | 63 --- .../statusprobe/ScheduledStatusProbe.java | 18 - .../monitoring/statusprobe/StatusProbe.java | 62 --- .../statusprobe/StatusProbeCriticality.java | 7 - .../statusprobe/StatusProbeLogger.java | 124 ----- .../statusprobe/TimeBasedStatusProbe.java | 64 --- .../RestTemplateTimeoutProperties.java | 34 -- .../template/property/ServiceProperties.java | 21 - .../property/StatusProbeProperties.java | 28 -- .../repository/GreetingRepository.java | 11 - .../template/service/GreetingService.java | 40 -- .../service/PropertyReloadService.java | 37 -- .../twomartens/timetable/MainApplication.kt | 39 ++ .../configuration/PropertyConfiguration.kt | 14 + .../timetable/property/ServiceProperties.kt | 12 + .../configuration/ClockConfiguration.kt | 15 + .../configuration/FilterConfiguration.kt | 13 + .../configuration/InterceptorConfiguration.kt | 20 + .../configuration/OpenApiConfiguration.kt | 39 ++ .../RestTemplateConfiguration.kt | 37 ++ .../configuration/StatusProbeConfiguration.kt | 25 + .../support/configuration/WebConfiguration.kt | 28 ++ .../configuration/WebSecurityConfiguration.kt | 95 ++++ .../support/controller/ExceptionController.kt | 36 ++ .../controller/VersionHtmlController.kt | 81 ++++ .../support/exception/HttpStatusException.kt | 6 + .../BufferingClientHttpResponseWrapper.kt | 45 ++ .../support/interceptor/HeaderInterceptor.kt | 85 ++++ .../interceptor/HeaderInterceptorRest.kt | 88 ++++ .../interceptor/LoggingInterceptorRest.kt | 438 ++++++++++++++++++ .../support/model/dto/ErrorMessage.kt | 3 + .../actuator/AbstractHealthIndicator.kt | 83 ++++ .../AbstractStatusProbeHealthIndicator.kt | 30 ++ .../support/monitoring/actuator/Preparable.kt | 7 + .../actuator/RestHealthIndicator.kt | 59 +++ .../statusprobe/CountBasedStatusProbe.kt | 25 + .../statusprobe/PercentageBasedStatusProbe.kt | 51 ++ .../statusprobe/ScheduledStatusProbe.kt | 22 + .../monitoring/statusprobe/StatusProbe.kt | 59 +++ .../statusprobe/StatusProbeCriticality.kt | 7 + .../statusprobe/StatusProbeLogger.kt | 109 +++++ .../statusprobe/TimeBasedStatusProbe.kt | 48 ++ .../property/RestTemplateTimeoutProperties.kt | 15 + .../support/property/StatusProbeProperties.kt | 20 + .../support/property/TimeProperties.kt | 14 + .../support/security/SpringPolicyEnforcer.kt | 65 +++ .../security/SpringPolicyEnforcerFilter.kt | 74 +++ .../src/main/resources/application-dev.yaml | 13 +- .../src/main/resources/application-prod.yaml | 7 + .../src/main/resources/application.yaml | 61 ++- .../main/resources/config/timetable-prod.yaml | 2 + .../src/main/resources/config/timetable.yaml | 7 + module-server/src/main/resources/log4j2.xml | 1 + .../src/main/resources/policy-enforcer.json | 6 + .../src/main/resources/templates/error.html | 2 +- .../main/resources/templates/error/404.html | 2 +- .../resources/templates/errorIncludes.html | 4 +- .../src/main/resources/templates/version.html | 4 +- settings.gradle | 10 - settings.gradle.kts | 10 + 122 files changed, 2402 insertions(+), 2343 deletions(-) delete mode 100644 .run/MainApplication.run.xml create mode 100644 .run/MainApplicationKt.run.xml delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/build.gradle.kts rename buildSrc/{settings.gradle => settings.gradle.kts} (79%) delete mode 100644 buildSrc/src/main/groovy/twomartens.base.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.checkstyle.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.java-base.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.java-preview.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.java.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.nebula-release.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.spring-boot.gradle delete mode 100644 buildSrc/src/main/groovy/twomartens.versions.gradle create mode 100644 buildSrc/src/main/kotlin/twomartens.base.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.checkstyle.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.java-base.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.java-preview.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.java.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.jib.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.nebula-release.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.spring-boot-cloud.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.spring-boot.gradle.kts create mode 100644 buildSrc/src/main/kotlin/twomartens.versions.gradle.kts create mode 100644 module-deploy/deploy.gradle.kts delete mode 100644 module-lib/lib.gradle delete mode 100644 module-lib/src/main/java/de/twomartens/template/model/Name.java delete mode 100644 module-plain/plain.gradle delete mode 100644 module-plain/src/main/java/de/twomartens/template/Main.java delete mode 100644 module-plain/src/main/resources/log4j2.xml delete mode 100644 module-server/server.gradle create mode 100644 module-server/server.gradle.kts delete mode 100644 module-server/src/main/java/de/twomartens/template/MainApplication.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java delete mode 100644 module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java delete mode 100644 module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java delete mode 100644 module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java delete mode 100644 module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java delete mode 100644 module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java delete mode 100644 module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java delete mode 100644 module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java delete mode 100644 module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java delete mode 100644 module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java delete mode 100644 module-server/src/main/java/de/twomartens/template/model/db/Greeting.java delete mode 100644 module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java delete mode 100644 module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java delete mode 100644 module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java delete mode 100644 module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java delete mode 100644 module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java delete mode 100644 module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java delete mode 100644 module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java delete mode 100644 module-server/src/main/java/de/twomartens/template/service/GreetingService.java delete mode 100644 module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/configuration/PropertyConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/property/ServiceProperties.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/ClockConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/FilterConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/InterceptorConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/OpenApiConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/RestTemplateConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/StatusProbeConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebSecurityConfiguration.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/controller/ExceptionController.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/controller/VersionHtmlController.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/exception/HttpStatusException.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/BufferingClientHttpResponseWrapper.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptor.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptorRest.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/LoggingInterceptorRest.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/model/dto/ErrorMessage.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractHealthIndicator.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractStatusProbeHealthIndicator.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/Preparable.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/RestHealthIndicator.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/CountBasedStatusProbe.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/PercentageBasedStatusProbe.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/ScheduledStatusProbe.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbe.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeCriticality.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeLogger.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/TimeBasedStatusProbe.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/property/RestTemplateTimeoutProperties.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/property/StatusProbeProperties.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/property/TimeProperties.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcer.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcerFilter.kt create mode 100644 module-server/src/main/resources/application-prod.yaml create mode 100644 module-server/src/main/resources/config/timetable-prod.yaml create mode 100644 module-server/src/main/resources/config/timetable.yaml create mode 100644 module-server/src/main/resources/policy-enforcer.json delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.run/MainApplication.run.xml b/.run/MainApplication.run.xml deleted file mode 100644 index 1ad0379..0000000 --- a/.run/MainApplication.run.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/.run/MainApplicationKt.run.xml b/.run/MainApplicationKt.run.xml new file mode 100644 index 0000000..8ee72f0 --- /dev/null +++ b/.run/MainApplicationKt.run.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index fe145dd..0000000 --- a/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'twomartens.versions' - id 'twomartens.nebula-release' -} - -versionCatalogUpdate { - sortByKey = false - keep { - keepUnusedVersions = true - keepUnusedLibraries = true - keepUnusedPlugins = true - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..5cca66b --- /dev/null +++ b/build.gradle.kts @@ -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) + } +} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle deleted file mode 100644 index af471e1..0000000 --- a/buildSrc/build.gradle +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..78398a7 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -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) +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle.kts similarity index 79% rename from buildSrc/settings.gradle rename to buildSrc/settings.gradle.kts index 7cd729c..50483dc 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = 'twomartens.config' +rootProject.name = "twomartens.config" dependencyResolutionManagement { versionCatalogs { diff --git a/buildSrc/src/main/groovy/twomartens.base.gradle b/buildSrc/src/main/groovy/twomartens.base.gradle deleted file mode 100644 index c6c8dd5..0000000 --- a/buildSrc/src/main/groovy/twomartens.base.gradle +++ /dev/null @@ -1,6 +0,0 @@ -plugins { - id 'idea' - id 'eclipse' -} - -group = projectgroup \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.checkstyle.gradle b/buildSrc/src/main/groovy/twomartens.checkstyle.gradle deleted file mode 100644 index ef5353a..0000000 --- a/buildSrc/src/main/groovy/twomartens.checkstyle.gradle +++ /dev/null @@ -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 - } - } - } -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.java-base.gradle b/buildSrc/src/main/groovy/twomartens.java-base.gradle deleted file mode 100644 index c85aa0d..0000000 --- a/buildSrc/src/main/groovy/twomartens.java-base.gradle +++ /dev/null @@ -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' -} diff --git a/buildSrc/src/main/groovy/twomartens.java-preview.gradle b/buildSrc/src/main/groovy/twomartens.java-preview.gradle deleted file mode 100644 index 1459a9d..0000000 --- a/buildSrc/src/main/groovy/twomartens.java-preview.gradle +++ /dev/null @@ -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' -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.java.gradle b/buildSrc/src/main/groovy/twomartens.java.gradle deleted file mode 100644 index 9b61efa..0000000 --- a/buildSrc/src/main/groovy/twomartens.java.gradle +++ /dev/null @@ -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") diff --git a/buildSrc/src/main/groovy/twomartens.nebula-release.gradle b/buildSrc/src/main/groovy/twomartens.nebula-release.gradle deleted file mode 100644 index 4cf743a..0000000 --- a/buildSrc/src/main/groovy/twomartens.nebula-release.gradle +++ /dev/null @@ -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") -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle b/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle deleted file mode 100644 index 2feb1ed..0000000 --- a/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle +++ /dev/null @@ -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 -} diff --git a/buildSrc/src/main/groovy/twomartens.spring-boot.gradle b/buildSrc/src/main/groovy/twomartens.spring-boot.gradle deleted file mode 100644 index f9ede99..0000000 --- a/buildSrc/src/main/groovy/twomartens.spring-boot.gradle +++ /dev/null @@ -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("") -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.versions.gradle b/buildSrc/src/main/groovy/twomartens.versions.gradle deleted file mode 100644 index 60eb26c..0000000 --- a/buildSrc/src/main/groovy/twomartens.versions.gradle +++ /dev/null @@ -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' -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/twomartens.base.gradle.kts b/buildSrc/src/main/kotlin/twomartens.base.gradle.kts new file mode 100644 index 0000000..a863b4c --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.base.gradle.kts @@ -0,0 +1,6 @@ +plugins { + idea +} + +val projectgroup: String = providers.gradleProperty("projectgroup").get() +group = projectgroup \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/twomartens.checkstyle.gradle.kts b/buildSrc/src/main/kotlin/twomartens.checkstyle.gradle.kts new file mode 100644 index 0000000..f45bb90 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.checkstyle.gradle.kts @@ -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( + "org.checkstyle.google.suppressionfilter.config" to + "${project.rootDir}/config/checkstyle/checkstyle-suppressions.xml") + +} +tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(true) + } +} diff --git a/buildSrc/src/main/kotlin/twomartens.java-base.gradle.kts b/buildSrc/src/main/kotlin/twomartens.java-base.gradle.kts new file mode 100644 index 0000000..c665e7d --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.java-base.gradle.kts @@ -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") + } +} diff --git a/buildSrc/src/main/kotlin/twomartens.java-preview.gradle.kts b/buildSrc/src/main/kotlin/twomartens.java-preview.gradle.kts new file mode 100644 index 0000000..55b0390 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.java-preview.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("twomartens.java-base") +} + +tasks.withType().configureEach { + options.compilerArgs.plusAssign("--enable-preview") +} + +tasks.withType().configureEach { + jvmArgs.plusAssign("--enable-preview") +} + +tasks.withType().configureEach { + jvmArgs.plusAssign("--enable-preview") +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/twomartens.java.gradle.kts b/buildSrc/src/main/kotlin/twomartens.java.gradle.kts new file mode 100644 index 0000000..09bed28 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.java.gradle.kts @@ -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() + +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().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") +} diff --git a/buildSrc/src/main/kotlin/twomartens.jib.gradle.kts b/buildSrc/src/main/kotlin/twomartens.jib.gradle.kts new file mode 100644 index 0000000..db3d096 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.jib.gradle.kts @@ -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") +} diff --git a/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts b/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts new file mode 100644 index 0000000..3b58353 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts @@ -0,0 +1,18 @@ +import org.gradle.accessors.dm.LibrariesForLibs + +plugins { + kotlin("jvm") +} + +val libs = the() + +dependencies { + implementation(libs.kotlin.logging) + implementation(kotlin("reflect")) +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xjvm-default=all") + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/twomartens.nebula-release.gradle.kts b/buildSrc/src/main/kotlin/twomartens.nebula-release.gradle.kts new file mode 100644 index 0000000..6f5f07d --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.nebula-release.gradle.kts @@ -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") + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/twomartens.spring-boot-cloud.gradle.kts b/buildSrc/src/main/kotlin/twomartens.spring-boot-cloud.gradle.kts new file mode 100644 index 0000000..bfbf0f4 --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.spring-boot-cloud.gradle.kts @@ -0,0 +1,49 @@ +import org.gradle.accessors.dm.LibrariesForLibs + +plugins { + id("twomartens.spring-boot") +} + +val libs = the() + +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("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 +} diff --git a/buildSrc/src/main/kotlin/twomartens.spring-boot.gradle.kts b/buildSrc/src/main/kotlin/twomartens.spring-boot.gradle.kts new file mode 100644 index 0000000..9a2a81b --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.spring-boot.gradle.kts @@ -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() + +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("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") +} diff --git a/buildSrc/src/main/kotlin/twomartens.versions.gradle.kts b/buildSrc/src/main/kotlin/twomartens.versions.gradle.kts new file mode 100644 index 0000000..9b54faf --- /dev/null +++ b/buildSrc/src/main/kotlin/twomartens.versions.gradle.kts @@ -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().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 { + rejectVersionIf { + candidate.version.isNonStable() + } +} + +tasks.named("versionCatalogUpdate").configure { + group = "version" +} + +tasks.named("dependencyUpdates").configure { + group = "version" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index b360187..fb0a158 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file +org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +kapt.include.compile.classpath=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2552a79..650fe55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..db9a6b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/module-deploy/deploy.gradle.kts b/module-deploy/deploy.gradle.kts new file mode 100644 index 0000000..be55c9a --- /dev/null +++ b/module-deploy/deploy.gradle.kts @@ -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") + } +} diff --git a/module-lib/lib.gradle b/module-lib/lib.gradle deleted file mode 100644 index aa3fd97..0000000 --- a/module-lib/lib.gradle +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - id 'twomartens.java' -} - -dependencies { - -} \ No newline at end of file diff --git a/module-lib/src/main/java/de/twomartens/template/model/Name.java b/module-lib/src/main/java/de/twomartens/template/model/Name.java deleted file mode 100644 index bc2c605..0000000 --- a/module-lib/src/main/java/de/twomartens/template/model/Name.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.twomartens.template.model; - -public record Name(String value) { - -} diff --git a/module-plain/plain.gradle b/module-plain/plain.gradle deleted file mode 100644 index db2effb..0000000 --- a/module-plain/plain.gradle +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - id 'twomartens.java' -} - -dependencies { - implementation project(':lib') -} \ No newline at end of file diff --git a/module-plain/src/main/java/de/twomartens/template/Main.java b/module-plain/src/main/java/de/twomartens/template/Main.java deleted file mode 100644 index 58e6980..0000000 --- a/module-plain/src/main/java/de/twomartens/template/Main.java +++ /dev/null @@ -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)); - } -} diff --git a/module-plain/src/main/resources/log4j2.xml b/module-plain/src/main/resources/log4j2.xml deleted file mode 100644 index 24bce61..0000000 --- a/module-plain/src/main/resources/log4j2.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/module-server/server.gradle b/module-server/server.gradle deleted file mode 100644 index d62c9f8..0000000 --- a/module-server/server.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id 'twomartens.spring-boot-cloud' -} - -dependencies { - implementation libs.mapstruct.base - annotationProcessor libs.mapstruct.processor - -} \ No newline at end of file diff --git a/module-server/server.gradle.kts b/module-server/server.gradle.kts new file mode 100644 index 0000000..28fcea7 --- /dev/null +++ b/module-server/server.gradle.kts @@ -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) +} diff --git a/module-server/src/main/java/de/twomartens/template/MainApplication.java b/module-server/src/main/java/de/twomartens/template/MainApplication.java deleted file mode 100644 index 344c0b3..0000000 --- a/module-server/src/main/java/de/twomartens/template/MainApplication.java +++ /dev/null @@ -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)); - } -} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java deleted file mode 100644 index dbfcccc..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java +++ /dev/null @@ -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()); - } - -} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java deleted file mode 100644 index b574c8f..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java deleted file mode 100644 index 5b43c18..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java deleted file mode 100644 index e03c787..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java +++ /dev/null @@ -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(); - } -} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java deleted file mode 100644 index de15d24..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java deleted file mode 100644 index 3b6378c..0000000 --- a/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java +++ /dev/null @@ -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("*"); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java b/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java deleted file mode 100644 index c09d4ab..0000000 --- a/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java +++ /dev/null @@ -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 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 handleRuntimeException(RuntimeException e) { - log.error("unexpected exception occurred", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorMessage.builder() - .message(e.getMessage()) - .build()); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java b/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java deleted file mode 100644 index 2f23900..0000000 --- a/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java +++ /dev/null @@ -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 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()); - } - - } - - -} diff --git a/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java b/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java deleted file mode 100644 index 0ac0e81..0000000 --- a/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java +++ /dev/null @@ -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; - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java b/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java deleted file mode 100644 index 8eac239..0000000 --- a/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java +++ /dev/null @@ -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; - } -} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java b/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java deleted file mode 100644 index 4404be0..0000000 --- a/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java deleted file mode 100644 index a750e30..0000000 --- a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java +++ /dev/null @@ -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 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 - } - } - } - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java deleted file mode 100644 index 97ddd4f..0000000 --- a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java +++ /dev/null @@ -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); - } -} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java b/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java deleted file mode 100644 index be23359..0000000 --- a/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java +++ /dev/null @@ -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> 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> 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> 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> 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> 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> extractHeaders(Iterator headerNames, - Function> headerValuesSupplier) { - Map> requestHeaders = new HashMap<>(); - while (headerNames.hasNext()) { - String name = headerNames.next(); - Collection values = requestHeaders.computeIfAbsent(name, n -> new TreeSet<>()); - Iterator headerValues = headerValuesSupplier.apply(name); - while (headerValues.hasNext()) { - values.add(headerValues.next()); - } - } - return requestHeaders; - } - - private static Map> extractHeaders(HttpHeaders headers) { - Map> result = new HashMap<>(); - for (Entry> entry : headers.entrySet()) { - result.put(entry.getKey(), new ArrayList<>(entry.getValue())); - } - return result; - } - - private static String toHeaderString(Map> 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> requestHeaders, - byte[] fullRequest) { - return StandardCharsets.UTF_8; - } - - /** - * usually returns UTF-8, but can be overridden to implement more complex logic - */ - public Charset determineResponseEncoding(Map> responseHeaders, - byte[] fullResponse) { - return StandardCharsets.UTF_8; - } - - @Builder - private record LogMessage(Map> requestHeaders, - Map> 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 - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java b/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java deleted file mode 100644 index 7bc4d90..0000000 --- a/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java +++ /dev/null @@ -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); -} diff --git a/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java b/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java deleted file mode 100644 index ed137f0..0000000 --- a/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java +++ /dev/null @@ -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; - -} diff --git a/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java b/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java deleted file mode 100644 index cb5a893..0000000 --- a/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java +++ /dev/null @@ -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; -} diff --git a/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java b/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java deleted file mode 100644 index 9a8fdde..0000000 --- a/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java +++ /dev/null @@ -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; - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java deleted file mode 100644 index 5b0cfd3..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java deleted file mode 100644 index 9ee1ab8..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java deleted file mode 100644 index 0b53049..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.twomartens.template.monitoring.actuator; - -import java.io.Closeable; - -@FunctionalInterface -public interface Preparable { - - Closeable prepare(); - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java deleted file mode 100644 index 62c1a5e..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java +++ /dev/null @@ -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. - *

- * 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. - */ -@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 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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java deleted file mode 100644 index 799337e..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java +++ /dev/null @@ -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); - } - } - - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java deleted file mode 100644 index a0ae97c..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java +++ /dev/null @@ -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(); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java deleted file mode 100644 index 8a19fd1..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java +++ /dev/null @@ -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); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java deleted file mode 100644 index 36a0bf9..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java +++ /dev/null @@ -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); - } - -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java deleted file mode 100644 index f7f4f55..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.twomartens.template.monitoring.statusprobe; - - -public enum StatusProbeCriticality { - K1, K2, K3 -} - diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java deleted file mode 100644 index 9ddfa43..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java +++ /dev/null @@ -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 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 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 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 probesDown, StatusProbeCriticality criticality) { - List 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) { - - } -} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java deleted file mode 100644 index 250dcba..0000000 --- a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java +++ /dev/null @@ -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); - } - } - - -} diff --git a/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java b/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java deleted file mode 100644 index d2673a7..0000000 --- a/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java +++ /dev/null @@ -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; -} - diff --git a/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java b/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java deleted file mode 100644 index 4b3f4fb..0000000 --- a/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java +++ /dev/null @@ -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; - -} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java b/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java deleted file mode 100644 index b1a0095..0000000 --- a/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java +++ /dev/null @@ -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; -} diff --git a/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java b/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java deleted file mode 100644 index aad78c3..0000000 --- a/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java +++ /dev/null @@ -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 { - - Optional findByMessageIgnoreCase(String message); - -} diff --git a/module-server/src/main/java/de/twomartens/template/service/GreetingService.java b/module-server/src/main/java/de/twomartens/template/service/GreetingService.java deleted file mode 100644 index 994384b..0000000 --- a/module-server/src/main/java/de/twomartens/template/service/GreetingService.java +++ /dev/null @@ -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); - } -} diff --git a/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java b/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java deleted file mode 100644 index 5864846..0000000 --- a/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java +++ /dev/null @@ -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 properties = contextRefresher.refresh(); - if (!properties.isEmpty()) { - log.info(new StringMapMessage() - .with(PARAM_MESSAGE, "properties changed") - .with(PARAM_PROPERTIES, String.join("\n", properties))); - } - } - -} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt new file mode 100644 index 0000000..6fd8825 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt @@ -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) { + runApplication(*args) +} + diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/configuration/PropertyConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/configuration/PropertyConfiguration.kt new file mode 100644 index 0000000..29fbf88 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/configuration/PropertyConfiguration.kt @@ -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 \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/property/ServiceProperties.kt b/module-server/src/main/kotlin/de/twomartens/timetable/property/ServiceProperties.kt new file mode 100644 index 0000000..6a9f01e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/property/ServiceProperties.kt @@ -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 +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/ClockConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/ClockConfiguration.kt new file mode 100644 index 0000000..93dc24d --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/ClockConfiguration.kt @@ -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) + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/FilterConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/FilterConfiguration.kt new file mode 100644 index 0000000..3f16f62 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/FilterConfiguration.kt @@ -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() + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/InterceptorConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/InterceptorConfiguration.kt new file mode 100644 index 0000000..15b8a01 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/InterceptorConfiguration.kt @@ -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() + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/OpenApiConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/OpenApiConfiguration.kt new file mode 100644 index 0000000..2d82750 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/OpenApiConfiguration.kt @@ -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) + ) + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/RestTemplateConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/RestTemplateConfiguration.kt new file mode 100644 index 0000000..c5dd219 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/RestTemplateConfiguration.kt @@ -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() + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/StatusProbeConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/StatusProbeConfiguration.kt new file mode 100644 index 0000000..f48b491 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/StatusProbeConfiguration.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebConfiguration.kt new file mode 100644 index 0000000..24b88bf --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebConfiguration.kt @@ -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" + ) + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebSecurityConfiguration.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebSecurityConfiguration.kt new file mode 100644 index 0000000..aafeda9 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/configuration/WebSecurityConfiguration.kt @@ -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()).permitAll() } + .authorizeHttpRequests { it.requestMatchers(HttpMethod.OPTIONS).permitAll() } + .authorizeHttpRequests { it.anyRequest().authenticated() } + .oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer -> 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 = listOf( + "/timetable/v1/healthCheck", + "/actuator/**", + "/timetable/v1/doc/**", + "/timetable/v1/api-docs/**", + "/error", + "/timetable/version", + ) + private val PATHS = buildPathConfigs() + + private fun buildPathConfigs(): List { + val paths: MutableList = mutableListOf() + for (path in PERMITTED_PATHS) { + val pathConfig = PathConfig() + pathConfig.path = path.replace("**", "*") + pathConfig.enforcementMode = EnforcementMode.DISABLED + paths.add(pathConfig) + } + return paths + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/ExceptionController.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/ExceptionController.kt new file mode 100644 index 0000000..c87986f --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/ExceptionController.kt @@ -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 { + 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 { + 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 {} + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/VersionHtmlController.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/VersionHtmlController.kt new file mode 100644 index 0000000..bdc2bee --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/controller/VersionHtmlController.kt @@ -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 { + 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 {} + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/exception/HttpStatusException.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/exception/HttpStatusException.kt new file mode 100644 index 0000000..99607f1 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/exception/HttpStatusException.kt @@ -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) \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/BufferingClientHttpResponseWrapper.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/BufferingClientHttpResponseWrapper.kt new file mode 100644 index 0000000..2a39f1a --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/BufferingClientHttpResponseWrapper.kt @@ -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() + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptor.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptor.kt new file mode 100644 index 0000000..47c6e48 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptor.kt @@ -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 + + init { + this.closeables = closeables + } + + override fun close() { + closeables.forEach { + try { + it.close() + } catch (ignored: Exception) { + // do nothing + } + } + } + } + +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptorRest.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptorRest.kt new file mode 100644 index 0000000..10a4169 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/HeaderInterceptorRest.kt @@ -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() +// } +// } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/LoggingInterceptorRest.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/LoggingInterceptorRest.kt new file mode 100644 index 0000000..7746561 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/interceptor/LoggingInterceptorRest.kt @@ -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> = 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> = 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>, 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, + ignoreList: List = emptyList(), + headerValuesSupplier: Function> + ): Map> { + val requestHeaders: MutableMap> = 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> { + val result: MutableMap> = mutableMapOf() + for ((key, value) in headers) { + result[key] = value.toList() + } + return result + } + + private fun toHeaderString(headerMap: Map>): 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>, + val responseHeaders: Map>, + 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 + } + + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/model/dto/ErrorMessage.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/model/dto/ErrorMessage.kt new file mode 100644 index 0000000..7019caf --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/model/dto/ErrorMessage.kt @@ -0,0 +1,3 @@ +package de.twomartens.timetable.support.model.dto + +data class ErrorMessage(val message: String) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractHealthIndicator.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractHealthIndicator.kt new file mode 100644 index 0000000..e3cc29d --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractHealthIndicator.kt @@ -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 {} + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractStatusProbeHealthIndicator.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractStatusProbeHealthIndicator.kt new file mode 100644 index 0000000..7198ae4 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/AbstractStatusProbeHealthIndicator.kt @@ -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" + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/Preparable.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/Preparable.kt new file mode 100644 index 0000000..3ca227e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/Preparable.kt @@ -0,0 +1,7 @@ +package de.twomartens.timetable.support.monitoring.actuator + +import java.io.Closeable + +fun interface Preparable { + fun prepare(): Closeable +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/RestHealthIndicator.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/RestHealthIndicator.kt new file mode 100644 index 0000000..3a54d04 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/actuator/RestHealthIndicator.kt @@ -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=" + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/CountBasedStatusProbe.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/CountBasedStatusProbe.kt new file mode 100644 index 0000000..f2c597e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/CountBasedStatusProbe.kt @@ -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) + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/PercentageBasedStatusProbe.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/PercentageBasedStatusProbe.kt new file mode 100644 index 0000000..5034b5c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/PercentageBasedStatusProbe.kt @@ -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() + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/ScheduledStatusProbe.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/ScheduledStatusProbe.kt new file mode 100644 index 0000000..bd87d22 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/ScheduledStatusProbe.kt @@ -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) +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbe.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbe.kt new file mode 100644 index 0000000..aaba095 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbe.kt @@ -0,0 +1,59 @@ +package de.twomartens.timetable.support.monitoring.statusprobe + +import lombok.Getter +import org.springframework.boot.actuate.health.Status +import java.time.Clock +import java.time.ZonedDateTime + +@Getter +open class StatusProbe( + private val clock: Clock, criticality: StatusProbeCriticality, private val name: String, + private val statusProbeLogger: StatusProbeLogger +) { + var status: Status = Status.UP + private set + var throwable: Throwable? = null + private set + var message: String? = null + private set + var lastStatusChange: ZonedDateTime? = null + private set + + init { + statusProbeLogger.registerStatusProbe(name, criticality) + } + + protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) { + if (status !== this.status) { + lastStatusChange = ZonedDateTime.now(clock) + statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable) + } + this.status = status + this.throwable = throwable + this.message = message + } + + fun up() { + setStatus(Status.UP, null, null) + } + + fun up(message: String) { + setStatus(Status.UP, null, message) + } + + fun down() { + setStatus(Status.DOWN, null, null) + } + + fun down(throwable: Throwable) { + setStatus(Status.DOWN, throwable, null) + } + + fun down(message: String) { + setStatus(Status.DOWN, null, message) + } + + protected fun down(throwable: Throwable, message: String) { + setStatus(Status.DOWN, throwable, message) + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeCriticality.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeCriticality.kt new file mode 100644 index 0000000..70df86b --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeCriticality.kt @@ -0,0 +1,7 @@ +package de.twomartens.timetable.support.monitoring.statusprobe + +enum class StatusProbeCriticality { + K1, + K2, + K3 +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeLogger.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeLogger.kt new file mode 100644 index 0000000..c34afad --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/StatusProbeLogger.kt @@ -0,0 +1,109 @@ +package de.twomartens.timetable.support.monitoring.statusprobe + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.apache.logging.log4j.MarkerManager +import org.apache.logging.log4j.message.StringMapMessage +import org.springframework.boot.actuate.health.Status +import java.time.Clock +import java.time.ZonedDateTime + +class StatusProbeLogger internal constructor(private val clock: Clock, private val commLog: Logger) { + private val statusProbeToStatus: MutableMap = mutableMapOf() + + constructor(clock: Clock) : this(clock, LogManager.getLogger("statusprobe")) + + fun registerStatusProbe(name: String, criticality: StatusProbeCriticality) { + statusProbeToStatus[ProbeIdent(name, criticality)] = Status.UP + logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null) + } + + fun logStatusChange( + name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?, + throwable: Throwable? + ) { + var probeIdent = getProbeIdent(name) + if (probeIdent == null) { + probeIdent = ProbeIdent(name, StatusProbeCriticality.K1) + } + statusProbeToStatus[probeIdent] = status + createLog(message, lastStatusChange, throwable) + } + + private fun getProbeIdent(name: String): ProbeIdent? { + return statusProbeToStatus.keys.asSequence() + .filter { it.name == name } + .firstOrNull() + } + + private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) { + val cleanedMessage = message ?: "" + val overallStatus = overallStatus + val criticality = overallCriticality + if (Status.UP == overallStatus) { + commLog.info( + MARKER, StringMapMessage() + .with(LABEL_CRITICALITY, criticality) + .with(LABEL_STATUS, overallStatus) + .with(LABEL_MESSAGE, cleanedMessage) + .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange) + ) + } else { + commLog.error( + MARKER, StringMapMessage() + .with(LABEL_CRITICALITY, criticality) + .with(LABEL_STATUS, overallStatus) + .with(LABEL_MESSAGE, cleanedMessage) + .with(LABEL_REASON, reason) + .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable + ) + } + } + + private val overallCriticality: StatusProbeCriticality + get() { + val crits = statusProbeToStatus.keys.asSequence() + .map { it.criticality } + .toList() + return if (crits.contains(StatusProbeCriticality.K1)) StatusProbeCriticality.K1 + else if (crits.contains(StatusProbeCriticality.K2)) StatusProbeCriticality.K2 + else StatusProbeCriticality.K3 + } + + private val overallStatus: Status + get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN + else Status.UP + + private val reason: String + get() { + val probesDown = statusProbeToStatus.entries.asSequence() + .filter { it.value == Status.DOWN } + .map { it.key } + .toList() + val reasonK1 = getDownStatusProbes(probesDown, StatusProbeCriticality.K1) + val reasonK2 = getDownStatusProbes(probesDown, StatusProbeCriticality.K2) + val reasonK3 = getDownStatusProbes(probesDown, StatusProbeCriticality.K3) + return "$reasonK1$reasonK2$reasonK3".trim { it <= ' ' } + } + + private fun getDownStatusProbes(probesDown: List, criticality: StatusProbeCriticality): String { + val downProbeNames = probesDown.asSequence() + .filter { it.criticality == criticality } + .map { it.name } + .toList() + return if (downProbeNames.isNotEmpty()) { + "$criticality failed: ${downProbeNames.joinToString(separator = ",")}\n" + } else "" + } + + data class ProbeIdent(val name: String, val criticality: StatusProbeCriticality) + + companion object { + private val MARKER = MarkerManager.getMarker("statusprobe") + private const val LABEL_CRITICALITY = "label.status.criticality" + private const val LABEL_STATUS = "label.status.status" + private const val LABEL_REASON = "label.status.reason" + private const val LABEL_MESSAGE = "label.status.description" + private const val LABEL_LAST_STATUS_CHANGE = "label.status.last_change" + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/TimeBasedStatusProbe.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/TimeBasedStatusProbe.kt new file mode 100644 index 0000000..f7beed4 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/monitoring/statusprobe/TimeBasedStatusProbe.kt @@ -0,0 +1,48 @@ +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 +import java.time.ZonedDateTime + +class TimeBasedStatusProbe( + private val maxFailureDuration: Duration, private val clock: Clock, + threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality, + name: String, statusProbeLogger: StatusProbeLogger +) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe { + private var lastSuccess: ZonedDateTime? = null + private var temporaryThrowable: Throwable? = null + private var temporaryMessage: String? = null + + init { + scheduleTask(threadPoolTaskScheduler, schedulePeriod) + } + + @Synchronized + override fun setStatus(status: Status, throwable: Throwable?, message: String?) { + if (status === Status.DOWN) { + this.temporaryThrowable = throwable + this.temporaryMessage = message + } else if (status === Status.UP) { + lastSuccess = ZonedDateTime.now(clock) + super.setStatus(status, throwable, message) + } + } + + private val isOverdue: Boolean + get() { + if (lastSuccess == null) { + return false + } + val timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock)) + return maxFailureDuration.minus(timeSinceLastSuccess).isNegative + } + + @Synchronized + override fun runScheduledTask() { + if (isOverdue) { + super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage) + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/property/RestTemplateTimeoutProperties.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/RestTemplateTimeoutProperties.kt new file mode 100644 index 0000000..53f17b6 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/RestTemplateTimeoutProperties.kt @@ -0,0 +1,15 @@ +package de.twomartens.timetable.property + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.bind.ConstructorBinding +import org.springframework.boot.convert.DurationUnit +import java.time.Duration +import java.time.temporal.ChronoUnit + +@ConfigurationProperties(prefix = "resttemplate.timeout") +data class RestTemplateTimeoutProperties @ConstructorBinding constructor( + @DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestHealthIndicatorInMillis: Duration, + @DurationUnit(ChronoUnit.MILLIS) val connectionRestHealthIndicatorTimeoutInMillis: Duration, + @DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestTemplateInMillis: Duration, + @DurationUnit(ChronoUnit.MILLIS) val connectionRestTemplateTimeoutInMillis: Duration +) \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/property/StatusProbeProperties.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/StatusProbeProperties.kt new file mode 100644 index 0000000..5f2ca08 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/StatusProbeProperties.kt @@ -0,0 +1,20 @@ +package de.twomartens.timetable.property + +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.bind.ConstructorBinding +import org.springframework.boot.convert.DurationUnit +import org.springframework.cloud.context.config.annotation.RefreshScope +import org.springframework.context.annotation.Configuration +import java.time.Duration +import java.time.temporal.ChronoUnit + +@RefreshScope +@ConfigurationProperties(prefix = "de.twomartens.timetable.statusprobe") +@Schema(description = "Properties, to configure this Application") +data class StatusProbeProperties @ConstructorBinding constructor( + @DurationUnit(ChronoUnit.SECONDS) val scheduleDuration: Duration, + @DurationUnit(ChronoUnit.MINUTES) val maxKafkaFailureDuration: Duration, + val maxBlobFailureCount: Int, + val maxFailurePercent: Int +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/property/TimeProperties.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/TimeProperties.kt new file mode 100644 index 0000000..d5b7501 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/property/TimeProperties.kt @@ -0,0 +1,14 @@ +package de.twomartens.timetable.property + +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.bind.ConstructorBinding +import org.springframework.cloud.context.config.annotation.RefreshScope +import java.time.ZoneId + +@RefreshScope +@ConfigurationProperties(prefix = "time") +@Schema(description = "Properties to configure time") +data class TimeProperties @ConstructorBinding constructor( + val defaultTimeZone: ZoneId +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcer.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcer.kt new file mode 100644 index 0000000..a62cc6d --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcer.kt @@ -0,0 +1,65 @@ +package de.twomartens.timetable.support.security + +import mu.KotlinLogging +import org.keycloak.AuthorizationContext +import org.keycloak.adapters.authorization.PolicyEnforcer +import org.keycloak.adapters.authorization.spi.HttpRequest +import org.keycloak.adapters.authorization.spi.HttpResponse +import org.keycloak.authorization.client.ClientAuthorizationContext +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode +import org.keycloak.representations.idm.authorization.Permission + +class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer, + private val policyEnforcerConfig: PolicyEnforcerConfig) { + + fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext { + if (log.isDebugEnabled) { + log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri) + } + val context = authorize(request, response) + if (log.isDebugEnabled) { + log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED") + log.debug("Returning authorization context with permissions:") + for (permission in context.permissions) { + log.debug(permission.toString()) + } + } + return context + } + + private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext { + val enforcementMode = policyEnforcerConfig.enforcementMode + return if (EnforcementMode.DISABLED == enforcementMode) { + createAuthorizedContext() + } else policyEnforcer.enforce(request, response) + } + + private fun createAuthorizedContext(): AuthorizationContext { + return object : ClientAuthorizationContext(policyEnforcer.authzClient) { + override fun hasPermission(resourceName: String, scopeName: String): Boolean { + return true + } + + override fun hasResourcePermission(resourceName: String): Boolean { + return true + } + + override fun hasScopePermission(scopeName: String): Boolean { + return true + } + + override fun getPermissions(): List { + return emptyList() + } + + override fun isGranted(): Boolean { + return true + } + } + } + + companion object { + private val log = KotlinLogging.logger {} + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcerFilter.kt b/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcerFilter.kt new file mode 100644 index 0000000..74b534c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/support/security/SpringPolicyEnforcerFilter.kt @@ -0,0 +1,74 @@ +package de.twomartens.timetable.support.security + +import jakarta.servlet.* +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import mu.KotlinLogging +import org.keycloak.AuthorizationContext +import org.keycloak.adapters.authorization.PolicyEnforcer +import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest +import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse +import org.keycloak.adapters.authorization.spi.ConfigurationResolver +import org.keycloak.adapters.authorization.spi.HttpRequest +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap + + +class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter { + private val policyEnforcer: MutableMap = ConcurrentHashMap() + + @Throws(IOException::class, ServletException::class) + override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) { + val request = servletRequest as HttpServletRequest + val response = servletResponse as HttpServletResponse? + val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) } + val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest) + val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response)) + request.setAttribute(AuthorizationContext::class.java.name, authzContext) + if (authzContext.isGranted) { + log.debug("Request authorized, continuing the filter chain") + filterChain.doFilter(servletRequest, servletResponse) + } else { + log.debug("Unauthorized request to path [{}], aborting the filter chain", request.requestURI) + } + } + + private fun extractBearerToken(request: HttpServletRequest): String? { + val authorizationHeaderValues = request.getHeaders("Authorization") + while (authorizationHeaderValues.hasMoreElements()) { + val value = authorizationHeaderValues.nextElement() + val parts = value.trim() + .split("\\s+".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + if (parts.size != 2) { + continue + } + val bearer = parts[0] + if (bearer.equals("Bearer", ignoreCase = true)) { + return parts[1] + } + } + return null + } + + private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer { + return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) } + } + + private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer { + val authServerUrl = enforcerConfig.authServerUrl + return SpringPolicyEnforcer(PolicyEnforcer.builder() + .authServerUrl(authServerUrl) + .realm(enforcerConfig.realm) + .clientId(enforcerConfig.resource) + .credentials(enforcerConfig.credentials) + .bearerOnly(false) + .enforcerConfig(enforcerConfig).build(), enforcerConfig) + } + + companion object { + private val log = KotlinLogging.logger {} + } +} \ No newline at end of file diff --git a/module-server/src/main/resources/application-dev.yaml b/module-server/src/main/resources/application-dev.yaml index c64a463..6e2e3c0 100644 --- a/module-server/src/main/resources/application-dev.yaml +++ b/module-server/src/main/resources/application-dev.yaml @@ -1,6 +1,9 @@ -#TODO zufaellige Ports eintragen -server.port: 12000 -grpc.server.port: 12002 -management.server.port: 12001 +server.port: 12100 +management.server.port: 12101 -de.twomartens.template.name: "Template" \ No newline at end of file + +spring: + config: + import: + - "classpath:config/timetable.yaml" + - "optional:configserver:${CONFIGSERVER_SCHEME:http}://${CONFIGSERVER_HOST:localhost}:${CONFIGSERVER_PORT:8888}${CONFIGSERVER_PREFIX:/config}" diff --git a/module-server/src/main/resources/application-prod.yaml b/module-server/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..cf5bd00 --- /dev/null +++ b/module-server/src/main/resources/application-prod.yaml @@ -0,0 +1,7 @@ +spring: + config: + import: + - "configserver:${CONFIGSERVER_SCHEME:http}://${CONFIGSERVER_HOST:configserver}:${CONFIGSERVER_PORT:8888}${CONFIGSERVER_PREFIX:/config}" + kafka: + bootstrap-servers: + - ${KAFKA_HOST:kafka}:${KAFKA_PORT:9092} diff --git a/module-server/src/main/resources/application.yaml b/module-server/src/main/resources/application.yaml index 4211d10..a57518c 100644 --- a/module-server/src/main/resources/application.yaml +++ b/module-server/src/main/resources/application.yaml @@ -1,12 +1,12 @@ server: - port: 8080 + port: 12100 shutdown: graceful -grpc.server.port: 9090 + forward-headers-strategy: framework ### technical configurations ### management: # for security, don't use same port as application - server.port: 8081 + server.port: 12101 health: livenessState.enabled: true readinessState.enabled: true @@ -15,7 +15,7 @@ management: show-details: always probes.enabled: true group: - readiness.include: readinessState,diskSpace,grpcChannel,grpc,rest + readiness.include: readinessState,diskSpace,rest liveness.include: livenessState endpoints.web.exposure.include: '*' # activate percentiles for web and grpc requests @@ -24,44 +24,61 @@ management: http.server.requests: true spring: + cloud: + config: + name: timetable main: banner-mode: off profiles: default: dev - -# properties for application -de.twomartens.template: - defaultTimeZone: Europe/Berlin - greeting: "Hello %s" + data.mongodb: + uri: ${MONGODB_CONNECTION_STRING} + auto-index-creation: true + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:2martens}/protocol/openid-connect/certs + application: + name: timetable springdoc: swagger-ui: - path: '/template/doc/' - api-docs: - path: '/template/api-docs/' + display-request-duration: true + operationsSorter: method + disable-swagger-default-url: true + path: '/timetable/v1/doc/' default-produces-media-type: 'application/json' + api-docs: + path: '/timetable/v1/api-docs' openapi: - #TODO write description/ changelog description: | - Open API Documentation for the Template - - ## Header 2 - - __YOUR TEXT HERE__ - + Open API Documentation for the Timetable API + + TODO + + ## Changelog |Version | Change | Author | Date | | ------ | -------------------------------------- | ------ | ---------- | - | 1.0 | added API Versioning (and changelog) | Jim | 28.05.2023 | + | 1.0 | added API Versioning (and changelog) | Jim | 13.07.2023 | version: 1.0 - title: 'Template API' + title: 'Timetable API' resttemplate: timeout: readTimeoutRestHealthIndicatorInMillis: 5000 connectionRestHealthIndicatorTimeoutInMillis: 5000 readTimeoutRestTemplateInMillis: 5000 - connectionRestTemplateTimeoutInMillis: 5000 \ No newline at end of file + connectionRestTemplateTimeoutInMillis: 5000 + +de.twomartens.timetable: + bahn-api: + client-id: ${DB_CLIENT_ID} + client-secret: ${DB_CLIENT_SECRET} + +time: + defaultTimeZone: Europe/Berlin diff --git a/module-server/src/main/resources/config/timetable-prod.yaml b/module-server/src/main/resources/config/timetable-prod.yaml new file mode 100644 index 0000000..0bbe067 --- /dev/null +++ b/module-server/src/main/resources/config/timetable-prod.yaml @@ -0,0 +1,2 @@ +de.twomartens.timetable: + greeting: "Moin %s, you are test" diff --git a/module-server/src/main/resources/config/timetable.yaml b/module-server/src/main/resources/config/timetable.yaml new file mode 100644 index 0000000..72dd1c3 --- /dev/null +++ b/module-server/src/main/resources/config/timetable.yaml @@ -0,0 +1,7 @@ +de.twomartens.timetable: + greeting: "Hello %s, on millenium" + statusprobe: + scheduleDuration: 60 + maxKafkaFailureDuration: 5 + maxBlobFailureCount: 5 + maxFailurePercent: 50 \ No newline at end of file diff --git a/module-server/src/main/resources/log4j2.xml b/module-server/src/main/resources/log4j2.xml index 31a9745..f9b9aa9 100644 --- a/module-server/src/main/resources/log4j2.xml +++ b/module-server/src/main/resources/log4j2.xml @@ -16,5 +16,6 @@ + \ No newline at end of file diff --git a/module-server/src/main/resources/policy-enforcer.json b/module-server/src/main/resources/policy-enforcer.json new file mode 100644 index 0000000..e5f9140 --- /dev/null +++ b/module-server/src/main/resources/policy-enforcer.json @@ -0,0 +1,6 @@ +{ + "realm": "2martens", + "auth-server-url": "https://id.2martens.de", + "resource": "timetable", + "http-method-as-scope": true +} \ No newline at end of file diff --git a/module-server/src/main/resources/templates/error.html b/module-server/src/main/resources/templates/error.html index a2d5c37..9ae71ff 100644 --- a/module-server/src/main/resources/templates/error.html +++ b/module-server/src/main/resources/templates/error.html @@ -1,5 +1,5 @@ - + diff --git a/module-server/src/main/resources/templates/error/404.html b/module-server/src/main/resources/templates/error/404.html index 6f9e73e..cefc157 100644 --- a/module-server/src/main/resources/templates/error/404.html +++ b/module-server/src/main/resources/templates/error/404.html @@ -1,5 +1,5 @@ - + diff --git a/module-server/src/main/resources/templates/errorIncludes.html b/module-server/src/main/resources/templates/errorIncludes.html index 78f10ae..0e1fde4 100644 --- a/module-server/src/main/resources/templates/errorIncludes.html +++ b/module-server/src/main/resources/templates/errorIncludes.html @@ -1,5 +1,5 @@ - + Error <th:block th:text="${status}"/> (<th:block th:text="${error}"/>)