Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sentry/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import controllers
from . import models
from .hooks import post_load
3 changes: 3 additions & 0 deletions sentry/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import queue_job
30 changes: 30 additions & 0 deletions sentry/controllers/queue_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging

import sentry_sdk

_logger = logging.getLogger(__name__)

try:
from odoo.addons.queue_job.controllers.main import RunJobController
except ImportError:
_logger.debug("'queue_job' is not available, its jobs keep the default name.")
else:

class SentryRunJobController(RunJobController):
@classmethod
def _runjob(cls, env, job):
"""Report a queue job under a name of its own.

A job already runs inside an HTTP request, so the WSGI middleware
gives it a scope of its own. What it does not give it is a name:
every job is reported as the route that ran it, which groups all
of them together whatever they were doing. Name it after the job
instead, keeping to the model and the method so that jobs of the
same kind still group together.
"""
sentry_sdk.get_current_scope().set_transaction_name(
f"{job.model_name}.{job.method_name}", source="task"
)
return super()._runjob(env, job)
3 changes: 3 additions & 0 deletions sentry/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import ir_cron
26 changes: 26 additions & 0 deletions sentry/models/ir_cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import sentry_sdk

from odoo import models


class IrCron(models.Model):
_inherit = "ir.cron"

def _callback(self, cron_name, server_action_id):
"""Run every job under a scope of its own.

The WSGI middleware gives each HTTP request a fresh scope, but a cron
worker serves no request, so its jobs otherwise keep the scope that
existed when ``sentry_sdk.init()`` ran: in the master process, before
the workers forked. Events coming from unrelated jobs then report the
same trace and carry each other's breadcrumbs.
"""
with sentry_sdk.isolation_scope() as scope:
# Forking a scope copies what is already on it, so the inherited
# trace and breadcrumbs have to be dropped explicitly.
scope.clear_breadcrumbs()
scope.set_new_propagation_context()
scope.set_transaction_name(cron_name, source="task")
return super()._callback(cron_name, server_action_id)
8 changes: 7 additions & 1 deletion sentry/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Copyright 2016-2017 Versada <https://versada.eu/>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import test_client, test_logutils, test_processor, test_generalutils
from . import (
test_client,
test_generalutils,
test_ir_cron,
test_logutils,
test_processor,
)
53 changes: 53 additions & 0 deletions sentry/tests/test_ir_cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from unittest.mock import patch

import sentry_sdk

from odoo.tests import TransactionCase

from odoo.addons.base.models.ir_cron import IrCron


class TestIrCronScope(TransactionCase):
def _run_two_jobs(self):
"""Run two jobs, reporting the scope each one ran under."""
seen = []

def spy(self, cron_name, server_action_id):
sentry_sdk.add_breadcrumb(message=cron_name)
scope = sentry_sdk.get_isolation_scope()
seen.append((scope.get_traceparent(), len(scope._breadcrumbs)))
return True

with patch.object(IrCron, "_callback", spy):
self.env["ir.cron"]._callback("job-1", 1)
self.env["ir.cron"]._callback("job-2", 2)
return seen

def test_each_job_runs_under_its_own_trace(self):
outside = sentry_sdk.get_isolation_scope().get_traceparent()
seen = self._run_two_jobs()
self.assertEqual(len(seen), 2)
self.assertTrue(
all(traceparent for traceparent, _ in seen),
"a job ran without any trace at all",
)
self.assertNotEqual(
seen[0][0],
seen[1][0],
"both jobs reported the same trace, so they shared a scope",
)
self.assertEqual(
sentry_sdk.get_isolation_scope().get_traceparent(),
outside,
"the scope of the worker itself must be left as it was",
)

def test_breadcrumbs_do_not_leak_between_jobs(self):
seen = self._run_two_jobs()
self.assertEqual(
[count for _, count in seen],
[1, 1],
"a job saw breadcrumbs left by another one",
)
Loading