Skip to content

feat(observability): instrument hot path + add bench harness - #60

Merged
fclairamb merged 22 commits into
mainfrom
claude/determined-fermat-120257
May 16, 2026
Merged

fclairamb merged 22 commits into
mainfrom
claude/determined-fermat-120257

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Summary

  • Add the perf-debugging signals SolidPing was missing: DB pool stats, per-stage timing inside executeJob, per-route HTTP latency, claim-jobs outcomes, busy-retry counters.
  • Build cmd/loadgen + make bench-checks so optimization work has a reproducible baseline (writes markdown reports under bench-results/).
  • Wire up the pre-existing solidping_check_executions_total / solidping_check_duration_seconds counters — they were defined but never observed in production.
  • No optimization changes. A follow-up spec drives actual perf fixes once we have numbers.

Driven by specs/todos/2026-05-16-01-perf-instrumentation-and-loadgen.md.

Signal it surfaces

A 20-second SQLite mini-run with 30 checks at period=5s, 5ms target latency, on an idle laptop:

Metric Value
Achieved checks/min 369
DB pool wait_count delta 749
DB pool wait_duration delta 359ms
save_result p99 25.4ms (p50: 3.3ms)
execute p99 10.0ms

That wait_count = 749 / 20s is the SQLite single-writer pool ceiling becoming visible for the first time — exactly the kind of signal the follow-up optimization spec will target.

Metric families added

Family Notes
solidping_db_pool_*{backend} Custom Collector wrapping sql.DB.Stats(); no idle cost
solidping_db_query_duration_seconds{operation,backend,status} Emitted from the Bun sloghook
solidping_db_busy_retries_total{backend} SQLITE_BUSY + PG 40001 serialization-failure classification
solidping_check_stage_duration_seconds{stage} fetch / execute / save_result / process_incident / release_lease
solidping_http_request_duration_seconds{method,route,status} + _requests_total Keyed by bunrouter pattern — low cardinality
solidping_claim_jobs_result_total{outcome} jobs / empty / lock_conflict / error

Telemetry surfaces (all independently toggleable, all already existed)

  • `SP_PROMETHEUS_ENABLED` (default `true`) — gates `/metrics` mount
  • `SP_PROFILER_ENABLED` (default `false`) — pprof on `SP_PROFILER_LISTEN`
  • `SP_OTEL_ENABLED` (default `false`) — OTLP traces/metrics/logs export

Documented in CLAUDE.md alongside the rate-limit knobs.

Bench harness

```bash
make bench-checks-sqlite # SQLite
make bench-checks-postgres # embedded PostgreSQL
make bench-checks # both, sequentially
make bench-checks BENCH_CHECKS=500 BENCH_DURATION=5m # turn the knobs
```

Each run writes `bench-results/-.md` (gitignored) with achieved checks/min, p50/p95/p99 per stage, DB pool snapshot, query timings, and claim-jobs outcomes.

Test plan

  • `make build` (with placeholder embed dirs) — green
  • `go test -short ./...` — 1823 passed, 0 failed across 123 packages
  • `golangci-lint run ./...` — no issues
  • Smoke: `/metrics` lists all new families with non-zero values; `SP_PROMETHEUS_ENABLED=false` returns 404; pprof off by default
  • End-to-end loadgen run against SQLite: 369 checks/min, DB-pool wait signal visible

🤖 Generated with Claude Code

fclairamb and others added 22 commits May 12, 2026 07:44
Adds two independent middlewares applied to mainGroup with path
exclusions for /api/v1/workers/, /api/v1/heartbeat/, /api/mgmt/health,
and /metrics:

- Rate limiter: token bucket (golang.org/x/time/rate), default
  300 req/min with burst 60. Returns 429 + Retry-After: 60 + code
  RATE_LIMITED when exhausted.
- Concurrency limiter: per-IP semaphore, default 20 in-flight. Returns
  429 + code CONCURRENCY_LIMITED when full (no delay — sheds load
  rather than holding goroutines).

Per-IP state lives in a sync.Map keyed by client IP; a 2-minute
cleanup goroutine evicts entries idle >5 min so an attacker can't
grow the map unbounded. extractIP() honors a TrustedProxies count
to parse X-Forwarded-For safely behind reverse proxies without
opening an IP-spoofing vector when run directly.

solidping_http_rate_limited_total{reason="rate"|"concurrency"}
exposes 429 counts for operator alerting.

Both limits opt out by setting RequestsPerMinute=0 or MaxConcurrent=0
independently via SP_SERVER_RATE_LIMITING_* env vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The original 016_org_entitlements migration was edited in-place after
some dev databases had already applied the old column-per-limit schema.
Those databases now drift from the model. Migration 022 drops and
recreates the table with the payload-text shape the code expects. No
data migration needed — entitlements are seeded lazily on first access,
so the table is always empty at migration time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves the translate="no" / google notranslate hints from the #root div
up to <html>, so browser translation extensions (Chrome auto-translate,
etc.) don't garble status labels and check names before the React app
mounts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The original test signalled "ready" before calling the handler and
then closed the release channel as soon as all three goroutines had
signalled. Under sequential goroutine scheduling, the first two
handlers could acquire the semaphore, see release already closed,
exit, and free the slot before the third goroutine ever called the
handler — so all three ended up returning 200.

