Skip to content

Commit a45af90

Browse files
authored
Merge branch 'main' into feature/valkey
2 parents a08cbb5 + cc7169c commit a45af90

File tree

13 files changed

+203
-43
lines changed

13 files changed

+203
-43
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
([#3380](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3381))
3232
- `opentelemetry-instrumentation-[asynclick/click]` Add missing opentelemetry-instrumentation dep
3333
([#3447](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3447))
34+
- `opentelemetry-instrumentation-botocore` Capture server attributes for botocore API calls
35+
([#3448](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3448))
3436

3537
## Version 1.32.0/0.53b0 (2025-04-10)
3638

exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868

6969

7070
def _ns_to_time(nanoseconds):
71-
ts = datetime.datetime.utcfromtimestamp(nanoseconds / 1e9)
71+
ts = datetime.datetime.fromtimestamp(
72+
nanoseconds / 1e9, datetime.timezone.utc
73+
)
7274
return ts.strftime("%H:%M:%S.%f")
7375

7476

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10-
## Version 0.2b0 (2025-04-25)
10+
## Version 0.2b0 (2025-04-28)
1111

1212
- Add more request configuration options to the span attributes ([#3374](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3374))
1313
- Restructure tests to keep in line with repository conventions ([#3344](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3344))

instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafka/__init__.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,35 @@
2020
2121
.. code:: python
2222
23+
import asyncio
2324
from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor
2425
from aiokafka import AIOKafkaProducer, AIOKafkaConsumer
2526
2627
# Instrument kafka
2728
AIOKafkaInstrumentor().instrument()
2829
2930
# report a span of type producer with the default settings
30-
producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092'])
31-
await producer.send('my-topic', b'raw_bytes')
31+
async def produce():
32+
producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092'])
33+
await producer.start()
34+
try:
35+
await producer.send_and_wait('my-topic', b'raw_bytes')
36+
finally:
37+
await producer.stop()
3238
3339
# report a span of type consumer with the default settings
34-
consumer = AIOKafkaConsumer('my-topic', group_id='my-group', bootstrap_servers=['localhost:9092'])
35-
async for message in consumer:
36-
# process message
40+
async def consume():
41+
consumer = AIOKafkaConsumer('my-topic', group_id='my-group', bootstrap_servers=['localhost:9092'])
42+
await consumer.start()
43+
try:
44+
async for message in consumer:
45+
# process message
46+
print(message)
47+
finally:
48+
await consumer.stop()
49+
50+
asyncio.run(produce())
51+
asyncio.run(consume())
3752
3853
The _instrument() method accepts the following keyword args:
3954
tracer_provider (TracerProvider) - an optional tracer provider
@@ -47,12 +62,14 @@ def async_consume_hook(span: Span, record: kafka.record.ABCRecord, args, kwargs)
4762
4863
.. code:: python
4964
50-
from opentelemetry.instrumentation.kafka import AIOKafkaInstrumentor
65+
import asyncio
66+
from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor
5167
from aiokafka import AIOKafkaProducer, AIOKafkaConsumer
5268
5369
async def async_produce_hook(span, args, kwargs):
5470
if span and span.is_recording():
5571
span.set_attribute("custom_user_attribute_from_async_response_hook", "some-value")
72+
5673
async def async_consume_hook(span, record, args, kwargs):
5774
if span and span.is_recording():
5875
span.set_attribute("custom_user_attribute_from_consume_hook", "some-value")
@@ -62,8 +79,15 @@ async def async_consume_hook(span, record, args, kwargs):
6279
6380
# Using kafka as normal now will automatically generate spans,
6481
# including user custom attributes added from the hooks
65-
producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092'])
66-
await producer.send('my-topic', b'raw_bytes')
82+
async def produce():
83+
producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092'])
84+
await producer.start()
85+
try:
86+
await producer.send_and_wait('my-topic', b'raw_bytes')
87+
finally:
88+
await producer.stop()
89+
90+
asyncio.run(produce())
6791
6892
API
6993
___

instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,53 @@
2323
2424
.. code-block:: python
2525
26+
import asyncio
2627
import aiopg
2728
from opentelemetry.instrumentation.aiopg import AiopgInstrumentor
2829
# Call instrument() to wrap all database connections
2930
AiopgInstrumentor().instrument()
3031
31-
cnx = await aiopg.connect(database='Database')
32-
cursor = await cnx.cursor()
33-
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
34-
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
35-
cursor.close()
36-
cnx.close()
32+
dsn = 'user=user password=password host=127.0.0.1'
3733
38-
pool = await aiopg.create_pool(database='Database')
34+
async def connect():
35+
cnx = await aiopg.connect(dsn)
36+
cursor = await cnx.cursor()
37+
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
38+
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
39+
cursor.close()
40+
cnx.close()
3941
40-
cnx = await pool.acquire()
41-
cursor = await cnx.cursor()
42-
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
43-
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
44-
cursor.close()
45-
cnx.close()
42+
async def create_pool():
43+
pool = await aiopg.create_pool(dsn)
44+
cnx = await pool.acquire()
45+
cursor = await cnx.cursor()
46+
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
47+
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
48+
cursor.close()
49+
cnx.close()
50+
51+
asyncio.run(connect())
52+
asyncio.run(create_pool())
4653
4754
.. code-block:: python
4855
56+
import asyncio
4957
import aiopg
5058
from opentelemetry.instrumentation.aiopg import AiopgInstrumentor
5159
60+
dsn = 'user=user password=password host=127.0.0.1'
61+
5262
# Alternatively, use instrument_connection for an individual connection
53-
cnx = await aiopg.connect(database='Database')
54-
instrumented_cnx = AiopgInstrumentor().instrument_connection(cnx)
55-
cursor = await instrumented_cnx.cursor()
56-
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
57-
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
58-
cursor.close()
59-
instrumented_cnx.close()
63+
async def go():
64+
cnx = await aiopg.connect(dsn)
65+
instrumented_cnx = AiopgInstrumentor().instrument_connection(cnx)
66+
cursor = await instrumented_cnx.cursor()
67+
await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
68+
await cursor.execute("INSERT INTO test (testField) VALUES (123)")
69+
cursor.close()
70+
instrumented_cnx.close()
71+
72+
asyncio.run(go())
6073
6174
API
6275
---

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def response_hook(span, service_name, operation_name, result):
9999
_BotocoreInstrumentorContext,
100100
)
101101
from opentelemetry.instrumentation.botocore.package import _instruments
102+
from opentelemetry.instrumentation.botocore.utils import get_server_attributes
102103
from opentelemetry.instrumentation.botocore.version import __version__
103104
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
104105
from opentelemetry.instrumentation.utils import (
@@ -277,6 +278,7 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
277278
SpanAttributes.RPC_METHOD: call_context.operation,
278279
# TODO: update when semantic conventions exist
279280
"aws.region": call_context.region,
281+
**get_server_attributes(call_context.endpoint_url),
280282
}
281283

282284
_safe_invoke(extension.extract_attributes, attributes)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# Includes work from:
1616
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
1717
# SPDX-License-Identifier: Apache-2.0
18+
# pylint: disable=too-many-lines
1819

1920
from __future__ import annotations
2021

@@ -41,6 +42,7 @@
4142
_BotoClientErrorT,
4243
_BotocoreInstrumentorContext,
4344
)
45+
from opentelemetry.instrumentation.botocore.utils import get_server_attributes
4446
from opentelemetry.metrics import Instrument, Meter
4547
from opentelemetry.semconv._incubating.attributes.error_attributes import (
4648
ERROR_TYPE,
@@ -146,7 +148,10 @@ def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]):
146148
)
147149

148150
def _extract_metrics_attributes(self) -> _AttributeMapT:
149-
attributes = {GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value}
151+
attributes = {
152+
GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value,
153+
**get_server_attributes(self._call_context.endpoint_url),
154+
}
150155

151156
model_id = self._call_context.params.get(_MODEL_ID_KEY)
152157
if not model_id:
@@ -163,6 +168,7 @@ def _extract_metrics_attributes(self) -> _AttributeMapT:
163168
attributes[GEN_AI_OPERATION_NAME] = (
164169
GenAiOperationNameValues.CHAT.value
165170
)
171+
166172
return attributes
167173

168174
def extract_attributes(self, attributes: _AttributeMapT):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
from __future__ import annotations
15+
16+
from urllib.parse import urlparse
17+
18+
from opentelemetry.semconv._incubating.attributes import (
19+
server_attributes as ServerAttributes,
20+
)
21+
from opentelemetry.util.types import AttributeValue
22+
23+
24+
def get_server_attributes(endpoint_url: str) -> dict[str, AttributeValue]:
25+
"""Extract server.* attributes from AWS endpoint URL."""
26+
parsed = urlparse(endpoint_url)
27+
attributes = {}
28+
if parsed.hostname:
29+
attributes[ServerAttributes.SERVER_ADDRESS] = parsed.hostname
30+
attributes[ServerAttributes.SERVER_PORT] = parsed.port or 443
31+
return attributes

instrumentation/opentelemetry-instrumentation-botocore/tests/bedrock_utils.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
from opentelemetry.semconv._incubating.attributes import (
3333
gen_ai_attributes as GenAIAttributes,
3434
)
35+
from opentelemetry.semconv._incubating.attributes import (
36+
server_attributes as ServerAttributes,
37+
)
3538
from opentelemetry.semconv._incubating.attributes.error_attributes import (
3639
ERROR_TYPE,
3740
)
@@ -221,6 +224,8 @@ def assert_all_attributes(
221224
request_temperature: int | None = None,
222225
request_max_tokens: int | None = None,
223226
request_stop_sequences: tuple[str] | None = None,
227+
server_address: str = "bedrock-runtime.us-east-1.amazonaws.com",
228+
server_port: int = 443,
224229
):
225230
assert span.name == f"{operation_name} {request_model}"
226231
assert (
@@ -235,6 +240,9 @@ def assert_all_attributes(
235240
request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]
236241
)
237242

243+
assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS]
244+
assert server_port == span.attributes[ServerAttributes.SERVER_PORT]
245+
238246
assert_equal_or_not_present(
239247
input_tokens, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, span
240248
)
@@ -303,7 +311,12 @@ def assert_message_in_logs(log, event_name, expected_content, parent_span):
303311

304312

305313
def assert_all_metric_attributes(
306-
data_point, operation_name: str, model: str, error_type: str | None = None
314+
data_point,
315+
operation_name: str,
316+
model: str,
317+
error_type: str | None = None,
318+
server_address: str = "bedrock-runtime.us-east-1.amazonaws.com",
319+
server_port: int = 443,
307320
):
308321
assert GenAIAttributes.GEN_AI_OPERATION_NAME in data_point.attributes
309322
assert (
@@ -318,6 +331,14 @@ def assert_all_metric_attributes(
318331
assert GenAIAttributes.GEN_AI_REQUEST_MODEL in data_point.attributes
319332
assert data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model
320333

334+
assert ServerAttributes.SERVER_ADDRESS in data_point.attributes
335+
assert (
336+
data_point.attributes[ServerAttributes.SERVER_ADDRESS]
337+
== server_address
338+
)
339+
assert ServerAttributes.SERVER_PORT in data_point.attributes
340+
assert data_point.attributes[ServerAttributes.SERVER_PORT] == server_port
341+
321342
if error_type is not None:
322343
assert ERROR_TYPE in data_point.attributes
323344
assert data_point.attributes[ERROR_TYPE] == error_type

0 commit comments

Comments
 (0)