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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-instrumentation-genai-openai-agents`: Move capture_message_content kwarg logic to genai-utils
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
)
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.trace import get_tracer
from opentelemetry.util.genai.types import ContentCapturingMode
from opentelemetry.util.genai.utils import resolve_capture_message_content

from .package import _instruments
from .span_processor import (
Expand All @@ -41,9 +43,7 @@

logger = logging.getLogger(__name__)

_CONTENT_CAPTURE_ENV = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
_SYSTEM_OVERRIDE_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_SYSTEM"
_CAPTURE_CONTENT_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT"
_CAPTURE_METRICS_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS"


Expand All @@ -70,47 +70,6 @@ def _resolve_system(value: str | None) -> str:
return value


def _resolve_content_mode(value: Any) -> ContentCaptureMode:
if isinstance(value, ContentCaptureMode):
return value
if isinstance(value, bool):
return (
ContentCaptureMode.SPAN_AND_EVENT
if value
else ContentCaptureMode.NO_CONTENT
)

if value is None:
return ContentCaptureMode.SPAN_AND_EVENT

text = str(value).strip().lower()
if not text:
return ContentCaptureMode.SPAN_AND_EVENT

mapping = {
"span_only": ContentCaptureMode.SPAN_ONLY,
"span-only": ContentCaptureMode.SPAN_ONLY,
"span": ContentCaptureMode.SPAN_ONLY,
"event_only": ContentCaptureMode.EVENT_ONLY,
"event-only": ContentCaptureMode.EVENT_ONLY,
"event": ContentCaptureMode.EVENT_ONLY,
"span_and_event": ContentCaptureMode.SPAN_AND_EVENT,
"span-and-event": ContentCaptureMode.SPAN_AND_EVENT,
"span_and_events": ContentCaptureMode.SPAN_AND_EVENT,
"all": ContentCaptureMode.SPAN_AND_EVENT,
"true": ContentCaptureMode.SPAN_AND_EVENT,
"1": ContentCaptureMode.SPAN_AND_EVENT,
"yes": ContentCaptureMode.SPAN_AND_EVENT,
"no_content": ContentCaptureMode.NO_CONTENT,
"false": ContentCaptureMode.NO_CONTENT,
"0": ContentCaptureMode.NO_CONTENT,
"no": ContentCaptureMode.NO_CONTENT,
"none": ContentCaptureMode.NO_CONTENT,
}

return mapping.get(text, ContentCaptureMode.SPAN_AND_EVENT)


def _resolve_bool(value: Any, default: bool) -> bool:
if value is None:
return default
Expand Down Expand Up @@ -148,12 +107,9 @@ def _instrument(self, **kwargs) -> None:
)
system = _resolve_system(system_override)

content_override = kwargs.get("capture_message_content")
if content_override is None:
content_override = os.getenv(_CONTENT_CAPTURE_ENV) or os.getenv(
_CAPTURE_CONTENT_ENV
)
content_mode = _resolve_content_mode(content_override)
content_mode = resolve_capture_message_content(
kwargs.get("capture_message_content")
)
Comment thread
rads-1996 marked this conversation as resolved.
Comment thread
rads-1996 marked this conversation as resolved.

metrics_override = kwargs.get("capture_metrics")
if metrics_override is None:
Expand All @@ -171,7 +127,7 @@ def _instrument(self, **kwargs) -> None:
tracer=tracer,
system_name=system,
include_sensitive_data=content_mode
!= ContentCaptureMode.NO_CONTENT,
!= ContentCapturingMode.NO_CONTENT,
content_mode=content_mode,
metrics_enabled=metrics_enabled,
agent_name=agent_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Sequence
from urllib.parse import urlparse

Expand Down Expand Up @@ -73,8 +72,13 @@
Tracer,
set_span_in_context,
)
from opentelemetry.util.genai.types import ContentCapturingMode
from opentelemetry.util.types import AttributeValue

# Backwards-compatible alias for the canonical ``ContentCapturingMode`` enum
# now defined in ``opentelemetry.util.genai.types``.
ContentCaptureMode = ContentCapturingMode
Comment thread
rads-1996 marked this conversation as resolved.

# Import all semantic convention constants
# ---- GenAI semantic convention helpers (embedded from constants.py) ----

Expand Down Expand Up @@ -307,29 +311,6 @@ def normalize_output_type(output_type: Optional[str]) -> str:
GEN_AI_SYSTEM_KEY = getattr(GenAIAttributes, "GEN_AI_SYSTEM", "gen_ai.system")


class ContentCaptureMode(Enum):
"""Controls whether sensitive content is recorded on spans, events, or both."""

NO_CONTENT = "no_content"
SPAN_ONLY = "span_only"
EVENT_ONLY = "event_only"
SPAN_AND_EVENT = "span_and_event"

@property
def capture_in_span(self) -> bool:
return self in (
ContentCaptureMode.SPAN_ONLY,
ContentCaptureMode.SPAN_AND_EVENT,
)

@property
def capture_in_event(self) -> bool:
return self in (
ContentCaptureMode.EVENT_ONLY,
ContentCaptureMode.SPAN_AND_EVENT,
)


@dataclass
class ContentPayload:
"""Container for normalized content associated with a span."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def _instrument_with_provider(**instrument_kwargs):
exporter = InMemorySpanExporter()
provider.add_span_processor(SimpleSpanProcessor(exporter))

# Most tests in this file inspect captured content, so default to
# ``span_and_event`` unless the caller overrides.
instrument_kwargs.setdefault("capture_message_content", "span_and_event")

instrumentor = OpenAIAgentsInstrumentor()
instrumentor.instrument(tracer_provider=provider, **instrument_kwargs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Tests for improving coverage of OpenAI Agents instrumentation.

This file targets uncovered lines identified in the coverage report:
- __init__.py: _resolve_system, _resolve_bool, _resolve_content_mode
- __init__.py: _resolve_system, _resolve_bool
- span_processor.py: normalization utilities, span data handlers, message normalization,
output type inference, metrics recording, and various edge cases.

Expand Down Expand Up @@ -158,77 +158,6 @@ def test_resolve_bool_returns_default_for_unknown(self):
assert init_module._resolve_bool("maybe", False) is False


class TestResolveContentMode:
"""Tests for _resolve_content_mode function."""

def test_resolve_content_mode_returns_value_for_enum(self):
sp, init_module = _get_modules()
result = init_module._resolve_content_mode(
sp.ContentCaptureMode.SPAN_ONLY
)
assert result == sp.ContentCaptureMode.SPAN_ONLY

def test_resolve_content_mode_for_bool_true(self):
sp, init_module = _get_modules()
result = init_module._resolve_content_mode(True)
assert result == sp.ContentCaptureMode.SPAN_AND_EVENT

def test_resolve_content_mode_for_bool_false(self):
sp, init_module = _get_modules()
result = init_module._resolve_content_mode(False)
assert result == sp.ContentCaptureMode.NO_CONTENT

def test_resolve_content_mode_for_none(self):
sp, init_module = _get_modules()
result = init_module._resolve_content_mode(None)
assert result == sp.ContentCaptureMode.SPAN_AND_EVENT

def test_resolve_content_mode_for_empty_string(self):
sp, init_module = _get_modules()
result = init_module._resolve_content_mode("")
assert result == sp.ContentCaptureMode.SPAN_AND_EVENT

def test_resolve_content_mode_span_only_variants(self):
sp, init_module = _get_modules()
for value in ["span_only", "span-only", "span"]:
result = init_module._resolve_content_mode(value)
assert result == sp.ContentCaptureMode.SPAN_ONLY, (
f"Failed for {value}"
)

def test_resolve_content_mode_event_only_variants(self):
sp, init_module = _get_modules()
for value in ["event_only", "event-only", "event"]:
result = init_module._resolve_content_mode(value)
assert result == sp.ContentCaptureMode.EVENT_ONLY, (
f"Failed for {value}"
)

def test_resolve_content_mode_span_and_event_variants(self):
sp, init_module = _get_modules()
for value in [
"span_and_event",
"span-and-event",
"span_and_events",
"all",
"true",
"1",
"yes",
]:
result = init_module._resolve_content_mode(value)
assert result == sp.ContentCaptureMode.SPAN_AND_EVENT, (
f"Failed for {value}"
)

def test_resolve_content_mode_no_content_variants(self):
sp, init_module = _get_modules()
for value in ["no_content", "false", "0", "no", "none"]:
result = init_module._resolve_content_mode(value)
assert result == sp.ContentCaptureMode.NO_CONTENT, (
f"Failed for {value}"
)


# ============================================================================
# Tests for span_processor.py normalization utilities
# ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ class ContentCapturingMode(Enum):
# Capture content in both spans and events.
SPAN_AND_EVENT = 3

@property
def capture_in_span(self) -> bool:
return self in (
ContentCapturingMode.SPAN_ONLY,
ContentCapturingMode.SPAN_AND_EVENT,
)

@property
def capture_in_event(self) -> bool:
return self in (
ContentCapturingMode.EVENT_ONLY,
ContentCapturingMode.SPAN_AND_EVENT,
)


@dataclass()
class GenericPart:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@
logger = logging.getLogger(__name__)


# Map of accepted kwarg string aliases to ContentCapturingMode. Kept lenient
# for backwards compatibility with existing instrumentations that historically
# accepted boolean-ish strings or hyphenated forms.
_CAPTURE_MODE_ALIASES: dict[str, ContentCapturingMode] = {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Do we still want to allow for such flexibility or make it more stricter?

"span_only": ContentCapturingMode.SPAN_ONLY,
"span-only": ContentCapturingMode.SPAN_ONLY,
"span": ContentCapturingMode.SPAN_ONLY,
"event_only": ContentCapturingMode.EVENT_ONLY,
"event-only": ContentCapturingMode.EVENT_ONLY,
"event": ContentCapturingMode.EVENT_ONLY,
"span_and_event": ContentCapturingMode.SPAN_AND_EVENT,
"span-and-event": ContentCapturingMode.SPAN_AND_EVENT,
"span_and_events": ContentCapturingMode.SPAN_AND_EVENT,
"all": ContentCapturingMode.SPAN_AND_EVENT,
"true": ContentCapturingMode.SPAN_AND_EVENT,
"1": ContentCapturingMode.SPAN_AND_EVENT,
"yes": ContentCapturingMode.SPAN_AND_EVENT,
"no_content": ContentCapturingMode.NO_CONTENT,
"false": ContentCapturingMode.NO_CONTENT,
"0": ContentCapturingMode.NO_CONTENT,
"no": ContentCapturingMode.NO_CONTENT,
"none": ContentCapturingMode.NO_CONTENT,
}


def get_content_capturing_mode() -> ContentCapturingMode:
"""Gets ContentCapturingMode from associated envvar, defaulting to NO_CONTENT if unset."""
envvar = os.environ.get(
Expand Down Expand Up @@ -103,3 +128,30 @@ def default(self, o: Any) -> Any:
)
"""Should be used by GenAI instrumentations when serializing objects that may contain
bytes, datetimes, etc. for GenAI observability."""


def resolve_capture_message_content(
capture_message_content: Any = None,
) -> ContentCapturingMode:
"""Resolve the effective content-capture mode for a GenAI instrumentation."""
if isinstance(capture_message_content, ContentCapturingMode):
return capture_message_content
if isinstance(capture_message_content, bool):
return (
ContentCapturingMode.SPAN_AND_EVENT
if capture_message_content
else ContentCapturingMode.NO_CONTENT
)
if capture_message_content is None:
return get_content_capturing_mode()
text = str(capture_message_content).strip().lower()
if not text:
return get_content_capturing_mode()
mode = _CAPTURE_MODE_ALIASES.get(text)
if mode is not None:
return mode
logger.warning(
"Unrecognized capture_message_content value %r; falling back to NO_CONTENT.",
capture_message_content,
)
return ContentCapturingMode.NO_CONTENT
Loading