Skip to content

feat(otel): OpenTelemetry metrics + span instrumentation for core mem… - #1718

Open
henrikrexed wants to merge 3 commits into
getzep:mainfrom
henrikrexed:otel-instrumentation
Open

feat(otel): OpenTelemetry metrics + span instrumentation for core mem…#1718
henrikrexed wants to merge 3 commits into
getzep:mainfrom
henrikrexed:otel-instrumentation

Conversation

@henrikrexed

Copy link
Copy Markdown

Base: getzep/graphiti:main · Head: henrikrexed/graphiti:otel-instrumentation
Linked RFC: (fill in issue #) — see docs/rfc-otel-instrumentation.md in this branch
⚠️ RFC gate: this PR must reference an accepted design issue or it will be labelled needs-rfc. File the RFC issue first and link it above before requesting review.

Closes issue #1714

Summary


Adds optional OpenTelemetry metrics and extends span instrumentation to Graphiti's core memory
operations. Users who run Graphiti with an OTel SDK can now observe latency, volume, and query
efficiency of add_episode, search, search_, and remove_episode in their existing backends
(Jaeger/Tempo for traces; Prometheus/Grafana/Datadog/Honeycomb for metrics). With no OTel meter
passed, everything is a no-op — zero behaviour change and no new required dependencies for
existing users.

This mirrors the design of the existing graphiti_core/tracer.py abstraction, so the meter follows
the same NoOp/OTel dual-impl discipline already established in the codebase.

Motivation


Graphiti is increasingly used as the production memory layer for AI agents. Operators currently
cannot attribute latency to specific Graphiti operations or distinguish LLM calls from graph writes,
and there is no metrics signal at all. This PR closes that gap:

  1. Latency — per-operation duration histograms.
  2. Volume — nodes/edges/episodes stored and invalidated.
  3. Query efficiency — result-set sizes per search.
  4. Error rate — operation count labelled by status.

Design

Dual-impl, zero-dependency pattern

  • New GraphitiMeter ABC with NoOpMeter (default, no deps) and OpenTelemetryMeter
    (active when the caller passes an opentelemetry.metrics.Meter).
  • create_meter(otel_meter=None) factory returns NoOpMeter when otel_meter is None or when
    opentelemetry-api is not installed. All instrumentation errors are suppressed — telemetry never
    breaks a memory operation.
  • Graphiti.__init__ gains an optional meter: GraphitiMeter | None = None parameter alongside the
    existing tracer. Default NoneNoOpMeter.

Metric instruments (memory.*, OTel naming)

Instrument Kind Unit Key attributes
memory.operation.duration Histogram ms memory.operation.name
memory.operation.count Counter 1 memory.operation.name, memory.operation.status (ok/error)
memory.items.stored Counter 1 memory.item.type (node/edge/episode)
memory.items.invalidated Counter 1 memory.item.type
memory.query.result_count Histogram 1 memory.operation.name

Units are plain UCUM 1/ms (not {item}-style annotations) so OTLP payloads are accepted by
strict backends.

Spans + metrics per operation

Operation Span Metrics added
add_episode existing node/edge/episode stored counts, invalidated edges
add_episode_bulk existing stored/invalidated counts (bulk)
search new duration, result count, op count/status
search_ new duration, result count, op count/status
remove_episode new duration, invalidated node/edge/episode counts, op count/status

Example usage

from opentelemetry import metrics, trace
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
from graphiti_core import Graphiti
from graphiti_core.meter import create_meterotel_tracer = trace.get_tracer("graphiti")
otel_meter  = metrics.get_meter("graphiti", version="0.1.0")
​
graphiti = Graphiti(
    uri="bolt://localhost:7687",
    user="neo4j",
    password="...",
    tracer=otel_tracer,
    meter=create_meter(otel_meter),
)

Files changed (upstream scope)

File Change
graphiti_core/meter.py newGraphitiMeter ABC, NoOpMeter, OpenTelemetryMeter, create_meter()
graphiti_core/graphiti.py wire meter param; add metric calls; add spans to search/search_/remove_episode
graphiti_core/__init__.py re-export meter types
OTEL_TRACING.md document the metrics layer alongside existing tracing docs
pyproject.toml tracing extra already provides opentelemetry-api/-sdk; no new required dep

Out of scope for this PR (fork/benchmark-specific, not upstreamed): server/, examples/,
Dockerfile, uv.lock, deploy overlays, and the Dynatrace-specific validation harness in
tests/validate_dynatrace.py (vendor-specific; kept in the fork).

Testing

  • make check (Ruff + Pyright + Pytest) passes; no existing tests modified.
  • Validated end-to-end: all 5 instrumented operations emit spans and metrics through a local
    OTel Collector into a backend (18 metric data points + 5 spans observed). Existing users with no
    meter argument exercise the NoOpMeter path — confirmed zero overhead / no new deps.

Backwards compatibility

  • Default meter=NoneNoOpMeter: no behaviour change.
  • opentelemetry-api remains optional (only pulled by the existing tracing extra).
  • Instrumentation failures are swallowed and never propagate into memory operations.

Open questions for reviewers

  1. Should memory.query.result_count stay a Histogram (percentiles) or become an UpDownCounter?
  2. Are add_episode / search / ... the desired memory.operation.name attribute values, or should
    they map to a specific semconv registry string?
  3. Preferred way to surface the meter param through the MCP/FastAPI server layers later (this PR
    instruments only the core library)?

…ory operations

Add an optional OTel metrics layer mirroring the existing tracer.py NoOp/OTel
dual-impl pattern, and extend span coverage to search/search_/remove_episode.

- graphiti_core/meter.py (new): GraphitiMeter ABC, NoOpMeter, OpenTelemetryMeter,
  create_meter(); instruments memory.operation.duration/count,
  memory.items.stored/invalidated, memory.query.result_count.
- graphiti_core/graphiti.py: wire meter= param; add spans+metrics to
  search/search_/remove_episode; add stored/invalidated metrics to add_episode(_bulk).
- graphiti_core/tracer.py: guard only span *creation* so an op that raises inside a
  span surfaces the real error instead of RuntimeError from the generator.
- OTEL_TRACING.md: document the metrics layer.
- docs/rfc-otel-instrumentation.md, docs/upstream-pr-description.md: RFC + PR body.

Default meter=None -> NoOpMeter: zero behaviour change, no new required deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zep-cla-assistant

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. For privacy information, see our Privacy Notice. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA behalf on myself, e-mail: example@example.com

or

I have read the CLA Document and I hereby sign the CLA behalf of my company, e-mail: example@example.com

Signature is valid for 6 months.


This bot will be retriggered when the Contributor License Agreement comment has been provided. Posted by the CLA Assistant Lite bot.

henrikrexed and others added 2 commits August 3, 2026 12:12
…p#1718

- meter.py: drop unused UpDownCounter import (F401); unquote instance-attr
  type annotations (UP037, safe — PEP 526 does not evaluate self.x: T);
  keep the meter: 'Meter' param annotation quoted for no-OTel import safety;
  convert the query-results try/except/pass to contextlib.suppress (SIM105).
- graphiti.py: ruff import-sort (I001) + format.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@henrikrexed

henrikrexed commented Aug 3, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA behalf on myself, e-mail: henrik.rexed@dynatrace.com

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant