Skip to content

Commit 737d508

Browse files
ajaycjAjay Chinthalapalli Jayakumar
and
Ajay Chinthalapalli Jayakumar
authored
Custom Event Hooks Event Emission (#164)
* custom hook impl Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * refactor and unit tests Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * readme and change log Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * handling both hook and new relic Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * more tests to increase coverage + static/format fixes Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * remove unused imports Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * remove noisy logs + more tests Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * fixes and implement review suggestions Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> * stic-fx and make format Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> --------- Signed-off-by: Ajay Chinthalapalli Jayakumar <[email protected]> Co-authored-by: Ajay Chinthalapalli Jayakumar <[email protected]>
1 parent 07ebb10 commit 737d508

File tree

6 files changed

+175
-29
lines changed

6 files changed

+175
-29
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ ENV/
128128
# IntelliJ
129129
/out/
130130

131+
# Visual Studio Code
132+
.vscode/
133+
131134
# mpeltonen/sbt-idea plugin
132135
.idea_modules/
133136

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
# [Unreleased]
88
- [PR 156](https://github.com/salesforce/django-declarative-apis/pull/156) Update GitHub action versions
99
- [PR 162](https://github.com/salesforce/django-declarative-apis/pull/162) Fix Makefile install target
10+
- [PR 164](https://github.com/salesforce/django-declarative-apis/pull/164) Custom Event Hooks Event Emission
1011

1112
# [0.31.7]
1213
- [PR 148](https://github.com/salesforce/django-declarative-apis/pull/148) chore: upgrade django 4.2 LTS

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,10 @@ class PingDefinition(machinery.BaseEndpointDefinition):
9595
def resource(self):
9696
return {'ping': 'pong'}
9797
```
98+
99+
Optional: Implement Custom Event Hooks for Event Emission
100+
-----
101+
```bash
102+
# settings.py
103+
DDA_EVENT_HOOK = "my_app.hooks.custom_event_handler"
104+
```

django_declarative_apis/events.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#
2+
# Copyright (c) 2025, salesforce.com, inc.
3+
# All rights reserved.
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
# For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
#
7+
8+
import importlib
9+
from enum import Enum
10+
import logging
11+
from django.conf import settings
12+
13+
logger = logging.getLogger(__name__)
14+
15+
HOOK = getattr(settings, "DDA_EVENT_HOOK", None)
16+
17+
18+
# Enum for event types
19+
class EventType(Enum):
20+
QUEUE_SNAPSHOT = "queue_snapshot"
21+
TASK_RETRY_ATTEMPT = "task_retry"
22+
23+
24+
def _import_hook(hook_path):
25+
"""
26+
Import and return a hook function from a string path.
27+
28+
This function dynamically imports a hook function specified by a dotted string path.
29+
It ensures that the imported object is callable and raises an error if it is not.
30+
31+
Args:
32+
hook_path (str): The dotted string path to the hook function.
33+
For example, 'module.submodule.function'.
34+
35+
Returns:
36+
Callable: The imported hook function.
37+
38+
Raises:
39+
TypeError: If the imported object is not callable.
40+
"""
41+
module_path, function_name = hook_path.rsplit(".", 1)
42+
module = importlib.import_module(module_path)
43+
function = getattr(module, function_name)
44+
if not callable(function):
45+
raise TypeError(f"Consumer getter ({hook_path}) must be callable")
46+
return function
47+
48+
49+
def emit_events(event_type, payload):
50+
"""
51+
Emit a metric event using the configured hook.
52+
This function sends an event to a custom hook, if configured, with the specified
53+
event type and payload. The hook is dynamically imported and executed. Logs any errors
54+
encountered during the hook execution.
55+
56+
Args:
57+
event_type (EventType): The type of the event, as defined in the EventType enum.
58+
payload (dict): A dictionary containing the data associated with the event.
59+
This data is passed to the custom hook for processing.
60+
Raises:
61+
Exception: Logs an error if the custom hook encounters an issue during execution.
62+
"""
63+
if HOOK:
64+
try:
65+
hook_callable = _import_hook(HOOK)
66+
hook_callable(event_type, payload)
67+
logger.info(f"Event emitted via custom hook: {event_type}")
68+
except Exception as e:
69+
logger.error(f"Error in custom hook for events: {e}", exc_info=True)

django_declarative_apis/machinery/tasks.py

+23-29
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616
from django.conf import settings
1717
from django.core.cache import cache
1818
import django.db.models
19-
20-
try:
21-
from newrelic import agent as newrelic_agent
22-
except ImportError:
23-
newrelic_agent = None
19+
from django_declarative_apis.events import emit_events, EventType
2420

2521
try:
2622
import cid.locals
@@ -73,19 +69,18 @@ def _log_task_stats(
7369

7470
cache.set(QUEUE_LENGTH_CACHE_KEY, queue_length)
7571

76-
if newrelic_agent:
77-
newrelic_agent.record_custom_event(
78-
"task_runner:queue_length",
79-
{
80-
"queue_length": queue_length,
81-
"queue": queue,
82-
"routing_key": routing_key,
83-
"wait_time_seconds": wait_time,
84-
"queue_delay_seconds": queue_delay,
85-
"process_task_count": process_task_count,
86-
"correlation_id": correlation_id,
87-
},
88-
)
72+
emit_events(
73+
EventType.QUEUE_SNAPSHOT,
74+
{
75+
"queue_length": queue_length,
76+
"queue": queue,
77+
"routing_key": routing_key,
78+
"wait_time_seconds": wait_time,
79+
"queue_delay_seconds": queue_delay,
80+
"process_task_count": process_task_count,
81+
"correlation_id": correlation_id,
82+
},
83+
)
8984

9085
logger.info(
9186
"method=%s, resource_id=%s, queue_length=%s, queue=%s, routing_key=%s, task_wait_time=%s, "
@@ -110,17 +105,16 @@ def _log_task_stats(
110105

111106

112107
def _log_retry_stats(method_name, resource_instance_id, correlation_id):
113-
if newrelic_agent:
114-
newrelic_agent.record_custom_event(
115-
"task_runner:retry",
116-
{"method_name": method_name, "resource_instance_id": resource_instance_id},
117-
)
118-
logger.warning(
119-
"will retry task: method=%s, resource_id=%s, correlation_id=%s",
120-
method_name,
121-
resource_instance_id,
122-
correlation_id,
123-
)
108+
emit_events(
109+
EventType.TASK_RETRY_ATTEMPT,
110+
{"method_name": method_name, "resource_instance_id": resource_instance_id},
111+
)
112+
logger.warning(
113+
"will retry task: method=%s, resource_id=%s, correlation_id=%s",
114+
method_name,
115+
resource_instance_id,
116+
correlation_id,
117+
)
124118

125119

126120
class RetryParams(NamedTuple):

tests/test_events.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#
2+
# Copyright (c) 2025, salesforce.com, inc.
3+
# All rights reserved.
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
# For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
#
7+
8+
import unittest
9+
from unittest.mock import patch, Mock
10+
from django_declarative_apis.events import _import_hook, emit_events
11+
from django.test import override_settings
12+
13+
14+
def test_function():
15+
pass
16+
17+
18+
class EmitEventsTest(unittest.TestCase):
19+
@override_settings(DDA_EVENT_HOOK="tests.test_events.test_function")
20+
def test_import_hook(self):
21+
hook_path = "tests.test_events.test_function"
22+
hook_function = _import_hook(hook_path)
23+
self.assertTrue(callable(hook_function))
24+
self.assertEqual(hook_function.__name__, "test_function")
25+
26+
def test_invalid_hook_path(self):
27+
with self.assertRaises(ValueError) as context:
28+
_import_hook("invalid_path")
29+
self.assertEqual(
30+
str(context.exception),
31+
"not enough values to unpack (expected 2, got 1)",
32+
)
33+
34+
def test_nonexistent_function(self):
35+
with self.assertRaises(AttributeError) as context:
36+
_import_hook("tests.test_events.no_function")
37+
self.assertIn(
38+
"module 'tests.test_events' has no attribute 'no_function'",
39+
str(context.exception),
40+
)
41+
42+
@patch("django_declarative_apis.events._import_hook")
43+
@patch("django_declarative_apis.events.logger")
44+
def test_emit_events_with_hook(self, mock_logger, mock_import_hook):
45+
event_type, payload = "test_event", {"key": "value"}
46+
mock_hook_function = Mock()
47+
mock_import_hook.return_value = mock_hook_function
48+
with patch(
49+
"django_declarative_apis.events.HOOK", "tests.test_events.test_function"
50+
):
51+
emit_events(event_type, payload)
52+
mock_import_hook.assert_called_once_with("tests.test_events.test_function")
53+
mock_hook_function.assert_called_once_with(event_type, payload)
54+
mock_logger.info.assert_called_once_with(
55+
"Event emitted via custom hook: test_event"
56+
)
57+
58+
@patch("django_declarative_apis.events._import_hook")
59+
@patch("django_declarative_apis.events.logger")
60+
@override_settings(DDA_EVENT_HOOK="tests.test_events.test_function")
61+
def test_emit_events_with_exception_calling_hook(
62+
self, mock_logger, mock_import_hook
63+
):
64+
event_type, payload = "test_event", {"key": "value"}
65+
mock_import_hook.return_value.side_effect = Exception("Simulated Exception")
66+
with patch(
67+
"django_declarative_apis.events.HOOK", "tests.test_events.test_function"
68+
):
69+
emit_events(event_type, payload)
70+
mock_logger.error.assert_called_once_with(
71+
"Error in custom hook for events: Simulated Exception", exc_info=True
72+
)

0 commit comments

Comments
 (0)