Skip to content

feat: record SemanticCache operation-duration and distance metrics (FDE-87) - #776

Open
vishal-bala wants to merge 5 commits into
feat/FDE-83-otel/02-FDE-86-foundationfrom
feat/FDE-83-otel/03-FDE-87-metrics
Open

vishal-bala wants to merge 5 commits into
feat/FDE-83-otel/02-FDE-86-foundationfrom
feat/FDE-83-otel/03-FDE-87-metrics

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Applications using SemanticCache can'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 tune distance_threshold. This change records two OpenTelemetry histograms through the redisvl[otel] plumbing from the PR below.

Changes

Two histograms

  • redisvl.semantic_cache.operation.duration (s), recorded for check, store, update and drop and their async forms. Attributes:

    • the cache name;
    • the operation;
    • the result (hit or miss) on check;
    • error.type on 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:

  • Duration buckets start at 100 µs, because a lookup on local Redis takes about 250 µs before embedding, while the GenAI buckets start at 10 ms.
  • Distance buckets are 0.01 wide up to 0.1, then coarser up to 2, the maximum cosine distance.

Applications can override either with an SDK View.

Recording never changes what the caller sees

  • Errors. error.type names the exception's class the way the SDK does, with a module prefix except for builtins, so a Redis ConnectionError and the builtin one stay distinct. The exception message is never recorded, and the exception re-raises unchanged.
  • Cancellation. A cancelled call records nothing.
  • Attributes. Prompts, filters, entry ids and distances never become metric attributes.
  • Telemetry failures. A failure inside recording is logged at debug level and dropped. One example is an opentelemetry-api older than 1.30, installed by another package, rejecting the bucket advice.

Tests

  • Integration tests read both histograms back through an in-memory reader, sync and async. They cover:
    • each operation's attributes;
    • nearest-first distance on hits and misses;
    • no distance point for an empty cache or a filter that excludes everything.
  • A hermetic unit test checks that a failing meter never reaches the caller.

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 wraps check, store, update and drop and their async forms, and _record_duration builds every duration point's attributes.

Where it could fail

  • semantic.py:515 and :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 because VectorQuery sorts 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-api 1.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).

Since the last brief: 200454c2 merges the updated base, which brings in main up to 506b961b (Python 3.15 support). This PR's own diff is unchanged.

Reviewed c70b7ed3 · Claude Code (claude-opus-5-5) · lenses: correctness, security, design, tests, docs, Redis behaviour


Note

Low Risk
Observability-only path with defensive try/except around recording; cache behavior and exceptions are unchanged aside from minor post-query telemetry calls.

Overview
Adds OpenTelemetry metrics for SemanticCache so apps can observe hit/miss mix, latency, and how close misses are to the threshold.

A new internal _telemetry module provides a timed decorator on check, store, update, and drop (sync and async). It emits redisvl.semantic_cache.operation.duration with low-cardinality attributes (cache name, operation, hit/miss on lookups, error.type on failure). check/acheck also call record_nearest_distance after the vector query to emit redisvl.semantic_cache.distance from the nearest KNN candidate (before threshold filtering), skipped when there are no candidates.

redisvl.utils.otel gains cached histogram() instruments with tailored bucket advice, error_type() for exception labels, and _clear_caches() for tests. Recording failures are swallowed (debug log only); exceptions and cancellation behavior stay unchanged—interrupted lookups record no metrics.

Integration tests assert histogram attributes and values via an in-memory reader; a unit test ensures a broken meter never affects callers.

Reviewed by Cursor Bugbot for commit 200454c. Bugbot is set up for automated code reviews on this repo. Configure here.

@vishal-bala vishal-bala added the auto:minor Increment the minor version when merged label Oct 20, 2026
@vishal-bala
vishal-bala added this pull request to stack #756 October 20, 2026 13:25
@vishal-bala
vishal-bala marked this pull request as ready for review October 30, 2026 08:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T08:45:22.893941Z f9a61d5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@limjoobin limjoobin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall.

Just one minor issue about syncing the distance metric recording and TTL refresh operation on acheck()

Comment on lines 599 to 602
_telemetry.record_nearest_distance(self.name, candidates, cache_hits)

# Refresh TTL on all found keys
await self._aexpire_many(redis_keys)

@limjoobin limjoobin Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
_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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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.
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants