Repository navigation
feat: record SemanticCache operation-duration and distance metrics (FDE-87) - #776
vishal-bala wants to merge 8 commits into
Conversation
SemanticCache now records two OpenTelemetry histograms through the redisvl[otel] plumbing: - redisvl.semantic_cache.operation.duration (s), for check, store, update and drop and their async forms, with the cache name and the operation, the result (hit or miss) on check, and error.type on failure. Its count split by result is the hit rate, so there is no counter. - redisvl.semantic_cache.distance (1), the nearest candidate's distance on every lookup that found one, hit or miss, with the cache name and result. Read against distance_threshold it shows how close misses come. Both set bucket advice. Duration buckets start at 100 us, since a lookup on local Redis takes about 250 us excluding embedding and the GenAI buckets start at 10 ms; distance buckets are dense up to 0.3. error.type names the exception's class as the SDK does on exception events, prefixed by its module unless it is a builtin, so a Redis ConnectionError and the builtin one stay distinct. The message is never recorded, and the exception re-raises unchanged. A cancelled call records nothing. Prompts, filters, entry ids and distances never become metric attributes. Recording can never fail a cache call: an error inside it, such as an opentelemetry-api older than 1.30 installed by another package rejecting the bucket advice, is logged at debug level and dropped.
CustomTextVectorizer is deprecated in favour of CustomVectorizer.
…-otel/03-FDE-87-metrics
…-otel/03-FDE-87-metrics
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
limjoobin
left a comment
There was a problem hiding this comment.
LGTM overall.
Just one minor issue about syncing the distance metric recording and TTL refresh operation on acheck()
| _telemetry.record_nearest_distance(self.name, candidates, cache_hits) | ||
|
|
||
| # Refresh TTL on all found keys | ||
| await self._aexpire_many(redis_keys) |
There was a problem hiding this comment.
Since the nearest distance is recorded before the TTL refresh finishes, a distance sample is still logged if acheck() is cancelled during the refresh. We can consider moving the distance recording operation after the TTL refresh so cancelled lookups record nothing.
| _telemetry.record_nearest_distance(self.name, candidates, cache_hits) | |
| # Refresh TTL on all found keys | |
| await self._aexpire_many(redis_keys) | |
| # Refresh TTL on all found keys | |
| await self._aexpire_many(redis_keys) | |
| _telemetry.record_nearest_distance(self.name, candidates, cache_hits) |
There was a problem hiding this comment.
Fixed in 7d58ffb: both check and acheck now record the distance after the TTL refresh, so a lookup that is cancelled or fails during the refresh records nothing, matching the duration histogram. A new integration test interrupts the refresh in both and asserts no distance point; it fails without the fix. #777 carries the same ordering.
🤖 Addressed by Claude Code
check and acheck recorded the nearest distance before refreshing the hits' TTLs. A lookup cancelled or failed during the refresh recorded no duration, but still added a distance point, labelled with a result the caller never received. The distance is now recorded after the refresh, so an interrupted lookup records nothing.
…-otel/03-FDE-87-metrics
…-otel/03-FDE-87-metrics
Motivation
Applications using
SemanticCachecan't see its hit rate, how long its operations take, or how close a miss came to matching. The last of these is what you need to tunedistance_threshold. This change records two OpenTelemetry histograms through theredisvl[otel]plumbing from the PR below.Changes
Two histograms
redisvl.semantic_cache.operation.duration(s), recorded forcheck,store,updateanddropand their async forms. Attributes:hitormiss) oncheck;error.typeon failure.The count split by result is the hit rate, so there is no separate counter. The duration covers the whole call, including any prompt embedding. The spans in the next PR separate the two.
redisvl.semantic_cache.distance(1), the nearest candidate's distance on every lookup that found one, hit or miss, with the cache name and the result. It is read from the KNN candidates before the threshold filter, which is why a miss still reports how close it came.Both histograms set bucket advice:
Applications can override either with an SDK View.
Recording never changes what the caller sees
error.typenames the exception's class the way the SDK does, with a module prefix except for builtins, so a RedisConnectionErrorand the builtin one stay distinct. The exception message is never recorded, and the exception re-raises unchanged.opentelemetry-apiolder than 1.30, installed by another package, rejecting the bucket advice.Tests
Notes
This PR sits in a stack on top of the
redisvl[otel]foundation, and below the spans and the user guide. It should merge together with them, not on its own.This PR records two of the three published metrics. The third,
gen_ai.client.operation.duration, is named in the foundation and recorded from the spans PR above, together with the embeddings spans that time the same provider calls.Reviewer Brief
Start here:
redisvl/extensions/cache/llm/_telemetry.py,timed(). It wrapscheck,store,updateanddropand their async forms, and_record_durationbuilds every duration point's attributes.Where it could fail
semantic.py:515and:602: the distance is recorded after the TTL refresh, so a lookup interrupted during the refresh records nothing. Moving it earlier brings back distance points for lookups the caller never received._telemetry.py:93(record_nearest_distance): it takes the first candidate as the nearest, which holds only becauseVectorQuerysorts by distance by default._telemetry.py:27(_record_duration): metric attributes must stay low-cardinality and free of content. A filter or tag value added here would be exported unconditionally.Not verified: cluster mode; the
opentelemetry-api1.30.0 floor, not re-run in this review.Beyond CI: the TTL-refresh interruption test fails against the code before the fix (2 failed).
Reviewed
c70b7ed3· Claude Code (claude-opus-5-5) · lenses: correctness, security, design, tests, docs, Redis behaviourNote
Medium Risk
Introduces stable public metric names and attributes that observability tooling will depend on, though cache behavior and error semantics are unchanged and metric recording is isolated from application outcomes.
Overview
Adds OpenTelemetry histogram instrumentation for
SemanticCacheso apps can observe lookup latency, hit/miss mix, and how close misses came to matching.A new internal
_telemetrymodule wraps sync/asynccheck,store,update, anddropwith atimeddecorator that recordsredisvl.semantic_cache.operation.durationusing low-cardinality attributes (cache name, operation,hit/misson lookups,error.typeon failure).check/acheckalso emitredisvl.semantic_cache.distancefor the nearest KNN candidate before threshold filtering, including on misses when any candidate exists. Recording is best-effort: telemetry errors are debug-logged and dropped, exceptions re-raise unchanged, and interrupted lookups (e.g. cancellation during TTL refresh) record nothing.redisvl.utils.otelgains cachedhistogram()instruments with explicit bucket advice tuned for sub-millisecond Redis lookups and cosine-distance tuning, pluserror_type()and_clear_caches()for tests. Integration and unit tests assert exported attributes, distances, and that a broken meter never affects callers.Reviewed by Cursor Bugbot for commit b0e5c91. Bugbot is set up for automated code reviews on this repo. Configure here.