Switched to a signal-after-acquire pattern: the holders publish on
`inside` once they're past the semaphore, the test waits for both
acquisitions to land, then runs the overflow request synchronously
and asserts 429 before releasing the holders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Public endpoint that returns the configured rate-limit and concurrency
limits plus the calling IP's current token balance and in-flight count.
Rate-limited clients can hit it to discover when their bucket refills.

The whole /api/mgmt/ prefix is now exempt from both middlewares
(was just /api/mgmt/health) so a 429'd client can still introspect.

Implementation:
- middleware.RateLimiter.StateFor(ip) takes a read-only snapshot of
  rate.Limiter.Tokens() and len(sem) without mutating either.
- Fresh IPs (no entry yet) report a full burst and zero in-flight.
- callerRemaining / callerInFlight are omitted when their respective
  limiter is disabled (RequestsPerMinute=0 or MaxConcurrent=0).
- Server holds a *RateLimiter reference so getLimits can call StateFor;
  RateLimiter.ExtractIP exposes the same trusted-proxy logic the
  middlewares use, so callers and limits agree on the IP.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…introspection)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ction)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The limiter was mounted on mainGroup and counted every static asset
(dash0 JS chunks, status0 assets, embedded SPA files) against the
per-IP MaxConcurrent=20 semaphore. A normal dashboard first paint fires
dozens of parallel asset requests from one IP and tripped the limit,
breaking page loads.

Static assets come from embedded FS and aren't an abuse surface, so
limit only /api/v1/ (keeping the existing workers/ and heartbeat/
sub-exclusions). /api/mgmt/ and /metrics remain unlimited as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add the missing perf-debugging signals (DB pool stats, per-stage timing,
per-route HTTP latency, claim-jobs outcomes, busy-retry counters) and
build a reproducible loadgen so optimization work has a baseline. No
optimizations in this change — instrumentation only.

Wires up the pre-existing solidping_check_executions_total /
_duration_seconds counters too: they were defined but never observed
outside tests.

- New metric families: solidping_db_pool_*, solidping_db_query_duration_seconds,
  solidping_db_busy_retries_total, solidping_check_stage_duration_seconds,
  solidping_http_request_duration_seconds, solidping_http_requests_total,
  solidping_claim_jobs_result_total
- Bun sloghook emits query histograms + classifies SQLITE_BUSY / PG 40001
- HTTP middleware keys by bunrouter pattern (low cardinality)
- cmd/loadgen + make bench-checks{,-sqlite,-postgres} write markdown reports
- All three telemetry surfaces (Prometheus, pprof, OTel) stay independently
  toggleable via existing env vars

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mat-120257

# Conflicts:
#	CLAUDE.md
#	server/internal/app/server.go
#	server/internal/middleware/ratelimit.go
#	server/internal/middleware/ratelimit_test.go
#	server/internal/prommetrics/metrics.go
The CI golangci-lint flagged 64 issues in the loadgen + db_collector
files. Three buckets:

1. Real dead code — gaugeSum / histogramQuantile / aggregateHistogram /
   histogramCount / bucketAcc were leftovers from an earlier iteration of
   the report layout. Removed.
2. cmd/loadgen is a single-file dev/bench harness, not production code.
   Added a path exclusion for the rules that don't fit that shape
   (err113, errcheck, varnamelen, funlen, protogetter, govet shadow,
   gocritic hugeParam, etc.).
3. db_collector.go has two unavoidable specifics (the prometheus.Collector
   interface uses `ch` for the channel; the registration guard mirrors
   the default Prometheus registry singleton). Each gets a targeted
   //nolint with a one-line rationale.

Plus a misspell fix (labelled → labeled) and a gofmt pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fclairamb
fclairamb merged commit c6604cc into main May 16, 2026
10 checks passed
@fclairamb
fclairamb deleted the claude/determined-fermat-120257 branch May 16, 2026 15:38
@fclairamb fclairamb mentioned this pull request May 16, 2026
fclairamb added a commit that referenced this pull request Jun 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.0](v0.0.0...v0.1.0)
(2026-06-22)


### Features

