[WIP] Sentry improvements (#108)

Stator clears scope during the main loop to behave more like
transactions. Transaction names are set.

Sentry tags:
* 'takahe.version'
* 'takahe.app' values 'web' or 'stator'

Added settings:
* TAKAHE_SENTRY_SAMPLE_RATE
* TAKAHE_SENTRY_TRACES_SAMPLE_RATE
This commit is contained in:
Michael Manfre 2022-12-04 20:08:23 -05:00 committed by GitHub
parent 258d992deb
commit 3f8045f412
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 28 deletions

View file

@ -1,3 +1,6 @@
from django.core.exceptions import MiddlewareNotUsed
from core import sentry
from core.models import Config from core.models import Config
@ -13,3 +16,19 @@ class ConfigLoadingMiddleware:
Config.system = Config.load_system() Config.system = Config.load_system()
response = self.get_response(request) response = self.get_response(request)
return response return response
class SentryTaggingMiddleware:
"""
Sets Sentry tags at the start of the request if Sentry is configured.
"""
def __init__(self, get_response):
if not sentry.SENTRY_ENABLED:
raise MiddlewareNotUsed()
self.get_response = get_response
def __call__(self, request):
sentry.set_takahe_app("web")
response = self.get_response(request)
return response

49
core/sentry.py Normal file
View file

@ -0,0 +1,49 @@
from contextlib import contextmanager
from django.conf import settings
SENTRY_ENABLED = False
try:
if settings.SETUP.SENTRY_DSN:
import sentry_sdk
SENTRY_ENABLED = True
except ImportError:
pass
def noop(*args, **kwargs):
pass
@contextmanager
def noop_context(*args, **kwargs):
yield
if SENTRY_ENABLED:
configure_scope = sentry_sdk.configure_scope
push_scope = sentry_sdk.push_scope
set_context = sentry_sdk.set_context
set_tag = sentry_sdk.set_tag
start_transaction = sentry_sdk.start_transaction
else:
configure_scope = noop_context
push_scope = noop_context
set_context = noop
set_tag = noop
start_transaction = noop_context
def set_takahe_app(name: str):
set_tag("takahe.app", name)
def scope_clear(scope):
if scope:
scope.clear()
def set_transaction_name(scope, name: str):
if scope:
scope.set_transaction_name(name)

View file

@ -7,7 +7,7 @@ from typing import List, Optional, Type
from django.utils import timezone from django.utils import timezone
from core import exceptions from core import exceptions, sentry
from core.models import Config from core.models import Config
from stator.models import StatorModel from stator.models import StatorModel
@ -38,6 +38,7 @@ class StatorRunner:
self.run_for = run_for self.run_for = run_for
async def run(self): async def run(self):
sentry.set_takahe_app("stator")
self.handled = 0 self.handled = 0
self.started = time.monotonic() self.started = time.monotonic()
self.last_clean = time.monotonic() - self.schedule_interval self.last_clean = time.monotonic() - self.schedule_interval
@ -45,6 +46,7 @@ class StatorRunner:
# For the first time period, launch tasks # For the first time period, launch tasks
print("Running main task loop") print("Running main task loop")
try: try:
with sentry.configure_scope() as scope:
while True: while True:
# Do we need to do cleaning? # Do we need to do cleaning?
if (time.monotonic() - self.last_clean) >= self.schedule_interval: if (time.monotonic() - self.last_clean) >= self.schedule_interval:
@ -54,14 +56,22 @@ class StatorRunner:
print("Running cleaning and scheduling") print("Running cleaning and scheduling")
await self.run_scheduling() await self.run_scheduling()
# Clear the cleaning breadcrumbs/extra for the main part of the loop
sentry.scope_clear(scope)
self.remove_completed_tasks() self.remove_completed_tasks()
await self.fetch_and_process_tasks() await self.fetch_and_process_tasks()
# Are we in limited run mode? # Are we in limited run mode?
if self.run_for and (time.monotonic() - self.started) > self.run_for: if (
self.run_for
and (time.monotonic() - self.started) > self.run_for
):
break break
# Prevent busylooping # Prevent busylooping
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
# Clear the Sentry breadcrumbs and extra for next loop
sentry.scope_clear(scope)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
# Wait for tasks to finish # Wait for tasks to finish
@ -79,6 +89,7 @@ class StatorRunner:
""" """
Do any transition cleanup tasks Do any transition cleanup tasks
""" """
with sentry.start_transaction(op="task", name="stator.run_scheduling"):
for model in self.models: for model in self.models:
asyncio.create_task(model.atransition_clean_locks()) asyncio.create_task(model.atransition_clean_locks())
asyncio.create_task(model.atransition_schedule_due()) asyncio.create_task(model.atransition_schedule_due())
@ -106,6 +117,18 @@ class StatorRunner:
""" """
Wrapper for atransition_attempt with fallback error handling Wrapper for atransition_attempt with fallback error handling
""" """
task_name = f"stator.run_transition:{instance._meta.label_lower}#{{id}} from {instance.state}"
with sentry.start_transaction(op="task", name=task_name):
sentry.set_context(
"instance",
{
"model": instance._meta.label_lower,
"pk": instance.pk,
"state": instance.state,
"state_age": instance.state_age,
},
)
try: try:
print( print(
f"Attempting transition on {instance._meta.label_lower}#{instance.pk} from state {instance.state}" f"Attempting transition on {instance._meta.label_lower}#{instance.pk} from state {instance.state}"

View file

@ -10,6 +10,8 @@ import sentry_sdk
from pydantic import AnyUrl, BaseSettings, EmailStr, Field, validator from pydantic import AnyUrl, BaseSettings, EmailStr, Field, validator
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from takahe import __version__
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -77,6 +79,8 @@ class Settings(BaseSettings):
#: An optional Sentry DSN for error reporting. #: An optional Sentry DSN for error reporting.
SENTRY_DSN: Optional[str] = None SENTRY_DSN: Optional[str] = None
SENTRY_SAMPLE_RATE: float = 1.0
SENTRY_TRACES_SAMPLE_RATE: float = 1.0
#: Fallback domain for links. #: Fallback domain for links.
MAIN_DOMAIN: str = "example.com" MAIN_DOMAIN: str = "example.com"
@ -150,6 +154,7 @@ INSTALLED_APPS = [
] ]
MIDDLEWARE = [ MIDDLEWARE = [
"core.middleware.SentryTaggingMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -269,10 +274,12 @@ if SETUP.SENTRY_DSN:
integrations=[ integrations=[
DjangoIntegration(), DjangoIntegration(),
], ],
traces_sample_rate=1.0, traces_sample_rate=SETUP.SENTRY_TRACES_SAMPLE_RATE,
sample_rate=SETUP.SENTRY_SAMPLE_RATE,
send_default_pii=True, send_default_pii=True,
environment=SETUP.ENVIRONMENT, environment=SETUP.ENVIRONMENT,
) )
sentry_sdk.set_tag("takahe.version", __version__)
SERVER_EMAIL = SETUP.EMAIL_FROM SERVER_EMAIL = SETUP.EMAIL_FROM
if SETUP.EMAIL_SERVER: if SETUP.EMAIL_SERVER: