Skip to content

Commit 8e0d0e0

Browse files
authoredSep 27, 2021
Falcon 3 support (#644)
1 parent 2b0a634 commit 8e0d0e0

File tree

9 files changed

+68
-30
lines changed

9 files changed

+68
-30
lines changed
 

‎CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- `opentelemetry-instrumentation-urllib3` Updated `_RequestHookT` with two additional fields - the request body and the request headers
2929
([#660](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/660))
3030

31+
### Changed
32+
- Tests for Falcon 3 support
33+
([#644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/644))
34+
3135
### Added
3236

3337
- `opentelemetry-instrumentation-urllib3`, `opentelemetry-instrumentation-requests`

‎instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi |
1212
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 |
1313
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 |
14-
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon ~= 2.0 |
14+
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 2.0.0, < 4.0.0 |
1515
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 |
1616
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0, < 3.0 |
1717
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 |

‎instrumentation/opentelemetry-instrumentation-falcon/README.rst

-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ OpenTelemetry Falcon Tracing
99
This library builds on the OpenTelemetry WSGI middleware to track web requests
1010
in Falcon applications.
1111

12-
Currently, only Falcon v2 is supported.
13-
1412
Installation
1513
------------
1614

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

+36-16
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ def response_hook(span, req, resp):
126126

127127
_response_propagation_setter = FuncSetter(falcon.Response.append_header)
128128

129+
if hasattr(falcon, "App"):
130+
# Falcon 3
131+
_instrument_app = "App"
132+
else:
133+
# Falcon 2
134+
_instrument_app = "API"
135+
129136

130137
class FalconInstrumentor(BaseInstrumentor):
131138
# pylint: disable=protected-access,attribute-defined-outside-init
@@ -138,14 +145,16 @@ def instrumentation_dependencies(self) -> Collection[str]:
138145
return _instruments
139146

140147
def _instrument(self, **kwargs):
141-
self._original_falcon_api = falcon.API
142-
falcon.API = partial(_InstrumentedFalconAPI, **kwargs)
148+
self._original_falcon_api = getattr(falcon, _instrument_app)
149+
setattr(
150+
falcon, _instrument_app, partial(_InstrumentedFalconAPI, **kwargs)
151+
)
143152

144153
def _uninstrument(self, **kwargs):
145-
falcon.API = self._original_falcon_api
154+
setattr(falcon, _instrument_app, self._original_falcon_api)
146155

147156

148-
class _InstrumentedFalconAPI(falcon.API):
157+
class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
149158
def __init__(self, *args, **kwargs):
150159
# inject trace middleware
151160
middlewares = kwargs.pop("middleware", [])
@@ -169,6 +178,15 @@ def __init__(self, *args, **kwargs):
169178
self._excluded_urls = get_excluded_urls("FALCON")
170179
super().__init__(*args, **kwargs)
171180

181+
def _handle_exception(
182+
self, req, resp, ex, params
183+
): # pylint: disable=C0103
184+
# Falcon 3 does not execute middleware within the context of the exception
185+
# so we capture the exception here and save it into the env dict
186+
_, exc, _ = exc_info()
187+
req.env[_ENVIRON_EXC] = exc
188+
return super()._handle_exception(req, resp, ex, params)
189+
172190
def __call__(self, env, start_response):
173191
# pylint: disable=E1101
174192
if self._excluded_urls.url_disabled(env.get("PATH_INFO", "/")):
@@ -193,7 +211,6 @@ def __call__(self, env, start_response):
193211
env[_ENVIRON_ACTIVATION_KEY] = activation
194212

195213
def _start_response(status, response_headers, *args, **kwargs):
196-
otel_wsgi.add_response_attributes(span, status, response_headers)
197214
response = start_response(
198215
status, response_headers, *args, **kwargs
199216
)
@@ -264,29 +281,32 @@ def process_response(
264281
if resource is None:
265282
status = "404"
266283
reason = "NotFound"
267-
268-
exc_type, exc, _ = exc_info()
269-
if exc_type and not req_succeeded:
270-
if "HTTPNotFound" in exc_type.__name__:
271-
status = "404"
272-
reason = "NotFound"
284+
else:
285+
if _ENVIRON_EXC in req.env:
286+
exc = req.env[_ENVIRON_EXC]
287+
exc_type = type(exc)
273288
else:
274-
status = "500"
275-
reason = "{}: {}".format(exc_type.__name__, exc)
289+
exc_type, exc = None, None
290+
if exc_type and not req_succeeded:
291+
if "HTTPNotFound" in exc_type.__name__:
292+
status = "404"
293+
reason = "NotFound"
294+
else:
295+
status = "500"
296+
reason = "{}: {}".format(exc_type.__name__, exc)
276297

277298
status = status.split(" ")[0]
278299
try:
279300
status_code = int(status)
280-
except ValueError:
281-
pass
282-
finally:
283301
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
284302
span.set_status(
285303
Status(
286304
status_code=http_status_to_status_code(status_code),
287305
description=reason,
288306
)
289307
)
308+
except ValueError:
309+
pass
290310

291311
propagator = get_global_response_propagator()
292312
if propagator:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
# limitations under the License.
1414

1515

16-
_instruments = ("falcon ~= 2.0",)
16+
_instruments = ("falcon >= 2.0.0, < 4.0.0",)

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ def on_get(self, req, resp):
3434

3535

3636
def make_app():
37-
app = falcon.API()
37+
if hasattr(falcon, "App"):
38+
# Falcon 3
39+
app = falcon.App()
40+
else:
41+
# Falcon 2
42+
app = falcon.API()
3843
app.add_route("/hello", HelloWorldResource())
3944
app.add_route("/ping", HelloWorldResource())
4045
app.add_route("/error", ErrorResource())

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

+11-3
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,19 @@ def test_head(self):
7878
self._test_method("HEAD")
7979

8080
def _test_method(self, method):
81-
self.client().simulate_request(method=method, path="/hello")
81+
self.client().simulate_request(
82+
method=method, path="/hello", remote_addr="127.0.0.1"
83+
)
8284
spans = self.memory_exporter.get_finished_spans()
8385
self.assertEqual(len(spans), 1)
8486
span = spans[0]
8587
self.assertEqual(
8688
span.name, "HelloWorldResource.on_{0}".format(method.lower())
8789
)
8890
self.assertEqual(span.status.status_code, StatusCode.UNSET)
91+
self.assertEqual(
92+
span.status.description, None,
93+
)
8994
self.assertSpanHasAttributes(
9095
span,
9196
{
@@ -105,12 +110,15 @@ def _test_method(self, method):
105110
self.memory_exporter.clear()
106111

107112
def test_404(self):
108-
self.client().simulate_get("/does-not-exist")
113+
self.client().simulate_get("/does-not-exist", remote_addr="127.0.0.1")
109114
spans = self.memory_exporter.get_finished_spans()
110115
self.assertEqual(len(spans), 1)
111116
span = spans[0]
112117
self.assertEqual(span.name, "HTTP GET")
113118
self.assertEqual(span.status.status_code, StatusCode.ERROR)
119+
self.assertEqual(
120+
span.status.description, "NotFound",
121+
)
114122
self.assertSpanHasAttributes(
115123
span,
116124
{
@@ -129,7 +137,7 @@ def test_404(self):
129137

130138
def test_500(self):
131139
try:
132-
self.client().simulate_get("/error")
140+
self.client().simulate_get("/error", remote_addr="127.0.0.1")
133141
except NameError:
134142
pass
135143
spans = self.memory_exporter.get_finished_spans()

‎pytest.ini

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
addopts = -rs -v
33
log_cli = true
44
log_cli_level = warning
5+
testpaths = instrumentation/opentelemetry-instrumentation-falcon/tests/

‎tox.ini

+8-6
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ envlist =
3838
pypy3-test-instrumentation-elasticsearch{2,5,6}
3939

4040
; opentelemetry-instrumentation-falcon
41-
py3{4,5,6,7,8,9}-test-instrumentation-falcon
42-
pypy3-test-instrumentation-falcon
41+
py3{4,5,6,7,8,9}-test-instrumentation-falcon{2,3}
42+
pypy3-test-instrumentation-falcon{2,3}
4343

4444
; opentelemetry-instrumentation-fastapi
4545
; fastapi only supports 3.6 and above.
@@ -175,6 +175,8 @@ deps =
175175
; FIXME: Elasticsearch >=7 causes CI workflow tests to hang, see open-telemetry/opentelemetry-python-contrib#620
176176
; elasticsearch7: elasticsearch-dsl>=7.0,<8.0
177177
; elasticsearch7: elasticsearch>=7.0,<8.0
178+
falcon2: falcon >=2.0.0,<3.0.0
179+
falcon3: falcon >=3.0.0,<4.0.0
178180
sqlalchemy11: sqlalchemy>=1.1,<1.2
179181
sqlalchemy14: aiosqlite
180182
sqlalchemy14: sqlalchemy~=1.4
@@ -193,7 +195,7 @@ changedir =
193195
test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests
194196
test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests
195197
test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests
196-
test-instrumentation-falcon: instrumentation/opentelemetry-instrumentation-falcon/tests
198+
test-instrumentation-falcon{2,3}: instrumentation/opentelemetry-instrumentation-falcon/tests
197199
test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests
198200
test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests
199201
test-instrumentation-urllib: instrumentation/opentelemetry-instrumentation-urllib/tests
@@ -236,16 +238,16 @@ commands_pre =
236238

237239
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
238240

239-
falcon,flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
240-
wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
241+
falcon{2,3},flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
242+
wsgi,falcon{2,3},flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
241243
asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
242244

243245
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
244246

245247
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test]
246248
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test]
247249

248-
falcon: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
250+
falcon{2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
249251

250252
flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
251253

0 commit comments

Comments
 (0)
Please sign in to comment.