[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,23 +46,32 @@ 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:
while True: with sentry.configure_scope() as scope:
# Do we need to do cleaning? while True:
if (time.monotonic() - self.last_clean) >= self.schedule_interval: # Do we need to do cleaning?
# Refresh the config if (time.monotonic() - self.last_clean) >= self.schedule_interval:
Config.system = await Config.aload_system() # Refresh the config
print(f"{self.handled} tasks processed so far") Config.system = await Config.aload_system()
print("Running cleaning and scheduling") print(f"{self.handled} tasks processed so far")
await self.run_scheduling() print("Running cleaning and scheduling")
await self.run_scheduling()
self.remove_completed_tasks() # Clear the cleaning breadcrumbs/extra for the main part of the loop
await self.fetch_and_process_tasks() sentry.scope_clear(scope)
# Are we in limited run mode? self.remove_completed_tasks()
if self.run_for and (time.monotonic() - self.started) > self.run_for: await self.fetch_and_process_tasks()
break
# Prevent busylooping # Are we in limited run mode?
await asyncio.sleep(0.5) if (
self.run_for
and (time.monotonic() - self.started) > self.run_for
):
break
# Prevent busylooping
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,10 +89,11 @@ class StatorRunner:
""" """
Do any transition cleanup tasks Do any transition cleanup tasks
""" """
for model in self.models: with sentry.start_transaction(op="task", name="stator.run_scheduling"):
asyncio.create_task(model.atransition_clean_locks()) for model in self.models:
asyncio.create_task(model.atransition_schedule_due()) asyncio.create_task(model.atransition_clean_locks())
self.last_clean = time.monotonic() asyncio.create_task(model.atransition_schedule_due())
self.last_clean = time.monotonic()
async def fetch_and_process_tasks(self): async def fetch_and_process_tasks(self):
# Calculate space left for tasks # Calculate space left for tasks
@ -106,14 +117,26 @@ class StatorRunner:
""" """
Wrapper for atransition_attempt with fallback error handling Wrapper for atransition_attempt with fallback error handling
""" """
try: task_name = f"stator.run_transition:{instance._meta.label_lower}#{{id}} from {instance.state}"
print( with sentry.start_transaction(op="task", name=task_name):
f"Attempting transition on {instance._meta.label_lower}#{instance.pk} from state {instance.state}" sentry.set_context(
"instance",
{
"model": instance._meta.label_lower,
"pk": instance.pk,
"state": instance.state,
"state_age": instance.state_age,
},
) )
await instance.atransition_attempt()
except BaseException as e: try:
await exceptions.acapture_exception(e) print(
traceback.print_exc() f"Attempting transition on {instance._meta.label_lower}#{instance.pk} from state {instance.state}"
)
await instance.atransition_attempt()
except BaseException as e:
await exceptions.acapture_exception(e)
traceback.print_exc()
def remove_completed_tasks(self): def remove_completed_tasks(self):
""" """

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: