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 2023/02/08 02:31:57 UTC

[skywalking-python] branch master updated: Add loguru support (#276)

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 20ce46a  Add loguru support (#276)
20ce46a is described below

commit 20ce46a4532ea31da1cca93887d71360003255ea
Author: Jedore <je...@protonmail.com>
AuthorDate: Wed Feb 8 10:31:51 2023 +0800

    Add loguru support (#276)
---
 CHANGELOG.md                                     |   1 +
 docs/en/setup/Configuration.md                   |   4 +-
 docs/en/setup/Plugins.md                         |   1 +
 docs/en/setup/advanced/LogReporter.md            |   2 +-
 poetry.lock                                      |  36 +++-
 pyproject.toml                                   |   1 +
 skywalking/config.py                             |   2 +
 skywalking/plugins/sw_loguru.py                  | 227 +++++++++++++++++++++++
 tests/plugin/data/sw_loguru/__init__.py          |  16 ++
 tests/plugin/data/sw_loguru/docker-compose.yml   |  69 +++++++
 tests/plugin/data/sw_loguru/expected.data.yml    | 181 ++++++++++++++++++
 tests/plugin/data/sw_loguru/services/__init__.py |  16 ++
 tests/plugin/data/sw_loguru/services/consumer.py |  32 ++++
 tests/plugin/data/sw_loguru/services/provider.py |  52 ++++++
 tests/plugin/data/sw_loguru/test_loguru.py       |  36 ++++
 15 files changed, 672 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72deb73..1f2f5ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
   - Add test and support for Python Slim base images (#249)
   - Add support for the tags of Virtual Cache for Redis (#263)
   - Add a new configuration `kafka_namespace` to prefix the kafka topic names (#277)
+  - Add log reporter support for loguru (#276)
 
 - Plugins:
   - Add aioredis, aiormq, amqp, asyncpg, aio-pika, kombu RMQ plugins (#230 Missing test coverage) 
diff --git a/docs/en/setup/Configuration.md b/docs/en/setup/Configuration.md
index 9a5a572..24ee266 100644
--- a/docs/en/setup/Configuration.md
+++ b/docs/en/setup/Configuration.md
@@ -68,8 +68,8 @@ export SW_AGENT_YourConfiguration=YourValue
 | log_reporter_max_buffer_size | SW_AGENT_LOG_REPORTER_MAX_BUFFER_SIZE | <class 'int'> | 10000 | The maximum queue backlog size for sending log data to backend, logs beyond this are silently dropped. |
 | log_reporter_level | SW_AGENT_LOG_REPORTER_LEVEL | <class 'str'> | WARNING | This config specifies the logger levels of concern, any logs with a level below the config will be ignored. |
 | log_reporter_ignore_filter | SW_AGENT_LOG_REPORTER_IGNORE_FILTER | <class 'bool'> | False | This config customizes whether to ignore the application-defined logger filters, if `True`, all logs are reported disregarding any filter rules. |
-| log_reporter_formatted | SW_AGENT_LOG_REPORTER_FORMATTED | <class 'bool'> | True | If `True`, the log reporter will transmit the logs as formatted. Otherwise, puts logRecord.msg and logRecord.args into message content and tags(`argument.n`), respectively. Along with an `exception` tag if an exception was raised. |
-| log_reporter_layout | SW_AGENT_LOG_REPORTER_LAYOUT | <class 'str'> | %(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s | The log reporter formats the logRecord message based on the layout given. |
+| log_reporter_formatted | SW_AGENT_LOG_REPORTER_FORMATTED | <class 'bool'> | True | If `True`, the log reporter will transmit the logs as formatted. Otherwise, puts logRecord.msg and logRecord.args into message content and tags(`argument.n`), respectively. Along with an `exception` tag if an exception was raised. Only applies to logging module. |
+| log_reporter_layout | SW_AGENT_LOG_REPORTER_LAYOUT | <class 'str'> | %(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s | The log reporter formats the logRecord message based on the layout given. Only applies to logging module. |
 | cause_exception_depth | SW_AGENT_CAUSE_EXCEPTION_DEPTH | <class 'int'> | 10 | This configuration is shared by log reporter and tracer. This config limits agent to report up to `limit` stacktrace, please refer to [Python traceback]( https://docs.python.org/3/library/traceback.html#traceback.print_tb) for more explanations. |
 ###  Meter Reporter Configurations
 | Configuration | Environment Variable | Type | Default Value | Description |
diff --git a/docs/en/setup/Plugins.md b/docs/en/setup/Plugins.md
index 5740829..23df62e 100644
--- a/docs/en/setup/Plugins.md
+++ b/docs/en/setup/Plugins.md
@@ -30,6 +30,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
 | [http_server](https://docs.python.org/3/library/http.server.html) | Python >=3.7 - ['*'];  | `sw_http_server` |
 | [werkzeug](https://werkzeug.palletsprojects.com/) | Python >=3.7 - ['1.0.1', '2.0'];  | `sw_http_server` |
 | [kafka-python](https://kafka-python.readthedocs.io) | Python >=3.7 - ['2.0'];  | `sw_kafka` |
+| [loguru](https://pypi.org/project/loguru/) | Python >=3.7 - ['0.6.0'];  | `sw_loguru` |
 | [mysqlclient](https://mysqlclient.readthedocs.io/) | Python >=3.7 - ['2.1.*'];  | `sw_mysqlclient` |
 | [psycopg[binary]](https://www.psycopg.org/) | Python >=3.7 - ['3.0'];  | `sw_psycopg` |
 | [psycopg2-binary](https://www.psycopg.org/) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['2.9'];  | `sw_psycopg2` |
diff --git a/docs/en/setup/advanced/LogReporter.md b/docs/en/setup/advanced/LogReporter.md
index 1603f08..acfe409 100644
--- a/docs/en/setup/advanced/LogReporter.md
+++ b/docs/en/setup/advanced/LogReporter.md
@@ -1,6 +1,6 @@
 # Python Agent Log Reporter
 
-This functionality reports logs collected from the Python logging module (in theory, also logging libraries depending on the core logging module).
+This functionality reports logs collected from the Python logging module (in theory, also logging libraries depending on the core logging module) and loguru module.
 
 To utilize this feature, you will need to add some new configurations to the agent initialization step.
 
diff --git a/poetry.lock b/poetry.lock
index 66484e6..114c102 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1568,6 +1568,25 @@ sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"]
 yaml = ["PyYAML (>=3.10)"]
 zookeeper = ["kazoo (>=1.3.1)"]
 
+[[package]]
+name = "loguru"
+version = "0.6.0"
+description = "Python logging made (stupidly) simple"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
+    {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
+win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"]
+
 [[package]]
 name = "markupsafe"
 version = "2.1.2"
@@ -3086,6 +3105,21 @@ files = [
 [package.extras]
 watchdog = ["watchdog"]
 
+[[package]]
+name = "win32-setctime"
+version = "1.1.0"
+description = "A small Python utility to set file creation time on Windows"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
+    {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
+]
+
+[package.extras]
+dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
+
 [[package]]
 name = "wrapt"
 version = "1.14.1"
@@ -3365,4 +3399,4 @@ kafka = ["kafka-python"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.7, <3.11"
-content-hash = "ed27bdc3c9662785ff444a7f117b2d793037caf96e51e135ea49cd6fedf10cef"
+content-hash = "2b84493cca5e5189414dc6ded1c4a2e881a3bb20a01a8da4e28b5b1cac2d47c1"
diff --git a/pyproject.toml b/pyproject.toml
index 66b76a8..1f21338 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -126,6 +126,7 @@ aiormq = "^6.4.2"
 asyncpg = "^0.27.0"
 happybase = "1.2.0"
 websockets = "^10.4"
+loguru = "^0.6.0"
 
 [tool.poetry.group.lint.dependencies]
 flake8 = "^5.0.4"
diff --git a/skywalking/config.py b/skywalking/config.py
index 1bc3c0a..2b2d618 100644
--- a/skywalking/config.py
+++ b/skywalking/config.py
@@ -146,8 +146,10 @@ log_reporter_level: str = os.getenv('SW_AGENT_LOG_REPORTER_LEVEL', 'WARNING')
 log_reporter_ignore_filter: bool = os.getenv('SW_AGENT_LOG_REPORTER_IGNORE_FILTER', '').lower() == 'true'
 # If `True`, the log reporter will transmit the logs as formatted. Otherwise, puts logRecord.msg and logRecord.args
 # into message content and tags(`argument.n`), respectively. Along with an `exception` tag if an exception was raised.
+# Only applies to logging module.
 log_reporter_formatted: bool = os.getenv('SW_AGENT_LOG_REPORTER_FORMATTED', '').lower() != 'false'
 # The log reporter formats the logRecord message based on the layout given.
+# Only applies to logging module.
 log_reporter_layout: str = os.getenv('SW_AGENT_LOG_REPORTER_LAYOUT',
                                      '%(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s')
 # This configuration is shared by log reporter and tracer.
diff --git a/skywalking/plugins/sw_loguru.py b/skywalking/plugins/sw_loguru.py
new file mode 100644
index 0000000..f3a94d2
--- /dev/null
+++ b/skywalking/plugins/sw_loguru.py
@@ -0,0 +1,227 @@
+#
+# 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
+import sys
+import traceback
+from multiprocessing import current_process
+from os.path import basename, splitext
+from threading import current_thread
+
+from skywalking import config, agent
+from skywalking.protocol.common.Common_pb2 import KeyStringValuePair
+from skywalking.protocol.logging.Logging_pb2 import LogData, LogDataBody, TraceContext, LogTags, TextLog
+from skywalking.trace.context import get_context
+from skywalking.utils.exception import IllegalStateError
+from skywalking.utils.filter import sw_filter
+
+link_vector = ['https://pypi.org/project/loguru/']
+support_matrix = {
+    'loguru': {
+        '>=3.7': ['0.6.0']
+    }
+}
+note = """"""
+
+
+def install():
+    from loguru import logger
+    from loguru._recattrs import RecordException, RecordFile, RecordLevel, RecordProcess, RecordThread
+    from loguru._datetime import aware_now
+    from loguru._get_frame import get_frame
+    from loguru._logger import start_time, context as logger_context, Logger
+    from types import MethodType
+
+    _log = logger._log
+    log_reporter_level = logging.getLevelName(config.log_reporter_level)  # type: int
+
+    def gen_record(self, level_id, static_level_no, from_decorator, options, message, args, kwargs):
+        """ Generate log record as loguru.logger._log """
+        core = self._core
+
+        if not core.handlers:
+            return
+
+        (exception, depth, record, lazy, colors, raw, capture, patcher, extra) = options
+
+        frame = get_frame(depth + 2)
+
+        try:
+            name = frame.f_globals['__name__']
+        except KeyError:
+            name = None
+
+        try:
+            if not core.enabled[name]:
+                return
+        except KeyError:
+            enabled = core.enabled
+            if name is None:
+                status = core.activation_none
+                enabled[name] = status
+                if not status:
+                    return
+            else:
+                dotted_name = name + '.'
+                for dotted_module_name, status in core.activation_list:
+                    if dotted_name[: len(dotted_module_name)] == dotted_module_name:
+                        if status:
+                            break
+                        enabled[name] = False
+                        return
+                enabled[name] = True
+
+        current_datetime = aware_now()
+
+        if level_id is None:
+            level_icon = ' '
+            level_no = static_level_no
+            level_name = f'Level {level_no}'  # not really level name, just as loguru
+        else:
+            level_name, level_no, _, level_icon = core.levels[level_id]
+
+        if level_no < core.min_level:
+            return
+
+        code = frame.f_code
+        file_path = code.co_filename
+        file_name = basename(file_path)
+        thread = current_thread()
+        process = current_process()
+        elapsed = current_datetime - start_time
+
+        if exception:
+            if isinstance(exception, BaseException):
+                type_, value, traceback = (type(exception), exception, exception.__traceback__)
+            elif isinstance(exception, tuple):
+                type_, value, traceback = exception
+            else:
+                type_, value, traceback = sys.exc_info()
+            exception = RecordException(type_, value, traceback)
+        else:
+            exception = None
+
+        log_record = {
+            'elapsed': elapsed,
+            'exception': exception,
+            'extra': {**core.extra, **logger_context.get(), **extra},
+            'file': RecordFile(file_name, file_path),
+            'function': code.co_name,
+            'level': RecordLevel(level_name, level_no, level_icon),
+            'line': frame.f_lineno,
+            'message': str(message),
+            'module': splitext(file_name)[0],
+            'name': name,
+            'process': RecordProcess(process.ident, process.name),
+            'thread': RecordThread(thread.ident, thread.name),
+            'time': current_datetime,
+        }
+
+        if capture and kwargs:
+            log_record['extra'].update(kwargs)
+
+        if record:
+            kwargs.update(record=log_record)
+
+        if args or kwargs:
+            log_record['message'] = message.format(*args, **kwargs)
+
+        if core.patcher:
+            core.patcher(log_record)
+
+        if patcher:
+            patcher(log_record)
+
+        return log_record
+
+    def _sw_log(self, level_id, static_level_no, from_decorator, options, message, args, kwargs):
+        _log(level_id, static_level_no, from_decorator, options, message, args, kwargs)
+        record = gen_record(self, level_id, static_level_no, from_decorator, options, message, args, kwargs)
+        if record is None:
+            return
+
+        core = self._core
+
+        if record['level'].no < log_reporter_level:
+            return
+
+        if not config.log_reporter_ignore_filter and record['level'].no < core.min_level:  # ignore filtered logs
+            return
+
+        # loguru has only one logger. Use tags referring Python-Agent doc
+        core_tags = [
+            KeyStringValuePair(key='level', value=record['level'].name),
+            KeyStringValuePair(key='logger', value='loguru'),
+            KeyStringValuePair(key='thread', value=record['thread'].name),
+        ]
+        tags = LogTags()
+        tags.data.extend(core_tags)
+
+        exception = record['exception']
+        if exception:
+            stack_trace = ''.join(traceback.format_exception(exception.type, exception.value, exception.traceback,
+                                                             limit=config.cause_exception_depth))
+            tags.data.append(KeyStringValuePair(key='exception',
+                                                value=sw_filter(stack_trace)
+                                                ))  # \n doesn't work in tags for UI
+
+        context = get_context()
+
+        active_span_id = -1
+        primary_endpoint_name = ''
+
+        try:
+            # Try to extract active span, if user code/plugin code throws uncaught
+            # exceptions before any span is even created, just ignore these fields and
+            # avoid appending 'no active span' traceback that could be confusing.
+            # Or simply the log is generated outside any span context.
+            active_span_id = context.active_span.sid
+            primary_endpoint_name = context.primary_endpoint.get_name()
+        except IllegalStateError:
+            pass
+
+        log_data = LogData(
+            timestamp=round(record['time'].timestamp() * 1000),
+            service=config.service_name,
+            serviceInstance=config.service_instance,
+            body=LogDataBody(
+                type='text',
+                text=TextLog(
+                    text=sw_filter(message)
+                )
+            ),
+            tags=tags,
+        )
+
+        if active_span_id != -1:
+            trace_context = TraceContext(
+                traceId=str(context.segment.related_traces[0]),
+                traceSegmentId=str(context.segment.segment_id),
+                spanId=active_span_id
+            )
+            log_data.traceContext.CopyFrom(trace_context)
+
+        if primary_endpoint_name:
+            log_data.endpoint = primary_endpoint_name
+
+        agent.archive_log(log_data)
+
+    # Bind _sw_log function to default logger instance.
+    bound_sw_log = MethodType(_sw_log, logger)
+    logger._log = bound_sw_log
+    # Bind _sw_log function to Logger class for new instance.
+    Logger._log = _sw_log
diff --git a/tests/plugin/data/sw_loguru/__init__.py b/tests/plugin/data/sw_loguru/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/__init__.py
@@ -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.
+#
diff --git a/tests/plugin/data/sw_loguru/docker-compose.yml b/tests/plugin/data/sw_loguru/docker-compose.yml
new file mode 100644
index 0000000..666994f
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/docker-compose.yml
@@ -0,0 +1,69 @@
+#
+# 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.
+#
+
+version: '2.1'
+
+services:
+  collector:
+    extends:
+      service: collector
+      file: ../../docker-compose.base.yml
+
+  provider:
+    extends:
+      service: agent
+      file: ../../docker-compose.base.yml
+    ports:
+      - 9091:9091
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip3 install uvicorn && pip3 install fastapi && pip3 install -r /app/requirements.txt && sw-python run python3 /app/services/provider.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9091"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+    environment:
+      SW_AGENT_SERVICE_NAME: provider
+      SW_AGENT_LOGGING_LEVEL: DEBUG
+      SW_AGENT_LOG_REPORTER_LEVEL: INFO
+
+  consumer:
+    extends:
+      service: agent
+      file: ../../docker-compose.base.yml
+    ports:
+      - 9090:9090
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip3 install uvicorn && pip3 install fastapi && pip3 install -r /app/requirements.txt && sw-python run python3 /app/services/consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+    environment:
+      SW_AGENT_SERVICE_NAME: consumer
+      SW_AGENT_LOGGING_LEVEL: DEBUG
+      SW_AGENT_FASTAPI_COLLECT_HTTP_PARAMS: 'True'
+
+
+networks:
+  beyond:
diff --git a/tests/plugin/data/sw_loguru/expected.data.yml b/tests/plugin/data/sw_loguru/expected.data.yml
new file mode 100644
index 0000000..afc8d0a
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/expected.data.yml
@@ -0,0 +1,181 @@
+#
+# 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.
+#
+
+logItems:
+  - serviceName: provider
+    logSize: 4
+    logs:
+      - timestamp: gt 0
+        endpoint: '/users'
+        body:
+          type: 'text'
+          content:
+            text: 'Loguru provider error reported.'
+        traceContext:
+          traceId: not null
+          traceSegmentId: not null
+          spanId: 0
+        tags:
+          data:
+            - key: level
+              value: ERROR
+            - key: logger
+              value: loguru
+            - key: thread
+              value: not null
+            - key: exception
+              value: not null
+        layer: ''
+
+      - timestamp: gt 0
+        endpoint: '/users'
+        body:
+          type: 'text'
+          content:
+            text: not null
+        traceContext:
+          traceId: not null
+          traceSegmentId: not null
+          spanId: 0
+        tags:
+          data:
+            - key: level
+              value: ERROR
+            - key: logger
+              value: not null
+            - key: thread
+              value: not null
+        layer: ''
+
+      - timestamp: gt 0
+        endpoint: '/users'
+        body:
+          type: 'text'
+          content:
+            text: 'Loguru provider warning reported.'
+        traceContext:
+          traceId: not null
+          traceSegmentId: not null
+          spanId: 0
+        tags:
+          data:
+            - key: level
+              value: WARNING
+            - key: logger
+              value: loguru
+            - key: thread
+              value: not null
+        layer: ''
+
+      - timestamp: gt 0
+        endpoint: '/users'
+        body:
+          type: 'text'
+          content:
+            text: not null
+        traceContext:
+          traceId: not null
+          traceSegmentId: not null
+          spanId: 0
+        tags:
+          data:
+            - key: level
+              value: CRITICAL
+            - key: logger
+              value: not null
+            - key: thread
+              value: not null
+        layer: ''
+
+meterItems: [ ]
+
+segmentItems:
+  - serviceName: provider
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: '/users'
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7014
+            isError: false
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: http.url
+                value: not null
+              - key: http.status_code
+                value: '200'
+            refs:
+              - parentEndpoint: '/users'
+                networkAddress: not null
+                refType: CrossProcess
+                parentSpanId: 1
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: consumer
+                traceId: not null
+
+  - serviceName: consumer
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - componentId: 7002
+            endTime: gt 0
+            isError: false
+            operationName: '/users'
+            parentSpanId: 0
+            peer: not null
+            skipAnalysis: false
+            spanId: 1
+            spanLayer: Http
+            spanType: Exit
+            startTime: gt 0
+            tags:
+              - key: http.method
+                value: GET
+              - key: http.url
+                value: not null
+              - key: http.status_code
+                value: '200'
+          - componentId: 7014
+            endTime: gt 0
+            isError: false
+            operationName: '/users'
+            parentSpanId: -1
+            peer: not null
+            skipAnalysis: false
+            spanId: 0
+            spanLayer: Http
+            spanType: Entry
+            startTime: gt 0
+            tags:
+              - key: http.method
+                value: GET
+              - key: http.url
+                value: not null
+              - key: http.status_code
+                value: '200'
+
diff --git a/tests/plugin/data/sw_loguru/services/__init__.py b/tests/plugin/data/sw_loguru/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/services/__init__.py
@@ -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.
+#
diff --git a/tests/plugin/data/sw_loguru/services/consumer.py b/tests/plugin/data/sw_loguru/services/consumer.py
new file mode 100644
index 0000000..45e145e
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/services/consumer.py
@@ -0,0 +1,32 @@
+#
+# 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 requests
+
+if __name__ == '__main__':
+    from fastapi import FastAPI
+    import uvicorn
+
+    app = FastAPI()
+
+
+    @app.get('/users')
+    async def application():
+        res = requests.get('http://provider:9091/users', timeout=5)
+        return {'http': res.json()}
+
+
+    uvicorn.run(app, host='0.0.0.0', port=9090)
diff --git a/tests/plugin/data/sw_loguru/services/provider.py b/tests/plugin/data/sw_loguru/services/provider.py
new file mode 100644
index 0000000..546c77d
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/services/provider.py
@@ -0,0 +1,52 @@
+#
+# 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
+import time
+
+from loguru import logger
+
+if __name__ == '__main__':
+    from fastapi import FastAPI
+    import uvicorn
+
+    app = FastAPI()
+
+    logging_logger = logging.getLogger()
+
+
+    @app.get('/users')
+    async def application():
+        time.sleep(0.5)
+
+        try:
+            raise Exception('Loguru Exception Test.')
+        except Exception:  # noqa
+            logger.opt(exception=True).error('Loguru provider error reported.')
+            logging_logger.error('Logging provider error reported.', exc_info=True)
+
+        # this will be filtered by SW_AGENT_LOG_REPORTER_LEVEL
+        logger.debug('Loguru provider debug reported.')
+
+        logger.warning('Loguru provider warning reported.')
+
+        logging_logger.critical('Logging provider critical reported.')
+
+        return {'song': 'Despacito', 'artist': 'Luis Fonsi'}
+
+
+    # error level filter the uvicorn's log by logging
+    uvicorn.run(app, host='0.0.0.0', port=9091, log_level='error')
diff --git a/tests/plugin/data/sw_loguru/test_loguru.py b/tests/plugin/data/sw_loguru/test_loguru.py
new file mode 100644
index 0000000..ba26e41
--- /dev/null
+++ b/tests/plugin/data/sw_loguru/test_loguru.py
@@ -0,0 +1,36 @@
+#
+# 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 typing import Callable
+
+import pytest
+import requests
+
+from skywalking.plugins.sw_loguru import support_matrix
+from tests.orchestrator import get_test_vector
+from tests.plugin.base import TestPluginBase
+
+
+@pytest.fixture
+def prepare():
+    # type: () -> Callable
+    return lambda *_: requests.get('http://0.0.0.0:9090/users', timeout=5)
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', get_test_vector(lib_name='loguru', support_matrix=support_matrix))
+    def test_plugin(self, docker_compose, version):
+        self.validate()