Skip to content

Commit abdd25f

Browse files
authored
Add falcon version 1.4.1 support to opentelemetry-instrumentation-falcon (#1000)
1 parent 4ad7592 commit abdd25f

File tree

9 files changed

+103
-30
lines changed

9 files changed

+103
-30
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
([#1004])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004)
2929
- `opentelemetry-instrumentation-psycopg2` extended the sql commenter support of dbapi into psycopg2
3030
([#940](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/940))
31+
- `opentelemetry-instrumentation-falcon` Add support for falcon==1.4.1
32+
([#1000])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1000)
3133
- `opentelemetry-instrumentation-falcon` Falcon: Capture custom request/response headers in span attributes
3234
([#1003])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1003)
3335
- `opentelemetry-instrumentation-elasticsearch` no longer creates unique span names by including search target, replaces them with `<target>` and puts the value in attribute `elasticsearch.target`

instrumentation/README.md

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

instrumentation/opentelemetry-instrumentation-falcon/setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ install_requires =
4545
opentelemetry-instrumentation == 0.29b0
4646
opentelemetry-api ~= 1.3
4747
opentelemetry-semantic-conventions == 0.29b0
48+
packaging >= 20.0
4849

4950
[options.extras_require]
5051
test =

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

+38-5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def response_hook(span, req, resp):
146146
from typing import Collection
147147

148148
import falcon
149+
from packaging import version as package_version
149150

150151
import opentelemetry.instrumentation.wsgi as otel_wsgi
151152
from opentelemetry import context, trace
@@ -177,12 +178,19 @@ def response_hook(span, req, resp):
177178

178179
_response_propagation_setter = FuncSetter(falcon.Response.append_header)
179180

180-
if hasattr(falcon, "App"):
181+
_parsed_falcon_version = package_version.parse(falcon.__version__)
182+
if _parsed_falcon_version >= package_version.parse("3.0.0"):
181183
# Falcon 3
182184
_instrument_app = "App"
183-
else:
185+
_falcon_version = 3
186+
elif _parsed_falcon_version >= package_version.parse("2.0.0"):
184187
# Falcon 2
185188
_instrument_app = "API"
189+
_falcon_version = 2
190+
else:
191+
# Falcon 1
192+
_instrument_app = "API"
193+
_falcon_version = 1
186194

187195

188196
class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
@@ -214,12 +222,30 @@ def __init__(self, *args, **kwargs):
214222
super().__init__(*args, **kwargs)
215223

216224
def _handle_exception(
217-
self, req, resp, ex, params
225+
self, arg1, arg2, arg3, arg4
218226
): # pylint: disable=C0103
219227
# Falcon 3 does not execute middleware within the context of the exception
220228
# so we capture the exception here and save it into the env dict
229+
230+
# Translation layer for handling the changed arg position of "ex" in Falcon > 2 vs
231+
# Falcon < 2
232+
if _falcon_version == 1:
233+
ex = arg1
234+
req = arg2
235+
resp = arg3
236+
params = arg4
237+
else:
238+
req = arg1
239+
resp = arg2
240+
ex = arg3
241+
params = arg4
242+
221243
_, exc, _ = exc_info()
222244
req.env[_ENVIRON_EXC] = exc
245+
246+
if _falcon_version == 1:
247+
return super()._handle_exception(ex, req, resp, params)
248+
223249
return super()._handle_exception(req, resp, ex, params)
224250

225251
def __call__(self, env, start_response):
@@ -311,7 +337,7 @@ def process_resource(self, req, resp, resource, params):
311337

312338
def process_response(
313339
self, req, resp, resource, req_succeeded=None
314-
): # pylint:disable=R0201
340+
): # pylint:disable=R0201,R0912
315341
span = req.env.get(_ENVIRON_SPAN_KEY)
316342

317343
if not span or not span.is_recording():
@@ -348,9 +374,16 @@ def process_response(
348374
description=reason,
349375
)
350376
)
377+
378+
# Falcon 1 does not support response headers. So
379+
# send an empty dict.
380+
response_headers = {}
381+
if _falcon_version > 1:
382+
response_headers = resp.headers
383+
351384
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
352385
otel_wsgi.add_custom_response_headers(
353-
span, resp.headers.items()
386+
span, response_headers.items()
354387
)
355388
except ValueError:
356389
pass

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.0, < 4.0.0",)
16+
_instruments = ("falcon >= 1.4.1, < 4.0.0",)

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

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import falcon
2+
from packaging import version as package_version
23

34
# pylint:disable=R0201,W0613,E0602
45

@@ -46,12 +47,14 @@ def on_get(self, _, resp):
4647

4748

4849
def make_app():
49-
if hasattr(falcon, "App"):
50+
_parsed_falcon_version = package_version.parse(falcon.__version__)
51+
if _parsed_falcon_version < package_version.parse("3.0.0"):
52+
# Falcon 1 and Falcon 2
53+
app = falcon.API()
54+
else:
5055
# Falcon 3
5156
app = falcon.App()
52-
else:
53-
# Falcon 2
54-
app = falcon.API()
57+
5558
app.add_route("/hello", HelloWorldResource())
5659
app.add_route("/ping", HelloWorldResource())
5760
app.add_route("/error", ErrorResource())

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

+44-13
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
14+
#
1515
from unittest.mock import Mock, patch
1616

17+
import pytest
18+
from falcon import __version__ as _falcon_verison
1719
from falcon import testing
20+
from packaging import version as package_version
1821

1922
from opentelemetry import trace
2023
from opentelemetry.instrumentation.falcon import FalconInstrumentor
@@ -84,9 +87,7 @@ def test_head(self):
8487
self._test_method("HEAD")
8588

8689
def _test_method(self, method):
87-
self.client().simulate_request(
88-
method=method, path="/hello", remote_addr="127.0.0.1"
89-
)
90+
self.client().simulate_request(method=method, path="/hello")
9091
spans = self.memory_exporter.get_finished_spans()
9192
self.assertEqual(len(spans), 1)
9293
span = spans[0]
@@ -105,17 +106,23 @@ def _test_method(self, method):
105106
SpanAttributes.NET_HOST_PORT: 80,
106107
SpanAttributes.HTTP_HOST: "falconframework.org",
107108
SpanAttributes.HTTP_TARGET: "/",
108-
SpanAttributes.NET_PEER_IP: "127.0.0.1",
109109
SpanAttributes.NET_PEER_PORT: "65133",
110110
SpanAttributes.HTTP_FLAVOR: "1.1",
111111
"falcon.resource": "HelloWorldResource",
112112
SpanAttributes.HTTP_STATUS_CODE: 201,
113113
},
114114
)
115+
# In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1
116+
# In falcon>3, NET_PEER_IP is not set to anything by default to
117+
# https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 # noqa
118+
if SpanAttributes.NET_PEER_IP in span.attributes:
119+
self.assertEqual(
120+
span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1"
121+
)
115122
self.memory_exporter.clear()
116123

117124
def test_404(self):
118-
self.client().simulate_get("/does-not-exist", remote_addr="127.0.0.1")
125+
self.client().simulate_get("/does-not-exist")
119126
spans = self.memory_exporter.get_finished_spans()
120127
self.assertEqual(len(spans), 1)
121128
span = spans[0]
@@ -130,16 +137,22 @@ def test_404(self):
130137
SpanAttributes.NET_HOST_PORT: 80,
131138
SpanAttributes.HTTP_HOST: "falconframework.org",
132139
SpanAttributes.HTTP_TARGET: "/",
133-
SpanAttributes.NET_PEER_IP: "127.0.0.1",
134140
SpanAttributes.NET_PEER_PORT: "65133",
135141
SpanAttributes.HTTP_FLAVOR: "1.1",
136142
SpanAttributes.HTTP_STATUS_CODE: 404,
137143
},
138144
)
145+
# In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1
146+
# In falcon>3, NET_PEER_IP is not set to anything by default to
147+
# https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 # noqa
148+
if SpanAttributes.NET_PEER_IP in span.attributes:
149+
self.assertEqual(
150+
span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1"
151+
)
139152

140153
def test_500(self):
141154
try:
142-
self.client().simulate_get("/error", remote_addr="127.0.0.1")
155+
self.client().simulate_get("/error")
143156
except NameError:
144157
pass
145158
spans = self.memory_exporter.get_finished_spans()
@@ -161,12 +174,18 @@ def test_500(self):
161174
SpanAttributes.NET_HOST_PORT: 80,
162175
SpanAttributes.HTTP_HOST: "falconframework.org",
163176
SpanAttributes.HTTP_TARGET: "/",
164-
SpanAttributes.NET_PEER_IP: "127.0.0.1",
165177
SpanAttributes.NET_PEER_PORT: "65133",
166178
SpanAttributes.HTTP_FLAVOR: "1.1",
167179
SpanAttributes.HTTP_STATUS_CODE: 500,
168180
},
169181
)
182+
# In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1
183+
# In falcon>3, NET_PEER_IP is not set to anything by default to
184+
# https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 # noqa
185+
if SpanAttributes.NET_PEER_IP in span.attributes:
186+
self.assertEqual(
187+
span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1"
188+
)
170189

171190
def test_uninstrument(self):
172191
self.client().simulate_get(path="/hello")
@@ -191,7 +210,7 @@ def test_exclude_lists(self):
191210
self.assertEqual(len(span_list), 1)
192211

193212
def test_traced_request_attributes(self):
194-
self.client().simulate_get(path="/hello?q=abc")
213+
self.client().simulate_get(path="/hello", query_string="q=abc")
195214
span = self.memory_exporter.get_finished_spans()[0]
196215
self.assertIn("query_string", span.attributes)
197216
self.assertEqual(span.attributes["query_string"], "q=abc")
@@ -201,7 +220,9 @@ def test_trace_response(self):
201220
orig = get_global_response_propagator()
202221
set_global_response_propagator(TraceResponsePropagator())
203222

204-
response = self.client().simulate_get(path="/hello?q=abc")
223+
response = self.client().simulate_get(
224+
path="/hello", query_string="q=abc"
225+
)
205226
self.assertTraceResponseHeaderMatchesSpan(
206227
response.headers, self.memory_exporter.get_finished_spans()[0]
207228
)
@@ -215,7 +236,7 @@ def test_traced_not_recording(self):
215236
mock_tracer.start_span.return_value = mock_span
216237
with patch("opentelemetry.trace.get_tracer") as tracer:
217238
tracer.return_value = mock_tracer
218-
self.client().simulate_get(path="/hello?q=abc")
239+
self.client().simulate_get(path="/hello", query_string="q=abc")
219240
self.assertFalse(mock_span.is_recording())
220241
self.assertTrue(mock_span.is_recording.called)
221242
self.assertFalse(mock_span.set_attribute.called)
@@ -261,7 +282,7 @@ def response_hook(self, span, req, resp):
261282
span.update_name("set from hook")
262283

263284
def test_hooks(self):
264-
self.client().simulate_get(path="/hello?q=abc")
285+
self.client().simulate_get(path="/hello", query_string="q=abc")
265286
span = self.memory_exporter.get_finished_spans()[0]
266287

267288
self.assertEqual(span.name, "set from hook")
@@ -343,6 +364,11 @@ def test_custom_request_header_not_added_in_internal_span(self):
343364
for key, _ in not_expected.items():
344365
self.assertNotIn(key, span.attributes)
345366

367+
@pytest.mark.skipif(
368+
condition=package_version.parse(_falcon_verison)
369+
< package_version.parse("2.0.0"),
370+
reason="falcon<2 does not implement custom response headers",
371+
)
346372
def test_custom_response_header_added_in_server_span(self):
347373
self.client().simulate_request(
348374
method="GET", path="/test_custom_response_headers"
@@ -366,6 +392,11 @@ def test_custom_response_header_added_in_server_span(self):
366392
for key, _ in not_expected.items():
367393
self.assertNotIn(key, span.attributes)
368394

395+
@pytest.mark.skipif(
396+
condition=package_version.parse(_falcon_verison)
397+
< package_version.parse("2.0.0"),
398+
reason="falcon<2 does not implement custom response headers",
399+
)
369400
def test_custom_response_header_not_added_in_internal_span(self):
370401
tracer = trace.get_tracer(__name__)
371402
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):

opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"instrumentation": "opentelemetry-instrumentation-elasticsearch==0.29b0",
5454
},
5555
"falcon": {
56-
"library": "falcon >= 2.0.0, < 4.0.0",
56+
"library": "falcon >= 1.4.1, < 4.0.0",
5757
"instrumentation": "opentelemetry-instrumentation-falcon==0.29b0",
5858
},
5959
"fastapi": {

tox.ini

+8-5
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ envlist =
5959
pypy3-test-instrumentation-elasticsearch5
6060

6161
; opentelemetry-instrumentation-falcon
62+
; py310 does not work with falcon 1
63+
py3{6,7,8,9}-test-instrumentation-falcon1
6264
py3{6,7,8,9,10}-test-instrumentation-falcon{2,3}
63-
pypy3-test-instrumentation-falcon{2,3}
65+
pypy3-test-instrumentation-falcon{1,2,3}
6466

6567
; opentelemetry-instrumentation-fastapi
6668
; fastapi only supports 3.6 and above.
@@ -219,6 +221,7 @@ deps =
219221
; FIXME: Elasticsearch >=7 causes CI workflow tests to hang, see open-telemetry/opentelemetry-python-contrib#620
220222
; elasticsearch7: elasticsearch-dsl>=7.0,<8.0
221223
; elasticsearch7: elasticsearch>=7.0,<8.0
224+
falcon1: falcon ==1.4.1
222225
falcon2: falcon >=2.0.0,<3.0.0
223226
falcon3: falcon >=3.0.0,<4.0.0
224227
sqlalchemy11: sqlalchemy>=1.1,<1.2
@@ -258,7 +261,7 @@ changedir =
258261
test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests
259262
test-instrumentation-django{1,2,3,4}: instrumentation/opentelemetry-instrumentation-django/tests
260263
test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests
261-
test-instrumentation-falcon{2,3}: instrumentation/opentelemetry-instrumentation-falcon/tests
264+
test-instrumentation-falcon{1,2,3}: instrumentation/opentelemetry-instrumentation-falcon/tests
262265
test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests
263266
test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests
264267
test-instrumentation-urllib: instrumentation/opentelemetry-instrumentation-urllib/tests
@@ -312,8 +315,8 @@ commands_pre =
312315

313316
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
314317

315-
falcon{2,3},flask,django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
316-
wsgi,falcon{2,3},flask,django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
318+
falcon{1,2,3},flask,django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
319+
wsgi,falcon{1,2,3},flask,django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
317320
asgi,django{3,4},starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
318321

319322
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
@@ -323,7 +326,7 @@ commands_pre =
323326
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test]
324327
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test]
325328

326-
falcon{2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
329+
falcon{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
327330

328331
flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
329332

0 commit comments

Comments
 (0)