You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2020/08/13 14:27:32 UTC

[skywalking-python] branch master updated: [Plugin] check supported version of packages when install plugins (#63)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-python.git


The following commit(s) were added to refs/heads/master by this push:
     new 518002f  [Plugin] check supported version of packages when install plugins (#63)
518002f is described below

commit 518002f0dea88f77657eb17a1cda4cb14a22c97a
Author: Humbertzhang <50...@qq.com>
AuthorDate: Thu Aug 13 22:27:23 2020 +0800

    [Plugin] check supported version of packages when install plugins (#63)
---
 docs/Developer.md                |  15 ++++++
 skywalking/plugins/__init__.py   |  74 +++++++++++++++++++++++++++++
 skywalking/plugins/sw_django.py  |   5 ++
 skywalking/plugins/sw_pymongo.py |  13 ++---
 tests/test_version_check.py      | 100 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 199 insertions(+), 8 deletions(-)

diff --git a/docs/Developer.md b/docs/Developer.md
index 0051e68..0cf7ba3 100644
--- a/docs/Developer.md
+++ b/docs/Developer.md
@@ -12,6 +12,21 @@
 You can always take [the existing plugins](../skywalking/plugins) as examples, while there are some general ideas for all plugins.
 1. A plugin is a module under directory [`skywalking/plugins/`](../skywalking/plugins) with an `install` method; 
 1. Inside the `install` method, you find out the relevant method(s) of the libraries that you plan to instrument, and create/close spans before/after those method(s).
+1. You should also provide version rules in the plugin module, which means the version of package your plugin support. You should init a dict with keys `name` and `rules`. the `name` is your plugin's corresponding package's name, the `rules` is the version rules this package should follow.
+   
+   You can use >, >=, ==, <=, <, and != operators in rules. 
+   
+   The relation between rules element in the rules array is **OR**, which means the version of the package should follow at least one rule in rules array.
+   
+   You can set many version rules in one element of rules array, separate each other with a space character, the relation of rules in one rule element is **AND**, which means the version of package should follow all rules in this rule element.
+   
+   For example, below `version_rule` indicates that the package version of `django` should `>=2.0 AND <=2.3 AND !=2.2.1` OR `>3.0`.
+   ```python
+   version_rule = {
+       "name": "django",
+       "rules": [">=2.0 <=2.3 !=2.2.1", ">3.0"]
+   }
+   ```
 1. Every plugin requires a corresponding test under [`tests/plugin`](../tests/plugin) before it can be merged, refer to [the plugin test guide](PluginTest.md) when writing a plugin test.
 1. Update [the supported list](Plugins.md).
 1. Add the environment variables to [the list](EnvVars.md) if any.
diff --git a/skywalking/plugins/__init__.py b/skywalking/plugins/__init__.py
index 3995f1d..591cb14 100644
--- a/skywalking/plugins/__init__.py
+++ b/skywalking/plugins/__init__.py
@@ -18,6 +18,9 @@ import inspect
 import logging
 import pkgutil
 import re
+import pkg_resources
+
+from packaging import version
 
 from skywalking import config
 
@@ -38,7 +41,78 @@ def install():
             continue
         logger.debug('installing plugin %s', modname)
         plugin = importer.find_module(modname).load_module(modname)
+
+        supported = pkg_version_check(plugin)
+        if not supported:
+            logger.debug('check version for plugin %s\'s corresponding package failed, thus '
+                         'won\'t be installed', modname)
+            continue
+
         if not hasattr(plugin, 'install') or inspect.ismethod(getattr(plugin, 'install')):
             logger.warning('no `install` method in plugin %s, thus the plugin won\'t be installed', modname)
             continue
         plugin.install()
+
+
+_operators = {
+    '<': lambda cv, ev: cv < ev,
+    '<=': lambda cv, ev: cv < ev or cv == ev,
+    '==': lambda cv, ev: cv == ev,
+    '>=': lambda cv, ev: cv > ev or cv == ev,
+    '>': lambda cv, ev: cv > ev,
+    '!=': lambda cv, ev: cv != ev
+}
+
+
+class VersionRuleException(Exception):
+    def __init__(self, message):
+        self.message = message
+
+
+def pkg_version_check(plugin):
+    supported = True
+
+    # no version rules was set, no checks
+    if not hasattr(plugin, "version_rule"):
+        return supported
+
+    pkg_name = plugin.version_rule.get("name")
+    rules = plugin.version_rule.get("rules")
+
+    try:
+        current_pkg_version = pkg_resources.get_distribution(pkg_name).version
+    except pkg_resources.DistributionNotFound:
+        # when failed to get the version, we consider it as supported.
+        return supported
+
+    current_version = version.parse(current_pkg_version)
+    # pass one rule in rules (OR)
+    for rule in rules:
+        if rule.find(" ") == -1:
+            if check(rule, current_version):
+                return supported
+        else:
+            # have to pass all rule_uint in this rule (AND)
+            rule_units = rule.split(" ")
+            results = [check(unit, current_version) for unit in rule_units]
+            if False in results:
+                # check failed, try to check next rule
+                continue
+            else:
+                return supported
+
+    supported = False
+    return supported
+
+
+def check(rule_unit, current_version):
+    idx = 2 if rule_unit[1] == '=' else 1
+    symbol = rule_unit[0:idx]
+    expect_pkg_version = rule_unit[idx:]
+
+    expect_version = version.parse(expect_pkg_version)
+    f = _operators.get(symbol) or None
+    if not f:
+        raise VersionRuleException("version rule {} error. only allow >,>=,==,<=,<,!= symbols".format(rule_unit))
+
+    return f(current_version, expect_version)
diff --git a/skywalking/plugins/sw_django.py b/skywalking/plugins/sw_django.py
index d920d2f..08681c4 100644
--- a/skywalking/plugins/sw_django.py
+++ b/skywalking/plugins/sw_django.py
@@ -24,6 +24,11 @@ from skywalking.trace.tags import Tag
 
 logger = logging.getLogger(__name__)
 
+version_rule = {
+    "name": "django",
+    "rules": [">=2.0"]
+}
+
 
 def install():
     try:
diff --git a/skywalking/plugins/sw_pymongo.py b/skywalking/plugins/sw_pymongo.py
index 06a8dcb..e371f8e 100644
--- a/skywalking/plugins/sw_pymongo.py
+++ b/skywalking/plugins/sw_pymongo.py
@@ -16,8 +16,6 @@
 #
 
 import logging
-import pkg_resources
-from packaging import version
 
 from skywalking import Layer, Component, config
 from skywalking.trace import tags
@@ -27,6 +25,11 @@ from skywalking.trace.tags import Tag
 
 logger = logging.getLogger(__name__)
 
+version_rule = {
+    "name": "pymongo",
+    "rules": [">=3.7.0"]
+}
+
 
 def install():
     try:
@@ -34,12 +37,6 @@ def install():
         from pymongo.cursor import Cursor
         from pymongo.pool import SocketInfo
 
-        # check pymongo version
-        pymongo_version = pkg_resources.get_distribution("pymongo").version
-        if version.parse(pymongo_version) < version.parse("3.7.0"):
-            logger.warning("support pymongo version 3.7.0 or above, current version:" + pymongo_version)
-            raise Exception
-
         bulk_op_map = {
                 0: "insert",
                 1: "update",
diff --git a/tests/test_version_check.py b/tests/test_version_check.py
new file mode 100644
index 0000000..371b14d
--- /dev/null
+++ b/tests/test_version_check.py
@@ -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 unittest
+
+from packaging import version
+
+from skywalking.plugins import _operators, check
+
+
+class TestVersionCheck(unittest.TestCase):
+    def test_operators(self):
+        # <
+        f = _operators.get("<")
+        v1 = version.parse("1.0.0")
+        v2 = version.parse("1.0.1")
+        self.assertTrue(f(v1, v2))
+        self.assertFalse(f(v2, v1))
+
+        v2 = version.parse("1.0.0")
+        self.assertFalse(f(v1, v2))
+
+        # <=
+        f = _operators.get("<=")
+        v1 = version.parse("1.0")
+        v2 = version.parse("1.0")
+        self.assertTrue(v1, v2)
+
+        v2 = version.parse("1.1.0")
+        self.assertTrue(f(v1, v2))
+        self.assertFalse(f(v2, v1))
+
+        # =
+        f = _operators.get("==")
+        v1 = version.parse("1.0.0")
+        v2 = version.parse("1.0.0")
+        self.assertTrue(f(v1, v2))
+
+        v2 = version.parse("1.0.1")
+        self.assertFalse(f(v1, v2))
+
+        # >=
+        f = _operators.get(">=")
+        v1 = version.parse("1.0.0")
+        v2 = version.parse("1.0.0")
+        self.assertTrue(f(v1, v2))
+
+        v2 = version.parse("1.0.1")
+        self.assertFalse(f(v1, v2))
+        self.assertTrue(f(v2, v1))
+
+        # >
+        f = _operators.get(">")
+        v1 = version.parse("1.0.0")
+        v2 = version.parse("1.0.1")
+        self.assertFalse(f(v1, v2))
+        self.assertTrue(f(v2, v1))
+
+        v2 = version.parse("1.0.0")
+        self.assertFalse(f(v1, v2))
+
+        # !=
+        f = _operators.get("!=")
+        v1 = version.parse("1.0.0")
+        v2 = version.parse("1.0.1")
+        self.assertTrue(f(v1, v2))
+
+        v2 = version.parse("1.0.0")
+        self.assertFalse(f(v1, v2))
+
+    def test_version_check(self):
+        current_version = version.parse("1.8.0")
+
+        self.assertTrue(check(">1.1.0", current_version))
+        self.assertTrue(check(">=1.0.0", current_version))
+        self.assertTrue(check("<2.0.0", current_version))
+        self.assertTrue(check("<=1.8.0", current_version))
+        self.assertTrue(check("==1.8.0", current_version))
+        self.assertTrue(check("!=1.6.0", current_version))
+
+        self.assertFalse(check(">1.9.0", current_version))
+        self.assertFalse(check(">=1.8.1", current_version))
+        self.assertFalse(check("<1.8.0", current_version))
+        self.assertFalse(check("<=1.7.0", current_version))
+        self.assertFalse(check("==1.0.0", current_version))
+        self.assertFalse(check("!=1.8.0", current_version))