Skip to content

Commit e605f92

Browse files
Merge branch 'main' into metrics-instrumentation-starlette
2 parents c7bf4be + 56530eb commit e605f92

File tree

20 files changed

+442
-219
lines changed

20 files changed

+442
-219
lines changed

Diff for: CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Flask sqlalchemy psycopg2 integration
1212
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
13+
- Add metric instrumentation in fastapi
14+
([#1199](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1199))
15+
- Add metric instrumentation in Pyramid
16+
([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242))
1317

1418
### Fixed
1519

1620
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.
17-
([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234))
21+
([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234))
22+
- `opentelemetry-instrumentation-pymongo` Change span names to not contain queries but only database name and command name
23+
([#1247](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1247))
1824
- restoring metrics in django framework
1925
([#1208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1208))
2026
- `opentelemetry-instrumentation-aiohttp-client` Fix producing additional spans with each newly created ClientSession
2127
- ([#1246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1246))
28+
- Add _is_openetlemetry_instrumented check in _InstrumentedFastAPI class
29+
([#1313](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1313))
2230

2331
## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08
2432

Diff for: instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes
1717
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No
1818
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | No
19-
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | No
19+
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes
2020
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0, < 3.0 | Yes
2121
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No
2222
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No

Diff for: instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,15 @@ def __init__(
396396
client_response_hook: _ClientResponseHookT = None,
397397
tracer_provider=None,
398398
meter_provider=None,
399+
meter=None,
399400
):
400401
self.app = guarantee_single_callable(app)
401402
self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)
402-
self.meter = get_meter(__name__, __version__, meter_provider)
403+
self.meter = (
404+
get_meter(__name__, __version__, meter_provider)
405+
if meter is None
406+
else meter
407+
)
403408
self.duration_histogram = self.meter.create_histogram(
404409
name="http.server.duration",
405410
unit="ms",

Diff for: instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ def client_response_hook(span: Span, message: dict):
137137

138138
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
139139
from opentelemetry.instrumentation.asgi.package import _instruments
140+
from opentelemetry.instrumentation.fastapi.version import __version__
140141
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
142+
from opentelemetry.metrics import get_meter
141143
from opentelemetry.semconv.trace import SpanAttributes
142144
from opentelemetry.trace import Span
143145
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
@@ -165,6 +167,7 @@ def instrument_app(
165167
client_request_hook: _ClientRequestHookT = None,
166168
client_response_hook: _ClientResponseHookT = None,
167169
tracer_provider=None,
170+
meter_provider=None,
168171
excluded_urls=None,
169172
):
170173
"""Instrument an uninstrumented FastAPI application."""
@@ -176,6 +179,7 @@ def instrument_app(
176179
excluded_urls = _excluded_urls_from_env
177180
else:
178181
excluded_urls = parse_excluded_urls(excluded_urls)
182+
meter = get_meter(__name__, __version__, meter_provider)
179183

180184
app.add_middleware(
181185
OpenTelemetryMiddleware,
@@ -185,6 +189,7 @@ def instrument_app(
185189
client_request_hook=client_request_hook,
186190
client_response_hook=client_response_hook,
187191
tracer_provider=tracer_provider,
192+
meter=meter,
188193
)
189194
app._is_instrumented_by_opentelemetry = True
190195
else:
@@ -223,6 +228,7 @@ def _instrument(self, **kwargs):
223228
if _excluded_urls is None
224229
else parse_excluded_urls(_excluded_urls)
225230
)
231+
_InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")
226232
fastapi.FastAPI = _InstrumentedFastAPI
227233

228234
def _uninstrument(self, **kwargs):
@@ -231,13 +237,17 @@ def _uninstrument(self, **kwargs):
231237

232238
class _InstrumentedFastAPI(fastapi.FastAPI):
233239
_tracer_provider = None
240+
_meter_provider = None
234241
_excluded_urls = None
235242
_server_request_hook: _ServerRequestHookT = None
236243
_client_request_hook: _ClientRequestHookT = None
237244
_client_response_hook: _ClientResponseHookT = None
238245

239246
def __init__(self, *args, **kwargs):
240247
super().__init__(*args, **kwargs)
248+
meter = get_meter(
249+
__name__, __version__, _InstrumentedFastAPI._meter_provider
250+
)
241251
self.add_middleware(
242252
OpenTelemetryMiddleware,
243253
excluded_urls=_InstrumentedFastAPI._excluded_urls,
@@ -246,7 +256,9 @@ def __init__(self, *args, **kwargs):
246256
client_request_hook=_InstrumentedFastAPI._client_request_hook,
247257
client_response_hook=_InstrumentedFastAPI._client_response_hook,
248258
tracer_provider=_InstrumentedFastAPI._tracer_provider,
259+
meter=meter,
249260
)
261+
self._is_instrumented_by_opentelemetry = True
250262

251263

252264
def _get_route_details(scope):

Diff for: instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414

1515

1616
_instruments = ("fastapi ~= 0.58",)
17+
18+
_supports_metrics = True

Diff for: instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

+142
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
from timeit import default_timer
1617
from unittest.mock import patch
1718

1819
import fastapi
@@ -22,16 +23,31 @@
2223
import opentelemetry.instrumentation.fastapi as otel_fastapi
2324
from opentelemetry import trace
2425
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
26+
from opentelemetry.sdk.metrics.export import (
27+
HistogramDataPoint,
28+
NumberDataPoint,
29+
)
2530
from opentelemetry.sdk.resources import Resource
2631
from opentelemetry.semconv.trace import SpanAttributes
2732
from opentelemetry.test.globals_test import reset_trace_globals
2833
from opentelemetry.test.test_base import TestBase
2934
from opentelemetry.util.http import (
3035
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
3136
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
37+
_active_requests_count_attrs,
38+
_duration_attrs,
3239
get_excluded_urls,
3340
)
3441

42+
_expected_metric_names = [
43+
"http.server.active_requests",
44+
"http.server.duration",
45+
]
46+
_recommended_attrs = {
47+
"http.server.active_requests": _active_requests_count_attrs,
48+
"http.server.duration": _duration_attrs,
49+
}
50+
3551

3652
class TestFastAPIManualInstrumentation(TestBase):
3753
def _create_app(self):
@@ -161,6 +177,124 @@ def test_fastapi_excluded_urls_not_env(self):
161177
spans = self.memory_exporter.get_finished_spans()
162178
self.assertEqual(len(spans), 0)
163179

180+
def test_fastapi_metrics(self):
181+
self._client.get("/foobar")
182+
self._client.get("/foobar")
183+
self._client.get("/foobar")
184+
metrics_list = self.memory_metrics_reader.get_metrics_data()
185+
number_data_point_seen = False
186+
histogram_data_point_seen = False
187+
self.assertTrue(len(metrics_list.resource_metrics) == 1)
188+
for resource_metric in metrics_list.resource_metrics:
189+
self.assertTrue(len(resource_metric.scope_metrics) == 1)
190+
for scope_metric in resource_metric.scope_metrics:
191+
self.assertTrue(len(scope_metric.metrics) == 2)
192+
for metric in scope_metric.metrics:
193+
self.assertIn(metric.name, _expected_metric_names)
194+
data_points = list(metric.data.data_points)
195+
self.assertEqual(len(data_points), 1)
196+
for point in data_points:
197+
if isinstance(point, HistogramDataPoint):
198+
self.assertEqual(point.count, 3)
199+
histogram_data_point_seen = True
200+
if isinstance(point, NumberDataPoint):
201+
number_data_point_seen = True
202+
for attr in point.attributes:
203+
self.assertIn(
204+
attr, _recommended_attrs[metric.name]
205+
)
206+
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
207+
208+
def test_basic_metric_success(self):
209+
start = default_timer()
210+
self._client.get("/foobar")
211+
duration = max(round((default_timer() - start) * 1000), 0)
212+
expected_duration_attributes = {
213+
"http.method": "GET",
214+
"http.host": "testserver",
215+
"http.scheme": "http",
216+
"http.flavor": "1.1",
217+
"http.server_name": "testserver",
218+
"net.host.port": 80,
219+
"http.status_code": 200,
220+
}
221+
expected_requests_count_attributes = {
222+
"http.method": "GET",
223+
"http.host": "testserver",
224+
"http.scheme": "http",
225+
"http.flavor": "1.1",
226+
"http.server_name": "testserver",
227+
}
228+
metrics_list = self.memory_metrics_reader.get_metrics_data()
229+
for metric in (
230+
metrics_list.resource_metrics[0].scope_metrics[0].metrics
231+
):
232+
for point in list(metric.data.data_points):
233+
if isinstance(point, HistogramDataPoint):
234+
self.assertDictEqual(
235+
expected_duration_attributes,
236+
dict(point.attributes),
237+
)
238+
self.assertEqual(point.count, 1)
239+
self.assertAlmostEqual(duration, point.sum, delta=20)
240+
if isinstance(point, NumberDataPoint):
241+
self.assertDictEqual(
242+
expected_requests_count_attributes,
243+
dict(point.attributes),
244+
)
245+
self.assertEqual(point.value, 0)
246+
247+
def test_basic_post_request_metric_success(self):
248+
start = default_timer()
249+
self._client.post("/foobar")
250+
duration = max(round((default_timer() - start) * 1000), 0)
251+
metrics_list = self.memory_metrics_reader.get_metrics_data()
252+
for metric in (
253+
metrics_list.resource_metrics[0].scope_metrics[0].metrics
254+
):
255+
for point in list(metric.data.data_points):
256+
if isinstance(point, HistogramDataPoint):
257+
self.assertEqual(point.count, 1)
258+
self.assertAlmostEqual(duration, point.sum, delta=30)
259+
if isinstance(point, NumberDataPoint):
260+
self.assertEqual(point.value, 0)
261+
262+
def test_metric_uninstruemnt_app(self):
263+
self._client.get("/foobar")
264+
self._instrumentor.uninstrument_app(self._app)
265+
self._client.get("/foobar")
266+
metrics_list = self.memory_metrics_reader.get_metrics_data()
267+
for metric in (
268+
metrics_list.resource_metrics[0].scope_metrics[0].metrics
269+
):
270+
for point in list(metric.data.data_points):
271+
if isinstance(point, HistogramDataPoint):
272+
self.assertEqual(point.count, 1)
273+
if isinstance(point, NumberDataPoint):
274+
self.assertEqual(point.value, 0)
275+
276+
def test_metric_uninstrument(self):
277+
# instrumenting class and creating app to send request
278+
self._instrumentor.instrument()
279+
app = self._create_fastapi_app()
280+
client = TestClient(app)
281+
client.get("/foobar")
282+
# uninstrumenting class and creating the app again
283+
self._instrumentor.uninstrument()
284+
app = self._create_fastapi_app()
285+
client = TestClient(app)
286+
client.get("/foobar")
287+
288+
metrics_list = self.memory_metrics_reader.get_metrics_data()
289+
for metric in (
290+
metrics_list.resource_metrics[0].scope_metrics[0].metrics
291+
):
292+
for point in list(metric.data.data_points):
293+
if isinstance(point, HistogramDataPoint):
294+
self.assertEqual(point.count, 1)
295+
if isinstance(point, NumberDataPoint):
296+
self.assertEqual(point.value, 0)
297+
164298
@staticmethod
165299
def _create_fastapi_app():
166300
app = fastapi.FastAPI()
@@ -274,6 +408,14 @@ def test_request(self):
274408
self.assertEqual(span.resource.attributes["key1"], "value1")
275409
self.assertEqual(span.resource.attributes["key2"], "value2")
276410

411+
def test_mulitple_way_instrumentation(self):
412+
self._instrumentor.instrument_app(self._app)
413+
count = 0
414+
for middleware in self._app.user_middleware:
415+
if middleware.cls is OpenTelemetryMiddleware:
416+
count += 1
417+
self.assertEqual(count, 1)
418+
277419
def tearDown(self):
278420
self._instrumentor.uninstrument()
279421
super().tearDown()

Diff for: instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ def started(self, event: monitoring.CommandStartedEvent):
121121
):
122122
return
123123
command = event.command.get(event.command_name, "")
124-
name = event.command_name
124+
name = event.database_name
125+
name += "." + event.command_name
125126
statement = event.command_name
126127
if command:
127-
name += "." + str(command)
128128
statement += " " + str(command)
129129

130130
try:

Diff for: instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def test_pymongo_instrumentor(self):
4040
)
4141
with patch:
4242
PymongoInstrumentor().instrument()
43-
4443
self.assertTrue(mock_register.called)
4544

4645
def test_started(self):
@@ -59,7 +58,7 @@ def test_started(self):
5958
# pylint: disable=protected-access
6059
span = command_tracer._pop_span(mock_event)
6160
self.assertIs(span.kind, trace_api.SpanKind.CLIENT)
62-
self.assertEqual(span.name, "command_name.find")
61+
self.assertEqual(span.name, "database_name.command_name")
6362
self.assertEqual(span.attributes[SpanAttributes.DB_SYSTEM], "mongodb")
6463
self.assertEqual(
6564
span.attributes[SpanAttributes.DB_NAME], "database_name"
@@ -189,8 +188,7 @@ def test_int_command(self):
189188

190189
self.assertEqual(len(spans_list), 1)
191190
span = spans_list[0]
192-
193-
self.assertEqual(span.name, "command_name.123")
191+
self.assertEqual(span.name, "database_name.command_name")
194192

195193

196194
class MockCommand:

0 commit comments

Comments
 (0)