|
4 | 4 | from datetime import datetime, timezone |
5 | 5 | from typing import Optional, List, Callable, TYPE_CHECKING, Any |
6 | 6 |
|
| 7 | +from sentry_sdk._batcher import Batcher |
7 | 8 | from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute |
8 | 9 | from sentry_sdk.envelope import Envelope, Item, PayloadRef |
9 | 10 |
|
10 | 11 | if TYPE_CHECKING: |
11 | 12 | from sentry_sdk._types import Log |
12 | 13 |
|
13 | 14 |
|
14 | | -class LogBatcher: |
15 | | - MAX_LOGS_BEFORE_FLUSH = 100 |
16 | | - MAX_LOGS_BEFORE_DROP = 1_000 |
| 15 | +class LogBatcher(Batcher): |
| 16 | + MAX_BEFORE_FLUSH = 100 |
| 17 | + MAX_BEFORE_DROP = 1_000 |
17 | 18 | FLUSH_WAIT_TIME = 5.0 |
18 | 19 |
|
19 | | - def __init__( |
20 | | - self, |
21 | | - capture_func: "Callable[[Envelope], None]", |
22 | | - record_lost_func: "Callable[..., None]", |
23 | | - ) -> None: |
24 | | - self._log_buffer: "List[Log]" = [] |
25 | | - self._capture_func = capture_func |
26 | | - self._record_lost_func = record_lost_func |
27 | | - self._running = True |
28 | | - self._lock = threading.Lock() |
| 20 | + TYPE = "log" |
| 21 | + CONTENT_TYPE = "application/vnd.sentry.items.log+json" |
29 | 22 |
|
30 | | - self._flush_event: "threading.Event" = threading.Event() |
31 | | - |
32 | | - self._flusher: "Optional[threading.Thread]" = None |
33 | | - self._flusher_pid: "Optional[int]" = None |
34 | | - |
35 | | - def _ensure_thread(self) -> bool: |
36 | | - """For forking processes we might need to restart this thread. |
37 | | - This ensures that our process actually has that thread running. |
38 | | - """ |
39 | | - if not self._running: |
40 | | - return False |
41 | | - |
42 | | - pid = os.getpid() |
43 | | - if self._flusher_pid == pid: |
44 | | - return True |
45 | | - |
46 | | - with self._lock: |
47 | | - # Recheck to make sure another thread didn't get here and start the |
48 | | - # the flusher in the meantime |
49 | | - if self._flusher_pid == pid: |
50 | | - return True |
51 | | - |
52 | | - self._flusher_pid = pid |
53 | | - |
54 | | - self._flusher = threading.Thread(target=self._flush_loop) |
55 | | - self._flusher.daemon = True |
56 | | - |
57 | | - try: |
58 | | - self._flusher.start() |
59 | | - except RuntimeError: |
60 | | - # Unfortunately at this point the interpreter is in a state that no |
61 | | - # longer allows us to spawn a thread and we have to bail. |
62 | | - self._running = False |
63 | | - return False |
64 | | - |
65 | | - return True |
66 | | - |
67 | | - def _flush_loop(self) -> None: |
68 | | - while self._running: |
69 | | - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) |
70 | | - self._flush_event.clear() |
71 | | - self._flush() |
72 | | - |
73 | | - def add( |
74 | | - self, |
75 | | - log: "Log", |
76 | | - ) -> None: |
77 | | - if not self._ensure_thread() or self._flusher is None: |
78 | | - return None |
79 | | - |
80 | | - with self._lock: |
81 | | - if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_DROP: |
82 | | - # Construct log envelope item without sending it to report lost bytes |
83 | | - log_item = Item( |
84 | | - type="log", |
85 | | - content_type="application/vnd.sentry.items.log+json", |
86 | | - headers={ |
87 | | - "item_count": 1, |
88 | | - }, |
89 | | - payload=PayloadRef( |
90 | | - json={"items": [LogBatcher._log_to_transport_format(log)]} |
91 | | - ), |
92 | | - ) |
93 | | - self._record_lost_func( |
94 | | - reason="queue_overflow", |
95 | | - data_category="log_item", |
96 | | - item=log_item, |
97 | | - quantity=1, |
98 | | - ) |
99 | | - return None |
100 | | - |
101 | | - self._log_buffer.append(log) |
102 | | - if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_FLUSH: |
103 | | - self._flush_event.set() |
104 | | - |
105 | | - def kill(self) -> None: |
106 | | - if self._flusher is None: |
107 | | - return |
108 | | - |
109 | | - self._running = False |
110 | | - self._flush_event.set() |
111 | | - self._flusher = None |
| 23 | + def _record_lost(self, item: "Log") -> None: |
| 24 | + # Construct log envelope item without sending it to report lost bytes |
| 25 | + log_item = Item( |
| 26 | + type=self.TYPE, |
| 27 | + content_type=self.CONTENT_TYPE, |
| 28 | + headers={ |
| 29 | + "item_count": 1, |
| 30 | + }, |
| 31 | + payload=PayloadRef(json={"items": [self._to_transport_format(item)]}), |
| 32 | + ) |
112 | 33 |
|
113 | | - def flush(self) -> None: |
114 | | - self._flush() |
| 34 | + self._record_lost_func( |
| 35 | + reason="queue_overflow", |
| 36 | + data_category="log_item", |
| 37 | + item=log_item, |
| 38 | + quantity=1, |
| 39 | + ) |
115 | 40 |
|
116 | 41 | @staticmethod |
117 | | - def _log_to_transport_format(log: "Log") -> "Any": |
118 | | - if "sentry.severity_number" not in log["attributes"]: |
119 | | - log["attributes"]["sentry.severity_number"] = log["severity_number"] |
120 | | - if "sentry.severity_text" not in log["attributes"]: |
121 | | - log["attributes"]["sentry.severity_text"] = log["severity_text"] |
| 42 | + def _to_transport_format(item: "Log") -> "Any": |
| 43 | + if "sentry.severity_number" not in item["attributes"]: |
| 44 | + item["attributes"]["sentry.severity_number"] = item["severity_number"] |
| 45 | + if "sentry.severity_text" not in item["attributes"]: |
| 46 | + item["attributes"]["sentry.severity_text"] = item["severity_text"] |
122 | 47 |
|
123 | 48 | res = { |
124 | | - "timestamp": int(log["time_unix_nano"]) / 1.0e9, |
125 | | - "trace_id": log.get("trace_id", "00000000-0000-0000-0000-000000000000"), |
126 | | - "span_id": log.get("span_id"), |
127 | | - "level": str(log["severity_text"]), |
128 | | - "body": str(log["body"]), |
| 49 | + "timestamp": int(item["time_unix_nano"]) / 1.0e9, |
| 50 | + "trace_id": item.get("trace_id", "00000000-0000-0000-0000-000000000000"), |
| 51 | + "span_id": item.get("span_id"), |
| 52 | + "level": str(item["severity_text"]), |
| 53 | + "body": str(item["body"]), |
129 | 54 | "attributes": { |
130 | | - k: serialize_attribute(v) for (k, v) in log["attributes"].items() |
| 55 | + k: serialize_attribute(v) for (k, v) in item["attributes"].items() |
131 | 56 | }, |
132 | 57 | } |
133 | 58 |
|
134 | 59 | return res |
135 | | - |
136 | | - def _flush(self) -> "Optional[Envelope]": |
137 | | - envelope = Envelope( |
138 | | - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} |
139 | | - ) |
140 | | - with self._lock: |
141 | | - if len(self._log_buffer) == 0: |
142 | | - return None |
143 | | - |
144 | | - envelope.add_item( |
145 | | - Item( |
146 | | - type="log", |
147 | | - content_type="application/vnd.sentry.items.log+json", |
148 | | - headers={ |
149 | | - "item_count": len(self._log_buffer), |
150 | | - }, |
151 | | - payload=PayloadRef( |
152 | | - json={ |
153 | | - "items": [ |
154 | | - self._log_to_transport_format(log) |
155 | | - for log in self._log_buffer |
156 | | - ] |
157 | | - } |
158 | | - ), |
159 | | - ) |
160 | | - ) |
161 | | - self._log_buffer.clear() |
162 | | - |
163 | | - self._capture_func(envelope) |
164 | | - return envelope |
0 commit comments