Skip to content

Commit dc8eac3

Browse files
committed
Adds support for request and response hooks to Falcon instrumentation.
1 parent 370952f commit dc8eac3

File tree

6 files changed

+114
-6
lines changed

6 files changed

+114
-6
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,6 @@ jobs:
9898
uses: actions/cache@v2
9999
with:
100100
path: .tox
101-
key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}
101+
key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt', 'docs-requirements.txt') }}
102102
- name: run tox
103103
run: tox -e ${{ matrix.tox-environment }}

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
2222
- `opentelemetry-instrumenation-django` now supports request and response hooks.
2323
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
24+
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.
25+
([#415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/415))
26+
27+
### Removed
28+
- Remove `http.status_text` from span attributes
29+
([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406))
30+
31+
### Removed
32+
- Remove `http.status_text` from span attributes
33+
([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406))
2434

2535
### Removed
2636
- Remove `http.status_text` from span attributes

docs-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ boto~=2.0
2525
botocore~=1.0
2626
celery>=4.0
2727
flask~=1.0
28+
falcon~=2.0
2829
grpcio~=1.27
2930
mysql-connector-python~=8.0
3031
pymongo~=3.1
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry Falcon Instrumentation
2+
====================================
3+
4+
.. automodule:: opentelemetry.instrumentation.falcon
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

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

+69-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@
2121
* The ``falcon.resource`` Span attribute is set so the matched resource.
2222
* Error from Falcon resources are properly caught and recorded.
2323
24+
Configuration
25+
-------------
26+
27+
Exclude lists
28+
*************
29+
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude.
30+
31+
For example,
32+
33+
::
34+
35+
export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck"
36+
37+
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
38+
39+
Request attributes
40+
********************
41+
To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma
42+
delimited list of request attribute names.
43+
44+
For example,
45+
46+
::
47+
48+
export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template'
49+
50+
will extract query_string and uri_template attributes from every traced request and add them as span attritbues.
51+
52+
Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1
53+
2454
Usage
2555
-----
2656
@@ -39,10 +69,27 @@ def on_get(self, req, resp):
3969
4070
app.add_route('/hello', HelloWorldResource())
4171
72+
73+
Request and Response hooks
74+
***************************
75+
The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request
76+
and right before the span is finished while processing a response. The hooks can be configured as follows:
77+
78+
::
79+
80+
def request_hook(span, req):
81+
pass
82+
83+
def response_hook(span, req, resp):
84+
pass
85+
86+
FalconInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
87+
4288
API
4389
---
4490
"""
4591

92+
from functools import partial
4693
from logging import getLogger
4794
from sys import exc_info
4895

@@ -83,21 +130,25 @@ class FalconInstrumentor(BaseInstrumentor):
83130

84131
def _instrument(self, **kwargs):
85132
self._original_falcon_api = falcon.API
86-
falcon.API = _InstrumentedFalconAPI
133+
falcon.API = partial(_InstrumentedFalconAPI, **kwargs)
87134

88135
def _uninstrument(self, **kwargs):
89136
falcon.API = self._original_falcon_api
90137

91138

92139
class _InstrumentedFalconAPI(falcon.API):
93140
def __init__(self, *args, **kwargs):
141+
# inject trace middleware
94142
middlewares = kwargs.pop("middleware", [])
95143
if not isinstance(middlewares, (list, tuple)):
96144
middlewares = [middlewares]
97145

98146
self._tracer = trace.get_tracer(__name__, __version__)
99147
trace_middleware = _TraceMiddleware(
100-
self._tracer, kwargs.get("traced_request_attributes")
148+
self._tracer,
149+
kwargs.pop("traced_request_attributes", None),
150+
kwargs.pop("request_hook", None),
151+
kwargs.pop("response_hook", None),
101152
)
102153
middlewares.insert(0, trace_middleware)
103154
kwargs["middleware"] = middlewares
@@ -148,12 +199,23 @@ def _start_response(status, response_headers, *args, **kwargs):
148199
class _TraceMiddleware:
149200
# pylint:disable=R0201,W0613
150201

151-
def __init__(self, tracer=None, traced_request_attrs=None):
202+
def __init__(
203+
self,
204+
tracer=None,
205+
traced_request_attrs=None,
206+
request_hook=None,
207+
response_hook=None,
208+
):
152209
self.tracer = tracer
153210
self._traced_request_attrs = _traced_request_attrs
211+
self._request_hook = request_hook
212+
self._response_hook = response_hook
154213

155214
def process_request(self, req, resp):
156215
span = req.env.get(_ENVIRON_SPAN_KEY)
216+
if span and self._request_hook:
217+
self._request_hook(span, req)
218+
157219
if not span or not span.is_recording():
158220
return
159221

@@ -178,6 +240,7 @@ def process_response(
178240
self, req, resp, resource, req_succeeded=None
179241
): # pylint:disable=R0201
180242
span = req.env.get(_ENVIRON_SPAN_KEY)
243+
181244
if not span or not span.is_recording():
182245
return
183246

@@ -209,3 +272,6 @@ def process_response(
209272
description=reason,
210273
)
211274
)
275+
276+
if self._response_hook:
277+
self._response_hook(span, req, resp)

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

+26-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424
from .app import make_app
2525

2626

27-
class TestFalconInstrumentation(TestBase):
27+
class TestFalconBase(TestBase):
2828
def setUp(self):
2929
super().setUp()
30-
FalconInstrumentor().instrument()
30+
FalconInstrumentor().instrument(
31+
request_hook=getattr(self, "request_hook", None),
32+
response_hook=getattr(self, "response_hook", None),
33+
)
3134
self.app = make_app()
3235
# pylint: disable=protected-access
3336
self.env_patch = patch.dict(
@@ -64,6 +67,8 @@ def tearDown(self):
6467
self.exclude_patch.stop()
6568
self.traced_patch.stop()
6669

70+
71+
class TestFalconInstrumentation(TestFalconBase):
6772
def test_get(self):
6873
self._test_method("GET")
6974

@@ -204,3 +209,22 @@ def test_traced_not_recording(self):
204209
self.assertTrue(mock_span.is_recording.called)
205210
self.assertFalse(mock_span.set_attribute.called)
206211
self.assertFalse(mock_span.set_status.called)
212+
213+
214+
class TestFalconInstrumentationHooks(TestFalconBase):
215+
# pylint: disable=no-self-use
216+
def request_hook(self, span, req):
217+
span.set_attribute("request_hook_attr", "value from hook")
218+
219+
def response_hook(self, span, req, resp):
220+
span.update_name("set from hook")
221+
222+
def test_hooks(self):
223+
self.client().simulate_get(path="/hello?q=abc")
224+
span = self.memory_exporter.get_finished_spans()[0]
225+
226+
self.assertEqual(span.name, "set from hook")
227+
self.assertIn("request_hook_attr", span.attributes)
228+
self.assertEqual(
229+
span.attributes["request_hook_attr"], "value from hook"
230+
)

0 commit comments

Comments
 (0)