diff --git a/testing-overhead/build.gradle.kts b/testing-overhead/build.gradle.kts new file mode 100644 index 0000000000..4a85b54380 --- /dev/null +++ b/testing-overhead/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("java") +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.testcontainers:testcontainers:1.15.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.2") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.7.2") + testImplementation("com.squareup.okhttp3:okhttp:4.9.1") + testImplementation("org.jooq:joox:1.6.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2") +} + +tasks { + test { + useJUnitPlatform() + } +} diff --git a/testing-overhead/gradle/wrapper/gradle-wrapper.jar b/testing-overhead/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..e708b1c023 Binary files /dev/null and b/testing-overhead/gradle/wrapper/gradle-wrapper.jar differ diff --git a/testing-overhead/gradle/wrapper/gradle-wrapper.properties b/testing-overhead/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5c19504588 --- /dev/null +++ b/testing-overhead/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionSha256Sum=2debee19271e1b82c6e41137d78e44e6e841035230a1a169ca47fd3fb09ed87b +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/testing-overhead/gradlew b/testing-overhead/gradlew new file mode 100755 index 0000000000..4f906e0c81 --- /dev/null +++ b/testing-overhead/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/testing-overhead/k6/basic.js b/testing-overhead/k6/basic.js new file mode 100644 index 0000000000..e6c4f2de93 --- /dev/null +++ b/testing-overhead/k6/basic.js @@ -0,0 +1,69 @@ +import http from "k6/http"; +import { check } from "k6"; +import names from "./names.js"; + +const baseUri = `http://petclinic:9966/petclinic/api`; + +export default function() { + const specialtiesUrl = `${baseUri}/specialties`; + const specialtiesResponse = http.get(specialtiesUrl); + const specialties = JSON.parse(specialtiesResponse.body); + + // Add a new vet to the list + const newVet = names.randomVet(specialties); + const response = http.post(`${baseUri}/vets`, JSON.stringify(newVet), + { headers: { 'Content-Type': 'application/json' } }); + // we don't guard against dupes, so this could fail on occasion + check(response, { "create vet status 201": (r) => r.status === 201 }); + + // make sure we can fetch that vet back out + const vetId = JSON.parse(response.body).id; + const vetUrl = `${baseUri}/vets/${vetId}` + const vetResponse = http.get(vetUrl); + check(vetResponse, { "fetch vet status 200": r => r.status === 200 }); + + // add a new owner + const newOwner = names.randomOwner(); + const newOwnerResponse = http.post(`${baseUri}/owners`, JSON.stringify(newOwner), + { headers: { 'Content-Type': 'application/json' } }); + check(newOwnerResponse, { "new owner status 201": r => r.status === 201}); + + // make sure we can fetch that owner back out + const ownerId = JSON.parse(newOwnerResponse.body).id; + const ownerResponse = http.get(`${baseUri}/owners/${ownerId}`); + check(ownerResponse, { "fetch new owner status 200": r => r.status === 200}); + const owner = JSON.parse(ownerResponse.body); + + // get the list of all pet types + const petTypes = JSON.parse(http.get(`${baseUri}/pettypes`).body); + const owners = JSON.parse(http.get(`${baseUri}/owners`).body); + + // create a 3 new random pets + const pet1 = names.randomPet(petTypes, owner); + const pet2 = names.randomPet(petTypes, owner); + const pet3 = names.randomPet(petTypes, owner); + + const petsUrl = `${baseUri}/pets`; + const newPetResponses = http.batch([ + ["POST", petsUrl, JSON.stringify(pet1), { headers: { 'Content-Type': 'application/json' } } ], + ["POST", petsUrl, JSON.stringify(pet2), { headers: { 'Content-Type': 'application/json' } } ], + ["POST", petsUrl, JSON.stringify(pet3), { headers: { 'Content-Type': 'application/json' } } ], + ]); + check(newPetResponses[0], { "pet status 201": r => r.status === 201}); + check(newPetResponses[1], { "pet status 201": r => r.status === 201}); + check(newPetResponses[2], { "pet status 201": r => r.status === 201}); + + const responses = http.batch([ + ["GET", `${baseUri}/pets/${JSON.parse(newPetResponses[0].body).id}`], + ["GET", `${baseUri}/pets/${JSON.parse(newPetResponses[1].body).id}`], + ["GET", `${baseUri}/pets/${JSON.parse(newPetResponses[2].body).id}`] + ]); + check(responses[0], { "pet exists 200": r => r.status === 200}); + check(responses[1], { "pet exists 200": r => r.status === 200}); + check(responses[2], { "pet exists 200": r => r.status === 200}); + + + //TODO: Set up a visit or two + //TODO: Fetch out the owner again because their model has been updated. + +}; \ No newline at end of file diff --git a/testing-overhead/k6/names.js b/testing-overhead/k6/names.js new file mode 100644 index 0000000000..ef069762a5 --- /dev/null +++ b/testing-overhead/k6/names.js @@ -0,0 +1,147 @@ + + +// curl 'https://data.muni.org/api/views/a9a7-y93v/rows.json?accessType=DOWNLOAD' | jq '.data | .[] | .[8]' | shuf | head -200 +const petNames = [ + "MARIE", "TELLY", "SLATER", "FINNIGAN", "KANU", "BILLY", "JACKIE", "ASUNA", "HUSTLER", "68 WHISKEY", "FISHBONES", + "SCHNITZEL", "DROOPY", "DANIKIS", "BLACK JACK", "BUBBLEZ", "ZORBA", "RAYNIE MAE", "Q", "ARTEMIS", "SPEEDY", "KRAZEE", + "BADGER", "AVERY", "SINA", "CASSI", "DANIELLE", "BRUCE WAYNE", "J.C.", "ZUNI", "KNOX", "D.O.G.", "HERSHEY", "YARROW", + "DAISEY", "GODIVA", "PHINNEAS", "ROMAC", "WILLIS", "LANI", "SQUALL", "LOU", "PEPE LE PEAU", "CHICKADEE", "JIMMY", + "APPLE", "ROXIE", "DEUCE", "GRIFFEN", "VALENTINO", "RASCAL", "DARCIE", "SAMMI", "B.B.", "FINN", "SHILOH", "ROCKY BLUE", + "CHUY", "MOANA", "LILA BLUEBEL", "PEASLEY", "LAIKA", "INDY", "MAXI", "MELISSA", "WALDO V", "SHASHA", "TAITTI", + "KOTA", "LENNY", "JEZABELLE", "GRUMBLE", "GINNY", "ROSIE", "CODY", "NILLA", "BUTCH", "AJAX", "BELLADONA", "CASH", + "LUKKA", "NERO", "SHERMAN", "LOKI", "BELL A DONNA", "MCGYVER", "MAXIMILLIAN", "MOCA", "LADY", "TATIA", "WARD", + "PESKY", "DELILAH", "SYRAH", "FARRAH", "SHOOTER", "MULDER", "GOOBER", "IRA", "HOOCH", "DENEKI 4", "MUSHU", "SHELDON", "TUBBY", + "BITZER", "MARKER", "KRASH", "SPIRIT", "HERA", "LUCY LU", "BLUE", "STORMY", "NAVI", "MARLI", "CHOCHO", "DYLAN", + "TOBY", "BRYCE", "TIGGER", "MARY ANN", "RIKU", "PHOENIX", "MEKA", "CUDDY", "SABRINA", "DAYTON", "DAKODA", "OGUN", + "SOLAR", "RAJ", "DIESEL", "RIPLEY 2", "CARLIE", "ROSEY", "SWISS", "KILLER", "KENLEY", "FEENA", "PIGLET", + "KEYONA", "CHEWIE", "TEINE", "CORWIN", "FLOPPER", "ADELAIDE", "HUNNY", "GEDEON", "BEE", "TALACHULITNA", "PATTON", "KLOEE", + "TRANGO", "IZZI", "PEPPERMINT", "TILLAMOOK", "QI", "KEESTER BOB", "KAINANI", "ELLERA", "RACHEL", "DUKE", "MAZEL", "PRINCE FREDR", + "KATE", "GHANDI", "DREAM", "NINA INDIANA", "JD", "ZIESA", "SAVAGE", "WRANGLER", "MOZZIE", "OSA", "BRODY JOE", "PI", "TOBO JET", + "KHERNAL", "HOPS", "CHEWEY", "KODIAK", "BARTLETT", "MACKENZIE", "RILEY", "GORDY", "SARGENT", "DIVER", "ATTU", "KETEL", + "MIKKI", "FOSBURY", "BESSA", "DESHKA", "WINCHESTER", "ZOEY MARIE", "AUSTIN", "DJANGO", "KINGSTON", "BLAZE", "SABBATH", + "SPRUCE", +]; + +// curl https://raw.githubusercontent.com/aruljohn/popular-baby-names/master/2017/boy_names_2017.json | jq '.names | .[]' | shuf | head -100 | sed -e "s/$/,/" +const firstNames = [ + "Finley", "Jade", "Kylee", "Kensley", "Kira", "Brenna", "Braelyn", "Marley", "Phoenix", "Raegan", "Phoebe", "Kaya", + "Khloe", "Angelique", "Kiana", "Magdalena", "Allison", "Anahi", "Elisabeth", "Juliet", "Miriam", "Elliott", + "Josie", "Brenda", "Hadassah", "Kathryn", "Nadia", "Cataleya", "Lilah", "Yamileth", "Alayna", "Rowan", "Andi", + "Ember", "Gwendolyn", "Jacqueline", "Cecelia", "Kimora", "Lindsey", "Opal", "Lyric", "Dakota", "Jennifer", "Dorothy", "Nia", + "Gabriela", "Mariana", "Haley", "Maliah", "Kelly", "Sloane", "Danica", "Stevie", "Erica", "Jaycee", "Aliya", "Marissa", "Elise", + "Mila", "Oakley", "Adelaide", "Maci", "Dayana", "Aniyah", "Estrella", "Scarlette", "Joy", "Raina", "Sarai", "Luella", "Averi", + "Rebecca", "Eloise", "June", "Hadleigh", "Lena", "Natasha", "Brynn", "Alexis", "Summer", "Clarissa", "Katelyn", "Alexa", "Louise", + "Marianna", "Elsa", "Kehlani", "Tessa", "Katie", "Paisley", "Jasmine", "Brooklyn", "Aubree", "Averie", "Quinn", "Chloe", + "Paula", "Kassidy", "Alessia", "Juliette", "Aydin", "Lucian", "Bronson", "Ryder", "Felix", "Major", "Darius", "Tony", "Junior", + "William", "Johnny", "Patrick", "Raiden", "Nikolas", "Phillip", "Nickolas", "Markus", "Vicente", "Kody", "Rory", "Julian", "Corbin", + "Benjamin", "Dexter", "Emilio", "Jesse", "Romeo", "August", "Gerald", "Gary", "Terrence", "Troy", "Axl", "Ronnie", + "Eugene", "Foster", "Matthias", "Maurice", "Wells", "Kyler", "Leland", "Elian", "Shaun", "Callan", "Samson", "Hector", "Curtis", + "Allan", "Jaxton", "Edgar", "Jonah", "Raul", "Lawrence", "Jude", "Jamison", "Kaysen", "Colten", "Alan", "Tripp", "Aarav", + "Xander", "Landen", "Caden", "Garrett", "Marcelo", "Sean", "Christian", "Reed", "Vaughn", "Kylen", "Ace", + "Kole", "Kristian", "Francis", "Brysen", "Josiah", "Riley", "Brantley", "Colby", "Sonny", "Braydon", "Conrad", + "Michael", "Trent", "Andres", "Billy", "Luka", "Stanley", "Alden", "Victor", "Axton", "Jamari", "Henry", "Jadiel", "Elliott", + "Kristopher", "Kyrie", "Ruben", "Ahmed", "Theodore" +]; + +// $ curl https://raw.githubusercontent.com/rossgoodwin/american-names/master/surnames.json | jq | shuf | tail -25 +const surnames = [ + "Coomes", "Kasputis", "Eing", "Budro", "Paszkiewicz", "Reichwald", "Mennona", "Esplin", "Trute", "Endlich", + "Kaman", "Coody", "Urish", "Styes", "Balles", "Semanek", "Tes", "Mediano", "Clave", "Beliard", "Christianson", + "Doy", "Bozman", "Waligura", "Templeman", "Gershenson", "Eckberg", "Harader", "Baurer", "Villao", "Decius", + "Marquardt", "Smaha", "Grzych", "Getto", "Wilberger", "Fleites", "Spoerl", "Oliger", "Gramza", "Prillaman", + "Beinlich", "Marzella", "Bota", "Arguilez", "Piotti", "Karri", "Spiropoulos", "Gambhir", "Franchak" +]; + +// source https://github.com/baliw/words/blob/master/adjectives.json +const adj = ["Ablaze", "Abrupt", "Accomplished", "Active", "Adored", "Adulated", "Adventurous", "Affectionate", "Amused", "Amusing", + "Animal-like", "Antique", "Appreciated", "Archaic", "Ardent", "Arrogant", "Astonished", "Audacious", "Authoritative", + "Awestruck", "Beaming", "Bewildered", "Bewitching", "Blissful", "Boisterous", "Booming", "Bouncy", "Breathtaking", + "Bright", "Brilliant", "Bubbling", "Calm", "Calming", "Capricious", "Celestial", "Charming", "Cheerful", "Cherished", + "Chiaroscuro", "Chilled", "Comical", "Commanding", "Companionable", "Confident", "Contentment", "Courage", "Crazy", + "Creepy", "Dancing", "Dazzling", "Delicate", "Delightful", "Demented", "Desirable", "Determined", "Devoted", "Dominant", + "Dramatic", "Drawn out", "Dripping", "Dumbstruck", "Ebullient", "Elated", "Elegant", "Enchanted", "Energetic", + "Enthusiastic", "Ethereal", "Exaggerated", "Exalted", "Expectant", "Expressive", "Exuberant", "Faint", "Fantastical", + "Favorable", "Febrile", "Feral", "Feverish", "Fiery", "Floating", "Flying", "Folksy", "Fond", "Forgiven", "Forgiving", + "Freakin' awesome", "Frenetic", "Frenzied", "Friendly", "Amorous", "From a distance", "Frosted", "Funny", "Furry", + "Galloping", "Gaping", "Gentle", "Giddy", "Glacial", "Gladness", "Gleaming", "Gleeful", "Gorgeous", "Graceful", + "Grateful", "Halting", "Happy", "Haunting", "Heavenly", "Hidden", "High-spirited", "Honor", "Hopeful", "Hopping", + "Humble", "Hushed", "Hypnotic", "Illuminated", "Immense", "Imperious", "Impudent", "In charge", "Inflated", "Innocent", + "Inspired", "Intimate", "Intrepid", "Jagged", "Joking", "Joyful", "Jubilant", "Kindly", "Languid", "Larger than life", + "Laughable", "Lickety-split", "Lighthearted", "Limping", "Linear", "Lively", "Lofty", "Love of", "Lovely", "Lulling", + "Luminescent", "Lush", "Luxurious", "Magical", "Maniacal", "Manliness", "March-like", "Masterful", "Merciful", "Merry", + "Mischievous", "Misty", "Modest", "Moonlit", "Mysterious", "Mystical", "Mythological", "Nebulous", "Nostalgic", "Onfire", + "Overstated", "Paganish", "Partying", "Perfunctory", "Perky", "Perplexed", "Persevering", "Pious", "Playful", + "Pleasurable", "Poignant", "Portentous", "Posh", "Powerful", "Pretty", "Prickly", "Prideful", "Princesslike", "Proud", + "Puzzled", "Queenly", "Questing", "Quiet", "Racy", "Ragged", "Regal", "Rejoicing", "Relaxed", "Reminiscent", + "Repentant", "Reserved", "Resolute", "Ridiculous", "Ritualistic", "Robust", "Running", "Sarcastic", "Scampering", + "Scoffing", "Scurrying", "Sensitive", "Serene", "Shaking", "Shining", "Silky", "Silly", "Simple", "Singing", "Skipping", + "Smooth", "Sneaky", "Soaring", "Sophisticated", "Sparkling", "Spell-like", "Spherical", "Spidery", "Splashing", + "Splendid", "Spooky", "Sprinting", "Starlit", "Starry", "Startling", "Successful", "Summery", "Surprised", + "Sympathetic", "Tapping", "Teasing", "Tender", "Thoughtful", "Thrilling", "Tingling", "Tinkling", "Tongue-in-cheek", + "Totemic", "Touching", "Tranquil", "Treasured", "Trembling", "Triumphant", "Twinkling", "Undulating", "Unruly", + "Urgent", "Veiled", "Velvety", "Victorious", "Vigorous", "Virile", "Walking", "Wild", "Witty", "Wondering", "Zealous", + "Zestful" +]; + +function randItem(list){ + return list[rand(list.length)]; +} + +function rand(max){ + return Math.floor(Math.random() * Math.floor(max)) +} + +function randomVet(specialties) { + const first = randItem(firstNames); + const last = randItem(adj); + const numSpec = rand(specialties.length); + const spec = [...Array(numSpec).keys()].map(n => randItem(specialties)) + return { + firstName: first, + lastName: last, + specialties: spec + }; +} + +function randomOwner(){ + const firstName = randItem(firstNames); + const lastName = randItem(surnames); + return { + "firstName": firstName, + "lastName": lastName, + "address": `${rand(10000)} ${randItem(adj)} Ln.`, + "city": "Anytown", + "telephone": "8005551212", + "pets": [] + }; +} + +function randomPet(types, owner) { + const birthDate = "2020/12/31"; + const petName = ((rand(100) > 50) ? "" : `${randItem(adj)} `) + randItem(petNames); + const typeId = randItem(types).id; + + return { + "birthDate": birthDate, + "id": 0, // one will be chosen for us + "name": petName, + "owner": { + "id": owner.id, + "firstName": null, + "lastName": "", + "address": "", + "city": "", + "telephone": "" + }, + "type": { + "id": typeId + }, + "visits": [] + } + } + +export default { + randomVet, + randomOwner, + randomPet +}; \ No newline at end of file diff --git a/testing-overhead/settings.gradle.kts b/testing-overhead/settings.gradle.kts new file mode 100644 index 0000000000..6abaef4a2e --- /dev/null +++ b/testing-overhead/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "opentelemetry-agent-overhead-test" \ No newline at end of file diff --git a/testing-overhead/src/test/java/io/opentelemetry/OverheadTests.java b/testing-overhead/src/test/java/io/opentelemetry/OverheadTests.java new file mode 100644 index 0000000000..cdb3c95cb1 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/OverheadTests.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import io.opentelemetry.agents.Agent; +import io.opentelemetry.config.Configs; +import io.opentelemetry.config.TestConfig; +import io.opentelemetry.containers.CollectorContainer; +import io.opentelemetry.containers.K6Container; +import io.opentelemetry.containers.PetClinicRestContainer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; + +public class OverheadTests { + + private static final Network NETWORK = Network.newNetwork(); + private static GenericContainer collector; + + @BeforeAll + static void setUp() { + collector = CollectorContainer.build(NETWORK); + collector.start(); + } + + @AfterAll + static void tearDown() { + collector.close(); + } + + @TestFactory + Stream runAllTestConfigurations() { + return Configs.all().map(config -> + dynamicTest(config.getName(), () -> runTestConfig(config)) + ); + } + + void runTestConfig(TestConfig config) { + config.getAgents().forEach(agent -> { + try { + runAppOnce(config, agent); + } catch (Exception e) { + fail("Unhandled exception in " + config.getName(), e); + } + }); + } + + void runAppOnce(TestConfig config, Agent agent) throws Exception { + GenericContainer petclinic = new PetClinicRestContainer(NETWORK, collector, agent).build(); + petclinic.start(); + + GenericContainer k6 = new K6Container(NETWORK, agent, config).build(); + k6.start(); + + // This is required to get a graceful exit of the VM before testcontainers kills it forcibly. + // Without it, our jfr file will be empty. + petclinic.execInContainer("kill", "1"); + while (petclinic.isRunning()) { + TimeUnit.MILLISECONDS.sleep(500); + } + + //TODO: Parse and aggregate the test results. + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/agents/Agent.java b/testing-overhead/src/test/java/io/opentelemetry/agents/Agent.java new file mode 100644 index 0000000000..c0bd6ea901 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/agents/Agent.java @@ -0,0 +1,66 @@ +package io.opentelemetry.agents; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Agent { + + final static String OTEL_LATEST = "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent-all.jar"; + + public final static Agent NONE = new Agent("none", "no agent at all"); + public final static Agent LATEST_RELEASE = new Agent("latest", "latest mainstream release", OTEL_LATEST); + public final static Agent LATEST_SNAPSHOT = new Agent("snapshot", "latest available snapshot version from main"); + + private final String name; + private final String description; + private final URL url; + private final List additionalJvmArgs; + + public Agent(String name, String description) { + this(name, description, null); + } + + public Agent(String name, String description, String url) { + this(name, description, url, Collections.emptyList()); + } + + public Agent(String name, String description, String url, List additionalJvmArgs) { + this.name = name; + this.description = description; + this.url = makeUrl(url); + this.additionalJvmArgs = new ArrayList<>(additionalJvmArgs); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public boolean hasUrl(){ + return url != null; + } + + public URL getUrl() { + return url; + } + + public List getAdditionalJvmArgs() { + return Collections.unmodifiableList(additionalJvmArgs); + } + + private static URL makeUrl(String url) { + try { + if(url == null) return null; + return URI.create(url).toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("Error parsing url", e); + } + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/agents/AgentResolver.java b/testing-overhead/src/test/java/io/opentelemetry/agents/AgentResolver.java new file mode 100644 index 0000000000..fc1566dec5 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/agents/AgentResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.agents; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Optional; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class AgentResolver { + + private final LatestAgentSnapshotResolver snapshotResolver = new LatestAgentSnapshotResolver(); + + public Optional resolve(Agent agent) throws Exception { + if(Agent.NONE.equals(agent)){ + return Optional.empty(); + } + if(Agent.LATEST_SNAPSHOT.equals(agent)){ + return snapshotResolver.resolve(); + } + if(agent.hasUrl()){ + return Optional.of(downloadAgent(agent.getUrl())); + } + throw new IllegalArgumentException("Unknown agent: " + agent); + } + + private Path downloadAgent(URL agentUrl) throws Exception { + if(agentUrl.getProtocol().equals("file")){ + Path source = Path.of(agentUrl.toURI()); + Path result = Paths.get(".", source.getFileName().toString()); + Files.copy(source, result, StandardCopyOption.REPLACE_EXISTING); + return result; + } + Request request = new Request.Builder().url(agentUrl).build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + byte[] raw = response.body().bytes(); + Path path = Paths.get(".", "opentelemetry-javaagent-all.jar"); + Files.write(path, raw, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + return path; + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/agents/LatestAgentSnapshotResolver.java b/testing-overhead/src/test/java/io/opentelemetry/agents/LatestAgentSnapshotResolver.java new file mode 100644 index 0000000000..6333078791 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/agents/LatestAgentSnapshotResolver.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.agents; + +import static org.joox.JOOX.$; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Optional; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.joox.Match; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; + +public class LatestAgentSnapshotResolver { + + private static final Logger logger = LoggerFactory.getLogger(LatestAgentSnapshotResolver.class); + + final static String BASE_URL = "https://oss.sonatype.org/content/repositories/snapshots/io/opentelemetry/javaagent/opentelemetry-javaagent"; + final static String LATEST_SNAPSHOT_META = BASE_URL + "/maven-metadata.xml"; + + Optional resolve() throws IOException { + String version = fetchLatestSnapshotVersion(); + logger.info("Latest snapshot version is {}", version); + String latestFilename = fetchLatestFilename(version); + String url = BASE_URL + "/" + version + "/" + latestFilename; + byte[] jarBytes = fetchBodyBytesFrom(url); + Path path = Paths.get(".", "opentelemetry-javaagent-SNAPSHOT-all.jar"); + Files.write(path, jarBytes, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + return Optional.of(path); + } + + private String fetchLatestFilename(String version) throws IOException { + String url = BASE_URL + "/" + version + "/maven-metadata.xml"; + String body = fetchBodyStringFrom(url); + Document document = $(body).document(); + Match match = $(document).xpath("/metadata/versioning/snapshotVersions/snapshotVersion"); + return match.get().stream() + .filter(elem -> { + String classifier = $(elem).child("classifier").content(); + String extension = $(elem).child("extension").content(); + return "all".equals(classifier) && "jar".equals(extension); + }) + .map(e -> $(e).child("value").content()) + .findFirst() + .map(value -> "opentelemetry-javaagent-" + value + "-all.jar") + .orElseThrow(); + } + + private String fetchLatestSnapshotVersion() throws IOException { + String url = LATEST_SNAPSHOT_META; + String body = fetchBodyStringFrom(url); + Document document = $(body).document(); + Match match = $(document).xpath("/metadata/versioning/latest"); + return match.get(0).getTextContent(); + } + + + private String fetchBodyStringFrom(String url) throws IOException { + return fetchBodyFrom(url).string(); + } + + private byte[] fetchBodyBytesFrom(String url) throws IOException { + return fetchBodyFrom(url).bytes(); + } + + private ResponseBody fetchBodyFrom(String url) throws IOException { + Request request = new Request.Builder().url(url).build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + ResponseBody body = response.body(); + return body; + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/config/Configs.java b/testing-overhead/src/test/java/io/opentelemetry/config/Configs.java new file mode 100644 index 0000000000..7ad32958e4 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/config/Configs.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.config; + +import io.opentelemetry.agents.Agent; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Defines all test configurations + */ +public enum Configs { + + RELEASE(TestConfig.builder() + .name("release") + .description("compares the latest stable release to no agent") + .withAgents(Agent.NONE, Agent.LATEST_RELEASE) + .build() + ), + SNAPSHOT(TestConfig.builder() + .name("snapshot") + .description("compares the latest snapshot to no agent") + .withAgents(Agent.NONE, Agent.LATEST_SNAPSHOT) + .build() + ), + SNAPSHOT_REGRESSION(TestConfig.builder() + .name("snapshot-regression") + .description("compares the latest snapshot to the latest stable release") + .withAgents(Agent.LATEST_RELEASE, Agent.LATEST_SNAPSHOT) + .build() + ) + ; + + public final TestConfig config; + + public static Stream all(){ + return Arrays.stream(Configs.values()).map(x -> x.config); + } + + Configs(TestConfig config) { + this.config = config; + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/config/TestConfig.java b/testing-overhead/src/test/java/io/opentelemetry/config/TestConfig.java new file mode 100644 index 0000000000..4497cbe14c --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/config/TestConfig.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.config; + +import io.opentelemetry.agents.Agent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Defines a test config. + */ +public class TestConfig { + + private final static int DEFAULT_MAX_REQUEST_RATE = 0; // none + private final static int DEFAULT_CONCURRENT_CONNECTIONS = 5; + private final static int DEFAULT_TOTAL_ITERATIONS = 500; + + private final String name; + private final String description; + private final List agents; + private final int maxRequestRate; + private final int concurrentConnections; + private final int totalIterations; + + public TestConfig(Builder builder) { + this.name = builder.name; + this.description = builder.description; + this.agents = Collections.unmodifiableList(builder.agents); + this.maxRequestRate = builder.maxRequestRate; + this.concurrentConnections = builder.concurrentConnections; + this.totalIterations = builder.totalIterations; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List getAgents() { + return Collections.unmodifiableList(agents); + } + + public int getMaxRequestRate() { + return maxRequestRate; + } + + public int getConcurrentConnections() { + return concurrentConnections; + } + + public int getTotalIterations() { + return totalIterations; + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + private String name; + private String description; + private List agents = new ArrayList<>(); + private int maxRequestRate = DEFAULT_MAX_REQUEST_RATE; + private int concurrentConnections = DEFAULT_CONCURRENT_CONNECTIONS; + private int totalIterations = DEFAULT_TOTAL_ITERATIONS; + + Builder name(String name) { + this.name = name; + return this; + } + + Builder description(String description) { + this.description = description; + return this; + } + + Builder withAgents(Agent ...agents) { + this.agents.addAll(Arrays.asList(agents)); + return this; + } + + Builder maxRequestRate(int maxRequestRate) { + this.maxRequestRate = maxRequestRate; + return this; + } + + Builder concurrentConnections(int concurrentConnections) { + this.concurrentConnections = concurrentConnections; + return this; + } + + Builder totalIterations(int totalIterations) { + this.totalIterations = totalIterations; + return this; + } + + TestConfig build(){ + return new TestConfig(this); + } + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/containers/CollectorContainer.java b/testing-overhead/src/test/java/io/opentelemetry/containers/CollectorContainer.java new file mode 100644 index 0000000000..78f909c8cc --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/containers/CollectorContainer.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.containers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class CollectorContainer { + + static final int COLLECTOR_PORT = 4317; + static final int COLLECTOR_HEALTH_CHECK_PORT = 13133; + + private static final Logger logger = LoggerFactory.getLogger(CollectorContainer.class); + + public static GenericContainer build(Network network) { + + return new GenericContainer<>( + DockerImageName.parse("otel/opentelemetry-collector-contrib:latest")) + .withNetwork(network) + .withNetworkAliases("collector") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withExposedPorts(COLLECTOR_PORT, COLLECTOR_HEALTH_CHECK_PORT) + .waitingFor(Wait.forHttp("/health").forPort(COLLECTOR_HEALTH_CHECK_PORT)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("collector.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/containers/K6Container.java b/testing-overhead/src/test/java/io/opentelemetry/containers/K6Container.java new file mode 100644 index 0000000000..c8e7b28967 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/containers/K6Container.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.containers; + +import io.opentelemetry.agents.Agent; +import io.opentelemetry.config.TestConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; +import java.time.Duration; + +public class K6Container { + private static final Logger logger = LoggerFactory.getLogger(K6Container.class); + private final Network network; + private final Agent agent; + private final TestConfig config; + + public K6Container(Network network, Agent agent, TestConfig config) { + this.network = network; + this.agent = agent; + this.config = config; + } + + public GenericContainer build(){ + String k6OutputFile = "/results/k6_out_" + agent + ".json"; + return new GenericContainer<>( + DockerImageName.parse("loadimpact/k6")) + .withNetwork(network) + .withNetworkAliases("k6") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forHostPath("./k6"), "/app") + .withFileSystemBind(".", "/results") + .withCommand( + "run", + "-u", String.valueOf(config.getConcurrentConnections()), + "-i", String.valueOf(config.getTotalIterations()), + "--rps", String.valueOf(config.getMaxRequestRate()), + "--summary-export", k6OutputFile, + "/app/basic.js" + ) + .withStartupCheckStrategy( + new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5)) + ); + } +} diff --git a/testing-overhead/src/test/java/io/opentelemetry/containers/PetClinicRestContainer.java b/testing-overhead/src/test/java/io/opentelemetry/containers/PetClinicRestContainer.java new file mode 100644 index 0000000000..c7b3906e00 --- /dev/null +++ b/testing-overhead/src/test/java/io/opentelemetry/containers/PetClinicRestContainer.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.opentelemetry.containers; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.opentelemetry.agents.Agent; +import io.opentelemetry.agents.AgentResolver; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class PetClinicRestContainer { + + private static final Logger logger = LoggerFactory.getLogger(PetClinicRestContainer.class); + private static final int PETCLINIC_PORT = 9966; + private final AgentResolver agentResolver = new AgentResolver(); + + private final Network network; + private final Startable collector; + private final Agent agent; + + public PetClinicRestContainer(Network network, Startable collector, Agent agent) { + this.network = network; + this.collector = collector; + this.agent = agent; + } + + public GenericContainer build() throws Exception { + + Optional agentJar = agentResolver.resolve(this.agent); + + GenericContainer container = new GenericContainer<>( + DockerImageName.parse("ghcr.io/open-telemetry/opentelemetry-java-instrumentation/petclinic-rest-base:latest")) + .withNetwork(network) + .withNetworkAliases("petclinic") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withExposedPorts(PETCLINIC_PORT) + .withFileSystemBind(".", "/results") + .waitingFor(Wait.forHttp("/petclinic/actuator/health").forPort(PETCLINIC_PORT)) + .dependsOn(collector) + .withCommand(buildCommandline(agentJar)); + + agentJar.ifPresent( + agentPath -> container.withCopyFileToContainer( + MountableFile.forHostPath(agentPath), + "/app/" + agentPath.getFileName().toString()) + ); + return container; + } + + @NotNull + private String[] buildCommandline(Optional agentJar) { + String jfrFile = "petclinic-" + this.agent.getName() + ".jfr"; + List result = new ArrayList<>(Arrays.asList( + "java", + "-XX:StartFlightRecording:dumponexit=true,disk=true,settings=profile,name=petclinic,filename=/results/" + + jfrFile, + "-Dotel.traces.exporter=otlp", + "-Dotel.imr.export.interval=5000", + "-Dotel.exporter.otlp.insecure=true", + "-Dotel.exporter.otlp.endpoint=http://collector:4317", + "-Dotel.resource.attributes=service.name=petclinic-otel-overhead" + )); + result.addAll(this.agent.getAdditionalJvmArgs()); + agentJar.ifPresent(path -> result.add("-javaagent:/app/" + path.getFileName())); + + result.add("-jar"); + result.add("/app/spring-petclinic-rest.jar"); + return result.toArray(new String[] {}); + } +} diff --git a/testing-overhead/src/test/resources/collector.yaml b/testing-overhead/src/test/resources/collector.yaml new file mode 100644 index 0000000000..1dcc1ba28d --- /dev/null +++ b/testing-overhead/src/test/resources/collector.yaml @@ -0,0 +1,33 @@ +extensions: + health_check: + +receivers: + otlp: + protocols: + grpc: + +processors: + batch: + +exporters: + logging/logging_debug: + loglevel: debug + logging/logging_info: + loglevel: info + +service: + pipelines: + traces: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging/logging_debug ] + metrics: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging/logging_info ] + logs: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging/logging_debug ] + + extensions: [ health_check ]