Skip to content

feat(benchmarks): replace wrk with k6, add APISIX comparative baseline#82

Merged
OliverWolffGIP merged 18 commits into
mainfrom
feature/plan-04b-comparative-benchmark
Jul 20, 2026
Merged

feat(benchmarks): replace wrk with k6, add APISIX comparative baseline#82
OliverWolffGIP merged 18 commits into
mainfrom
feature/plan-04b-comparative-benchmark

Conversation

@OliverWolffGIP

@OliverWolffGIP OliverWolffGIP commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces wrk with k6 as the sole benchmark framework for API Sheriff and stands up
an on-demand, never-CI-bound comparative baseline against Apache APISIX on the same
upstreams, Keycloak realm, and TLS certs. Doc-first: doc/plan/04b-comparative-benchmark.adoc
records the k6-over-wrk decision, the aspect matrix, upload-size rationale, and fairness
rules; doc/plan/README.adoc links 04b and moves the benchmark-convention wording from
wrk/lua to k6.

Changes

  • Docs: added doc/plan/04b-comparative-benchmark.adoc; updated doc/plan/README.adoc,
    benchmarks/README.adoc (via test/resource sweep), doc/LogMessages.adoc,
    doc/configuration.adoc, doc/security-threat-model.adoc, and
    doc/adr/0011-gatewayyaml_exposes_JWKS_trust_and_egress_as_neutral_names_bound_by_the_deployment.adoc.
  • k6 migration: added K6BenchmarkConverter, K6BenchmarkLogMessages,
    K6ResultPostProcessor, and ComparisonSummaryWriter under
    benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/, wired through the
    existing BenchmarkResultProcessor / gh-pages flow unchanged.
  • k6 scripts: added benchmarks/src/main/resources/k6-scripts/ (gateway_health,
    health_live, bearer_proxied, proxied_static, http2, graphql, upload_small,
    upload_large, plus lib/summary.js, lib/target.js, and
    pre-benchmark-health-check.sh), migrating all four existing wrk benchmarks and
    replacing the custom error-rate gate with native k6 thresholds.
  • wrk removal: deleted benchmarks/Dockerfile.wrk and all
    benchmarks/src/main/resources/wrk-scripts/* (sh + lua) and their associated wrk
    benchmark tests/fixtures; replaced with benchmarks/Dockerfile.k6 and
    benchmarks/Dockerfile.k6-dashboard.
  • Tests: added K6BenchmarkConverterTest and ComparisonSummaryWriterTest using
    known-numbers k6 summary fixtures (k6-summary-gateway-health.json,
    k6-summary-health-live.json); removed the superseded wrk result-processor tests.
  • APISIX comparison stack: added integration-tests/docker-compose.apisix.yml,
    integration-tests/src/main/docker/apisix/{apisix.yaml,config.yaml,render-tls-and-start.sh},
    and a benchmark Keycloak realm (integration-tests/src/main/docker/keycloak/benchmark-realm.json)
    with route-for-route parity against the same upstreams/TLS certs as API Sheriff
    (openid-connect bearer_only, graphql POST, upload routes), plus pre-provisioned
    (behavior-inert) ws/grpc routes for roadmap 05.
  • Comparison harness: added benchmarks/scripts/run-comparison.sh — a
    target-neutral, never-CI-wired entry point (gateway_target recorded in result
    metadata) that runs the full aspect matrix or a named subset against whichever stack
    is started, producing a per-aspect side-by-side summary kept segregated from CI
    baselines/pages.
  • Build: updated benchmarks/pom.xml and .github/workflows/benchmark.yml for the
    k6-based benchmark toolchain; updated pom.xml.
  • Gateway/config: updated IssuerConfig, TokenValidatorProducer,
    JwksTrustProfileResolver, DispatchStage, GatewayEdgeRoute, application.properties,
    and schema/gateway.schema.json (plus their tests) supporting the APISIX-parity route
    configuration and TLS/JWKS trust setup exercised by the comparison stack.

Test Plan

  • Verification command passed (verify -Ppre-commit)
  • Manual testing completed (if applicable)

Related Issues

N/A


Generated by plan-finalize skill

Summary by CodeRabbit

  • New Features
    • Added per-issuer JWKS HTTP egress allowlists and configurable TLS trust profiles.
    • Introduced k6-based integration benchmarks (health, bearer, HTTP/2, GraphQL, uploads) and on-demand gateway comparison runs with Markdown summaries and optional HTML dashboards.
  • Bug Fixes
    • Improved fast-fail behavior for misconfigured or unavailable TLS trust profiles.
    • Hardened gateway dispatch error handling for safer shutdown scenarios.
  • Documentation
    • Updated configuration, schema, security, benchmark methodology, and benchmark/logging documentation for the k6 migration.
  • Tests
    • Expanded unit/integration coverage for JWKS allowlist and trust-profile behavior.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @OliverWolffGIP, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@OliverWolffGIP, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c01c3507-44d6-4175-9bfe-83e592a5c69a

📥 Commits

Reviewing files that changed from the base of the PR and between bace022 and 68d2bbb.

📒 Files selected for processing (68)
  • .github/workflows/benchmark.yml
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/JwksTrustProfileResolver.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/config/model/IssuerConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/edge/DispatchStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/resources/application.properties
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/JwksTrustProfileResolverTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TestTlsConfigurationRegistry.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TokenValidatorProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/config/load/ConfigLoaderTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/config/model/ConfigModelContractTest.java
  • benchmarks/Dockerfile.k6
  • benchmarks/Dockerfile.k6-dashboard
  • benchmarks/Dockerfile.wrk
  • benchmarks/README.adoc
  • benchmarks/pom.xml
  • benchmarks/scripts/run-comparison.sh
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkLogMessages.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6ResultPostProcessor.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/package-info.java
  • benchmarks/src/main/resources/k6-scripts/bearer_proxied.js
  • benchmarks/src/main/resources/k6-scripts/gateway_health.js
  • benchmarks/src/main/resources/k6-scripts/graphql.js
  • benchmarks/src/main/resources/k6-scripts/health_live.js
  • benchmarks/src/main/resources/k6-scripts/http2.js
  • benchmarks/src/main/resources/k6-scripts/lib/summary.js
  • benchmarks/src/main/resources/k6-scripts/lib/target.js
  • benchmarks/src/main/resources/k6-scripts/pre-benchmark-health-check.sh
  • benchmarks/src/main/resources/k6-scripts/proxied_static.js
  • benchmarks/src/main/resources/k6-scripts/upload_large.js
  • benchmarks/src/main/resources/k6-scripts/upload_small.js
  • benchmarks/src/main/resources/wrk-scripts/api_health_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/api_health_check.lua
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_check.lua
  • benchmarks/src/main/resources/wrk-scripts/health_live_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/health_live_check.lua
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_check.lua
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriterTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/BearerProxiedRouteBenchmarkTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/GatewayEndpointBenchmarkTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/WrkResultPostProcessorTest.java
  • benchmarks/src/test/resources/k6-summary-gateway-health.json
  • benchmarks/src/test/resources/k6-summary-health-live.json
  • benchmarks/src/test/resources/wrk-api-health-results.txt
  • benchmarks/src/test/resources/wrk-health-results.txt
  • doc/LogMessages.adoc
  • doc/adr/0011-gatewayyaml_exposes_JWKS_trust_and_egress_as_neutral_names_bound_by_the_deployment.adoc
  • doc/adr/0012-Comparative_cross-gateway_benchmarking_runs_on-demand_and_stays_segregated_from_the_CI_baseline.adoc
  • doc/configuration.adoc
  • doc/plan/04b-comparative-benchmark.adoc
  • doc/plan/README.adoc
  • doc/security-threat-model.adoc
  • integration-tests/docker-compose.apisix.yml
  • integration-tests/docker-compose.yml
  • integration-tests/src/main/docker/apisix/apisix.yaml
  • integration-tests/src/main/docker/apisix/config.yaml
  • integration-tests/src/main/docker/apisix/render-tls-and-start.sh
  • integration-tests/src/main/docker/keycloak/benchmark-realm.json
  • integration-tests/src/main/docker/sheriff-config/endpoints/httpbin.yaml
  • integration-tests/src/main/docker/sheriff-config/gateway.yaml
  • pom.xml
📝 Walkthrough

Walkthrough

The pull request adds JWKS egress and TLS trust-profile configuration, eagerly validates token configuration at startup, and replaces WRK integration benchmarks with k6. It also adds k6 result processing, comparative APISIX benchmarking, matching integration topology, tests, documentation, and CI updates.

Changes

Security configuration and token validation

Layer / File(s) Summary
JWKS trust and egress wiring
api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/*, api-sheriff/src/main/java/de/cuioss/sheriff/api/config/*, api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/*
JWKS configuration supports host-exact egress allowlists and logical TLS profiles, with eager startup validation and CONFIG_INVALID failures for unresolved or unloadable trust material.

k6 benchmark migration

Layer / File(s) Summary
k6 benchmark engine and scripts
benchmarks/Dockerfile.k6*, benchmarks/pom.xml, benchmarks/src/main/resources/k6-scripts/*, .github/workflows/benchmark.yml
Benchmark execution moves from WRK to k6 scripts, native thresholds, readiness checks, and expanded CI coverage.
k6 result conversion and comparison
benchmarks/src/main/java/de/cuioss/sheriff/api/k6/*, benchmarks/scripts/run-comparison.sh, benchmarks/src/test/java/de/cuioss/sheriff/api/k6/*
k6 JSON summaries are converted into existing report models, processed for published metrics, and rendered into comparative Markdown output with missing values shown as n/a.

Comparison integration topology

Layer / File(s) Summary
APISIX comparison topology
integration-tests/docker-compose*.yml, integration-tests/src/main/docker/apisix/*, integration-tests/src/main/docker/sheriff-config/*, integration-tests/src/main/docker/keycloak/*
An on-demand APISIX overlay, mirrored routes, TLS rendering, HTTP/2 configuration, upload routes, GraphQL routes, and benchmark issuer setup are added.

Documentation and edge contracts

Layer / File(s) Summary
Configuration and benchmark contracts
doc/*, benchmarks/README.adoc
Architecture, configuration, security, logging, benchmark methodology, comparison lanes, and k6 migration behavior are documented.
Edge exception boundaries
api-sheriff/src/main/java/de/cuioss/sheriff/api/edge/*
Dispatch failure comments are expanded and executor rollback handling is narrowed to rejected execution failures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • cuioss/API-Sheriff#60: Introduces the configuration model and schema extended here with JWKS egress and TLS profile fields.
  • cuioss/API-Sheriff#76: Adds the token validator wiring further extended here for JWKS egress and trust-profile handling.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: migrating benchmarks from wrk to k6 and adding an APISIX comparative baseline.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the benchmark toolchain from wrk to k6, introducing a target-neutral harness and a comparative baseline against Apache APISIX. It adds support for logical trust profiles (tls_profile) and SSRF egress allowlists (allowed_egress_hosts) in gateway.yaml to securely handle JWKS validation. The wrk-based post-processing is replaced with K6ResultPostProcessor and K6BenchmarkConverter, and a ComparisonSummaryWriter is added to generate side-by-side markdown summaries. The review feedback focuses on improving the robustness of JSON parsing across these new post-processing utilities by catching syntax exceptions and verifying that parsed JSON elements are primitives before extracting them as strings.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
benchmarks/README.adoc (1)

243-301: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the working directory for executing the comparison.

The documentation instructs the user to run the comparison from the benchmarks/ directory (scripts/run-comparison.sh), but later provides a Maven command (./mvnw -pl benchmarks ...) that must be executed from the repository root. Executing ./mvnw from inside a submodule typically fails, as the wrapper is usually located at the project root.

Consider updating the instructions to use a consistent execution context (e.g., executing everything from the repository root, or adjusting the Maven command to ../mvnw if the user is expected to remain in the benchmarks/ directory).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/README.adoc` around lines 243 - 301, Clarify the working directory
in the comparison instructions so both scripts/run-comparison.sh and the
ComparisonSummaryWriter Maven command are executable from the same location.
Update the commands and surrounding text consistently, ensuring the Maven
wrapper path and -pl benchmarks invocation resolve correctly from that
directory.
benchmarks/pom.xml (1)

231-451: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Eight near-identical docker compose run k6 run /scripts/*.js executions — consider a driver script.

Each block only varies by script path and (once) VU override. A single shell script (in the same spirit as pre-benchmark-health-check.sh) that loops over an aspect list and invokes docker compose run per entry would collapse ~220 lines of repeated XML into one execution plus a short script, and adding a new aspect would become a one-line change instead of a new 26-line block.

♻️ Sketch of the consolidation
# benchmarks/src/main/resources/k6-scripts/run-benchmark-aspects.sh
aspects="health_live gateway_health proxied_static bearer_proxied http2 graphql upload_small upload_large"
for aspect in $aspects; do
  vus="$BENCHMARK_VUS"
  [ "$aspect" = "upload_large" ] && vus="$BENCHMARK_VUS_UPLOAD_LARGE"
  BENCHMARK_VUS="$vus" docker compose run --rm k6 run "/scripts/${aspect}.js"
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/pom.xml` around lines 231 - 451, Consolidate the repeated k6
executions in the Maven configuration into one driver execution that invokes a
new run-benchmark-aspects.sh script. Have the script iterate over health_live,
gateway_health, proxied_static, bearer_proxied, http2, graphql, upload_small,
and upload_large, passing the standard VU value except using the upload-large
override for upload_large, while preserving the existing compose working
directory, duration, output directory, and timeout behavior.
benchmarks/scripts/run-comparison.sh (1)

110-117: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Normalize OUTPUT_DIR to an absolute path before using it as a bind-mount source.

The default is absolute, but a relative --output (e.g. ./results) flows into -v "${target_dir}:/results" at Line 147. Docker treats a non-absolute bind source inconsistently, so a relative --output can silently fail to mount the results directory. Resolve it once after arg parsing.

♻️ Suggested normalization
 mkdir -p "${OUTPUT_DIR}"
+OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)"
 echo "Comparison run -> ${OUTPUT_DIR}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/scripts/run-comparison.sh` around lines 110 - 117, Normalize
OUTPUT_DIR to an absolute path immediately after argument parsing and before
creating directories or constructing bind-mount paths. Ensure relative --output
values such as ./results are resolved once, then reuse the normalized value when
computing target_dir and passing it to Docker.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java`:
- Around line 81-83: Update onStartup in TokenValidatorProducer to explicitly
invoke the injected TokenValidator, forcing gatewayTokenValidator() and its
tls_profile validation during application startup rather than merely resolving
the normal-scoped proxy. Add a `@QuarkusTest` that boots with an unknown
tls_profile and verifies startup fails.

In `@benchmarks/Dockerfile.k6-dashboard`:
- Around line 8-40: Remove the custom builder stage and all
xk6-dashboard-related arguments, package installation, and build commands from
the Dockerfile. Use the stock digest-pinned grafana/k6:2.1.0 image directly, and
configure the dashboard through K6_WEB_DASHBOARD=true and
K6_WEB_DASHBOARD_EXPORT instead of linking an extension.

---

Nitpick comments:
In `@benchmarks/pom.xml`:
- Around line 231-451: Consolidate the repeated k6 executions in the Maven
configuration into one driver execution that invokes a new
run-benchmark-aspects.sh script. Have the script iterate over health_live,
gateway_health, proxied_static, bearer_proxied, http2, graphql, upload_small,
and upload_large, passing the standard VU value except using the upload-large
override for upload_large, while preserving the existing compose working
directory, duration, output directory, and timeout behavior.

In `@benchmarks/README.adoc`:
- Around line 243-301: Clarify the working directory in the comparison
instructions so both scripts/run-comparison.sh and the ComparisonSummaryWriter
Maven command are executable from the same location. Update the commands and
surrounding text consistently, ensuring the Maven wrapper path and -pl
benchmarks invocation resolve correctly from that directory.

In `@benchmarks/scripts/run-comparison.sh`:
- Around line 110-117: Normalize OUTPUT_DIR to an absolute path immediately
after argument parsing and before creating directories or constructing
bind-mount paths. Ensure relative --output values such as ./results are resolved
once, then reuse the normalized value when computing target_dir and passing it
to Docker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3928edde-f2fc-4571-9172-a92abd74c209

📥 Commits

Reviewing files that changed from the base of the PR and between 33899ad and 731f92b.

📒 Files selected for processing (66)
  • .github/workflows/benchmark.yml
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/JwksTrustProfileResolver.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/config/model/IssuerConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/edge/DispatchStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/resources/application.properties
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/JwksTrustProfileResolverTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TestTlsConfigurationRegistry.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TokenValidatorProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/config/load/ConfigLoaderTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/config/model/ConfigModelContractTest.java
  • benchmarks/Dockerfile.k6
  • benchmarks/Dockerfile.k6-dashboard
  • benchmarks/Dockerfile.wrk
  • benchmarks/README.adoc
  • benchmarks/pom.xml
  • benchmarks/scripts/run-comparison.sh
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkLogMessages.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6ResultPostProcessor.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/package-info.java
  • benchmarks/src/main/resources/k6-scripts/bearer_proxied.js
  • benchmarks/src/main/resources/k6-scripts/gateway_health.js
  • benchmarks/src/main/resources/k6-scripts/graphql.js
  • benchmarks/src/main/resources/k6-scripts/health_live.js
  • benchmarks/src/main/resources/k6-scripts/http2.js
  • benchmarks/src/main/resources/k6-scripts/lib/summary.js
  • benchmarks/src/main/resources/k6-scripts/lib/target.js
  • benchmarks/src/main/resources/k6-scripts/pre-benchmark-health-check.sh
  • benchmarks/src/main/resources/k6-scripts/proxied_static.js
  • benchmarks/src/main/resources/k6-scripts/upload_large.js
  • benchmarks/src/main/resources/k6-scripts/upload_small.js
  • benchmarks/src/main/resources/wrk-scripts/api_health_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/api_health_check.lua
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_check.lua
  • benchmarks/src/main/resources/wrk-scripts/health_live_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/health_live_check.lua
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_check.lua
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriterTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/BearerProxiedRouteBenchmarkTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/GatewayEndpointBenchmarkTest.java
  • benchmarks/src/test/resources/k6-summary-gateway-health.json
  • benchmarks/src/test/resources/k6-summary-health-live.json
  • benchmarks/src/test/resources/wrk-api-health-results.txt
  • benchmarks/src/test/resources/wrk-health-results.txt
  • doc/LogMessages.adoc
  • doc/adr/0011-gatewayyaml_exposes_JWKS_trust_and_egress_as_neutral_names_bound_by_the_deployment.adoc
  • doc/configuration.adoc
  • doc/plan/04b-comparative-benchmark.adoc
  • doc/plan/README.adoc
  • doc/security-threat-model.adoc
  • integration-tests/docker-compose.apisix.yml
  • integration-tests/docker-compose.yml
  • integration-tests/src/main/docker/apisix/apisix.yaml
  • integration-tests/src/main/docker/apisix/config.yaml
  • integration-tests/src/main/docker/apisix/render-tls-and-start.sh
  • integration-tests/src/main/docker/keycloak/benchmark-realm.json
  • integration-tests/src/main/docker/sheriff-config/endpoints/httpbin.yaml
  • integration-tests/src/main/docker/sheriff-config/gateway.yaml
  • pom.xml
💤 Files with no reviewable changes (13)
  • benchmarks/src/main/resources/wrk-scripts/health_live_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/api_health_benchmark.sh
  • benchmarks/Dockerfile.wrk
  • benchmarks/src/test/resources/wrk-health-results.txt
  • benchmarks/src/test/resources/wrk-api-health-results.txt
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_check.lua
  • benchmarks/src/main/resources/wrk-scripts/api_health_check.lua
  • benchmarks/src/main/resources/wrk-scripts/health_live_check.lua
  • benchmarks/src/main/resources/wrk-scripts/bearer_proxied_benchmark.sh
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_benchmark.sh
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/BearerProxiedRouteBenchmarkTest.java
  • benchmarks/src/main/resources/wrk-scripts/proxied_static_check.lua
  • benchmarks/src/test/java/de/cuioss/sheriff/api/wrk/benchmark/GatewayEndpointBenchmarkTest.java

Comment thread benchmarks/Dockerfile.k6-dashboard
OliverWolffGIP added a commit that referenced this pull request Jul 20, 2026
onStartup observed StartupEvent with the @ApplicationScoped validator as a
lazy CDI client proxy, so ArC never invoked gatewayTokenValidator() at boot —
tls_profile resolution was deferred to the first bearer request instead of
failing startup. Invoke a method on the injected proxy to force
contextual-instance creation at boot, and correct the misleading comment.
Add onStartup unit tests proving the validator is now dereferenced.

Addresses PR #82 CodeRabbit review (TokenValidatorProducer.java:81-83).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hh2epJFjdLAt2wZyxEeh3p
OliverWolffGIP added a commit that referenced this pull request Jul 20, 2026
…primitive JSON

Three PR #82 review fixes in the k6 post-processing harness:

- ComparisonSummaryWriter.readTargetSummaries now wraps each per-file parse in
  try/catch (JsonSyntaxException | IllegalStateException), logging the bad file
  via FAILED_PARSE_SUMMARY and continuing, so one truncated summary cannot abort
  the whole comparison (Gemini).
- K6ResultPostProcessor.stringOrNull/instantOrNull now guard with isJsonPrimitive()
  before getAsString(), so a present-but-non-primitive metadata field yields the
  absent-field path instead of an uncaught UnsupportedOperationException (Gemini).
- K6BenchmarkConverter.requiredString and the gateway_target read now guard with
  isJsonPrimitive(): a non-primitive required field is treated as missing and a
  non-primitive gateway_target falls back to "unknown" (Gemini).

Adds unit coverage for the malformed-skip and non-primitive-guard branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hh2epJFjdLAt2wZyxEeh3p

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java (1)

90-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Suppress PMD warning for intentional proxy initialization.

Invoking validator.toString() correctly forces the Quarkus ArC client proxy to initialize the underlying bean at startup, satisfying the fail-fast requirement. However, this triggers a UselessPureMethodCall PMD warning since the return value is ignored.

Consider suppressing this warning on the method to keep the static analysis build clean while preserving the eager-initialization behavior.

💡 Proposed fix
+    `@SuppressWarnings`("PMD.UselessPureMethodCall")
     void onStartup(`@Observes` StartupEvent event, `@GatewayValidator` TokenValidator validator) {
         // Invoke a method on the injected proxy to force contextual-instance creation at boot;
         // without this the `@ApplicationScoped` validator stays unassembled until the first request.
         validator.toString();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java`
around lines 90 - 94, Suppress PMD’s UselessPureMethodCall warning on the
onStartup method, preserving the intentional validator.toString() invocation
that eagerly initializes the proxy at startup. Do not remove or replace the
call.

Source: Linters/SAST tools

benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java (2)

298-332: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin down what happens to the malformed record itself.

The test only asserts goodRun survives; it never asserts whether badTime (non-primitive start_time) ends up present with a fallback or is dropped entirely. That leaves the actual contract for non-primitive metadata fields unverified — add an assertion on benchmarkNamed(json, "badTime") (either assertNotNull/assertNull, or a check on its metadata value) so a future regression in that fallback path is caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java`
around lines 298 - 332, Add an assertion in
nonPrimitiveMetadataFieldIsSkippedWithoutCrashing for benchmarkNamed(json,
"badTime") that verifies the intended contract for a non-primitive
start_time—either its presence with the expected fallback metadata value or its
complete omission. Keep the existing goodRun assertion and processing behavior
unchanged.

210-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared helper for inline k6 summary fixtures.

Five test methods repeat the same k6Dir = tempDir.resolve(...)Files.createDirectories(k6Dir)Files.writeString(k6Dir.resolve(name), """...""") sequence, and the "good-summary.json" text block at Lines 267-276 is duplicated verbatim at Lines 305-314. Extracting a small writeSummary(String fileName, String json) helper (mirroring copyFixtureToK6Dir) would remove the boilerplate and the exact-duplicate literal.

♻️ Example helper
+    private void writeSummary(String fileName, String json) throws IOException {
+        Path k6Dir = tempDir.resolve(K6ResultPostProcessor.K6_RESULTS_SUBDIRECTORY);
+        Files.createDirectories(k6Dir);
+        Files.writeString(k6Dir.resolve(fileName), json);
+    }

Also applies to: 233-259, 261-296, 298-332, 334-370

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java`
around lines 210 - 231, Extract a private writeSummary(String fileName, String
json) helper in K6BenchmarkConverterTest that resolves the k6 results directory,
creates it, and writes the named summary file. Replace the repeated setup in all
five affected test methods, including both duplicated good-summary.json
fixtures, with this helper while preserving each test’s existing JSON content
and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java`:
- Around line 90-94: Suppress PMD’s UselessPureMethodCall warning on the
onStartup method, preserving the intentional validator.toString() invocation
that eagerly initializes the proxy at startup. Do not remove or replace the
call.

In
`@benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java`:
- Around line 298-332: Add an assertion in
nonPrimitiveMetadataFieldIsSkippedWithoutCrashing for benchmarkNamed(json,
"badTime") that verifies the intended contract for a non-primitive
start_time—either its presence with the expected fallback metadata value or its
complete omission. Keep the existing goodRun assertion and processing behavior
unchanged.
- Around line 210-231: Extract a private writeSummary(String fileName, String
json) helper in K6BenchmarkConverterTest that resolves the k6 results directory,
creates it, and writes the named summary file. Replace the repeated setup in all
five affected test methods, including both duplicated good-summary.json
fixtures, with this helper while preserving each test’s existing JSON content
and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3cafdc4a-bb91-4f57-84d5-9285a459342f

📥 Commits

Reviewing files that changed from the base of the PR and between 731f92b and bace022.

📒 Files selected for processing (7)
  • api-sheriff/src/main/java/de/cuioss/sheriff/api/auth/TokenValidatorProducer.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TokenValidatorProducerTest.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6ResultPostProcessor.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriterTest.java
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverterTest.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • benchmarks/src/test/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriterTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/api/auth/TokenValidatorProducerTest.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/ComparisonSummaryWriter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6BenchmarkConverter.java
  • benchmarks/src/main/java/de/cuioss/sheriff/api/k6/benchmark/K6ResultPostProcessor.java

OliverWolffGIP and others added 18 commits July 20, 2026 19:14
Author doc/plan/04b-comparative-benchmark.adoc following the existing
doc/plan/*.adoc convention. It records the eight decisions this plan is
built on:

- D1 k6 replaces wrk, because wrk cannot serve plan 05's WebSocket/gRPC
  obligation; Gatling rejected on footprint, Fortio on capability;
  cross-gateway numbers are relative, not absolute.
- D2 the six-aspect matrix (unauth, bearer, http2, graphql, upload 1MB,
  upload 50MB), with ws/grpc pre-provisioned as declarations and deferred
  to plan 05.
- D3 the upload-size rationale: 1MB measures the streaming path as a
  request rate, 50MB measures transfer capacity at reduced concurrency.
- D4 the fairness rules for the comparison lane.
- D5 the K6BenchmarkConverter upstreaming proposal to benchmarking-common,
  explicitly a cross-repo follow-up and not implemented here.
- D6 the one-time GitHub Pages baseline restart.
- D7 the CI baseline coverage decision, taking -Pbenchmark from 3 wired
  executions today to 8, with the accepted runtime cost.
- D8 the xk6-dashboard report as a comparison-run-only supplementary
  artifact.

Update doc/plan/README.adoc: add the 04b row to the Plan Sequence table
sequenced after 04, and reword the forward-looking benchmark convention
to name k6 scripts plus native thresholds instead of wrk lua/sh. Plan
01's "a proxied-route WRK benchmark" phrase is left intact as a
historical record of what plan 01 delivered.

Co-Authored-By: Claude <noreply@anthropic.com>
Implement the k6 side of the BenchmarkConverter seam so the existing
report pipeline (badges, 10-run history, trends, GitHub Pages) consumes
k6 runs unchanged. No new Maven dependency: benchmarking-common is
already on the benchmarks classpath.

K6BenchmarkConverter maps a k6 handleSummary() export onto BenchmarkData
-- throughput score, scoreUnit, and the 50/75/90/99 latency percentiles.
k6 reports http_req_duration in milliseconds, so no unit conversion is
performed and none can regress. Only measured percentiles are emitted;
absent ones are never estimated.

K6ResultPostProcessor is the main-entry replacement for the wrk-era
processor and preserves its downstream sequence exactly, including the
exact-match Prometheus benchmark-name lookup (the nondeterministic
bidirectional contains() fallback is deliberately not reintroduced).
Summaries are read from a k6 subdirectory, mirroring the wrk contract,
and execution windows come from the summary JSON rather than a text
metadata block. A missing or empty k6 directory fails loud, so an empty
report is never published.

Two findings surfaced while pinning the tests, both worth recording:

- The report pipeline's convertBenchmarks() whitelists the fields it
  emits and drops additionalData entirely. Recording gateway_target only
  there would never reach a report, so the target is also encoded into
  the serialized fullName (k6.<target>.<name>). name stays the bare
  benchmark name, so the gh-pages history and trend keys are unaffected.
- The k6 lane uses the general MetricConversionUtil.formatThroughput
  rather than the wrk-branded formatter. These agree above 2K but differ
  below it ("1.50K" vs "1.5K"); the one-time gh-pages baseline restart
  means no historical series depends on the old rendering.

The k6-specific log records live in a local K6Benchmark-prefixed
catalogue, since every upstream counterpart is wrk-branded and would
emit a factually wrong message for a k6 run; toolchain-neutral messages
still reuse the upstream Benchmarking catalogue. Documented in
doc/LogMessages.adoc.

Tests pin concrete P50/P75/P90/P99 values and formatted scores against
known-numbers classpath fixtures, carrying over the outgoing wrk suite's
value-asserting discipline plus its full gh-pages structure and
missing-input checks. 20 tests green in the benchmarks module.

Co-Authored-By: Claude <noreply@anthropic.com>
Give the k6 aspect matrix a working API Sheriff side, so the on-demand
APISIX comparison has a route-for-route counterpart to measure against.

The load-bearing change is the second token_validation issuer. The
bearer aspect mints its token from the Keycloak 'benchmark' realm, whose
import pins frontendUrl https://keycloak:8443 — so a minted token
carries iss https://keycloak:8443/realms/benchmark regardless of the
host-published 1443 mint port. gateway.yaml previously declared only the
unrelated it-static issuer, so every bearer request would have been
rejected 401 and the aspect would have measured the rejection path while
reporting it as bearer-validation throughput. A rejection is *faster*
than a validation, so that failure presents as flatteringly good numbers
rather than as an error. The new benchmark-keycloak issuer declares that
exact container-internal iss value and fetches JWKS over the shared
api-sheriff network. it-static is retained untouched: BearerValidationIT's
rejection scenarios resolve against it and stay green.

Also added:
- graphql and upload anchors (POST), the latter with max_body_bytes at
  64 MiB so the 50 MB upload aspect is measured rather than rejected at
  the edge; APISIX mirrors the same limit.
- tls.alpn advertising h2 alongside http/1.1, so the http2 aspect
  negotiates HTTP/2 instead of silently falling back to HTTP/1.1.
- ws and grpc anchors as declarations only — no behaviour; the pipeline
  still boot-rejects those protocols and roadmap plan 05 owns them.
- graphql and the two upload routes on the existing go-httpbin endpoint,
  each overriding the endpoint's default anchor and allow-listing
  Content-Type (the forward stage is deny-by-default, so without it the
  POST body would not be echoed as sent). go-httpbin's /anything/* echo
  accepts POST bodies, so no new upstream service is needed.

497 unit tests green. Note the end-to-end assertion that a benchmark-realm
token returns 200 on /secure requires the running container stack and is
not covered by this build.

Co-Authored-By: Claude <noreply@anthropic.com>
… toolchain

Replace the wrk/lua toolchain with k6 as the sole benchmark framework. The four
benchmark names (healthLiveCheck, gatewayHealth, proxiedStatic, bearerProxied)
are preserved verbatim so no gh-pages history or trend series is orphaned.

- benchmarks/pom.xml: swap the wrk.* property block for k6.* (duration, vus,
  script.dir, results.dir, output.dir); retarget the three benchmark executions
  at `docker compose run --rm k6 run` against the new .js scripts, renamed
  run-k6-*; repoint the post-processing mainClass at K6ResultPostProcessor.
- Add run-k6-bearer-proxied-benchmark. This is new CI coverage, not a retarget:
  the wrk-era bearer_proxied_benchmark.sh was registered in no Maven execution,
  so -Pbenchmark now carries four wired executions (3 today + 1 added).
- Add the four k6 scripts plus the shared lib/summary.js handleSummary() helper
  emitting the cuioss summary format K6BenchmarkConverter consumes. Native k6
  thresholds (http_req_failed ceiling + a checks rate gate) replace the custom
  WRK_MAX_ERROR_RATE lua gate wholesale, including its strict validation of a
  set-but-invalid override.
- bearer_proxied.js mints one token from the Keycloak benchmark realm in setup()
  and replays it, so the run measures gateway validation rather than Keycloak
  issuance. Its checks threshold asserts HTTP 200 explicitly, so a 401-only run
  fails the build instead of reporting the fast rejection path as a good result.
- Add the digest-pinned benchmarks/Dockerfile.k6 (grafana/k6 2.1.0) and replace
  the wrk compose service with a k6 service carrying the results bind mount.
- Relocate pre-benchmark-health-check.sh from wrk-scripts/ to k6-scripts/.
- Delete the whole wrk toolchain: four runner scripts, four lua gates,
  Dockerfile.wrk, WrkResultPostProcessor, its three test classes and the two
  wrk parser fixtures. The tests' coverage is not dropped -- it is carried by
  K6BenchmarkConverterTest against known-numbers k6 fixtures.

Known behavioural change: latency_ms.stdev is absent from k6 summaries because
k6 exposes no standard deviation for a trend metric and the samples are not
reachable from handleSummary(). The field is omitted rather than fabricated, so
the report's error / variability / confidence-band columns are zero-width for
k6 runs. The D6 one-time baseline restart means no historical series depends on
the wrk-era values.

The single sanctioned surviving wrk token is the upstream
PrometheusMetricsManager.collectMetricsForWrkBenchmark(...) call site (D5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…for-route parity

Adds docker-compose.apisix.yml as a third compose overlay (not a self-contained
stack: nginx-static is defined only in docker-compose.benchmark.yml and the
api-sheriff network is Compose-project-scoped). The comparison lane is one
three-file merge behind a dedicated 'comparison' profile, so the default stack
and verify -Pintegration-tests start no APISIX service.

apisix.yaml mirrors the deliverable-4 API Sheriff routes one-for-one: unauth
proxied, bearer, graphql POST, and the two upload routes sharing the same
64 MiB body cap, plus ws/grpc as declarations only. The bearer route uses
openid-connect in bearer_only mode with use_jwks: true so it validates offline
against the same Keycloak benchmark realm -- without use_jwks the plugin falls
back to introspection and would measure a per-request Keycloak round-trip.

TLS could not be configured by file path as originally scoped. Verified against
the pinned image's own sources: apisix/cli/ops.lua overwrites the configured
apisix.ssl.ssl_cert paths with a built-in placeholder on every start, and
apisix/init.lua's ssl_client_hello phase aborts any handshake matching no 'ssls'
entry, whose cert/key fields accept inline PEM only (this build resolves no
$env:// references). render-tls-and-start.sh therefore splices the mounted
certificate into the route table at start-up, keeping
src/main/docker/certificates the single source of truth rather than committing
the private key a second time.

Verified against a live stack: /proxy/static, /graphql, /upload/small and
/upload/large all return 200, /secure returns 401 without a token and 200 with a
valid benchmark-realm token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…or upstream parity

The APISIX route table sent the graphql and upload aspects to go-httpbin while
API Sheriff sent them to nginx-static (the benchmark overlay repoints the
UPSTREAM alias for the whole endpoint, not just /proxy). That asymmetry measured
go-httpbin's JSON serialization as APISIX latency and biased both aspects in API
Sheriff's favour, violating the spec's SAME-upstreams fairness requirement.

Repoint graphql-post, upload-small and upload-large at static-upstream so both
gateways hit the identical backend, and drop the now-unused echo-upstream and the
dead go-httpbin depends_on edge. The proxy-rewrite entries are kept because API
Sheriff rewrites the same way and nginx-static answers 200 on any path/method.

Consequence: the graphql and upload aspects measure gateway processing rather
than upstream work, which is the benchmark's stated intent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
The benchmark aspect's `benchmark-keycloak` issuer fetches its JWKS from
https://keycloak:8443, a compose service name that resolves to the bridge
network's site-local 172.x address. token-sheriff's EgressPolicy.secureDefault()
refuses exactly that, so the key set never loaded and every bearer request
failed — the aspect would have measured a rejection path while reporting it as
bearer-validation throughput.

Adds the narrow widening the threat model already prescribes (GW-05 / BFF-07:
"keep the secure egress defaults; widen only via allowedEgressHost for a trusted
IdP") as a per-issuer config key:

- gateway.schema.json: `allowed_egress_hosts` on the jwks object, which is
  additionalProperties: false — without the schema entry the key is a boot
  rejection, not a silent no-op.
- IssuerConfig.Jwks: `List<String> allowedEgressHosts`, normalized to List.of()
  when absent.
- TokenValidatorProducer: each entry forwarded to
  HttpJwksLoaderConfigBuilder.allowedEgressHost(String). When the list is empty
  no egress builder method is called at all, so the config keeps
  EgressPolicy.secureDefault().

Secure by default is asserted, not assumed: tests cover absent and empty lists
(structurally equal to secureDefault(), and behaviourally still refusing a
loopback-resolving JWKS URL), host-exactness (allowlisting one host does not
exempt another), multi-entry application, YAML binding through the SNAKE_CASE
strategy, and schema rejection of a non-array value.

Only the `benchmark-keycloak` issuer is widened. `it-static` is deliberately
untouched — it loads from a mounted file and needs no egress. No default or
production config is widened.

Scope: this is an accepted scope deviation. It was escalated before any code was
written (the originally-planned sheriff.token.issuers.* property keys would have
been a silent no-op, since the gateway drives issuers from its own YAML via
@GatewayValidator, not the extension's cui.jwt.* surface) and the operator chose
this option, logged as (scope-deviation:accept).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…d by deployment

The JWKS client verified the IdP's certificate against the JVM default trust
store, with no way to change that from configuration. An IdP behind an internal
or corporate CA — or a cluster-internal IdP using the cluster's own authority —
therefore failed PKIX path building, the key set never loaded, and every bearer
request was rejected. That is a production gap, not a test gap: together with
the egress default (which refuses private addresses), API Sheriff could only
validate tokens from a public-internet IdP, which is backwards for a gateway
that normally sits inside the same private network as its IdP.

Adds `token_validation.issuers[].jwks.tls_profile`: the name of a logical trust
profile in API Sheriff's own vocabulary.

The design is a mapping, deliberately not a direct reference:

- gateway.yaml and gateway.schema.json name a trust profile and nothing else —
  no truststore path, no password, no runtime construct. Letting the document
  reference a runtime concept directly would make the product's configuration
  language depend on the identity of the framework beneath it, so a change of
  runtime would invalidate documents operators wrote and committed.
- JwksTrustProfileResolver is the single seam that knows the binding. It maps
  the logical name to the runtime's named TLS configuration and hands the
  resulting SSLContext to HttpJwksLoaderConfigBuilder.sslContext(...). No other
  class knows the mapping exists; TokenValidatorProducer deals only in the
  logical name.
- Trust material and passwords stay on the deployment side on purpose: secrets
  never enter gateway.yaml, and keystore loading/rotation is not reimplemented.

Boundary rule — config neutral, diagnostics concrete. The schema and the
document never name the runtime; the startup error does, because an operator
who names an unbound profile needs the exact knob, not an abstraction:

  Issuer 'X' names jwks.tls_profile 'P' but no such trust profile is
  configured — define it via quarkus.tls.P.trust-store.*

An unresolvable profile fails startup rather than falling back to default
trust — a silent fallback would either surface later as an obscure fetch error
or succeed against anchors nobody intended. The validator is now assembled
eagerly at boot so that failure lands at startup, matching ConfigProducer's
existing refuse-to-start posture.

Absent tls_profile preserves current behaviour exactly: no SSL context is set
and the client keeps the JVM default trust store. Asserted, not assumed —
tests cover absent (identity-compared against a defined profile's context, so
the assertion cannot pass vacuously), the resolver never being consulted when
absent, applied-and-identical when named, unresolvable, unloadable material,
co-existence with allowed_egress_hosts, YAML binding, and schema rejection of
a non-string value. The registry double is hand-written; no mocking framework.

doc/configuration.adoc carries the full mapping: what tls_profile means, how a
name resolves to concrete anchors, a worked example, the failure mode and its
message, the rationale for the indirection, and how it differs from
allowed_egress_hosts ("may I connect?" vs "do I trust what I get back?").

ADR-0011 records the config-surface decision, why a direct runtime reference
was rejected, the boundary rule, and the deferred knobs — several of which
(connect/read timeouts, retryConfig, refreshIntervalSeconds, keyRotation
GracePeriod, maxRetiredKeySets) are production-significant and currently
unconfigurable; refresh interval is effectively fixed at 600s.

Benchmark stack wiring:
- gateway.yaml names tls_profile benchmark-idp on benchmark-keycloak only;
  it-static is untouched (file-sourced, no egress, no TLS).
- application.properties binds that name under the `it` profile to the
  already-mounted localhost truststore.
- benchmark-realm.json gains an oidc-sub-mapper on the openid client scope.
  The realm's clientScopes replace Keycloak's built-ins, so the `basic` scope
  that normally supplies `sub` was absent and every benchmark token was
  rejected for a missing mandatory claim — the same silent-wrong-result class
  as the TLS failure: the aspect would measure a rejection path while
  reporting it as bearer-validation throughput.

Verified against the running stack (native runner and image rebuilt):
JWKS now loads over TLS with no handshake error, and a benchmark-realm token
returns 200 on /secure while an unauthenticated request still returns 401
(previously 401/401).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…f recipe

The OpenRewrite UseMapOf recipe rewrote the additionalData LinkedHashMap
into new HashMap<>(Map.of(...)) without adding the java.util.HashMap
import, leaving the tree broken for the next compile. Pre-commit itself
passed because the rewrite lands after the module compiles, so the
breakage only surfaced on the following build and would have broken CI.

Constructing the map with Map.of directly satisfies the recipe, so there
is nothing left to rewrite -- the defect class is removed rather than
suppressed. Ordering is provably irrelevant here: the map is never
iterated or mutated after construction, and convertBenchmarks() drops
additionalData entirely downstream.

The percentiles LinkedHashMap at line 227 is retained, where insertion
order is meaningful.

Resolves finding c0d56f.
…path

Three InvalidExceptionUsageRecipe markers sat on HEAD in the request dispatch
path. CLAUDE.md forbids committing code with markers present. Each was judged
on its merits rather than blanket-suppressed:

- GatewayEdgeRoute#handle (executor dispatch): NARROWED to
  RejectedExecutionException. Executor#execute contracts this as its only
  failure -- the submitted command is a non-null lambda literal, so the NPE arm
  is unreachable, and process(ctx) runs on the virtual thread rather than
  inline, so no pipeline exception can surface here. Behaviour-preserving.

- GatewayEdgeRoute#process (pipeline tail): SUPPRESSED. A deliberate
  last-resort per-request safety net over a dozen independent stages; the
  failure type is genuinely unknowable. Narrowing would abandon the response
  without ending it (client hangs until timeout) and strand the admission
  permit, since the end handler never fires.

- DispatchStage#guardedDispatch: SUPPRESSED. Guard#call propagates the checked
  Exception declared by the Callable it wraps and the method declares no throws
  clause, so a narrower catch would not compile. It is also the single point
  translating any guard failure into a mapped GatewayException.

Both suppressions carry a rationale comment and sit before the enclosing try,
which is where the recipe actually honours them -- placing them inside the
preceding catch block left the markers being re-inserted on the next run.

Verified: verify -Ppre-commit BUILD SUCCESS with zero markers re-added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…arison entry point

Deliverable 6 (TASK-008, TASK-009).

Harness:
- lib/target.js resolves GATEWAY_TARGET to a base URL, defaulting to API Sheriff
  so the CI lane needs no plumbing. An unknown target is fatal rather than
  defaulted: silently falling back would label an API Sheriff run as the other
  gateway, and mislabelled comparison data is worse than a failed run.
- lib/summary.js now sources gatewayTarget() from target.js (single owner) and
  grows an opt-in throughput_mbps derived from k6's own data_sent rate, so a
  partially-transferred body cannot be counted as fully delivered bytes.
- proxied_static.js / bearer_proxied.js resolve their host via target.js.
  health_live.js and gateway_health.js deliberately stay API-Sheriff-bound:
  they measure the Quarkus management port and /api/health, which APISIX does
  not expose, so making them 'neutral' would point them at absent routes.

New matrix aspects (graphql, upload_small 1MB, upload_large 50MB, http2), each
declaring native k6 thresholds. Two anti-silent-wrong-result gates: http2.js
asserts the negotiated protocol per response, because k6 falls back to HTTP/1.1
transparently and a fallback would file HTTP/1.1 numbers under the h2 aspect;
upload_large.js asserts HTTP 200 so an undersized body cap fails the run rather
than reporting the faster rejection path as excellent throughput.

upload_large runs at reduced concurrency (5 VUs). Every pom execution passes
BENCHMARK_VUS explicitly, so a dedicated k6.vus.upload.large property is what
actually keeps the shared k6.vus (50) from overriding the script default --
otherwise the aspect would measure link saturation and the generator's own
per-VU 50MB payload memory instead of gateway transfer cost.

run-comparison.sh is the single on-demand entry point, registered in no Maven
profile and referenced by no workflow (verified). It writes to a results root
segregated from both the CI baseline and the gh-pages tree, and closes the
aspect matrix to the six comparable aspects so the API-Sheriff-only health
benchmarks can never be driven under a non-default target label.

ComparisonSummaryWriter renders the per-aspect side-by-side table, selecting
MBps for the transfer-bound uploadLarge aspect and RPS elsewhere, with P50/P99
per gateway. An unmeasured metric renders n/a, never 0, so an absent
measurement is not read as a measured collapse. Formatting is pinned to
Locale.ROOT so a German-locale run cannot emit decimal commas into the
artifact. CI lane now wires eight benchmark executions.

Verified: verify -pl benchmarks -am BUILD SUCCESS; 16 tests green (7 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…artifact

Deliverable 7 (TASK-010).

Dockerfile.k6-dashboard is a SECOND image beside Dockerfile.k6, never a
replacement: xk6-dashboard is a k6 output extension and k6 loads output
extensions only when compiled in, so the HTML report requires rebuilding k6
with the extension linked.

Engine parity is the load-bearing constraint -- a dashboard run and a baseline
run measured by different engines are not comparable. Two things enforce it:
the builder stage pins K6_VERSION=v2.1.0 to match the version behind
Dockerfile.k6's digest, and the runtime stage reuses that exact digest-pinned
grafana/k6 image and replaces only the binary, so everything but the linked-in
extension is byte-identical to the baseline environment. All three build
versions (k6, xk6, xk6-dashboard) are explicit pins; no floating latest.

run-comparison.sh grows --no-dashboard and routes through the dashboard image
by default, passing web-dashboard=report=... so each aspect emits a
self-contained single-file HTML report (assets inlined, no CDN, no served
endpoint) per run and per aspect into the same segregated results directory.
The report is purely additive: the handleSummary() JSON in the generic cuioss
format is emitted either way, so the flag changes what a person can browse,
never what ComparisonSummaryWriter computes from.

The k6-dashboard compose service sits behind its own comparison-dashboard
profile, deliberately distinct from the apisix service's comparison profile.
Verified by config --services: the default stack and the comparison profile
both resolve without it, and it resolves without apisix -- so the default
stack, verify -Pintegration-tests and the -Pbenchmark CI lane start no
dashboard container. docker compose run implicitly enables the target
service's profile, which is what lets run-comparison.sh drive it directly.

Verified: benchmarks/pom.xml and .github/workflows/ carry no reference to the
dashboard image or profile; verify -pl benchmarks -am BUILD SUCCESS, 16 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
…word the CI step

Deliverable 8 (TASK-011).

benchmarks/README.adoc is reframed from 'WRK-based HTTP benchmarks' to k6 and
gains the full aspect matrix in place of the three-row table, plus:

- Lane composition, stated precisely: the CI baseline lane runs eight
  executions (six matrix aspects + healthLiveCheck/gatewayHealth), the
  on-demand comparison lane runs the six matrix aspects only. Records WHY the
  health benchmarks are excluded from comparison: they have no APISIX
  counterpart, so driving them would mislabel API Sheriff data as apisix.
- Methodology: single node/same host, identical resource envelopes, shared TLS
  material, identical routes, dedicated load-generator capacity, and the
  explicit relative-not-absolute framing for cross-gateway numbers.
- The fairness invariant: BOTH sides target nginx-static, so the graphql and
  upload aspects measure gateway processing rather than upstream work -- stated
  as the benchmark's intent, not a limitation.
- The accepted latency_ms.stdev fidelity loss: k6 exposes no stdev for trend
  metrics, so the error/variability/confidence-band columns are permanently
  zero-width. Documented as accepted and NOT to be worked around, with
  percentiles named as the surviving spread signal.
- Reproducibility dependencies on jwks.tls_profile and allowed_egress_hosts
  (ADR-0011), and why the bearer aspect asserts HTTP 200.
- Comparative runs: the exact three-file compose merge, the on-demand-only
  status, the segregated results layout, and the --build warning.
- Dashboard reports: per-aspect self-contained HTML, offline opening, the
  disable flag, and the k6/xk6-dashboard version pins tracking the baseline.
- The wrk-to-k6 swap and the one-time gh-pages baseline restart, including the
  expected one-time discontinuity at the swap commit and an explicit
  instruction not to investigate it as a regression.

Remaining 'wrk' mentions are historical/migration references that the swap
section requires; the document's framing is fully k6.

benchmark.yml: step renamed to 'Run Integration Benchmarks with k6' with prose
naming k6 and the eight executions; timeout-minutes 45 -> 75, with the
rationale inline -- five additional runs land in the same cap, and the
transfer-bound upload-50MB aspect is the dominant contributor beyond a budget
originally sized for three 60-second runs. Maven invocation, history handling,
artifact assembly and Pages deployment are unchanged.

Verified: verify -Ppre-commit BUILD SUCCESS (520 tests, zero markers); workflow
YAML valid; asciidoc validate reports 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5wfKpAQkWGjHN3qBmnXAp
Picks up two fixes relevant to this branch:

- 1.5.2 excludes the defective JavaUtilAPIs/UseMapOf recipe from the
  pre-commit profile (cuioss/cuioss-parent-pom#1323). That recipe rewrote
  a LinkedHashMap into new HashMap<>(Map.of(...)) without adding the
  java.util.HashMap import, leaving the tree uncompilable for the next
  build while the rewriting build itself still reported BUILD SUCCESS.
- 1.5.3 excludes Docker Compose files from OpenRewrite parsing
  (cuioss/cuioss-parent-pom#1329), which matters now that this branch
  adds docker-compose.apisix.yml and the APISIX config set.

Two version vectors were checked and are unaffected: version.quarkus is
pinned locally at 3.37.2 so the parent's 3.37.3 does not apply, and
smallrye-config convergence is decided by quarkus-bom being declared
first, not by the parent. Quarkus config-mapping augmentation runs during
package and passed, which is the check that pin protects.

Also carries the resulting OpenRewrite simplification: the explicit
StandardCharsets.UTF_8 arguments on Files.readString/writeString are
redundant, since both are specified as UTF-8 by the JDK. Verified as a
stable fixed point -- a second pre-commit run produced no further
rewrites, 520 tests green.
onStartup observed StartupEvent with the @ApplicationScoped validator as a
lazy CDI client proxy, so ArC never invoked gatewayTokenValidator() at boot —
tls_profile resolution was deferred to the first bearer request instead of
failing startup. Invoke a method on the injected proxy to force
contextual-instance creation at boot, and correct the misleading comment.
Add onStartup unit tests proving the validator is now dereferenced.

Addresses PR #82 CodeRabbit review (TokenValidatorProducer.java:81-83).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hh2epJFjdLAt2wZyxEeh3p
…primitive JSON

Three PR #82 review fixes in the k6 post-processing harness:

- ComparisonSummaryWriter.readTargetSummaries now wraps each per-file parse in
  try/catch (JsonSyntaxException | IllegalStateException), logging the bad file
  via FAILED_PARSE_SUMMARY and continuing, so one truncated summary cannot abort
  the whole comparison (Gemini).
- K6ResultPostProcessor.stringOrNull/instantOrNull now guard with isJsonPrimitive()
  before getAsString(), so a present-but-non-primitive metadata field yields the
  absent-field path instead of an uncaught UnsupportedOperationException (Gemini).
- K6BenchmarkConverter.requiredString and the gateway_target read now guard with
  isJsonPrimitive(): a non-primitive required field is treated as missing and a
  non-primitive gateway_target falls back to "unknown" (Gemini).

Adds unit coverage for the malformed-skip and non-primitive-guard branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hh2epJFjdLAt2wZyxEeh3p
…ve-benchmark

Consolidate the duplicated has(field) && isJsonPrimitive() guard into a
single helper in K6BenchmarkConverter and delegate instantOrNull to
stringOrNull in K6ResultPostProcessor.

Co-Authored-By: Claude <noreply@anthropic.com>
Record the operator-confirmed architectural decision that comparative
cross-gateway benchmarking is a target-neutral, on-demand-only lane
segregated from the authoritative CI baseline, with fairness parity as
a precondition.

Co-Authored-By: Claude <noreply@anthropic.com>
@OliverWolffGIP
OliverWolffGIP force-pushed the feature/plan-04b-comparative-benchmark branch from 10a79f2 to 68d2bbb Compare July 20, 2026 17:14
@OliverWolffGIP

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@OliverWolffGIP
OliverWolffGIP added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit fbf84e3 Jul 20, 2026
24 checks passed
@OliverWolffGIP
OliverWolffGIP deleted the feature/plan-04b-comparative-benchmark branch July 20, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant