Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bfcbefc
Run k6 via Docker and remove remote import in k6 script
viqueen Mar 7, 2026
0d7d58f
Enable gRPC reflection and use k6 reflect mode
viqueen Mar 7, 2026
4879a6c
Fix k6 FieldMask serialization for gRPC reflection mode
viqueen Mar 7, 2026
ee6e435
Fix toolkit result collection: exec return, gRPC metrics, versions
viqueen Mar 7, 2026
4bfd990
Add .env support for Grafana credentials and update README
viqueen Mar 7, 2026
8fa4766
Pin grafana/k6 Docker image to v1.6.1
viqueen Mar 7, 2026
d942e65
Clarify Grafana Cloud credential instructions in .env.example and README
viqueen Mar 7, 2026
3f3a0cb
Simplify Grafana config to endpoint + token
viqueen Mar 7, 2026
5e5b240
Use standard OTel env vars for Grafana Cloud credentials
viqueen Mar 7, 2026
a1d1048
Support multiple files in publish command
viqueen Mar 7, 2026
49f2b8e
Replace custom OTLP publisher with OpenTelemetry JS SDK
viqueen Mar 7, 2026
057fa18
Update README quick start with publish command, clear .env.example va…
viqueen Mar 7, 2026
df455fb
Fix OTel v2 imports, add devloop scripts, move deps to toolkit
viqueen Mar 8, 2026
4a45aa1
Add Grafana dashboard and fix OTLP metric visibility
viqueen Mar 8, 2026
4787b20
Fix stat panels to use instant queries for gauge data
viqueen Mar 8, 2026
05ef7ce
Address PR review comments
viqueen Mar 8, 2026
2b5a404
Add per-stage Docker build metrics
viqueen Mar 8, 2026
754c9e6
Fix k6 connectivity on macOS and add health check before loadtest
viqueen Mar 8, 2026
24241ac
Simplify Grafana dashboard for legibility
viqueen Mar 8, 2026
ae3fcf0
Fix build stage parsing to check both stdout and stderr
viqueen Mar 8, 2026
51ec813
Replace custom build stage parsing with BuildKit OTLP traces
viqueen Mar 8, 2026
146e348
Fix k6 summary parser to match k6 1.6.1 export format
viqueen Mar 8, 2026
68db4ec
Simplify Grafana dashboard layout
viqueen Mar 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/agents/connect-rpc-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ package main

import (
"connectrpc.com/connect"
"connectrpc.com/grpcreflect"

contentv1connect "<module>/gen/proto/content/v1/contentv1connect"
contentapi "<module>/internal/api/content"
Expand All @@ -1002,6 +1003,13 @@ func setupGateway(cfg *config.Config, domains *Domains) connectapp.App {
)
application.Handle(path, h)

// gRPC server reflection — enables k6 reflect:true and tools like grpcurl
reflector := grpcreflect.NewStaticReflector(
contentv1connect.ContentServiceName,
)
application.Handle(grpcreflect.NewHandlerV1(reflector))
application.Handle(grpcreflect.NewHandlerV1Alpha(reflector))

return application
}
```
Expand Down Expand Up @@ -1139,6 +1147,7 @@ services:
|---|---|
| Go | 1.25.6 |
| connectrpc.com/connect | v1.19.1 |
| connectrpc.com/grpcreflect | latest stable |
| connectrpc.com/validate | latest stable |
| google.golang.org/protobuf | v1.36.11 |
| github.com/jackc/pgx/v5 | latest stable |
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/scaffold-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Add a target entry to `benchmark.config.json`:

For protobuf targets, adjust:
- `protocol`: `"grpc"`
- `k6.env`: use `GRPC_HOST` (set to `localhost:8080`) and `PROTO_DIR` (e.g., `./projects/<project>/_shared/protobuf`) instead of `BASE_URL`
- `k6.env`: use `GRPC_HOST` (set to `localhost:8080`) instead of `BASE_URL`

## Step 7 — Verify

Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Grafana Cloud OTLP credentials (standard OpenTelemetry env vars)
# Find these at: Grafana Cloud portal > Your Stack > Connections > OpenTelemetry (OTLP)
# Generate an API token and copy the two environment variables shown
#
# These env vars serve two purposes:
# 1. Publishing benchmark metrics (build/deploy/loadtest) to Grafana Cloud
# 2. BuildKit automatically picks them up and sends per-stage build traces to Tempo
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_EXPORTER_OTLP_HEADERS=
103 changes: 70 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,36 @@ API benchmark platform for comparing backend service implementations. Define you

- [Node.js](https://nodejs.org/) >= 22
- [Docker](https://docs.docker.com/get-docker/) with Compose v2
- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/) for load testing
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) for scaffolding implementations
- A [Grafana Cloud](https://grafana.com/products/cloud/) account (optional, for publishing results)

k6 load tests run inside Docker (`grafana/k6:1.6.1`) — no local k6 installation required.

## Setup

```bash
npm install
```

### Grafana Cloud (optional)

To publish benchmark results to Grafana Cloud, create a `.env` file at the repository root:

```bash
cp .env.example .env
```

Then fill in the standard OpenTelemetry env vars:

```env
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <token>
```

To find these values, sign in to [Grafana Cloud](https://grafana.com), open your stack, and go to **Connections** > **OpenTelemetry (OTLP)**. Generate an API token and copy the two environment variables shown on the page.

The toolkit uses the official [OpenTelemetry JS SDK](https://opentelemetry.io/docs/languages/js/) to export metrics, so it reads these env vars natively.

## Quick start

### 1. Define an API
Expand Down Expand Up @@ -81,7 +101,7 @@ npm run benchmark -- run content-api/spring-boot
npm run benchmark -- compare results/content-api-connect-rpc-*.json results/content-api-spring-boot-*.json

# publish results to Grafana Cloud
npm run benchmark -- run content-api/connect-rpc --publish
npm run benchmark -- publish results/content-api-connect-rpc-*.json
```

