Skip to content

Commit 4b3965e

Browse files
authored
Merge branch 'open-telemetry:main' into xray_traces
2 parents 08cd626 + 44754e2 commit 4b3965e

20 files changed

Lines changed: 1085 additions & 25 deletions

CHANGELOG.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4747
([#3200](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3200))
4848
- `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock ConverseStream API
4949
([#3204](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3204))
50+
- `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock InvokeModelWithStreamResponse API
51+
([#3206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3206))
5052
- `opentelemetry-instrumentation-pymssql` Add pymssql instrumentation
5153
([#394](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/394))
5254

@@ -65,14 +67,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6567

6668
- `opentelemetry-instrumentation-sqlalchemy` including sqlcomment in `db.statement` span attribute value is now opt-in
6769
([#3112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3112))
68-
69-
### Breaking changes
70-
71-
- `opentelemetry-instrumentation-dbapi` including sqlcomment in `db.statement` span attribute value is now opt-in
72-
([#3115](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3115))
73-
74-
### Breaking changes
75-
7670
- `opentelemetry-instrumentation-dbapi` including sqlcomment in `db.statement` span attribute value is now opt-in
7771
([#3115](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3115))
7872
- `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-pymysql`: including sqlcomment in `db.statement` span attribute value is now opt-in

instrumentation-genai/opentelemetry-instrumentation-vertexai/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
([#3192](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3192))
1212
- Initial VertexAI instrumentation
1313
([#3123](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3123))
14+
- Add server attributes to Vertex AI spans
15+
([#3208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3208))

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from opentelemetry.instrumentation.vertexai.utils import (
2626
GenerateContentParams,
2727
get_genai_request_attributes,
28+
get_server_attributes,
2829
get_span_name,
2930
)
3031
from opentelemetry.trace import SpanKind, Tracer
@@ -100,7 +101,11 @@ def traced_method(
100101
kwargs: Any,
101102
):
102103
params = _extract_params(*args, **kwargs)
103-
span_attributes = get_genai_request_attributes(params)
104+
api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType]
105+
span_attributes = {
106+
**get_genai_request_attributes(params),
107+
**get_server_attributes(api_endpoint),
108+
}
104109

105110
span_name = get_span_name(span_attributes)
106111
with tracer.start_as_current_span(

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
Mapping,
2323
Sequence,
2424
)
25+
from urllib.parse import urlparse
2526

2627
from opentelemetry.semconv._incubating.attributes import (
2728
gen_ai_attributes as GenAIAttributes,
2829
)
30+
from opentelemetry.semconv.attributes import server_attributes
2931
from opentelemetry.util.types import AttributeValue
3032

3133
if TYPE_CHECKING:
@@ -58,6 +60,24 @@ class GenerateContentParams:
5860
) = None
5961

6062

63+
def get_server_attributes(
64+
endpoint: str,
65+
) -> dict[str, AttributeValue]:
66+
"""Get server.* attributes from the endpoint, which is a hostname with optional port e.g.
67+
- ``us-central1-aiplatform.googleapis.com``
68+
- ``us-central1-aiplatform.googleapis.com:5431``
69+
"""
70+
parsed = urlparse(f"scheme://{endpoint}")
71+
72+
if not parsed.hostname:
73+
return {}
74+
75+
return {
76+
server_attributes.SERVER_ADDRESS: parsed.hostname,
77+
server_attributes.SERVER_PORT: parsed.port or 443,
78+
}
79+
80+
6181
def get_genai_request_attributes(
6282
params: GenerateContentParams,
6383
operation_name: GenAIAttributes.GenAiOperationNameValues = GenAIAttributes.GenAiOperationNameValues.CHAT,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def test_generate_content(
3434
"gen_ai.operation.name": "chat",
3535
"gen_ai.request.model": "gemini-1.5-flash-002",
3636
"gen_ai.system": "vertex_ai",
37+
"server.address": "us-central1-aiplatform.googleapis.com",
38+
"server.port": 443,
3739
}
3840

3941

@@ -62,6 +64,8 @@ def test_generate_content_empty_model(
6264
"gen_ai.operation.name": "chat",
6365
"gen_ai.request.model": "",
6466
"gen_ai.system": "vertex_ai",
67+
"server.address": "us-central1-aiplatform.googleapis.com",
68+
"server.port": 443,
6569
}
6670
assert_span_error(spans[0])
6771

@@ -91,6 +95,8 @@ def test_generate_content_missing_model(
9195
"gen_ai.operation.name": "chat",
9296
"gen_ai.request.model": "gemini-does-not-exist",
9397
"gen_ai.system": "vertex_ai",
98+
"server.address": "us-central1-aiplatform.googleapis.com",
99+
"server.port": 443,
94100
}
95101
assert_span_error(spans[0])
96102

@@ -122,6 +128,8 @@ def test_generate_content_invalid_temperature(
122128
"gen_ai.request.model": "gemini-1.5-flash-002",
123129
"gen_ai.request.temperature": 1000.0,
124130
"gen_ai.system": "vertex_ai",
131+
"server.address": "us-central1-aiplatform.googleapis.com",
132+
"server.port": 443,
125133
}
126134
assert_span_error(spans[0])
127135

@@ -158,6 +166,8 @@ def test_generate_content_extra_params(span_exporter, instrument_no_content):
158166
"gen_ai.request.temperature": 0.20000000298023224,
159167
"gen_ai.request.top_p": 0.949999988079071,
160168
"gen_ai.system": "vertex_ai",
169+
"server.address": "us-central1-aiplatform.googleapis.com",
170+
"server.port": 443,
161171
}
162172

163173

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
from opentelemetry.instrumentation.vertexai.utils import get_server_attributes
17+
18+
19+
def test_get_server_attributes() -> None:
20+
# without port
21+
assert get_server_attributes("us-central1-aiplatform.googleapis.com") == {
22+
"server.address": "us-central1-aiplatform.googleapis.com",
23+
"server.port": 443,
24+
}
25+
26+
# with port
27+
assert get_server_attributes(
28+
"us-central1-aiplatform.googleapis.com:5432"
29+
) == {
30+
"server.address": "us-central1-aiplatform.googleapis.com",
31+
"server.port": 5432,
32+
}

instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Available examples
2020
- `converse.py` uses `bedrock-runtime` `Converse API <https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html>_`.
2121
- `converse_stream.py` uses `bedrock-runtime` `ConverseStream API <https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html>_`.
2222
- `invoke_model.py` uses `bedrock-runtime` `InvokeModel API <https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html>_`.
23+
- `invoke_model_stream.py` uses `bedrock-runtime` `InvokeModelWithResponseStrea API <https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModelWithResponseStream.html>_`.
2324

2425
Setup
2526
-----
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
import os
3+
4+
import boto3
5+
6+
7+
def main():
8+
chat_model = os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1")
9+
prompt = "Write a short poem on OpenTelemetry."
10+
if "amazon.titan" in chat_model:
11+
body = {
12+
"inputText": prompt,
13+
"textGenerationConfig": {},
14+
}
15+
elif "amazon.nova" in chat_model:
16+
body = {
17+
"messages": [{"role": "user", "content": [{"text": prompt}]}],
18+
"schemaVersion": "messages-v1",
19+
}
20+
elif "anthropic.claude" in chat_model:
21+
body = {
22+
"messages": [
23+
{"role": "user", "content": [{"text": prompt, "type": "text"}]}
24+
],
25+
"anthropic_version": "bedrock-2023-05-31",
26+
"max_tokens": 200,
27+
}
28+
else:
29+
raise ValueError()
30+
client = boto3.client("bedrock-runtime")
31+
response = client.invoke_model_with_response_stream(
32+
modelId=chat_model,
33+
body=json.dumps(body),
34+
)
35+
36+
answer = ""
37+
for event in response["body"]:
38+
json_bytes = event.get("chunk", {}).get("bytes", b"")
39+
decoded = json_bytes.decode("utf-8")
40+
chunk = json.loads(decoded)
41+
if "outputText" in chunk:
42+
answer += chunk["outputText"]
43+
elif "completion" in chunk:
44+
answer += chunk["completion"]
45+
elif "contentBlockDelta" in chunk:
46+
answer += chunk["contentBlockDelta"]["delta"]["text"]
47+
print(answer)
48+
49+
50+
if __name__ == "__main__":
51+
main()

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import (
3030
ConverseStreamWrapper,
31+
InvokeModelWithResponseStreamWrapper,
3132
)
3233
from opentelemetry.instrumentation.botocore.extensions.types import (
3334
_AttributeMapT,
@@ -66,8 +67,16 @@ class _BedrockRuntimeExtension(_AwsSdkExtension):
6667
Amazon Bedrock Runtime</a>.
6768
"""
6869

69-
_HANDLED_OPERATIONS = {"Converse", "ConverseStream", "InvokeModel"}
70-
_DONT_CLOSE_SPAN_ON_END_OPERATIONS = {"ConverseStream"}
70+
_HANDLED_OPERATIONS = {
71+
"Converse",
72+
"ConverseStream",
73+
"InvokeModel",
74+
"InvokeModelWithResponseStream",
75+
}
76+
_DONT_CLOSE_SPAN_ON_END_OPERATIONS = {
77+
"ConverseStream",
78+
"InvokeModelWithResponseStream",
79+
}
7180

7281
def should_end_span_on_exit(self):
7382
return (
@@ -257,6 +266,12 @@ def _invoke_model_on_success(
257266
if original_body is not None:
258267
original_body.close()
259268

269+
def _on_stream_error_callback(self, span: Span, exception):
270+
span.set_status(Status(StatusCode.ERROR, str(exception)))
271+
if span.is_recording():
272+
span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
273+
span.end()
274+
260275
def on_success(self, span: Span, result: dict[str, Any]):
261276
if self._call_context.operation not in self._HANDLED_OPERATIONS:
262277
return
@@ -273,8 +288,11 @@ def stream_done_callback(response):
273288
self._converse_on_success(span, response)
274289
span.end()
275290

291+
def stream_error_callback(exception):
292+
self._on_stream_error_callback(span, exception)
293+
276294
result["stream"] = ConverseStreamWrapper(
277-
result["stream"], stream_done_callback
295+
result["stream"], stream_done_callback, stream_error_callback
278296
)
279297
return
280298

@@ -288,6 +306,26 @@ def stream_done_callback(response):
288306
# InvokeModel
289307
if "body" in result and isinstance(result["body"], StreamingBody):
290308
self._invoke_model_on_success(span, result, model_id)
309+
return
310+
311+
# InvokeModelWithResponseStream
312+
if "body" in result and isinstance(result["body"], EventStream):
313+
314+
def invoke_model_stream_done_callback(response):
315+
# the callback gets data formatted as the simpler converse API
316+
self._converse_on_success(span, response)
317+
span.end()
318+
319+
def invoke_model_stream_error_callback(exception):
320+
self._on_stream_error_callback(span, exception)
321+
322+
result["body"] = InvokeModelWithResponseStreamWrapper(
323+
result["body"],
324+
invoke_model_stream_done_callback,
325+
invoke_model_stream_error_callback,
326+
model_id,
327+
)
328+
return
291329

292330
# pylint: disable=no-self-use
293331
def _handle_amazon_titan_response(

0 commit comments

Comments
 (0)