diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 3b60c7d..649cb35 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -4,7 +4,7 @@ name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: - branches: ['main', 'feature/**'] + branches: ['main'] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/.gitignore b/.gitignore index 928638e..3d536e8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ yarn-error.log # Apache Jmeter jmeter.log + +# K6 +k6 diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php new file mode 100644 index 0000000..00b294f --- /dev/null +++ b/app/Http/Controllers/Api/ApiController.php @@ -0,0 +1,9 @@ +request->path())) + ->skip(2); + + if ($parts->isNotEmpty()) { + $this->challenge = $parts->shift(); + } + + if ($parts->isNotEmpty()) { + $this->method = $parts->shift(); + } + } + + public function index(): JsonResponse + { + return response()->json([ + 'success' => true, + 'data' => $this->gainsService->getGains( + $this->challenge, + $this->method + ), + ]); + } +} diff --git a/app/Http/Controllers/Api/KataController.php b/app/Http/Controllers/Api/KataController.php new file mode 100644 index 0000000..4bf6740 --- /dev/null +++ b/app/Http/Controllers/Api/KataController.php @@ -0,0 +1,64 @@ +request->path())) + ->skip(2); + + if ($parts->isNotEmpty()) { + $this->challenge = $parts->shift(); + } + + if ($parts->isNotEmpty()) { + $this->method = $parts->shift(); + } + } + + public function index(): JsonResponse + { + // Run the method + if (! is_null($this->method) && ! is_null($this->challenge)) { + return response()->json([ + 'success' => true, + 'data' => $this->kataService->runChallengeMethod( + $this->request, + $this->challenge, + $this->method + ), + ]); + } + + $challenges = $this->kataService->getChallenges(); + + if (! is_null($this->challenge)) { + $challenges = $challenges->filter(fn (string $challenge) => $challenge === $this->challenge); + } + + $challenges = $challenges->map(fn (string $challenge) => [ + 'challenge' => $challenge, + 'methods' => $this->kataService->getChallengeMethods($challenge) + ->filter(fn (string $method) => is_null($this->method) || $method === $this->method) + ->values(), + ])->values(); + + return response()->json([ + 'success' => true, + 'data' => $challenges, + ]); + } +} diff --git a/app/Kata/KataRunner.php b/app/Kata/KataRunner.php index 9954973..533052d 100644 --- a/app/Kata/KataRunner.php +++ b/app/Kata/KataRunner.php @@ -273,18 +273,20 @@ protected function getReportData( if (config('laravel-kata.save-outputs')) { $filePath = sprintf( - 'laravel-kata/%s/result-%s.json', - $this->createdAt->format('Ymd-His'), - Str::slug(implode(' ', [$className, $methodName])), + 'public/gains/%s-%s.json', + $className, + $methodName ); Storage::disk('local')->put($filePath, json_encode($result)); - $this->command?->warn(sprintf('Saved output to %s', $filePath)); + $this->command?->info(sprintf('Saved output to %s', $filePath)); } if (config('laravel-kata.debug-mode')) { - $this->addExitHintsFromViolations($statsBaseline['violations']); - $this->addExitHintsFromViolations($statsBefore['violations']); + $this->addExitHintsFromViolations(array_merge( + $statsBaseline['violations'], + $statsBefore['violations'] + )); } $this->addExitHintsFromViolations($statsRecord['violations']); diff --git a/app/Services/GainsService.php b/app/Services/GainsService.php new file mode 100644 index 0000000..cf4abe3 --- /dev/null +++ b/app/Services/GainsService.php @@ -0,0 +1,82 @@ +kataService->getChallenges() as $challengeName) { + if (! is_null($challenge) && $challengeName !== $challenge) { + continue; + } + + /** @var string $methodName */ + foreach ($this->kataService->getChallengeMethods($challengeName) as $methodName) { + if (! is_null($method) && $methodName !== $method) { + continue; + } + + $gains->push($this->getChallengeMethodGains($challengeName, $methodName)); + } + } + + return $gains; + } + + public function getChallengeMethodGains( + string $challenge, + string $method + ): array { + $errors = []; + $suggestions = []; + $path = sprintf( + 'gains/%s-%s.json', + $challenge, + $method + ); + $gains = Storage::disk('public')->get($path); + + if (is_null($gains)) { + $errors[] = sprintf( + 'Gains file not found (%s)', + asset(sprintf('storage/%s', $path)), + ); + $suggestions[] = 'Run the following command `npm run benchmark:kata`'; + } + + if (! is_null($gains)) { + try { + $gains = json_decode($gains, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + $errors[] = sprintf( + 'Unable to decode file. %s (%s)', + $exception->getMessage(), + asset(sprintf('storage/%s', $path)), + ); + $suggestions[] = 'Run the following command `npm run benchmark:kata`'; + } + } + + return [ + 'success' => empty($errors), + 'gains' => $gains, + 'errors' => $errors, + 'suggestions' => $suggestions, + ]; + } +} diff --git a/app/Services/KataService.php b/app/Services/KataService.php new file mode 100644 index 0000000..951e0fc --- /dev/null +++ b/app/Services/KataService.php @@ -0,0 +1,74 @@ +map(function ($className) { + $classNameParts = explode('\\', $className); + + return array_pop($classNameParts); + }); + } + + public function getChallengeMethods(string $challenge): Collection + { + $class = sprintf( + 'App\\Kata\\Challenges\\%s', + $challenge + ); + + try { + $reflectionClass = new ReflectionClass($class); + } catch (ReflectionException $exception) { + throw new Exception(sprintf( + 'Something bad happened: %s', + $exception->getMessage() + )); + } + + return collect($reflectionClass->getMethods()) + ->filter(fn (ReflectionMethod $method) => $method->class === $class) + ->filter(fn (ReflectionMethod $method) => $method->isPublic()) + ->filter(fn (ReflectionMethod $method) => $method->name !== 'baseline') + ->map(fn ($method) => $method->name) + ->values(); + } + + public function runChallengeMethod(Request $request, string $challenge, string $method): array + { + $className = sprintf( + 'App\\Kata\\Challenges\\%s', + $challenge + ); + + $instance = app($className, [ + 'request' => $request, + ]); + + $outputs = collect(); + $iterations = $request->get('iterations', 1); + foreach (range(1, $iterations) as $iteration) { + $output = $instance->{$method}($iteration); + $outputs->push(json_encode($output)); + } + + $json = $outputs->toJson(); + + return [ + 'outputs_md5' => md5($json), + 'outputs_last_10' => $outputs->take(-10), + 'outputs' => $json, + ]; + } +} diff --git a/astro.config.mjs b/astro.config.mjs index d8b1623..a393905 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -9,14 +9,16 @@ let site = null; // https://astro.build/config let config = { + output: 'server', base: '/laravel-kata', srcDir: './client/src', publicDir: './client/public', outDir: './client/dist', + routes: './src/pages', integrations: [ vue(), svelte() - ] + ], }; if (process.env.CI_MODE === 'local') { diff --git a/bin/k6.js b/bin/k6.js index 6f02ff8..f35477d 100644 --- a/bin/k6.js +++ b/bin/k6.js @@ -1,5 +1,5 @@ import http from "k6/http"; -import { sleep } from 'k6'; +import { sleep, check } from "k6"; /** * Integrate load tests with CI/CD @@ -7,32 +7,88 @@ import { sleep } from 'k6'; * Refs * - https://k6.io/blog/integrating-load-testing-with-circleci/ * - https://circleci.com/developer/orbs/orb/k6io/test + * - https://k6.io/docs/examples/advanced-api-flow/ */ +const CLASS = 'KataChallengeSample'; +const METHOD = 'calculatePi'; +const MODE = 'Before'; +// const MODE = 'Record'; -const URL = 'http://localhost/api/kata/KataChallengeMySQL/orVersusIn'; -// const URL = 'http://localhost/api/kata/KataChallengeMySQLRecord/getCollectionUnique'; +const VUS_DEFAULT = 5; +const VUS_MAX = 200; -export let options = { - vus: 5, +export const options = { + vus: VUS_DEFAULT, stages: [ - { duration: "10s", target: 10 }, - { duration: "30s", target: 20 }, - { duration: "1m", target: 100 }, + { duration: "5s", target: 10 }, + { duration: "1m", target: Math.floor(VUS_MAX / 5) }, + { duration: "3m", target: VUS_MAX }, { duration: "30s", target: 0 }, - ] + ], + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(90) < 400'], + }, + ext: { + loadimpact: { + projectID: 3620115, + name: CLASS, + }, + }, }; +// Placeholder for authentication export function setup() { - return { - 'token': '123456' - } + return { + token: "123456", + }; } -export default function (data) { - http.get(URL); - sleep(1); -} +let iterations = 1; -export function teardown(data) { -// console.log('teardown()'); -} +export default (authToken) => { + const requestConfigWithTag = (tag) => ({ + headers: { + Authorization: `Bearer ${authToken}`, + }, + tags: Object.assign( + {}, + { + name: 'DefaultTag', + }, + tag + ), + tag + }); + + const payload = {}; + + iterations++; + + if (MODE === 'Before') { + const url = `http://localhost/api/kata/${CLASS}/${METHOD}`; + const params = { + iterations + } + const response = http.request('GET', url, params, requestConfigWithTag({ + name: 'Before' + })); + + check(response, { "status is 200": (r) => r.status === 200 }); + } + + if (MODE === 'Record') { + const classRecord = `${CLASS}Record`; + const url = `http://localhost/api/kata/${classRecord}/${METHOD}`; + const params = { + iterations + } + const response = http.request('GET', url, params, requestConfigWithTag({ + name: 'Record' + })); + + check(response, { "status is 200": (r) => r.status === 200 }); + } + + sleep(.300); +}; diff --git a/bin/k6/ewoks.js b/bin/k6/ewoks.js new file mode 100644 index 0000000..0bb9419 --- /dev/null +++ b/bin/k6/ewoks.js @@ -0,0 +1,28 @@ +import http from 'k6/http'; +import { check, sleep } from "k6"; + +const isNumeric = (value) => /^\d+$/.test(value); + +const default_vus = 5; + +const target_vus_env = `${__ENV.TARGET_VUS}`; +const target_vus = isNumeric(target_vus_env) ? Number(target_vus_env) : default_vus; + +export let options = { + stages: [ + // Ramp-up from 1 to TARGET_VUS virtual users (VUs) in 5s + { duration: "5s", target: target_vus }, + + // Stay at rest on TARGET_VUS VUs for 10s + { duration: "10s", target: target_vus }, + + // Ramp-down from TARGET_VUS to 0 VUs for 5s + { duration: "5s", target: 0 } + ] +}; + +export default function () { + const response = http.get("https://swapi.dev/api/people/30/", {headers: {Accepts: "application/json"}}); + check(response, { "status is 200": (r) => r.status === 200 }); + sleep(.300); +}; diff --git a/bin/reset.sh b/bin/reset.sh index 5f942d5..e552b3c 100755 --- a/bin/reset.sh +++ b/bin/reset.sh @@ -19,9 +19,7 @@ Convenient script to ensure environment is ready to go PATH_TO_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" PATH_TO_REPO="$PATH_TO_SCRIPT_DIR/../" -echo "PATH_TO_REPO is '${PATH_TO_REPO}'" -echo "Listing files..." -ls -la $PATH_TO_REPO +# TODO: Check if this breaks the logic! source $PATH_TO_REPO/.env # Always load the example env @@ -37,9 +35,11 @@ if [ "${CI_MODE}" == "railway" ]; then source $PATH_TO_REPO/.env php artisan migrate --seed --no-interaction --force + php artisan storage:link # TODO: Sync main production database to start with some base data - # - Performance, test with some load (exchange rates 20 years back) + # - Performance boost, test with some load (exchange rates 20 years back) + # - Set up shared env in Railway fi if [ "${CI_MODE}" == "circleci" ]; then @@ -54,6 +54,7 @@ if [ "${CI_MODE}" == "circleci" ]; then php artisan migrate:refresh --seed --no-interaction --force php artisan migrate:refresh --database=testing --seed --force --no-interaction + php artisan storage:link fi if [ "${CI_MODE}" == "local" ]; then @@ -64,8 +65,5 @@ if [ "${CI_MODE}" == "local" ]; then docker exec -it kata-mysql mysql -uroot -proot_password -e "GRANT ALL PRIVILEGES ON *.* TO 'sail'@'%'; FLUSH PRIVILEGES;" ./vendor/bin/sail artisan migrate:refresh --seed --force --no-interaction ./vendor/bin/sail artisan migrate:refresh --database=testing --seed --force --no-interaction + # ./vendor/bin/sail artisan storage:link fi - -# TODO: Figure out how to best link storage os agnostic -# Generic commands -# php artisan storage:link diff --git a/client/src/components/GainsCard.svelte b/client/src/components/GainsCard.svelte index 5e890f1..1329e88 100644 --- a/client/src/components/GainsCard.svelte +++ b/client/src/components/GainsCard.svelte @@ -1,9 +1,50 @@
-

