Skip to content

Commit bd04439

Browse files
committed
botocore: metrics setup
Add lazy init of meters
1 parent f98f568 commit bd04439

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def response_hook(span, service_name, operation_name, result):
104104
suppress_http_instrumentation,
105105
unwrap,
106106
)
107+
from opentelemetry.metrics import Instrument, Meter, get_meter
107108
from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator
108109
from opentelemetry.semconv.trace import SpanAttributes
109110
from opentelemetry.trace import get_tracer
@@ -134,6 +135,10 @@ def _instrument(self, **kwargs):
134135
self._tracers = {}
135136
# event_loggers are lazy initialized per-extension in _get_event_logger
136137
self._event_loggers = {}
138+
# meters are lazy initialized per-extension in _get_meter
139+
self._meters = {}
140+
# metrics are lazy initial per-extension in _get_metrics
141+
self._metrics: dict[str, dict[str, Instrument]] = {}
137142

138143
self.request_hook = kwargs.get("request_hook")
139144
self.response_hook = kwargs.get("response_hook")
@@ -144,6 +149,7 @@ def _instrument(self, **kwargs):
144149

145150
self.tracer_provider = kwargs.get("tracer_provider")
146151
self.event_logger_provider = kwargs.get("event_logger_provider")
152+
self.meter_provider = kwargs.get("meter_provider")
147153

148154
wrap_function_wrapper(
149155
"botocore.client",
@@ -201,6 +207,38 @@ def _get_event_logger(self, extension: _AwsSdkExtension):
201207

202208
return self._event_loggers[instrumentation_name]
203209

210+
def _get_meter(self, extension: _AwsSdkExtension):
211+
"""This is a multiplexer in order to have an event logger per extension"""
212+
213+
instrumentation_name = self._get_instrumentation_name(extension)
214+
meter = self._meters.get(instrumentation_name)
215+
if meter:
216+
return meter
217+
218+
schema_version = extension.meter_schema_version()
219+
self._meters[instrumentation_name] = get_meter(
220+
instrumentation_name,
221+
"",
222+
schema_url=f"https://opentelemetry.io/schemas/{schema_version}",
223+
meter_provider=self.meter_provider,
224+
)
225+
226+
return self._meters[instrumentation_name]
227+
228+
def _get_metrics(
229+
self, extension: _AwsSdkExtension, meter: Meter
230+
) -> dict[str, Instrument]:
231+
"""This is a multiplexer for lazy initial metrics required by extensions"""
232+
instrumentation_name = self._get_instrumentation_name(extension)
233+
metrics = self._metrics.get(instrumentation_name)
234+
if metrics is not None:
235+
return metrics
236+
237+
self._metrics.setdefault(instrumentation_name, {})
238+
metrics = self._metrics[instrumentation_name]
239+
_safe_invoke(extension.setup_metrics, meter, metrics)
240+
return metrics
241+
204242
def _uninstrument(self, **kwargs):
205243
unwrap(BaseClient, "_make_api_call")
206244
unwrap(Endpoint, "prepare_request")
@@ -244,8 +282,11 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
244282

245283
tracer = self._get_tracer(extension)
246284
event_logger = self._get_event_logger(extension)
285+
meter = self._get_meter(extension)
286+
metrics = self._get_metrics(extension, meter)
247287
instrumentor_ctx = _BotocoreInstrumentorContext(
248-
event_logger=event_logger
288+
event_logger=event_logger,
289+
metrics=metrics,
249290
)
250291
with tracer.start_as_current_span(
251292
call_context.span_name,

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@
3939
_BotoClientErrorT,
4040
_BotocoreInstrumentorContext,
4141
)
42+
from opentelemetry.metrics import Instrument, Meter
4243
from opentelemetry.semconv._incubating.attributes.error_attributes import (
4344
ERROR_TYPE,
4445
)
4546
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
47+
GEN_AI_CLIENT_OPERATION_DURATION,
48+
GEN_AI_CLIENT_TOKEN_USAGE,
4649
GEN_AI_OPERATION_NAME,
4750
GEN_AI_REQUEST_MAX_TOKENS,
4851
GEN_AI_REQUEST_MODEL,
@@ -61,6 +64,40 @@
6164

6265
_logger = logging.getLogger(__name__)
6366

67+
_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS = [
68+
0.01,
69+
0.02,
70+
0.04,
71+
0.08,
72+
0.16,
73+
0.32,
74+
0.64,
75+
1.28,
76+
2.56,
77+
5.12,
78+
10.24,
79+
20.48,
80+
40.96,
81+
81.92,
82+
]
83+
84+
_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS = [
85+
1,
86+
4,
87+
16,
88+
64,
89+
256,
90+
1024,
91+
4096,
92+
16384,
93+
65536,
94+
262144,
95+
1048576,
96+
4194304,
97+
16777216,
98+
67108864,
99+
]
100+
64101
_MODEL_ID_KEY: str = "modelId"
65102

66103

@@ -88,6 +125,20 @@ def should_end_span_on_exit(self):
88125
not in self._DONT_CLOSE_SPAN_ON_END_OPERATIONS
89126
)
90127

128+
def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]):
129+
metrics[GEN_AI_CLIENT_OPERATION_DURATION] = meter.create_histogram(
130+
name=GEN_AI_CLIENT_OPERATION_DURATION,
131+
description="GenAI operation duration",
132+
unit="s",
133+
explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS,
134+
)
135+
metrics[GEN_AI_CLIENT_TOKEN_USAGE] = meter.create_histogram(
136+
name=GEN_AI_CLIENT_TOKEN_USAGE,
137+
description="Measures number of input and output tokens used",
138+
unit="{token}",
139+
explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS,
140+
)
141+
91142
def extract_attributes(self, attributes: _AttributeMapT):
92143
if self._call_context.operation not in self._HANDLED_OPERATIONS:
93144
return

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Any, Dict, Optional, Tuple
1717

1818
from opentelemetry._events import EventLogger
19+
from opentelemetry.metrics import Instrument, Meter
1920
from opentelemetry.trace import SpanKind
2021
from opentelemetry.trace.span import Span
2122
from opentelemetry.util.types import AttributeValue
@@ -91,8 +92,11 @@ def _get_attr(obj, name: str, default=None):
9192

9293

9394
class _BotocoreInstrumentorContext:
94-
def __init__(self, event_logger: EventLogger):
95+
def __init__(
96+
self, event_logger: EventLogger, metrics: dict[str, Instrument]
97+
):
9598
self.event_logger = event_logger
99+
self.metrics = metrics
96100

97101

98102
class _AwsSdkExtension:
@@ -109,6 +113,11 @@ def event_logger_schema_version() -> str:
109113
"""Returns the event logger OTel schema version the extension is following"""
110114
return "1.30.0"
111115

116+
@staticmethod
117+
def meter_schema_version() -> str:
118+
"""Returns the meter OTel schema version the extension is following"""
119+
return "1.30.0"
120+
112121
def should_trace_service_call(self) -> bool: # pylint:disable=no-self-use
113122
"""Returns if the AWS SDK service call should be traced or not
114123
@@ -125,6 +134,12 @@ def should_end_span_on_exit(self) -> bool: # pylint:disable=no-self-use
125134
"""
126135
return True
127136

137+
def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]):
138+
"""Callback which gets invoked to setup metrics.
139+
140+
Extensions might override this function to add to the metrics dictionary all the metrics
141+
they want to receive later in _BotocoreInstrumentorContext."""
142+
128143
def extract_attributes(self, attributes: _AttributeMapT):
129144
"""Callback which gets invoked before the span is created.
130145

0 commit comments

Comments
 (0)