Skip to content

Commit a7f011e

Browse files
committed
Add vcr tests for vertexai
1 parent e218307 commit a7f011e

File tree

4 files changed

+233
-36
lines changed

4 files changed

+233
-36
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"contents": [
6+
{
7+
"role": "user",
8+
"parts": [
9+
{
10+
"text": "Say this is a test"
11+
}
12+
]
13+
}
14+
]
15+
}
16+
headers:
17+
Accept:
18+
- '*/*'
19+
Accept-Encoding:
20+
- gzip, deflate
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '141'
25+
Content-Type:
26+
- application/json
27+
User-Agent:
28+
- python-requests/2.32.3
29+
method: POST
30+
uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-1.5-flash-002:generateContent?%24alt=json%3Benum-encoding%3Dint
31+
response:
32+
body:
33+
string: |-
34+
{
35+
"candidates": [
36+
{
37+
"content": {
38+
"role": "model",
39+
"parts": [
40+
{
41+
"text": "Okay, I understand. I'm ready for your test. Please proceed.\n"
42+
}
43+
]
44+
},
45+
"finishReason": 1,
46+
"avgLogprobs": -0.00556742831280357
47+
}
48+
],
49+
"usageMetadata": {
50+
"promptTokenCount": 5,
51+
"candidatesTokenCount": 19,
52+
"totalTokenCount": 24
53+
},
54+
"modelVersion": "gemini-1.5-flash-002"
55+
}
56+
headers:
57+
Content-Type:
58+
- application/json; charset=UTF-8
59+
Transfer-Encoding:
60+
- chunked
61+
Vary:
62+
- Origin
63+
- X-Origin
64+
- Referer
65+
content-length:
66+
- '451'
67+
status:
68+
code: 200
69+
message: OK
70+
version: 1

instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/conftest.py

Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
11
"""Unit tests configuration module."""
22

33
import json
4+
import os
5+
import re
6+
from typing import Any, Mapping, MutableMapping
47

58
import pytest
9+
import vertexai
610
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
715

16+
from opentelemetry.instrumentation.vertexai.utils import (
17+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
18+
)
19+
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
820
from opentelemetry.sdk._events import EventLoggerProvider
921
from opentelemetry.sdk._logs import LoggerProvider
1022
from opentelemetry.sdk._logs.export import (
1123
InMemoryLogExporter,
1224
SimpleLogRecordProcessor,
1325
)
26+
from opentelemetry.sdk.metrics import (
27+
MeterProvider,
28+
)
29+
from opentelemetry.sdk.metrics.export import (
30+
InMemoryMetricReader,
31+
)
1432
from opentelemetry.sdk.trace import TracerProvider
1533
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
1634
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
1735
InMemorySpanExporter,
1836
)
1937

38+
# from opentelemetry.instrumentation.vertexai_v2 import VertexAIInstrumentor
39+
2040

2141
@pytest.fixture(scope="function", name="span_exporter")
2242
def fixture_span_exporter():
@@ -30,6 +50,12 @@ def fixture_log_exporter():
3050
yield exporter
3151

3252

53+
@pytest.fixture(scope="function", name="metric_reader")
54+
def fixture_metric_reader():
55+
exporter = InMemoryMetricReader()
56+
yield exporter
57+
58+
3359
@pytest.fixture(scope="function", name="tracer_provider")
3460
def fixture_tracer_provider(span_exporter):
3561
provider = TracerProvider()
@@ -46,17 +72,110 @@ def fixture_event_logger_provider(log_exporter):
4672
return event_logger_provider
4773

