Skip to content

Commit 53ff952

Browse files
committed
Copy and rename for git history
1 parent e654b89 commit 53ff952

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""Unit tests configuration module."""
2+
3+
import json
4+
import os
5+
import re
6+
from typing import Any, Mapping, MutableMapping
7+
8+
import pytest
9+
import vertexai
10+
import yaml
11+
from google.auth.credentials import AnonymousCredentials
12+
from vcr import VCR
13+
from vcr.record_mode import RecordMode
14+
from vcr.request import Request
15+
16+
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
17+
from opentelemetry.instrumentation.vertexai.utils import (
18+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
19+
)
20+
from opentelemetry.sdk._events import EventLoggerProvider
21+
from opentelemetry.sdk._logs import LoggerProvider
22+
from opentelemetry.sdk._logs.export import (
23+
InMemoryLogExporter,
24+
SimpleLogRecordProcessor,
25+
)
26+
from opentelemetry.sdk.metrics import (
27+
MeterProvider,
28+
)
29+
from opentelemetry.sdk.metrics.export import (
30+
InMemoryMetricReader,
31+
)
32+
from opentelemetry.sdk.trace import TracerProvider
33+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
34+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
35+
InMemorySpanExporter,
36+
)
37+
38+
FAKE_PROJECT = "fake-project"
39+
40+
41+
@pytest.fixture(scope="function", name="span_exporter")
42+
def fixture_span_exporter():
43+
exporter = InMemorySpanExporter()
44+
yield exporter
45+
46+
47+
@pytest.fixture(scope="function", name="log_exporter")
48+
def fixture_log_exporter():
49+
exporter = InMemoryLogExporter()
50+
yield exporter
51+
52+
53+
@pytest.fixture(scope="function", name="metric_reader")
54+
def fixture_metric_reader():
55+
exporter = InMemoryMetricReader()
56+
yield exporter
57+
58+
59+
@pytest.fixture(scope="function", name="tracer_provider")
60+
def fixture_tracer_provider(span_exporter):
61+
provider = TracerProvider()
62+
provider.add_span_processor(SimpleSpanProcessor(span_exporter))
63+
return provider
64+
65+
66+
@pytest.fixture(scope="function", name="event_logger_provider")
67+
def fixture_event_logger_provider(log_exporter):
68+
provider = LoggerProvider()
69+
provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
70+
event_logger_provider = EventLoggerProvider(provider)
71+
72+
return event_logger_provider
73+
74+
75+
@pytest.fixture(scope="function", name="meter_provider")
76+
def fixture_meter_provider(metric_reader):
77+
return MeterProvider(
78+
metric_readers=[metric_reader],
79+
)
80+
81+
82+
@pytest.fixture(autouse=True)
83+
def vertexai_init(vcr: VCR) -> None:
84+
# When not recording (in CI), don't do any auth. That prevents trying to read application
85+
# default credentials from the filesystem or metadata server and oauth token exchange. This
86+
# is not the interesting part of our instrumentation to test.
87+
credentials = None
88+
project = None
89+
if vcr.record_mode == RecordMode.NONE:
90+
credentials = AnonymousCredentials()
91+
project = FAKE_PROJECT
92+
vertexai.init(
93+
api_transport="rest", credentials=credentials, project=project
94+
)
95+
96+
97+
@pytest.fixture
98+
def instrument_no_content(
99+
tracer_provider, event_logger_provider, meter_provider
100+
):
101+
os.environ.update(
102+
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"}
103+
)
104+
105+
instrumentor = VertexAIInstrumentor()
106+
instrumentor.instrument(
107+
tracer_provider=tracer_provider,
108+
event_logger_provider=event_logger_provider,
109+
meter_provider=meter_provider,
110+
)
111+
112+
yield instrumentor
113+
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
114+
instrumentor.uninstrument()
115+
116+
117+
@pytest.fixture
118+
def instrument_with_content(
119+
tracer_provider, event_logger_provider, meter_provider
120+
):
121+
os.environ.update(
122+
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}
123+
)
124+
instrumentor = VertexAIInstrumentor()
125+
instrumentor.instrument(
126+
tracer_provider=tracer_provider,
127+
event_logger_provider=event_logger_provider,
128+
meter_provider=meter_provider,
129+
)
130+
131+
yield instrumentor
132+
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
133+
instrumentor.uninstrument()
134+
135+
136+
@pytest.fixture(scope="module")
137+
def vcr_config():
138+
filter_header_regexes = [
139+
r"X-.*",
140+
"Server",
141+
"Date",
142+
"Expires",
143+
"Authorization",
144+
]
145+
146+
def filter_headers(headers: Mapping[str, str]) -> Mapping[str, str]:
147+
return {
148+
key: val
149+
for key, val in headers.items()
150+
if not any(
151+
re.match(filter_re, key, re.IGNORECASE)
152+
for filter_re in filter_header_regexes
153+
)
154+
}
155+
156+
def before_record_cb(request: Request):
157+
request.headers = filter_headers(request.headers)
158+
request.uri = re.sub(
159+
r"/projects/[^/]+/", "/projects/fake-project/", request.uri
160+
)
161+
return request
162+
163+
def before_response_cb(response: MutableMapping[str, Any]):
164+
response["headers"] = filter_headers(response["headers"])
165+
return response
166+
167+
return {
168+
"decode_compressed_response": True,
169+
"before_record_request": before_record_cb,
170+
"before_record_response": before_response_cb,
171+
"ignore_hosts": ["oauth2.googleapis.com"],
172+
}
173+
174+
175+
class LiteralBlockScalar(str):
176+
"""Formats the string as a literal block scalar, preserving whitespace and
177+
without interpreting escape characters"""
178+
179+
180+
def literal_block_scalar_presenter(dumper, data):
181+
"""Represents a scalar string as a literal block, via '|' syntax"""
182+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
183+
184+
185+
yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter)
186+
187+
188+
def process_string_value(string_value):
189+
"""Pretty-prints JSON or returns long strings as a LiteralBlockScalar"""
190+
try:
191+
json_data = json.loads(string_value)
192+
return LiteralBlockScalar(json.dumps(json_data, indent=2))
193+
except (ValueError, TypeError):
194+
if len(string_value) > 80:
195+
return LiteralBlockScalar(string_value)
196+
return string_value
197+
198+
199+
def convert_body_to_literal(data):
200+
"""Searches the data for body strings, attempting to pretty-print JSON"""
201+
if isinstance(data, dict):
202+
for key, value in data.items():
203+
# Handle response body case (e.g., response.body.string)
204+
if key == "body" and isinstance(value, dict) and "string" in value:
205+
value["string"] = process_string_value(value["string"])
206+
207+
# Handle request body case (e.g., request.body)
208+
elif key == "body" and isinstance(value, str):
209+
data[key] = process_string_value(value)
210+
211+
else:
212+
convert_body_to_literal(value)
213+
214+
elif isinstance(data, list):
215+
for idx, choice in enumerate(data):
216+
data[idx] = convert_body_to_literal(choice)
217+
218+
return data
219+
220+
221+
class PrettyPrintJSONBody:
222+
"""This makes request and response body recordings more readable."""
223+
224+
@staticmethod
225+
def serialize(cassette_dict):
226+
cassette_dict = convert_body_to_literal(cassette_dict)
227+
return yaml.dump(
228+
cassette_dict, default_flow_style=False, allow_unicode=True
229+
)
230+
231+
@staticmethod
232+
def deserialize(cassette_string):
233+
return yaml.load(cassette_string, Loader=yaml.Loader)
234+
235+
236+
@pytest.fixture(scope="module", autouse=True)
237+
def fixture_vcr(vcr):
238+
vcr.register_serializer("yaml", PrettyPrintJSONBody)
239+
return vcr

0 commit comments

Comments
 (0)