getAllPersons() {
+ return this.repository.findAll();
+ }
+}
diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml
new file mode 100644
index 00000000..bf91784a
--- /dev/null
+++ b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml
@@ -0,0 +1,8 @@
+spring:
+ datasource:
+ # Эти свойства будут перегружены свойствами в docker-compose.yml
+ url: jdbc:postgresql://localhost:5432/db
+ username: postgres
+ password: postgres
+ jpa:
+ generate-ddl: true
diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html
new file mode 100644
index 00000000..a80ea187
--- /dev/null
+++ b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Главная страницв
+
+
+Главная страница
+Список всех лиц доступен по ссылке.
+Перезапустив приложение можно добавить ещё в БД.
+
+
diff --git a/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java b/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java
new file mode 100644
index 00000000..93fe13c4
--- /dev/null
+++ b/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.docker;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@AutoConfigureTestDatabase
+class DockerComposeExampleApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/2025-11/spring-33-docker/helloWorld.txt b/2025-11/spring-33-docker/helloWorld.txt
new file mode 100644
index 00000000..f2c3689e
--- /dev/null
+++ b/2025-11/spring-33-docker/helloWorld.txt
@@ -0,0 +1,20 @@
+docker image ls
+docker pull hello-world
+docker image ls
+docker image rm hello-world
+docker ps
+docker run hello-world
+docker ps
+docker run hello-world
+docker ps -all
+docker run --rm hello-world
+
+docker run -it --name=ubuntu-run ubuntu bash
+docker start ubuntu-run
+docker exec -it ubuntu-run bash
+
+docker run -d -p:8080:80 --name=my-nginx nginx
+docker ps -a
+curl http://localhost:8080
+docker kill my-nginx
+docker rm my-nginx
diff --git a/2025-11/spring-33-docker/image/Dockerfile b/2025-11/spring-33-docker/image/Dockerfile
new file mode 100644
index 00000000..c51e4bde
--- /dev/null
+++ b/2025-11/spring-33-docker/image/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.11-alpine
+COPY index.html /usr/share/nginx/html/index.html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/2025-11/spring-33-docker/image/index.html b/2025-11/spring-33-docker/image/index.html
new file mode 100644
index 00000000..f3e333e8
--- /dev/null
+++ b/2025-11/spring-33-docker/image/index.html
@@ -0,0 +1 @@
+Hello World
diff --git a/2025-11/spring-33-docker/image/readme.txt b/2025-11/spring-33-docker/image/readme.txt
new file mode 100644
index 00000000..9dde7083
--- /dev/null
+++ b/2025-11/spring-33-docker/image/readme.txt
@@ -0,0 +1,5 @@
+docker build -t my-demo-image:v1 .
+docker images | grep my-demo-image
+docker run -p:80:80 --rm my-demo-image:v1
+docker ps
+curl http://localhost
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/.gitignore b/2025-11/spring-34-kuber/.gitignore
new file mode 100755
index 00000000..dae2cf99
--- /dev/null
+++ b/2025-11/spring-34-kuber/.gitignore
@@ -0,0 +1,39 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+!gradle-wrapper.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Ignore Gradle project-specific cache directory
+.gradle
+/buildSrc/.gradle/
+
+# Ignore Gradle build output directory
+build
+out
+
+#Idea
+*.iml
+*.iws
+*.ipr
+*.idea
+
diff --git a/2025-11/spring-34-kuber/HttpRequests.http b/2025-11/spring-34-kuber/HttpRequests.http
new file mode 100755
index 00000000..29913ccf
--- /dev/null
+++ b/2025-11/spring-34-kuber/HttpRequests.http
@@ -0,0 +1,34 @@
+###
+GET http://localhost:8080/hi?name=Ivan
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+POST http://localhost:8080/response/Ivan
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+{
+ "param1": "pa1",
+ "param2": "pa2"
+}
+
+###
+GET http://localhost:8090/actuator/health
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8090/actuator/health/liveness
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8090/actuator/health/readiness
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
diff --git a/2025-11/spring-34-kuber/LICENSE b/2025-11/spring-34-kuber/LICENSE
new file mode 100755
index 00000000..ada0a70c
--- /dev/null
+++ b/2025-11/spring-34-kuber/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Sergey Petrelevich
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/2025-11/spring-34-kuber/README.md b/2025-11/spring-34-kuber/README.md
new file mode 100755
index 00000000..abe57084
--- /dev/null
+++ b/2025-11/spring-34-kuber/README.md
@@ -0,0 +1 @@
+# gitlab Hello
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/build.gradle.kts b/2025-11/spring-34-kuber/build.gradle.kts
new file mode 100755
index 00000000..dff4344e
--- /dev/null
+++ b/2025-11/spring-34-kuber/build.gradle.kts
@@ -0,0 +1,116 @@
+import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
+import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
+import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel
+import org.gradle.api.plugins.JavaPluginExtension
+import fr.brouillard.oss.gradle.plugins.JGitverPluginExtension
+import fr.brouillard.oss.gradle.plugins.JGitverPlugin
+import name.remal.gradle_plugins.sonarlint.SonarLintExtension
+
+plugins {
+ idea
+ id("fr.brouillard.oss.gradle.jgitver")
+ id("io.spring.dependency-management")
+ id("org.springframework.boot") apply false
+ id("name.remal.sonarlint") apply false
+ id("com.diffplug.spotless") apply false
+}
+
+
+idea {
+ project {
+ languageLevel = IdeaLanguageLevel(21)
+ }
+ module {
+ isDownloadJavadoc = true
+ isDownloadSources = true
+ }
+}
+
+
+allprojects {
+ group = "ru.petrelevich"
+
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ }
+
+ apply(plugin = "io.spring.dependency-management")
+ dependencyManagement {
+ dependencies {
+ imports {
+ mavenBom(BOM_COORDINATES)
+ }
+ }
+ }
+
+ configurations.all {
+ resolutionStrategy {
+ failOnVersionConflict()
+
+ force("commons-io:commons-io:2.16.1")
+ force("org.eclipse.jgit:org.eclipse.jgit:6.9.0.202403050737-r")
+ force("org.apache.commons:commons-compress:1.26.1")
+ force("com.google.errorprone:error_prone_annotations:2.28.0")
+ force("org.jetbrains:annotations:19.0.0")
+ }
+ }
+
+ tasks.withType {
+ options.encoding = "UTF-8"
+ options.compilerArgs.addAll(listOf("-Xlint:all,-serial,-processing", "-Werror"))
+ }
+
+ apply()
+ configure {
+ nodeJs {
+ detectNodeJs = false
+ logNodeJsNotFound = false
+ }
+ }
+
+ apply()
+ configure {
+ java {
+ palantirJavaFormat("2.50.0")
+ }
+ }
+
+ tasks.withType {
+ useJUnitPlatform()
+ testLogging.showExceptions = true
+ reports {
+ junitXml.required.set(true)
+ html.required.set(true)
+ }
+ }
+
+ plugins.apply(JavaPlugin::class.java)
+ extensions.configure {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+
+ plugins.apply(JGitverPlugin::class.java)
+ extensions.configure {
+ strategy("PATTERN")
+ nonQualifierBranches("main,master")
+ tagVersionPattern("\${v}\${()
+ .managedVersions
+ .toSortedMap()
+ .map { "${it.key}:${it.value}" }
+ .forEach(::println)
+ }
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/gradle.properties b/2025-11/spring-34-kuber/gradle.properties
new file mode 100755
index 00000000..efdab048
--- /dev/null
+++ b/2025-11/spring-34-kuber/gradle.properties
@@ -0,0 +1,12 @@
+# -------Gradle--------
+org.gradle.jvmargs=-Xmx4g
+org.gradle.daemon=true
+org.gradle.parallel=true
+# -------Plugins---------
+jgitver=0.10.0-rc03
+dependencyManagement=1.1.6
+springframeworkBoot=3.4.0
+jib=3.4.4
+sonarlint=4.3.1
+spotless=6.25.0
+# -------Versions--------
diff --git a/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 00000000..7f93135c
Binary files /dev/null and b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 00000000..e2847c82
--- /dev/null
+++ b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/2025-11/spring-34-kuber/gradlew b/2025-11/spring-34-kuber/gradlew
new file mode 100755
index 00000000..1aa94a42
--- /dev/null
+++ b/2025-11/spring-34-kuber/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# 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"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/2025-11/spring-34-kuber/gradlew.bat b/2025-11/spring-34-kuber/gradlew.bat
new file mode 100755
index 00000000..93e3f59f
--- /dev/null
+++ b/2025-11/spring-34-kuber/gradlew.bat
@@ -0,0 +1,92 @@
+@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=.
+@rem This is normally unused
+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% equ 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% equ 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!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/2025-11/spring-34-kuber/kube/config.yaml b/2025-11/spring-34-kuber/kube/config.yaml
new file mode 100755
index 00000000..01378922
--- /dev/null
+++ b/2025-11/spring-34-kuber/kube/config.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: rest-service-config
+data:
+ JAVA_TOOL_OPTIONS: -XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/kube/deployment.yaml b/2025-11/spring-34-kuber/kube/deployment.yaml
new file mode 100755
index 00000000..8bbb1d80
--- /dev/null
+++ b/2025-11/spring-34-kuber/kube/deployment.yaml
@@ -0,0 +1,52 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: rest-hello-deployment
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: rest-hello
+ strategy:
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 1
+ type: RollingUpdate
+ template:
+ metadata:
+ labels:
+ app: rest-hello
+ spec:
+ containers:
+ - image: registry.gitlab.com/petrelevich/dockerregistry/rest-hello:2.0.2-1.2d40be46.dirty-SNAPSHOT
+ name: rest-hello
+ ports:
+ - containerPort: 8080
+ envFrom:
+ - configMapRef:
+ name: rest-service-config
+ resources:
+ requests:
+ memory: "256M"
+ limits:
+ memory: "256M"
+ readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /actuator/health/readiness
+ port: 8090
+ periodSeconds: 10
+ successThreshold: 1
+ timeoutSeconds: 1
+ livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /actuator/health/liveness
+ port: 8090
+ periodSeconds: 10
+ successThreshold: 1
+ timeoutSeconds: 1
+ initialDelaySeconds: 10
+ imagePullSecrets:
+ - name: regcred
diff --git a/2025-11/spring-34-kuber/kube/ingress.yaml b/2025-11/spring-34-kuber/kube/ingress.yaml
new file mode 100755
index 00000000..ea92a13f
--- /dev/null
+++ b/2025-11/spring-34-kuber/kube/ingress.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: rest-hello
+spec:
+ ingressClassName: traefik
+ rules:
+ - http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: rest-hello
+ port:
+ number: 80
diff --git a/2025-11/spring-34-kuber/kube/service.yaml b/2025-11/spring-34-kuber/kube/service.yaml
new file mode 100755
index 00000000..21b1add5
--- /dev/null
+++ b/2025-11/spring-34-kuber/kube/service.yaml
@@ -0,0 +1,12 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: rest-hello
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ selector:
+ app: rest-hello
+ type: ClusterIP
diff --git a/2025-11/spring-34-kuber/rest-hello/build.gradle.kts b/2025-11/spring-34-kuber/rest-hello/build.gradle.kts
new file mode 100755
index 00000000..4c77e8c9
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/build.gradle.kts
@@ -0,0 +1,39 @@
+plugins {
+ id("java")
+ id("org.springframework.boot")
+ id("com.google.cloud.tools.jib")
+ id("fr.brouillard.oss.gradle.jgitver")
+}
+
+
+apply(plugin = "com.google.cloud.tools.jib")
+
+dependencies {
+ implementation("org.springframework.boot:spring-boot-starter-web")
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+}
+
+tasks {
+ jib {
+ container {
+ creationTime.set("USE_CURRENT_TIMESTAMP")
+ }
+ from {
+ image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1"
+ }
+ to {
+ tags = setOf(project.version.toString())
+ image = "registry.gitlab.com/petrelevich/dockerregistry/${project.name}"
+ auth {
+ username = System.getenv("GITLAB_USERNAME")
+ password = System.getenv("GITLAB_PASSWORD")
+ }
+ }
+ }
+//docker login registry.gitlab.com
+//docker run -p 8080:8080 registry.gitlab.com/petrelevich/dockerregistry/rest-hello
+
+ build {
+ dependsOn(jib)
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java
new file mode 100755
index 00000000..5abbd89c
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java
@@ -0,0 +1,23 @@
+package ru.petrelevich;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class Application {
+ private static final Logger logger = LoggerFactory.getLogger(Application.class);
+
+ public static void main(String[] args) {
+ com.sun.management.OperatingSystemMXBean os = (com.sun.management.OperatingSystemMXBean)
+ java.lang.management.ManagementFactory.getOperatingSystemMXBean();
+
+ logger.info("availableProcessors:{}", Runtime.getRuntime().availableProcessors());
+ logger.info("TotalMemorySize, mb:{}", os.getTotalMemorySize() / 1024 / 1024);
+ logger.info("maxMemory, mb:{}", Runtime.getRuntime().maxMemory() / 1024 / 1024);
+ logger.info("freeMemory, mb:{}", Runtime.getRuntime().freeMemory() / 1024 / 1024);
+
+ new SpringApplicationBuilder().sources(Application.class).run(args);
+ }
+}
diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java
new file mode 100755
index 00000000..7c1d9701
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java
@@ -0,0 +1,25 @@
+package ru.petrelevich.controller;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class IndexController {
+
+ @GetMapping("/hi")
+ public String hi(@RequestParam(name = "name") String name) throws UnknownHostException {
+ return String.format(
+ "Hi, %s. It works, host: %s", name, InetAddress.getLocalHost().getHostName());
+ }
+
+ @PostMapping("/response/{name}")
+ public Response response(@PathVariable("name") String name, @RequestBody Request params) {
+ return new Response(name, String.format("%s-%s", params.param1(), params.param2()));
+ }
+}
diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java
new file mode 100755
index 00000000..4aba59c4
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java
@@ -0,0 +1,3 @@
+package ru.petrelevich.controller;
+
+public record Request(String param1, String param2) {}
diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java
new file mode 100755
index 00000000..91b27a4f
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java
@@ -0,0 +1,3 @@
+package ru.petrelevich.controller;
+
+public record Response(String name, String result) {}
diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml b/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml
new file mode 100755
index 00000000..bb9154dc
--- /dev/null
+++ b/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml
@@ -0,0 +1,10 @@
+management:
+ server:
+ port: 8090
+ endpoints:
+ enabled-by-default: false
+ endpoint:
+ health:
+ enabled: true
+ probes:
+ enabled: true
\ No newline at end of file
diff --git a/2025-11/spring-34-kuber/settings.gradle.kts b/2025-11/spring-34-kuber/settings.gradle.kts
new file mode 100755
index 00000000..a89f82f3
--- /dev/null
+++ b/2025-11/spring-34-kuber/settings.gradle.kts
@@ -0,0 +1,20 @@
+rootProject.name = "spring-kuber"
+include ("rest-hello")
+
+pluginManagement {
+ val jgitver: String by settings
+ val dependencyManagement: String by settings
+ val springframeworkBoot: String by settings
+ val jib: String by settings
+ val sonarlint: String by settings
+ val spotless: String by settings
+
+ plugins {
+ id("fr.brouillard.oss.gradle.jgitver") version jgitver
+ id("io.spring.dependency-management") version dependencyManagement
+ id("org.springframework.boot") version springframeworkBoot
+ id("com.google.cloud.tools.jib") version jib
+ id("name.remal.sonarlint") version sonarlint
+ id("com.diffplug.spotless") version spotless
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/.gitignore b/2025-11/spring-35-36-spring-cloud/.gitignore
new file mode 100755
index 00000000..dae2cf99
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/.gitignore
@@ -0,0 +1,39 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+!gradle-wrapper.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Ignore Gradle project-specific cache directory
+.gradle
+/buildSrc/.gradle/
+
+# Ignore Gradle build output directory
+build
+out
+
+#Idea
+*.iml
+*.iws
+*.ipr
+*.idea
+
diff --git a/2025-11/spring-35-36-spring-cloud/README.md b/2025-11/spring-35-36-spring-cloud/README.md
new file mode 100755
index 00000000..387c879c
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/README.md
@@ -0,0 +1 @@
+# Spring Cloud example
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts
new file mode 100755
index 00000000..bd891347
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ id("com.google.cloud.tools.jib")
+}
+
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+ implementation("ch.qos.logback:logback-classic")
+
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation("org.springframework.cloud:spring-cloud-starter-gateway")
+ implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+}
+
+jib {
+ container {
+ creationTime.set("USE_CURRENT_TIMESTAMP")
+ }
+ from {
+ image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1"
+ }
+
+ to {
+ image = "localrun/api-gateway"
+ tags = setOf(project.version.toString())
+ }
+}
+
+tasks {
+ build {
+ dependsOn(spotlessApply)
+ dependsOn(jibBuildTar)
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/hey b/2025-11/spring-35-36-spring-cloud/api-gateway/hey
new file mode 100755
index 00000000..27aec446
Binary files /dev/null and b/2025-11/spring-35-36-spring-cloud/api-gateway/hey differ
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh b/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh
new file mode 100755
index 00000000..45350fd6
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+../gradlew :api-gateway:build
+
+docker load --input build/jib-image.tar
+
+docker stop api-gateway
+
+docker run --rm -d --name api-gateway \
+--memory=512m \
+--cpus 1 \
+--network="host" \
+-v $HOME/.ssh:/root/.ssh \
+-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \
+localrun/api-gateway:latest
+
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh b/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh
new file mode 100755
index 00000000..7a0a9b20
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+./hey -n=1000000 -c=1 -m GET http://localhost:7777/client/info?name=testClientName
+
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java
new file mode 100755
index 00000000..6c3bc85a
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java
@@ -0,0 +1,22 @@
+package ru.demo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ApiGateway {
+ private static final Logger log = LoggerFactory.getLogger(ApiGateway.class);
+
+ /*
+ curl -v http://localhost:7777/client/info?name=testClientName
+ curl -v http://localhost:7777/order/info?id=testOrderId
+
+ */
+
+ public static void main(String[] args) {
+ SpringApplication.run(ApiGateway.class, args);
+ log.info("SrvApiGateway started");
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java
new file mode 100755
index 00000000..c9526422
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java
@@ -0,0 +1,34 @@
+package ru.demo.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import ru.demo.controller.XrequestFilter;
+
+@Configuration
+@EnableConfigurationProperties(ApplConfigProperties.class)
+@EnableDiscoveryClient
+public class ApiConfig {
+
+ @Bean
+ public XrequestFilter xrequestFilter() {
+ return new XrequestFilter();
+ }
+
+ @Bean
+ RouteLocator gateway(
+ RouteLocatorBuilder rlb, ApplConfigProperties applConfigProperties, XrequestFilter xrequestFilter) {
+ var routesBuilder = rlb.routes();
+ for (var route : applConfigProperties.getApiRoutes()) {
+ routesBuilder.route(route.id(), routeSpec -> routeSpec
+ .path(String.format("/%s/**", route.from()))
+ .filters(fs -> fs.filters(xrequestFilter)
+ .rewritePath(String.format("/%s/(?.*)", route.from()), "/${segment}"))
+ .uri(String.format("%s/@", route.to())));
+ }
+ return routesBuilder.build();
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java
new file mode 100755
index 00000000..85e84ccf
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java
@@ -0,0 +1,3 @@
+package ru.demo.config;
+
+public record ApiRoute(String id, String from, String to) {}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java
new file mode 100755
index 00000000..47cc8501
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java
@@ -0,0 +1,30 @@
+package ru.demo.config;
+
+import java.util.List;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.bind.ConstructorBinding;
+
+@ConfigurationProperties(prefix = "application")
+public class ApplConfigProperties {
+ private final String authHost;
+ private final List apiRoutes;
+
+ @ConstructorBinding
+ public ApplConfigProperties(String authHost, List apiRoutes) {
+ this.authHost = authHost;
+ this.apiRoutes = apiRoutes;
+ }
+
+ public String getAuthHost() {
+ return authHost;
+ }
+
+ public List getApiRoutes() {
+ return apiRoutes;
+ }
+
+ @Override
+ public String toString() {
+ return "ApplConfigProperties{" + "authHost='" + authHost + '\'' + ", apiRoutes=" + apiRoutes + '}';
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java
new file mode 100755
index 00000000..79f779be
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java
@@ -0,0 +1,38 @@
+package ru.demo.controller;
+
+import java.net.ConnectException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import reactor.util.annotation.NonNull;
+
+@Order(-2)
+@Component
+public class ErrorHandler implements ErrorWebExceptionHandler {
+ private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class);
+
+ @Override
+ public @NonNull Mono handle(@NonNull ServerWebExchange serverWebExchange, @NonNull Throwable thr) {
+ var bufferFactory = serverWebExchange.getResponse().bufferFactory();
+ var response = serverWebExchange.getResponse();
+ response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+
+ if (thr instanceof ConnectException) {
+ log.error("Target host connection error", thr);
+ response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
+ var dataBuffer = bufferFactory.wrap("Target host connection error".getBytes());
+ return response.writeWith(Mono.just(dataBuffer));
+ } else {
+ log.error("Unhandled exception", thr);
+ response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+ var dataBuffer = bufferFactory.wrap("Unhandled error".getBytes());
+ return response.writeWith(Mono.just(dataBuffer));
+ }
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java
new file mode 100755
index 00000000..0b8a5176
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java
@@ -0,0 +1,24 @@
+package ru.demo.controller;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+public class XrequestFilter implements GatewayFilter {
+ private static final String HEADER_X_REQUEST_ID = "X-Request-Id";
+ private static final Logger log = LoggerFactory.getLogger(XrequestFilter.class);
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ var guid = java.util.UUID.randomUUID().toString();
+ log.info("requestId:{}", guid);
+
+ var request =
+ exchange.getRequest().mutate().header(HEADER_X_REQUEST_ID, guid).build();
+
+ return chain.filter(exchange.mutate().request(request).build());
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml
new file mode 100755
index 00000000..fdd7a1ff
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml
@@ -0,0 +1,15 @@
+spring:
+ application:
+ name: api-gateway
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ initial-interval: 5000
+ max-attempts: 10
+ max-interval: 5000
+ multiplier: 1.2
+ config:
+ import: optional:configserver:http://localhost:8888
+ codec:
+ max-in-memory-size: 10MB
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml
new file mode 100755
index 00000000..c20eb884
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"api-gateway"}
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/build.gradle.kts
new file mode 100755
index 00000000..f9c9d78a
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/build.gradle.kts
@@ -0,0 +1,121 @@
+import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
+import name.remal.gradle_plugins.sonarlint.SonarLintExtension
+import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
+import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel
+import org.springframework.boot.gradle.plugin.SpringBootPlugin
+
+plugins {
+ idea
+ id("fr.brouillard.oss.gradle.jgitver")
+ id("io.spring.dependency-management")
+ id("org.springframework.boot") apply false
+ id("name.remal.sonarlint") apply false
+ id("com.diffplug.spotless") apply false
+}
+
+allprojects {
+ group = "ru.demo"
+
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ }
+
+ val springCloudVersion: String by project
+ val logbackEncoder: String by project
+
+ apply(plugin = "io.spring.dependency-management")
+ dependencyManagement {
+ dependencies {
+ imports {
+ mavenBom(BOM_COORDINATES)
+ mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
+ }
+ dependency("net.logstash.logback:logstash-logback-encoder:$logbackEncoder")
+ }
+ }
+
+ configurations.all {
+ resolutionStrategy {
+ failOnVersionConflict()
+
+ force("org.glassfish.hk2.external:aopalliance-repackaged:3.1.1")
+ force("org.glassfish.hk2:hk2-utils:3.1.1")
+ force("commons-logging:commons-logging:1.3.1")
+ force("com.fasterxml.woodstox:woodstox-core:6.6.2")
+ force("org.glassfish.hk2:hk2-api:3.1.1")
+ force("org.apache.httpcomponents:httpclient:4.5.14")
+ force("org.sonarsource.analyzer-commons:sonar-analyzer-commons:2.11.0.2861")
+ force("org.sonarsource.analyzer-commons:sonar-xml-parsing:2.11.0.2861")
+ force("org.sonarsource.sslr:sslr-core:1.24.0.633")
+ force("org.sonarsource.analyzer-commons:sonar-analyzer-recognizers:2.11.0.2861")
+ force("commons-io:commons-io:2.15.1")
+ force("com.google.guava:guava:32.1.3-jre")
+ force("com.google.code.findbugs:jsr305:3.0.2")
+ force("org.codehaus.woodstox:stax2-api:4.2.2")
+ force("io.opentelemetry:opentelemetry-api-incubator:1.38.0-alpha")
+ }
+ }
+}
+
+subprojects {
+ plugins.apply(SpringBootPlugin::class.java)
+ plugins.apply(JavaPlugin::class.java)
+ extensions.configure {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+
+ tasks.withType {
+ options.encoding = "UTF-8"
+ options.compilerArgs.addAll(listOf("-Xlint:all,-serial,-processing"))
+
+ dependsOn("spotlessApply")
+ }
+
+ apply()
+ configure {
+ nodeJs {
+ detectNodeJs = false
+ logNodeJsNotFound = false
+ }
+ }
+ apply()
+ configure {
+ java {
+ palantirJavaFormat("2.38.0")
+ }
+ }
+
+ plugins.apply(fr.brouillard.oss.gradle.plugins.JGitverPlugin::class.java)
+ extensions.configure {
+ strategy("PATTERN")
+ nonQualifierBranches("main,master")
+ tagVersionPattern("\${v}\${ {
+ useJUnitPlatform()
+ testLogging.showExceptions = true
+ reports {
+ junitXml.required.set(true)
+ html.required.set(true)
+ }
+ }
+}
+
+tasks {
+ val managedVersions by registering {
+ doLast {
+ project.extensions.getByType()
+ .managedVersions
+ .toSortedMap()
+ .map { "${it.key}:${it.value}" }
+ .forEach(::println)
+ }
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts
new file mode 100755
index 00000000..6d4b0399
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts
@@ -0,0 +1,37 @@
+plugins {
+ id("com.google.cloud.tools.jib")
+}
+
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation ("org.springframework.cloud:spring-cloud-config-server")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+}
+
+jib {
+ container {
+ creationTime.set("USE_CURRENT_TIMESTAMP")
+ }
+ from {
+ image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1"
+ }
+
+ to {
+ image = "localrun/config-server"
+ tags = setOf(project.version.toString())
+ }
+}
+
+tasks {
+ build {
+ dependsOn(spotlessApply)
+ dependsOn(jibBuildTar)
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh b/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh
new file mode 100755
index 00000000..5cb30633
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+../gradlew :config-server:build
+
+docker load --input build/jib-image.tar
+
+docker stop config-server
+
+docker run --rm -d --name config-server \
+--memory=1024m \
+--cpus 1 \
+--network="host" \
+-v $HOME/.ssh:/root/.ssh \
+-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \
+localrun/config-server:latest
+
diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java b/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java
new file mode 100755
index 00000000..bbbb0a42
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java
@@ -0,0 +1,19 @@
+package ru.demo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.config.server.EnableConfigServer;
+
+@SpringBootApplication
+@EnableConfigServer
+public class ConfigServer {
+ private static final Logger log = LoggerFactory.getLogger(ConfigServer.class);
+
+ public static void main(String[] args) {
+
+ SpringApplication.run(ConfigServer.class, args);
+ log.info("ConfigServer started");
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml
new file mode 100755
index 00000000..2c0a5866
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml
@@ -0,0 +1,40 @@
+server:
+ port: 8888
+
+spring:
+ application:
+ name: config-server
+ cloud:
+ config:
+ server:
+ git:
+ uri: "git@github.com:OtusTeam/Spring.git"
+ search-paths: "2025-11/spring-35-36-spring-cloud/git-config"
+ ignoreLocalSshSettings: false
+ strictHostKeyChecking: false
+ defaultLabel: "master"
+
+management:
+ tracing:
+ sampling:
+ probability: 1.0
+ endpoint:
+ prometheus:
+ enabled: true
+ metrics:
+ enabled: true
+ health:
+ enabled: true
+ probes:
+ enabled: true
+ refresh:
+ enabled: true
+ endpoints:
+ web:
+ exposure:
+ include:
+ - prometheus
+ - health
+ - metrics
+ - refresh
+ enabled-by-default: false
diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml
new file mode 100755
index 00000000..85e2f041
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"config-server"}
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json b/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json
new file mode 100644
index 00000000..b0f42ea7
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json
@@ -0,0 +1,3880 @@
+{
+ "__inputs": [
+ {
+ "name": "DS_PROMETHEUS",
+ "label": "prometheus",
+ "description": "",
+ "type": "datasource",
+ "pluginId": "prometheus",
+ "pluginName": "Prometheus"
+ }
+ ],
+ "__elements": {},
+ "__requires": [
+ {
+ "type": "grafana",
+ "id": "grafana",
+ "name": "Grafana",
+ "version": "11.4.0"
+ },
+ {
+ "type": "datasource",
+ "id": "prometheus",
+ "name": "Prometheus",
+ "version": "1.0.0"
+ },
+ {
+ "type": "panel",
+ "id": "stat",
+ "name": "Stat",
+ "version": ""
+ },
+ {
+ "type": "panel",
+ "id": "timeseries",
+ "name": "Time series",
+ "version": ""
+ }
+ ],
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "limit": 100,
+ "name": "Annotations & Alerts",
+ "showIn": 0,
+ "type": "dashboard"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "enable": true,
+ "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0",
+ "hide": false,
+ "iconColor": "rgba(255, 96, 96, 1)",
+ "name": "Restart Detection",
+ "showIn": 0,
+ "step": "1m",
+ "tagKeys": "restart-tag",
+ "textFormat": "uptime reset",
+ "titleFormat": "Restart"
+ }
+ ]
+ },
+ "description": "Dashboard for Micrometer instrumented applications (Java, Spring Boot, Micronaut)",
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 1,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "reqps"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 148,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "min",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "disableTextWrap": false,
+ "editorMode": "builder",
+ "expr": "rate(request_counter_total{application=\"service-client\"}[1m])",
+ "fullMetaSearch": false,
+ "includeNullMetadata": true,
+ "legendFormat": "{{application}}",
+ "range": true,
+ "refId": "A",
+ "useBackend": false,
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ }
+ }
+ ],
+ "title": "Счетчик запросов",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 149,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "min",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "disableTextWrap": false,
+ "editorMode": "builder",
+ "expr": "request_duration{application=\"service-client\"}",
+ "fullMetaSearch": false,
+ "includeNullMetadata": true,
+ "legendFormat": "{{application}}",
+ "range": true,
+ "refId": "A",
+ "useBackend": false,
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ }
+ }
+ ],
+ "title": "Время выполнения запроса",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 8
+ },
+ "id": 139,
+ "panels": [],
+ "title": "Quick Facts",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "decimals": 1,
+ "mappings": [
+ {
+ "options": {
+ "match": "null",
+ "result": {
+ "text": "N/A"
+ }
+ },
+ "type": "special"
+ }
+ ],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 6,
+ "x": 0,
+ "y": 9
+ },
+ "id": 63,
+ "maxDataPoints": 100,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "horizontal",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_uptime_seconds{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 14400
+ }
+ ],
+ "title": "Uptime",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [
+ {
+ "options": {
+ "match": "null",
+ "result": {
+ "text": "N/A"
+ }
+ },
+ "type": "special"
+ }
+ ],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "dateTimeAsIso"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 6,
+ "x": 6,
+ "y": 9
+ },
+ "id": 92,
+ "maxDataPoints": 100,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "horizontal",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_start_time_seconds{application=\"$application\", instance=\"$instance\"}*1000",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 14400
+ }
+ ],
+ "title": "Start time",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "decimals": 2,
+ "mappings": [
+ {
+ "options": {
+ "match": "null",
+ "result": {
+ "text": "N/A"
+ }
+ },
+ "type": "special"
+ }
+ ],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(50, 172, 45, 0.97)",
+ "value": null
+ },
+ {
+ "color": "rgba(237, 129, 40, 0.89)",
+ "value": 70
+ },
+ {
+ "color": "rgba(245, 54, 54, 0.9)",
+ "value": 90
+ }
+ ]
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 6,
+ "x": 12,
+ "y": 9
+ },
+ "id": 65,
+ "maxDataPoints": 100,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "horizontal",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"heap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "refId": "A",
+ "step": 14400
+ }
+ ],
+ "title": "Heap used",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "decimals": 2,
+ "mappings": [
+ {
+ "options": {
+ "match": "null",
+ "result": {
+ "text": "N/A"
+ }
+ },
+ "type": "special"
+ },
+ {
+ "options": {
+ "from": -1e+32,
+ "result": {
+ "text": "N/A"
+ },
+ "to": 0
+ },
+ "type": "range"
+ }
+ ],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(50, 172, 45, 0.97)",
+ "value": null
+ },
+ {
+ "color": "rgba(237, 129, 40, 0.89)",
+ "value": 70
+ },
+ {
+ "color": "rgba(245, 54, 54, 0.9)",
+ "value": 90
+ }
+ ]
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 6,
+ "x": 18,
+ "y": 9
+ },
+ "id": 75,
+ "maxDataPoints": 100,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "horizontal",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"nonheap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "refId": "A",
+ "step": 14400
+ }
+ ],
+ "title": "Non-Heap used",
+ "type": "stat"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 12
+ },
+ "id": 140,
+ "panels": [],
+ "title": "I/O Overview",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 0,
+ "y": 13
+ },
+ "id": 111,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\"}[1m]))",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "HTTP",
+ "refId": "A"
+ }
+ ],
+ "title": "Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "HTTP"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#890f02",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "HTTP - 5xx"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#bf1b00",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "HTTP"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#890f02",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "HTTP - 5xx"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#bf1b00",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 6,
+ "y": 13
+ },
+ "id": 112,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status=~\"5..\"}[1m]))",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "HTTP - 5xx",
+ "refId": "A"
+ }
+ ],
+ "title": "Errors",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 12,
+ "y": 13
+ },
+ "id": 113,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(rate(http_server_requests_seconds_sum{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))/sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "HTTP - AVG",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "max(http_server_requests_seconds_max{application=\"$application\", instance=\"$instance\", status!~\"5..\"})",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "HTTP - MAX",
+ "refId": "B"
+ }
+ ],
+ "title": "Duration",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 18,
+ "y": 13
+ },
+ "id": 119,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "tomcat_threads_busy_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "TOMCAT - BSY",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "tomcat_threads_current_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "TOMCAT - CUR",
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "tomcat_threads_config_max_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "TOMCAT - MAX",
+ "refId": "C"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jetty_threads_busy{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "JETTY - BSY",
+ "refId": "D"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jetty_threads_current{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "JETTY - CUR",
+ "refId": "E"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jetty_threads_config_max{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "JETTY - MAX",
+ "refId": "F"
+ }
+ ],
+ "title": "Utilisation",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 20
+ },
+ "id": 141,
+ "panels": [],
+ "title": "JVM Memory",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 0,
+ "y": 21
+ },
+ "id": 24,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "committed",
+ "refId": "B",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "refId": "C",
+ "step": 2400
+ }
+ ],
+ "title": "JVM Heap",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 6,
+ "y": 21
+ },
+ "id": 25,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "committed",
+ "refId": "B",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "refId": "C",
+ "step": 2400
+ }
+ ],
+ "title": "JVM Non-Heap",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 12,
+ "y": 21
+ },
+ "id": 26,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "committed",
+ "refId": "B",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\"})",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "refId": "C",
+ "step": 2400
+ }
+ ],
+ "title": "JVM Total",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 18,
+ "y": 21
+ },
+ "id": 86,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": true,
+ "intervalFactor": 2,
+ "legendFormat": "vss",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "rss",
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "swap",
+ "refId": "C"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "total",
+ "refId": "D"
+ }
+ ],
+ "title": "JVM Process Memory",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 28
+ },
+ "id": 142,
+ "panels": [],
+ "title": "JVM Misc",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 1,
+ "mappings": [],
+ "max": 1,
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "percentunit"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 0,
+ "y": 29
+ },
+ "id": 106,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "system_cpu_usage{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "system",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_cpu_usage{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "process",
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "avg_over_time(process_cpu_usage{application=\"$application\", instance=\"$instance\"}[15m])",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "process-15m",
+ "refId": "C"
+ }
+ ],
+ "title": "CPU Usage",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 1,
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 6,
+ "y": 29
+ },
+ "id": 93,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "system_load_average_1m{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "system-1m",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "system_cpu_count{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "cpus",
+ "refId": "B"
+ }
+ ],
+ "title": "Load",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 0,
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 12,
+ "y": 29
+ },
+ "id": 32,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_threads_live_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "live",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_threads_daemon_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "daemon",
+ "metric": "",
+ "refId": "B",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_threads_peak_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "peak",
+ "refId": "C",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "process",
+ "refId": "D",
+ "step": 2400
+ }
+ ],
+ "title": "Threads",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "blocked"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#bf1b00",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "new"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#fce2de",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "runnable"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#7eb26d",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "terminated"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#511749",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "timed-waiting"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#c15c17",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "waiting"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#eab839",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "blocked"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#bf1b00",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "new"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#fce2de",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "runnable"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#7eb26d",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "terminated"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#511749",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "timed-waiting"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#c15c17",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "waiting"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#eab839",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 18,
+ "y": 29
+ },
+ "id": 124,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_threads_states_threads{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "{{state}}",
+ "refId": "A"
+ }
+ ],
+ "title": "Thread States",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "description": "The percent of time spent on Garbage Collection over all CPUs assigned to the JVM process.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 1,
+ "mappings": [],
+ "max": 1,
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "percentunit"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 0,
+ "y": 36
+ },
+ "id": 138,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "sum(rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])) by (application, instance) / on(application, instance) system_cpu_count",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "CPU time spent on GC",
+ "refId": "A"
+ }
+ ],
+ "title": "GC Pressure",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 0,
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "opm"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "debug"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#1F78C1",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "error"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#BF1B00",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "info"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#508642",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "trace"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#6ED0E0",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "warn"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "#EAB839",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 6,
+ "y": 36
+ },
+ "id": 91,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "increase(logback_events_total{application=\"$application\", instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "{{level}}",
+ "metric": "",
+ "refId": "A",
+ "step": 1200
+ }
+ ],
+ "title": "Log Events",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "log": 10,
+ "type": "log"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": 0,
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 6,
+ "x": 18,
+ "y": 36
+ },
+ "id": 61,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_files_open_files{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "open",
+ "metric": "",
+ "refId": "A",
+ "step": 2400
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "process_files_max_files{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "metric": "",
+ "refId": "B",
+ "step": 2400
+ }
+ ],
+ "title": "File Descriptors",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 43
+ },
+ "id": 143,
+ "panels": [],
+ "repeat": "persistence_counts",
+ "title": "JVM Memory Pools (Heap)",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 24,
+ "x": 0,
+ "y": 44
+ },
+ "id": 3,
+ "maxPerRow": 3,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "repeat": "jvm_memory_pool_heap",
+ "repeatDirection": "h",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "metric": "",
+ "refId": "A",
+ "step": 1800
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "commited",
+ "metric": "",
+ "refId": "B",
+ "step": 1800
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "metric": "",
+ "refId": "C",
+ "step": 1800
+ }
+ ],
+ "title": "$jvm_memory_pool_heap",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 51
+ },
+ "id": 144,
+ "panels": [],
+ "title": "JVM Memory Pools (Non-Heap)",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 24,
+ "x": 0,
+ "y": 52
+ },
+ "id": 78,
+ "maxPerRow": 3,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull",
+ "max"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "repeat": "jvm_memory_pool_nonheap",
+ "repeatDirection": "h",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "metric": "",
+ "refId": "A",
+ "step": 1800
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "commited",
+ "metric": "",
+ "refId": "B",
+ "step": 1800
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "max",
+ "metric": "",
+ "refId": "C",
+ "step": 1800
+ }
+ ],
+ "title": "$jvm_memory_pool_nonheap",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 66
+ },
+ "id": 145,
+ "panels": [],
+ "title": "Garbage Collection",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 0,
+ "y": 67
+ },
+ "id": 98,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 1,
+ "legendFormat": "{{action}} ({{cause}})",
+ "refId": "A"
+ }
+ ],
+ "title": "Collections",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 8,
+ "y": 67
+ },
+ "id": 101,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "intervalFactor": 1,
+ "legendFormat": "avg {{action}} ({{cause}})",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_gc_pause_seconds_max{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "intervalFactor": 1,
+ "legendFormat": "max {{action}} ({{cause}})",
+ "refId": "B"
+ }
+ ],
+ "title": "Pause Durations",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "Bps"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 16,
+ "y": 67
+ },
+ "id": 99,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "rate(jvm_gc_memory_allocated_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 1,
+ "legendFormat": "allocated",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "rate(jvm_gc_memory_promoted_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 1,
+ "legendFormat": "promoted",
+ "refId": "B"
+ }
+ ],
+ "title": "Allocated/Promoted",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 74
+ },
+ "id": 146,
+ "panels": [],
+ "title": "Classloading",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 0,
+ "y": 75
+ },
+ "id": 37,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_classes_loaded_classes{application=\"$application\", instance=\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "loaded",
+ "metric": "",
+ "refId": "A",
+ "step": 1200
+ }
+ ],
+ "title": "Classes loaded",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 12,
+ "y": 75
+ },
+ "id": 38,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.4.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "delta(jvm_classes_loaded_classes{application=\"$application\",instance=\"$instance\"}[1m])",
+ "format": "time_series",
+ "hide": false,
+ "interval": "",
+ "intervalFactor": 1,
+ "legendFormat": "delta-1m",
+ "metric": "",
+ "refId": "A",
+ "step": 1200
+ }
+ ],
+ "title": "Class delta",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 82
+ },
+ "id": 147,
+ "panels": [],
+ "title": "Buffer Pools",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "fieldConfig": {
+ "defaults": {},
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "count"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "buffers"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "count"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "buffers"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "count"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "buffers"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "count"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "buffers"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "short"
+ },
+ {
+ "id": "decimals",
+ "value": 0
+ },
+ {
+ "id": "custom.axisPlacement",
+ "value": "right"
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 24,
+ "x": 0,
+ "y": 83
+ },
+ "id": 131,
+ "maxPerRow": 3,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "repeat": "jvm_buffer_pool",
+ "repeatDirection": "h",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "used",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "capacity",
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "expr": "jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "buffers",
+ "refId": "C"
+ }
+ ],
+ "title": "$jvm_buffer_pool",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 40,
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "includeAll": false,
+ "label": "Application",
+ "name": "application",
+ "options": [],
+ "query": "label_values(application)",
+ "refresh": 2,
+ "regex": "",
+ "type": "query"
+ },
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "includeAll": false,
+ "label": "Instance",
+ "name": "instance",
+ "options": [],
+ "query": "label_values(jvm_memory_used_bytes{application=\"$application\"}, instance)",
+ "refresh": 2,
+ "regex": "",
+ "type": "query"
+ },
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "hide": 2,
+ "includeAll": true,
+ "label": "JVM Memory Pools Heap",
+ "name": "jvm_memory_pool_heap",
+ "options": [],
+ "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)",
+ "refresh": 1,
+ "regex": "",
+ "sort": 1,
+ "type": "query"
+ },
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "hide": 2,
+ "includeAll": true,
+ "label": "JVM Memory Pools Non-Heap",
+ "name": "jvm_memory_pool_nonheap",
+ "options": [],
+ "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)",
+ "refresh": 1,
+ "regex": "",
+ "sort": 2,
+ "type": "query"
+ },
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${DS_PROMETHEUS}"
+ },
+ "hide": 2,
+ "includeAll": true,
+ "label": "JVM Buffer Pools",
+ "name": "jvm_buffer_pool",
+ "options": [],
+ "query": "label_values(jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\"},id)",
+ "refresh": 1,
+ "regex": "",
+ "sort": 1,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "browser",
+ "title": "JVM (Micrometer)",
+ "uid": "fe6atqkegecqoe",
+ "version": 4,
+ "weekStart": ""
+}
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml b/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml
new file mode 100755
index 00000000..e2131c28
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml
@@ -0,0 +1,149 @@
+
+services:
+ opensearch-node1:
+ image: opensearchproject/opensearch:2.18.0
+ container_name: opensearch-node1
+ environment:
+ - cluster.name=opensearch-cluster # Name the cluster
+ - node.name=opensearch-node1 # Name the node that will run in this container
+ - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
+ - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager
+ - bootstrap.memory_lock=true # Disable JVM heap memory swapping
+ - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
+ - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch
+ - "DISABLE_SECURITY_PLUGIN=true" # Disables Security plugin
+ ulimits:
+ memlock:
+ soft: -1 # Set memlock to unlimited (no soft or hard limit)
+ hard: -1
+ nofile:
+ soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
+ hard: 65536
+ volumes:
+ - opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
+ ports:
+ - 9200:9200 # REST API
+ - 9600:9600 # Performance Analyzer
+ networks:
+ - opensearch-net # All of the containers will join the same Docker bridge network
+ opensearch-node2:
+ image: opensearchproject/opensearch:2.18.0
+ container_name: opensearch-node2
+ environment:
+ - cluster.name=opensearch-cluster # Name the cluster
+ - node.name=opensearch-node2 # Name the node that will run in this container
+ - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
+ - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager
+ - bootstrap.memory_lock=true # Disable JVM heap memory swapping
+ - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
+ - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch
+ - "DISABLE_SECURITY_PLUGIN=true" # Disables Security plugin
+ ulimits:
+ memlock:
+ soft: -1 # Set memlock to unlimited (no soft or hard limit)
+ hard: -1
+ nofile:
+ soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
+ hard: 65536
+ volumes:
+ - opensearch-data2:/usr/share/opensearch/data # Creates volume called opensearch-data2 and mounts it to the container
+ networks:
+ - opensearch-net # All of the containers will join the same Docker bridge network
+ opensearch-dashboards:
+ image: opensearchproject/opensearch-dashboards:2.18.0
+ container_name: opensearch-dashboards
+ ports:
+ - 5601:5601 # Map host port 5601 to container port 5601
+ expose:
+ - "5601" # Expose port 5601 for web access to OpenSearch Dashboards
+ environment:
+ - 'OPENSEARCH_HOSTS=["http://opensearch-node1:9200","http://opensearch-node2:9200"]'
+ - "DISABLE_SECURITY_DASHBOARDS_PLUGIN=true" # disables security dashboards plugin in OpenSearch Dashboards
+ networks:
+ - opensearch-net
+
+#logstash
+
+ logstash:
+ image: opensearchproject/logstash-oss-with-opensearch-output-plugin:7.16.2
+ container_name: logstash
+ volumes:
+ - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
+ networks:
+ - opensearch-net
+
+ zookeeper:
+ image: confluentinc/cp-zookeeper:6.2.0
+ container_name: zookeeper
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+ networks:
+ - opensearch-net
+
+ broker:
+ image: confluentinc/cp-kafka:7.0.0
+ container_name: broker
+ ports:
+ - "9092:9092"
+ depends_on:
+ - zookeeper
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ networks:
+ - opensearch-net
+
+ kafka-ui:
+ container_name: kafka-ui
+ image: provectuslabs/kafka-ui:latest
+ network_mode: "host"
+ environment:
+ DYNAMIC_CONFIG_ENABLED: 'true'
+ KAFKA_CLUSTERS_0_NAME: local
+ KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: localhost:9092
+ deploy:
+ resources:
+ limits:
+ memory: "256M"
+
+ zipkin:
+ container_name: zipkin
+ image: openzipkin/zipkin:3
+ network_mode: "host"
+ expose:
+ - "9411"
+
+ prometheus:
+ image: bitnami/prometheus:3.0.1
+ container_name: prometheus
+ network_mode: "host"
+ expose:
+ - "9090"
+ volumes:
+ - type: bind
+ source: ./prometheus.yml
+ target: /etc/prometheus/prometheus.yml
+
+ grafana:
+ image: grafana/grafana-enterprise:11.4.0
+ container_name: grafana
+ network_mode: "host"
+ expose:
+ - "3000"
+ environment:
+ GF_SECURITY_ADMIN_USER: "grafana"
+ GF_SECURITY_ADMIN_PASSWORD: "grafana"
+
+
+volumes:
+ opensearch-data1:
+ opensearch-data2:
+
+networks:
+ opensearch-net:
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/elk/logstash.conf b/2025-11/spring-35-36-spring-cloud/elk/logstash.conf
new file mode 100644
index 00000000..35bb470e
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/elk/logstash.conf
@@ -0,0 +1,24 @@
+input {
+ http {
+ id => "test_http_plugin_id"
+ host => "127.0.0.1"
+ port => 8080
+ }
+
+ kafka {
+ bootstrap_servers => "broker:29092"
+ topics => "applLogs"
+ codec => "json"
+ }
+}
+
+output {
+ opensearch {
+ hosts => "http://opensearch-node1:9200"
+ user => "admin"
+ password => "admin"
+ index => "logstash-logs-%{+YYYY.MM.dd}"
+ ssl_certificate_verification => false
+ ssl => false
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml b/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml
new file mode 100755
index 00000000..efdaf62d
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml
@@ -0,0 +1,11 @@
+# JVM (Micrometer) Grafana dashboard: 4701
+global:
+ scrape_interval: 10s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: service-client
+ metrics_path: '/actuator/prometheus'
+ scrape_interval: 5s
+ static_configs:
+ - targets: ['localhost:8081']
diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts
new file mode 100755
index 00000000..f7b03f13
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts
@@ -0,0 +1,38 @@
+plugins {
+ id("com.google.cloud.tools.jib")
+}
+
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation ("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+}
+
+jib {
+ container {
+ creationTime.set("USE_CURRENT_TIMESTAMP")
+ }
+ from {
+ image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1"
+ }
+
+ to {
+ image = "localrun/eureka-server"
+ tags = setOf(project.version.toString())
+ }
+}
+
+tasks {
+ build {
+ dependsOn(spotlessApply)
+ dependsOn(jibBuildTar)
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh b/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh
new file mode 100755
index 00000000..5eedff0e
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+../gradlew :eureka-server:build
+
+docker load --input build/jib-image.tar
+
+docker stop eureka-server
+
+
+docker run --rm -d --name eureka-server \
+--memory=512m \
+--cpus 1 \
+--network="host" \
+-v $HOME/.ssh:/root/.ssh \
+-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \
+localrun/eureka-server:latest
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java
new file mode 100755
index 00000000..34063ca5
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java
@@ -0,0 +1,19 @@
+package ru.demo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
+
+@SpringBootApplication
+@EnableEurekaServer
+public class EurekaServer {
+ private static final Logger log = LoggerFactory.getLogger(EurekaServer.class);
+
+ public static void main(String[] args) {
+
+ SpringApplication.run(EurekaServer.class, args);
+ log.info("EurekaServer started");
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml
new file mode 100755
index 00000000..6d04a554
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml
@@ -0,0 +1,17 @@
+spring:
+ application:
+ name: eureka-server
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ initial-interval: 5000
+ max-attempts: 10
+ max-interval: 5000
+ multiplier: 1.2
+ config:
+ import: optional:configserver:http://localhost:8888
+ codec:
+ max-in-memory-size: 10MB
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml
new file mode 100755
index 00000000..01075dfc
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"eureka-server"}
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml b/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml
new file mode 100755
index 00000000..8a75eba0
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml
@@ -0,0 +1,16 @@
+server:
+ port: 7777
+
+application:
+ api-routes:
+ - id: "service-client"
+ from: "client"
+ to: "lb://SERVICE-CLIENT"
+ - id: "service-order"
+ from: "order"
+ to: "lb://SERVICE-ORDER"
+
+eureka:
+ client:
+ registerWithEureka: false
+ fetchRegistry: true
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/application.yml b/2025-11/spring-35-36-spring-cloud/git-config/application.yml
new file mode 100755
index 00000000..d9b9ee4e
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/application.yml
@@ -0,0 +1,36 @@
+management:
+ tracing:
+ sampling:
+ probability: 1.0
+ endpoint:
+ prometheus:
+ enabled: true
+ metrics:
+ enabled: true
+ health:
+ enabled: true
+ probes:
+ enabled: true
+ refresh:
+ enabled: true
+ endpoints:
+ web:
+ exposure:
+ include:
+ - prometheus
+ - health
+ - metrics
+ - refresh
+ enabled-by-default: false
+ metrics:
+ tags:
+ application: ${spring.application.name}
+
+eureka:
+ client:
+ serviceUrl:
+ defaultZone: http://localhost:9999/eureka/
+ instance:
+ lease-renewal-interval-in-seconds: 30
+ metadataMap:
+ zone: zone1
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml b/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml
new file mode 100755
index 00000000..fdc99a79
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml
@@ -0,0 +1,14 @@
+server:
+ port: 9999
+
+eureka:
+ server:
+ renewal-percent-threshold: 0.5
+ instance:
+ hostname: localhost
+ lease-expiration-duration-in-seconds: 90
+ client:
+ registerWithEureka: false
+ fetchRegistry: false
+ serviceUrl:
+ defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml
new file mode 100755
index 00000000..1e3605aa
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml
@@ -0,0 +1,6 @@
+server:
+ port: 8083
+
+eureka:
+ client:
+ fetchRegistry: false
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml
new file mode 100755
index 00000000..1b028c1d
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml
@@ -0,0 +1,6 @@
+server:
+ port: 8081
+
+eureka:
+ client:
+ fetchRegistry: true
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml
new file mode 100755
index 00000000..3b80dc61
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml
@@ -0,0 +1,8 @@
+server:
+ port: 8082
+
+eureka:
+ client:
+ fetchRegistry: false
+ instance:
+ instance-id: service-order:${spring.application.instance_id:0}
\ No newline at end of file
diff --git a/2025-11/spring-35-36-spring-cloud/gradle.properties b/2025-11/spring-35-36-spring-cloud/gradle.properties
new file mode 100755
index 00000000..157d79a8
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/gradle.properties
@@ -0,0 +1,15 @@
+# -------Gradle--------
+org.gradle.jvmargs=-Xmx4g
+org.gradle.daemon=true
+org.gradle.parallel=true
+# -------Plugins---------
+jgitver=0.10.0-rc03
+dependencyManagement=1.1.5
+springframeworkBoot=3.3.1
+springCloudVersion=2023.0.3
+
+sonarlint=4.2.4
+spotless=6.25.0
+jib=3.4.5
+# -------Versions--------
+logbackEncoder=8.0
diff --git a/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 00000000..d64cd491
Binary files /dev/null and b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 00000000..df97d72b
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/2025-11/spring-35-36-spring-cloud/gradlew b/2025-11/spring-35-36-spring-cloud/gradlew
new file mode 100755
index 00000000..1aa94a42
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# 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"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/2025-11/spring-35-36-spring-cloud/gradlew.bat b/2025-11/spring-35-36-spring-cloud/gradlew.bat
new file mode 100755
index 00000000..93e3f59f
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/gradlew.bat
@@ -0,0 +1,92 @@
+@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=.
+@rem This is normally unused
+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% equ 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% equ 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!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts
new file mode 100755
index 00000000..93ab7ab5
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts
@@ -0,0 +1,10 @@
+dependencies {
+ implementation("ch.qos.logback:logback-classic")
+ implementation ("org.apache.kafka:kafka-clients")
+}
+
+tasks {
+ bootJar {
+ enabled = false
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java
new file mode 100755
index 00000000..d57ddd0f
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java
@@ -0,0 +1,8 @@
+package ru.appender.kafka;
+
+public class AppenderException extends RuntimeException {
+
+ public AppenderException(String message) {
+ super(message);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java
new file mode 100644
index 00000000..f7dbb53e
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java
@@ -0,0 +1,5 @@
+package ru.appender.kafka;
+
+import java.util.function.Consumer;
+
+public interface ErrorMsgConsumer extends Consumer {}
diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java
new file mode 100755
index 00000000..a0e30079
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java
@@ -0,0 +1,84 @@
+package ru.appender.kafka;
+
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.UnsynchronizedAppenderBase;
+import ch.qos.logback.core.encoder.Encoder;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+
+public class LogAppender extends UnsynchronizedAppenderBase {
+ private static final String MESSAGE_TEMPLATE = "[Kafka appender] %s";
+
+ private String bootstrapServers;
+ private String topicName;
+
+ private final Queue eventsQueue = new ArrayBlockingQueue<>(1000);
+
+ private Thread senderThread;
+ private Encoder encoder;
+
+ private final ErrorMsgConsumer errorMsgConsumer = error -> addError(String.format(MESSAGE_TEMPLATE, error));
+
+ public void setEncoder(Encoder encoder) {
+ this.encoder = encoder;
+ }
+
+ public void setBootstrapServers(String bootstrapServers) {
+ this.bootstrapServers = bootstrapServers;
+ addInfo(String.format(MESSAGE_TEMPLATE, "set bootstrapServers:" + bootstrapServers));
+ }
+
+ public void setTopicName(String topicName) {
+ this.topicName = topicName;
+ addInfo(String.format(MESSAGE_TEMPLATE, "set topicName:" + topicName));
+ }
+
+ @Override
+ public void start() {
+ if (bootstrapServers == null) {
+ addError(String.format(MESSAGE_TEMPLATE, "bootstrapServers is null"));
+ return;
+ }
+
+ if (topicName == null) {
+ addError(String.format(MESSAGE_TEMPLATE, "topicName is null"));
+ return;
+ }
+ var logProducer = new LogProducer(bootstrapServers, topicName);
+
+ senderThread = Thread.ofVirtual().name("senderThread").start(() -> sendMessages(logProducer));
+ super.start();
+ addInfo(String.format(MESSAGE_TEMPLATE, "started"));
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ if (senderThread != null) {
+ senderThread.interrupt();
+ }
+ addInfo(String.format(MESSAGE_TEMPLATE, "stopped"));
+ }
+
+ @Override
+ public void append(LoggingEvent eventObject) {
+ var result = eventsQueue.offer(eventObject);
+ if (!result) {
+ addWarn(String.format(MESSAGE_TEMPLATE, "eventsQueue is full"));
+ }
+ }
+
+ private void sendMessages(LogProducer logProducer) {
+ while (!Thread.currentThread().isInterrupted()) {
+ var event = eventsQueue.poll();
+ if (event != null) {
+ try {
+ var messageAsText = new String(encoder.encode(event));
+ logProducer.send(messageAsText, errorMsgConsumer);
+ } catch (Exception ex) {
+ addError(String.format(MESSAGE_TEMPLATE, ex.getMessage()));
+ }
+ }
+ }
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java
new file mode 100755
index 00000000..72c03095
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java
@@ -0,0 +1,56 @@
+package ru.appender.kafka;
+
+import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.RETRIES_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.BUFFER_MEMORY_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.MAX_BLOCK_MS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG;
+
+import java.util.Properties;
+import java.util.function.Consumer;
+import org.apache.kafka.clients.producer.KafkaProducer;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.common.serialization.StringSerializer;
+
+public class LogProducer {
+ private final KafkaProducer kafkaProducer;
+ private final String topicName;
+ private long lastSendKey = System.currentTimeMillis();
+
+ public LogProducer(String bootstrapServers, String topicName) {
+ this.topicName = topicName;
+ Properties props = new Properties();
+ props.put(CLIENT_ID_CONFIG, "myKafkaProducer");
+ props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ACKS_CONFIG, "1");
+ props.put(RETRIES_CONFIG, 1);
+ props.put(BATCH_SIZE_CONFIG, 16384);
+ props.put(LINGER_MS_CONFIG, 10);
+ props.put(BUFFER_MEMORY_CONFIG, 33_554_432); // bytes
+ props.put(MAX_BLOCK_MS_CONFIG, 1_000); // ms
+ props.put(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ props.put(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+
+ kafkaProducer = new KafkaProducer<>(props);
+
+ Runtime.getRuntime().addShutdownHook(new Thread(this::close));
+ }
+
+ public void send(String value, Consumer errorCallback) {
+ var key = lastSendKey++;
+ kafkaProducer.send(new ProducerRecord<>(topicName, String.valueOf(key), value), (metadata, exception) -> {
+ if (exception != null) {
+ errorCallback.accept(String.format(exception.getMessage()));
+ }
+ });
+ }
+
+ public void close() {
+ kafkaProducer.close();
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts
new file mode 100755
index 00000000..d931e6d2
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts
@@ -0,0 +1,14 @@
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+
+ implementation ("org.springframework.boot:spring-boot-starter-web")
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java
new file mode 100755
index 00000000..c5ce2a91
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java
@@ -0,0 +1,11 @@
+package ru.demo;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class ServiceClientInfo {
+ public static void main(String[] args) {
+ new SpringApplicationBuilder().sources(ServiceClientInfo.class).run(args);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java
new file mode 100755
index 00000000..be04c7bd
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java
@@ -0,0 +1,19 @@
+package ru.demo.config;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.OncePerRequestFilter;
+import ru.demo.filter.MdcFilter;
+
+@Configuration
+public class ApplConf {
+
+ @Bean
+ public FilterRegistrationBean mdcFilterRegistrationBean() {
+ var registrationBean = new FilterRegistrationBean();
+ registrationBean.setFilter(new MdcFilter());
+ registrationBean.setOrder(1);
+ return registrationBean;
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java
new file mode 100755
index 00000000..273a35f1
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java
@@ -0,0 +1,30 @@
+package ru.demo.controller;
+
+import java.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import ru.demo.model.ClientData;
+
+@RestController
+public class ClientInfoController {
+ private static final Logger logger = LoggerFactory.getLogger(ClientInfoController.class);
+
+ // curl -v http://localhost:8083/additional-info?name="testClient"
+
+ // docker run -d --rm -p 9411:9411 openzipkin/zipkin
+ // http://localhost:9411
+
+ @GetMapping(value = "/additional-info")
+ public ClientData info(@RequestParam(name = "name") String name) throws InterruptedException {
+ logger.info("request. name:{}", name);
+ doJob();
+ return new ClientData(String.format("additional ClientInfo name:%s", name));
+ }
+
+ private void doJob() throws InterruptedException {
+ Thread.sleep(Duration.ofMillis(100));
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java
new file mode 100755
index 00000000..5b134b08
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java
@@ -0,0 +1,39 @@
+package ru.demo.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+public class MdcFilter extends OncePerRequestFilter {
+ private final Logger log = LoggerFactory.getLogger(MdcFilter.class);
+ private static final String HEADER_X_REQUEST_ID = "X-Request-Id";
+ private static final String MDC_REQUEST_ID = "requestId";
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ var xRequestId = request.getHeader(HEADER_X_REQUEST_ID);
+ log.info("method:{}, xRequestId:{}", request.getMethod(), xRequestId);
+ if (xRequestId != null) {
+ MDC.put(MDC_REQUEST_ID, xRequestId);
+ }
+
+ var headerIterator = request.getHeaderNames().asIterator();
+ var headers = new ArrayList();
+ while (headerIterator.hasNext()) {
+ headers.add(headerIterator.next());
+ }
+ log.info("request headers:{}", headers);
+
+ response.addHeader(HEADER_X_REQUEST_ID, xRequestId);
+ filterChain.doFilter(request, response);
+ MDC.remove(MDC_REQUEST_ID);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java
new file mode 100755
index 00000000..75a423ed
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java
@@ -0,0 +1,3 @@
+package ru.demo.model;
+
+public record ClientData(String data) {}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml
new file mode 100755
index 00000000..ca36cd5c
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml
@@ -0,0 +1,16 @@
+spring:
+ application:
+ name: service-client-info
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ initial-interval: 5000
+ max-attempts: 10
+ max-interval: 5000
+ multiplier: 1.2
+ config:
+ import: optional:configserver:http://localhost:8888
+ codec:
+ max-in-memory-size: 10MB
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml
new file mode 100755
index 00000000..5507f6db
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"service-client-info"}
+ requestId
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts
new file mode 100755
index 00000000..3b63f3e7
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts
@@ -0,0 +1,19 @@
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation("org.springframework.boot:spring-boot-starter-web")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
+ implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+ implementation("io.github.openfeign:feign-micrometer")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java
new file mode 100755
index 00000000..98e9c7a0
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java
@@ -0,0 +1,12 @@
+package ru.demo;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class ServiceClient {
+ public static void main(String[] args) {
+
+ new SpringApplicationBuilder().sources(ServiceClient.class).run(args);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java
new file mode 100755
index 00000000..58e6e120
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java
@@ -0,0 +1,23 @@
+package ru.demo.config;
+
+import feign.RequestTemplate;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import java.lang.reflect.Type;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RequestEncoder implements Encoder {
+ private static final Logger log = LoggerFactory.getLogger(RequestEncoder.class);
+ private final Encoder defaultEncoder;
+
+ public RequestEncoder(Encoder defaultEncoder) {
+ this.defaultEncoder = defaultEncoder;
+ }
+
+ @Override
+ public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
+ log.info("encode value:{}", object);
+ defaultEncoder.encode(object, bodyType, template);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java
new file mode 100755
index 00000000..1ae51572
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java
@@ -0,0 +1,30 @@
+package ru.demo.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import feign.FeignException;
+import feign.Response;
+import feign.codec.Decoder;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.demo.model.ClientData;
+
+public class ResponseDecoder implements Decoder {
+ private static final Logger log = LoggerFactory.getLogger(ResponseDecoder.class);
+
+ private final Decoder defaultDecoder;
+ private final ObjectMapper mapper;
+
+ public ResponseDecoder(Decoder defaultDecoder, ObjectMapper mapper) {
+ this.defaultDecoder = defaultDecoder;
+ this.mapper = mapper;
+ }
+
+ @Override
+ public ClientData decode(Response response, Type type) throws IOException, FeignException {
+ var responseAsString = (String) defaultDecoder.decode(response, String.class);
+ log.info("response:{}", responseAsString);
+ return mapper.readValue(responseAsString, ClientData.class);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java
new file mode 100755
index 00000000..4078e0a2
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java
@@ -0,0 +1,106 @@
+package ru.demo.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import feign.Contract;
+import feign.Feign;
+import feign.Logger;
+import feign.Retryer;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.micrometer.MicrometerCapability;
+import feign.micrometer.MicrometerObservationCapability;
+import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
+import io.github.resilience4j.ratelimiter.RateLimiter;
+import io.github.resilience4j.ratelimiter.RateLimiterConfig;
+import io.github.resilience4j.timelimiter.TimeLimiterConfig;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.observation.ObservationRegistry;
+import java.time.Duration;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
+import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
+import org.springframework.cloud.client.circuitbreaker.Customizer;
+import org.springframework.cloud.openfeign.FeignClientsConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.filter.OncePerRequestFilter;
+import ru.demo.controller.ClientAdditionalInfoClient;
+import ru.demo.filter.MdcFilter;
+import ru.demo.metrics.MetricsManager;
+import ru.demo.metrics.MicrometerMetricsManager;
+
+@Configuration
+@Import(FeignClientsConfiguration.class)
+public class ServiceClientApplConf {
+
+ @Bean
+ public RateLimiterConfig rateLimiterConfig() {
+ return RateLimiterConfig.custom()
+ .timeoutDuration(Duration.ofMillis(100))
+ .limitRefreshPeriod(Duration.ofSeconds(1))
+ .limitForPeriod(1000)
+ .build();
+ }
+
+ @Bean
+ public RateLimiter rateLimiter(RateLimiterConfig config) {
+ return RateLimiter.of("defaultRateLimiter", config);
+ }
+
+ @Bean
+ public Customizer defaultCustomizer() {
+ return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
+ .timeLimiterConfig(TimeLimiterConfig.custom()
+ .timeoutDuration(Duration.ofSeconds(5))
+ .build())
+ .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
+ .build());
+ }
+
+ @Bean
+ public CircuitBreaker circuitBreaker(CircuitBreakerFactory, ?> circuitBreakerFactory) {
+ return circuitBreakerFactory.create("defaultCircuitBreaker");
+ }
+
+ @Bean
+ public FilterRegistrationBean mdcFilterRegistrationBean() {
+ var registrationBean = new FilterRegistrationBean();
+ registrationBean.setFilter(new MdcFilter());
+ registrationBean.setOrder(1);
+ return registrationBean;
+ }
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return JsonMapper.builder().build();
+ }
+
+ @Bean
+ public ClientAdditionalInfoClient clientAdditionalInfoClient(
+ Decoder decoder,
+ Encoder encoder,
+ Contract contract,
+ ObjectMapper mapper,
+ MeterRegistry meterRegistry,
+ ObservationRegistry observationRegistry) {
+
+ return Feign.builder()
+ .encoder(new RequestEncoder(encoder))
+ .decoder(new ResponseDecoder(decoder, mapper))
+ .contract(contract)
+ .logLevel(Logger.Level.FULL)
+ .addCapability(new MicrometerObservationCapability(observationRegistry)) // <-- THIS IS NEW
+ .addCapability(new MicrometerCapability(meterRegistry)) // <-- THIS IS NEW
+ .retryer(new Retryer.Default(500, 5_000, 10))
+ .target(ClientAdditionalInfoClient.class, "http");
+ }
+
+ @Bean
+ public MetricsManager micrometerMetricsManager(MeterRegistry meterRegistry) {
+ return new MicrometerMetricsManager(meterRegistry);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java
new file mode 100755
index 00000000..e9b1470a
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java
@@ -0,0 +1,16 @@
+package ru.demo.controller;
+
+import static ru.demo.filter.MdcFilter.HEADER_X_REQUEST_ID;
+
+import java.net.URI;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestParam;
+import ru.demo.model.ClientData;
+
+public interface ClientAdditionalInfoClient {
+
+ @GetMapping(value = "/additional-info", consumes = "application/json")
+ ClientData additionalInfo(
+ @RequestHeader(HEADER_X_REQUEST_ID) String xRequestId, URI baseUri, @RequestParam("name") String nameVal);
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java
new file mode 100755
index 00000000..e3d996ca
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java
@@ -0,0 +1,90 @@
+package ru.demo.controller;
+
+import static ru.demo.filter.MdcFilter.MDC_REQUEST_ID;
+import static ru.demo.metrics.Meter.REQUEST_COUNTER;
+
+import com.netflix.discovery.EurekaClient;
+import io.github.resilience4j.core.functions.CheckedFunction;
+import io.github.resilience4j.ratelimiter.RateLimiter;
+import java.net.URI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import ru.demo.metrics.Meter;
+import ru.demo.metrics.MetricsManager;
+import ru.demo.model.RequestForData;
+
+@RestController
+public class ClientController {
+ private static final Logger log = LoggerFactory.getLogger(ClientController.class);
+
+ private final MetricsManager metricsManager;
+ private final ClientAdditionalInfoClient clientAdditionalInfoClient;
+ private final EurekaClient discoveryClient;
+ private final CheckedFunction getAdditionalInfoFunction;
+
+ // curl -v -H "X-Request-Id: 123" http://localhost:8081/info?name="testClient"
+
+ public ClientController(
+ MetricsManager metricsManager,
+ ClientAdditionalInfoClient clientAdditionalInfoClient,
+ EurekaClient discoveryClient,
+ CircuitBreaker circuitBreaker,
+ RateLimiter rateLimiter) {
+ this.metricsManager = metricsManager;
+ this.clientAdditionalInfoClient = clientAdditionalInfoClient;
+ this.discoveryClient = discoveryClient;
+
+ this.getAdditionalInfoFunction = RateLimiter.decorateCheckedFunction(
+ rateLimiter,
+ requestForData -> circuitBreaker.run(() -> doRequest(requestForData), t -> {
+ log.error("delay call failed error:{}", t.getMessage());
+ return "unknown info";
+ }));
+ }
+
+ @GetMapping(value = "/info")
+ public String info(@RequestParam(name = "name") String name) {
+ var startTime = System.currentTimeMillis();
+ metricsManager.incrementValue(REQUEST_COUNTER);
+ log.info("request. name:{}", name);
+ String additionalInfo = null;
+ try {
+ additionalInfo = getAdditionalInfoFunction.apply(new RequestForData(name, MDC.get(MDC_REQUEST_ID)));
+ } catch (Throwable ex) {
+ log.error("can't execute additional info, name:{}, error:{}", name, ex.getMessage());
+ }
+ var requestResult = String.format("ClientInfo name:%s, additional:%s", name, additionalInfo);
+
+ var duration = System.currentTimeMillis() - startTime;
+ metricsManager.putValue(Meter.REQUEST_DURATION, duration);
+ return requestResult;
+ }
+
+ private String doRequest(RequestForData requestForData) {
+ try {
+ MDC.put(MDC_REQUEST_ID, requestForData.requestId());
+ return getAdditionalInfo(requestForData.name());
+ } finally {
+ MDC.remove(MDC_REQUEST_ID);
+ }
+ }
+
+ private String getAdditionalInfo(String name) {
+ try {
+ var clientInfo = discoveryClient.getNextServerFromEureka("SERVICE-CLIENT-INFO", false);
+ log.info("clientInfo from Eureka:{}", clientInfo);
+ var additionalInfo = clientAdditionalInfoClient.additionalInfo(
+ MDC.get(MDC_REQUEST_ID), new URI(clientInfo.getHomePageUrl()), name);
+ log.info("additionalInfo:{}", additionalInfo);
+ return additionalInfo.data();
+ } catch (Exception ex) {
+ log.error("can't get additional info, name:{}, error:{}", name, ex.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java
new file mode 100755
index 00000000..ebd5fcff
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java
@@ -0,0 +1,38 @@
+package ru.demo.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+public class MdcFilter extends OncePerRequestFilter {
+ public static final String HEADER_X_REQUEST_ID = "X-Request-Id";
+ public static final String MDC_REQUEST_ID = "requestId";
+ private final Logger log = LoggerFactory.getLogger(MdcFilter.class);
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ var xRequestId = request.getHeader(HEADER_X_REQUEST_ID);
+ log.debug("xRequestId:{}", xRequestId);
+ if (xRequestId != null) {
+ MDC.put(MDC_REQUEST_ID, xRequestId);
+ }
+ var headerIterator = request.getHeaderNames().asIterator();
+ var headers = new ArrayList();
+ while (headerIterator.hasNext()) {
+ headers.add(headerIterator.next());
+ }
+ log.debug("request headers:{}", headers);
+ response.addHeader(HEADER_X_REQUEST_ID, xRequestId);
+ filterChain.doFilter(request, response);
+ MDC.remove(MDC_REQUEST_ID);
+ log.debug("response headers:{}", response.getHeaderNames());
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java
new file mode 100644
index 00000000..57a79577
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java
@@ -0,0 +1,16 @@
+package ru.demo.metrics;
+
+public enum Meter {
+ REQUEST_COUNTER("request_counter"),
+ REQUEST_DURATION("request_duration");
+
+ private final String meterName;
+
+ Meter(String meterName) {
+ this.meterName = meterName;
+ }
+
+ public String getMeterName() {
+ return meterName;
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java
new file mode 100644
index 00000000..67365084
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java
@@ -0,0 +1,11 @@
+package ru.demo.metrics;
+
+import java.util.function.Supplier;
+
+public interface MetricsManager {
+ void putValue(Meter meterName, long value);
+
+ void incrementValue(Meter meterName);
+
+ void registerGauge(Meter gaugeName, Supplier gaugeGetter);
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java
new file mode 100644
index 00000000..e15242f5
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java
@@ -0,0 +1,52 @@
+package ru.demo.metrics;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+
+public class MicrometerMetricsManager implements MetricsManager {
+ private final MeterRegistry meterRegistry;
+ private final Map gauges = new ConcurrentHashMap<>();
+ private final Map counters = new ConcurrentHashMap<>();
+
+ public MicrometerMetricsManager(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
+ }
+
+ @Override
+ public void putValue(Meter meterName, long value) {
+ var gaugeName = makeMeterName(meterName);
+ var gauge = gauges.computeIfAbsent(gaugeName, key -> {
+ var newGauge = new AtomicLong();
+ registerGauge(meterName, newGauge::get);
+ return newGauge;
+ });
+ gauge.set(value);
+ }
+
+ @Override
+ public void registerGauge(Meter gaugeName, Supplier gaugeGetter) {
+ var builder = Gauge.builder(gaugeName.getMeterName(), gaugeGetter);
+ builder.register(meterRegistry);
+ }
+
+ @Override
+ public void incrementValue(Meter meterName) {
+ var counterName = makeMeterName(meterName);
+ var counter = counters.computeIfAbsent(counterName, key -> makeCounter(meterName));
+ counter.increment();
+ }
+
+ private Counter makeCounter(Meter meterName) {
+ var builder = Counter.builder(meterName.getMeterName());
+ return builder.register(meterRegistry);
+ }
+
+ private String makeMeterName(Meter meterName) {
+ return String.format("%s--", meterName.getMeterName());
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java
new file mode 100755
index 00000000..75a423ed
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java
@@ -0,0 +1,3 @@
+package ru.demo.model;
+
+public record ClientData(String data) {}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java
new file mode 100644
index 00000000..68ba5ff2
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java
@@ -0,0 +1,3 @@
+package ru.demo.model;
+
+public record RequestForData(String name, String requestId) {}
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml
new file mode 100755
index 00000000..c40d2f5c
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ application:
+ name: service-client
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ initial-interval: 5000
+ max-attempts: 10
+ max-interval: 5000
+ multiplier: 1.2
+ config:
+ import: optional:configserver:http://localhost:8888
+ codec:
+ max-in-memory-size: 10MB
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml
new file mode 100755
index 00000000..d20f5eca
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"service-client"}
+ requestId
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts
new file mode 100755
index 00000000..f3da6edd
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts
@@ -0,0 +1,39 @@
+plugins {
+ id("com.google.cloud.tools.jib")
+}
+
+dependencies {
+ implementation(project(":kafka-log-appender"))
+ implementation("net.logstash.logback:logstash-logback-encoder")
+
+ implementation ("org.springframework.boot:spring-boot-starter-web")
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus")
+
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
+
+ implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry.
+ implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin.
+}
+
+jib {
+ container {
+ creationTime.set("USE_CURRENT_TIMESTAMP")
+ }
+ from {
+ image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1"
+ }
+
+ to {
+ image = "localrun/service-order"
+ tags = setOf(project.version.toString())
+ }
+}
+
+tasks {
+ build {
+ dependsOn(spotlessApply)
+ dependsOn(jibBuildTar)
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh b/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh
new file mode 100755
index 00000000..b6f76415
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+../gradlew :service-order:build
+
+docker load --input build/jib-image.tar
+
+docker stop service-order-1
+docker stop service-order-2
+
+docker run --rm -d --name service-order-1 \
+--memory=256m \
+--cpus 1 \
+--network="host" \
+-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \
+-e SPRING_APPLICATION_INSTANCE_ID="i1" \
+-e SERVER_PORT=8091 \
+localrun/service-order:latest
+
+docker run --rm -d --name service-order-2 \
+--memory=256m \
+--cpus 1 \
+--network="host" \
+-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \
+-e SPRING_APPLICATION_INSTANCE_ID="i2" \
+-e SERVER_PORT=8092 \
+localrun/service-order:latest
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java
new file mode 100755
index 00000000..ac7f5a38
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java
@@ -0,0 +1,15 @@
+package ru.demo;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import ru.demo.controller.InstanceId;
+
+@Configuration
+public class Config {
+
+ @Bean
+ InstanceId instanceId(@Value("${spring.application.instance_id}") String id) {
+ return new InstanceId(id);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java
new file mode 100755
index 00000000..106c40b3
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java
@@ -0,0 +1,11 @@
+package ru.demo;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class ServiceOrder {
+ public static void main(String[] args) {
+ new SpringApplicationBuilder().sources(ServiceOrder.class).run(args);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java
new file mode 100755
index 00000000..73d8c740
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java
@@ -0,0 +1,3 @@
+package ru.demo.controller;
+
+public record InstanceId(String name) {}
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java
new file mode 100755
index 00000000..33307da2
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java
@@ -0,0 +1,28 @@
+package ru.demo.controller;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class OrderController {
+ private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
+ private final InstanceId instanceId;
+
+ public OrderController(InstanceId instanceId) {
+ this.instanceId = instanceId;
+ }
+
+ // curl -v http://localhost:8082/info?id="idClient"
+
+ // curl -v http://localhost:8091/info?id="idClient"
+ // curl -v http://localhost:8092/info?id="idClient"
+
+ @GetMapping(value = "/info")
+ public String info(@RequestParam(name = "id") String id) {
+ logger.info("instanceId:{}, request. id:{}", instanceId.name(), id);
+ return String.format("%s, Order id:%s", instanceId.name(), id);
+ }
+}
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml
new file mode 100755
index 00000000..472355c8
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ application:
+ name: service-order
+ instance_id: i0
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ initial-interval: 5000
+ max-attempts: 10
+ max-interval: 5000
+ multiplier: 1.2
+ config:
+ import: optional:configserver:http://localhost:8888
+ codec:
+ max-in-memory-size: 10MB
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml
new file mode 100755
index 00000000..c74d4279
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n
+
+
+
+
+ localhost:9092
+ applLogs
+
+
+ [ignore]
+
+ {"appname":"service-order"}
+ requestId
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-35-36-spring-cloud/settings.gradle.kts b/2025-11/spring-35-36-spring-cloud/settings.gradle.kts
new file mode 100755
index 00000000..4934408f
--- /dev/null
+++ b/2025-11/spring-35-36-spring-cloud/settings.gradle.kts
@@ -0,0 +1,27 @@
+pluginManagement {
+ val jgitver: String by settings
+ val dependencyManagement: String by settings
+ val springframeworkBoot: String by settings
+ val sonarlint: String by settings
+ val spotless: String by settings
+ val jib: String by settings
+
+ plugins {
+ id("fr.brouillard.oss.gradle.jgitver") version jgitver
+ id("io.spring.dependency-management") version dependencyManagement
+ id("org.springframework.boot") version springframeworkBoot
+ id("name.remal.sonarlint") version sonarlint
+ id("com.diffplug.spotless") version spotless
+ id("com.google.cloud.tools.jib") version jib
+ }
+}
+rootProject.name = "spring-cloud"
+
+include("api-gateway")
+include("config-server")
+include("eureka-server")
+include("service-client")
+include("service-client-info")
+include("service-order")
+
+include("kafka-log-appender")
diff --git a/2025-11/spring-37-rabbit/README.md b/2025-11/spring-37-rabbit/README.md
new file mode 100644
index 00000000..9ce1271d
--- /dev/null
+++ b/2025-11/spring-37-rabbit/README.md
@@ -0,0 +1,41 @@
+## Пример взаимодействия приложений через RabbitMQ
+
+В примере демонстрируется:
+
+* *использование `@Scheduled` таймера для имитации пользовательской активности*
+* *оправка и прием сообщений между двумя приложениями через RabbitMQ*
+* *создание и отправка email сообщений средствами SpringMail*
+* *аггрегация данных с помощью SpringData/JPQL*
+* *создание кастомного endpoint-а actuator-а для вывода статистики*
+
+Описание примера:
+
+user-activity-emitter-microservice:
+
+* *по таймеру `UserActivityEmitterService` достает из БД случайный тип активности и пользователя*
+* *после чего формирует из них объект активности (`UserActivity`) и отправляет в очередь сообщений RabbitMQ с
+ помощью `RabbitTemplate`
+ настроенный на работу с "main-exchange"*
+* *большее число активностей имеют `routingKey` = "user.activity.message.simple" *
+* *активности, у которых в названии типа есть вхождение "Вредн" имееют свой `routingKey` ("
+ user.activity.message.important") *
+* *так же приложение по таймеру, с помощью `ActivityStatCalculationEmitterSerivce` инициирует подсчет статистики с
+ помощью отправки
+ сообщения в очередь RabbitMQ c `routingKey` = "user.activity.stat"*
+
+user-activity-processor-microservice:
+
+* *в приложении для обработки сообщений есть несколько очередей, куда попадают сообщения в зависимости от `routingKey`*
+* *их прослушивает компонент `RabbitMqListener`, который содержит по методу на каждую очередь"*
+* *в "all-activity-queue" попадают все активности. На выходе из данной очереди активности сохраняются в БД*
+* *в "important-activity-queue" попадают важные активности. На выходе из данной очереди активности преобразуются в
+ письма и отправляются на
+ почту администратору*
+* *в "stat-calc-commands-queue" попадают команды, которые инициируют расчет статистики. *
+* *в методе-обработчике сообщений данной очереди происходит удаление старых статистических данных, а так же подсчет и
+ сохранение в БД новых*
+* *за вывод статистических данных отвечает кастомный endpoint actuator-а `ActivityStatEndpoint`*
+
+Для работы приложений требуется работающий RabbitMQ. За его запуск отвечает docker-compose.yml
+Адрес консоли RabbitMQ: http://localhost:15672/
+Логин и пароль: guest
\ No newline at end of file
diff --git a/2025-11/spring-37-rabbit/docker-compose.yml b/2025-11/spring-37-rabbit/docker-compose.yml
new file mode 100644
index 00000000..2bdf971a
--- /dev/null
+++ b/2025-11/spring-37-rabbit/docker-compose.yml
@@ -0,0 +1,7 @@
+version: '3'
+services:
+ rabbitmq:
+ image: rabbitmq:management
+ ports:
+ - "5672:5672"
+ - "15672:15672"
\ No newline at end of file
diff --git a/2025-11/spring-37-rabbit/pom.xml b/2025-11/spring-37-rabbit/pom.xml
new file mode 100644
index 00000000..55178e58
--- /dev/null
+++ b/2025-11/spring-37-rabbit/pom.xml
@@ -0,0 +1,38 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.2
+
+
+
+ ru.otus.example
+ spring-mail-rabbitmq-demo
+ 1.0
+
+ pom
+
+
+ user-activity-models
+ user-activity-emitter-microservice
+ user-activity-processor-microservice
+
+
+
+ 17
+ 17
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore
new file mode 100644
index 00000000..a2a3040a
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore
@@ -0,0 +1,31 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml
new file mode 100644
index 00000000..98ca0cdf
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+
+ ru.otus.example
+ spring-mail-rabbitmq-demo
+ 1.0
+
+
+ user-activity-emitter-microservice
+ 0.0.1-SNAPSHOT
+ user-activity-emitter-microservice
+ User activity emitter microservice
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+
+ ru.otus.example
+ user-activity-models
+ 0.0.1-SNAPSHOT
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java
new file mode 100644
index 00000000..fc68506f
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java
@@ -0,0 +1,17 @@
+package ru.otus.example.rabbitmq;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@EnableScheduling
+@EntityScan("ru.otus.example.useractivitymodels")
+@SpringBootApplication
+public class EmitterMicroServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(EmitterMicroServiceApplication.class, args);
+ }
+
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java
new file mode 100644
index 00000000..e6c6124c
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java
@@ -0,0 +1,34 @@
+package ru.otus.example.rabbitmq.rabbitmq;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.connection.ConnectionFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RabbitMqConfig {
+
+ private static final String MAIN_EXCHANGE_NAME = "main-exchange";
+
+ @Bean
+ public Jackson2JsonMessageConverter jsonConverter(ObjectMapper objectMapper) {
+ return new Jackson2JsonMessageConverter(objectMapper);
+ }
+
+ @Bean
+ public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
+ Jackson2JsonMessageConverter jsonConverter) {
+ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
+ rabbitTemplate.setExchange(MAIN_EXCHANGE_NAME);
+ rabbitTemplate.setMessageConverter(jsonConverter);
+ return rabbitTemplate;
+ }
+
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange(MAIN_EXCHANGE_NAME);
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java
new file mode 100644
index 00000000..ec1a77a6
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java
@@ -0,0 +1,9 @@
+package ru.otus.example.rabbitmq.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+@Transactional
+public interface ActivityRepository extends JpaRepository {
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java
new file mode 100644
index 00000000..b1ab8572
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java
@@ -0,0 +1,7 @@
+package ru.otus.example.rabbitmq.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.otus.example.useractivitymodels.ActivityType;
+
+public interface ActivityTypeRepository extends JpaRepository {
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java
new file mode 100644
index 00000000..cb45abc1
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java
@@ -0,0 +1,9 @@
+package ru.otus.example.rabbitmq.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.useractivitymodels.AppUser;
+
+@Transactional
+public interface AppUserRepository extends JpaRepository {
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java
new file mode 100644
index 00000000..0de464c1
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java
@@ -0,0 +1,24 @@
+package ru.otus.example.rabbitmq.services;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class ActivityStatCalculationEmitterSerivce {
+ private static final String USER_ACTIVITY_STAT_ROUTING_KEY = "user.activity.stat";
+ private static final String CALC_STAT_COMMAND = "{\"command\": \"calc stat now\"}";
+
+ private final RabbitTemplate rabbitTemplate;
+
+ @Scheduled(initialDelay = 3000, fixedRate = 10000)
+ public void emitAppUserActivityStatCalculation() {
+// log.warn("Stat send!!!");
+ rabbitTemplate.convertAndSend(USER_ACTIVITY_STAT_ROUTING_KEY, CALC_STAT_COMMAND);
+ log.warn("Stat calculated!!!");
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java
new file mode 100644
index 00000000..8e6b6986
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java
@@ -0,0 +1,40 @@
+package ru.otus.example.rabbitmq.services;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import ru.otus.example.rabbitmq.repositories.ActivityTypeRepository;
+import ru.otus.example.rabbitmq.repositories.AppUserRepository;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+import java.util.Random;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class UserActivityEmitterService {
+ private final ActivityTypeRepository activityTypeRepository;
+ private final AppUserRepository appUserRepository;
+ private final RabbitTemplate rabbitTemplate;
+
+ @SuppressWarnings("unused")
+ @Scheduled(initialDelay = 2000, fixedRate = 3000)
+ public void emitAppUserActivity() {
+ val random = new Random();
+ val activityTypes = activityTypeRepository.findAll();
+ val appUsers = appUserRepository.findAll();
+
+ val activityType = activityTypes.get(random.nextInt(activityTypes.size()));
+ val appUser = appUsers.get(random.nextInt(appUsers.size()));
+ val appUserActivity = new UserActivity(activityType, appUser);
+ val isImportant = activityType.getName().contains("Вредн");
+
+ val routingKey = String.format("user.activity.message.%s", isImportant ? "important" : "simple");
+ rabbitTemplate.convertAndSend(routingKey, appUserActivity);
+ log.info("Send activity: {}", appUserActivity);
+
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml
new file mode 100644
index 00000000..c9f02077
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml
@@ -0,0 +1,14 @@
+server:
+ port: 9090
+spring:
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+ show-sql: false
+
+ rabbitmq:
+ addresses: "localhost"
+ sql:
+ init:
+ mode: always
\ No newline at end of file
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql
new file mode 100644
index 00000000..e15ece69
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql
@@ -0,0 +1,44 @@
+insert into app_users(name, email)
+values ('Рафаель Губерманович Тыгыдым', 'test@mail.ru');
+
+insert into app_users(name, email)
+values ('Артем Демосфенович Шмяк', 'test@mail.ru');
+
+insert into app_users(name, email)
+values ('Ифигения Бореславовна Фуфелшмерц', 'test@mail.ru');
+
+
+insert into activity_types(name)
+values ('Очень полезное дело №4');
+
+insert into activity_types(name)
+values ('Очень полезное дело №13');
+
+insert into activity_types(name)
+values ('Очень полезное дело №34');
+
+insert into activity_types(name)
+values ('Очень полезное дело №48');
+
+insert into activity_types(name)
+values ('Очень полезное дело №53');
+
+insert into activity_types(name)
+values ('Вредное дело №11');
+
+insert into activity_types(name)
+values ('Вредное дело №12');
+
+insert into activity_types(name)
+values ('Вредное дело №13');
+
+insert into activity_types(name)
+values ('Вредное дело №14');
+
+insert into activity_types(name)
+values ('Вредное дело №15');
+
+insert into activity_types(name)
+values ('Вредное дело №16');
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql
new file mode 100644
index 00000000..03bcef37
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table activity_types
+(
+ id bigint auto_increment,
+ name varchar(255),
+ primary key (id)
+);
+
+create table app_users
+(
+ id bigint auto_increment,
+ email varchar(255),
+ name varchar(255),
+ primary key (id)
+);
+
+
+create table app_users_activity
+(
+ id bigint auto_increment,
+ activity_time timestamp,
+ app_user_id bigint,
+ activity_type_id bigint,
+ primary key (id)
+);
+
+alter table app_users_activity
+ add constraint app_users_activity_user_id_fk foreign key (app_user_id) references app_users;
+
+alter table app_users_activity
+ add constraint app_users_activity_activity_type_id_fk foreign key (activity_type_id) references activity_types;
+
diff --git a/2025-11/spring-37-rabbit/user-activity-models/.gitignore b/2025-11/spring-37-rabbit/user-activity-models/.gitignore
new file mode 100644
index 00000000..a2a3040a
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/.gitignore
@@ -0,0 +1,31 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
diff --git a/2025-11/spring-37-rabbit/user-activity-models/pom.xml b/2025-11/spring-37-rabbit/user-activity-models/pom.xml
new file mode 100644
index 00000000..eae783db
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+
+ ru.otus.example
+ spring-mail-rabbitmq-demo
+ 1.0
+
+
+ user-activity-models
+ 0.0.1-SNAPSHOT
+ user-activity-models
+ Models for spring mail rabbitmq demo project
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+
+
+
+
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java
new file mode 100644
index 00000000..4cc6e62f
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java
@@ -0,0 +1,37 @@
+package ru.otus.example.useractivitymodels;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@Entity
+@Table(name = "activity_stat")
+public class ActivityStatElem {
+
+ @JsonIgnore
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @JsonProperty("Имя пользователя")
+ @Column(name = "app_user_name")
+ private String appUserName;
+
+ @JsonProperty("Тип активности")
+ @Column(name = "activity_type")
+ private String activityType;
+
+ @JsonProperty("Количество")
+ @Column(name = "activities_count")
+ private long activitiesCount;
+
+ public ActivityStatElem(String appUserName, String activityType, long activitiesCount) {
+ this.appUserName = appUserName;
+ this.activityType = activityType;
+ this.activitiesCount = activitiesCount;
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java
new file mode 100644
index 00000000..2ffc1322
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java
@@ -0,0 +1,21 @@
+package ru.otus.example.useractivitymodels;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "activity_types")
+public class ActivityType {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "name")
+ private String name;
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java
new file mode 100644
index 00000000..84936afd
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java
@@ -0,0 +1,24 @@
+package ru.otus.example.useractivitymodels;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "app_users")
+public class AppUser {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "email")
+ private String email;
+
+ @Column(name = "name")
+ private String name;
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java
new file mode 100644
index 00000000..b318688d
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java
@@ -0,0 +1,38 @@
+package ru.otus.example.useractivitymodels;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "app_users_activity")
+public class UserActivity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "activity_time")
+ private LocalDateTime activityTime;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "activity_type_id")
+ private ActivityType type;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "app_user_id")
+ private AppUser appUser;
+
+ public UserActivity(ActivityType type, AppUser appUser) {
+ this.id = id;
+ this.type = type;
+ this.appUser = appUser;
+ this.activityTime = LocalDateTime.now();
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore b/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore
new file mode 100644
index 00000000..a2a3040a
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore
@@ -0,0 +1,31 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml b/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml
new file mode 100644
index 00000000..513df465
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+
+ ru.otus.example
+ spring-mail-rabbitmq-demo
+ 1.0
+
+
+ user-activity-processor-microservice
+ 0.0.1-SNAPSHOT
+ user-activity-processor-microservice
+ User activity processor microservice
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+ ru.otus.example
+ user-activity-models
+ 0.0.1-SNAPSHOT
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java
new file mode 100644
index 00000000..a7b6eda5
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java
@@ -0,0 +1,17 @@
+package ru.otus.example.rabbitmq;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@EnableScheduling
+@EntityScan("ru.otus.example.useractivitymodels")
+@SpringBootApplication
+public class ProcessorMicroServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ProcessorMicroServiceApplication.class, args);
+ }
+
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java
new file mode 100644
index 00000000..a6170e07
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java
@@ -0,0 +1,23 @@
+package ru.otus.example.rabbitmq.actuator;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
+import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
+import org.springframework.stereotype.Component;
+import ru.otus.example.rabbitmq.repositories.ActivityStatRepository;
+import ru.otus.example.useractivitymodels.ActivityStatElem;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Component
+@Endpoint(id = "activity-stat")
+public class ActivityStatEndpoint {
+
+ private final ActivityStatRepository activityStatRepository;
+
+ @ReadOperation
+ public List getAppUsersActivityStat() {
+ return activityStatRepository.findAll();
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java
new file mode 100644
index 00000000..473d175e
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java
@@ -0,0 +1,13 @@
+package ru.otus.example.rabbitmq.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties("app")
+public class AppProps {
+ private String serverEmail;
+ private String adminEmail;
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java
new file mode 100644
index 00000000..caf54bcc
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java
@@ -0,0 +1,75 @@
+package ru.otus.example.rabbitmq.rabbitmq;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.amqp.core.*;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RabbitMqConfig {
+ @Bean
+ public Jackson2JsonMessageConverter jsonConverter(ObjectMapper objectMapper) {
+ return new Jackson2JsonMessageConverter(objectMapper);
+ }
+
+ @Bean
+ public Queue allActivityQueue() {
+ return new Queue("all-activity-queue");
+ }
+
+ @Bean
+ public Queue importantActivityQueue() {
+ return new Queue("important-activity-queue");
+ }
+
+ @Bean
+ public Queue statCalcCommandsQueue() {
+ return QueueBuilder.durable("stat-calc-commands-queue")
+ .maxLength(5)
+ .deadLetterExchange("dead-letter-exchange")
+ .build();
+ }
+
+ @Bean
+ public Queue deadLetterQueue() {
+ return new Queue("dead-letter-queue");
+ }
+
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange("main-exchange");
+ }
+
+ @Bean
+ public FanoutExchange deadLetterExchange() {
+ return new FanoutExchange("dead-letter-exchange");
+ }
+
+ @Bean
+ public Binding allActivityBinding() {
+ return BindingBuilder.bind(allActivityQueue())
+ .to(topicExchange())
+ .with("user.activity.message.*");
+ }
+
+ @Bean
+ public Binding importantActivityBinding() {
+ return BindingBuilder.bind(importantActivityQueue())
+ .to(topicExchange())
+ .with("user.activity.message.important");
+ }
+
+ @Bean
+ public Binding statCalcCommandsBinding() {
+ return BindingBuilder.bind(statCalcCommandsQueue())
+ .to(topicExchange())
+ .with("user.activity.stat");
+ }
+
+ @Bean
+ public Binding deadLetterBinding() {
+ return BindingBuilder.bind(deadLetterQueue())
+ .to(deadLetterExchange());
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java
new file mode 100644
index 00000000..ba6a63e5
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java
@@ -0,0 +1,70 @@
+package ru.otus.example.rabbitmq.rabbitmq;
+
+import com.rabbitmq.client.Channel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.springframework.amqp.AmqpRejectAndDontRequeueException;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Service;
+import ru.otus.example.rabbitmq.repositories.ActivityRepository;
+import ru.otus.example.rabbitmq.repositories.ActivityStatRepository;
+import ru.otus.example.rabbitmq.services.UserActivityToEmailTransformer;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class RabbitMqListener {
+
+ private final ActivityRepository activityRepository;
+ private final ActivityStatRepository activityStatRepository;
+ private final UserActivityToEmailTransformer messageTransformer;
+ private final JavaMailSender mailSender;
+
+ @RabbitListener(queues = "important-activity-queue")
+ public void processImportantMessages(UserActivity message) {
+ log.info("RECEIVED FROM important-activity-queue: " + message);
+
+ try {
+ val mailMessage = messageTransformer.transform(message);
+ log.info("Как будто посылаем письмо: " + mailMessage);
+ //mailSender.send(mailMessage);
+ } catch (Exception e) {
+ throw new AmqpRejectAndDontRequeueException("Ooops");
+ }
+ }
+
+ @RabbitListener(queues = "all-activity-queue")
+ public void processAllMessages(UserActivity message) {
+ log.info("RECEIVED FROM all-activity-queue: " + message);
+ try {
+ activityRepository.save(message);
+ } catch (Exception e) {
+ throw new AmqpRejectAndDontRequeueException("Ooops");
+ }
+ }
+
+ @RabbitListener(queues = "stat-calc-commands-queue", ackMode = "MANUAL")
+ public void processStatCalcCommandsMessages(String message,
+ Channel channel,
+ @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
+ log.warn("RECEIVED FROM stat-calc-commands-queue: " + message);
+
+ activityStatRepository.deleteAll();
+ val activityStat = activityStatRepository.calcActivityStat();
+ activityStatRepository.saveAll(activityStat);
+// sleep(5000);
+ channel.basicAck(tag, false);
+
+ // Для ackMode = "MANUAL" и перехода в dead letter exchange
+ //channel.basicNack(tag, false, false);
+
+ // Для ackMode = "AUTO" и перехода в dead letter exchange
+ //throw new AmqpRejectAndDontRequeueException("Ooops");
+
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java
new file mode 100644
index 00000000..ec1a77a6
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java
@@ -0,0 +1,9 @@
+package ru.otus.example.rabbitmq.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+@Transactional
+public interface ActivityRepository extends JpaRepository {
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java
new file mode 100644
index 00000000..cde54ceb
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java
@@ -0,0 +1,21 @@
+package ru.otus.example.rabbitmq.repositories;
+
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.useractivitymodels.ActivityStatElem;
+
+import java.util.List;
+
+@Transactional
+public interface ActivityStatRepository extends JpaRepository {
+
+ @Transactional(readOnly = true)
+ @Query("select new ru.otus.example.useractivitymodels.ActivityStatElem(u.name, t.name, count(a)) " +
+ "from UserActivity a left join a.appUser u left join a.type t " +
+ "group by u.name, t.name " +
+ "order by count(a) desc, u.name, t.name")
+ List calcActivityStat();
+
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java
new file mode 100644
index 00000000..8a61b36f
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java
@@ -0,0 +1,8 @@
+package ru.otus.example.rabbitmq.services;
+
+import org.springframework.mail.SimpleMailMessage;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+public interface UserActivityToEmailTransformer {
+ SimpleMailMessage transform(UserActivity activity);
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java
new file mode 100644
index 00000000..d5dc0c08
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java
@@ -0,0 +1,25 @@
+package ru.otus.example.rabbitmq.services;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.stereotype.Service;
+import ru.otus.example.rabbitmq.config.AppProps;
+import ru.otus.example.useractivitymodels.UserActivity;
+
+@RequiredArgsConstructor
+@Service
+public class UserActivityToEmailTransformerImpl implements UserActivityToEmailTransformer {
+
+ private final AppProps appProps;
+
+ @Override
+ public SimpleMailMessage transform(UserActivity activity) {
+ SimpleMailMessage mailMessage = new SimpleMailMessage();
+ mailMessage.setTo(appProps.getAdminEmail());
+ mailMessage.setFrom(appProps.getServerEmail());
+ mailMessage.setSubject("Обнаружена вредная активность");
+ mailMessage.setText(String.format("Внимание!!! Обнаружена вредная активность! Время: %s, пользователь: %s, тип активности: %s",
+ activity.getActivityTime(), activity.getAppUser().getName(), activity.getType().getName()));
+ return mailMessage;
+ }
+}
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml
new file mode 100644
index 00000000..e0fa092d
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml
@@ -0,0 +1,40 @@
+app:
+ # адрес почты, через которую сервер отправляет письма
+ server-email: ${server.email}
+ # адрес почты администратора, на которую сервер отправляет письма
+ admin-email: ${admin.email}
+
+spring:
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+ show-sql: true
+
+ rabbitmq:
+ addresses: "localhost"
+
+ mail:
+ host: smtp.mail.ru
+ port: 465
+ # логин и пароль для почты, через которую сервер отправляет письма
+ username: ${email.server.user}
+ password: ${email.server.password}
+ protocol: smtps
+ properties:
+ mail:
+ smtp:
+ auth: true
+ starttls.enable: true
+ sql:
+ init:
+ mode:
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health, info, activity-stat
+ endpoint:
+ health:
+ show-details: always
\ No newline at end of file
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql
new file mode 100644
index 00000000..e15ece69
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql
@@ -0,0 +1,44 @@
+insert into app_users(name, email)
+values ('Рафаель Губерманович Тыгыдым', 'test@mail.ru');
+
+insert into app_users(name, email)
+values ('Артем Демосфенович Шмяк', 'test@mail.ru');
+
+insert into app_users(name, email)
+values ('Ифигения Бореславовна Фуфелшмерц', 'test@mail.ru');
+
+
+insert into activity_types(name)
+values ('Очень полезное дело №4');
+
+insert into activity_types(name)
+values ('Очень полезное дело №13');
+
+insert into activity_types(name)
+values ('Очень полезное дело №34');
+
+insert into activity_types(name)
+values ('Очень полезное дело №48');
+
+insert into activity_types(name)
+values ('Очень полезное дело №53');
+
+insert into activity_types(name)
+values ('Вредное дело №11');
+
+insert into activity_types(name)
+values ('Вредное дело №12');
+
+insert into activity_types(name)
+values ('Вредное дело №13');
+
+insert into activity_types(name)
+values ('Вредное дело №14');
+
+insert into activity_types(name)
+values ('Вредное дело №15');
+
+insert into activity_types(name)
+values ('Вредное дело №16');
+
+
diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql
new file mode 100644
index 00000000..0f67d6e4
--- /dev/null
+++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql
@@ -0,0 +1,41 @@
+create table activity_types
+(
+ id bigint auto_increment,
+ name varchar(255),
+ primary key (id)
+);
+
+create table app_users
+(
+ id bigint auto_increment,
+ email varchar(255),
+ name varchar(255),
+ primary key (id)
+);
+
+
+create table app_users_activity
+(
+ id bigint auto_increment,
+ activity_time timestamp,
+ app_user_id bigint,
+ activity_type_id bigint,
+ primary key (id)
+);
+
+alter table app_users_activity
+ add constraint app_users_activity_user_id_fk foreign key (app_user_id) references app_users;
+
+alter table app_users_activity
+ add constraint app_users_activity_activity_type_id_fk foreign key (activity_type_id) references activity_types;
+
+
+create table activity_stat
+(
+ id bigint auto_increment,
+ app_user_name varchar(255),
+ activity_type varchar(255),
+ activities_count bigint,
+ primary key (id)
+);
+
diff --git a/2025-11/spring-38-kafka/.gitignore b/2025-11/spring-38-kafka/.gitignore
new file mode 100644
index 00000000..dae2cf99
--- /dev/null
+++ b/2025-11/spring-38-kafka/.gitignore
@@ -0,0 +1,39 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+!gradle-wrapper.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Ignore Gradle project-specific cache directory
+.gradle
+/buildSrc/.gradle/
+
+# Ignore Gradle build output directory
+build
+out
+
+#Idea
+*.iml
+*.iws
+*.ipr
+*.idea
+
diff --git a/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..e76d1f32
--- /dev/null
+++ b/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..642d572c
--- /dev/null
+++ b/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..e76d1f32
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..642d572c
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/2025-11/spring-38-kafka/consumer/mvnw b/2025-11/spring-38-kafka/consumer/mvnw
new file mode 100755
index 00000000..a16b5431
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ 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
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ 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
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/2025-11/spring-38-kafka/consumer/mvnw.cmd b/2025-11/spring-38-kafka/consumer/mvnw.cmd
new file mode 100644
index 00000000..c8d43372
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. 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,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/2025-11/spring-38-kafka/consumer/pom.xml b/2025-11/spring-38-kafka/consumer/pom.xml
new file mode 100644
index 00000000..f270f281
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+
+ ru.otus
+ spring-38-kafka
+ 1.0
+
+
+ consumer
+ 1.0
+
+
+ 21
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java
new file mode 100644
index 00000000..a9e6aa58
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java
@@ -0,0 +1,11 @@
+package ru.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ConsumerApp {
+ public static void main(String[] args) {
+ SpringApplication.run(ConsumerApp.class, args);
+ }
+}
diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java
new file mode 100644
index 00000000..a44ba11f
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java
@@ -0,0 +1,112 @@
+package ru.demo.config;
+
+import static org.springframework.kafka.support.serializer.JsonDeserializer.TYPE_MAPPINGS;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.config.KafkaListenerContainerFactory;
+import org.springframework.kafka.config.TopicBuilder;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
+import org.springframework.kafka.support.JacksonUtils;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
+import ru.demo.model.StringValue;
+import ru.demo.service.StringValueConsumer;
+import ru.demo.service.StringValueConsumerLogger;
+
+@Configuration
+public class ApplicationConfig {
+ private static final Logger log = LoggerFactory.getLogger(ApplicationConfig.class);
+ public final String topicName;
+
+ public ApplicationConfig(@Value("${application.kafka.topic}") String topicName) {
+ this.topicName = topicName;
+ }
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return JacksonUtils.enhancedObjectMapper();
+ }
+
+ @Bean
+ public ConsumerFactory consumerFactory(
+ KafkaProperties kafkaProperties, ObjectMapper mapper) {
+ var props = kafkaProperties.buildConsumerProperties();
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+ props.put(TYPE_MAPPINGS, "ru.demo.model.StringValue:ru.demo.model.StringValue");
+ // Максимальное количество записей, возвращаемых за один вызов poll()
+ props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 3);
+ // Максимальный интервал в миллисекундах между вызовами poll() (после превышения consumer считается неактивным)
+ props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 3_000);
+
+ var kafkaConsumerFactory = new DefaultKafkaConsumerFactory(props);
+ kafkaConsumerFactory.setValueDeserializer(new JsonDeserializer<>(mapper));
+ return kafkaConsumerFactory;
+ }
+
+ @Bean("listenerContainerFactory")
+ public KafkaListenerContainerFactory>
+ listenerContainerFactory(ConsumerFactory consumerFactory) {
+ var factory = new ConcurrentKafkaListenerContainerFactory();
+ factory.setConsumerFactory(consumerFactory);
+ factory.setBatchListener(true);
+ factory.setConcurrency(1);
+ // Время в миллисекундах между вызовами poll() к Kafka брокеру
+ factory.getContainerProperties().setIdleBetweenPolls(1_000);
+ // Таймаут в миллисекундах для операции poll() (время ожидания новых сообщений)
+ factory.getContainerProperties().setPollTimeout(1_000);
+
+ var executor = new SimpleAsyncTaskExecutor("k-consumer-");
+ executor.setConcurrencyLimit(10);
+ var listenerTaskExecutor = new ConcurrentTaskExecutor(executor);
+ factory.getContainerProperties().setListenerTaskExecutor(listenerTaskExecutor);
+ return factory;
+ }
+
+ @Bean
+ public NewTopic topic() {
+ return TopicBuilder.name(topicName).partitions(1).replicas(1).build();
+ }
+
+ @Bean
+ public StringValueConsumer stringValueConsumerLogger() {
+ return new StringValueConsumerLogger();
+ }
+
+ @Bean
+ public KafkaClient stringValueConsumer(StringValueConsumer stringValueConsumer) {
+ return new KafkaClient(stringValueConsumer);
+ }
+
+ public static class KafkaClient {
+ private final StringValueConsumer stringValueConsumer;
+
+ public KafkaClient(StringValueConsumer stringValueConsumer) {
+ this.stringValueConsumer = stringValueConsumer;
+ }
+
+ @KafkaListener(
+ topics = "${application.kafka.topic}",
+ containerFactory = "listenerContainerFactory")
+ public void listen(@Payload List values) {
+ log.info("values, values.size:{}", values.size());
+ stringValueConsumer.accept(values);
+ }
+ }
+}
diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java
new file mode 100644
index 00000000..213f1309
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java
@@ -0,0 +1,3 @@
+package ru.demo.model;
+
+public record StringValue(long id, String value) {}
diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java
new file mode 100644
index 00000000..ff5cd296
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java
@@ -0,0 +1,9 @@
+package ru.demo.service;
+
+import java.util.List;
+import ru.demo.model.StringValue;
+
+public interface StringValueConsumer {
+
+ void accept(List value);
+}
diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java
new file mode 100644
index 00000000..c791cbea
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java
@@ -0,0 +1,17 @@
+package ru.demo.service;
+
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.demo.model.StringValue;
+
+public class StringValueConsumerLogger implements StringValueConsumer {
+ private static final Logger log = LoggerFactory.getLogger(StringValueConsumerLogger.class);
+
+ @Override
+ public void accept(List values) {
+ for (var value : values) {
+ log.info("log:{}", value);
+ }
+ }
+}
diff --git a/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml b/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml
new file mode 100644
index 00000000..23adc138
--- /dev/null
+++ b/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml
@@ -0,0 +1,13 @@
+application:
+ kafka:
+ topic: "demo-topic"
+
+
+spring:
+ kafka:
+ consumer:
+ group-id: "test-group"
+ bootstrap-servers: "localhost:9092"
+ client-id: "demo-consumer"
+ # auto-offset-reset - стратегия чтения при отсутствии сохраненного offset: earliest (с начала), latest (с конца)
+ auto-offset-reset: earliest
diff --git a/2025-11/spring-38-kafka/docker/docker-compose.yml b/2025-11/spring-38-kafka/docker/docker-compose.yml
new file mode 100644
index 00000000..c36a5e37
--- /dev/null
+++ b/2025-11/spring-38-kafka/docker/docker-compose.yml
@@ -0,0 +1,35 @@
+
+services:
+ zookeeper:
+ image: confluentinc/cp-zookeeper:6.2.0
+ container_name: zookeeper
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+
+ broker:
+ image: confluentinc/cp-kafka:7.0.0
+ container_name: broker
+ ports:
+ - "9092:9092"
+ depends_on:
+ - zookeeper
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+
+ kafdrop:
+ image: obsidiandynamics/kafdrop:4.0.1
+ container_name: kafdrop
+ ports:
+ - "9000:9000"
+ depends_on:
+ - broker
+ environment:
+ KAFKA_BROKERCONNECT: "broker:29092"
+ JVM_OPTS: "-Xms32M -Xmx64M"
\ No newline at end of file
diff --git a/2025-11/spring-38-kafka/mvnw b/2025-11/spring-38-kafka/mvnw
new file mode 100755
index 00000000..a16b5431
--- /dev/null
+++ b/2025-11/spring-38-kafka/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ 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
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ 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
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/2025-11/spring-38-kafka/mvnw.cmd b/2025-11/spring-38-kafka/mvnw.cmd
new file mode 100644
index 00000000..c8d43372
--- /dev/null
+++ b/2025-11/spring-38-kafka/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. 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,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/2025-11/spring-38-kafka/pom.xml b/2025-11/spring-38-kafka/pom.xml
new file mode 100644
index 00000000..f761fa7a
--- /dev/null
+++ b/2025-11/spring-38-kafka/pom.xml
@@ -0,0 +1,92 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-38-kafka
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.5
+
+
+ pom
+
+
+ consumer
+ producer
+
+
+
+ UTF-8
+ 21
+ 21
+ 3.0.0-M3
+ 3.1.1
+ 3.3.9
+ 1.0.12.RELEASE
+ 3.4.5
+ 3.0.2
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${springframeworkBoot.version}
+ pom
+ import
+
+
+ com.google.code.findbugs
+ jsr305
+ ${jsr305.version}
+
+
+
+
+
+
+
+
+
+ maven-enforcer-plugin
+ ${maven-enforcer-plugin.version}
+
+
+ enforce-maven
+
+ enforce
+
+
+
+
+
+ ${minimal.maven.version}
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ ${maven-shade-plugin.version}
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ ${maven-assembly-plugin.version}
+
+
+
+
+
diff --git a/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..e76d1f32
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..642d572c
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/2025-11/spring-38-kafka/producer/mvnw b/2025-11/spring-38-kafka/producer/mvnw
new file mode 100755
index 00000000..a16b5431
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ 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
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ 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
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/2025-11/spring-38-kafka/producer/mvnw.cmd b/2025-11/spring-38-kafka/producer/mvnw.cmd
new file mode 100644
index 00000000..c8d43372
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. 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,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/2025-11/spring-38-kafka/producer/pom.xml b/2025-11/spring-38-kafka/producer/pom.xml
new file mode 100644
index 00000000..c3598163
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+
+ ru.otus
+ spring-38-kafka
+ 1.0
+
+
+ producer
+ 1.0
+
+
+ 21
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java
new file mode 100644
index 00000000..e9d022bb
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java
@@ -0,0 +1,11 @@
+package ru.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ProducerApp {
+ public static void main(String[] args) {
+ SpringApplication.run(ProducerApp.class, args);
+ }
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java
new file mode 100644
index 00000000..5e60699e
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java
@@ -0,0 +1,82 @@
+package ru.demo.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.TopicBuilder;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+import org.springframework.kafka.support.JacksonUtils;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+import ru.demo.model.StringValue;
+import ru.demo.service.DataSender;
+import ru.demo.service.DataSenderKafka;
+import ru.demo.service.StringValueSource;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+@Configuration
+public class ApplicationConfig {
+ private static final Logger log = LoggerFactory.getLogger(ApplicationConfig.class);
+ public final String topicName;
+
+ public ApplicationConfig(@Value("${application.kafka.topic}") String topicName) {
+ this.topicName = topicName;
+ }
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return JacksonUtils.enhancedObjectMapper();
+ }
+
+ @Bean
+ public ProducerFactory producerFactory(
+ KafkaProperties kafkaProperties, ObjectMapper mapper) {
+ var props = kafkaProperties.buildProducerProperties();
+ props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+
+ var kafkaProducerFactory = new DefaultKafkaProducerFactory(props);
+ kafkaProducerFactory.setValueSerializer(new JsonSerializer<>(mapper));
+ return kafkaProducerFactory;
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate(
+ ProducerFactory producerFactory) {
+ return new KafkaTemplate<>(producerFactory);
+ }
+
+ @Bean
+ public NewTopic topic() {
+ // Создание топика с указанным количеством партиций и реплик
+ return TopicBuilder.name(topicName).partitions(1).replicas(1).build();
+ }
+
+ @Bean
+ public DataSender dataSender(NewTopic topic, KafkaTemplate kafkaTemplate) {
+ return new DataSenderKafka(
+ topic.name(),
+ kafkaTemplate,
+ stringValue -> log.info("asked, value:{}", stringValue));
+ }
+
+ @Bean(value = "sourceExecutor", destroyMethod = "close")
+ public ScheduledExecutorService sourceExecutor() {
+ return Executors.newScheduledThreadPool(1);
+ }
+
+ @Bean
+ public StringValueSource stringValueSource(DataSender dataSender, ScheduledExecutorService sourceExecutor) {
+ return new StringValueSource(dataSender, sourceExecutor);
+ }
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java
new file mode 100644
index 00000000..213f1309
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java
@@ -0,0 +1,3 @@
+package ru.demo.model;
+
+public record StringValue(long id, String value) {}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java
new file mode 100644
index 00000000..321e3f5f
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java
@@ -0,0 +1,7 @@
+package ru.demo.service;
+
+import ru.demo.model.StringValue;
+
+public interface DataSender {
+ void send(StringValue value);
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java
new file mode 100644
index 00000000..e3494ece
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java
@@ -0,0 +1,49 @@
+package ru.demo.service;
+
+import java.util.function.Consumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import ru.demo.model.StringValue;
+
+public class DataSenderKafka implements DataSender {
+ private static final Logger log = LoggerFactory.getLogger(DataSenderKafka.class);
+
+ private final KafkaTemplate template;
+
+ private final Consumer sendAsk;
+
+ private final String topic;
+
+ public DataSenderKafka(
+ String topic,
+ KafkaTemplate template,
+ Consumer sendAsk) {
+ this.topic = topic;
+ this.template = template;
+ this.sendAsk = sendAsk;
+ }
+
+ @Override
+ public void send(StringValue value) {
+ try {
+ log.info("value:{}", value);
+ var key = String.valueOf(value.id());
+ template.send(topic, key, value)
+ .whenComplete(
+ (result, ex) -> {
+ if (ex == null) {
+ log.info(
+ "message id:{} was sent, offset:{}",
+ value.id(),
+ result.getRecordMetadata().offset());
+ sendAsk.accept(value);
+ } else {
+ log.error("message id:{} was not sent", value.id(), ex);
+ }
+ });
+ } catch (Exception ex) {
+ log.error("send error, value:{}", value, ex);
+ }
+ }
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java
new file mode 100644
index 00000000..16c25602
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java
@@ -0,0 +1,18 @@
+package ru.demo.service;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Service;
+
+@Service
+public class Runner implements CommandLineRunner {
+ private final ValueSource valueSource;
+
+ public Runner(ValueSource valueSource) {
+ this.valueSource = valueSource;
+ }
+
+ @Override
+ public void run(String... args) {
+ valueSource.generate();
+ }
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java
new file mode 100644
index 00000000..4c474f4c
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java
@@ -0,0 +1,33 @@
+package ru.demo.service;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.demo.model.StringValue;
+
+public class StringValueSource implements ValueSource {
+ private static final Logger log = LoggerFactory.getLogger(StringValueSource.class);
+ private final AtomicLong nextValue = new AtomicLong(1);
+ private final DataSender valueConsumer;
+ private final ScheduledExecutorService executor;
+
+
+ public StringValueSource(DataSender dataSender, ScheduledExecutorService executor) {
+ this.valueConsumer = dataSender;
+ this.executor = executor;
+ }
+
+ @Override
+ public void generate() {
+ executor.scheduleAtFixedRate(() -> valueConsumer.send(makeValue()), 0, 1, TimeUnit.SECONDS);
+ log.info("generation started");
+ }
+
+ private StringValue makeValue() {
+ var id = nextValue.getAndIncrement();
+ return new StringValue(id, "stVal:" + id);
+ }
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java
new file mode 100644
index 00000000..1204f5e6
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java
@@ -0,0 +1,5 @@
+package ru.demo.service;
+
+public interface ValueSource {
+ void generate();
+}
diff --git a/2025-11/spring-38-kafka/producer/src/main/resources/application.yml b/2025-11/spring-38-kafka/producer/src/main/resources/application.yml
new file mode 100644
index 00000000..72fa1f46
--- /dev/null
+++ b/2025-11/spring-38-kafka/producer/src/main/resources/application.yml
@@ -0,0 +1,11 @@
+application:
+ kafka:
+ topic: "demo-topic"
+
+spring:
+ kafka:
+ producer:
+ bootstrap-servers: "127.0.0.1:9092"
+ client-id: "demo-producer"
+
+
diff --git a/2025-11/spring-39-kafka-webflux/.gitignore b/2025-11/spring-39-kafka-webflux/.gitignore
new file mode 100755
index 00000000..d8762a8d
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/.gitignore
@@ -0,0 +1,35 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+!gradle-wrapper.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Ignore Gradle build output directory
+build
+out
+
+#Idea
+*.iml
+*.iws
+*.ipr
+*.idea
+
diff --git a/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..e76d1f32
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..642d572c
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/2025-11/spring-39-kafka-webflux/client/HttpRequests.http b/2025-11/spring-39-kafka-webflux/client/HttpRequests.http
new file mode 100755
index 00000000..3432cafa
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/HttpRequests.http
@@ -0,0 +1,11 @@
+###
+GET http://localhost:8082/data-mono/16
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8082/data/5
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
diff --git a/2025-11/spring-39-kafka-webflux/client/curlLoop.sh b/2025-11/spring-39-kafka-webflux/client/curlLoop.sh
new file mode 100755
index 00000000..32d83a38
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/curlLoop.sh
@@ -0,0 +1,5 @@
+date
+for run in {1..100000}; do
+ curl -s "http://localhost:8082/data-mono/$run" > /dev/null
+done
+date
\ No newline at end of file
diff --git a/2025-11/spring-39-kafka-webflux/client/pom.xml b/2025-11/spring-39-kafka-webflux/client/pom.xml
new file mode 100755
index 00000000..b5f78173
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/pom.xml
@@ -0,0 +1,64 @@
+
+
+ 4.0.0
+
+
+ ru.otus
+ spring-39-kafka-webflux
+ 1.0
+
+
+ client
+ 1.0
+
+
+ 21
+
+
+
+
+ ru.otus
+ common
+ 1.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
+ com.github.tomakehurst
+ wiremock
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java
new file mode 100755
index 00000000..341f9828
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java
@@ -0,0 +1,12 @@
+package com.datasrc;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ClientData {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ClientData.class, args);
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java
new file mode 100755
index 00000000..1761ca74
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java
@@ -0,0 +1,58 @@
+package com.datasrc;
+
+
+import com.datasrc.config.ReactiveSender;
+import com.datasrc.model.Request;
+import com.datasrc.model.RequestId;
+import com.datasrc.model.StreamData;
+import com.datasrc.model.StringValue;
+import java.util.concurrent.atomic.AtomicLong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+
+// http://localhost:8082/data/5
+@RestController
+public class ClientDataController {
+ private static final Logger log = LoggerFactory.getLogger(ClientDataController.class);
+ private final AtomicLong idGenerator = new AtomicLong(0);
+
+ private final WebClient webClient;
+ private final ReactiveSender requestSender;
+
+ private final StringValueStorage stringValueStorage;
+
+ public ClientDataController(WebClient webClient, ReactiveSender requestSender, StringValueStorage stringValueStorage) {
+ this.webClient = webClient;
+ this.requestSender = requestSender;
+ this.stringValueStorage = stringValueStorage;
+ }
+
+ @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE)
+ public Flux data(@PathVariable("seed") long seed) {
+ log.info("request for data, seed:{}", seed);
+
+ return webClient.get().uri(String.format("/data/%d", seed))
+ .accept(MediaType.APPLICATION_NDJSON)
+ .retrieve()
+ .bodyToFlux(StreamData.class)
+ .doOnNext(val -> log.info("val:{}", val));
+ }
+
+ @GetMapping(value = "/data-mono/{seed}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public Mono dataMono(@PathVariable("seed") long seed) {
+ log.info("request for string data-mono, seed:{}", seed);
+ var request = new Request(new RequestId(idGenerator.incrementAndGet()), seed);
+
+ return requestSender.send(request, requestSend -> log.info("send ok: {}", requestSend))
+ .flatMap(v -> stringValueStorage.get(new RequestId(v.correlationMetadata().id())))
+ .doOnNext(stringValue -> log.info("value for client:{}", stringValue));
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java
new file mode 100755
index 00000000..8ffdd2a9
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java
@@ -0,0 +1,47 @@
+package com.datasrc;
+
+import com.datasrc.model.RequestId;
+import com.datasrc.model.StringValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.ConnectableFlux;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.SignalType;
+import reactor.core.publisher.Sinks;
+import reactor.util.annotation.NonNull;
+
+public class StringValueStorage implements Sinks.EmitFailureHandler {
+
+ private final Sinks.Many sink;
+ private final ConnectableFlux sinkConnectable;
+ private static final Logger log = LoggerFactory.getLogger(StringValueStorage.class);
+
+ public StringValueStorage() {
+ sink = Sinks.many().multicast().onBackpressureBuffer();
+ sinkConnectable = sink.asFlux().publish();
+ sinkConnectable.connect();
+ }
+
+ public void put(RequestId requestId, StringValue value) {
+ log.info("put. requestId:{}, value:{}", requestId, value);
+ sink.emitNext(new ResponseData(requestId, value), this);
+ }
+
+ public Mono get(RequestId requestId) {
+ return Mono.from(sinkConnectable
+ .filter(responseData -> {
+ log.info("waiting:{}, fact:{}", requestId, responseData.requestId);
+ return responseData.requestId.id() == requestId.id();
+ })
+ .map(responseData -> responseData.stringValue));
+ }
+
+
+ @Override
+ public boolean onEmitFailure(@NonNull SignalType signalType, @NonNull Sinks.EmitResult emitResult) {
+ return false;
+ }
+
+ private record ResponseData(RequestId requestId, StringValue stringValue) {
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java
new file mode 100755
index 00000000..70edb4aa
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java
@@ -0,0 +1,117 @@
+package com.datasrc.config;
+
+import com.datasrc.StringValueStorage;
+import com.datasrc.model.Request;
+import com.datasrc.model.Response;
+import io.netty.channel.nio.NioEventLoopGroup;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
+import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ReactorResourceFactory;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+import reactor.util.annotation.NonNull;
+
+@Configuration
+@SuppressWarnings("java:S2095")
+public class ApplConfig {
+ private static final int THREAD_POOL_SIZE = 4;
+ private static final int RESPONSE_RECEIVER_POOL_SIZE = 1;
+ private static final int KAFKA_POOL_SIZE = 1;
+
+ @Bean(name ="serverThreadEventLoop", destroyMethod = "close")
+ public NioEventLoopGroup serverThreadEventLoop() {
+ return new NioEventLoopGroup(THREAD_POOL_SIZE,
+ new ThreadFactory() {
+ private final AtomicLong threadIdGenerator = new AtomicLong(0);
+ @Override
+ public Thread newThread(@NonNull Runnable task) {
+ return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet());
+ }
+ });
+ }
+
+ @Bean
+ public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) {
+ var factory = new NettyReactiveWebServerFactory();
+ factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop));
+ return factory;
+ }
+
+ @Bean(name ="clientThreadEventLoop", destroyMethod = "close")
+ public NioEventLoopGroup clientThreadEventLoop() {
+ return new NioEventLoopGroup(THREAD_POOL_SIZE,
+ new ThreadFactory() {
+ private final AtomicLong threadIdGenerator = new AtomicLong(0);
+
+ @Override
+ public Thread newThread(@NonNull Runnable task) {
+ return new Thread(task, "client-thread-" + threadIdGenerator.incrementAndGet());
+ }
+ });
+ }
+
+ @Bean
+ public ReactorResourceFactory reactorResourceFactory(@Qualifier("clientThreadEventLoop") NioEventLoopGroup clientThreadEventLoop) {
+ var resourceFactory = new org.springframework.http.client.ReactorResourceFactory();
+ resourceFactory.setLoopResources(b -> clientThreadEventLoop);
+ resourceFactory.setUseGlobalResources(false);
+ return resourceFactory;
+ }
+
+ @Bean
+ public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory resourceFactory) {
+ return new ReactorClientHttpConnector(resourceFactory, mapper -> mapper);
+ }
+
+ @Bean("responseReceiverScheduler")
+ public Scheduler responseReceiverScheduler() {
+ return Schedulers.newParallel("response-receiver", RESPONSE_RECEIVER_POOL_SIZE);
+ }
+
+ @Bean("kafkaScheduler")
+ public Scheduler kafkaScheduler() {
+ return Schedulers.newParallel("kafka-scheduler", KAFKA_POOL_SIZE);
+ }
+
+ @Bean
+ public WebClient webClient(WebClient.Builder builder,
+ @Value("${application.processor.url}") String url) {
+ return builder
+ .baseUrl(url)
+ .build();
+ }
+
+ @Bean(destroyMethod = "close")
+ public ReactiveSender requestSender(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers,
+ @Value("${application.topic-request}") String topicRequest,
+ @Qualifier("kafkaScheduler") Scheduler kafkaScheduler
+ ) {
+ return new ReactiveSender<>(bootstrapServers, kafkaScheduler, topicRequest);
+ }
+
+ @Bean
+ public StringValueStorage stringValueStorage() {
+ return new StringValueStorage();
+ }
+
+ @Bean(destroyMethod = "close")
+ public ReactiveReceiver responseReceiver(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers,
+ @Value("${application.topic-response}") String topicResponse,
+ @Value("${application.kafka-group-id}") String groupId,
+ @Qualifier("responseReceiverScheduler") Scheduler responseReceiverScheduler,
+ StringValueStorage stringValueStorage) {
+ return new ReactiveReceiver<>(bootstrapServers, Response.class, topicResponse, responseReceiverScheduler, groupId,
+ response -> stringValueStorage.put(response.data().requestId(), response.data()));
+ }
+
+}
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml b/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml
new file mode 100755
index 00000000..29b56723
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml
@@ -0,0 +1,10 @@
+server:
+ port: 8082
+
+application:
+ processor:
+ url: http://localhost:8081
+ kafka-bootstrap-servers: localhost:9092
+ kafka-group-id: clientConsumerGroup
+ topic-request: request
+ topic-response: response
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml b/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml
new file mode 100755
index 00000000..b1f9bfe2
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml
@@ -0,0 +1,11 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html
new file mode 100755
index 00000000..d42e4d95
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Stream Demo
+
+
+Stream Demo
+
+
+
+
+
\ No newline at end of file
diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js
new file mode 100755
index 00000000..9287c9b8
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js
@@ -0,0 +1,24 @@
+const streamErr = e => {
+ console.warn("error");
+ console.warn(e);
+}
+
+fetch("http://localhost:8082/data/5").then((response) => {
+ return can.ndjsonStream(response.body);
+}).then(dataStream => {
+ const reader = dataStream.getReader();
+ const read = result => {
+ if (result.done) {
+ return;
+ }
+ render(result.value);
+ reader.read().then(read, streamErr);
+ }
+ reader.read().then(read, streamErr);
+});
+
+const render = value => {
+ const div = document.createElement('div');
+ div.append(new Date() + ' stringValue:', JSON.stringify(value));
+ document.getElementById('dataBlock').append(div);
+};
\ No newline at end of file
diff --git a/2025-11/spring-39-kafka-webflux/common/HttpRequests.http b/2025-11/spring-39-kafka-webflux/common/HttpRequests.http
new file mode 100755
index 00000000..83afa6d0
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/HttpRequests.http
@@ -0,0 +1,11 @@
+###
+GET http://localhost:8082/data-mono/13
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8082/data/5
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
diff --git a/2025-11/spring-39-kafka-webflux/common/curlLoop.sh b/2025-11/spring-39-kafka-webflux/common/curlLoop.sh
new file mode 100755
index 00000000..32d83a38
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/curlLoop.sh
@@ -0,0 +1,5 @@
+date
+for run in {1..100000}; do
+ curl -s "http://localhost:8082/data-mono/$run" > /dev/null
+done
+date
\ No newline at end of file
diff --git a/2025-11/spring-39-kafka-webflux/common/pom.xml b/2025-11/spring-39-kafka-webflux/common/pom.xml
new file mode 100755
index 00000000..0091a0e8
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ ru.otus
+ spring-39-kafka-webflux
+ 1.0
+
+
+ common
+ 1.0
+
+
+ 21
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ provided
+
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+
+
+
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java
new file mode 100755
index 00000000..1df2c495
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java
@@ -0,0 +1,7 @@
+package com.datasrc.config;
+
+public class ConsumerException extends RuntimeException {
+ public ConsumerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java
new file mode 100755
index 00000000..fa76ab49
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java
@@ -0,0 +1,42 @@
+package com.datasrc.config;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import org.apache.kafka.common.errors.SerializationException;
+import org.apache.kafka.common.serialization.Deserializer;
+
+public class JsonDeserializer implements Deserializer {
+ public static final String OBJECT_MAPPER = "objectMapper";
+ public static final String TYPE_REFERENCE = "typeReference";
+ private final String encoding = StandardCharsets.UTF_8.name();
+ private ObjectMapper mapper;
+ private JavaType javaType;
+
+ @Override
+ public void configure(Map configs, boolean isKey) {
+ mapper = (ObjectMapper) configs.get(OBJECT_MAPPER);
+ if (mapper == null) {
+ throw new IllegalArgumentException("config property OBJECT_MAPPER was not set");
+ }
+ javaType = (JavaType) configs.get(TYPE_REFERENCE);
+ if (javaType == null) {
+ throw new IllegalArgumentException("config property TYPE_REFERENCE was not set");
+ }
+ }
+
+ @Override
+ public T deserialize(String topic, byte[] data) {
+ try {
+ if (data == null) {
+ return null;
+ } else {
+ var valueAsString = new String(data, encoding);
+ return mapper.readValue(valueAsString, javaType);
+ }
+ } catch (Exception e) {
+ throw new SerializationException("Error when deserializing byte[] to StringValue", e);
+ }
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java
new file mode 100755
index 00000000..84b48d44
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java
@@ -0,0 +1,34 @@
+package com.datasrc.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import org.apache.kafka.common.errors.SerializationException;
+import org.apache.kafka.common.serialization.Serializer;
+
+public class JsonSerializer implements Serializer {
+ public static final String OBJECT_MAPPER = "objectMapper";
+ private final String encoding = StandardCharsets.UTF_8.name();
+ private ObjectMapper mapper;
+
+ @Override
+ public void configure(Map configs, boolean isKey) {
+ mapper = (ObjectMapper) configs.get(OBJECT_MAPPER);
+ if (mapper == null) {
+ throw new IllegalArgumentException("config property OBJECT_MAPPER was not set");
+ }
+ }
+
+ @Override
+ public byte[] serialize(String topic, T data) {
+ try {
+ if (data == null) {
+ return new byte[]{};
+ } else {
+ return mapper.writeValueAsString(data).getBytes(encoding);
+ }
+ } catch (Exception e) {
+ throw new SerializationException("Error when serializing StringValue to byte[] ", e);
+ }
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java
new file mode 100755
index 00000000..02cd1d35
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java
@@ -0,0 +1,127 @@
+package com.datasrc.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.Random;
+import java.util.concurrent.CancellationException;
+import java.util.function.Consumer;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.serialization.LongDeserializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Hooks;
+import reactor.core.scheduler.Scheduler;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOptions;
+import reactor.util.retry.Retry;
+
+import static com.datasrc.config.JsonDeserializer.OBJECT_MAPPER;
+import static com.datasrc.config.JsonDeserializer.TYPE_REFERENCE;
+import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.GROUP_ID_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.GROUP_INSTANCE_ID_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_RECORDS_CONFIG;
+import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG;
+
+public class ReactiveReceiver {
+ private static final Logger log = LoggerFactory.getLogger(ReactiveReceiver.class);
+ private final Random random = new Random();
+
+ private final Disposable kafkaSubscriber;
+ private final Disposable kafkaConnection;
+
+ public static final int MAX_POLL_INTERVAL_MS = 1_000;
+
+ public ReactiveReceiver(
+ String bootstrapServers, Class> valueClass, String topicName, Scheduler schedulerValueReceiver, String groupId, Consumer valueConsumer
+ ) {
+ Properties props = new Properties();
+ props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(GROUP_ID_CONFIG, groupId);
+ props.put(GROUP_INSTANCE_ID_CONFIG, makeGroupInstanceIdConfig(groupId));
+ props.put(ENABLE_AUTO_COMMIT_CONFIG, "true");
+ props.put(AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
+ props.put(AUTO_OFFSET_RESET_CONFIG, "earliest");
+ props.put(KEY_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class);
+ props.put(VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+
+ var objectMapper = new ObjectMapper();
+ objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ props.put(OBJECT_MAPPER, objectMapper);
+ props.put(TYPE_REFERENCE, objectMapper.getTypeFactory().constructType(valueClass));
+
+ props.put(MAX_POLL_RECORDS_CONFIG, 3);
+ props.put(MAX_POLL_INTERVAL_MS_CONFIG, MAX_POLL_INTERVAL_MS);
+
+ ReceiverOptions receiverOptions =
+ ReceiverOptions.create(props)
+ .pollTimeout(Duration.ofSeconds(500))
+ .schedulerSupplier(() -> schedulerValueReceiver)
+ .subscription(Collections.singleton(topicName));
+
+ Flux> inboundFlux = KafkaReceiver.create(receiverOptions)
+ .receiveAutoAck()
+ .concatMap(
+ consumerRecordFlux -> {
+ log.info("consumerRecordFlux done, commit");
+ return consumerRecordFlux;
+ })
+ .retryWhen(Retry.backoff(3, Duration.of(5, ChronoUnit.SECONDS)));
+
+ Hooks.onErrorDropped(
+ error -> {
+ if (error instanceof CancellationException) {
+ log.info("Cancellation event:", error);
+ } else {
+ log.error("error:", error);
+ }
+ });
+
+ var kafkaFlow = inboundFlux.doOnCancel(() -> log.info("connection canceled"))
+ .doOnError(error -> log.error("Consuming error", error))
+ .publish();
+
+ log.info("start consuming");
+ kafkaConnection = kafkaFlow.connect();
+ kafkaSubscriber =
+ kafkaFlow.subscribe(
+ receiverRecord -> {
+ var key = receiverRecord.key();
+ var value = receiverRecord.value();
+ log.info("key:{}, value:{}, record:{}", key, value, receiverRecord);
+ valueConsumer.accept(value);
+ });
+ }
+
+ public void close() {
+ log.info("stop consuming");
+ kafkaSubscriber.dispose();
+ kafkaConnection.dispose();
+ }
+
+ private String makeGroupInstanceIdConfig(String groupId) {
+ try {
+ var hostName = InetAddress.getLocalHost().getHostName();
+ return String.join(
+ "-",
+ groupId,
+ hostName,
+ String.valueOf(random.nextInt(100_999_999)));
+ } catch (Exception ex) {
+ throw new ConsumerException("can't make GroupInstanceIdConfig", ex);
+ }
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java
new file mode 100755
index 00000000..362708dd
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java
@@ -0,0 +1,92 @@
+package com.datasrc.config;
+
+import static com.datasrc.config.JsonSerializer.OBJECT_MAPPER;
+import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG;
+import static org.apache.kafka.clients.CommonClientConfigs.RETRIES_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.BUFFER_MEMORY_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.MAX_BLOCK_MS_CONFIG;
+import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG;
+
+import com.datasrc.model.DataForSending;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Properties;
+import java.util.function.Consumer;
+import org.apache.kafka.common.serialization.LongSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderOptions;
+import reactor.kafka.sender.SenderRecord;
+import reactor.kafka.sender.SenderResult;
+
+public class ReactiveSender> {
+ private static final Logger log = LoggerFactory.getLogger(ReactiveSender.class);
+ private final KafkaSender sender;
+ private final String topicName;
+
+ public ReactiveSender(String bootstrapServers, Scheduler schedulerKafka, String topicName) {
+ this.topicName = topicName;
+ var props = new Properties();
+ props.put(CLIENT_ID_CONFIG, "KafkaReactiveSender");
+ props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ACKS_CONFIG, "1");
+ props.put(RETRIES_CONFIG, 1);
+ props.put(BATCH_SIZE_CONFIG, 16384);
+ props.put(LINGER_MS_CONFIG, 10);
+ props.put(BUFFER_MEMORY_CONFIG, 26384); // bytes
+ props.put(MAX_BLOCK_MS_CONFIG, 1_000); // ms
+ props.put(KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class);
+ props.put(VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+
+ var objectMapper = new ObjectMapper();
+ objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ props.put(OBJECT_MAPPER, objectMapper);
+
+ SenderOptions senderOptions =
+ SenderOptions.create(props)
+ .maxInFlight(10)
+ .scheduler(schedulerKafka);
+
+ sender = KafkaSender.create(senderOptions);
+
+ var shutdownHook =
+ new Thread(
+ () -> {
+ log.info("closing kafka sender");
+ sender.close();
+ });
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+ public Mono> send(T data, Consumer sendAsk) {
+ return sender.send(Mono.just(SenderRecord.create(
+ topicName,
+ null,
+ null,
+ data.id(),
+ data,
+ data)))
+ .doOnError(error -> log.error("Send failed", error))
+ .doOnNext(
+ senderResult -> {
+ log.info(
+ "message id:{} was sent, offset:{}",
+ senderResult.correlationMetadata().id(),
+ senderResult.recordMetadata().offset());
+ sendAsk.accept(senderResult.correlationMetadata());
+ }).next();
+ }
+
+ public void close() {
+ sender.close();
+ }
+}
diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java
new file mode 100755
index 00000000..6aa91026
--- /dev/null
+++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java
@@ -0,0 +1,7 @@
+package com.datasrc.model;
+
+public interface DataForSending