Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/skills/new-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode
link_vector = ['<documentation URL>']
support_matrix = {
'<pip-package-name>': {
'>=3.7': ['<version1>', '<version2>']
'>=3.13': ['<major>.*'], # use .* wildcard for latest patch (e.g., '4.*')
'>=3.10': ['<older_minor>.*', '<major>.*'],
}
}
note = """"""
Expand Down
18 changes: 18 additions & 0 deletions .claude/skills/plugin-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ docker build --build-arg BASE_PYTHON_IMAGE=3.11-slim \

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.

## Version Format in support_matrix

Use `.*` wildcard to always test the **latest patch** of each minor version:
```python
support_matrix = {
'falcon': {
'>=3.13': ['4.*'], # latest falcon 4.x
'>=3.10': ['3.1.*', '4.*'],
}
}
```

- `'4.*'` → pip installs `falcon==4.*` → latest 4.x (e.g., 4.2.0 today, 4.3.0 when released)
- `'4.2.*'` → pip installs `falcon==4.2.*` → latest 4.2.x patch
- `'4.2'` → pip installs `falcon==4.2` → always 4.2.0 (misses patches)

**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.

## Step 4: Interpret Results

### Success
Expand Down
11 changes: 9 additions & 2 deletions docs/en/setup/Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
### Plugin Support Table
| Library | Python Version - Lib Version | Plugin Name |
| :--- | :--- | :--- |
| [aiohttp](https://docs.aiohttp.org) | Python >=3.8 - NOT SUPPORTED YET; | `sw_aiohttp` |
| [aiohttp](https://docs.aiohttp.org) | Python >=3.10 - ['3.9.*', '3.11.*']; | `sw_aiohttp` |
| [aioredis](https://aioredis.readthedocs.io/) | Python >=3.7 - ['2.0.*']; | `sw_aioredis` |
| [aiormq](https://pypi.org/project/aiormq/) | Python >=3.7 - ['6.3', '6.4']; | `sw_aiormq` |
| [amqp](https://pypi.org/project/amqp/) | Python >=3.7 - ['2.6.1']; | `sw_amqp` |
Expand All @@ -24,6 +24,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
| [django](https://www.djangoproject.com/) | Python >=3.13 - ['5.1']; Python >=3.10 - ['3.2']; | `sw_django` |
| [elasticsearch](https://github.com/elastic/elasticsearch-py) | Python >=3.7 - ['7.13', '7.14', '7.15']; | `sw_elasticsearch` |
| [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` |
| [falcon](https://falcon.readthedocs.io/en/stable/) | Python >=3.13 - ['4.*']; Python >=3.10 - ['3.1.*', '4.*']; | `sw_falcon_v3` |
| [fastapi](https://fastapi.tiangolo.com) | Python >=3.7 - ['0.89.*', '0.88.*']; | `sw_fastapi` |
| [flask](https://flask.palletsprojects.com) | Python >=3.14 - ['3.0']; Python >=3.10 - ['2.0']; | `sw_flask` |
| [grpcio](https://grpc.io/docs/languages/python) | Python >=3.8 - ['1.*']; | `sw_grpc` |
Expand All @@ -36,7 +37,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
| [mysqlclient](https://mysqlclient.readthedocs.io/) | Python >=3.7 - ['2.1.*']; | `sw_mysqlclient` |
| [neo4j](https://neo4j.com/docs/python-manual/5/) | Python >=3.7 - ['5.*']; | `sw_neo4j` |
| [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` |
| [psycopg2-binary](https://www.psycopg.org/) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['2.9']; | `sw_psycopg2` |
| [psycopg2-binary](https://www.psycopg.org/) | Python >=3.10 - ['2.9.*']; | `sw_psycopg2` |
| [pulsar-client](https://github.com/apache/pulsar-client-python) | Python >=3.12 - ['3.9.0']; Python >=3.10 - ['3.3.0']; | `sw_pulsar` |
| [pymongo](https://pymongo.readthedocs.io) | Python >=3.7 - ['3.11.*']; | `sw_pymongo` |
| [pymysql](https://pymysql.readthedocs.io/en/latest/) | Python >=3.7 - ['1.0']; | `sw_pymysql` |
Expand All @@ -45,6 +46,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
| [redis](https://github.com/andymccurdy/redis-py/) | Python >=3.7 - ['3.5.*', '4.5.1']; | `sw_redis` |
| [requests](https://requests.readthedocs.io/en/master/) | Python >=3.7 - ['2.26', '2.25']; | `sw_requests` |
| [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['20.12']; | `sw_sanic` |
| [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.14 - ['24.12.*']; Python >=3.10 - ['23.12.*', '24.12.*']; | `sw_sanic_v2` |
| [tornado](https://www.tornadoweb.org) | Python >=3.14 - ['6.4']; Python >=3.10 - ['6.0', '6.1']; | `sw_tornado` |
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - NOT SUPPORTED YET; Python >=3.10 - ['1.26', '1.25']; | `sw_urllib3` |
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - ['2.3', '2.0']; | `sw_urllib3_v2` |
Expand All @@ -57,8 +59,13 @@ in SkyWalking currently. Celery clients can use whatever protocol they want.
- While Falcon is instrumented, only Hug is tested.
Hug is believed to be abandoned project, use this plugin with a bit more caution.
Instead of Hug, plugin test should move to test actual Falcon.
- Falcon 3.x/4.x plugin. For legacy hug-based instrumentation, see sw_falcon.
- The Neo4j plugin integrates neo4j python driver 5.x.x versions which
support both Neo4j 5 and 4.4 DBMS.
- Sanic 21.9+ plugin using signal listeners.
For legacy Sanic <=21.3, see sw_sanic.
Note: Sanic's touchup system recompiles handle_request at startup,
so we use signal listeners instead of monkey-patching handle_request.
- urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2.
- urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3.
- The websocket instrumentation only traces client side connection handshake,
Expand Down
12 changes: 8 additions & 4 deletions skywalking/plugins/sw_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
link_vector = ['https://docs.aiohttp.org']
support_matrix = {
'aiohttp': {
'>=3.8': []
'>=3.10': ['3.9.*', '3.11.*'],
}
}
note = """"""
Expand Down Expand Up @@ -81,7 +81,7 @@ async def _sw_request(self: ClientSession, method: str, str_or_url, **kwargs):

_handle_request = RequestHandler._handle_request

async def _sw_handle_request(self, request: BaseRequest, start_time: float):
async def _sw_handle_request(self, request: BaseRequest, start_time: float, *args, **kwargs):

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

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

resp, reset = await _handle_request(self, request, start_time)
resp, reset = await _handle_request(self, request, start_time, *args, **kwargs)

span.tag(TagHttpStatusCode(resp.status))

Expand Down
91 changes: 91 additions & 0 deletions skywalking/plugins/sw_falcon_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from skywalking import Layer, Component, config
from skywalking.trace.carrier import Carrier
from skywalking.trace.context import get_context, NoopContext
from skywalking.trace.span import NoopSpan
from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpParams, TagHttpStatusCode, TagHttpStatusMsg

link_vector = ['https://falcon.readthedocs.io/en/stable/']
support_matrix = {
'falcon': {
'>=3.13': ['4.*'],
'>=3.10': ['3.1.*', '4.*'],
}
}
note = """Falcon 3.x/4.x plugin. For legacy hug-based instrumentation, see sw_falcon."""


def install():
from falcon import App

# Guard: if falcon.App doesn't exist, this is falcon 2.x or older — let sw_falcon handle it
_original_falcon_app = App.__call__

def _sw_falcon_app(this: App, env, start_response):
from falcon import Request, RequestOptions

context = get_context()
carrier = Carrier()
req = Request(env, RequestOptions())
headers = req.headers
method = req.method

for item in carrier:
key = item.key.upper()
if key in headers:
item.val = headers[key]

span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
else context.new_entry_span(op=req.path, carrier=carrier)

with span:
span.layer = Layer.Http
span.component = Component.Falcon
span.peer = req.remote_addr

span.tag(TagHttpMethod(method))
span.tag(TagHttpURL(str(req.url)))

if req.params:
span.tag(TagHttpParams(','.join([f'{k}={v}' for k, v in req.params.items()])))

def _start_response(resp_status, headers):
try:
code, msg = resp_status.split(' ', 1)
code = int(code)
except Exception:
code, msg = 500, 'Internal Server Error'

if code >= 400:
span.error_occurred = True

span.tag(TagHttpStatusCode(code))
span.tag(TagHttpStatusMsg(msg))

return start_response(resp_status, headers)

try:
return _original_falcon_app(this, env, _start_response)

except Exception:
span.raised()

raise

App.__call__ = _sw_falcon_app
3 changes: 1 addition & 2 deletions skywalking/plugins/sw_psycopg2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
link_vector = ['https://www.psycopg.org/']
support_matrix = {
'psycopg2-binary': {
'>=3.10': [],
'>=3.7': ['2.9'] # transition to psycopg(3), not working for python 3.10
'>=3.10': ['2.9.*'],
}
}
note = """"""
Expand Down
100 changes: 100 additions & 0 deletions skywalking/plugins/sw_sanic_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import logging

from skywalking import Layer, Component, config
from skywalking.trace.carrier import Carrier
from skywalking.trace.context import get_context, NoopContext
from skywalking.trace.span import NoopSpan
from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode, TagHttpParams

logger = logging.getLogger(__name__)

link_vector = ['https://sanic.readthedocs.io/en/latest']
support_matrix = {
'sanic': {
'>=3.14': ['24.12.*'],
'>=3.10': ['23.12.*', '24.12.*'],
}
}
note = """Sanic 21.9+ plugin using signal listeners.
For legacy Sanic <=21.3, see sw_sanic.
Note: Sanic's touchup system recompiles handle_request at startup,
so we use signal listeners instead of monkey-patching handle_request."""


def install():
from sanic import Sanic

# Guard: if handle_request still has write_callback param, this is old Sanic — let sw_sanic handle it
import inspect
sig = inspect.signature(Sanic.handle_request)
if 'write_callback' in sig.parameters:
return # old Sanic, skip

_original_init = Sanic.__init__

def _sw_init(self, *args, **kwargs):
_original_init(self, *args, **kwargs)
_register_listeners(self)

Sanic.__init__ = _sw_init


def _register_listeners(app):

def params_tostring(params):
return '\n'.join([f"{k}=[{','.join(params.getlist(k))}]" for k, _ in params.items()])

@app.on_request
async def sw_on_request(request):
carrier = Carrier()
method = request.method

for item in carrier:
if item.key.capitalize() in request.headers:
item.val = request.headers[item.key.capitalize()]

span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
else get_context().new_entry_span(op=request.path, carrier=carrier)

span.start()
span.layer = Layer.Http
span.component = Component.Sanic
span.peer = f'{request.remote_addr or request.ip}:{request.port}'
span.tag(TagHttpMethod(method))
span.tag(TagHttpURL(request.url.split('?')[0]))
if config.plugin_sanic_collect_http_params and request.args:
span.tag(TagHttpParams(
params_tostring(request.args)[0:config.plugin_http_http_params_length_threshold]
))

request.ctx._sw_span = span

@app.on_response
async def sw_on_response(request, response):
span = getattr(request.ctx, '_sw_span', None)
if span is None:
return

if response is not None:
span.tag(TagHttpStatusCode(response.status))
if response.status >= 400:
span.error_occurred = True

span.stop()
2 changes: 1 addition & 1 deletion tests/e2e/case/expected/traces-list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ traces:
- segmentid: {{ notEmpty .segmentid }}
endpointnames:
{{- contains .endpointnames }}
- /artist-provider
- {{ regexp . "/artist-(consumer|provider)" }}
{{- end }}
duration: {{ ge .duration 0 }}
start: {{ notEmpty .start}}
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/case/logging-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
- query: |
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql logs list --service-name=e2e-service-provider --trace-id=$( \
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace ls \
| yq e '.traces | select(.[].endpointnames[0]=="/artist-provider") | .[0].traceids[0]' -
| yq e '.traces | select(.[].endpointnames[] == "/artist-provider") | .[0].traceids[0]' -
)
expected: expected/logs-list.yml
2 changes: 1 addition & 1 deletion tests/e2e/case/tracing-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
- query: |
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace $( \
swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql trace ls --service-name="e2e-service-consumer|namespace"\
| yq e '.traces | select(.[].endpointnames[0]=="/artist-consumer") | .[0].traceids[0]' -
| yq e '.traces | select(.[].endpointnames[] == "/artist-consumer") | .[0].traceids[0]' -
)
expected: expected/trace-artist-detail.yml
8 changes: 5 additions & 3 deletions tests/plugin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ def validate(self, expected_file_name=None):

response = requests.post(url='http://localhost:12800/dataValidate', data=expected_data)

if response.status_code != 200:
# heuristically retry once
time.sleep(10)
# Retry with backoff — segments may not have been reported yet
for i in range(3):
if response.status_code == 200:
break
time.sleep(5 * (i + 1))
response = requests.post(url='http://localhost:12800/dataValidate', data=expected_data)

if response.status_code != 200:
Expand Down
16 changes: 16 additions & 0 deletions tests/plugin/web/sw_falcon_v3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
Loading
Loading