Skip to content

Commit 9188c82

Browse files
committed
fixes indentify errors
1 parent afe3c88 commit 9188c82

6 files changed

Lines changed: 148 additions & 19 deletions

File tree

ckanext/push_errors/logging.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ckan.plugins import toolkit
99
from ckanext.push_errors import __VERSION__ as push_errors_version
1010
from ckanext.push_errors.redis import get_cache
11+
from ckanext.push_errors.utils import get_error_log_id
1112

1213
log = logging.getLogger(__name__)
1314

@@ -63,7 +64,12 @@ def emit(self, record):
6364
f'[{extras.get("name")}]::{extras.get("levelname")}::'
6465
f'{extras.get("asctime")}'
6566
)
66-
push_message(msg)
67+
68+
# Generate unique log ID
69+
error_id = get_error_log_id(record)
70+
71+
# Send with additional context
72+
push_message(msg, extra_context={"error_id": error_id})
6773

6874

6975
def push_message(message, extra_context={}):

ckanext/push_errors/plugin.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ckanext.push_errors.logging import PushErrorHandler, push_message
88
from ckanext.push_errors.cli import push_errors as push_errors_commands
99
from ckanext.push_errors.error_logger import log_error
10+
from ckanext.push_errors.utils import get_error_trace_id
1011

1112
from ckanext.push_errors.blueprints.push_errors import push_error_bp
1213

@@ -59,16 +60,17 @@ def error_handler(exception):
5960
params = toolkit.request.args if toolkit.request else '-'
6061
path = toolkit.request.path if toolkit.request else '-'
6162
user = current_user.name if current_user else '-'
63+
error_id = get_error_trace_id(exception)
6264

6365
error_message = (
64-
f'INTERNAL_ERROR `{exception_str}` \n\t'
65-
f'TRACE\n```{trace}```\n\t'
66-
f'on page {path}\n\t'
67-
f'params: {params}\n\t'
66+
f'INTERNAL_ERROR `{exception_str}`\n'
67+
f'TRACE\n'
68+
f'```{trace}```\n'
69+
f'on page {path}\n'
70+
f'params: {params}\n'
6871
f'by user *{user}*'
6972
)
70-
push_message(error_message)
71-
# Continue to raise the error
73+
push_message(error_message, extra_context={'error_id': error_id})
7274
raise exception
7375

7476
app.register_error_handler(Exception, error_handler)
@@ -81,14 +83,8 @@ def make_error_log_middleware(self, app, config):
8183
# Prepare and add the PushErrorHandler
8284
push_error_handler = PushErrorHandler()
8385
push_error_handler.setLevel(logging.ERROR)
84-
85-
# Add to the ckan logger
86-
ckan_log = logging.getLogger('ckan')
87-
ckan_log.addHandler(push_error_handler)
88-
# Add to the ckanext logger for all extensions
89-
ckanext_log = logging.getLogger('ckanext')
90-
ckanext_log.addHandler(push_error_handler)
91-
86+
logging.getLogger('ckan').addHandler(push_error_handler)
87+
logging.getLogger('ckanext').addHandler(push_error_handler)
9288
return app
9389

9490
# IClick

ckanext/push_errors/tests/test_logging.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import pytest
44
from datetime import datetime
5-
from unittest.mock import patch, ANY
5+
from unittest.mock import patch
66
from ckanext.push_errors.logging import push_message, PushErrorHandler
77
from ckanext.push_errors import __VERSION__ as push_errors_version
88

@@ -85,4 +85,7 @@ def test_critical_error_logging(self):
8585
push_error_handler = PushErrorHandler()
8686
log.addHandler(push_error_handler)
8787
log.critical("This is a critical error!")
88-
mock_push_message.assert_called_once_with(ANY)
88+
mock_push_message.assert_called_once()
89+
args, kwargs = mock_push_message.call_args
90+
assert "This is a critical error!" in args[0]
91+
assert "error_id" in kwargs["extra_context"]

ckanext/push_errors/tests/test_push_errors_plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from unittest.mock import patch, ANY, MagicMock
2+
from unittest.mock import patch, MagicMock
33
from werkzeug.exceptions import InternalServerError, Unauthorized, Forbidden, NotFound
44
from ckanext.push_errors.plugin import PushErrorsPlugin
55

@@ -55,7 +55,10 @@ def test_middleware_handles_multiple_exceptions(mock_push_message, exception):
5555
if isinstance(exception, (Unauthorized, Forbidden, NotFound)):
5656
mock_push_message.assert_not_called()
5757
else:
58-
mock_push_message.assert_called_once_with(ANY)
58+
mock_push_message.assert_called_once()
59+
args, kwargs = mock_push_message.call_args
60+
assert "Internal error" in args[0]
61+
assert "error_id" in kwargs["extra_context"]
5962

