Skip to content

Commit eb7bf18

Browse files
committed
implements #7362
1 parent 3c8330d commit eb7bf18

File tree

2 files changed

+56
-6
lines changed

2 files changed

+56
-6
lines changed

packages/common-library/src/common_library/error_codes.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" osparc ERROR CODES (OEC)
1+
"""osparc ERROR CODES (OEC)
22
Unique identifier of an exception instance
33
Intended to report a user about unexpected errors.
44
Unexpected exceptions can be traced by matching the
@@ -7,13 +7,15 @@
77
SEE test_error_codes for some use cases
88
"""
99

10+
import hashlib
1011
import re
12+
import traceback
1113
from typing import TYPE_CHECKING, Annotated
1214

1315
from pydantic import StringConstraints, TypeAdapter
1416

1517
_LABEL = "OEC:{}"
16-
_PATTERN = r"OEC:\d+"
18+
_PATTERN = r"OEC:[a-zA-Z0-9]+"
1719

1820
if TYPE_CHECKING:
1921
ErrorCodeStr = str
@@ -22,9 +24,24 @@
2224
str, StringConstraints(strip_whitespace=True, pattern=_PATTERN)
2325
]
2426

27+
_LEN = 12 # chars (~48 bits)
28+
29+
30+
def _generate_error_fingerprint(exc: BaseException) -> str:
31+
"""
32+
Unique error fingerprint for deduplication purposes
33+
"""
34+
tb = traceback.extract_tb(exc.__traceback__)
35+
frame_sigs = [f"{frame.name}:{frame.lineno}" for frame in tb]
36+
fingerprint = f"{type(exc).__name__}|" + "|".join(frame_sigs)
37+
# E.g. ZeroDivisionError|foo:23|main:10
38+
return hashlib.sha256(fingerprint.encode()).hexdigest()[:_LEN]
39+
2540

2641
def create_error_code(exception: BaseException) -> ErrorCodeStr:
27-
return TypeAdapter(ErrorCodeStr).validate_python(_LABEL.format(id(exception)))
42+
return TypeAdapter(ErrorCodeStr).validate_python(
43+
_LABEL.format(_generate_error_fingerprint(exception))
44+
)
2845

2946

3047
def parse_error_code(obj) -> set[ErrorCodeStr]:

packages/common-library/tests/test_error_codes.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,43 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14-
def test_error_code_use_case(caplog: pytest.LogCaptureFixture):
15-
"""use case for error-codes"""
14+
def _level_three(v):
15+
msg = f"An error occurred in level three with {v}"
16+
raise RuntimeError(msg)
17+
18+
19+
def _level_two(v):
20+
_level_three(v)
21+
22+
23+
def _level_one(v=None):
24+
_level_two(v)
25+
26+
27+
def test_exception_fingerprint_consistency():
28+
error_codes = []
29+
30+
for v in range(2):
31+
# emulates different runs of the same function (e.g. different sessions)
32+
try:
33+
_level_one(v) # same even if different value!
34+
except Exception as err:
35+
error_code = create_error_code(err)
36+
error_codes.append(error_code)
37+
38+
assert error_codes == [error_codes[0]] * len(error_codes)
39+
40+
try:
41+
# Same function but different location
42+
_level_one(0)
43+
except Exception as e2:
44+
error_code_2 = create_error_code(e2)
45+
assert error_code_2 != error_code[0]
46+
47+
48+
def test_create_log_and_parse_error_code(caplog: pytest.LogCaptureFixture):
1649
with pytest.raises(RuntimeError) as exc_info:
17-
raise RuntimeError("Something unexpected went wrong")
50+
_level_one()
1851

1952
# 1. Unexpected ERROR
2053
err = exc_info.value

0 commit comments

Comments
 (0)