Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions .github/workflows/loongsuite_test_0.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,44 +374,6 @@ jobs:
- name: Run tests
run: tox -c tox-loongsuite.ini -e py313-test-loongsuite-instrumentation-dashscope-latest -- -ra

py39-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest:
name: LoongSuite loongsuite-instrumentation-mem0-oldest 3.9 Ubuntu
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repo @ SHA - ${{ github.sha }}
uses: actions/checkout@v4

- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: "3.9"

- name: Install tox
run: pip install tox-uv

- name: Run tests
run: tox -c tox-loongsuite.ini -e py39-test-loongsuite-instrumentation-mem0-oldest -- -ra

py39-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest:
name: LoongSuite loongsuite-instrumentation-mem0-latest 3.9 Ubuntu
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repo @ SHA - ${{ github.sha }}
uses: actions/checkout@v4

- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: "3.9"

- name: Install tox
run: pip install tox-uv

- name: Run tests
run: tox -c tox-loongsuite.ini -e py39-test-loongsuite-instrumentation-mem0-latest -- -ra

py310-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest:
name: LoongSuite loongsuite-instrumentation-mem0-oldest 3.10 Ubuntu
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ repos:
hooks:
- id: rstcheck
additional_dependencies: ['rstcheck[sphinx]']
args: ["--report-level", "warning"]
args: ["--report-level", "warning"]
2 changes: 1 addition & 1 deletion CHANGELOG-loongsuite.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# Added

