Skip to content

Add loguru integration #1994

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 9, 2023
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
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