Skip to content

Commit 3157070

Browse files
committed
feat: improve exception handling
1 parent f00d10c commit 3157070

5 files changed

Lines changed: 69 additions & 25 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,6 @@ __marimo__/
208208

209209
# VS Code
210210
.vscode/
211+
212+
# Misc
213+
.temp/

README.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# python-victorialogs-handler
1+
# victorialogs-handler
22

33
A Python log handler for Victoria Logs.
44

@@ -12,11 +12,48 @@ STATUS: In development
1212

1313
## Description
1414

15-
This package provides a Python log handler for Victoria Logs. The log handler is designed to work with Python's default logging module and will send all log events to a configured Victoria Logs server for log ingestion.
15+
**victorialogs-handler** is a high-performance Python log handler tailored for [VictoriaLogs](https://victoriametrics.com/products/victorialogs/). It integrates seamlessly with Python’s native logging module, allowing you to stream log events to a VictoriaLogs instance with minimal configuration and zero friction.
1616

1717
## Key Features
1818

19-
- Asynchronous design: Log events are queued and then processed in a separate thread so that the performance impact on the main program remains minimal.
20-
- Request batching: Log events are processed without delay. Multiple log events will be sent in a single request as batch to minimize the number of requests to the VictoriaLogs server.
21-
- Supports extras: Extra fields are supported. This includes non-standard types like sets and datetime objects. Fields that can not be serialized to JSON (e.g. functions) will be converted into their string representation
22-
- Supports dict config: The handler supports dict configuration, e.g. for a Django server
19+
- **Asynchronous Processing**: Log events are queued and dispatched in a dedicated background thread, ensuring your application's main execution flow remains non-blocking and highly responsive.
20+
21+
- **Efficient Request Batching**: To optimize network throughput and reduce overhead on your VictoriaLogs server, multiple log events are automatically combined into single-request batches.
22+
23+
- **Rich Exception Handling**: Automatically captures and flattens stack traces. Log messages include both the exception name and the full traceback as searchable fields.
24+
25+
- **Smart Serialization**: Supports extra fields out of the box. It intelligently handles non-standard types like set and datetime. Any non-serializable objects (such as functions or custom classes) are gracefully converted to their string representation.
26+
27+
- **Standard Configuration Support**: Fully compatible with `logging.config.dictConfig`, making it easy to drop into frameworks like Django, Flask, or FastAPI.
28+
29+
## Technical details
30+
31+
This section documents technical details of the solution.
32+
33+
### LogRecord fields
34+
35+
The following fields will be transferred for each log event. They are derived from Python's [LogRecord](https://docs.python.org/3/library/logging.html#logrecord-objects):
36+
37+
Name | Description | Example | Optional
38+
-- | -- | -- | --
39+
`exception_name` | Name of the exception | `ZeroDivisionError` | yes
40+
`exception` | Full traceback of the exception | `Traceback ...` | yes
41+
`function` | Name of the function that emitted the log event | `my_function` | no
42+
`level` | Name of the level of the emitted log event | `INFO` | no
43+
`line_number` | Line number where the log event was emitted | `89` | no
44+
`logger` | Name of the related Python logger | `my_package.my_module` | no
45+
`message` | The logged message | 'This is a log entry' | no
46+
`stream` | Name of the top-level Python package that emitted the log the event. | `my_package` | no
47+
`timestamp` | Timestamp of the log event, represented as fractional UNIX epoch | `1775081468.4308655` | no
48+
49+
In addition any custom `extras' fields will be added as they are encountered.
50+
51+
### VictoriaLogs special fields
52+
53+
VictoriaLogs handles three fields in a special:
54+
55+
- `_msg`: The logged message. This is a mandatory field and is mapped to `message`.
56+
- `_time`: The timestamp of the log event. This field and is mapped to `timestamp`.
57+
- `_stream`: The source of a log event, which is used to group and filter logs. This field is mapped to `stream`.
58+
59+
For more information please also see [VictoriaLogs Data model](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).

src/vlogs_handler/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .handler import VictoriaLogsHandler # noqa: F401
44

55
__title__ = "Victoria Logs Handler"
6-
__version__ = "0.1.0dev2"
6+
__version__ = "0.1.0dev3"
77

88
# TO-DOs
99
# [ ] Options for defining stream

src/vlogs_handler/handler.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import queue
88
import threading
99
import traceback
10-
from typing import Any, Dict, List
10+
from typing import Any, Dict, List, Tuple
1111

1212
import requests
1313

@@ -19,6 +19,7 @@
1919

2020
_STANDARD_ATTRS = {
2121
"args",
22+
"asctime",
2223
"created",
2324
"exc_info",
2425
"exc_text",
@@ -37,18 +38,14 @@
3738
"processName",
3839
"relativeCreated",
3940
"stack_info",
41+
"taskName",
4042
"thread",
4143
"threadName",
4244
}
4345

4446

4547
class VictoriaLogsHandler(logging.Handler):
46-
"""VictoriaLogsHandler dispatches log events to a Victoria Logs server.
47-
48-
Events are sent asynchronously for best performance.
49-
Events are sent without delay.
50-
Events are batched together to reduce the amount of requests to the vlogs server.
51-
"""
48+
"""VictoriaLogsHandler dispatches log events to a Victoria Logs server."""
5249

5350
def __init__(
5451
self,
@@ -98,7 +95,9 @@ def format_log_entry(self, record: logging.LogRecord) -> Dict[str, Any]:
9895
"process_name": record.processName,
9996
}
10097
if record.exc_info:
101-
entry["exception"] = _format_exception(record.exc_info)
98+
entry["exception_name"], entry["exception"] = _format_exception(
99+
record.exc_info
100+
)
102101

103102
for k, v in record.__dict__.items():
104103
if k not in _STANDARD_ATTRS:
@@ -123,7 +122,7 @@ def _send(self, entries: List[Dict[str, Any]]):
123122
lines = []
124123
for entry in entries:
125124
try:
126-
data = json.dumps(entry, cls=JSONEncoderPlus)
125+
data = json.dumps(entry, cls=_JSONEncoderPlus)
127126
lines.append(data)
128127
except Exception as ex:
129128
log.exception("convert entry to JSON", ex, entry=entry)
@@ -142,14 +141,15 @@ def _send(self, entries: List[Dict[str, Any]]):
142141
log.exception("send entry", ex)
143142

144143

145-
def _format_exception(ei):
144+
def _format_exception(ei) -> Tuple[str, str]:
146145
"""
147-
Format and return the specified exception information as a string.
146+
Format and return the name of the exception
147+
and specified exception information as strings.
148148
149149
This default implementation just uses
150150
traceback.print_exception()
151151
152-
Source: logging.Formatter.formatException()
152+
Based on: logging.Formatter.formatException()
153153
"""
154154
sio = io.StringIO()
155155
tb = ei[2]
@@ -158,7 +158,9 @@ def _format_exception(ei):
158158
sio.close()
159159
if s[-1:] == "\n":
160160
s = s[:-1]
161-
return s
161+
162+
name = ei[0].__name__ if ei[0] is not None else ""
163+
return name, s
162164

163165

164166
def _calc_stream_from_record(record: logging.LogRecord):
@@ -173,7 +175,7 @@ def _calc_stream_from_record(record: logging.LogRecord):
173175
return stream
174176

175177

176-
class JSONEncoderPlus(json.JSONEncoder):
178+
class _JSONEncoderPlus(json.JSONEncoder):
177179
"""JSONEncoderPlus is an improved encoder that can convert dates and does not break.
178180
179181
Instead of breaking it will return a string representation

tests/test_handler.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import requests_mock
88

99
from vlogs_handler import VictoriaLogsHandler
10-
from vlogs_handler.handler import JSONEncoderPlus
10+
from vlogs_handler.handler import _JSONEncoderPlus
1111

1212

1313
@requests_mock.Mocker()
@@ -48,6 +48,7 @@ def test_handler_should_send_normal_log(self, m: requests_mock.Mocker):
4848
self.assertEqual(got["level"], "INFO")
4949
self.assertEqual(got["logger"], "test_logger")
5050
self.assertEqual(got["message"], "Alpha")
51+
self.assertNotIn("exception", got)
5152

5253
def test_handler_should_send_log_with_extras(self, m: requests_mock.Mocker):
5354
# given
@@ -97,6 +98,7 @@ def test_handler_should_send_exception_log(self, m: requests_mock.Mocker):
9798
self.assertEqual(got["level"], "ERROR")
9899
self.assertEqual(got["logger"], "test_logger")
99100
self.assertEqual(got["message"], "Bravo")
101+
self.assertEqual("ZeroDivisionError", got["exception_name"])
100102
self.assertIn("ZeroDivisionError", got["exception"])
101103

102104

@@ -157,7 +159,7 @@ def my_func():
157159

158160
my_date = dt.datetime(2026, 1, 11, 12, 15, 42, 99, tzinfo=dt.timezone.utc)
159161
data = {
160-
"class": JSONEncoderPlus,
162+
"class": _JSONEncoderPlus,
161163
"date": my_date.date(),
162164
"datetime": my_date,
163165
"float": 1.23,
@@ -168,11 +170,11 @@ def my_func():
168170
}
169171

170172
# when
171-
got = json.loads(json.dumps(data, cls=JSONEncoderPlus))
173+
got = json.loads(json.dumps(data, cls=_JSONEncoderPlus))
172174

173175
# then
174176
self.assertEqual(
175-
got["class"], "<class 'vlogs_handler.handler.JSONEncoderPlus'>"
177+
got["class"], "<class 'vlogs_handler.handler._JSONEncoderPlus'>"
176178
)
177179
self.assertEqual(got["date"], "2026-01-11")
178180
self.assertEqual(got["datetime"], "2026-01-11T12:15:42.000099+00:00")

0 commit comments

Comments
 (0)