Skip to content

Commit 86857fa

Browse files
wu-shengclaude
andcommitted
feat: re-enable aiohttp/psycopg2 and add falcon v3/sanic v2 plugins
Re-enable previously skipped plugins and add new plugins for modern framework versions. Also fix flaky E2E test. aiohttp (re-enabled): - Add support_matrix: >=3.10 with versions 3.9, 3.11 - Fix _handle_request signature: add *args for forward compat (aiohttp added request_handler positional param) - Fix request.url ValueError with yarl >= 1.18: fallback to manual URL construction when Host contains ':' - Verified: aiohttp 3.9 and 3.11 PASSED on Python 3.13 psycopg2 (re-enabled): - Change support_matrix from >=3.10: [] to >=3.10: ['2.9.*'] (wildcard so pip resolves to 2.9.11 which has cp313 wheels) - Verified: psycopg2-binary 2.9.* PASSED on Python 3.13 falcon v3 (new plugin): - New sw_falcon_v3.py hooking falcon.App.__call__ (falcon.API removed in falcon 5.0) - support_matrix: >=3.13: ['4.0'], >=3.10: ['3.1', '4.0'] (falcon 3.1 has no cp313 wheels, only test 4.0 on 3.13+) - New test directory with falcon-native services (wsgiref) - Verified: falcon 4.0 PASSED on Python 3.13 sanic v2 (new plugin): - New sw_sanic_v2.py using Sanic signal listeners (@app.on_request / @app.on_response) instead of monkey-patching handle_request - Reason: Sanic's touchup system recompiles handle_request at startup via compile()+exec(), losing the patched function's globals (NameError: Carrier). Signal listeners avoid this. - support_matrix: >=3.10: ['23.12', '24.12'] - New test directory with single_process=True services - Verified: sanic 23.12 and 24.12 PASSED on Python 3.13 E2E flaky fix: - tracing-cases.yaml and logging-cases.yaml: change endpointnames[0] to endpointnames[] in yq select queries - The endpoint names array order is non-deterministic from OAP; using [] matches at any position instead of assuming index 0 Regenerate Plugins.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 78bed98 commit 86857fa

21 files changed

Lines changed: 788 additions & 10 deletions

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.0']; Python >=3.10 - ['3.1', '4.0']; | `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.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.0'],
28+
'>=3.10': ['3.1', '4.0'],
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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.10': ['23.12', '24.12'],
32+
}
33+
}
34+
note = """Sanic 21.9+ plugin using signal listeners.
35+
For legacy Sanic <=21.3, see sw_sanic.
36+
Note: Sanic's touchup system recompiles handle_request at startup,
37+
so we use signal listeners instead of monkey-patching handle_request."""
38+
39+
40+
def install():
41+
from sanic import Sanic
42+
43+
# Guard: if handle_request still has write_callback param, this is old Sanic — let sw_sanic handle it
44+
import inspect
45+
sig = inspect.signature(Sanic.handle_request)
46+
if 'write_callback' in sig.parameters:
47+
return # old Sanic, skip
48+
49+
_original_init = Sanic.__init__
50+
51+
def _sw_init(self, *args, **kwargs):
52+
_original_init(self, *args, **kwargs)
53+
_register_listeners(self)
54+
55+
Sanic.__init__ = _sw_init
56+
57+
58+
def _register_listeners(app):
59+
60+
def params_tostring(params):
61+
return '\n'.join([f"{k}=[{','.join(params.getlist(k))}]" for k, _ in params.items()])
62+
63+
@app.on_request
64+
async def sw_on_request(request):
65+
carrier = Carrier()
66+
method = request.method
67+
68+
for item in carrier:
69+
if item.key.capitalize() in request.headers:
70+
item.val = request.headers[item.key.capitalize()]
71+
72+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
73+
else get_context().new_entry_span(op=request.path, carrier=carrier)
74+
75+
span.start()
76+
span.layer = Layer.Http
77+
span.component = Component.Sanic
78+
span.peer = f'{request.remote_addr or request.ip}:{request.port}'
79+
span.tag(TagHttpMethod(method))
80+
span.tag(TagHttpURL(request.url.split('?')[0]))
81+
if config.plugin_sanic_collect_http_params and request.args:
82+
span.tag(TagHttpParams(
83+
params_tostring(request.args)[0:config.plugin_http_http_params_length_threshold]
84+
))
85+
86+
request.ctx._sw_span = span
87+
88+
@app.on_response
89+
async def sw_on_response(request, response):
90+
span = getattr(request.ctx, '_sw_span', None)
91+
if span is None:
92+
return
93+
94+
if response is not None:
95+
span.tag(TagHttpStatusCode(response.status))
96+
if response.status >= 400:
97+
span.error_occurred = True
98+
99+
span.stop()

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
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
#
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
version: '2.1'
19+
20+
services:
21+
collector:
22+
extends:
23+
service: collector
24+
file: ../../docker-compose.base.yml
25+
26+
provider:
27+
extends:
28+
service: agent
29+
file: ../../docker-compose.base.yml
30+
ports:
31+
- 9091:9091
32+
volumes:
33+
- .:/app
34+
command: ['bash', '-c', 'pip install -r /app/requirements.txt && sw-python run python3 /app/services/provider.py']
35+
depends_on:
36+
collector:
37+
condition: service_healthy
38+
healthcheck:
39+
test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9091"]
40+
interval: 5s
41+
timeout: 60s
42+
retries: 120
43+
environment:
44+
SW_AGENT_NAME: provider
45+
SW_AGENT_LOGGING_LEVEL: DEBUG
46+
47+
consumer:
48+
extends:
49+
service: agent
50+
file: ../../docker-compose.base.yml
51+
ports:
52+
- 9090:9090
53+
volumes:
54+
- .:/app
55+
command: ['bash', '-c', 'pip install -r /app/requirements.txt && sw-python run python3 /app/services/consumer.py']
56+
depends_on:
57+
collector:
58+
condition: service_healthy
59+
provider:
60+
condition: service_healthy
61+
environment:
62+
SW_AGENT_NAME: consumer
63+
SW_AGENT_LOGGING_LEVEL: DEBUG
64+
65+
networks:
66+
beyond:

0 commit comments

Comments
 (0)