opentelemetry-instrumentation-vertexai 0.38.7__py3-none-any.whl → 2.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentelemetry-instrumentation-vertexai might be problematic. Click here for more details.

@@ -1,368 +1,96 @@
1
- """OpenTelemetry Vertex AI instrumentation"""
2
-
3
- import logging
4
- import os
5
- import types
6
- from typing import Collection
7
- from opentelemetry.instrumentation.vertexai.config import Config
8
- from opentelemetry.instrumentation.vertexai.utils import dont_throw
9
- from wrapt import wrap_function_wrapper
10
-
11
- from opentelemetry import context as context_api
12
- from opentelemetry.trace import get_tracer, SpanKind
13
- from opentelemetry.trace.status import Status, StatusCode
14
-
15
- from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
16
- from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
17
-
18
- from opentelemetry.semconv_ai import (
19
- SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
20
- SpanAttributes,
21
- LLMRequestTypeValues,
22
- )
23
- from opentelemetry.instrumentation.vertexai.version import __version__
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- _instruments = ("google-cloud-aiplatform >= 1.38.1",)
28
-
29
- WRAPPED_METHODS = [
30
- {
31
- "package": "vertexai.generative_models",
32
- "object": "GenerativeModel",
33
- "method": "generate_content",
34
- "span_name": "vertexai.generate_content",
35
- "is_async": False,
36
- },
37
- {
38
- "package": "vertexai.generative_models",
39
- "object": "GenerativeModel",
40
- "method": "generate_content_async",
41
- "span_name": "vertexai.generate_content_async",
42
- "is_async": True,
43
- },
44
- {
45
- "package": "vertexai.preview.generative_models",
46
- "object": "GenerativeModel",
47
- "method": "generate_content",
48
- "span_name": "vertexai.generate_content",
49
- "is_async": False,
50
- },
51
- {
52
- "package": "vertexai.preview.generative_models",
53
- "object": "GenerativeModel",
54
- "method": "generate_content_async",
55
- "span_name": "vertexai.generate_content_async",
56
- "is_async": True,
57
- },
58
- {
59
- "package": "vertexai.language_models",
60
- "object": "TextGenerationModel",
61
- "method": "predict",
62
- "span_name": "vertexai.predict",
63
- "is_async": False,
64
- },
65
- {
66
- "package": "vertexai.language_models",
67
- "object": "TextGenerationModel",
68
- "method": "predict_async",
69
- "span_name": "vertexai.predict_async",
70
- "is_async": True,
71
- },
72
- {
73
- "package": "vertexai.language_models",
74
- "object": "TextGenerationModel",
75
- "method": "predict_streaming",
76
- "span_name": "vertexai.predict_streaming",
77
- "is_async": False,
78
- },
79
- {
80
- "package": "vertexai.language_models",
81
- "object": "TextGenerationModel",
82
- "method": "predict_streaming_async",
83
- "span_name": "vertexai.predict_streaming_async",
84
- "is_async": True,
85
- },
86
- {
87
- "package": "vertexai.language_models",
88
- "object": "ChatSession",
89
- "method": "send_message",
90
- "span_name": "vertexai.send_message",
91
- "is_async": False,
92
- },
93
- {
94
- "package": "vertexai.language_models",
95
- "object": "ChatSession",
96
- "method": "send_message_streaming",
97
- "span_name": "vertexai.send_message_streaming",
98
- "is_async": False,
99
- },
100
- ]
101
-
102
-
103
- def should_send_prompts():
104
- return (
105
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
106
- ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
107
-
108
-
109
- def is_streaming_response(response):
110
- return isinstance(response, types.GeneratorType)
111
-
112
-
113
- def is_async_streaming_response(response):
114
- return isinstance(response, types.AsyncGeneratorType)
115
-
116
-
117
- def _set_span_attribute(span, name, value):
118
- if value is not None:
119
- if value != "":
120
- span.set_attribute(name, value)
121
- return
122
-
123
-
124
- def _set_input_attributes(span, args, kwargs, llm_model):
125
- if should_send_prompts() and args is not None and len(args) > 0:
126
- prompt = ""
127
- for arg in args:
128
- if isinstance(arg, str):
129
- prompt = f"{prompt}{arg}\n"
130
- elif isinstance(arg, list):
131
- for subarg in arg:
132
- prompt = f"{prompt}{subarg}\n"
133
-
134
- _set_span_attribute(
135
- span,
136
- f"{SpanAttributes.LLM_PROMPTS}.0.user",
137
- prompt,
138
- )
139
-
140
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
141
- _set_span_attribute(
142
- span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
143
- )
144
- _set_span_attribute(
145
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
146
- )
147
- _set_span_attribute(
148
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
149
- )
150
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
151
- _set_span_attribute(span, SpanAttributes.LLM_TOP_K, kwargs.get("top_k"))
152
- _set_span_attribute(
153
- span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
154
- )
155
- _set_span_attribute(
156
- span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
157
- )
158
-
159
- return
160
-
161
-
162
- @dont_throw
163
- def _set_response_attributes(span, llm_model, generation_text, token_usage):
164
- _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, llm_model)
165
-
166
- if token_usage:
167
- _set_span_attribute(
168
- span,
169
- SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
170
- token_usage.total_token_count,
171
- )
172
- _set_span_attribute(
173
- span,
174
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
175
- token_usage.candidates_token_count,
176
- )
177
- _set_span_attribute(
178
- span,
179
- SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
180
- token_usage.prompt_token_count,
181
- )
182
-
183
- _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
184
- _set_span_attribute(
185
- span,
186
- f"{SpanAttributes.LLM_COMPLETIONS}.0.content",
187
- generation_text,
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
+ VertexAI client instrumentation supporting `google-cloud-aiplatform` SDK, it can be enabled by
17
+ using ``VertexAIInstrumentor``.
18
+
19
+ .. _vertexai: https://pypi.org/project/google-cloud-aiplatform/
20
+
21
+ Usage
22
+ -----
23
+
24
+ .. code:: python
25
+
26
+ import vertexai
27
+ from vertexai.generative_models import GenerativeModel
28
+ from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
29
+
30
+ VertexAIInstrumentor().instrument()
31
+
32
+ vertexai.init()
33
+ model = GenerativeModel("gemini-1.5-flash-002")
34
+ chat_completion = model.generate_content(
35
+ "Write a short poem on OpenTelemetry."
188
36
  )
189
37
 
38
+ API
39
+ ---
40
+ """
190
41
 
191
- def _build_from_streaming_response(span, response, llm_model):
192
- complete_response = ""
193
- token_usage = None
194
- for item in response:
195
- item_to_yield = item
196
- complete_response += str(item.text)
197
- if item.usage_metadata:
198
- token_usage = item.usage_metadata
199
-
200
- yield item_to_yield
201
-
202
- _set_response_attributes(span, llm_model, complete_response, token_usage)
203
-
204
- span.set_status(Status(StatusCode.OK))
205
- span.end()
206
-
207
-
208
- async def _abuild_from_streaming_response(span, response, llm_model):
209
- complete_response = ""
210
- token_usage = None
211
- async for item in response:
212
- item_to_yield = item
213
- complete_response += str(item.text)
214
- if item.usage_metadata:
215
- token_usage = item.usage_metadata
42
+ from typing import Any, Collection
216
43
 
217
- yield item_to_yield
218
-
219
- _set_response_attributes(span, llm_model, complete_response, token_usage)
220
-
221
- span.set_status(Status(StatusCode.OK))
222
- span.end()
223
-
224
-
225
- @dont_throw
226
- def _handle_request(span, args, kwargs, llm_model):
227
- if span.is_recording():
228
- _set_input_attributes(span, args, kwargs, llm_model)
229
-
230
-
231
- @dont_throw
232
- def _handle_response(span, response, llm_model):
233
- if span.is_recording():
234
- _set_response_attributes(
235
- span, llm_model, response.candidates[0].text, response.usage_metadata
236
- )
237
-
238
- span.set_status(Status(StatusCode.OK))
239
-
240
-
241
- def _with_tracer_wrapper(func):
242
- """Helper for providing tracer for wrapper functions."""
243
-
244
- def _with_tracer(tracer, to_wrap):
245
- def wrapper(wrapped, instance, args, kwargs):
246
- return func(tracer, to_wrap, wrapped, instance, args, kwargs)
247
-
248
- return wrapper
249
-
250
- return _with_tracer
251
-
252
-
253
- @_with_tracer_wrapper
254
- async def _awrap(tracer, to_wrap, wrapped, instance, args, kwargs):
255
- """Instruments and calls every function defined in TO_WRAP."""
256
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
257
- SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
258
- ):
259
- return await wrapped(*args, **kwargs)
260
-
261
- llm_model = "unknown"
262
- if hasattr(instance, "_model_id"):
263
- llm_model = instance._model_id
264
- if hasattr(instance, "_model_name"):
265
- llm_model = instance._model_name.replace("publishers/google/models/", "")
266
-
267
- name = to_wrap.get("span_name")
268
- span = tracer.start_span(
269
- name,
270
- kind=SpanKind.CLIENT,
271
- attributes={
272
- SpanAttributes.LLM_SYSTEM: "VertexAI",
273
- SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
274
- },
275
- )
276
-
277
- _handle_request(span, args, kwargs, llm_model)
278
-
279
- response = await wrapped(*args, **kwargs)
280
-
281
- if response:
282
- if is_streaming_response(response):
283
- return _build_from_streaming_response(span, response, llm_model)
284
- elif is_async_streaming_response(response):
285
- return _abuild_from_streaming_response(span, response, llm_model)
286
- else:
287
- _handle_response(span, response, llm_model)
288
-
289
- span.end()
290
- return response
291
-
292
-
293
- @_with_tracer_wrapper
294
- def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
295
- """Instruments and calls every function defined in TO_WRAP."""
296
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
297
- SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
298
- ):
299
- return wrapped(*args, **kwargs)
300
-
301
- llm_model = "unknown"
302
- if hasattr(instance, "_model_id"):
303
- llm_model = instance._model_id
304
- if hasattr(instance, "_model_name"):
305
- llm_model = instance._model_name.replace("publishers/google/models/", "")
306
-
307
- name = to_wrap.get("span_name")
308
- span = tracer.start_span(
309
- name,
310
- kind=SpanKind.CLIENT,
311
- attributes={
312
- SpanAttributes.LLM_SYSTEM: "VertexAI",
313
- SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
314
- },
315
- )
316
-
317
- _handle_request(span, args, kwargs, llm_model)
318
-
319
- response = wrapped(*args, **kwargs)
320
-
321
- if response:
322
- if is_streaming_response(response):
323
- return _build_from_streaming_response(span, response, llm_model)
324
- elif is_async_streaming_response(response):
325
- return _abuild_from_streaming_response(span, response, llm_model)
326
- else:
327
- _handle_response(span, response, llm_model)
44
+ from wrapt import (
45
+ wrap_function_wrapper, # type: ignore[reportUnknownVariableType]
46
+ )
328
47
 
329
- span.end()
330
- return response
48
+ from opentelemetry._events import get_event_logger
49
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
50
+ from opentelemetry.instrumentation.vertexai.package import _instruments
51
+ from opentelemetry.instrumentation.vertexai.patch import (
52
+ generate_content_create,
53
+ )
54
+ from opentelemetry.instrumentation.vertexai.utils import is_content_enabled
55
+ from opentelemetry.semconv.schemas import Schemas
56
+ from opentelemetry.trace import get_tracer
331
57
 
332
58
 
333
59
  class VertexAIInstrumentor(BaseInstrumentor):
334
- """An instrumentor for VertextAI's client library."""
335
-
336
- def __init__(self, exception_logger=None):
337
- super().__init__()
338
- Config.exception_logger = exception_logger
339
-
340
60
  def instrumentation_dependencies(self) -> Collection[str]:
341
61
  return _instruments
342
62
 
343
- def _instrument(self, **kwargs):
63
+ def _instrument(self, **kwargs: Any):
64
+ """Enable VertexAI instrumentation."""
344
65
  tracer_provider = kwargs.get("tracer_provider")
345
- tracer = get_tracer(__name__, __version__, tracer_provider)
346
- for wrapped_method in WRAPPED_METHODS:
347
- wrap_package = wrapped_method.get("package")
348
- wrap_object = wrapped_method.get("object")
349
- wrap_method = wrapped_method.get("method")
66
+ tracer = get_tracer(
67
+ __name__,
68
+ "",
69
+ tracer_provider,
70
+ schema_url=Schemas.V1_28_0.value,
71
+ )
72
+ event_logger_provider = kwargs.get("event_logger_provider")
73
+ event_logger = get_event_logger(
74
+ __name__,
75
+ "",
76
+ schema_url=Schemas.V1_28_0.value,
77
+ event_logger_provider=event_logger_provider,
78
+ )
350
79
 
351
- wrap_function_wrapper(
352
- wrap_package,
353
- f"{wrap_object}.{wrap_method}",
354
- (
355
- _awrap(tracer, wrapped_method)
356
- if wrapped_method.get("is_async")
357
- else _wrap(tracer, wrapped_method)
358
- ),
359
- )
80
+ wrap_function_wrapper(
81
+ module="google.cloud.aiplatform_v1beta1.services.prediction_service.client",
82
+ name="PredictionServiceClient.generate_content",
83
+ wrapper=generate_content_create(
84
+ tracer, event_logger, is_content_enabled()
85
+ ),
86
+ )
87
+ wrap_function_wrapper(
88
+ module="google.cloud.aiplatform_v1.services.prediction_service.client",
89
+ name="PredictionServiceClient.generate_content",
90
+ wrapper=generate_content_create(
91
+ tracer, event_logger, is_content_enabled()
92
+ ),
93
+ )
360
94
 
361
- def _uninstrument(self, **kwargs):
362
- for wrapped_method in WRAPPED_METHODS:
363
- wrap_package = wrapped_method.get("package")
364
- wrap_object = wrapped_method.get("object")
365
- unwrap(
366
- f"{wrap_package}.{wrap_object}",
367
- wrapped_method.get("method", ""),
368
- )
95
+ def _uninstrument(self, **kwargs: Any) -> None:
96
+ """TODO: implemented in later PR"""
@@ -0,0 +1,188 @@
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 __future__ import annotations
24
+
25
+ from dataclasses import asdict, dataclass
26
+ from typing import Any, Iterable, Literal
27
+
28
+ from opentelemetry._events import Event
29
+ from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
30
+ from opentelemetry.util.types import AnyValue
31
+
32
+
33
+ def user_event(
34
+ *,
35
+ role: str = "user",
36
+ content: AnyValue = None,
37
+ ) -> Event:
38
+ """Creates a User event
39
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event
40
+ """
41
+ body: dict[str, AnyValue] = {
42
+ "role": role,
43
+ }
44
+ if content is not None:
45
+ body["content"] = content
46
+ return Event(
47
+ name="gen_ai.user.message",
48
+ attributes={
49
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
50
+ },
51
+ body=body,
52
+ )
53
+
54
+
55
+ def assistant_event(
56
+ *,
57
+ role: str = "assistant",
58
+ content: AnyValue = None,
59
+ ) -> Event:
60
+ """Creates an Assistant event
61
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
62
+ """
63
+ body: dict[str, AnyValue] = {
64
+ "role": role,
65
+ }
66
+ if content is not None:
67
+ body["content"] = content
68
+ return Event(
69
+ name="gen_ai.assistant.message",
70
+ attributes={
71
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
72
+ },
73
+ body=body,
74
+ )
75
+
76
+
77
+ def system_event(
78
+ *,
79
+ role: str = "system",
80
+ content: AnyValue = None,
81
+ ) -> Event:
82
+ """Creates a System event
83
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#system-event
84
+ """
85
+ body: dict[str, AnyValue] = {
86
+ "role": role,
87
+ }
88
+ if content is not None:
89
+ body["content"] = content
90
+ return Event(
91
+ name="gen_ai.system.message",
92
+ attributes={
93
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
94
+ },
95
+ body=body,
96
+ )
97
+
98
+
99
+ def tool_event(
100
+ *,
101
+ role: str | None,
102
+ id_: str,
103
+ content: AnyValue = None,
104
+ ) -> Event:
105
+ """Creates a Tool message event
106
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aitoolmessage
107
+ """
108
+ if not role:
109
+ role = "tool"
110
+
111
+ body: dict[str, AnyValue] = {
112
+ "role": role,
113
+ "id": id_,
114
+ }
115
+ if content is not None:
116
+ body["content"] = content
117
+ return Event(
118
+ name="gen_ai.tool.message",
119
+ attributes={
120
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
121
+ },
122
+ body=body,
123
+ )
124
+
125
+
126
+ @dataclass
127
+ class ChoiceMessage:
128
+ """The message field for a gen_ai.choice event"""
129
+
130
+ content: AnyValue = None
131
+ role: str = "assistant"
132
+
133
+
134
+ @dataclass
135
+ class ChoiceToolCall:
136
+ """The tool_calls field for a gen_ai.choice event"""
137
+
138
+ @dataclass
139
+ class Function:
140
+ name: str
141
+ arguments: AnyValue = None
142
+
143
+ function: Function
144
+ id: str
145
+ type: Literal["function"] = "function"
146
+
147
+
148
+ FinishReason = Literal[
149
+ "content_filter", "error", "length", "stop", "tool_calls"
150
+ ]
151
+
152
+
153
+ def choice_event(
154
+ *,
155
+ finish_reason: FinishReason | str,
156
+ index: int,
157
+ message: ChoiceMessage,
158
+ tool_calls: Iterable[ChoiceToolCall] = (),
159
+ ) -> Event:
160
+ """Creates a choice event, which describes the Gen AI response message.
161
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aichoice
162
+ """
163
+ body: dict[str, AnyValue] = {
164
+ "finish_reason": finish_reason,
165
+ "index": index,
166
+ "message": _asdict_filter_nulls(message),
167
+ }
168
+
169
+ tool_calls_list = [
170
+ _asdict_filter_nulls(tool_call) for tool_call in tool_calls
171
+ ]
172
+ if tool_calls_list:
173
+ body["tool_calls"] = tool_calls_list
174
+
175
+ return Event(
176
+ name="gen_ai.choice",
177
+ attributes={
178
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
179
+ },
180
+ body=body,
181
+ )
182
+
183
+
184
+ def _asdict_filter_nulls(instance: Any) -> dict[str, AnyValue]:
185
+ return asdict(
186
+ instance,
187
+ dict_factory=lambda kvs: {k: v for (k, v) in kvs if v is not None},
188
+ )
@@ -0,0 +1,16 @@
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
+ _instruments = ("google-cloud-aiplatform >= 1.64",)