From 82e03978b89938219958032efb1448cc76baa181 Mon Sep 17 00:00:00 2001 From: Saumit Date: Sat, 27 Sep 2025 02:14:26 +0530 Subject: Initial snapshot - OpenTelemetry demo 2.1.3 -f --- src/ad/.java-version | 1 + src/ad/Dockerfile | 35 +++ src/ad/README.md | 41 +++ src/ad/build.gradle | 143 +++++++++ src/ad/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43504 bytes src/ad/gradle/wrapper/gradle-wrapper.properties | 7 + src/ad/gradlew | 252 ++++++++++++++++ src/ad/gradlew.bat | 94 ++++++ src/ad/settings.gradle | 2 + src/ad/src/main/java/oteldemo/AdService.java | 328 +++++++++++++++++++++ .../main/java/oteldemo/problempattern/CPULoad.java | 116 ++++++++ .../problempattern/GarbageCollectionTrigger.java | 80 +++++ .../java/oteldemo/problempattern/MemoryUtils.java | 65 ++++ src/ad/src/main/resources/log4j2.xml | 18 ++ 14 files changed, 1182 insertions(+) create mode 100644 src/ad/.java-version create mode 100644 src/ad/Dockerfile create mode 100644 src/ad/README.md create mode 100644 src/ad/build.gradle create mode 100644 src/ad/gradle/wrapper/gradle-wrapper.jar create mode 100644 src/ad/gradle/wrapper/gradle-wrapper.properties create mode 100755 src/ad/gradlew create mode 100644 src/ad/gradlew.bat create mode 100644 src/ad/settings.gradle create mode 100644 src/ad/src/main/java/oteldemo/AdService.java create mode 100644 src/ad/src/main/java/oteldemo/problempattern/CPULoad.java create mode 100644 src/ad/src/main/java/oteldemo/problempattern/GarbageCollectionTrigger.java create mode 100644 src/ad/src/main/java/oteldemo/problempattern/MemoryUtils.java create mode 100644 src/ad/src/main/resources/log4j2.xml (limited to 'src/ad') 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 +``` + +## 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 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 Binary files /dev/null and b/src/ad/gradle/wrapper/gradle-wrapper.jar 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 adRequestTypeKey = + AttributeKey.stringKey("app.ads.ad_request_type"); + private static final AttributeKey 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 responseObserver) { + AdService service = AdService.getInstance(); + + // get the current span in context + Span span = Span.current(); + try { + List 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 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 adsMap = createAdsMap(); + + @WithSpan("getAdsByCategory") + private Collection getAdsByCategory(@SpanAttribute("app.ads.category") String category) { + Collection ads = adsMap.get(category); + Span.current().setAttribute("app.ads.count", ads.size()); + return ads; + } + + private static final Random random = new Random(); + + private List getRandomAds() { + + List 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 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 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.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 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 @@ + + + + + + + + + + + + + + + -- cgit v1.2.3