summaryrefslogtreecommitdiff
path: root/src/ad
diff options
context:
space:
mode:
authorSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
committerSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
commit82e03978b89938219958032efb1448cc76baa181 (patch)
tree626f3e54d52ecd49be0ed3bee30abacc0453d081 /src/ad
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/ad')
-rw-r--r--src/ad/.java-version1
-rw-r--r--src/ad/Dockerfile35
-rw-r--r--src/ad/README.md41
-rw-r--r--src/ad/build.gradle143
-rw-r--r--src/ad/gradle/wrapper/gradle-wrapper.jarbin0 -> 43504 bytes
-rw-r--r--src/ad/gradle/wrapper/gradle-wrapper.properties7
-rwxr-xr-xsrc/ad/gradlew252
-rw-r--r--src/ad/gradlew.bat94
-rw-r--r--src/ad/settings.gradle2
-rw-r--r--src/ad/src/main/java/oteldemo/AdService.java328
-rw-r--r--src/ad/src/main/java/oteldemo/problempattern/CPULoad.java116
-rw-r--r--src/ad/src/main/java/oteldemo/problempattern/GarbageCollectionTrigger.java80
-rw-r--r--src/ad/src/main/java/oteldemo/problempattern/MemoryUtils.java65
-rw-r--r--src/ad/src/main/resources/log4j2.xml18
14 files changed, 1182 insertions, 0 deletions
diff --git a/src/ad/.java-version b/src/ad/.java-version
new file mode 100644
index 0000000..5f39e91
--- /dev/null
+++ b/src/ad/.java-version
@@ -0,0 +1 @@
+21.0
diff --git a/src/ad/Dockerfile b/src/ad/Dockerfile
new file mode 100644
index 0000000..92f69c0
--- /dev/null
+++ b/src/ad/Dockerfile
@@ -0,0 +1,35 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+
+FROM --platform=${BUILDPLATFORM} eclipse-temurin:21-jdk AS builder
+ARG _JAVA_OPTIONS
+WORKDIR /usr/src/app/
+
+COPY ./src/ad/gradlew* ./src/ad/settings.gradle* ./src/ad/build.gradle ./
+COPY ./src/ad/gradle ./gradle
+
+RUN chmod +x ./gradlew
+RUN ./gradlew
+RUN ./gradlew downloadRepos
+
+COPY ./src/ad/ ./
+COPY ./pb/ ./proto
+RUN chmod +x ./gradlew
+RUN ./gradlew installDist -PprotoSourceDir=./proto
+
+# -----------------------------------------------------------------------------
+
+FROM eclipse-temurin:21-jre
+
+ARG OTEL_JAVA_AGENT_VERSION
+ARG _JAVA_OPTIONS
+
+WORKDIR /usr/src/app/
+
+COPY --from=builder /usr/src/app/ ./
+ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v$OTEL_JAVA_AGENT_VERSION/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar
+ENV JAVA_TOOL_OPTIONS=-javaagent:/usr/src/app/opentelemetry-javaagent.jar
+
+EXPOSE ${AD_PORT}
+ENTRYPOINT [ "./build/install/opentelemetry-demo-ad/bin/Ad" ]
diff --git a/src/ad/README.md b/src/ad/README.md
new file mode 100644
index 0000000..f19b735
--- /dev/null
+++ b/src/ad/README.md
@@ -0,0 +1,41 @@
+# Ad Service
+
+The Ad service provides advertisement based on context keys. If no context keys
+are provided then it returns random ads.
+
+## Building Locally
+
+The Ad service requires at least JDK 17 to build and uses gradlew to
+compile/install/distribute. Gradle wrapper is already part of the source code.
+To build Ad Service, run:
+
+```sh
+./gradlew installDist
+```
+
+It will create an executable script
+`src/ad/build/install/oteldemo/bin/Ad`.
+
+To run the Ad Service:
+
+```sh
+export AD_PORT=8080
+export FEATURE_FLAG_GRPC_SERVICE_ADDR=featureflagservice:50053
+./build/install/opentelemetry-demo-ad/bin/Ad
+```
+
+### Upgrading Gradle
+
+If you need to upgrade the version of gradle then run
+
+```sh
+./gradlew wrapper --gradle-version <new-version>
+```
+
+## Building Docker
+
+From the root of `opentelemetry-demo`, run:
+
+```sh
+docker build --file ./src/ad/Dockerfile ./
+```
diff --git a/src/ad/build.gradle b/src/ad/build.gradle
new file mode 100644
index 0000000..2d4e8e1
--- /dev/null
+++ b/src/ad/build.gradle
@@ -0,0 +1,143 @@
+
+plugins {
+ id 'com.google.protobuf' version '0.9.5'
+ id 'com.github.sherter.google-java-format' version '0.9'
+ id 'idea'
+ id 'application'
+ id 'com.github.ben-manes.versions' version '0.52.0'
+}
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+description = 'Ad Service'
+group = "ad"
+version = "0.1.0-SNAPSHOT"
+
+def opentelemetryVersion = "1.54.1"
+def opentelemetryInstrumentationVersion = "2.20.1"
+def grpcVersion = "1.75.0"
+def jacksonVersion = "2.20.0"
+def protocVersion = "4.32.1"
+
+tasks.withType(JavaCompile).configureEach {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+ext {
+ speed = project.hasProperty('speed') ? project.getProperty('speed') : false
+ Provider<Directory> output = layout.buildDirectory.dir("outputLocation")
+ offlineCompile = output.get().asFile
+}
+
+dependencies {
+ if (speed) {
+ implementation fileTree(dir: offlineCompile, include: '*.jar')
+ } else {
+ implementation platform("io.opentelemetry:opentelemetry-bom:${opentelemetryVersion}")
+ implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:${opentelemetryInstrumentationVersion}")
+
+ implementation "com.google.api.grpc:proto-google-common-protos:2.61.2",
+ "com.google.protobuf:protobuf-java:${protocVersion}",
+ "javax.annotation:javax.annotation-api:1.3.2",
+ "io.grpc:grpc-protobuf:${grpcVersion}",
+ "io.grpc:grpc-stub:${grpcVersion}",
+ "io.grpc:grpc-netty:${grpcVersion}",
+ "io.grpc:grpc-services:${grpcVersion}",
+ "io.opentelemetry:opentelemetry-api",
+ "io.opentelemetry:opentelemetry-sdk",
+ "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations",
+ "org.apache.logging.log4j:log4j-core:2.25.2",
+ "dev.openfeature.contrib.providers:flagd:0.11.15",
+ 'dev.openfeature:sdk:1.18.1'
+
+ runtimeOnly "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}",
+ "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}",
+ "io.netty:netty-tcnative-boringssl-static:2.0.74.Final"
+ }
+}
+
+// Default protoSourceDir is in /opentelemetry-demo/pb. Optionally override the
+// location for the docker build, which copies the protos to a different location.
+def protoSourceDir = findProperty('protoSourceDir')?: project.projectDir.parentFile.parentFile.toPath().toString() + "/pb"
+def protoDestDir = project.buildDir.toPath().toString() + "/proto"
+
+// Copy protos to the build directory
+tasks.register('copyProtos', Copy) {
+ from protoSourceDir
+ into protoDestDir
+}
+
+// Include the output directory of copyProtos in main source set so they are
+// picked up by the protobuf plugin
+sourceSets {
+ main {
+ proto {
+ srcDir(protoDestDir)
+ }
+ }
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:${protocVersion}"
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
+ }
+ }
+ generateProtoTasks { task ->
+ all()*.plugins {
+ grpc {}
+ }
+ ofSourceSet('main')
+ }
+}
+
+afterEvaluate {
+ // Ensure protos are copy before classes are generated
+ tasks.getByName('processResources').dependsOn 'copyProtos'
+ tasks.getByName('generateProto').dependsOn 'copyProtos'
+}
+
+googleJavaFormat {
+ toolVersion '1.18.1'
+}
+
+// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
+sourceSets {
+ main {
+ java {
+ srcDirs 'oteldemo'
+ srcDirs 'build/generated/source/proto/main/java/oteldemo'
+ srcDirs 'build/generated/source/proto/main/grpc/oteldemo'
+ }
+ }
+}
+
+startScripts.enabled = false
+
+// This to cache dependencies during Docker image building. First build will take time.
+// Subsequent build will be incremental.
+task downloadRepos(type: Copy) {
+ from configurations.compileClasspath
+ into offlineCompile
+ from configurations.runtimeClasspath
+ into offlineCompile
+}
+
+task ad(type: CreateStartScripts) {
+ mainClass.set('oteldemo.AdService')
+ applicationName = 'Ad'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = startScripts.classpath
+}
+
+applicationDistribution.into('bin') {
+ from(ad)
+ fileMode = 0755
+}
diff --git a/src/ad/gradle/wrapper/gradle-wrapper.jar b/src/ad/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..2c35211
--- /dev/null
+++ b/src/ad/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/src/ad/gradle/wrapper/gradle-wrapper.properties b/src/ad/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e18bc25
--- /dev/null
+++ b/src/ad/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/src/ad/gradlew b/src/ad/gradlew
new file mode 100755
index 0000000..f5feea6
--- /dev/null
+++ b/src/ad/gradlew
@@ -0,0 +1,252 @@
+#!/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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# 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/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || 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/src/ad/gradlew.bat b/src/ad/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/src/ad/gradlew.bat
@@ -0,0 +1,94 @@
+@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
+@rem SPDX-License-Identifier: Apache-2.0
+@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. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+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/src/ad/settings.gradle b/src/ad/settings.gradle
new file mode 100644
index 0000000..b85c24c
--- /dev/null
+++ b/src/ad/settings.gradle
@@ -0,0 +1,2 @@
+
+rootProject.name = 'opentelemetry-demo-ad'
diff --git a/src/ad/src/main/java/oteldemo/AdService.java b/src/ad/src/main/java/oteldemo/AdService.java
new file mode 100644
index 0000000..d7f2688
--- /dev/null
+++ b/src/ad/src/main/java/oteldemo/AdService.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package oteldemo;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import io.grpc.*;
+import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
+import io.grpc.protobuf.services.*;
+import io.grpc.stub.StreamObserver;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.baggage.Baggage;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.annotations.SpanAttribute;
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import oteldemo.Demo.Ad;
+import oteldemo.Demo.AdRequest;
+import oteldemo.Demo.AdResponse;
+import oteldemo.problempattern.GarbageCollectionTrigger;
+import oteldemo.problempattern.CPULoad;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.MutableContext;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import java.util.UUID;
+
+
+public final class AdService {
+
+ private static final Logger logger = LogManager.getLogger(AdService.class);
+
+ @SuppressWarnings("FieldCanBeLocal")
+ private static final int MAX_ADS_TO_SERVE = 2;
+
+ private Server server;
+ private HealthStatusManager healthMgr;
+
+ private static final AdService service = new AdService();
+ private static final Tracer tracer = GlobalOpenTelemetry.getTracer("ad");
+ private static final Meter meter = GlobalOpenTelemetry.getMeter("ad");
+
+ private static final LongCounter adRequestsCounter =
+ meter
+ .counterBuilder("app.ads.ad_requests")
+ .setDescription("Counts ad requests by request and response type")
+ .build();
+
+ private static final AttributeKey<String> adRequestTypeKey =
+ AttributeKey.stringKey("app.ads.ad_request_type");
+ private static final AttributeKey<String> adResponseTypeKey =
+ AttributeKey.stringKey("app.ads.ad_response_type");
+
+ private void start() throws IOException {
+ int port =
+ Integer.parseInt(
+ Optional.ofNullable(System.getenv("AD_PORT"))
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ "environment vars: AD_PORT must not be null")));
+ healthMgr = new HealthStatusManager();
+
+ // Create a flagd instance with OpenTelemetry
+ FlagdOptions options =
+ FlagdOptions.builder()
+ .withGlobalTelemetry(true)
+ .build();
+
+ FlagdProvider flagdProvider = new FlagdProvider(options);
+ // Set flagd as the OpenFeature Provider
+ OpenFeatureAPI.getInstance().setProvider(flagdProvider);
+
+ server =
+ ServerBuilder.forPort(port)
+ .addService(new AdServiceImpl())
+ .addService(healthMgr.getHealthService())
+ .build()
+ .start();
+ logger.info("Ad service started, listening on " + port);
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+ System.err.println(
+ "*** shutting down gRPC ads server since JVM is shutting down");
+ AdService.this.stop();
+ System.err.println("*** server shut down");
+ }));
+ healthMgr.setStatus("", ServingStatus.SERVING);
+ }
+
+ private void stop() {
+ if (server != null) {
+ healthMgr.clearStatus("");
+ server.shutdown();
+ }
+ }
+
+ private enum AdRequestType {
+ TARGETED,
+ NOT_TARGETED
+ }
+
+ private enum AdResponseType {
+ TARGETED,
+ RANDOM
+ }
+
+ private static class AdServiceImpl extends oteldemo.AdServiceGrpc.AdServiceImplBase {
+
+ private static final String AD_FAILURE = "adFailure";
+ private static final String AD_MANUAL_GC_FEATURE_FLAG = "adManualGc";
+ private static final String AD_HIGH_CPU_FEATURE_FLAG = "adHighCpu";
+ private static final Client ffClient = OpenFeatureAPI.getInstance().getClient();
+
+ private AdServiceImpl() {}
+
+ /**
+ * Retrieves ads based on context provided in the request {@code AdRequest}.
+ *
+ * @param req the request containing context.
+ * @param responseObserver the stream observer which gets notified with the value of {@code
+ * AdResponse}
+ */
+ @Override
+ public void getAds(AdRequest req, StreamObserver<AdResponse> responseObserver) {
+ AdService service = AdService.getInstance();
+
+ // get the current span in context
+ Span span = Span.current();
+ try {
+ List<Ad> allAds = new ArrayList<>();
+ AdRequestType adRequestType;
+ AdResponseType adResponseType;
+
+ Baggage baggage = Baggage.fromContextOrNull(Context.current());
+ MutableContext evaluationContext = new MutableContext();
+ if (baggage != null) {
+ final String sessionId = baggage.getEntryValue("session.id");
+ span.setAttribute("session.id", sessionId);
+ evaluationContext.setTargetingKey(sessionId);
+ evaluationContext.add("session", sessionId);
+ } else {
+ logger.info("no baggage found in context");
+ }
+
+ CPULoad cpuload = CPULoad.getInstance();
+ cpuload.execute(ffClient.getBooleanValue(AD_HIGH_CPU_FEATURE_FLAG, false, evaluationContext));
+
+ span.setAttribute("app.ads.contextKeys", req.getContextKeysList().toString());
+ span.setAttribute("app.ads.contextKeys.count", req.getContextKeysCount());
+ if (req.getContextKeysCount() > 0) {
+ logger.info("Targeted ad request received for " + req.getContextKeysList());
+ for (int i = 0; i < req.getContextKeysCount(); i++) {
+ Collection<Ad> ads = service.getAdsByCategory(req.getContextKeys(i));
+ allAds.addAll(ads);
+ }
+ adRequestType = AdRequestType.TARGETED;
+ adResponseType = AdResponseType.TARGETED;
+ } else {
+ logger.info("Non-targeted ad request received, preparing random response.");
+ allAds = service.getRandomAds();
+ adRequestType = AdRequestType.NOT_TARGETED;
+ adResponseType = AdResponseType.RANDOM;
+ }
+ if (allAds.isEmpty()) {
+ // Serve random ads.
+ allAds = service.getRandomAds();
+ adResponseType = AdResponseType.RANDOM;
+ }
+ span.setAttribute("app.ads.count", allAds.size());
+ span.setAttribute("app.ads.ad_request_type", adRequestType.name());
+ span.setAttribute("app.ads.ad_response_type", adResponseType.name());
+
+ adRequestsCounter.add(
+ 1,
+ Attributes.of(
+ adRequestTypeKey, adRequestType.name(), adResponseTypeKey, adResponseType.name()));
+
+ // Throw 1/10 of the time to simulate a failure when the feature flag is enabled
+ if (ffClient.getBooleanValue(AD_FAILURE, false, evaluationContext) && random.nextInt(10) == 0) {
+ throw new StatusRuntimeException(Status.UNAVAILABLE);
+ }
+
+ if (ffClient.getBooleanValue(AD_MANUAL_GC_FEATURE_FLAG, false, evaluationContext)) {
+ logger.warn("Feature Flag " + AD_MANUAL_GC_FEATURE_FLAG + " enabled, performing a manual gc now");
+ GarbageCollectionTrigger gct = new GarbageCollectionTrigger();
+ gct.doExecute();
+ }
+
+ AdResponse reply = AdResponse.newBuilder().addAllAds(allAds).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ } catch (StatusRuntimeException e) {
+ span.addEvent(
+ "Error", Attributes.of(AttributeKey.stringKey("exception.message"), e.getMessage()));
+ span.setStatus(StatusCode.ERROR);
+ logger.log(Level.WARN, "GetAds Failed with status {}", e.getStatus());
+ responseObserver.onError(e);
+ }
+ }
+ }
+
+ private static final ImmutableListMultimap<String, Ad> adsMap = createAdsMap();
+
+ @WithSpan("getAdsByCategory")
+ private Collection<Ad> getAdsByCategory(@SpanAttribute("app.ads.category") String category) {
+ Collection<Ad> ads = adsMap.get(category);
+ Span.current().setAttribute("app.ads.count", ads.size());
+ return ads;
+ }
+
+ private static final Random random = new Random();
+
+ private List<Ad> getRandomAds() {
+
+ List<Ad> ads = new ArrayList<>(MAX_ADS_TO_SERVE);
+
+ // create and start a new span manually
+ Span span = tracer.spanBuilder("getRandomAds").startSpan();
+
+ // put the span into context, so if any child span is started the parent will be set properly
+ try (Scope ignored = span.makeCurrent()) {
+
+ Collection<Ad> allAds = adsMap.values();
+ for (int i = 0; i < MAX_ADS_TO_SERVE; i++) {
+ ads.add(Iterables.get(allAds, random.nextInt(allAds.size())));
+ }
+ span.setAttribute("app.ads.count", ads.size());
+
+ } finally {
+ span.end();
+ }
+
+ return ads;
+ }
+
+ private static AdService getInstance() {
+ return service;
+ }
+
+ /** Await termination on the main thread since the grpc library uses daemon threads. */
+ private void blockUntilShutdown() throws InterruptedException {
+ if (server != null) {
+ server.awaitTermination();
+ }
+ }
+
+ private static ImmutableListMultimap<String, Ad> createAdsMap() {
+ Ad binoculars =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/2ZYFJ3GM2N")
+ .setText("Roof Binoculars for sale. 50% off.")
+ .build();
+ Ad explorerTelescope =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/66VCHSJNUP")
+ .setText("Starsense Explorer Refractor Telescope for sale. 20% off.")
+ .build();
+ Ad colorImager =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/0PUK6V6EV0")
+ .setText("Solar System Color Imager for sale. 30% off.")
+ .build();
+ Ad opticalTube =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/9SIQT8TOJO")
+ .setText("Optical Tube Assembly for sale. 10% off.")
+ .build();
+ Ad travelTelescope =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/1YMWWN1N4O")
+ .setText(
+ "Eclipsmart Travel Refractor Telescope for sale. Buy one, get second kit for free")
+ .build();
+ Ad solarFilter =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/6E92ZMYYFZ")
+ .setText("Solar Filter for sale. Buy two, get third one for free")
+ .build();
+ Ad cleaningKit =
+ Ad.newBuilder()
+ .setRedirectUrl("/product/L9ECAV7KIM")
+ .setText("Lens Cleaning Kit for sale. Buy one, get second one for free")
+ .build();
+ return ImmutableListMultimap.<String, Ad>builder()
+ .putAll("binoculars", binoculars)
+ .putAll("telescopes", explorerTelescope)
+ .putAll("accessories", colorImager, solarFilter, cleaningKit)
+ .putAll("assembly", opticalTube)
+ .putAll("travel", travelTelescope)
+ // Keep the books category free of ads to ensure the random code branch is tested
+ .build();
+ }
+
+ /** Main launches the server from the command line. */
+ public static void main(String[] args) throws IOException, InterruptedException {
+ // Start the RPC server. You shouldn't see any output from gRPC before this.
+ logger.info("Ad service starting.");
+ final AdService service = AdService.getInstance();
+ service.start();
+ service.blockUntilShutdown();
+ }
+}
diff --git a/src/ad/src/main/java/oteldemo/problempattern/CPULoad.java b/src/ad/src/main/java/oteldemo/problempattern/CPULoad.java
new file mode 100644
index 0000000..178f773
--- /dev/null
+++ b/src/ad/src/main/java/oteldemo/problempattern/CPULoad.java
@@ -0,0 +1,116 @@
+/*
+* Copyright The OpenTelemetry Authors
+* SPDX-License-Identifier: Apache-2.0
+*/
+package oteldemo.problempattern;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import io.grpc.ManagedChannelBuilder;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * This class is designed to simulate a high CPU load scenario.
+ * It contains methods to start and stop a specified number of worker threads designed to
+ * perform CPU-intensive calculations.
+ */
+public class CPULoad {
+ private static final Logger logger = LogManager.getLogger(CPULoad.class.getName());
+ private static final int THREAD_COUNT = 4;
+ private boolean running = false;
+ private final List<Logarithmizer> runningWorkers = new ArrayList<>();
+
+ private static CPULoad instance;
+
+ /**
+ * Singleton pattern to get the instance of CPULoad.
+ * @return The singleton instance of CPULoad.
+ */
+ public static CPULoad getInstance() {
+ if (instance == null) {
+ instance = new CPULoad();
+ }
+ return instance;
+ }
+
+ /**
+ * Starts or stops the CPU load generation based on the input parameter.
+ * If enabled, it launches worker threads. If disabled, it stops any running threads.
+ *
+ * @param enabled Flag to start (true) or stop (false) the CPU load simulation.
+ */
+ public void execute(Boolean enabled) {
+ if (enabled) {
+ logger.info("High CPU-Load problempattern enabled");
+ if (!running) {
+ spawnLoadWorkers(THREAD_COUNT);
+ running = true;
+ }
+ } else {
+ running = false;
+ stopWorkers();
+ }
+ }
+
+ /**
+ * Creates and starts a specified number of Logarithmizer threads to simulate CPU load.
+ *
+ * @param threadCount The number of threads to be started.
+ */
+ private void spawnLoadWorkers(int threadCount) {
+ synchronized(runningWorkers) {
+ for (int i = 0; i < threadCount; i++) {
+ Logarithmizer logarithmizer = new Logarithmizer();
+ Thread thread = new Thread(logarithmizer);
+ thread.setDaemon(true);
+ thread.start();
+ runningWorkers.add(logarithmizer);
+ }
+ }
+ }
+
+ /**
+ * Signals all running Logarithmizer threads to stop and clears the list of running workers.
+ */
+ private void stopWorkers() {
+ synchronized(runningWorkers) {
+ for (Logarithmizer logarithmizer : runningWorkers) {
+ logarithmizer.setShouldRun(false);
+ }
+ runningWorkers.clear();
+ }
+ }
+
+ /**
+ * Inner class representing a worker focused on calculating logarithms to consume CPU resources.
+ */
+ private static class Logarithmizer implements Runnable {
+
+ private volatile boolean shouldRun = true;
+
+ /**
+ * Continuously calculates the logarithm of the current system time until
+ * requested to stop.
+ */
+ @Override
+ public void run() {
+ while (shouldRun) {
+ Math.log(System.currentTimeMillis());
+ }
+ }
+
+ /**
+ * Sets the shouldRun flag to control whether this Logarithmizer should continue
+ * to run.
+ *
+ * @param shouldRun A boolean flag to continue (true) or stop (false) the logarithm computation.
+ */
+ public void setShouldRun(boolean shouldRun) {
+ this.shouldRun = shouldRun;
+ }
+ }
+}
diff --git a/src/ad/src/main/java/oteldemo/problempattern/GarbageCollectionTrigger.java b/src/ad/src/main/java/oteldemo/problempattern/GarbageCollectionTrigger.java
new file mode 100644
index 0000000..aa72bc1
--- /dev/null
+++ b/src/ad/src/main/java/oteldemo/problempattern/GarbageCollectionTrigger.java
@@ -0,0 +1,80 @@
+/*
+* Copyright The OpenTelemetry Authors
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package oteldemo.problempattern;
+
+import java.lang.management.ManagementFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * The GarbageCollectionTrigger class is responsible for triggering manual garbage collection
+* at specified intervals to simulate memory pressure and measure the impact on performance.
+*/
+public class GarbageCollectionTrigger {
+ private static final Logger logger = LogManager.getLogger(GarbageCollectionTrigger.class.getName());
+
+ private final long gc_delay;
+ private final int finalize_delay;
+ private final int maxObjects;
+
+ private long lastGC = 0;
+
+ private final MemoryUtils memUtils;
+
+ /**
+ * Constructs a new GarbageCollectionTrigger with default values.
+ */
+ public GarbageCollectionTrigger() {
+ memUtils = new MemoryUtils(ManagementFactory.getMemoryMXBean());
+ gc_delay = TimeUnit.SECONDS.toMillis(10);
+ finalize_delay = 500;
+ maxObjects = 500000;
+ }
+
+ /**
+ * Triggers manual garbage collection at specified intervals and measures the impact on performance.
+ * It creates Entry objects to fill up memory and initiates garbage collection.
+ */
+ public void doExecute() {
+ if (System.currentTimeMillis() - lastGC > gc_delay) {
+ logger.info("Triggering a manual garbage collection, next one in " + (gc_delay/1000) + " seconds.");
+ // clear old data, we want to clear old Entry objects, because their finalization is expensive
+ System.gc();
+
+ long total = 0;
+ for (int i = 0; i < 10; i++) {
+ while (memUtils.getHeapUsage() < 0.9 && memUtils.getObjectPendingFinalizationCount() < maxObjects) {
+ new Entry();
+ }
+ long start = System.currentTimeMillis();
+ System.gc();
+ total += System.currentTimeMillis() - start;
+ }
+ logger.info("The artificially triggered GCs took: " + total + " ms");
+ lastGC = System.currentTimeMillis();
+ }
+
+ }
+
+ /**
+ * The Entry class represents objects created for the purpose of triggering garbage collection.
+ */
+ private class Entry {
+ /**
+ * Overrides the finalize method to introduce a delay, simulating finalization during garbage collection.
+ *
+ * @throws Throwable If an exception occurs during finalization.
+ */
+ @SuppressWarnings("removal")
+ @Override
+ protected void finalize() throws Throwable {
+ TimeUnit.MILLISECONDS.sleep(finalize_delay);
+ super.finalize();
+ }
+ }
+}
diff --git a/src/ad/src/main/java/oteldemo/problempattern/MemoryUtils.java b/src/ad/src/main/java/oteldemo/problempattern/MemoryUtils.java
new file mode 100644
index 0000000..6b31414
--- /dev/null
+++ b/src/ad/src/main/java/oteldemo/problempattern/MemoryUtils.java
@@ -0,0 +1,65 @@
+/*
+* Copyright The OpenTelemetry Authors
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package oteldemo.problempattern;
+
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/**
+ * This class provides JVM heap related utility methods.
+*/
+public class MemoryUtils {
+
+ private static final Logger logger = LogManager.getLogger(MemoryUtils.class.getName());
+
+ private static final long NO_HEAP_LIMIT = -1;
+
+ private final MemoryMXBean memoryBean;
+
+ /**
+ * @param memoryBean defines which {@link MemoryMXBean} is to use
+ */
+ public MemoryUtils(MemoryMXBean memoryBean) {
+ this.memoryBean = memoryBean;
+ }
+
+
+ /**
+ * @return The current heap usage as a decimal number between 0.0 and 1.0.
+ * That is, if the returned value is 0.85, 85% of the max heap is used.
+ *
+ * If no max heap is set, the method returns -1.0.
+ */
+ public double getHeapUsage() {
+ MemoryUsage heapProps = memoryBean.getHeapMemoryUsage();
+ long heapUsed = heapProps.getUsed();
+ long heapMax = heapProps.getMax();
+
+ if (heapMax == NO_HEAP_LIMIT) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("No maximum heap is set");
+ }
+ return NO_HEAP_LIMIT;
+ }
+
+
+ double heapUsage = (double) heapUsed / heapMax;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Current heap usage is {0} percent" + (heapUsage * 100));
+ }
+ return heapUsage;
+ }
+
+ /**
+ * see {@link MemoryMXBean#getObjectPendingFinalizationCount()}
+ */
+ public int getObjectPendingFinalizationCount() {
+ return memoryBean.getObjectPendingFinalizationCount();
+ }
+}
diff --git a/src/ad/src/main/resources/log4j2.xml b/src/ad/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..db5cb39
--- /dev/null
+++ b/src/ad/src/main/resources/log4j2.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright The OpenTelemetry Authors
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<Configuration status="WARN">
+ <Appenders>
+ <Console name="STDOUT" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="INFO">
+ <AppenderRef ref="STDOUT"/>
+ </Root>
+ </Loggers>
+</Configuration>