Skip to content

Commit

Permalink
Add loguru integration (#1994)
Browse files Browse the repository at this point in the history
* Add `loguru` integration

Actually, this is the solution in comments under #653 adapted to
codebase and tested as well.
#653 (comment)

I also changed `logging` integration to use methods instead of
functions in handlers, as in that way we can easily overwrite parts
that are different in `loguru` integration. It shouldn't be a problem,
as those methods are private and used only in that file.

---------

Co-authored-by: Anton Pirker <anton.pirker@sentry.io>
  • Loading branch information
PerchunPak and antonpirker authored May 9, 2023
1 parent 4b6a381 commit 8a2b74f
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 71 deletions.
78 changes: 78 additions & 0 deletions .github/workflows/test-integration-loguru.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Test loguru

on:
push:
branches:
- master
- release/**

pull_request:

# Cancel in progress workflows on pull_requests.
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

env:
BUILD_CACHE_KEY: ${{ github.sha }}
CACHED_BUILD_PATHS: |
${{ github.workspace }}/dist-serverless
jobs:
test:
name: loguru, python ${{ matrix.python-version }}, ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45

strategy:
fail-fast: false
matrix:
python-version: ["3.5","3.6","3.7","3.8","3.9","3.10","3.11"]
# python3.6 reached EOL and is no longer being supported on
# new versions of hosted runners on Github Actions
# ubuntu-20.04 is the last version that supported python3.6
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877
os: [ubuntu-20.04]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Setup Test Env
run: |
pip install coverage "tox>=3,<4"
- name: Test loguru
timeout-minutes: 45
shell: bash
run: |
set -x # print commands that are executed
coverage erase
# Run tests
./scripts/runtox.sh "py${{ matrix.python-version }}-loguru" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
coverage combine .coverage*
coverage xml -i
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml

check_required_tests:
name: All loguru tests passed or skipped
needs: test
# Always run this, even if a dependent job failed
if: always()
runs-on: ubuntu-20.04
steps:
- name: Check for failures
if: contains(needs.test.result, 'failure')
run: |
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1
1 change: 1 addition & 0 deletions linter-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ types-certifi
types-redis
types-setuptools
pymongo # There is no separate types module.
loguru # There is no separate types module.
flake8-bugbear==22.12.6
pep8-naming==0.13.2
pre-commit # local linting
137 changes: 67 additions & 70 deletions sentry_sdk/integrations/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,75 +107,61 @@ def sentry_patched_callhandlers(self, record):
logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore


def _can_record(record):
# type: (LogRecord) -> bool
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name, logger):
return False
return True


def _breadcrumb_from_record(record):
# type: (LogRecord) -> Dict[str, Any]
return {
"type": "log",
"level": _logging_to_event_level(record),
"category": record.name,
"message": record.message,
"timestamp": datetime.datetime.utcfromtimestamp(record.created),
"data": _extra_from_record(record),
}


def _logging_to_event_level(record):
# type: (LogRecord) -> str
return LOGGING_TO_EVENT_LEVEL.get(
record.levelno, record.levelname.lower() if record.levelname else ""
class _BaseHandler(logging.Handler, object):
COMMON_RECORD_ATTRS = frozenset(
(
"args",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"linenno",
"lineno",
"message",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack",
"tags",
"thread",
"threadName",
"stack_info",
)
)

def _can_record(self, record):
# type: (LogRecord) -> bool
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name, logger):
return False
return True

def _logging_to_event_level(self, record):
# type: (LogRecord) -> str
return LOGGING_TO_EVENT_LEVEL.get(
record.levelno, record.levelname.lower() if record.levelname else ""
)

COMMON_RECORD_ATTRS = frozenset(
(
"args",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"linenno",
"lineno",
"message",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack",
"tags",
"thread",
"threadName",
"stack_info",
)
)


def _extra_from_record(record):
# type: (LogRecord) -> Dict[str, None]
return {
k: v
for k, v in iteritems(vars(record))
if k not in COMMON_RECORD_ATTRS
and (not isinstance(k, str) or not k.startswith("_"))
}
def _extra_from_record(self, record):
# type: (LogRecord) -> Dict[str, None]
return {
k: v
for k, v in iteritems(vars(record))
if k not in self.COMMON_RECORD_ATTRS
and (not isinstance(k, str) or not k.startswith("_"))
}


class EventHandler(logging.Handler, object):
class EventHandler(_BaseHandler):
"""
A logging handler that emits Sentry events for each log record
Expand All @@ -190,7 +176,7 @@ def emit(self, record):

def _emit(self, record):
# type: (LogRecord) -> None
if not _can_record(record):
if not self._can_record(record):
return

hub = Hub.current
Expand Down Expand Up @@ -232,7 +218,7 @@ def _emit(self, record):

hint["log_record"] = record

event["level"] = _logging_to_event_level(record)
event["level"] = self._logging_to_event_level(record)
event["logger"] = record.name

# Log records from `warnings` module as separate issues
Expand All @@ -255,7 +241,7 @@ def _emit(self, record):
"params": record.args,
}

event["extra"] = _extra_from_record(record)
event["extra"] = self._extra_from_record(record)

hub.capture_event(event, hint=hint)

Expand All @@ -264,7 +250,7 @@ def _emit(self, record):
SentryHandler = EventHandler


class BreadcrumbHandler(logging.Handler, object):
class BreadcrumbHandler(_BaseHandler):
"""
A logging handler that records breadcrumbs for each log record.
Expand All @@ -279,9 +265,20 @@ def emit(self, record):

def _emit(self, record):
# type: (LogRecord) -> None
if not _can_record(record):
if not self._can_record(record):
return

Hub.current.add_breadcrumb(
_breadcrumb_from_record(record), hint={"log_record": record}
self._breadcrumb_from_record(record), hint={"log_record": record}
)

def _breadcrumb_from_record(self, record):
# type: (LogRecord) -> Dict[str, Any]
return {
"type": "log",
"level": self._logging_to_event_level(record),
"category": record.name,
"message": record.message,
"timestamp": datetime.datetime.utcfromtimestamp(record.created),
"data": self._extra_from_record(record),
}
89 changes: 89 additions & 0 deletions sentry_sdk/integrations/loguru.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import absolute_import

import enum

from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.logging import (
BreadcrumbHandler,
EventHandler,
_BaseHandler,
)

if TYPE_CHECKING:
from logging import LogRecord
from typing import Optional, Tuple

try:
from loguru import logger
except ImportError:
raise DidNotEnable("LOGURU is not installed")


class LoggingLevels(enum.IntEnum):
TRACE = 5
DEBUG = 10
INFO = 20
SUCCESS = 25
WARNING = 30
ERROR = 40
CRITICAL = 50


DEFAULT_LEVEL = LoggingLevels.INFO.value
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
# We need to save the handlers to be able to remove them later
# in tests (they call `LoguruIntegration.__init__` multiple times,
# and we can't use `setup_once` because it's called before
# than we get configuration).
_ADDED_HANDLERS = (None, None) # type: Tuple[Optional[int], Optional[int]]


class LoguruIntegration(Integration):
identifier = "loguru"

def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
# type: (Optional[int], Optional[int]) -> None
global _ADDED_HANDLERS
breadcrumb_handler, event_handler = _ADDED_HANDLERS

if breadcrumb_handler is not None:
logger.remove(breadcrumb_handler)
breadcrumb_handler = None
if event_handler is not None:
logger.remove(event_handler)
event_handler = None

if level is not None:
breadcrumb_handler = logger.add(
LoguruBreadcrumbHandler(level=level), level=level
)

if event_level is not None:
event_handler = logger.add(
LoguruEventHandler(level=event_level), level=event_level
)

_ADDED_HANDLERS = (breadcrumb_handler, event_handler)

@staticmethod
def setup_once():
# type: () -> None
pass # we do everything in __init__


class _LoguruBaseHandler(_BaseHandler):
def _logging_to_event_level(self, record):
# type: (LogRecord) -> str
try:
return LoggingLevels(record.levelno).name.lower()
except ValueError:
return record.levelname.lower() if record.levelname else ""


class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""


class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ def get_file_text(file_name):
"fastapi": ["fastapi>=0.79.0"],
"pymongo": ["pymongo>=3.1"],
"opentelemetry": ["opentelemetry-distro>=0.35b0"],
"grpcio": ["grpcio>=1.21.1"]
"grpcio": ["grpcio>=1.21.1"],
"loguru": ["loguru>=0.5"],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
3 changes: 3 additions & 0 deletions tests/integrations/loguru/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest

pytest.importorskip("loguru")
Loading

0 comments on commit 8a2b74f

Please sign in to comment.