Add a duration histogram for Global Search

🤖 AI-authored change.

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_type is clamped on the histogram only. It is client-controlled and unvalidated on SearchController#count and EE #aggregations, so an unrecognised value would mint 25 series. The histogram records presence_in(recorded_search_types) — the recorded list, not the request-parameter list, so semantic survives 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

  1. Run bundle exec rspec spec/lib/gitlab/metrics/global_search_slis_spec.rb ee/spec/lib/ee/gitlab/metrics/global_search_slis_spec.rb.
  2. Perform a search in the GDK, then read gitlab_global_search_duration_seconds from the Rails /-/metrics endpoint.

🤖 Automated change — mention me to get a response. Feedback welcome. Ask to speak to John directly anytime.

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 a duration_histogram.observe(...) call at :82, inside the same guarded block that already increments the apdex SLI. Labels are search_type: search_type.presence_in(recorded_search_types), search_level, search_scope; the value is the untouched elapsed. The clamp rationale is an inline comment above it.
  • duration_histogram (:142) returns Gitlab::Metrics.histogram(:gitlab_global_search_duration_seconds, ..., {}, DURATION_BUCKETS) under strong_memoize_attr (:150). search_types (:152) delegates to ::SearchService.supported_search_types. recorded_search_types (:158) now also carries strong_memoize_attr (:161) — it runs inside record_apdex on 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.rb and ee/spec/lib/ee/gitlab/metrics/global_search_slis_spec.rb together — 58 examples, 0 failures at be9e0984.
  • 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.observe block turns 7 of 40 examples red (measured at 24bc5e023cec).
  • Reverting only the search_type clamp turns exactly the 2 clamp examples red.
  • Deleting the two sub-100ms edges fails the new resolution example.
  • Deleting strong_memoize_attr on duration_histogram fails the memoisation example.

What I did not verify

  • No live Prometheus scrape was observed. The :prometheus specs read the in-process registry; nobody has confirmed the metric appears on a real /metrics endpoint, 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 observe call on the search request path, nor of the newly memoized recorded_search_types.
Edited by John Mason

Merge request reports

Loading
Loading