Add a duration histogram for Global Search
What does this MR do and why?
Global Search records an apdex SLI for every search but no latency distribution, so per-scope percentiles are not computable — the counters only report whether a fixed 2022 target was met, and a target cannot be re-derived from that signal.
This adds gitlab_global_search_duration_seconds, a histogram of the same elapsed value record_apdex already measures, labelled search_type / search_level / search_scope. The apdex counters and the existing log line are unchanged.
Two things a reviewer should know:
search_typeis clamped on the histogram only. It is client-controlled and unvalidated onSearchController#countand EE#aggregations, so an unrecognised value would mint 25 series. The histogram recordspresence_in(recorded_search_types)— the recorded list, not the request-parameter list, sosemanticsurvives the clamp; the apdex sample keeps the raw value. The resulting divergence is tracked separately.- The bucket edges include two below 100ms, chosen from gprd p50s for autocomplete and tab counts, and every apdex target is an exact edge.
References
How to set up and validate locally
- Run
bundle exec rspec spec/lib/gitlab/metrics/global_search_slis_spec.rb ee/spec/lib/ee/gitlab/metrics/global_search_slis_spec.rb. - Perform a search in the GDK, then read
gitlab_global_search_duration_secondsfrom the Rails/-/metricsendpoint.
Mechanism, citations and verification
Mechanism
Five files change; no new class, no migration, no feature flag.
record_apdex keeps emitting the apdex pass/fail counters and the log line untouched, and additionally observes the same elapsed into gitlab_global_search_duration_seconds. The 22 edges run from 0.025s to 60s: dense between 2s and 30s because code searches run for tens of seconds and the default web-latency buckets top out at 10s, with two edges below 100ms because a gprd measurement puts 67% of SearchController#autocomplete and 55% of #count requests there (p50 75ms and 90ms), and bucket resolution cannot be recovered after the fact. Every apdex target is an exact edge, so "fraction under target" reconciles with the counters instead of being interpolated.
endpoint_id is omitted to keep the bucket count down; the apdex SLI still carries it. Cardinality: 59 valid (type, level, scope) combinations x 25 series (22 edges, +Inf, _sum, _count) is about 1,500 series per process, four to six times the apdex footprint, created lazily. search_scope needs no clamp: every record_apdex caller passes a resolved search_service.scope.
lib/gitlab/metrics/global_search_slis.rb
DURATION_BUCKETS— 22 frozen edges. Six are literal copies of apdex target constants, commented at their edge; the constants live in the module body alongside the buckets, and the edges stay literals so a target cannot move and drag its edge with it.record_apdex(:63) gains aduration_histogram.observe(...)call at:82, inside the same guarded block that already increments the apdex SLI. Labels aresearch_type: search_type.presence_in(recorded_search_types),search_level,search_scope; the value is the untouchedelapsed. The clamp rationale is an inline comment above it.duration_histogram(:142) returnsGitlab::Metrics.histogram(:gitlab_global_search_duration_seconds, ..., {}, DURATION_BUCKETS)understrong_memoize_attr(:150).search_types(:152) delegates to::SearchService.supported_search_types.recorded_search_types(:158) now also carriesstrong_memoize_attr(:161) — it runs insiderecord_apdexon every request and previously allocated a fresh array per call.
ee/lib/ee/gitlab/metrics/global_search_slis.rb — overrides recorded_search_types to append SEMANTIC_SEARCH_TYPE, since the semantic endpoint records that type but SearchService.supported_search_types never lists it as a requestable parameter, so the CE clamp would otherwise erase it to nil.
spec/lib/gitlab/metrics/global_search_slis_spec.rb — guards three things: that every apdex target is still an exact bucket edge (and that at least two edges sit below 0.1), that an unrecognised search_type reaches the histogram as nil while the apdex sample keeps the raw value, and that the histogram is registered once rather than per request. The last group runs against the real Prometheus registry rather than a double.
ee/spec/lib/ee/gitlab/metrics/global_search_slis_spec.rb — asserts the EE recorded_search_types override admits semantic, and that every search_type label initialize_slis! registers is admitted by the clamp.
gitlab_metrics.md — one table row, inserted in alphabetical position, documenting the metric as Histogram, 19.6, labels search_type, search_level, search_scope. The row states search_type is basic, and calls out advanced/zoekt/semantic as EE-only — FOSS's SearchService.supported_search_types returns only %w[basic].
Verification
Run against a real GDK checkout:
spec/lib/gitlab/metrics/global_search_slis_spec.rbandee/spec/lib/ee/gitlab/metrics/global_search_slis_spec.rbtogether — 58 examples, 0 failures atbe9e0984.- RuboCop clean on both changed Ruby files, re-run at the current head (
8c9ab204).
Mutations run to confirm the assertions bite:
- Deleting the
duration_histogram.observeblock turns 7 of 40 examples red (measured at24bc5e023cec). - Reverting only the
search_typeclamp turns exactly the 2 clamp examples red. - Deleting the two sub-100ms edges fails the new resolution example.
- Deleting
strong_memoize_attronduration_histogramfails the memoisation example.
What I did not verify
- No live Prometheus scrape was observed. The
:prometheusspecs read the in-process registry; nobody has confirmed the metric appears on a real/metricsendpoint, nor that the series count in production matches the estimate. - Actual series cardinality is an estimate, not a measurement. The 59 combinations were enumerated from
Search::Scopes.scope_definitions, but the resulting series count was not counted against a running instance. - The 2022 apdex targets are still unvalidated. This MR makes them re-derivable; it does not re-derive them. That is follow-up work.
- No performance measurement of the added
observecall on the search request path, nor of the newly memoizedrecorded_search_types.