6063

6164
@pytest.mark.ckan_config("ckanext.push_errors.traceback_length", "1000")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import logging
2+
from ckanext.push_errors.utils import get_error_trace_id, get_error_log_id
3+
4+
5+
class CustomError(Exception):
6+
pass
7+
8+
9+
def raise_custom_error():
10+
"""Raises a CustomError to simulate an error scenario for testing purposes."""
11+
raise CustomError("Simulated error")
12+
13+
14+
def test_get_error_trace_id_returns_path_and_line():
15+
"""
16+
Test that get_error_trace_id returns a string containing the file path and line number
17+
where the CustomError was raised, formatted as 'path/to/file.py:<line>'.
18+
Ensures the returned trace_id contains a colon and does not end with '.py'.
19+
"""
20+
try:
21+
raise_custom_error()
22+
except CustomError as e:
23+
trace_id = get_error_trace_id(e)
24+
assert ":" in trace_id
25+
assert not trace_id.endswith(".py") # it only ends with ":<line>"
26+
27+
28+
def test_get_error_trace_id_unknown_when_no_tb():
29+
"""
30+
Test that get_error_trace_id returns 'unknown' when the exception has no traceback.
31+
32+
This test creates a CustomError instance without a traceback and verifies that
33+
the get_error_trace_id function returns the string 'unknown' as expected.
34+
"""
35+
e = CustomError("No traceback")
36+
e.__traceback__ = None
37+
trace_id = get_error_trace_id(e)
38+
assert trace_id == "unknown"
39+
40+
41+
def test_get_error_log_id_from_exc_info():
42+
"""
43+
Tests the get_error_log_id function to ensure it generates an appropriate log identifier
44+
from a logging record containing exception information.
45+
46+
- Creates a test logger.
47+
- Raises and catches a custom exception.
48+
- Generates a log record with the captured exception.
49+
- Obtains the log identifier using get_error_log_id.
50+
- Verifies that the identifier does not start with "loghash:" and contains ":".
51+
52+
This function checks that get_error_log_id correctly processes exception information
53+
in a logging record and generates the expected identifier.
54+
"""
55+
logger = logging.getLogger("testlogger")
56+
57+
try:
58+
raise_custom_error()
59+
except CustomError as e:
60+
record = logger.makeRecord(
61+
name="testlogger",
62+
level=logging.CRITICAL,
63+
fn="test_file.py",
64+
lno=99,
65+
msg="An error occurred",
66+
args=(),
67+
exc_info=(type(e), e, e.__traceback__),
68+
func="test_func",
69+
extra=None
70+
)
71+
log_id = get_error_log_id(record)
72+
assert log_id.startswith("loghash:") is False
73+
assert ":" in log_id
74+
75+
76+
def test_get_error_log_id_from_message_hash():
77+
"""
78+
Test that get_error_log_id generates a unique log ID for a given LogRecord.
79+
80+
This test creates a LogRecord with a critical error message and verifies that
81+
the generated log ID starts with the expected prefix ("loghash:") and has a
82+
length greater than 12 characters, ensuring the uniqueness and format of the
83+
log identifier.
84+
"""
85+
record = logging.LogRecord(
86+
name="test", level=logging.CRITICAL, pathname="", lineno=0,
87+
msg="Some random critical error", args=(), exc_info=None
88+
)
89+
log_id = get_error_log_id(record)
90+
assert log_id.startswith("loghash:")
91+
assert len(log_id) > 12

ckanext/push_errors/utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import traceback
2+
import hashlib
3+
import logging
4+
5+
6+
def get_error_trace_id(error: Exception) -> str:
7+
"""
8+
Generates a unique identifier for an exception using the file path and line number.
9+
"""
10+
tb = traceback.extract_tb(error.__traceback__)
11+
if tb:
12+
file_path, line_number, _, _ = tb[-1]
13+
return f"{file_path}:{line_number}"
14+
return "unknown"
15+
16+
17+
def get_error_log_id(record: logging.LogRecord) -> str:
18+
"""
19+
Generates a unique identifier for a log record. Uses traceback info if present,
20+
otherwise hashes the message to produce a shorter ID.
21+
"""
22+
if record.exc_info:
23+
tb = traceback.extract_tb(record.exc_info[2])
24+
if tb:
25+
file_path, line_number, _, _ = tb[-1]
26+
return f"{file_path}:{line_number}"
27+
# Fallback to hash of the log message
28+
message = record.getMessage()
29+
msg_hash = hashlib.sha256(message.encode("utf-8")).hexdigest()
30+
return f"loghash:{msg_hash[:12]}"

0 commit comments

Comments
 (0)