## Configuration
Expand Down Expand Up @@ -129,11 +149,6 @@ All targets are defined in `benchmark.config.json` at the repo root. Each target
"timeoutMs": 120000
}
},
"grafana": {
"endpoint": "https://otlp-gateway-prod-us-central-0.grafana.net/otlp",
"instanceId": "${GRAFANA_INSTANCE_ID}",
"apiKey": "${GRAFANA_API_KEY}"
},
"output": { "dir": "./results" }
}
```
Expand All @@ -153,14 +168,12 @@ All targets are defined in `benchmark.config.json` at the repo root. Each target

### Environment variables

Grafana Cloud credentials are resolved from environment variables. Create a `.env` file or export them in your shell:

```bash
export GRAFANA_INSTANCE_ID=your-instance-id
export GRAFANA_API_KEY=your-api-key
```
Grafana Cloud credentials are read from the standard `OTEL_EXPORTER_OTLP_*` environment variables when publishing results. The CLI automatically loads a `.env` file from the repository root (see [Setup](#grafana-cloud-optional)).

Values in the config using `${VAR_NAME}` syntax are interpolated from the environment at load time.
| Variable | Description |
| ------------------------------ | -------------------------------------------- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Grafana Cloud OTLP gateway URL |
| `OTEL_EXPORTER_OTLP_HEADERS` | Auth header (`Authorization=Basic <token>`) |

## Commands

Expand Down Expand Up @@ -212,12 +225,16 @@ npm run benchmark -- loadtest my-api
npm run benchmark -- loadtest my-api --k6-vus 100 --k6-duration 1m
```

### `benchmark publish <results>`
### `benchmark publish <results...>`

Publishes a results JSON file to Grafana Cloud.
Publishes one or more results JSON files to Grafana Cloud. Accepts multiple files so you can batch-publish results from previous runs.

```bash
# publish a single result
npm run benchmark -- publish results/my-api-2026-03-04T12-00-00-000Z.json

# publish multiple results at once
npm run benchmark -- publish results/connect-rpc-*.json results/spring-boot-*.json
```

### `benchmark compare <targets...>`
Expand Down Expand Up @@ -265,33 +282,52 @@ Time from `docker compose up -d` until the service health check passes. Health i

### Load test (k6)

Parsed from k6's `--summary-export` JSON output:
Parsed from k6's `--summary-export` JSON output. Supports both HTTP (`http_req_duration`) and gRPC (`grpc_req_duration`) protocols:

| Metric | Description |
| --------------------- | ---------------------------------- |
| `httpReqs` | Total HTTP requests |
| `httpReqsPerSec` | Throughput (requests/second) |
| `httpReqDuration.avg` | Average response time (ms) |
| `httpReqDuration.med` | Median / p50 response time (ms) |
| `httpReqDuration.p90` | 90th percentile response time (ms) |
| `httpReqDuration.p95` | 95th percentile response time (ms) |
| `httpReqDuration.p99` | 99th percentile response time (ms) |
| `httpReqFailed` | Error rate (0.0 - 1.0) |
| `checksPassRate` | k6 check pass rate (0.0 - 1.0) |
| Metric | Description |
| ------------------ | ---------------------------------- |
| `reqs` | Total requests (iterations) |
| `reqsPerSec` | Throughput (requests/second) |
| `reqDuration.avg` | Average response time (ms) |
| `reqDuration.med` | Median / p50 response time (ms) |
| `reqDuration.p90` | 90th percentile response time (ms) |
| `reqDuration.p95` | 95th percentile response time (ms) |
| `reqDuration.p99` | 99th percentile response time (ms) |
| `reqFailedRate` | Error rate (0.0 - 1.0) |
| `checksPassRate` | k6 check pass rate (0.0 - 1.0) |

## Grafana Cloud publishing

Results are pushed to Grafana Cloud using the [OTLP/HTTP JSON](https://opentelemetry.io/docs/specs/otlp/) protocol. Each metric is sent as a gauge data point labeled with the target name, tag, and environment info.
Results are pushed to Grafana Cloud using the [OpenTelemetry JS SDK](https://opentelemetry.io/docs/languages/js/) via OTLP/HTTP. Each metric is sent as a gauge data point with the target name, tag, and environment info as resource attributes. The SDK reads `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_HEADERS` from the environment (loaded via `.env`).

Metrics appear in Grafana with the `benchmark.*` prefix:

- `benchmark.build.duration`
- `benchmark.deploy.duration`
- `benchmark.http_reqs_per_sec`
- `benchmark.http_req.duration.{avg,med,p90,p95,p99}`
- `benchmark.http_req_failed_rate`
- `benchmark.reqs_per_sec`
- `benchmark.req.duration.{avg,med,p90,p95,p99}`
- `benchmark.req_failed_rate`
- `benchmark.checks_pass_rate`

### Grafana dashboard

A pre-built dashboard is included at `grafana/benchmark-dashboard.json`. To import it:

1. In Grafana, go to **Dashboards** > **New** > **Import**
2. Upload `grafana/benchmark-dashboard.json`
3. Select your Prometheus data source
4. Click **Import**

The dashboard includes:

- **Overview** — stat panels for throughput, error rate, checks pass rate, build and deploy times
- **Latency Comparison** — bar chart of avg/med/p90/p95/p99 grouped by target
- **Throughput & Volume** — bar gauges for requests/sec and iterations/sec
- **Build & Deploy Comparison** — bar gauges comparing build and deploy times
- **Summary Table** — all metrics in a sortable table

Use the **Target** and **Tag** dropdowns at the top to filter by implementation and run tag.

## Custom k6 scripts

The toolkit ships with default k6 scripts for HTTP and gRPC APIs in `toolkit/k6/scripts/`. To use a custom script, set the `k6.script` path in your target config:
Expand Down Expand Up @@ -372,7 +408,7 @@ toolkit/ # the benchmark CLI toolkit
config/ # zod schema, loader, defaults
core/ # docker, k6, timer, health check
metrics/ # result collector, k6 parser
publish/ # OTLP formatter, Grafana Cloud client
publish/ # OpenTelemetry OTLP metrics export
report/ # comparison logic, terminal table, JSON writer
k6/scripts/ # bundled k6 test scripts
projects/ # benchmark target projects
Expand All @@ -382,6 +418,7 @@ projects/ # benchmark target projects
.claude/
commands/ # Claude Code slash commands
agents/ # architecture agents for code generation
grafana/ # Grafana dashboard JSON (importable)
results/ # benchmark output (gitignored)
```

Expand Down
8 changes: 1 addition & 7 deletions benchmark.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
"vus": 50,
"duration": "30s",
"env": {
"GRPC_HOST": "localhost:8080",
"PROTO_DIR": "./projects/content-api/_shared/protobuf"
"GRPC_HOST": "localhost:8080"
}
},
"tags": {
Expand All @@ -41,11 +40,6 @@
"timeoutMs": 120000
}
},
"grafana": {
"endpoint": "https://otlp-gateway-prod-us-central-0.grafana.net/otlp",
"instanceId": "${GRAFANA_INSTANCE_ID}",
"apiKey": "${GRAFANA_API_KEY}"
},
"output": {
"dir": "./results"
}
Expand Down
Loading