Skip to content

Commit 38be0b1

Browse files
committed
Improve redaction and test naming.
1 parent ed424fd commit 38be0b1

File tree

2 files changed

+131
-14
lines changed
  • instrumentation-genai/opentelemetry-instrumentation-google-genai/tests

2 files changed

+131
-14
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/common/auth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import google.auth
15+
import google.auth.credentials
16+
1617

1718
class FakeCredentials(google.auth.credentials.AnonymousCredentials):
1819

1920
def refresh(self, request):
20-
pass
21+
pass

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_e2e.py

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
secondary goal of this test. Detailed testing of the instrumentation
2424
output is the purview of the other tests in this directory."""
2525

26+
27+
import subprocess
28+
import json
29+
import yaml
30+
import google.auth
31+
import google.auth.credentials
2632
import google.genai
2733
from google.genai import types as genai_types
2834
import os
@@ -45,22 +51,28 @@ def _should_redact_header(header_key):
4551
return True
4652
if header_key.startswith('sec-goog'):
4753
return True
54+
if header_key in ['server', 'server-timing']:
55+
return True
4856
return False
4957

5058

5159
def _redact_headers(headers):
60+
to_redact = []
5261
for header_key in headers:
5362
if _should_redact_header(header_key.lower()):
54-
del headers[header_key]
55-
63+
to_redact.append(header_key)
64+
for header_key in to_redact:
65+
headers[header_key] = "<REDACTED>"
5666

5767
def _before_record_request(request):
58-
_redact_headers(request.headers)
68+
if request.headers:
69+
_redact_headers(request.headers)
5970
return request
6071

6172

6273
def _before_record_response(response):
63-
_redact_headers(response.headers)
74+
if hasattr(response, "headers") and response.headers:
75+
_redact_headers(response.headers)
6476
return response
6577

6678

@@ -89,13 +101,91 @@ def vcr_config():
89101
'key'
90102
],
91103
'filter_headers': [
104+
'x-goog-api-key',
92105
'authorization',
106+
'server',
107+
'Server'
108+
'Server-Timing',
109+
'Date',
93110
],
94111
'before_record_request': _before_record_request,
95112
'before_record_response': _before_record_response,
113+
'ignore_hosts': [
114+
'oauth2.googleapis.com',
115+
'iam.googleapis.com',
116+
],
96117
}
97118

98119

120+
class _LiteralBlockScalar(str):
121+
"""Formats the string as a literal block scalar, preserving whitespace and
122+
without interpreting escape characters"""
123+
124+
125+
def _literal_block_scalar_presenter(dumper, data):
126+
"""Represents a scalar string as a literal block, via '|' syntax"""
127+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
128+
129+
130+
@pytest.fixture(scope="module", autouse=True)
131+
def setup_yaml_pretty_formattinmg():
132+
yaml.add_representer(_LiteralBlockScalar, _literal_block_scalar_presenter)
133+
134+
135+
def _process_string_value(string_value):
136+
"""Pretty-prints JSON or returns long strings as a LiteralBlockScalar"""
137+
try:
138+
json_data = json.loads(string_value)
139+
return _LiteralBlockScalar(json.dumps(json_data, indent=2))
140+
except (ValueError, TypeError):
141+
if len(string_value) > 80:
142+
return _LiteralBlockScalar(string_value)
143+
return string_value
144+
145+
146+
def _convert_body_to_literal(data):
147+
"""Searches the data for body strings, attempting to pretty-print JSON"""
148+
if isinstance(data, dict):
149+
for key, value in data.items():
150+
# Handle response body case (e.g., response.body.string)
151+
if key == "body" and isinstance(value, dict) and "string" in value:
152+
value["string"] = _process_string_value(value["string"])
153+
154+
# Handle request body case (e.g., request.body)
155+
elif key == "body" and isinstance(value, str):
156+
data[key] = _process_string_value(value)
157+
158+
else:
159+
_convert_body_to_literal(value)
160+
161+
elif isinstance(data, list):
162+
for idx, choice in enumerate(data):
163+
data[idx] = _convert_body_to_literal(choice)
164+
165+
return data
166+
167+
168+
class _PrettyPrintJSONBody:
169+
"""This makes request and response body recordings more readable."""
170+
171+
@staticmethod
172+
def serialize(cassette_dict):
173+
cassette_dict = _convert_body_to_literal(cassette_dict)
174+
return yaml.dump(
175+
cassette_dict, default_flow_style=False, allow_unicode=True
176+
)
177+
178+
@staticmethod
179+
def deserialize(cassette_string):
180+
return yaml.load(cassette_string, Loader=yaml.Loader)
181+
182+
183+
@pytest.fixture(scope="module", autouse=True)
184+
def setup_vcr(vcr):
185+
vcr.register_serializer("yaml", _PrettyPrintJSONBody)
186+
return vcr
187+
188+
99189
@pytest.fixture
100190
def instrumentor():
101191
return GoogleGenAiSdkInstrumentor()
@@ -116,9 +206,10 @@ def otel_mocker():
116206
result.uninstall()
117207

118208

119-
@pytest.fixture(autouse=True, params=[True, False])
209+
@pytest.fixture(autouse=True, params=["logcontent", "excludecontent"])
120210
def setup_content_recording(request):
121-
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str(request.param)
211+
enabled = request.param == "logcontent"
212+
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str(enabled)
122213

123214

124215
@pytest.fixture
@@ -131,10 +222,29 @@ def in_replay_mode(vcr_record_mode):
131222
return vcr_record_mode == RecordMode.NONE
132223

133224

225+
def _try_get_project_from_gcloud():
226+
try:
227+
gcloud_call_result = subprocess.run("gcloud config get project", shell=True, capture_output=True)
228+
except subprocess.CalledProcessError:
229+
return None
230+
gcloud_output = gcloud_call_result.stdout.decode()
231+
return gcloud_output.strip()
232+
233+
134234
@pytest.fixture
135235
def gcloud_project(in_replay_mode):
136236
if in_replay_mode:
137237
return "test-project"
238+
project_envs = ["GCLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT"]
239+
for project_env in project_envs:
240+
project_env_val = os.getenv(project_env)
241+
if project_env_val:
242+
return project_env_val
243+
from_gcloud = _try_get_project_from_gcloud()
244+
if from_gcloud:
245+
os.environ["GOOGLE_CLOUD_PROJECT"] = from_gcloud
246+
os.environ["GCLOUD_PROJECT"] = from_gcloud
247+
return from_gcloud
138248
_, from_creds = google.auth.default()
139249
return from_creds
140250

@@ -151,7 +261,7 @@ def gcloud_credentials(in_replay_mode):
151261
if in_replay_mode:
152262
return FakeCredentials()
153263
creds, _ = google.auth.default()
154-
return creds
264+
return google.auth.credentials.with_scopes_if_required(creds, ["https://www.googleapis.com/auth/cloud-platform"])
155265

156266

157267
@pytest.fixture(autouse=True)
@@ -165,12 +275,13 @@ def gcloud_api_key(in_replay_mode):
165275
@pytest.fixture
166276
def nonvertex_client_factory(gcloud_api_key):
167277
def _factory():
278+
print(f"Using API key: {gcloud_api_key}")
168279
return google.genai.Client(api_key=gcloud_api_key)
169280
return _factory
170281

171282

172283
@pytest.fixture
173-
def vertex_client_factory(in_replay_mode):
284+
def vertex_client_factory(in_replay_mode, gcloud_project, gcloud_location, gcloud_credentials):
174285
def _factory():
175286
return google.genai.Client(
176287
vertexai=True,
@@ -180,21 +291,26 @@ def _factory():
180291
return _factory
181292

182293

183-
@pytest.fixture(params=[True, False])
184-
def use_vertex(request):
294+
@pytest.fixture(params=["vertexaiapi", "geminiapi"])
295+
def genai_sdk_backend(request):
185296
return request.param
186297

187298

299+
@pytest.fixture
300+
def use_vertex(genai_sdk_backend):
301+
return genai_sdk_backend == "vertexaiapi"
302+
303+
188304
@pytest.fixture
189305
def client(vertex_client_factory, nonvertex_client_factory, use_vertex):
190306
if use_vertex:
191307
return vertex_client_factory()
192308
return nonvertex_client_factory()
193309

194310

195-
@pytest.fixture(params=[True, False])
311+
@pytest.fixture(params=["sync", "async"])
196312
def is_async(request):
197-
return request.param
313+
return request.param == "async"
198314

199315

200316
@pytest.fixture(params=["gemini-1.0-flash", "gemini-2.0-flash"])

0 commit comments

Comments
 (0)