-
Notifications
You must be signed in to change notification settings - Fork 569
feat(django): Add span around Task.enqueue
#5209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d3f416f
aefbc18
11180e6
3158781
8a6cc39
176a660
1589371
7c5ed8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| from functools import wraps | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk.consts import OP | ||
| from sentry_sdk.tracing import SPANSTATUS | ||
| from sentry_sdk.utils import qualname_from_function | ||
|
|
||
| try: | ||
| # django.tasks were added in Django 6.0 | ||
| from django.tasks.base import Task, TaskResultStatus | ||
| except ImportError: | ||
| Task = None | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Any | ||
|
|
||
|
|
||
| def patch_tasks(): | ||
| # type: () -> None | ||
| if Task is None: | ||
| return | ||
|
|
||
| old_task_enqueue = Task.enqueue | ||
|
|
||
| @wraps(old_task_enqueue) | ||
| def _sentry_enqueue(self, *args, **kwargs): | ||
| # type: (Any, Any, Any) -> Any | ||
| from sentry_sdk.integrations.django import DjangoIntegration | ||
|
|
||
| integration = sentry_sdk.get_client().get_integration(DjangoIntegration) | ||
| if integration is None: | ||
| return old_task_enqueue(self, *args, **kwargs) | ||
|
|
||
| name = qualname_from_function(self.func) or "<unknown Django task>" | ||
|
|
||
| with sentry_sdk.start_span( | ||
| op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Queue span uses HTTP origin instead of queue originThe
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a valid point @sentrivana.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IDK if When someone actually uses Celery/some other queuing library as a backend for |
||
| ): | ||
| return old_task_enqueue(self, *args, **kwargs) | ||
|
|
||
| Task.enqueue = _sentry_enqueue | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import pytest | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk import start_span | ||
| from sentry_sdk.integrations.django import DjangoIntegration | ||
| from sentry_sdk.consts import OP | ||
|
|
||
|
|
||
| try: | ||
| from django.tasks import task | ||
|
|
||
| HAS_DJANGO_TASKS = True | ||
| except ImportError: | ||
| HAS_DJANGO_TASKS = False | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def immediate_backend(settings): | ||
| """Configure Django to use the immediate task backend for synchronous testing.""" | ||
| settings.TASKS = { | ||
| "default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"} | ||
| } | ||
|
|
||
|
|
||
| if HAS_DJANGO_TASKS: | ||
|
|
||
| @task | ||
| def simple_task(): | ||
| return "result" | ||
|
|
||
| @task | ||
| def add_numbers(a, b): | ||
| return a + b | ||
|
|
||
| @task | ||
| def greet(name, greeting="Hello"): | ||
| return f"{greeting}, {name}!" | ||
|
|
||
| @task | ||
| def failing_task(): | ||
| raise ValueError("Task failed!") | ||
|
|
||
| @task | ||
| def task_one(): | ||
| return 1 | ||
|
|
||
| @task | ||
| def task_two(): | ||
| return 2 | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not HAS_DJANGO_TASKS, | ||
| reason="Django tasks are only available in Django 6.0+", | ||
| ) | ||
| def test_task_span_is_created(sentry_init, capture_events, immediate_backend): | ||
| """Test that the queue.submit.django span is created when a task is enqueued.""" | ||
| sentry_init( | ||
| integrations=[DjangoIntegration()], | ||
| traces_sample_rate=1.0, | ||
| ) | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction(name="test_transaction"): | ||
| simple_task.enqueue() | ||
|
|
||
| (event,) = events | ||
| assert event["type"] == "transaction" | ||
|
|
||
| queue_submit_spans = [ | ||
| span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO | ||
| ] | ||
| assert len(queue_submit_spans) == 1 | ||
| assert ( | ||
| queue_submit_spans[0]["description"] | ||
| == "tests.integrations.django.test_tasks.simple_task" | ||
| ) | ||
| assert queue_submit_spans[0]["origin"] == "auto.http.django" | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not HAS_DJANGO_TASKS, | ||
| reason="Django tasks are only available in Django 6.0+", | ||
| ) | ||
| def test_task_enqueue_returns_result(sentry_init, immediate_backend): | ||
| """Test that the task enqueuing behavior is unchanged from the user perspective.""" | ||
| sentry_init( | ||
| integrations=[DjangoIntegration()], | ||
| traces_sample_rate=1.0, | ||
| ) | ||
|
|
||
| result = add_numbers.enqueue(3, 5) | ||
|
|
||
| assert result is not None | ||
| assert result.return_value == 8 | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not HAS_DJANGO_TASKS, | ||
| reason="Django tasks are only available in Django 6.0+", | ||
| ) | ||
| def test_task_enqueue_with_kwargs(sentry_init, immediate_backend, capture_events): | ||
| """Test that task enqueuing works correctly with keyword arguments.""" | ||
| sentry_init( | ||
| integrations=[DjangoIntegration()], | ||
| traces_sample_rate=1.0, | ||
| ) | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction(name="test_transaction"): | ||
| result = greet.enqueue(name="World", greeting="Hi") | ||
|
|
||
| assert result.return_value == "Hi, World!" | ||
|
|
||
| (event,) = events | ||
| queue_submit_spans = [ | ||
| span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO | ||
| ] | ||
| assert len(queue_submit_spans) == 1 | ||
| assert ( | ||
| queue_submit_spans[0]["description"] | ||
| == "tests.integrations.django.test_tasks.greet" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not HAS_DJANGO_TASKS, | ||
| reason="Django tasks are only available in Django 6.0+", | ||
| ) | ||
| def test_task_error_reporting(sentry_init, immediate_backend, capture_events): | ||
| """Test that errors in tasks are correctly reported and don't break the span.""" | ||
| sentry_init( | ||
| integrations=[DjangoIntegration()], | ||
| traces_sample_rate=1.0, | ||
| ) | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction(name="test_transaction"): | ||
| result = failing_task.enqueue() | ||
|
|
||
| with pytest.raises(ValueError, match="Task failed"): | ||
| _ = result.return_value | ||
|
|
||
| assert len(events) == 2 | ||
| transaction_event = events[-1] | ||
| assert transaction_event["type"] == "transaction" | ||
|
|
||
| queue_submit_spans = [ | ||
| span | ||
| for span in transaction_event["spans"] | ||
| if span["op"] == OP.QUEUE_SUBMIT_DJANGO | ||
| ] | ||
| assert len(queue_submit_spans) == 1 | ||
| assert ( | ||
| queue_submit_spans[0]["description"] | ||
| == "tests.integrations.django.test_tasks.failing_task" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not HAS_DJANGO_TASKS, | ||
| reason="Django tasks are only available in Django 6.0+", | ||
| ) | ||
| def test_multiple_task_enqueues_create_multiple_spans( | ||
| sentry_init, capture_events, immediate_backend | ||
| ): | ||
| """Test that enqueueing multiple tasks creates multiple spans.""" | ||
| sentry_init( | ||
| integrations=[DjangoIntegration()], | ||
| traces_sample_rate=1.0, | ||
| ) | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction(name="test_transaction"): | ||
| task_one.enqueue() | ||
| task_two.enqueue() | ||
| task_one.enqueue() | ||
|
|
||
| (event,) = events | ||
| queue_submit_spans = [ | ||
| span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO | ||
| ] | ||
| assert len(queue_submit_spans) == 3 | ||
|
|
||
| span_names = [span["description"] for span in queue_submit_spans] | ||
| assert span_names.count("tests.integrations.django.test_tasks.task_one") == 2 | ||
| assert span_names.count("tests.integrations.django.test_tasks.task_two") == 1 |
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not public, but it's def there and pretty integral to the whole tasks feature, so I don't expect it to disappear.