Gains Card (

-

TODO: Add score info here...

+ {#if $loading} + Loading... + {:else if $error} + Failed to load + {:else} +

+ + + Gains + + + / {theClass} / {theMethod} +

+ + + + + + + + + + {#each [ + 'outputs_md5', + 'line_count', + 'violations_count', + 'iterations', + 'duration', + ] as field} + + + + + + + {/each} + +
BeforeRecordGains
{field}{$data.data[0].gains.stats.before[field]}{$data.data[0].gains.stats.record[field]}{$data.data[0].gains.stats.record[`${field}_gains_perc`]}
+ {/if}
diff --git a/client/src/components/GainsCards.svelte b/client/src/components/GainsCards.svelte new file mode 100644 index 0000000..ae1d1c2 --- /dev/null +++ b/client/src/components/GainsCards.svelte @@ -0,0 +1,48 @@ + + +
+ {#if $loading} + Loading... + {:else if $error} + Failed to load + {:else} +

+ + + Gains + + +

+ + + + + + + + {#each $data.data as record} + + + + + {/each} + +
Challenge::methodResult
+ + {record.gains.class}::{record.gains.method}() + + + {#if record.gains.stats.record.gains_success} + {Math.round(record.gains.stats.record.gains_perc, 2)}% + {:else} + {Math.round(record.gains.stats.record.gains_perc, 2)}% + {/if} +
+ {/if} +
+ diff --git a/client/src/components/Time.svelte b/client/src/components/Time.svelte deleted file mode 100644 index efad70f..0000000 --- a/client/src/components/Time.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{formatter.format($time)} diff --git a/client/src/pages/gains/[...slug].astro b/client/src/pages/gains/[...slug].astro new file mode 100644 index 0000000..fc48ae0 --- /dev/null +++ b/client/src/pages/gains/[...slug].astro @@ -0,0 +1,13 @@ +--- +import Layout from '../../layouts/Layout.astro'; +import GainsCard from '../../components/GainsCard.svelte'; +const { slug } = Astro.params; + +const slugParts = slug.split('/'); +--- + + +
+ +
+
diff --git a/client/src/pages/index.astro b/client/src/pages/index.astro index fce9349..d54816e 100644 --- a/client/src/pages/index.astro +++ b/client/src/pages/index.astro @@ -1,83 +1,10 @@ --- -import GainsCard from '../components/GainsCard.svelte' import Layout from '../layouts/Layout.astro'; -import Card from '../components/Card.astro'; +import GainsCards from '../components/GainsCards.svelte'; --- - +
-

Welcome to Laravel Kata

-

- To get started, open the directory src/pages in your project.
- Code Challenge: Tweak the "Welcome to Astro" message above. -

- - +
- - diff --git a/client/src/stores/fetch.js b/client/src/stores/fetch.js new file mode 100644 index 0000000..107a66a --- /dev/null +++ b/client/src/stores/fetch.js @@ -0,0 +1,27 @@ +import { writable } from 'svelte/store' + +export default function (url) { + const loading = writable(false) + const error = writable(false) + const data = writable({}) + + async function get() { + loading.set(true) + error.set(false) + try { + const response = await fetch(url, { + headers: { + 'Access-Control-Allow-Origin': '*' + } + }) + data.set(await response.json()) + } catch(e) { + error.set(e) + } + loading.set(false) + } + + get() + + return [ data, loading, error, get] +} diff --git a/config/cors.php b/config/cors.php index 83b3e8f..41d353f 100644 --- a/config/cors.php +++ b/config/cors.php @@ -15,7 +15,11 @@ | */ - 'paths' => ['api/*', 'storage/*', 'sanctum/csrf-cookie'], + 'paths' => [ + 'api/*', + 'storage/**/*', + 'sanctum/csrf-cookie', + ], 'allowed_methods' => ['*'], diff --git a/docker-compose.yml b/docker-compose.yml index 4fc7bec..71e4422 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,15 @@ # For more information: https://laravel.com/docs/sail +# - https://medium.com/@nairgirish100/k6-with-docker-compose-influxdb-grafana-344ded339540 version: '3' + +networks: + laravel-kata: + driver: bridge + laravel-kata-k6: + driver: bridge + laravel-kata-grafana: + driver: bridge + services: laravel.test: container_name: kata-laravel @@ -9,12 +19,10 @@ services: args: CI_MODE: '${CI_MODE}' WWWGROUP: '${WWWGROUP}' - image: sail-8.1/app extra_hosts: - 'host.docker.internal:host-gateway' ports: - '${APP_PORT:-80}:80' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: CI_MODE: '${CI_MODE}' WWWUSER: '${WWWUSER}' @@ -28,6 +36,11 @@ services: depends_on: - mysql - redis + restart: unless-stopped + healthcheck: + test: [ "CMD", "curl", "http://localhost/" ] + retries: 3 + timeout: 5s mysql: container_name: kata-mysql image: 'mysql/mysql-server:latest' @@ -61,11 +74,36 @@ services: test: ["CMD", "redis-cli", "ping"] retries: 3 timeout: 5s -networks: - laravel-kata: - driver: bridge + + influxdb: + container_name: kata-influxdb + image: influxdb:1.8 + networks: + - laravel-kata-k6 + - laravel-kata-grafana + ports: + - "8086:8086" + environment: + - INFLUXDB_DB=k6 + + grafana: + container_name: kata-grafana + image: grafana/grafana:latest + networks: + - laravel-kata-grafana + ports: + - "3001:3000" + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - 'laravel-kata-grafana:/etc/grafana/provisioning/' + volumes: laravel-kata-mysql: driver: local laravel-kata-redis: driver: local + laravel-kata-grafana: + driver: local diff --git a/docker-compose.yml.bck b/docker-compose.yml.bck new file mode 100644 index 0000000..eec5cfe --- /dev/null +++ b/docker-compose.yml.bck @@ -0,0 +1,77 @@ +# For more information: https://laravel.com/docs/sail +version: '3' + +networks: + laravel-kata: + driver: bridge + +services: + laravel.test: + container_name: kata-laravel + build: + context: ./docker/8.1 + dockerfile: Dockerfile + args: + CI_MODE: '${CI_MODE}' + WWWGROUP: '${WWWGROUP}' + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-80}:80' + environment: + CI_MODE: '${CI_MODE}' + WWWUSER: '${WWWUSER}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + volumes: + - '.:/var/www/html' + networks: + - laravel-kata + depends_on: + - mysql + - redis + restart: unless-stopped + healthcheck: + test: [ "CMD", "curl", "http://localhost/" ] + retries: 3 + timeout: 5s + mysql: + container_name: kata-mysql + image: 'mysql/mysql-server:latest' + ports: + - '${FORWARD_DB_PORT:-3306}:3306' + environment: + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: '${DB_ROOT_PASSWORD}' + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + MYSQL_DATABASE: '${DB_DATABASE}' + MYSQL_USER: '${DB_USERNAME}' + MYSQL_PASSWORD: '${DB_PASSWORD}' + volumes: + - 'laravel-kata-mysql:/var/lib/mysql' + networks: + - laravel-kata + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] + retries: 3 + timeout: 5s + redis: + container_name: kata-redis + image: 'redis:alpine' + ports: + - '${FORWARD_REDIS_PORT:-6379}:6379' + volumes: + - 'laravel-kata-redis:/data' + networks: + - laravel-kata + healthcheck: + test: ["CMD", "redis-cli", "ping"] + retries: 3 + timeout: 5s + +volumes: + laravel-kata-mysql: + driver: local + laravel-kata-redis: + driver: local diff --git a/docker/8.1/Dockerfile b/docker/8.1/Dockerfile index 0277709..dd1a89c 100644 --- a/docker/8.1/Dockerfile +++ b/docker/8.1/Dockerfile @@ -14,7 +14,7 @@ ENV TZ=UTC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN apt-get update \ - && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 \ && mkdir -p ~/.gnupg \ && chmod 600 ~/.gnupg \ && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ diff --git a/docker/8.1/supervisord.conf b/docker/8.1/supervisord.conf index 69324a9..176da43 100644 --- a/docker/8.1/supervisord.conf +++ b/docker/8.1/supervisord.conf @@ -9,9 +9,9 @@ pidfile=/var/run/supervisord.pid ; note: probably the fastest dev experience command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 -; roadrunner: prod mode +; roadrunner: prod mode (a lot slower!) ; note: probably the fastest prod experience -; command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=roadrunner --host=0.0.0.0 --rpc-port=6001 --port=80 +; command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --workers=8 --server=roadrunner --host=0.0.0.0 --rpc-port=6001 --port=80 ; roadrunner: dev mode ; note: probably the fastest prod experience diff --git a/grafana/dashboards/performance-test-dasboard.json b/grafana/dashboards/performance-test-dasboard.json new file mode 100644 index 0000000..d01f7e9 --- /dev/null +++ b/grafana/dashboards/performance-test-dasboard.json @@ -0,0 +1,1822 @@ +{ + "__inputs": [ + { + "name": "DS_INFLUXDB", + "label": "InfluxDB", + "description": "", + "type": "datasource", + "pluginId": "influxdb", + "pluginName": "InfluxDB" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.3.2" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph (old)", + "version": "" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "datasource", + "id": "influxdb", + "name": "InfluxDB", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "A dashboard for visualizing results from the k6.io load testing tool, using the InfluxDB exporter", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 2587, + "graphTooltip": 2, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 18, + "panels": [], + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "refId": "A" + } + ], + "title": "Dashboard Row", + "type": "row" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 1 + }, + "hiddenSeries": false, + "id": 1, + "interval": ">1s", + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.3.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Active VUs", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "vus", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Virtual Users", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 1 + }, + "hiddenSeries": false, + "id": 17, + "interval": "1s", + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.3.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Requests per Second", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "http_reqs", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Requests per Second", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 1 + }, + "hiddenSeries": false, + "id": 7, + "interval": "1s", + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": false, + "min": false, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.3.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Num Errors", + "color": "#BF1B00" + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "Num Errors", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "errors", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "count" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Errors Per Second", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 1 + }, + "hiddenSeries": false, + "id": 10, + "interval": ">1s", + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": false, + "min": false, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.3.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Num Errors", + "color": "#BF1B00" + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "$tag_check", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "check" + ], + "type": "tag" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "checks", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Checks Per Second", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 19, + "panels": [], + "repeat": "Measurement", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "refId": "A" + } + ], + "title": "$Measurement", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 9 + }, + "id": 11, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT mean(\"value\") FROM $Measurement WHERE $timeFilter ", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (mean)", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 9 + }, + "id": 14, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT max(\"value\") FROM $Measurement WHERE $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (max)", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 9 + }, + "id": 15, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT median(\"value\") FROM $Measurement WHERE $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (med)", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 9 + }, + "id": 16, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT min(\"value\") FROM $Measurement WHERE $timeFilter and value > 0", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (min)", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 16, + "y": 9 + }, + "id": 12, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT percentile(\"value\", 90) FROM $Measurement WHERE $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (p90)", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 9 + }, + "id": 13, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT percentile(\"value\", 95) FROM $Measurement WHERE $timeFilter ", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (p95)", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "description": "Grouped by 1 sec intervals", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 12 + }, + "height": "250px", + "hiddenSeries": false, + "id": 5, + "interval": ">1s", + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.3.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "max", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "/^$Measurement$/", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT max(\"value\") FROM /^$Measurement$/ WHERE $timeFilter and value > 0 GROUP BY time($__interval) fill(none)", + "rawQuery": true, + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "max" + } + ] + ], + "tags": [] + }, + { + "alias": "p95", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "/^$Measurement$/", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT percentile(\"value\", 95) FROM /^$Measurement$/ WHERE $timeFilter and value > 0 GROUP BY time($__interval) fill(none)", + "rawQuery": true, + "refId": "D", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [ + 95 + ], + "type": "percentile" + } + ] + ], + "tags": [] + }, + { + "alias": "p90", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "/^$Measurement$/", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT percentile(\"value\", 90) FROM /^$Measurement$/ WHERE $timeFilter and value > 0 GROUP BY time($__interval) fill(none)", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [ + "90" + ], + "type": "percentile" + } + ] + ], + "tags": [] + }, + { + "alias": "min", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "/^$Measurement$/", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT min(\"value\") FROM /^$Measurement$/ WHERE $timeFilter and value > 0 GROUP BY time($__interval) fill(none)", + "rawQuery": true, + "refId": "E", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "min" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeRegions": [], + "title": "$Measurement (over time)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 2, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "cards": {}, + "color": { + "cardColor": "rgb(0, 234, 255)", + "colorScale": "sqrt", + "colorScheme": "interpolateRdYlGn", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 12 + }, + "heatmap": {}, + "height": "250px", + "highlightCards": true, + "id": 8, + "interval": ">1s", + "links": [], + "options": { + "calculate": true, + "calculation": { + "yBuckets": { + "mode": "count", + "scale": { + "log": 2, + "type": "log" + } + } + }, + "cellGap": 2, + "cellValues": {}, + "color": { + "exponent": 0.5, + "fill": "rgb(0, 234, 255)", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlGn", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false + }, + "rowsFrame": { + "layout": "auto" + }, + "showValue": "never", + "tooltip": { + "show": true, + "yHistogram": true + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "ms" + } + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB}" + }, + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "http_req_duration", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT \"value\" FROM $Measurement WHERE $timeFilter and value > 0", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "$Measurement (over time)", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "yAxis": { + "format": "ms", + "logBase": 2, + "show": true + } + }, + { + "collapsed": false, + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 20, + "panels": [], + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "17QDCac4z" + }, + "refId": "A" + } + ], + "title": "Dashboard Row", + "type": "row" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "tags": [], + "text": "http_req_duration + http_req_blocked", + "value": [ + "http_req_duration", + "http_req_blocked" + ] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "Measurement", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "http_req_duration", + "value": "http_req_duration" + }, + { + "selected": true, + "text": "http_req_blocked", + "value": "http_req_blocked" + }, + { + "selected": false, + "text": "http_req_connecting", + "value": "http_req_connecting" + }, + { + "selected": false, + "text": "http_req_looking_up", + "value": "http_req_looking_up" + }, + { + "selected": false, + "text": "http_req_receiving", + "value": "http_req_receiving" + }, + { + "selected": false, + "text": "http_req_sending", + "value": "http_req_sending" + }, + { + "selected": false, + "text": "http_req_waiting", + "value": "http_req_waiting" + } + ], + "query": "http_req_duration,http_req_blocked,http_req_connecting,http_req_looking_up,http_req_receiving,http_req_sending,http_req_waiting", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "2022-12-17T23:03:50.548Z", + "to": "2022-12-17T23:08:46.792Z" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "k6 Load Testing Results", + "uid": "Ye2dj-c4k", + "version": 2, + "weekStart": "" +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 03b7c12..7ce29ab 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,11 @@ getPDO(); - // $connection->getDatabaseName(); - // } catch (Exception $exception) { - // return JsonResource::make([ - // 'success' => false, - // 'message' => 'Unable to connect to MySQL', - // 'error' => $exception->getMessage(), - // ]); - // } - - // try { - // Redis::connection()->ping(); - // } catch (Exception $exception) { - // return JsonResource::make([ - // 'success' => false, - // 'message' => 'Unable to connect to Redis', - // 'error' => $exception->getMessage(), - // ]); - // } - - return JsonResource::make([ - 'success' => true, - ]); -}); - -/** - * Get the list of challenges - */ -Route::get('/kata', function (Request $request) { - return JsonResource::make([ - 'success' => true, - 'data' => collect(config('laravel-kata.challenges', [])) - ->map(function ($className) { - $classNameParts = explode('\\', $className); - - return array_pop($classNameParts); - }) - ->toArray(), - ]); -}); - -/** - * Get the list of challenge's methods - */ -Route::get('/kata/{challenge}', function (Request $request, string $challenge) { - $class = sprintf( - 'App\\Kata\\Challenges\\%s', - $challenge - ); + $data = [ + 'api' => true, + 'database' => false, + 'cache' => false, + ]; + $errors = []; + $suggestions = []; try { - $reflectionClass = new ReflectionClass($class); - } catch (ReflectionException $exception) { - throw new Exception(sprintf( - 'Something bad happened: %s', + /** @var MySqlConnection $connection */ + $connection = DB::connection(); + $connection->getPDO(); + $connection->getDatabaseName(); + $data['database'] = true; + } catch (Exception $exception) { + $errors[] = sprintf( + 'Unable to connect to MySQL: %s', $exception->getMessage() - )); + ); + $suggestions[] = 'Run the following command `sail down && sail up -d && npm run reset`'; + $suggestions[] = sprintf( + 'How to test: `docker exec -it kata-mysql mysql -u%s -p%s -e"SELECT User, Host FROM mysql.user;"`', + config('database.connections.mysql.username'), + config('database.connections.mysql.password') + ); } - $data = collect($reflectionClass->getMethods()) - ->filter(fn (ReflectionMethod $method) => $method->class === $class) - ->filter(fn (ReflectionMethod $method) => $method->isPublic()) - ->filter(fn (ReflectionMethod $method) => $method->name !== 'baseline') - ->map(fn ($method) => $method->name) - ->toArray(); + try { + Redis::connection()->ping(); + $data['cache'] = true; + } catch (Exception $exception) { + $errors[] = sprintf( + 'Unable to connect to Redis: %s', + $exception->getMessage() + ); + $suggestions[] = 'Run the following command `sail down && sail up -d && npm run reset`'; + } return JsonResource::make([ - 'success' => true, + 'success' => true, // empty($errors), // Not ready for this yet... 'data' => $data, + 'errors' => $errors, + 'suggestions' => $suggestions, ]); }); -/** - * Hit the challenge's method - */ -Route::get('/kata/{challenge}/{method}', function (Request $request, string $challenge, string $method) { - $data = [ +Route::group([ + 'prefix' => 'kata', + 'namespace' => 'Api', +], function (Router $router) { + $router->get('/', [KataController::class, 'index']); + $router->get('/{challenge}', fn (Request $request, string $challenge) => app(KataController::class, [ + 'request' => $request, + 'challenge' => $challenge, + ])->index() + ); + $router->get('/{challenge}/{method}', fn (Request $request, string $challenge, string $method) => app(KataController::class, [ + 'request' => $request, 'challenge' => $challenge, 'method' => $method, - ]; - - $className = sprintf( - 'App\\Kata\\Challenges\\%s', - $challenge + ])->index() ); +}); - $instance = app($className, [ +/** + * Get the latest gains report data + * + * Note: This is only a workaround for the CORS issue when + * fetching http://localhost/storage/gains/KataChallengeSample-calculatePi.json + */ +Route::group([ + 'prefix' => 'gains', + 'namespace' => 'Api', +], function (Router $router) { + $router->get('/', [GainsController::class, 'index']); + $router->get('/{challenge}', fn (Request $request, string $challenge) => app(GainsController::class, [ 'request' => $request, - ]); - - $data = []; - $iterations = $request->get('iterations', 1); - foreach (range(1, $iterations) as $iteration) { - $data[] = $instance->{$method}($iteration); - } - - return JsonResource::make([ - 'success' => true, - 'data' => $data, - ]); + 'challenge' => $challenge, + ])->index() + ); + $router->get('/{challenge}/{method}', fn (Request $request, string $challenge, string $method) => app(GainsController::class, [ + 'request' => $request, + 'challenge' => $challenge, + 'method' => $method, + ])->index() + ); }); diff --git a/tests/Feature/KataFeatureTest.php b/tests/Feature/KataFeatureTest.php index 2bd2852..7eeeaa0 100644 --- a/tests/Feature/KataFeatureTest.php +++ b/tests/Feature/KataFeatureTest.php @@ -15,12 +15,14 @@ final class KataFeatureTest extends TestCase 'challenges' => [ 'success' => 'boolean', 'data' => 'array', - 'data.0' => 'string', + 'data.0.challenge' => 'string', + 'data.0.methods' => 'array', ], 'challenge' => [ 'success' => 'boolean', 'data' => 'array', - 'data.0' => 'string', + 'data.0.challenge' => 'string', + 'data.0.methods' => 'array', ], ]; @@ -39,7 +41,7 @@ public function test_api_kata_challenges(): array $response->assertStatus(200); $this->assertJsonResponseFormat($response, self::RESPONSE_STRUCTURES['challenges']); - return array_values($response->json('data')); + return collect($response->json('data'))->pluck('challenge')->toArray(); } /** @@ -57,7 +59,7 @@ public function test_api_kata_challenges_challenge(array $challenges): array $response->assertStatus(200); $this->assertJsonResponseFormat($response, self::RESPONSE_STRUCTURES['challenge']); - $challengeMethods[$challenge] = $response->json('data'); + $challengeMethods[$challenge] = collect($response->json('data'))->first()['methods']; } return $challengeMethods;