Repository navigation
feat(observability): instrument hot path + add bench harness - #60
Merged
Merged
Conversation
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>
This reverts commit 1294882.
# Conflicts: # server/go.mod
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>
Merged
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
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
executeJob, per-route HTTP latency, claim-jobs outcomes, busy-retry counters.cmd/loadgen+make bench-checksso optimization work has a reproducible baseline (writes markdown reports underbench-results/).solidping_check_executions_total/solidping_check_duration_secondscounters — they were defined but never observed in production.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:
wait_countdeltawait_durationdeltasave_resultp99executep99That
wait_count = 749 / 20sis 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
solidping_db_pool_*{backend}sql.DB.Stats(); no idle costsolidping_db_query_duration_seconds{operation,backend,status}solidping_db_busy_retries_total{backend}solidping_check_stage_duration_seconds{stage}solidping_http_request_duration_seconds{method,route,status}+_requests_totalsolidping_claim_jobs_result_total{outcome}Telemetry surfaces (all independently toggleable, all already existed)
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
🤖 Generated with Claude Code