Skip to content

Commit 78e98b6

Browse files
authored
feat(transport): Add EnvelopePrinterTransport for debug logging (#6181)
Adds `EnvelopePrinterTransport`, a decorator transport that wraps the real transport and pretty-prints each envelope's headers and item payloads to the SDK debug logger before forwarding the envelope. Enabled via `SENTRY_PRINT_ENVELOPES=1` (also accepts `true`/`yes`). When unset or falsy, no wrapping occurs and there's no runtime cost. Useful for local debugging without having to run a local Sentry instance or intercept network traffic. Fixes PY-2398 Fixes #6183
1 parent 4a56e34 commit 78e98b6

2 files changed

Lines changed: 123 additions & 36 deletions

File tree

sentry_sdk/transport.py

Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
from abc import ABC, abstractmethod
21
import asyncio
2+
import gzip
33
import io
4+
import json
5+
import logging
46
import os
5-
import gzip
67
import socket
78
import ssl
89
import time
910
import warnings
10-
from datetime import datetime, timedelta, timezone
11+
from abc import ABC, abstractmethod
1112
from collections import defaultdict
13+
from datetime import datetime, timedelta, timezone
1214
from urllib.request import getproxies
1315

1416
try:
@@ -35,36 +37,37 @@
3537
except ImportError:
3638
ASYNC_TRANSPORT_AVAILABLE = False
3739

38-
import urllib3
40+
from typing import TYPE_CHECKING, Dict, List, cast
41+
3942
import certifi
43+
import urllib3
4044

4145
import sentry_sdk
4246
from sentry_sdk.consts import EndpointType
47+
from sentry_sdk.envelope import Envelope, Item, PayloadRef
4348
from sentry_sdk.utils import (
4449
Dsn,
45-
logger,
4650
capture_internal_exceptions,
51+
logger,
4752
mark_sentry_task_internal,
4853
)
49-
from sentry_sdk.worker import BackgroundWorker, Worker, AsyncWorker
50-
from sentry_sdk.envelope import Envelope, Item, PayloadRef
51-
52-
from typing import TYPE_CHECKING, cast, List, Dict
54+
from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker
5355

5456
if TYPE_CHECKING:
55-
from typing import Any
56-
from typing import Callable
57-
from typing import DefaultDict
58-
from typing import Iterable
59-
from typing import Mapping
60-
from typing import Optional
61-
from typing import Self
62-
from typing import Tuple
63-
from typing import Type
64-
from typing import Union
65-
66-
from urllib3.poolmanager import PoolManager
67-
from urllib3.poolmanager import ProxyManager
57+
from typing import (
58+
Any,
59+
Callable,
60+
DefaultDict,
61+
Iterable,
62+
Mapping,
63+
Optional,
64+
Self,
65+
Tuple,
66+
Type,
67+
Union,
68+
)
69+
70+
from urllib3.poolmanager import PoolManager, ProxyManager
6871

6972
from sentry_sdk._types import Event, EventDataCategory
7073

@@ -1081,6 +1084,83 @@ def _make_pool(
10811084
return httpcore.ConnectionPool(**opts)
10821085

10831086

1087+
class _EnvelopePrinterTransport(Transport):
1088+
"""Wraps another transport, printing envelope contents to the SDK debug logger before sending."""
1089+
1090+
def __init__(self, transport: "Transport") -> None:
1091+
Transport.__init__(self, options=transport.options)
1092+
self._inner = transport
1093+
self.parsed_dsn = transport.parsed_dsn
1094+
1095+
self.envelope_logger = logging.getLogger("sentry_sdk.envelopes")
1096+
self.envelope_logger.setLevel(logging.INFO)
1097+
self.envelope_logger.propagate = False
1098+
if not self.envelope_logger.handlers:
1099+
handler = logging.StreamHandler()
1100+
handler.setLevel(logging.INFO)
1101+
handler.setFormatter(logging.Formatter("%(message)s"))
1102+
self.envelope_logger.addHandler(handler)
1103+
1104+
@property # type: ignore[misc]
1105+
def __class__(self) -> type:
1106+
return self._inner.__class__
1107+
1108+
def capture_envelope(self, envelope: "Envelope") -> None:
1109+
try:
1110+
self.envelope_logger.info("--- Sentry Envelope ---")
1111+
self.envelope_logger.info(
1112+
"Headers: %s", json.dumps(envelope.headers, indent=2, default=str)
1113+
)
1114+
for item in envelope.items:
1115+
self.envelope_logger.info(" Item type: %s", item.type)
1116+
self.envelope_logger.info(
1117+
" Item headers: %s",
1118+
json.dumps(item.headers, indent=2, default=str),
1119+
)
1120+
try:
1121+
payload = json.loads(item.get_bytes())
1122+
self.envelope_logger.info(
1123+
" Payload:\n%s",
1124+
json.dumps(payload, indent=2, default=str),
1125+
)
1126+
except (ValueError, TypeError):
1127+
self.envelope_logger.info(
1128+
" Payload: <binary %d bytes>",
1129+
len(item.get_bytes()),
1130+
)
1131+
self.envelope_logger.info("--- End Envelope ---")
1132+
except Exception:
1133+
pass
1134+
1135+
self._inner.capture_envelope(envelope)
1136+
1137+
def flush(
1138+
self,
1139+
timeout: float,
1140+
callback: "Optional[Any]" = None,
1141+
) -> "Any":
1142+
return self._inner.flush(timeout, callback)
1143+
1144+
def kill(self) -> "Any":
1145+
return self._inner.kill()
1146+
1147+
def record_lost_event(
1148+
self,
1149+
reason: str,
1150+
data_category: "Optional[EventDataCategory]" = None,
1151+
item: "Optional[Item]" = None,
1152+
*,
1153+
quantity: int = 1,
1154+
) -> None:
1155+
self._inner.record_lost_event(reason, data_category, item, quantity=quantity)
1156+
1157+
def is_healthy(self) -> bool:
1158+
return self._inner.is_healthy()
1159+
1160+
def __getattr__(self, name: str) -> "Any":
1161+
return getattr(self._inner, name)
1162+
1163+
10841164
class _FunctionTransport(Transport):
10851165
"""
10861166
DEPRECATED: Users wishing to provide a custom transport should subclass
@@ -1147,8 +1227,10 @@ def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]":
11471227
"You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport."
11481228
)
11491229

1230+
transport: "Optional[Transport]" = None
1231+
11501232
if isinstance(ref_transport, Transport):
1151-
return ref_transport
1233+
transport = ref_transport
11521234
elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
11531235
transport_cls = ref_transport
11541236
elif callable(ref_transport):
@@ -1158,11 +1240,16 @@ def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]":
11581240
DeprecationWarning,
11591241
stacklevel=2,
11601242
)
1161-
return _FunctionTransport(ref_transport)
1243+
transport = _FunctionTransport(ref_transport)
11621244

11631245
# if a transport class is given only instantiate it if the dsn is not
11641246
# empty or None
1165-
if options["dsn"]:
1166-
return transport_cls(options)
1247+
if transport is None and options["dsn"]:
1248+
transport = transport_cls(options)
1249+
1250+
if transport is not None and os.environ.get(
1251+
"SENTRY_PRINT_ENVELOPES", ""
1252+
).lower() in ("1", "true", "yes"):
1253+
transport = _EnvelopePrinterTransport(transport)
11671254

1168-
return None
1255+
return transport

tests/test_transport.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import asyncio
12
import logging
2-
import pickle
33
import os
4+
import pickle
45
import socket
56
import sys
6-
import asyncio
77
from collections import defaultdict
88
from datetime import datetime, timedelta, timezone
99
from unittest import mock
1010

1111
import pytest
12+
1213
from tests.conftest import CapturingServer
1314

1415
try:
@@ -30,23 +31,22 @@
3031
import sentry_sdk
3132
from sentry_sdk import (
3233
Client,
34+
Hub,
3335
add_breadcrumb,
3436
capture_message,
35-
isolation_scope,
3637
get_isolation_scope,
37-
Hub,
38+
isolation_scope,
3839
)
3940
from sentry_sdk._compat import PY37, PY38
40-
from sentry_sdk.envelope import Envelope, Item, parse_json, PayloadRef
41+
from sentry_sdk.envelope import Envelope, Item, PayloadRef, parse_json
42+
from sentry_sdk.integrations.asyncio import AsyncioIntegration
43+
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
4144
from sentry_sdk.transport import (
4245
KEEP_ALIVE_SOCKET_OPTIONS,
43-
_parse_rate_limits,
4446
AsyncHttpTransport,
4547
HttpTransport,
48+
_parse_rate_limits,
4649
)
47-
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
48-
from sentry_sdk.integrations.asyncio import AsyncioIntegration
49-
5050

5151
server = None
5252

0 commit comments

Comments
 (0)