Skip to content

Commit 9a2285a

Browse files
authoredSep 22, 2022
Metric instrumentation falcon (#1230)
1 parent 08db974 commit 9a2285a

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed
 

‎CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241))
1313
- Flask sqlalchemy psycopg2 integration
1414
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
15+
- Add metric instrumentation in Falcon
16+
([#1230](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1230))
1517
- Add metric instrumentation in fastapi
1618
([#1199](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1199))
1719
- Add metric instrumentation in Pyramid

‎instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ def response_hook(span, req, resp):
144144
from logging import getLogger
145145
from sys import exc_info
146146
from time import time_ns
147+
from timeit import default_timer
147148
from typing import Collection
148149

149150
import falcon
@@ -163,6 +164,7 @@ def response_hook(span, req, resp):
163164
extract_attributes_from_object,
164165
http_status_to_status_code,
165166
)
167+
from opentelemetry.metrics import get_meter
166168
from opentelemetry.semconv.trace import SpanAttributes
167169
from opentelemetry.trace.status import Status
168170
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
@@ -202,12 +204,24 @@ def __init__(self, *args, **kwargs):
202204
# inject trace middleware
203205
self._middlewares_list = kwargs.pop("middleware", [])
204206
tracer_provider = otel_opts.pop("tracer_provider", None)
207+
meter_provider = otel_opts.pop("meter_provider", None)
205208
if not isinstance(self._middlewares_list, (list, tuple)):
206209
self._middlewares_list = [self._middlewares_list]
207210

208211
self._otel_tracer = trace.get_tracer(
209212
__name__, __version__, tracer_provider
210213
)
214+
self._otel_meter = get_meter(__name__, __version__, meter_provider)
215+
self.duration_histogram = self._otel_meter.create_histogram(
216+
name="http.server.duration",
217+
unit="ms",
218+
description="measures the duration of the inbound HTTP request",
219+
)
220+
self.active_requests_counter = self._otel_meter.create_up_down_counter(
221+
name="http.server.active_requests",
222+
unit="requests",
223+
description="measures the number of concurrent HTTP requests that are currently in-flight",
224+
)
211225

212226
trace_middleware = _TraceMiddleware(
213227
self._otel_tracer,
@@ -261,6 +275,7 @@ def _handle_exception(
261275

262276
def __call__(self, env, start_response):
263277
# pylint: disable=E1101
278+
# pylint: disable=too-many-locals
264279
if self._otel_excluded_urls.url_disabled(env.get("PATH_INFO", "/")):
265280
return super().__call__(env, start_response)
266281

@@ -276,9 +291,14 @@ def __call__(self, env, start_response):
276291
context_carrier=env,
277292
context_getter=otel_wsgi.wsgi_getter,
278293
)
294+
attributes = otel_wsgi.collect_request_attributes(env)
295+
active_requests_count_attrs = (
296+
otel_wsgi._parse_active_request_count_attrs(attributes)
297+
)
298+
duration_attrs = otel_wsgi._parse_duration_attrs(attributes)
299+
self.active_requests_counter.add(1, active_requests_count_attrs)
279300

280301
if span.is_recording():
281-
attributes = otel_wsgi.collect_request_attributes(env)
282302
for key, value in attributes.items():
283303
span.set_attribute(key, value)
284304
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
@@ -302,6 +322,7 @@ def _start_response(status, response_headers, *args, **kwargs):
302322
context.detach(token)
303323
return response
304324

325+
start = default_timer()
305326
try:
306327
return super().__call__(env, _start_response)
307328
except Exception as exc:
@@ -313,6 +334,13 @@ def _start_response(status, response_headers, *args, **kwargs):
313334
if token is not None:
314335
context.detach(token)
315336
raise
337+
finally:
338+
duration_attrs[
339+
SpanAttributes.HTTP_STATUS_CODE
340+
] = span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
341+
duration = max(round((default_timer() - start) * 1000), 0)
342+
self.duration_histogram.record(duration, duration_attrs)
343+
self.active_requests_counter.add(-1, active_requests_count_attrs)
316344

317345

318346
class _TraceMiddleware:

‎instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py

+99
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
#
15+
from timeit import default_timer
1516
from unittest.mock import Mock, patch
1617

1718
import pytest
@@ -26,6 +27,14 @@
2627
get_global_response_propagator,
2728
set_global_response_propagator,
2829
)
30+
from opentelemetry.instrumentation.wsgi import (
31+
_active_requests_count_attrs,
32+
_duration_attrs,
33+
)
34+
from opentelemetry.sdk.metrics.export import (
35+
HistogramDataPoint,
36+
NumberDataPoint,
37+
)
2938
from opentelemetry.sdk.resources import Resource
3039
from opentelemetry.semconv.trace import SpanAttributes
3140
from opentelemetry.test.test_base import TestBase
@@ -38,6 +47,15 @@
3847

3948
from .app import make_app
4049

50+
_expected_metric_names = [
51+
"http.server.active_requests",
52+
"http.server.duration",
53+
]
54+
_recommended_attrs = {
55+
"http.server.active_requests": _active_requests_count_attrs,
56+
"http.server.duration": _duration_attrs,
57+
}
58+
4159

4260
class TestFalconBase(TestBase):
4361
def setUp(self):
@@ -254,6 +272,87 @@ def test_uninstrument_after_instrument(self):
254272
spans = self.memory_exporter.get_finished_spans()
255273
self.assertEqual(len(spans), 0)
256274

275+
def test_falcon_metrics(self):
276+
self.client().simulate_get("/hello/756")
277+
self.client().simulate_get("/hello/756")
278+
self.client().simulate_get("/hello/756")
279+
metrics_list = self.memory_metrics_reader.get_metrics_data()
280+
number_data_point_seen = False
281+
histogram_data_point_seen = False
282+
self.assertTrue(len(metrics_list.resource_metrics) != 0)
283+
for resource_metric in metrics_list.resource_metrics:
284+
self.assertTrue(len(resource_metric.scope_metrics) != 0)
285+
for scope_metric in resource_metric.scope_metrics:
286+
self.assertTrue(len(scope_metric.metrics) != 0)
287+
for metric in scope_metric.metrics:
288+
self.assertIn(metric.name, _expected_metric_names)
289+
data_points = list(metric.data.data_points)
290+
self.assertEqual(len(data_points), 1)
291+
for point in data_points:
292+
if isinstance(point, HistogramDataPoint):
293+
self.assertEqual(point.count, 3)
294+
histogram_data_point_seen = True
295+
if isinstance(point, NumberDataPoint):
296+
number_data_point_seen = True
297+
for attr in point.attributes:
298+
self.assertIn(
299+
attr, _recommended_attrs[metric.name]
300+
)
301+
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
302+
303+
def test_falcon_metric_values(self):
304+
expected_duration_attributes = {
305+
"http.method": "GET",
306+
"http.host": "falconframework.org",
307+
"http.scheme": "http",
308+
"http.flavor": "1.1",
309+
"http.server_name": "falconframework.org",
310+
"net.host.port": 80,
311+
"http.status_code": 404,
312+
}
313+
expected_requests_count_attributes = {
314+
"http.method": "GET",
315+
"http.host": "falconframework.org",
316+
"http.scheme": "http",
317+
"http.flavor": "1.1",
318+
"http.server_name": "falconframework.org",
319+
}
320+
start = default_timer()
321+
self.client().simulate_get("/hello/756")
322+
duration = max(round((default_timer() - start) * 1000), 0)
323+
metrics_list = self.memory_metrics_reader.get_metrics_data()
324+
for resource_metric in metrics_list.resource_metrics:
325+
for scope_metric in resource_metric.scope_metrics:
326+
for metric in scope_metric.metrics:
327+
for point in list(metric.data.data_points):
328+
if isinstance(point, HistogramDataPoint):
329+
self.assertDictEqual(
330+
expected_duration_attributes,
331+
dict(point.attributes),
332+
)
333+
self.assertEqual(point.count, 1)
334+
self.assertAlmostEqual(
335+
duration, point.sum, delta=10
336+
)
337+
if isinstance(point, NumberDataPoint):
338+
self.assertDictEqual(
339+
expected_requests_count_attributes,
340+
dict(point.attributes),
341+
)
342+
self.assertEqual(point.value, 0)
343+
344+
def test_metric_uninstrument(self):
345+
self.client().simulate_request(method="POST", path="/hello/756")
346+
FalconInstrumentor().uninstrument()
347+
self.client().simulate_request(method="POST", path="/hello/756")
348+
metrics_list = self.memory_metrics_reader.get_metrics_data()
349+
for resource_metric in metrics_list.resource_metrics:
350+
for scope_metric in resource_metric.scope_metrics:
351+
for metric in scope_metric.metrics:
352+
for point in list(metric.data.data_points):
353+
if isinstance(point, HistogramDataPoint):
354+
self.assertEqual(point.count, 1)
355+
257356

258357
class TestFalconInstrumentationWithTracerProvider(TestBase):
259358
def setUp(self):

0 commit comments

Comments
 (0)
Please sign in to comment.