diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c04657 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +.gradle/ +.gradle/** \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..41ea7ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,104 @@ +FROM gradle:7.6.0-jdk17 AS dependencies + +# Disable the Gradle daemon for Continuous Integration servers as correctness +# is usually a priority over speed in CI environments. Using a fresh +# runtime for each build is more reliable since the runtime is completely +# isolated from any previous builds. +ARG GRADLE_OPTS= +ENV GRADLE_OPTS="-Dorg.gradle.daemon=false -Dorg.gradle.caching=true ${GRADLE_OPTS}" + +ENV GRADLE_USER_HOME=/home/gradle/gradle-user-home + +WORKDIR /home/gradle/app + +COPY ./service/build.gradle ./service/ + +COPY build.gradle settings.gradle gradle.properties ./ + +RUN \ + gradle downloadDependencies + +COPY ./service/ ./service/ +#COPY ./agent/ ./agent/ + +ENTRYPOINT [ "gradle" ] + + +FROM dependencies AS test + +RUN \ + gradle testClasses \ + && cp --recursive \ + /home/gradle/gradle-user-home \ + /home/gradle/gradle-user-home-from-cache + +ENV TZ=Europe/Moscow +ENV GRADLE_USER_HOME=/home/gradle/gradle-user-home-from-cache + +CMD [ "test" ] + + +FROM dependencies AS builder + +# Parameter org.gradle.parallel improves build time on muti-core processors +RUN \ + gradle -Dorg.gradle.parallel=true assemble + + +FROM builder AS builder-with-caches + +RUN \ + cp --recursive \ + /home/gradle/gradle-user-home \ + /home/gradle/gradle-user-home-from-cache + +ENV GRADLE_USER_HOME=/home/gradle/gradle-user-home-from-cache + + +FROM openjdk:17.0.2-jdk-slim-buster AS package-upgrade + +COPY ./.package_upgrade / +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive xargs --arg-file=/.package_upgrade \ + apt-get install \ + --assume-yes \ + --no-install-suggests + + +FROM builder-with-caches AS build + +WORKDIR /app + +RUN mkdir -p lib config \ + && chown -R 20000:20000 . \ + && chmod -R 0755 . \ + && groupadd --system --gid 20000 app \ + && adduser --system --uid 20000 --shell /sbin/nologin --home /app --gid 20000 app + +ENV TZ=Europe/Moscow +ENV SERVER_PORT=8080 +ENV MANAGEMENT_SERVER_PORT=8282 +ENV MANAGEMENT_ENDPOINTS_WEB_BASEPATH=/actuator +ENV JAVA_TOOL_OPTIONS="\ +-Dfile.encoding=UTF-8 \ +-Djava.security.egd=file:///dev/urandom \ +-Dnetworkaddress.cache.ttl=60s \ +-XX:MaxRAMPercentage=50" + +USER app + +HEALTHCHECK --start-period=60s \ + CMD wget --quiet --timeout=1 --output-document=/dev/null \ + "http://127.0.0.1:${MANAGEMENT_SERVER_PORT}${MANAGEMENT_ENDPOINTS_WEB_BASEPATH}/health" || exit 1 + + +FROM build AS build-service + +#COPY --from=builder \ +# /home/gradle/app/agent/opentelemetry-javaagent.jar ./agent/opentelemetry-javaagent.jar +COPY --chown=app:app --from=builder \ + /home/gradle/app/service/build/libs/service.jar ./lib/service.jar + +EXPOSE ${SERVER_PORT} + +ENTRYPOINT [ "java", "-jar", "lib/service.jar" ] diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d023692 --- /dev/null +++ b/build.gradle @@ -0,0 +1,113 @@ +gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS + +buildscript { + ext { + springBootVersion = '3.4.3' + springCloudVersion = '2024.0.0' + gatewayStarterVersion = '4.2.0' + logbackVersion = '1.2.9' + logstashLogbackVersion = '7.3' + lombokVersion = '1.18.36' + } + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" + } +} + +subprojects { + apply plugin: 'java' + apply plugin: 'org.springframework.boot' + apply plugin: 'io.spring.dependency-management' + + group = 'me.bvn13.jateway' + + sourceCompatibility = 17 + targetCompatibility = 17 + + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + } + + configurations { + compileOnly { + extendsFrom annotationProcessor + } + } + + repositories { + mavenCentral() + } + +//------------------- Настройка интеграционных тестов (start) --------------------- + sourceSets { + integrationTest { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + compileClasspath += sourceSets.test.output + runtimeClasspath += sourceSets.test.output + srcDir file('src/integration-test/java') + } + resources.srcDir file('src/integration-test/resources') + } + } + + configurations { + integrationTestImplementation.extendsFrom testImplementation + integrationTestRuntimeOnly.extendsFrom testRuntimeOnly + } + + task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + outputs.upToDateWhen { false } + } + + check.dependsOn integrationTest + integrationTest.mustRunAfter test +//------------------- Настройка интеграционных тестов (end) --------------------- + + dependencies { + implementation "org.springframework.boot:spring-boot-starter:${springBootVersion}" + + testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + + compileOnly "org.projectlombok:lombok:${lombokVersion}" + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + + // testImplementation 'org.assertj:assertj-core' + // testImplementation 'org.mockito:mockito-core' + } + + test { + useJUnitPlatform() + systemProperties System.properties + // Нужно, иначе Jacoco не работает + systemProperties['user.dir'] = workingDir + } + integrationTest { + useJUnitPlatform() + systemProperties System.properties + } +} + +// Task to download all dependencies for all targets +task downloadDependencies { + doLast { + rootProject.allprojects { project -> + Set configurations = project.buildscript.configurations + project.configurations + configurations.findAll { c -> c.canBeResolved } + .forEach { c -> c.resolve() } + } + } +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..66ddf24 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,24 @@ +version: '3.8' + +networks: + your-hike: + external: true + +services: + + yh-backend-gateway: + build: ./ + container_name: yh-gateway + networks: + - your-hike + restart: always + environment: + EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE?Adjust default zone to Eureka} + SPRING_CLOUD_CONFIG_URL: ${SPRING_CLOUD_CONFIG_URL?Specify config server URL} + SPRING_CONFIG_IMPORT: optional:configserver:${SPRING_CLOUD_CONFIG_URL?Specify config server URL} + AUTH_URL: ${AUTH_URL?Specify Auth URL} + SPRING_BOOT_ADMIN_CLIENT_URL: http://yh-admin-panel:8080/ + OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT?Specify OpenTelemetry exporter endpoint like http://localhost:4317} + OTEL_SERVICE_NAME: yh-gateway + volumes: + - /var/log/yh:/var/log/yh diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e474769 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.console = plain +org.gradle.logging.level = info +org.gradle.jvmargs = -Xmx4G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e382118 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/service/.gitignore b/service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/service/build.gradle b/service/build.gradle new file mode 100644 index 0000000..821751e --- /dev/null +++ b/service/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'java' +} + +repositories { + mavenCentral() +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +dependencies { + implementation "org.springframework.cloud:spring-cloud-starter-gateway:${gatewayStarterVersion}" + // https://mvnrepository.com/artifact/com.google.guava/guava + implementation group: 'com.google.guava', name: 'guava', version: '33.4.0-jre' + + // actuator + implementation "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + + // logging + implementation "net.logstash.logback:logstash-logback-encoder:${logstashLogbackVersion}" + + // testing + testImplementation "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testImplementation "org.springframework.cloud:spring-cloud-contract-wiremock" +} + +springBoot { + // для использования со Spring Actuator: отображает в actuator/info информацию о приложении + buildInfo() +} diff --git a/service/src/main/java/me/bvn13/jateway/JatewayApplication.java b/service/src/main/java/me/bvn13/jateway/JatewayApplication.java new file mode 100644 index 0000000..620ef53 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/JatewayApplication.java @@ -0,0 +1,15 @@ +package me.bvn13.jateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class JatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(JatewayApplication.class, args); + } + +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/GatewayRoutingLoggingFilter.java b/service/src/main/java/me/bvn13/jateway/logging/GatewayRoutingLoggingFilter.java new file mode 100644 index 0000000..8a07d67 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/GatewayRoutingLoggingFilter.java @@ -0,0 +1,39 @@ +package me.bvn13.jateway.logging; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@Component +public class GatewayRoutingLoggingFilter implements GlobalFilter { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + + final Set uris = exchange.getAttributeOrDefault(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, Collections.emptySet()); + final String originalUri = (uris.isEmpty()) + ? exchange.getRequest().getURI().toString() + : uris.iterator().next().toString(); + final Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + final URI routeUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + log.info("Incoming request " + originalUri + " is routed with <" + route.getId() + "> to URL: " + routeUrl); + return chain.filter(exchange); + + } + +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/IgnoringActuatorLoggingFilter.java b/service/src/main/java/me/bvn13/jateway/logging/IgnoringActuatorLoggingFilter.java new file mode 100644 index 0000000..ba13479 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/IgnoringActuatorLoggingFilter.java @@ -0,0 +1,39 @@ +package me.bvn13.jateway.logging; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +public class IgnoringActuatorLoggingFilter extends Filter { + private static final Pattern ACTUATOR = + Pattern.compile("GET \"/(health|prometheus)\", parameters=\\{}"); + private static final Pattern COMPLETED = + Pattern.compile("Completed 200 OK"); + + private final Set activeThreads = new HashSet<>(); + + @Override + public FilterReply decide(ILoggingEvent loggingEvent) { + if (isHealthOrPrometheus(loggingEvent.getMessage())) { + activeThreads.add(loggingEvent.getThreadName()); + return FilterReply.DENY; + } else if (isCompleted200Ok(loggingEvent.getMessage()) && activeThreads.remove(loggingEvent.getThreadName())) { + return FilterReply.DENY; + } else { + return FilterReply.ACCEPT; + } + } + + private boolean isHealthOrPrometheus(String message) { + return ACTUATOR.matcher(message).matches(); + } + + private boolean isCompleted200Ok(String message) { + return COMPLETED.matcher(message).matches(); + } +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/LoggingConfig.java b/service/src/main/java/me/bvn13/jateway/logging/LoggingConfig.java new file mode 100644 index 0000000..8df8558 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/LoggingConfig.java @@ -0,0 +1,18 @@ +package me.bvn13.jateway.logging; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@Configuration +public class LoggingConfig { + + @Bean + public ReactiveLoggingFilter reactiveLoggingFilter() { + return new ReactiveLoggingFilter(); + } + +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/ReactiveLoggingFilter.java b/service/src/main/java/me/bvn13/jateway/logging/ReactiveLoggingFilter.java new file mode 100644 index 0000000..d4ac726 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/ReactiveLoggingFilter.java @@ -0,0 +1,88 @@ +package me.bvn13.jateway.logging; + +import static me.bvn13.jateway.utils.DateFormats.*; + +import java.time.Instant; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebExchangeDecorator; + +import lombok.extern.slf4j.Slf4j; +import me.bvn13.jateway.utils.LogEscapeUtils; +import me.bvn13.jateway.utils.LogFormatUtils; +import reactor.core.publisher.Mono; + +@Slf4j +public class ReactiveLoggingFilter implements GlobalFilter, Ordered { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + final Instant requestTime = Instant.now(); + + final StringBuilder reqBb = new StringBuilder(); + final StringBuilder respBb = new StringBuilder(); + + ServerWebExchangeDecorator exchangeDecorator = new ServerWebExchangeDecorator(exchange) { + @Override + public ServerHttpRequest getRequest() { + return new RequestLoggingWrapper(super.getRequest(), reqBb); + } + + @Override + public ServerHttpResponse getResponse() { + return new ResponseLoggingWrapper(super.getResponse(), respBb); + } + }; + return chain.filter(exchangeDecorator).doOnSuccess(aVoid -> { + log(requestTime, exchange.getRequest(), reqBb.toString(), exchange.getResponse(), respBb.toString(), null); + }).doOnError(throwable -> { + log(requestTime, exchange.getRequest(), reqBb.toString(), exchange.getResponse(), respBb.toString(), throwable); + }); + } + + + @Override + public int getOrder() { +// return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; + return Ordered.HIGHEST_PRECEDENCE; + } + + private void log(Instant requestTime, ServerHttpRequest request, String requestBody, ServerHttpResponse response, String responseBody, @Nullable Throwable throwable) { + String message = formatMessage(requestTime, request, requestBody, response, responseBody); + + if (throwable == null) { + log.info(message); + } else { + log.info(message, throwable); + } + } + + private String formatMessage(Instant requestTime, ServerHttpRequest request, String requestBody, ServerHttpResponse response, String responseBody) { + StringBuilder req = new StringBuilder("Normalized "); + req.append(" " + request.getMethod() + " uri=" + request.getURI() + "\n") + .append("request_headers=[\n" + LogFormatUtils.formatHeaders(request.getHeaders()) + "]\n") + .append("request_time=" + formatLocalDateTime(requestTime) + "\n"); + if (!requestBody.isEmpty()) { + req.append("request_body=[\n" + requestBody + "\n]"); + } + String filteredReqMsg = req.toString(); + + StringBuilder resp = new StringBuilder(); + resp.append("\nResponse " + response.getStatusCode() + "\n" + + "response_headers=[\n" + LogFormatUtils.formatHeaders(response.getHeaders()) + "]\n"); + if (!responseBody.isEmpty()) { + resp.append("response_body=[\n" + responseBody + "\n]"); + } + + String filteredRespMsg = resp.toString(); + + return LogEscapeUtils.escapeWithLf(filteredReqMsg + filteredRespMsg); + } + +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/RequestLoggingWrapper.java b/service/src/main/java/me/bvn13/jateway/logging/RequestLoggingWrapper.java new file mode 100644 index 0000000..b0866e7 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/RequestLoggingWrapper.java @@ -0,0 +1,34 @@ +package me.bvn13.jateway.logging; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import reactor.core.publisher.Flux; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; + +@Slf4j +class RequestLoggingWrapper extends ServerHttpRequestDecorator { + private final StringBuilder bb; + + public RequestLoggingWrapper(ServerHttpRequest delegate, StringBuilder bb) { + super(delegate); + this.bb = bb; + } + + @Override + public Flux getBody() { + return super.getBody().doOnNext(data ->{ + try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Channels.newChannel(baos).write(data.asByteBuffer().asReadOnlyBuffer()); + bb.append(new String(baos.toByteArray(), StandardCharsets.UTF_8)); + } catch (IOException ex) { + log.error("Error when attempt to collect request body", ex); + } + }); + } +} diff --git a/service/src/main/java/me/bvn13/jateway/logging/ResponseLoggingWrapper.java b/service/src/main/java/me/bvn13/jateway/logging/ResponseLoggingWrapper.java new file mode 100644 index 0000000..e1a0f3f --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/logging/ResponseLoggingWrapper.java @@ -0,0 +1,40 @@ +package me.bvn13.jateway.logging; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; + +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +class ResponseLoggingWrapper extends ServerHttpResponseDecorator { + private final StringBuilder bb; + + public ResponseLoggingWrapper(ServerHttpResponse delegate, StringBuilder bb) { + super(delegate); + this.bb = bb; + } + + @Override + public Mono writeWith(Publisher body) { + final Logger l = log; + Flux buffer = Flux.from(body); + return super.writeWith(buffer.doOnNext( data -> { + try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Channels.newChannel(baos).write(data.asByteBuffer().asReadOnlyBuffer()); + bb.append(new String(baos.toByteArray(), StandardCharsets.UTF_8)); + } catch (IOException ex) { + l.error("Error when attempt to collect response body", ex); + } + })); + } +} diff --git a/service/src/main/java/me/bvn13/jateway/utils/DateFormats.java b/service/src/main/java/me/bvn13/jateway/utils/DateFormats.java new file mode 100644 index 0000000..74ab3a4 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/utils/DateFormats.java @@ -0,0 +1,26 @@ +package me.bvn13.jateway.utils; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import jakarta.annotation.Nonnull; + +public class DateFormats { + private static final String OFFSET_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss+Z"; + private static final DateTimeFormatter LOCAL_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") + .withZone(ZoneId.systemDefault()); + private static final DateTimeFormatter OFFSET_DATE_TIME = DateTimeFormatter.ofPattern(OFFSET_DATE_TIME_FORMAT); + + @Nonnull + public static String formatLocalDateTime(@Nonnull Instant timestamp) { + return LOCAL_DATE_TIME.format(timestamp); + } + + public static String format(OffsetDateTime offsetDateTime) { + return OFFSET_DATE_TIME.format(offsetDateTime); + } + private DateFormats() { + } +} diff --git a/service/src/main/java/me/bvn13/jateway/utils/LogEscapeUtils.java b/service/src/main/java/me/bvn13/jateway/utils/LogEscapeUtils.java new file mode 100644 index 0000000..a820a15 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/utils/LogEscapeUtils.java @@ -0,0 +1,24 @@ +package me.bvn13.jateway.utils; + +import com.google.common.escape.Escaper; +import com.google.common.net.PercentEscaper; + +import jakarta.annotation.Nullable; + +public class LogEscapeUtils { + private static final Escaper ESCAPER_WITH_LF = new PercentEscaper("\n $#%!?={}[]()<>\"':;,._/\\*-+@&№^" + + "абвгдеёжзийклмнопрстуфхцчшщъыьэюя" + + "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ", false); + + public static String escapeWithLf(@Nullable String str) { + if (str == null) { + return "null"; + } + str = str.replace("\r", ""); + str = str.replace("\t", " "); + return ESCAPER_WITH_LF.escape(str); + } + + private LogEscapeUtils() { + } +} diff --git a/service/src/main/java/me/bvn13/jateway/utils/LogFormatUtils.java b/service/src/main/java/me/bvn13/jateway/utils/LogFormatUtils.java new file mode 100644 index 0000000..7b93383 --- /dev/null +++ b/service/src/main/java/me/bvn13/jateway/utils/LogFormatUtils.java @@ -0,0 +1,17 @@ +package me.bvn13.jateway.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +public class LogFormatUtils { + public static String formatHeaders(Map> responseHeaders) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + responseHeaders.forEach((name, values) -> { + values.forEach(value -> pw.println(name + ": " + value)); + }); + return sw.toString(); + } +} diff --git a/service/src/main/resources/application.yaml b/service/src/main/resources/application.yaml new file mode 100644 index 0000000..b4a082e --- /dev/null +++ b/service/src/main/resources/application.yaml @@ -0,0 +1,44 @@ +spring: + application: + name: jateway + profiles: + active: dev + +management: + server: + port: 8203 + endpoints: + enabled-by-default: false + web: + base-path: /actuator + exposure: + include: health,info,env,loggers,mappings,metrics,logfile,routing,httptrace + endpoint: + env: + enabled: true + health: + enabled: true + info: + enabled: true + loggers: + enabled: true + mappings: + enabled: true + metrics: + enabled: true + logfile: + enabled: true + routing: + enabled: true + httptrace: + enabled: true + +logging: + level: + root: info + org.springframework.web.filter.CommonsRequestLoggingFilter: debug + org.springframework.web.server.adapter.HttpWebHandlerAdapter: debug + org.springframework.web.reactive.handler.SimpleUrlHandlerMapping: debug + org.springframework.web.reactive.resource.ResourceWebHandler: debug + org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler: debug +# org.springframework.web: debug diff --git a/service/src/main/resources/logback.xml b/service/src/main/resources/logback.xml new file mode 100644 index 0000000..9d7095b --- /dev/null +++ b/service/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %date %level [%thread] %logger{10} [%file:%line] %msg%n + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7b2a1c1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'jateway' + +include 'service'