* add check type registry, sample configs, notification senders, and
observability integrations
([eea049e](eea049e))
* batch — Warning/Degraded statuses, SSL/Docker check upgrades, MCP
OAuth 2.1, config-as-code
([#85](#85))
([936d830](936d830))
* batch spec implementations (dashboard UI, jobs, notifications, SaaS,
perf) ([#78](#78))
([fe003e4](fe003e4))
* bulk feature integration (discovery, web push, badges, webhooks, and
more) ([#73](#73))
([a976856](a976856))
* custom user-agent, refresh checks, vite plugin switch, CI updates
([#1](#1))
([cc00858](cc00858))
* documentation site served at /docs (co-located in web/docs)
([#88](#88))
([27d9ccd](27d9ccd))
* email check frontend (spec 03)
([#30](#30))
([aebd106](aebd106))
* email inbox foundation via JMAP (spec 01)
([#24](#24))
([0f3b0c1](0f3b0c1))
* email passive checks (spec 02)
([#27](#27))
([ba5536f](ba5536f))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute + status
page history fixes
([#50](#50))
([a007de0](a007de0))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute, enforce
both ([#49](#49))
([d25cc5f](d25cc5f))
* group incident correlation (spec 04 backend v1)
([#31](#31))
([1883986](1883986))
* **observability:** instrument hot path + add bench harness
([#60](#60))
([c6604cc](c6604cc))
* **oncall:** searchable timezone dropdown + sidebar logo polish
([#46](#46))
([4396898](4396898))
* per-IP HTTP rate limiting and concurrency limiting
([#59](#59))
([f9cc273](f9cc273))
* soften HTTP rate limits with bounded queues + request timeout
([#63](#63))
([025501b](025501b))
* SolidPing — distributed uptime monitoring platform
([eef4383](eef4383))
* **statuspages:** drag-and-drop resource reordering + dash conventions
([#41](#41))
([cf910d4](cf910d4))


### Bug Fixes

* **deps:** update dependency i18next to v26
([#26](#26))
([d5d1b7e](d5d1b7e))
* **deps:** update dependency lucide-react to v1
([#28](#28))
([cdc8e8c](cdc8e8c))
* **deps:** update dependency react-i18next to v17
([#29](#29))
([13135d8](13135d8))
* **deps:** update dependency recharts to v3.8.1
([#14](#14))
([5d95a3b](5d95a3b))
* **deps:** update go dependencies (non-major)
([#16](#16))
([2450f13](2450f13))
* **deps:** update go dependencies (non-major)
([#19](#19))
([f842310](f842310))
* **deps:** update go dependencies (non-major)
([#37](#37))
([c020eca](c020eca))
* **deps:** update go dependencies (non-major)
([#44](#44))
([ff0bcd0](ff0bcd0))
* **deps:** update go dependencies (non-major)
([#52](#52))
([2e3bc28](2e3bc28))
* **deps:** update go dependencies (non-major)
([#69](#69))
([a65ed4c](a65ed4c))
* **deps:** update go dependencies (non-major)
([#77](#77))
([a0920ff](a0920ff))
* **deps:** update go dependencies (non-major) to v1.4.2
([#86](#86))
([1eb7504](1eb7504))
* **deps:** update module github.com/aws/aws-sdk-go-v2/config to
v1.32.18 ([#72](#72))
([6406537](6406537))
* **deps:** update module github.com/aws/aws-sdk-go-v2/service/s3 to
v1.101.0 ([#39](#39))
([f9c9563](f9c9563))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.3
([#45](#45))
([593931a](593931a))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.4
([#70](#70))
([fca1c97](fca1c97))
* **deps:** update module github.com/ibm/sarama to v1.48.1
([#47](#47))
([93ccf3a](93ccf3a))
* **deps:** update module github.com/ibm/sarama to v1.48.2
([#54](#54))
([e84d868](e84d868))
* **deps:** update module github.com/ibm/sarama to v1.49.0
([#64](#64))
([fd61bd7](fd61bd7))
* **deps:** update module github.com/oapi-codegen/runtime to v1.4.1
([#65](#65))
([a3a3e9f](a3a3e9f))
* **deps:** update module github.com/prometheus/common to v0.67.5
([#61](#61))
([095dd6f](095dd6f))
* **deps:** update module github.com/slack-go/slack to v0.24.0
([#75](#75))
([6e86d5b](6e86d5b))
* **deps:** update module golang.org/x/sys to v0.44.0
([#42](#42))
([a245730](a245730))
* **deps:** update module golang.org/x/sys to v0.45.0
([#67](#67))
([49c3d90](49c3d90))
* **deps:** update module golang.org/x/term to v0.43.0
([#43](#43))
([4a934f1](4a934f1))
* **deps:** update module google.golang.org/grpc to v1.81.0
([#36](#36))
([1be183a](1be183a))
* **deps:** update module google.golang.org/grpc to v1.81.1
([#55](#55))
([6bfac2d](6bfac2d))
* status lifecycle improvements and created/running result handling
([#5](#5))
([fc64c7d](fc64c7d))
* **status0:** white page header and de-duplicated footer
([#48](#48))
([77506a7](77506a7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Sep 8, 2026
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