Skip to content

Commit

Permalink
feat: Unify level and event_level with sentry-sdk
Browse files Browse the repository at this point in the history
- BREAKING CHANGE: SentryProcessor level argument renamed to event_level
- SentryProcessor breadcrumb_level argument renamed to level
  • Loading branch information
paveldedik committed Aug 22, 2022
1 parent d206f5e commit 356a2d0
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 36 deletions.
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ structlog.configure(
processors=[
structlog.stdlib.add_logger_name, # optional, but before SentryProcessor()
structlog.stdlib.add_log_level, # required before SentryProcessor()
SentryProcessor(level=logging.ERROR),
SentryProcessor(event_level=logging.ERROR),
],
logger_factory=...,
wrapper_class=...,
Expand All @@ -45,17 +45,28 @@ Do not forget to add the `structlog.stdlib.add_log_level` and optionally the
`structlog.stdlib.add_logger_name` processors before `SentryProcessor`. The
`SentryProcessor` class takes the following arguments:

- `level` - events of this or higher levels will be reported to Sentry,
default is `WARNING`
- `active` - default is `True`, setting to `False` disables the processor

Now exceptions are automatically captured by Sentry with `log.error()`:
- `level` Events of this or higher levels will be reported as Sentry
breadcrumbs. Dfault is :obj:`logging.INFO`.
- `event_level` Events of this or higher levels will be reported to Sentry
as events. Default is :obj:`logging.WARNING`.
- `active` A flag to make this processor enabled/disabled.
- `as_context` Send `event_dict` as extra info to Sentry.
Default is :obj:`True`.
- `tag_keys` A list of keys. If any if these keys appear in `event_dict`,
the key and its corresponding value in `event_dict` will be used as Sentry
event tags. use `"__all__"` to report all key/value pairs of event as tags.
- `ignore_loggers` A list of logger names to ignore any events from.
- `verbose` Report the action taken by the logger in the `event_dict`.
Default is :obj:`False`.
- `hub` Optionally specify :obj:`sentry_sdk.Hub`.

Now events are automatically captured by Sentry with `log.error()`:

```python
try:
1/0
except ZeroDivisionError:
log.error()
log.error("zero divsiion")

try:
resp = requests.get(f"https://api.example.com/users/{user_id}/")
Expand All @@ -73,19 +84,20 @@ processor, make that the `SentryProcessor` comes *before* `format_exc_info`!
Otherwise, the `SentryProcessor` won't have an `exc_info` to work with, because
it's removed from the event by `format_exc_info`.

Logging calls with no `sys.exc_info()` are also automatically captured by Sentry:
Logging calls with no `sys.exc_info()` are also automatically captured by Sentry
either as breadcrumbs (if configured by the `level` argument) or as events:

```python
log.info("info message", scope="accounts")
log.warning("warning message", scope="invoices")
log.error("error message", scope="products")
```

If you do not want to forward logs into Sentry, just pass the `sentry_skip=True`
optional argument to logger methods, like this:
If you do not want to forward a specific logs into Sentry, you can pass the
`sentry_skip=True` optional argument to logger methods, like this:

```python
log.error(sentry_skip=True)
log.error("error message", sentry_skip=True)
```

### Sentry Tags
Expand Down Expand Up @@ -150,23 +162,23 @@ structlog.configure(
### Logging as JSON

If you want to configure `structlog` to format the output as **JSON** (maybe for
[elk-stack](https://www.elastic.co/elk-stack)) you have to enable the
`LoggingIntegration(event_level=None, level=None)` integration to prevent
duplication of an event reported to sentry:
[elk-stack](https://www.elastic.co/elk-stack)) you have to disable standard logging
integration in Sentry SDK by passing the `LoggingIntegration(event_level=None, level=None)`
instance to `sentry_sdk.init` method. This prevents duplication of an event reported to sentry:

```python
from sentry_sdk.integrations.logging import LoggingIntegration


INTEGRATIONS = [
# ... your other integrations
# ... other integrations
LoggingIntegration(event_level=None, level=None),
]

sentry_sdk.init(integrations=INTEGRATIONS)
```

This integration tells sentry_sdk to *ignore* standard logging and captures the events manually.
This integration tells `sentry_sdk` to *ignore* standard logging and captures the events manually.

## Testing

Expand Down
30 changes: 17 additions & 13 deletions structlog_sentry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class SentryProcessor:

def __init__(
self,
level: int = logging.WARNING,
breadcrumb_level: int = logging.INFO,
level: int = logging.INFO,
event_level: int = logging.WARNING,
active: bool = True,
as_context: bool = True,
tag_keys: list[str] | str | None = None,
Expand All @@ -44,19 +44,23 @@ def __init__(
hub: Hub | None = None,
) -> None:
"""
:param level: events of this or higher levels will be reported to Sentry.
:param breadcrumb_level: events of this or higher levels will be reported as
Sentry breadcrumbs.
:param active: a flag to make this processor enabled/disabled.
:param as_context: send `event_dict` as extra info to Sentry.
:param tag_keys: a list of keys. If any if these keys appear in `event_dict`,
:param level: Events of this or higher levels will be reported as
Sentry breadcrumbs. Dfault is :obj:`logging.INFO`.
:param event_level: Events of this or higher levels will be reported to Sentry
as events. Default is :obj:`logging.WARNING`.
:param active: A flag to make this processor enabled/disabled.
:param as_context: Send `event_dict` as extra info to Sentry.
Default is :obj:`True`.
:param tag_keys: A list of keys. If any if these keys appear in `event_dict`,
the key and its corresponding value in `event_dict` will be used as Sentry
event tags. use `"__all__"` to report all key/value pairs of event as tags.
:param ignore_loggers: a list of logger names to ignore any events from.
:param verbose: report the action taken by the logger in the `event_dict`.
:param ignore_loggers: A list of logger names to ignore any events from.
:param verbose: Report the action taken by the logger in the `event_dict`.
Default is :obj:`False`.
:param hub: Optionally specify :obj:`sentry_sdk.Hub`.
"""
self.event_level = event_level
self.level = level
self.breadcrumb_level = breadcrumb_level
self.active = active
self.tag_keys = tag_keys
self.verbose = verbose
Expand Down Expand Up @@ -169,10 +173,10 @@ def __call__(
if self.active and not sentry_skip and self._can_record(logger, event_dict):
level = getattr(logging, event_dict["level"].upper())

if level >= self.level:
if level >= self.event_level:
self._handle_event(event_dict)

if level >= self.breadcrumb_level:
if level >= self.level:
self._handle_breadcrumb(event_dict)

if self.verbose:
Expand Down
14 changes: 7 additions & 7 deletions test/test_sentry_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ def test_sentry_sent():
def test_sentry_log(sentry_events, level):
event_data = {"level": level, "event": level + " message"}

processor = SentryProcessor(level=getattr(logging, level.upper()))
processor = SentryProcessor(event_level=getattr(logging, level.upper()))
processor(None, None, event_data)

assert_event_dict(event_data, sentry_events)


@pytest.mark.parametrize("level", ["debug", "info", "warning"])
def test_sentry_log_only_errors(sentry_events, level):
processor_only_errors = SentryProcessor(level=logging.ERROR, verbose=True)
processor_only_errors = SentryProcessor(event_level=logging.ERROR, verbose=True)
event_dict = processor_only_errors(
None, None, {"level": level, "event": level + " message"}
)
Expand All @@ -82,7 +82,7 @@ def test_sentry_log_failure(sentry_events, level):
'exception' information after processing
"""
event_data = {"level": level, "event": level + " message"}
processor = SentryProcessor(level=getattr(logging, level.upper()))
processor = SentryProcessor(event_level=getattr(logging, level.upper()))
try:
1 / 0
except ZeroDivisionError:
Expand All @@ -99,7 +99,7 @@ def test_sentry_log_failure_exc_info_true(sentry_events, level):
are used.
"""
event_data = {"level": level, "event": level + " message", "exc_info": True}
processor = SentryProcessor(level=getattr(logging, level.upper()))
processor = SentryProcessor(event_level=getattr(logging, level.upper()))
try:
1 / 0
except ZeroDivisionError:
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_sentry_log_leave_exc_info_untouched(sentry_events):
def test_sentry_log_all_as_tags(sentry_events, level):
event_data = {"level": level, "event": level + " message"}
processor = SentryProcessor(
level=getattr(logging, level.upper()), tag_keys="__all__"
event_level=getattr(logging, level.upper()), tag_keys="__all__"
)
processor(None, None, event_data)

Expand All @@ -170,7 +170,7 @@ def test_sentry_log_specific_keys_as_tags(sentry_events, level):
}
tag_keys = ["info1", "required", "some non existing key"]
processor = SentryProcessor(
level=getattr(logging, level.upper()), tag_keys=tag_keys
event_level=getattr(logging, level.upper()), tag_keys=tag_keys
)
processor(None, None, event_data)

Expand Down Expand Up @@ -219,7 +219,7 @@ def test_sentry_ignore_logger(sentry_events, level):
blacklisted_logger = MockLogger("test.blacklisted")
whitelisted_logger = MockLogger("test.whitelisted")
processor = SentryProcessor(
level=getattr(logging, level.upper()),
event_level=getattr(logging, level.upper()),
ignore_loggers=["test.blacklisted"],
verbose=True,
)
Expand Down

0 comments on commit 356a2d0

Please sign in to comment.