- `loongsuite-instrumentation-mem0`: add support for mem0
- `loongsuite-instrumentation-mem0`: add support for mem0 && use memory handler
([#67](https://github.com/alibaba/loongsuite-python-agent/pull/67))
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<trace_endpoint>
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_SERVICE_NAME=mem0-demo

# Enable GenAI experimental semantic conventions (required for GenAI content/event features)
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental

# (Optional) Capture message content – may contain sensitive data
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
# Must be one of: NO_CONTENT | SPAN_ONLY | EVENT_ONLY | SPAN_AND_EVENT
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY

# (Optional) Emit GenAI events (LogRecord). Requires a LoggerProvider exporter in your app.
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true
```

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

```bash
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true

loongsuite-instrument \
--traces_exporter console \
Expand All @@ -94,7 +103,9 @@ You can control the Mem0 instrumentation using environment variables.
|-----------------------------------------------------------|---------|-----------------------------------------------------------------------------|
| `OTEL_INSTRUMENTATION_MEM0_ENABLED` | `true` | Enable or disable the Mem0 instrumentation entirely. |
| `OTEL_INSTRUMENTATION_MEM0_INNER_ENABLED` | `false` | Enable internal phases (Vector Store, Graph Store, Rerank). |
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `false` | Capture input/output message content (may contain PII or sensitive data). |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | *(empty)* | Set to `gen_ai_latest_experimental` to enable GenAI experimental semantics (required for content/event). |
| `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). |
| `OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT` | `false` | Emit GenAI events (`LogRecord`). Requires configuring a `LoggerProvider` exporter. |

### Configuration Examples

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

# Enable content capture (be careful with sensitive data)
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT

# Enable event emission (requires LoggerProvider exporter)
export OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT=true
```

## Semantic Conventions Status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -29,6 +28,7 @@ dependencies = [
"opentelemetry-api ~=1.37",
"opentelemetry-instrumentation ~=0.58b0",
"opentelemetry-semantic-conventions ~=0.58b0",
"opentelemetry-util-genai ~= 0.2b0",
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from opentelemetry.instrumentation.mem0.version import __version__
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler

# Module-level logger
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -121,6 +122,16 @@ def _instrument(self, **kwargs: Any) -> None:
if not tracer_provider:
tracer_provider = trace_api.get_tracer_provider()

# Optional: logger provider for GenAI events (util will no-op if not provided)
logger_provider = kwargs.get("logger_provider")

# Create util GenAI handler (strong dependency, no fallback).
# Avoid singleton here so tests (and multiple tracer providers) don't leak across runs.
telemetry_handler = ExtendedTelemetryHandler(
tracer_provider=tracer_provider,
logger_provider=logger_provider,
)

# Create tracer
tracer = trace_api.get_tracer(
"opentelemetry.instrumentation.mem0",
Expand All @@ -129,10 +140,9 @@ def _instrument(self, **kwargs: Any) -> None:
schema_url=Schemas.V1_28_0.value,
)

# Wrap ThreadPoolExecutor.submit to support context propagation
# Execute instrumentation (traces only, metrics removed)
self._instrument_memory_operations(tracer)
self._instrument_memory_client_operations(tracer)
self._instrument_memory_operations(telemetry_handler)
self._instrument_memory_client_operations(telemetry_handler)
# Sub-phases controlled by toggle, avoid binding wrapper when disabled to reduce overhead
if mem0_config.is_internal_phases_enabled():
self._instrument_vector_operations(tracer)
Expand Down Expand Up @@ -379,7 +389,7 @@ def _wrapper(wrapped, instance, args, kwargs):

return _wrapper

def _instrument_memory_operations(self, tracer):
def _instrument_memory_operations(self, telemetry_handler):
"""Instrument Memory and AsyncMemory operations."""
try:
if (
Expand All @@ -393,7 +403,7 @@ def _instrument_memory_operations(self, tracer):
)
return

wrapper = MemoryOperationWrapper(tracer)
wrapper = MemoryOperationWrapper(telemetry_handler)

# Instrument Memory (sync)
for method in self._public_methods_of(
Expand Down Expand Up @@ -431,7 +441,7 @@ def _instrument_memory_operations(self, tracer):
except Exception as e:
logger.debug(f"Failed to instrument Memory operations: {e}")

def _instrument_memory_client_operations(self, tracer):
def _instrument_memory_client_operations(self, telemetry_handler):
"""Instrument MemoryClient and AsyncMemoryClient operations."""
try:
if (
Expand All @@ -445,7 +455,7 @@ def _instrument_memory_client_operations(self, tracer):
)
return

wrapper = MemoryOperationWrapper(tracer)
wrapper = MemoryOperationWrapper(telemetry_handler)

# Instrument MemoryClient (sync)
for method in self._public_methods_of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@
from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Optional

OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
)
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH = (
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH"
)

SLOW_REQUEST_THRESHOLD_SECONDS = 5.0

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


@dataclass
class GenAITelemetryOptions:
"""GenAI telemetry configuration options."""

capture_message_content: Optional[bool] = None
capture_message_content_max_length: Optional[int] = None

def __post_init__(self):
if self.capture_message_content is None:
self.capture_message_content = (
os.getenv(
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
).lower()
== "true"
)

if self.capture_message_content_max_length is None:
self.capture_message_content_max_length = int(
os.getenv(
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT_MAX_LENGTH,
"1048576",
)
)

def should_capture_content(self) -> bool:
"""Check if content capture is enabled."""
return self.capture_message_content

def truncate_content(self, content: str) -> str:
"""Truncate content to max length if needed."""
if not content:
return content
if len(content) > self.capture_message_content_max_length:
return (
content[: self.capture_message_content_max_length]
+ "...[truncated]"
)
return content

@classmethod
def from_env(cls) -> "GenAITelemetryOptions":
"""Create configuration from environment variables."""
return cls()


class Mem0InstrumentationConfig:
"""Mem0 instrumentation configuration."""

Expand All @@ -124,16 +70,6 @@ def is_internal_phases_enabled() -> bool:
)


def should_capture_content() -> bool:
"""Check if message content capture is enabled."""
return GenAITelemetryOptions.from_env().should_capture_content()


def get_slow_threshold_seconds() -> float:
"""Get slow request threshold in seconds."""
return SLOW_REQUEST_THRESHOLD_SECONDS


def get_telemetry_options() -> GenAITelemetryOptions:
"""Get GenAI telemetry configuration options."""
return GenAITelemetryOptions.from_env()
Loading