Skip to content

Commit b2f6b32

Browse files
committed
VertexAI emit user events
1 parent ec3c51d commit b2f6b32

File tree

5 files changed

+143
-20
lines changed

5 files changed

+143
-20
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Factories for event types described in
17+
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#system-event.
18+
19+
Hopefully this code can be autogenerated by Weaver once Gen AI semantic conventions are
20+
schematized in YAML and the Weaver tool supports it.
21+
"""
22+
23+
from opentelemetry._events import Event
24+
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
25+
from opentelemetry.util.types import AnyValue
26+
27+
28+
def user_event(
29+
*,
30+
role: str = "user",
31+
content: AnyValue = None,
32+
) -> Event:
33+
"""Creates a User event
34+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event
35+
"""
36+
body: dict[str, AnyValue] = {
37+
"role": role,
38+
}
39+
if content is not None:
40+
body["content"] = content
41+
return Event(
42+
name="gen_ai.user.message",
43+
attributes={
44+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
45+
},
46+
body=body,
47+
)
48+
49+
50+
def assistant_event(
51+
*,
52+
role: str = "model",
53+
content: AnyValue = None,
54+
) -> Event:
55+
"""Creates an Assistant event
56+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
57+
"""
58+
# TODO: add tool_calls once instrumentation supports it
59+
60+
return Event(
61+
name="gen_ai.assistant.message",
62+
attributes={
63+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
64+
},
65+
body={
66+
"role": role,
67+
"content": content,
68+
},
69+
)

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
GenerateContentParams,
2727
get_genai_request_attributes,
2828
get_span_name,
29+
request_to_events,
2930
)
3031
from opentelemetry.trace import SpanKind, Tracer
3132

@@ -107,13 +108,12 @@ def traced_method(
107108
name=span_name,
108109
kind=SpanKind.CLIENT,
109110
attributes=span_attributes,
110-
) as _span:
111-
# TODO: emit request events
112-
# if span.is_recording():
113-
# for message in kwargs.get("messages", []):
114-
# event_logger.emit(
115-
# message_to_event(message, capture_content)
116-
# )
111+
) as span:
112+
if span.is_recording():
113+
for event in request_to_events(
114+
params=params, capture_content=capture_content
115+
):
116+
event_logger.emit(event)
117117

118118
# TODO: set error.type attribute
119119
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@
1616

1717
import re
1818
from dataclasses import dataclass
19+
from enum import Enum
1920
from os import environ
2021
from typing import (
2122
TYPE_CHECKING,
23+
Iterable,
2224
Mapping,
2325
Sequence,
26+
cast,
2427
)
2528

29+
from opentelemetry._events import Event
30+
from opentelemetry.instrumentation.vertexai.events import user_event
2631
from opentelemetry.semconv._incubating.attributes import (
2732
gen_ai_attributes as GenAIAttributes,
2833
)
29-
from opentelemetry.util.types import AttributeValue
34+
from opentelemetry.util.types import AnyValue, AttributeValue
3035

3136
if TYPE_CHECKING:
3237
from google.cloud.aiplatform_v1.types import content, tool
@@ -137,3 +142,21 @@ def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str:
137142
if not model:
138143
return f"{name}"
139144
return f"{name} {model}"
145+
146+
147+
def request_to_events(
148+
*, params: GenerateContentParams, capture_content: bool
149+
) -> Iterable[Event]:
150+
for content in params.contents or []:
151+
if content.role == "model":
152+
# TODO: handle assistant message
153+
pass
154+
# Assume user event but role should be "user"
155+
else:
156+
request_content = None
157+
if capture_content:
158+
request_content = [
159+
cast(dict[str, AnyValue], type(part).to_dict(part)) # type: ignore[reportUnknownMemberType]
160+
for part in content.parts
161+
]
162+
yield user_event(role=content.role, content=request_content)

instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettes/test_generate_content_invalid_temperature.yaml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@ interactions:
44
{
55
"contents": [
66
{
7-
"role": "user",
7+
"role": "my_invalid_role",
88
"parts": [
99
{
1010
"text": "Say this is a test"
1111
}
1212
]
1313
}
14-
],
15-
"generationConfig": {
16-
"temperature": 1000.0
17-
}
14+
]
1815
}
1916
headers:
2017
Accept:
@@ -24,7 +21,7 @@ interactions:
2421
Connection:
2522
- keep-alive
2623
Content-Length:
27-
- '196'
24+
- '152'
2825
Content-Type:
2926
- application/json
3027
User-Agent:
@@ -37,7 +34,7 @@ interactions:
3734
{
3835
"error": {
3936
"code": 400,
40-
"message": "Unable to submit request because it has a temperature value of 1000 but the supported range is from 0 (inclusive) to 2.0001 (exclusive). Update the value and try again.",
37+
"message": "Please use a valid role: user, model.",
4138
"status": "INVALID_ARGUMENT",
4239
"details": []
4340
}
@@ -52,7 +49,7 @@ interactions:
5249
- X-Origin
5350
- Referer
5451
content-length:
55-
- '809'
52+
- '416'
5653
status:
5754
code: 400
5855
message: Bad Request

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
)
99

1010
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
11+
from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import (
12+
InMemoryLogExporter,
13+
)
1114
from opentelemetry.sdk.trace import ReadableSpan
1215
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
1316
InMemorySpanExporter,
@@ -18,6 +21,7 @@
1821
@pytest.mark.vcr
1922
def test_generate_content(
2023
span_exporter: InMemorySpanExporter,
24+
log_exporter: InMemoryLogExporter,
2125
instrument_with_content: VertexAIInstrumentor,
2226
):
2327
model = GenerativeModel("gemini-1.5-flash-002")
@@ -27,6 +31,7 @@ def test_generate_content(
2731
]
2832
)
2933

34+
# Emits span
3035
spans = span_exporter.get_finished_spans()
3136
assert len(spans) == 1
3237
assert spans[0].name == "chat gemini-1.5-flash-002"
@@ -36,6 +41,23 @@ def test_generate_content(
3641
"gen_ai.system": "vertex_ai",
3742
}
3843

44+
# Emits content event
45+
logs = log_exporter.get_finished_logs()
46+
assert len(logs) == 1
47+
log_record = logs[0].log_record
48+
span_context = spans[0].get_span_context()
49+
assert log_record.trace_id == span_context.trace_id
50+
assert log_record.span_id == span_context.span_id
51+
assert log_record.trace_flags == span_context.trace_flags
52+
assert log_record.attributes == {
53+
"gen_ai.system": "vertex_ai",
54+
"event.name": "gen_ai.user.message",
55+
}
56+
assert log_record.body == {
57+
"content": [{"text": "Say this is a test"}],
58+
"role": "user",
59+
}
60+
3961

4062
@pytest.mark.vcr
4163
def test_generate_content_empty_model(
@@ -98,18 +120,19 @@ def test_generate_content_missing_model(
98120
@pytest.mark.vcr
99121
def test_generate_content_invalid_temperature(
100122
span_exporter: InMemorySpanExporter,
123+
log_exporter: InMemoryLogExporter,
101124
instrument_with_content: VertexAIInstrumentor,
102125
):
103126
model = GenerativeModel("gemini-1.5-flash-002")
104127
try:
105-
# Temperature out of range causes error
128+
# Fails because role must be "user" or "model"
106129
model.generate_content(
107130
[
108131
Content(
109-
role="user", parts=[Part.from_text("Say this is a test")]
132+
role="my_invalid_role",
133+
parts=[Part.from_text("Say this is a test")],
110134
)
111135
],
112-
generation_config=GenerationConfig(temperature=1000),
113136
)
114137
except BadRequest:
115138
pass
@@ -120,11 +143,22 @@ def test_generate_content_invalid_temperature(
120143
assert dict(spans[0].attributes) == {
121144
"gen_ai.operation.name": "chat",
122145
"gen_ai.request.model": "gemini-1.5-flash-002",
123-
"gen_ai.request.temperature": 1000.0,
124146
"gen_ai.system": "vertex_ai",
125147
}
126148
assert_span_error(spans[0])
127149

150+
# Emits the faulty content which caused the request to fail
151+
logs = log_exporter.get_finished_logs()
152+
assert len(logs) == 1
153+
assert logs[0].log_record.attributes == {
154+
"gen_ai.system": "vertex_ai",
155+
"event.name": "gen_ai.user.message",
156+
}
157+
assert logs[0].log_record.body == {
158+
"content": [{"text": "Say this is a test"}],
159+
"role": "my_invalid_role",
160+
}
161+
128162

129163
@pytest.mark.vcr()
130164
def test_generate_content_extra_params(span_exporter, instrument_no_content):

0 commit comments

Comments
 (0)