mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-08-02 19:47:17 +08:00

* small fixes in OpenAI examples * up * add comment * leverage histogram bucket advice in 1.30 * Update instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_metrics.py * Update instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_metrics.py * up --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
243 lines
7.0 KiB
Python
243 lines
7.0 KiB
Python
"""Unit tests configuration module."""
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
import yaml
|
|
from openai import AsyncOpenAI, OpenAI
|
|
|
|
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
|
|
from opentelemetry.instrumentation.openai_v2.utils import (
|
|
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
|
|
)
|
|
from opentelemetry.sdk._events import EventLoggerProvider
|
|
from opentelemetry.sdk._logs import LoggerProvider
|
|
from opentelemetry.sdk._logs.export import (
|
|
InMemoryLogExporter,
|
|
SimpleLogRecordProcessor,
|
|
)
|
|
from opentelemetry.sdk.metrics import (
|
|
MeterProvider,
|
|
)
|
|
from opentelemetry.sdk.metrics.export import (
|
|
InMemoryMetricReader,
|
|
)
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
InMemorySpanExporter,
|
|
)
|
|
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
|
|
|
|
|
|
@pytest.fixture(scope="function", name="span_exporter")
|
|
def fixture_span_exporter():
|
|
exporter = InMemorySpanExporter()
|
|
yield exporter
|
|
|
|
|
|
@pytest.fixture(scope="function", name="log_exporter")
|
|
def fixture_log_exporter():
|
|
exporter = InMemoryLogExporter()
|
|
yield exporter
|
|
|
|
|
|
@pytest.fixture(scope="function", name="metric_reader")
|
|
def fixture_metric_reader():
|
|
exporter = InMemoryMetricReader()
|
|
yield exporter
|
|
|
|
|
|
@pytest.fixture(scope="function", name="tracer_provider")
|
|
def fixture_tracer_provider(span_exporter):
|
|
provider = TracerProvider()
|
|
provider.add_span_processor(SimpleSpanProcessor(span_exporter))
|
|
return provider
|
|
|
|
|
|
@pytest.fixture(scope="function", name="event_logger_provider")
|
|
def fixture_event_logger_provider(log_exporter):
|
|
provider = LoggerProvider()
|
|
provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
|
|
event_logger_provider = EventLoggerProvider(provider)
|
|
|
|
return event_logger_provider
|
|
|
|
|
|
@pytest.fixture(scope="function", name="meter_provider")
|
|
def fixture_meter_provider(metric_reader):
|
|
meter_provider = MeterProvider(
|
|
metric_readers=[metric_reader],
|
|
)
|
|
|
|
return meter_provider
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def environment():
|
|
if not os.getenv("OPENAI_API_KEY"):
|
|
os.environ["OPENAI_API_KEY"] = "test_openai_api_key"
|
|
|
|
|
|
@pytest.fixture
|
|
def openai_client():
|
|
return OpenAI()
|
|
|
|
|
|
@pytest.fixture
|
|
def async_openai_client():
|
|
return AsyncOpenAI()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def vcr_config():
|
|
return {
|
|
"filter_headers": [
|
|
("cookie", "test_cookie"),
|
|
("authorization", "Bearer test_openai_api_key"),
|
|
("openai-organization", "test_openai_org_id"),
|
|
("openai-project", "test_openai_project_id"),
|
|
],
|
|
"decode_compressed_response": True,
|
|
"before_record_response": scrub_response_headers,
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def instrument_no_content(
|
|
tracer_provider, event_logger_provider, meter_provider
|
|
):
|
|
os.environ.update(
|
|
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"}
|
|
)
|
|
|
|
instrumentor = OpenAIInstrumentor()
|
|
instrumentor.instrument(
|
|
tracer_provider=tracer_provider,
|
|
event_logger_provider=event_logger_provider,
|
|
meter_provider=meter_provider,
|
|
)
|
|
|
|
yield instrumentor
|
|
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
|
|
instrumentor.uninstrument()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def instrument_with_content(
|
|
tracer_provider, event_logger_provider, meter_provider
|
|
):
|
|
os.environ.update(
|
|
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}
|
|
)
|
|
instrumentor = OpenAIInstrumentor()
|
|
instrumentor.instrument(
|
|
tracer_provider=tracer_provider,
|
|
event_logger_provider=event_logger_provider,
|
|
meter_provider=meter_provider,
|
|
)
|
|
|
|
yield instrumentor
|
|
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
|
|
instrumentor.uninstrument()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def instrument_with_content_unsampled(
|
|
span_exporter, event_logger_provider, meter_provider
|
|
):
|
|
os.environ.update(
|
|
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}
|
|
)
|
|
|
|
tracer_provider = TracerProvider(sampler=ALWAYS_OFF)
|
|
tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
|
|
|
|
instrumentor = OpenAIInstrumentor()
|
|
instrumentor.instrument(
|
|
tracer_provider=tracer_provider,
|
|
event_logger_provider=event_logger_provider,
|
|
meter_provider=meter_provider,
|
|
)
|
|
|
|
yield instrumentor
|
|
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
|
|
instrumentor.uninstrument()
|
|
|
|
|
|
class LiteralBlockScalar(str):
|
|
"""Formats the string as a literal block scalar, preserving whitespace and
|
|
without interpreting escape characters"""
|
|
|
|
|
|
def literal_block_scalar_presenter(dumper, data):
|
|
"""Represents a scalar string as a literal block, via '|' syntax"""
|
|
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
|
|
|
|
|
yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter)
|
|
|
|
|
|
def process_string_value(string_value):
|
|
"""Pretty-prints JSON or returns long strings as a LiteralBlockScalar"""
|
|
try:
|
|
json_data = json.loads(string_value)
|
|
return LiteralBlockScalar(json.dumps(json_data, indent=2))
|
|
except (ValueError, TypeError):
|
|
if len(string_value) > 80:
|
|
return LiteralBlockScalar(string_value)
|
|
return string_value
|
|
|
|
|
|
def convert_body_to_literal(data):
|
|
"""Searches the data for body strings, attempting to pretty-print JSON"""
|
|
if isinstance(data, dict):
|
|
for key, value in data.items():
|
|
# Handle response body case (e.g., response.body.string)
|
|
if key == "body" and isinstance(value, dict) and "string" in value:
|
|
value["string"] = process_string_value(value["string"])
|
|
|
|
# Handle request body case (e.g., request.body)
|
|
elif key == "body" and isinstance(value, str):
|
|
data[key] = process_string_value(value)
|
|
|
|
else:
|
|
convert_body_to_literal(value)
|
|
|
|
elif isinstance(data, list):
|
|
for idx, choice in enumerate(data):
|
|
data[idx] = convert_body_to_literal(choice)
|
|
|
|
return data
|
|
|
|
|
|
class PrettyPrintJSONBody:
|
|
"""This makes request and response body recordings more readable."""
|
|
|
|
@staticmethod
|
|
def serialize(cassette_dict):
|
|
cassette_dict = convert_body_to_literal(cassette_dict)
|
|
return yaml.dump(
|
|
cassette_dict, default_flow_style=False, allow_unicode=True
|
|
)
|
|
|
|
@staticmethod
|
|
def deserialize(cassette_string):
|
|
return yaml.load(cassette_string, Loader=yaml.Loader)
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def fixture_vcr(vcr):
|
|
vcr.register_serializer("yaml", PrettyPrintJSONBody)
|
|
return vcr
|
|
|
|
|
|
def scrub_response_headers(response):
|
|
"""
|
|
This scrubs sensitive response headers. Note they are case-sensitive!
|
|
"""
|
|
response["headers"]["openai-organization"] = "test_openai_org_id"
|
|
response["headers"]["Set-Cookie"] = "test_set_cookie"
|
|
return response
|