Notes on Working with the Sentry Source Code

Sentry basics

Steps to self-host Sentry

Sentry is a server-side logging platform. The basic setup flow (Python environment; see the official docs for Docker) is:

  1. Install Redis and PostgreSQL locally.
  2. Initialize and start both databases.
  3. Create a Python virtualenv (everything in this post happens inside one).
  4. pip install sentry
  5. Initialize config: sentry init /etc/sentry (choose any path; Sentry defaults to ~/.sentry).
  6. Update the config files, especially database settings.
  7. Run the database migrations.
  8. Start the web, worker, and cron services with the sentry CLI.

Once the instance is up, open the web UI, create a project, and follow the instructions to send events—typically via the Raven SDK for Python.

Architecture

Sentry consists of:

  • Server-side services:
    • web: Django + uWSGI handling HTTP traffic
    • worker: Celery workers for asynchronous tasks (database I/O, etc.)
    • cron: scheduled jobs
  • SDKs: language-specific clients (Raven for Python, etc.).
  • APIs: REST endpoints for reading and writing events.

Development environment

These notes assume CentOS 7.

Runtime dependencies

Clone the source:

git clone https://github.com/getsentry/sentry

Install dependencies much like the self-host flow, plus the development requirements:

pip install -r requirements-base.txt

Entry point

setup.py defines the console script:

1
2
3
4
entry_points={
'console_scripts': [
'sentry = sentry.runner:main',
]

main lives in sentry/runner/__init__.py:

1
2
def main():
cli(prog_name=get_prog(), obj={}, max_content_width=100)

The CLI itself uses click:

1
2
3
4
5
6
7
8
@click.group()
@click.option('--config', default='', envvar='SENTRY_CONF', help='Path to configuration files.', metavar='PATH')
@click.version_option(version=version_string)
@click.pass_context
def cli(ctx, config):
if config:
os.environ['SENTRY_CONF'] = config
os.environ.setdefault('SENTRY_CONF', '~/.sentry')

Each subcommand eventually calls into specific handlers. For example, sentry run web maps to sentry.runner.commands.run:web:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@run.command()
@click.option('--bind', '-b', default=None, help='Bind address.', type=Address)
@click.option('--workers', '-w', default=0, help='The number of worker processes for handling requests.')
@click.option('--upgrade', default=False, is_flag=True, help='Upgrade before starting.')
@click.option('--with-lock', default=False, is_flag=True, help='Use a lock if performing an upgrade.')
@click.option('--noinput', default=False, is_flag=True, help='Do not prompt the user for input of any kind.')
@log_options()
@configuration
def web(bind, workers, upgrade, with_lock, noinput):
"Run web service."
if upgrade:
click.echo('Performing upgrade before service startup...')
from sentry.runner import call_command
try:
call_command(
'sentry.runner.commands.upgrade.upgrade',
verbosity=0,
noinput=noinput,
lock=with_lock,
)
except click.ClickException:
if with_lock:
click.echo('!! Upgrade currently running from another process, skipping.', err=True)

…and so on.

Working with API endpoints

Take group_details.py as an example: when deleting an issue it does the following (simplified):

  1. Validate access and locate the group.
  2. Mark the group’s status as pending deletion.
  3. Capture the previous status in an audit entry.
  4. Queue the actual deletion via delete_group.apply_async with a one-hour countdown.
  5. Log the deletion.
  6. Return HTTP 202.

Actual deletion happens in Celery workers, not in the request thread.

Adding a new endpoint

Suppose we want to delete a single Event. Two complications:

  • The Event model (sentry_message) has no status column.
  • There is no ready-made delete_event helper.

To keep changes minimal we can ship a synchronous delete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# sentry.api.endpoints.project_event_details.ProjectEventDetailsEndpoint

class ProjectEventDetailsEndpoint(ProjectEndpoint):
......
def delete(self, request, project, event_id):
"""
Remove an Event
"""
try:
event = Event.objects.get(
event_id=event_id,
project_id=project.id,
)
event.delete()
except Event.DoesNotExist:
return Response({'detail': 'Event not found'}, status=404)

transaction_id = uuid4().hex
self.create_audit_entry(
request=request,
organization_id=project.organization_id if project else None,
target_object=event.id,
transaction_id=transaction_id,
)

delete_logger.info(
'object.delete.queued',
extra={
'object_id': event.id,
'transaction_id': transaction_id,
'model': type(event).__name__,
}
)
return Response(status=202)

See https://github.com/chroming/sentry for a complete example.

Running the source

If you invoke sentry from the shell it uses the installed package, not your checkout. Installing from source is one option, but during development it is helpful to run directly.

Create a helper script:

1
2
3
4
5
6
7
8
# run_server.py
from sentry.runner.commands.run import web

def main():
web.main()

if __name__ == '__main__':
main()

This still uses uWSGI, so breakpoints do not work. For debugging APIs, run a simple WSGI server instead:

1
2
3
4
5
6
7
8
9
10
# run_simple_server.py
from wsgiref import simple_server
from sentry.wsgi import application

def main():
server = simple_server.make_server('', 9000, application)
server.serve_forever()

if __name__ == '__main__':
main()

Static assets won’t load, but you can debug API requests comfortably.

Installing back into the environment

Once the changes are ready, install them.

Prerequisites:

sudo yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel python-devel gcc-c++

sudo yum install npm

If you are modifying a 9.0.x branch, adjust requirements-base.txt:

Change redis-py-cluster>=1.3.4,<1.4.0 to redis-py-cluster==1.3.4 to avoid a version conflict with the redis package (Sentry pins redis<=1.3.5, while newer redis-py-cluster requires redis>=2.10.6).

Install in editable mode:

pip install --editable .

You can now use the sentry command as usual.