Skip to content

Commit 71640af

Browse files
authored
feat: re-enable aiohttp/psycopg2 and add falcon v3/sanic v2 plugins (#389)
## Summary Re-enable previously skipped plugins, add new plugins for modern framework versions, fix E2E flaky tests, and improve plugin test stability. ### New Plugins - `sw_falcon_v3.py`: Falcon 3.x/4.x plugin, hooks `falcon.App.__call__`. Replaces abandoned hug-based `sw_falcon` for modern Falcon. - `sw_sanic_v2.py`: Sanic 21.9+ plugin, uses `@app.on_request`/`@app.on_response` signal listeners instead of monkey-patching `handle_request` (Sanic's touchup system recompiles patched methods via `compile()+exec()`, losing closure variables). ### Re-enabled Plugins - `sw_aiohttp.py`: Was `>=3.8: []` (fully skipped). Now tests aiohttp 3.9/3.11. Fixed `_handle_request` signature (`*args` for forward compat). Fixed `request.url` ValueError with yarl >= 1.18 (fallback URL construction). - `sw_psycopg2.py`: Was `>=3.10: []` (skipped). Now tests `2.9.*` on all supported Python versions. ### New Test Directories - `tests/plugin/web/sw_falcon_v3/`: Full integration test with Falcon-native services using `wsgiref`. - `tests/plugin/web/sw_sanic_v2/`: Full integration test with `single_process=True` Sanic services. ### E2E Flaky Fixes - `tracing-cases.yaml`: `endpointnames[0]` → `endpointnames[]` in yq select (matches endpoint at any array position). - `logging-cases.yaml`: Same fix. - `traces-list.yml`: Hardcoded `/artist-provider` → `{{ regexp . "/artist-(consumer|provider)" }}` (endpoint name is non-deterministic from OAP). ### Test Stability - `base.py`: Increased validation retry from 1×10s to 3× with backoff (5s/10s/15s = 30s total max). Fixes timing-sensitive tests (e.g., happybase) where segments haven't arrived at the collector yet. ### Docs - `Plugins.md`: Regenerated with new plugins and updated versions.
1 parent 78bed98 commit 71640af

File tree

25 files changed

+815
-15
lines changed

25 files changed

+815
-15
lines changed

.claude/skills/new-plugin/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode
3434
link_vector = ['<documentation URL>']
3535
support_matrix = {
3636
'<pip-package-name>': {
37-
'>=3.7': ['<version1>', '<version2>']
37+
'>=3.13': ['<major>.*'], # use .* wildcard for latest patch (e.g., '4.*')
38+
'>=3.10': ['<older_minor>.*', '<major>.*'],
3839
}
3940
}
4041
note = """"""

.claude/skills/plugin-test/SKILL.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ docker build --build-arg BASE_PYTHON_IMAGE=3.11-slim \
135135

136136
Note: E2E tests require the `e2e` CLI tool from SkyWalking infra-e2e. They are typically only run in CI. Inform the user if they ask for E2E.
137137

138+
## Version Format in support_matrix
139+
140+
Use `.*` wildcard to always test the **latest patch** of each minor version:
141+
```python
142+
support_matrix = {
143+
'falcon': {
144+
'>=3.13': ['4.*'], # latest falcon 4.x
145+
'>=3.10': ['3.1.*', '4.*'],
146+
}
147+
}
148+
```
149+
150+
- `'4.*'` → pip installs `falcon==4.*` → latest 4.x (e.g., 4.2.0 today, 4.3.0 when released)
151+
- `'4.2.*'` → pip installs `falcon==4.2.*` → latest 4.2.x patch
152+
- `'4.2'` → pip installs `falcon==4.2` → always 4.2.0 (misses patches)
153+
154+
**Convention**: use `major.*` (e.g., `'4.*'`) when the plugin supports the whole major version, or `minor.*` (e.g., `'3.11.*'`) when only specific minors are tested. This keeps CI testing fresh and the Plugins.md doc meaningful.
155+
138156
## Step 4: Interpret Results
139157

140158
### Success

docs/en/setup/Plugins.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
1313
### Plugin Support Table
1414
| Library | Python Version - Lib Version | Plugin Name |
1515
| :--- | :--- | :--- |
16-
| [aiohttp](https://docs.aiohttp.org) | Python >=3.8 - NOT SUPPORTED YET; | `sw_aiohttp` |
16+
| [aiohttp](https://docs.aiohttp.org) | Python >=3.10 - ['3.9.*', '3.11.*']; | `sw_aiohttp` |
1717
| [aioredis](https://aioredis.readthedocs.io/) | Python >=3.7 - ['2.0.*']; | `sw_aioredis` |
1818
| [aiormq](https://pypi.org/project/aiormq/) | Python >=3.7 - ['6.3', '6.4']; | `sw_aiormq` |
1919
| [amqp](https://pypi.org/project/amqp/) | Python >=3.7 - ['2.6.1']; | `sw_amqp` |
@@ -24,6 +24,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
2424
| [django](https://www.djangoproject.com/) | Python >=3.13 - ['5.1']; Python >=3.10 - ['3.2']; | `sw_django` |
2525
| [elasticsearch](https://github.com/elastic/elasticsearch-py) | Python >=3.7 - ['7.13', '7.14', '7.15']; | `sw_elasticsearch` |
2626
| [hug](https://falcon.readthedocs.io/en/stable/) | Python >=3.11 - NOT SUPPORTED YET; Python >=3.10 - ['2.5', '2.6']; Python >=3.7 - ['2.4.1', '2.5', '2.6']; | `sw_falcon` |
27+
| [falcon](https://falcon.readthedocs.io/en/stable/) | Python >=3.13 - ['4.*']; Python >=3.10 - ['3.1.*', '4.*']; | `sw_falcon_v3` |
2728
| [fastapi](https://fastapi.tiangolo.com) | Python >=3.7 - ['0.89.*', '0.88.*']; | `sw_fastapi` |
2829
| [flask](https://flask.palletsprojects.com) | Python >=3.14 - ['3.0']; Python >=3.10 - ['2.0']; | `sw_flask` |
2930
| [grpcio](https://grpc.io/docs/languages/python) | Python >=3.8 - ['1.*']; | `sw_grpc` |
@@ -36,7 +37,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
3637
| [mysqlclient](https://mysqlclient.readthedocs.io/) | Python >=3.7 - ['2.1.*']; | `sw_mysqlclient` |
3738
| [neo4j](https://neo4j.com/docs/python-manual/5/) | Python >=3.7 - ['5.*']; | `sw_neo4j` |
3839
| [psycopg[binary]](https://www.psycopg.org/) | Python >=3.13 - ['3.2.*']; Python >=3.11 - ['3.1.*']; Python >=3.10 - ['3.0.18', '3.1.*']; | `sw_psycopg` |
39-
| [psycopg2-binary](https://www.psycopg.org/) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['2.9']; | `sw_psycopg2` |
40+
| [psycopg2-binary](https://www.psycopg.org/) | Python >=3.10 - ['2.9.*']; | `sw_psycopg2` |
4041
| [pulsar-client](https://github.com/apache/pulsar-client-python) | Python >=3.12 - ['3.9.0']; Python >=3.10 - ['3.3.0']; | `sw_pulsar` |
4142
| [pymongo](https://pymongo.readthedocs.io) | Python >=3.7 - ['3.11.*']; | `sw_pymongo` |
4243
| [pymysql](https://pymysql.readthedocs.io/en/latest/) | Python >=3.7 - ['1.0']; | `sw_pymysql` |
@@ -45,6 +46,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
4546
| [redis](https://github.com/andymccurdy/redis-py/) | Python >=3.7 - ['3.5.*', '4.5.1']; | `sw_redis` |
4647
| [requests](https://requests.readthedocs.io/en/master/) | Python >=3.7 - ['2.26', '2.25']; | `sw_requests` |
4748
| [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['20.12']; | `sw_sanic` |
49+
| [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.14 - ['24.12.*']; Python >=3.10 - ['23.12.*', '24.12.*']; | `sw_sanic_v2` |
4850
| [tornado](https://www.tornadoweb.org) | Python >=3.14 - ['6.4']; Python >=3.10 - ['6.0', '6.1']; | `sw_tornado` |
4951
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - NOT SUPPORTED YET; Python >=3.10 - ['1.26', '1.25']; | `sw_urllib3` |
5052
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - ['2.3', '2.0']; | `sw_urllib3_v2` |
@@ -57,8 +59,13 @@ in SkyWalking currently. Celery clients can use whatever protocol they want.
5759
- While Falcon is instrumented, only Hug is tested.
5860
Hug is believed to be abandoned project, use this plugin with a bit more caution.
5961
Instead of Hug, plugin test should move to test actual Falcon.
62+
- Falcon 3.x/4.x plugin. For legacy hug-based instrumentation, see sw_falcon.
6063
- The Neo4j plugin integrates neo4j python driver 5.x.x versions which
6164
support both Neo4j 5 and 4.4 DBMS.
65+
- Sanic 21.9+ plugin using signal listeners.
66+
For legacy Sanic <=21.3, see sw_sanic.
67+
Note: Sanic's touchup system recompiles handle_request at startup,
68+
so we use signal listeners instead of monkey-patching handle_request.
6269
- urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2.
6370
- urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3.
6471
- The websocket instrumentation only traces client side connection handshake,

skywalking/plugins/sw_aiohttp.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
link_vector = ['https://docs.aiohttp.org']
2525
support_matrix = {
2626
'aiohttp': {
27-
'>=3.8': []
27+
'>=3.10': ['3.9.*', '3.11.*'],
2828
}
2929
}
3030
note = """"""
@@ -81,7 +81,7 @@ async def _sw_request(self: ClientSession, method: str, str_or_url, **kwargs):
8181

8282
_handle_request = RequestHandler._handle_request
8383

84-
async def _sw_handle_request(self, request: BaseRequest, start_time: float):
84+
async def _sw_handle_request(self, request: BaseRequest, start_time: float, *args, **kwargs):
8585

8686
if config.agent_protocol == 'http' and config.agent_collector_backend_services.rstrip('/') \
8787
.endswith(f'{request.url.host}:{request.url.port}'):
@@ -109,9 +109,13 @@ async def _sw_handle_request(self, request: BaseRequest, start_time: float):
109109
span.peer = f'{peer_name}'
110110

111111
span.tag(TagHttpMethod(method)) # pyre-ignore
112-
span.tag(TagHttpURL(str(request.url))) # pyre-ignore
112+
try:
113+
span.tag(TagHttpURL(str(request.url))) # pyre-ignore
114+
except ValueError:
115+
# yarl >= 1.18 rejects host:port in URL.build; fallback to path
116+
span.tag(TagHttpURL(f'{request.scheme}://{request.host}{request.path}'))
113117

114-
resp, reset = await _handle_request(self, request, start_time)
118+
resp, reset = await _handle_request(self, request, start_time, *args, **kwargs)
115119

116120
span.tag(TagHttpStatusCode(resp.status))
117121

skywalking/plugins/sw_falcon_v3.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
from skywalking import Layer, Component, config
19+
from skywalking.trace.carrier import Carrier
20+
from skywalking.trace.context import get_context, NoopContext
21+
from skywalking.trace.span import NoopSpan
22+
from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpParams, TagHttpStatusCode, TagHttpStatusMsg
23+
24+
link_vector = ['https://falcon.readthedocs.io/en/stable/']
25+
support_matrix = {
26+
'falcon': {
27+
'>=3.13': ['4.*'],
28+
'>=3.10': ['3.1.*', '4.*'],
29+
}
30+
}
31+
note = """Falcon 3.x/4.x plugin. For legacy hug-based instrumentation, see sw_falcon."""
32+
33+
34+
def install():
35+
from falcon import App
36+
37+
# Guard: if falcon.App doesn't exist, this is falcon 2.x or older — let sw_falcon handle it
38+
_original_falcon_app = App.__call__
39+
40+
def _sw_falcon_app(this: App, env, start_response):
41+
from falcon import Request, RequestOptions
42+
43+
context = get_context()
44+
carrier = Carrier()
45+
req = Request(env, RequestOptions())
46+
headers = req.headers
47+
method = req.method
48+
49+
for item in carrier:
50+
key = item.key.upper()
51+
if key in headers:
52+
item.val = headers[key]
53+
54+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
55+
else context.new_entry_span(op=req.path, carrier=carrier)
56+
57+
with span:
58+
span.layer = Layer.Http
59+
span.component = Component.Falcon
60+
span.peer = req.remote_addr
61+
62+
span.tag(TagHttpMethod(method))
63+
span.tag(TagHttpURL(str(req.url)))
64+
65+
if req.params:
66+
span.tag(TagHttpParams(','.join([f'{k}={v}' for k, v in req.params.items()])))
67+
68+
def _start_response(resp_status, headers):
69+
try:
70+
code, msg = resp_status.split(' ', 1)
71+
code = int(code)
72+
except Exception:
73+
code, msg = 500, 'Internal Server Error'
74+
75+
if code >= 400:
76+
span.error_occurred = True
77+
78+
span.tag(TagHttpStatusCode(code))
79+
span.tag(TagHttpStatusMsg(msg))
80+
81+
return start_response(resp_status, headers)
82+
83+
try:
84+
return _original_falcon_app(this, env, _start_response)
85+
86+
except Exception:
87+
span.raised()
88+
89+
raise
90+
91+
App.__call__ = _sw_falcon_app

skywalking/plugins/sw_psycopg2.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
link_vector = ['https://www.psycopg.org/']
2323
support_matrix = {
2424
'psycopg2-binary': {
25-
'>=3.10': [],
26-
'>=3.7': ['2.9'] # transition to psycopg(3), not working for python 3.10
25+
'>=3.10': ['2.9.*'],
2726
}
2827
}
2928
note = """"""

skywalking/plugins/sw_sanic_v2.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import logging
19+
20+
from skywalking import Layer, Component, config
21+
from skywalking.trace.carrier import Carrier
22+
from skywalking.trace.context import get_context, NoopContext
23+
from skywalking.trace.span import NoopSpan
24+
from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode, TagHttpParams
25+
26+
logger = logging.getLogger(__name__)
27+
28+
link_vector = ['https://sanic.readthedocs.io/en/latest']
29+
support_matrix = {
30+
'sanic': {
31+
'>=3.14': ['24.12.*'],
32+
'>=3.10': ['23.12.*', '24.12.*'],
33+
}
34+
}
35+
note = """Sanic 21.9+ plugin using signal listeners.
36+
For legacy Sanic <=21.3, see sw_sanic.
37+
Note: Sanic's touchup system recompiles handle_request at startup,
38+
so we use signal listeners instead of monkey-patching handle_request."""
39+
40+
41+
def install():
42+
from sanic import Sanic
43+
44+
# Guard: if handle_request still has write_callback param, this is old Sanic — let sw_sanic handle it
45+
import inspect
46+
sig = inspect.signature(Sanic.handle_request)
47+
if 'write_callback' in sig.parameters:
48+
return # old Sanic, skip
49+
50+
_original_init = Sanic.__init__
51+
52+
def _sw_init(self, *args, **kwargs):
53+
_original_init(self, *args, **kwargs)
54+
_register_listeners(self)
55+
56+
Sanic.__init__ = _sw_init
57+
58+
59+
def _register_listeners(app):
60+
61+
def params_tostring(params):
62+
return '\n'.join([f"{k}=[{','.join(params.getlist(k))}]" for k, _ in params.items()])
63+
64+
@app.on_request
65+
async def sw_on_request(request):
66+
carrier = Carrier()
67+
method = request.method
68+
69+
for item in carrier:
70+
if item.key.capitalize() in request.headers:
71+
item.val = request.headers[item.key.capitalize()]
72+
73+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
74+
else get_context().new_entry_span(op=request.path, carrier=carrier)
75+
76+
span.start()
77+
span.layer = Layer.Http
78+
span.component = Component.Sanic
79+
span.peer = f'{request.remote_addr or request.ip}:{request.port}'
80+
span.tag(TagHttpMethod(method))
81+
span.tag(TagHttpURL(request.url.split('?')[0]))
82+
if config.plugin_sanic_collect_http_params and request.args:
83+
span.tag(TagHttpParams(
84+
params_tostring(request.args)[0:config.plugin_http_http_params_length_threshold]
85+
))
86+
87+
request.ctx._sw_span = span
88+
89+
@app.on_response
90+
async def sw_on_response(request, response):
91+
span = getattr(request.ctx, '_sw_span', None)
92+
if span is None:
93+
return
94+
95+
if response is not None:
96+
span.tag(TagHttpStatusCode(response.status))
97+
if response.status >= 400:
98+
span.error_occurred = True
99+
100+
span.stop()

tests/e2e/case/expected/traces-list.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ traces:
1919
- segmentid: {{ notEmpty .segmentid }}
2020
endpointnames:
2121
{{- contains .endpointnames }}
22-
- /artist-provider
22+
- {{ regexp . "/artist-(consumer|provider)" }}
2323
{{- end }}
2424
duration: {{ ge .duration 0 }}
2525
start: {{ notEmpty .start}}

tests/e2e/case/logging-cases.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
- query: |
2121
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql logs list --service-name=e2e-service-provider --trace-id=$( \
2222
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace ls \
23-
| yq e '.traces | select(.[].endpointnames[0]=="/artist-provider") | .[0].traceids[0]' -
23+
| yq e '.traces | select(.[].endpointnames[] == "/artist-provider") | .[0].traceids[0]' -
2424
)
2525
expected: expected/logs-list.yml

tests/e2e/case/tracing-cases.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@
2424
- query: |
2525
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace $( \
2626
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace ls --service-name="e2e-service-consumer|namespace"\
27-
| yq e '.traces | select(.[].endpointnames[0]=="/artist-consumer") | .[0].traceids[0]' -
27+
| yq e '.traces | select(.[].endpointnames[] == "/artist-consumer") | .[0].traceids[0]' -
2828
)
2929
expected: expected/trace-artist-detail.yml

0 commit comments

Comments
 (0)