Skip to content

Commit 5142f2c

Browse files
authored
[feat] mem0 instrumentation: change to use memory handler (#89)
* feat:mem0 instrumentation: change to use memory handler
1 parent 432a887 commit 5142f2c

File tree

16 files changed

+750
-1426
lines changed

16 files changed

+750
-1426
lines changed

.github/workflows/loongsuite_test_0.yml

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -374,44 +374,6 @@ jobs:
374374
- name: Run tests
375375
run: tox -c tox-loongsuite.ini -e py313-test-loongsuite-instrumentation-dashscope-latest -- -ra
376376

377-
py39-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest:
378-
name: LoongSuite loongsuite-instrumentation-mem0-oldest 3.9 Ubuntu
379-
runs-on: ubuntu-latest
380-
timeout-minutes: 30
381-
steps:
382-
- name: Checkout repo @ SHA - ${{ github.sha }}
383-
uses: actions/checkout@v4
384-
385-
- name: Set up Python 3.9
386-
uses: actions/setup-python@v5
387-
with:
388-
python-version: "3.9"
389-
390-
- name: Install tox
391-
run: pip install tox-uv
392-
393-
- name: Run tests
394-
run: tox -c tox-loongsuite.ini -e py39-test-loongsuite-instrumentation-mem0-oldest -- -ra
395-
396-
py39-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest:
397-
name: LoongSuite loongsuite-instrumentation-mem0-latest 3.9 Ubuntu
398-
runs-on: ubuntu-latest
399-
timeout-minutes: 30
400-
steps:
401-
- name: Checkout repo @ SHA - ${{ github.sha }}
402-
uses: actions/checkout@v4
403-
404-
- name: Set up Python 3.9
405-
uses: actions/setup-python@v5
406-
with:
407-
python-version: "3.9"
408-
409-
- name: Install tox
410-
run: pip install tox-uv
411-
412-
- name: Run tests
413-
run: tox -c tox-loongsuite.ini -e py39-test-loongsuite-instrumentation-mem0-latest -- -ra
414-
415377
py310-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest:
416378
name: LoongSuite loongsuite-instrumentation-mem0-oldest 3.10 Ubuntu
417379
runs-on: ubuntu-latest

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ repos:
1818
hooks:
1919
- id: rstcheck
2020
additional_dependencies: ['rstcheck[sphinx]']
21-
args: ["--report-level", "warning"]
21+
args: ["--report-level", "warning"]

CHANGELOG-loongsuite.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- `loongsuite-instrumentation-mem0`: use memory handler
17+
([#89](https://github.com/alibaba/loongsuite-python-agent/pull/89))
18+
1619
- Add `from __future__ import annotations` to fix Python 3.9 compatibility for union type syntax (`X | Y`)
1720
([#80](https://github.com/alibaba/loongsuite-python-agent/pull/80))
1821

instrumentation-loongsuite/loongsuite-instrumentation-mem0/README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<trace_endpoint>
4545
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
4646
export OTEL_SERVICE_NAME=mem0-demo
4747

48+
# Enable GenAI experimental semantic conventions (required for GenAI content/event features)
49+
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
50+
4851
# (Optional) Capture message content – may contain sensitive data
49-
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
52+
# Must be one of: NO_CONTENT | SPAN_ONLY | EVENT_ONLY | SPAN_AND_EVENT
53+
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY
54+
55+
# (Optional) Emit GenAI events (LogRecord). Requires a LoggerProvider exporter in your app.
56+
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true
5057
```
5158

5259
### Option 1: Using opentelemetry-instrument
@@ -69,7 +76,9 @@ If everything is working, you should see spans for:
6976
You can also start your application with `loongsuite-instrument` to forward data to LoongSuite/Jaeger:
7077

7178
```bash
72-
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
79+
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
80+
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY
81+
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true
7382

7483
loongsuite-instrument \
7584
--traces_exporter console \
@@ -94,7 +103,9 @@ You can control the Mem0 instrumentation using environment variables.
94103
|-----------------------------------------------------------|---------|-----------------------------------------------------------------------------|
95104
| `OTEL_INSTRUMENTATION_MEM0_ENABLED` | `true` | Enable or disable the Mem0 instrumentation entirely. |
96105
| `OTEL_INSTRUMENTATION_MEM0_INNER_ENABLED` | `false` | Enable internal phases (Vector Store, Graph Store, Rerank). |
97-
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `false` | Capture input/output message content (may contain PII or sensitive data). |
106+
| `OTEL_SEMCONV_STABILITY_OPT_IN` | *(empty)* | Set to `gen_ai_latest_experimental` to enable GenAI experimental semantics (required for content/event). |
107+
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `NO_CONTENT` | Content capturing mode: `NO_CONTENT`, `SPAN_ONLY`, `EVENT_ONLY`, `SPAN_AND_EVENT` (may contain PII/sensitive data). |
108+
| `OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT` | `false` | Emit GenAI events (`LogRecord`). Requires configuring a `LoggerProvider`. |
98109

99110
### Configuration Examples
100111

@@ -103,7 +114,11 @@ You can control the Mem0 instrumentation using environment variables.
103114
export OTEL_INSTRUMENTATION_MEM0_INNER_ENABLED=true
104115

105116
# Enable content capture (be careful with sensitive data)
106-
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
117+
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
118+
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT
119+
120+
# Enable event emission (requires LoggerProvider exporter)
121+
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true
107122
```
108123

109124
## Semantic Conventions Status

instrumentation-loongsuite/loongsuite-instrumentation-mem0/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ classifiers = [
1818
"License :: OSI Approved :: Apache Software License",
1919
"Programming Language :: Python",
2020
"Programming Language :: Python :: 3",
21-
"Programming Language :: Python :: 3.9",
2221
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
2423
"Programming Language :: Python :: 3.12",
@@ -29,6 +28,7 @@ dependencies = [
2928
"opentelemetry-api ~=1.37",
3029
"opentelemetry-instrumentation ~=0.58b0",
3130
"opentelemetry-semantic-conventions ~=0.58b0",
31+
"opentelemetry-util-genai ~= 0.2b0",
3232
]
3333

3434
[project.optional-dependencies]

instrumentation-loongsuite/loongsuite-instrumentation-mem0/src/opentelemetry/instrumentation/mem0/__init__.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from opentelemetry.instrumentation.mem0.version import __version__
2727
from opentelemetry.instrumentation.utils import unwrap
2828
from opentelemetry.semconv.schemas import Schemas
29+
from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler
2930

3031
# Module-level logger
3132
logger = logging.getLogger(__name__)
@@ -121,6 +122,19 @@ def _instrument(self, **kwargs: Any) -> None:
121122
if not tracer_provider:
122123
tracer_provider = trace_api.get_tracer_provider()
123124

125+
meter_provider = kwargs.get("meter_provider")
126+
127+
# Optional: logger provider for GenAI events (util will no-op if not provided)
128+
logger_provider = kwargs.get("logger_provider")
129+
130+
# Create util GenAI handler (strong dependency, no fallback).
131+
# Avoid singleton here so tests (and multiple tracer providers) don't leak across runs.
132+
telemetry_handler = ExtendedTelemetryHandler(
133+
tracer_provider=tracer_provider,
134+
meter_provider=meter_provider,
135+
logger_provider=logger_provider,
136+
)
137+
124138
# Create tracer
125139
tracer = trace_api.get_tracer(
126140
"opentelemetry.instrumentation.mem0",
@@ -129,10 +143,9 @@ def _instrument(self, **kwargs: Any) -> None:
129143
schema_url=Schemas.V1_28_0.value,
130144
)
131145

132-
# Wrap ThreadPoolExecutor.submit to support context propagation
133146
# Execute instrumentation (traces only, metrics removed)
134-
self._instrument_memory_operations(tracer)
135-
self._instrument_memory_client_operations(tracer)
147+
self._instrument_memory_operations(telemetry_handler)
148+
self._instrument_memory_client_operations(telemetry_handler)
136149
# Sub-phases controlled by toggle, avoid binding wrapper when disabled to reduce overhead
137150
if mem0_config.is_internal_phases_enabled():
138151
self._instrument_vector_operations(tracer)
@@ -379,7 +392,7 @@ def _wrapper(wrapped, instance, args, kwargs):
379392

380393
return _wrapper
381394

382-
def _instrument_memory_operations(self, tracer):
395+
def _instrument_memory_operations(self, telemetry_handler):
383396
"""Instrument Memory and AsyncMemory operations."""
384397
try:
385398
if (
@@ -393,7 +406,7 @@ def _instrument_memory_operations(self, tracer):
393406
)
394407
return
395408

396-
wrapper = MemoryOperationWrapper(tracer)
409+
wrapper = MemoryOperationWrapper(telemetry_handler)
397410

398411
# Instrument Memory (sync)
399412
for method in self._public_methods_of(
@@ -431,7 +444,7 @@ def _instrument_memory_operations(self, tracer):
431444
except Exception as e:
432445
logger.debug(f"Failed to instrument Memory operations: {e}")
433446

434-
def _instrument_memory_client_operations(self, tracer):
447+
def _instrument_memory_client_operations(self, telemetry_handler):
435448
"""Instrument MemoryClient and AsyncMemoryClient operations."""
436449
try:
437450
if (
@@ -445,7 +458,7 @@ def _instrument_memory_client_operations(self, tracer):
445458
)
446459
return
447460

448-
wrapper = MemoryOperationWrapper(tracer)
461+
wrapper = MemoryOperationWrapper(telemetry_handler)
449462

450463
# Instrument MemoryClient (sync)
451464
for method in self._public_methods_of(

instrumentation-loongsuite/loongsuite-instrumentation-mem0/src/opentelemetry/instrumentation/mem0/config.py

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@
55
from __future__ import annotations
66

77
import os
8-
from dataclasses import dataclass
9-
from typing import Optional
10-
11-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
12-
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
13-
)
14-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH = (
15-
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH"
16-
)
178

189
SLOW_REQUEST_THRESHOLD_SECONDS = 5.0
1910

@@ -58,51 +49,6 @@ def first_present_bool(keys: list[str], default: bool) -> bool:
5849
return default
5950

6051

61-
@dataclass
62-
class GenAITelemetryOptions:
63-
"""GenAI telemetry configuration options."""
64-
65-
capture_message_content: Optional[bool] = None
66-
capture_message_content_max_length: Optional[int] = None
67-
68-
def __post_init__(self):
69-
if self.capture_message_content is None:
70-
self.capture_message_content = (
71-
os.getenv(
72-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
73-
).lower()
74-
== "true"
75-
)
76-
77-
if self.capture_message_content_max_length is None:
78-
self.capture_message_content_max_length = int(
79-
os.getenv(
80-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH,
81-
"1048576",
82-
)
83-
)
84-
85-
def should_capture_content(self) -> bool:
86-
"""Check if content capture is enabled."""
87-
return self.capture_message_content
88-
89-
def truncate_content(self, content: str) -> str:
90-
"""Truncate content to max length if needed."""
91-
if not content:
92-
return content
93-
if len(content) > self.capture_message_content_max_length:
94-
return (
95-
content[: self.capture_message_content_max_length]
96-
+ "...[truncated]"
97-
)
98-
return content
99-
100-
@classmethod
101-
def from_env(cls) -> "GenAITelemetryOptions":
102-
"""Create configuration from environment variables."""
103-
return cls()
104-
105-
10652
class Mem0InstrumentationConfig:
10753
"""Mem0 instrumentation configuration."""
10854

@@ -124,16 +70,6 @@ def is_internal_phases_enabled() -> bool:
12470
)
12571

12672

127-
def should_capture_content() -> bool:
128-
"""Check if message content capture is enabled."""
129-
return GenAITelemetryOptions.from_env().should_capture_content()
130-
131-
13273
def get_slow_threshold_seconds() -> float:
13374
"""Get slow request threshold in seconds."""
13475
return SLOW_REQUEST_THRESHOLD_SECONDS
135-
136-
137-
def get_telemetry_options() -> GenAITelemetryOptions:
138-
"""Get GenAI telemetry configuration options."""
139-
return GenAITelemetryOptions.from_env()

0 commit comments

Comments
 (0)