4874

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(scope="function", name="metric_reader")
83+
def fixture_metric_reader():
84+
exporter = InMemoryMetricReader()
85+
yield exporter
86+
87+
88+
@pytest.fixture(autouse=True)
89+
def vertexai_init(vcr: VCR) -> None:
90+
# Unfortunately I couldn't find a nice way to globally reset the global_config for each
91+
# test because different vertex submodules reference the global instance directly
92+
# https://github.com/googleapis/python-aiplatform/blob/v1.74.0/google/cloud/aiplatform/initializer.py#L687
93+
# so this config will leak if we don't call init() for each test.
94+
95+
# When not recording (in CI), don't do any auth. That prevents trying to read application
96+
# default credentials from the filesystem or metadata server and oauth token exchange. This
97+
# is not the interesting part of our instrumentation to test.
98+
if vcr.record_mode is RecordMode.NONE:
99+
vertexai.init(credentials=AnonymousCredentials())
100+
else:
101+
vertexai.init()
102+
103+
104+
@pytest.fixture
105+
def instrument_no_content(
106+
tracer_provider, event_logger_provider, meter_provider
107+
):
108+
os.environ.update(
109+
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"}
110+
)
111+
112+
instrumentor = VertexAIInstrumentor()
113+
instrumentor.instrument(
114+
tracer_provider=tracer_provider,
115+
event_logger_provider=event_logger_provider,
116+
meter_provider=meter_provider,
117+
)
118+
119+
yield instrumentor
120+
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
121+
instrumentor.uninstrument()
122+
123+
124+
@pytest.fixture
125+
def instrument_with_content(
126+
tracer_provider, event_logger_provider, meter_provider
127+
):
128+
os.environ.update(
129+
{OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}
130+
)
131+
instrumentor = VertexAIInstrumentor()
132+
instrumentor.instrument(
133+
tracer_provider=tracer_provider,
134+
event_logger_provider=event_logger_provider,
135+
meter_provider=meter_provider,
136+
)
137+
138+
yield instrumentor
139+
os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
140+
instrumentor.uninstrument()
141+
142+
49143
@pytest.fixture(scope="module")
50144
def vcr_config():
145+
filter_header_regexes = [
146+
r"X-.*",
147+
"Server",
148+
"Date",
149+
"Expires",
150+
"Authorization",
151+
]
152+
153+
def filter_headers(headers: Mapping[str, str]) -> Mapping[str, str]:
154+
return {
155+
key: val
156+
for key, val in headers.items()
157+
if not any(
158+
re.match(filter_re, key, re.IGNORECASE)
159+
for filter_re in filter_header_regexes
160+
)
161+
}
162+
163+
def before_record_cb(request: Request):
164+
request.headers = filter_headers(request.headers)
165+
request.uri = re.sub(
166+
r"/projects/[^/]+/", "/projects/fake-project/", request.uri
167+
)
168+
return request
169+
170+
def before_response_cb(response: MutableMapping[str, Any]):
171+
response["headers"] = filter_headers(response["headers"])
172+
return response
173+
51174
return {
52-
"filter_headers": [
53-
("cookie", "test_cookie"),
54-
("authorization", "Bearer test_vertexai_api_key"),
55-
("vertexai-organization", "test_vertexai_org_id"),
56-
("vertexai-project", "test_vertexai_project_id"),
57-
],
58175
"decode_compressed_response": True,
59-
"before_record_response": scrub_response_headers,
176+
"before_record_request": before_record_cb,
177+
"before_record_response": before_response_cb,
178+
"ignore_hosts": ["oauth2.googleapis.com"],
60179
}
61180

62181

@@ -125,12 +244,3 @@ def deserialize(cassette_string):
125244
def fixture_vcr(vcr):
126245
vcr.register_serializer("yaml", PrettyPrintJSONBody)
127246
return vcr
128-
129-
130-
def scrub_response_headers(response):
131-
"""
132-
This scrubs sensitive response headers. Note they are case-sensitive!
133-
"""
134-
response["headers"]["vertexai-organization"] = "test_vertexai_org_id"
135-
response["headers"]["Set-Cookie"] = "test_set_cookie"
136-
return response
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
2+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
3+
InMemorySpanExporter,
4+
)
5+
import pytest
6+
from vertexai.generative_models import Content, GenerativeModel, Part
7+
8+
# from opentelemetry.semconv_ai import SpanAttributes
9+
10+
11+
@pytest.mark.vcr
12+
def test_vertexai_generate_content(
13+
span_exporter: InMemorySpanExporter,
14+
instrument_with_content: VertexAIInstrumentor,
15+
):
16+
model = GenerativeModel("gemini-1.5-flash-002")
17+
model.generate_content(
18+
[
19+
Content(role="user", parts=[Part.from_text("Say this is a test")]),
20+
]
21+
)
22+
23+
spans = span_exporter.get_finished_spans()
24+
assert [span.name for span in spans] == ["chat gemini-1.5-flash-002"]
25+
26+
vertexai_span = spans[0]
27+
assert len(spans) == 1
28+
29+
assert dict(vertexai_span.attributes) == {
30+
"gen_ai.operation.name": "chat",
31+
"gen_ai.request.model": "gemini-1.5-flash-002",
32+
"gen_ai.system": "vertex_ai",
33+
}
34+
# missing some e.g.
35+
# "gen_ai.response.model": "gemini-pro-vision",
36+
# "gen_ai.usage.output_tokens": 35,
37+
# "gen_ai.usage.input_tokens": 265,

instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_placeholder.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)