diff options
| -rw-r--r-- | .coveragerc | 13 | ||||
| -rw-r--r-- | .dockerignore | 9 | ||||
| -rw-r--r-- | .github/workflows/build.yaml | 24 | ||||
| -rw-r--r-- | .github/workflows/deploy.yaml | 15 | ||||
| -rw-r--r-- | .github/workflows/main.yaml | 4 | ||||
| -rw-r--r-- | .github/workflows/test.yaml | 9 | ||||
| -rw-r--r-- | Dockerfile | 21 | ||||
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | config/gunicorn.conf.py | 3 | ||||
| -rw-r--r-- | docker-compose.yml | 3 | ||||
| -rw-r--r-- | pyproject.toml | 66 | ||||
| -rw-r--r-- | requirements/requirements.in | 7 | ||||
| -rw-r--r-- | requirements/requirements.pip | 12 | ||||
| -rw-r--r-- | scripts/version.py | 38 | ||||
| -rw-r--r-- | snekbox/__init__.py | 55 | ||||
| -rw-r--r-- | snekbox/__main__.py | 2 | ||||
| -rw-r--r-- | snekbox/api/app.py | 3 | ||||
| -rw-r--r-- | snekbox/api/resources/eval.py | 2 | ||||
| -rw-r--r-- | snekbox/nsjail.py | 2 | ||||
| -rw-r--r-- | snekbox/utils/__init__.py | 4 | ||||
| -rw-r--r-- | snekbox/utils/cgroup.py | 2 | ||||
| -rw-r--r-- | snekbox/utils/gunicorn.py | 33 | ||||
| -rw-r--r-- | snekbox/utils/logging.py | 35 | ||||
| -rw-r--r-- | snekbox/utils/swap.py | 2 | ||||
| -rw-r--r-- | tests/gunicorn_utils.py | 4 | 
25 files changed, 255 insertions, 119 deletions
| diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ce475d7..0000000 --- a/.coveragerc +++ /dev/null @@ -1,13 +0,0 @@ -[run] -branch = true -data_file = ${COVERAGE_DATAFILE-.coverage} -include = snekbox/* -omit = -    snekbox/api/app.py -    snekbox/config_pb2.py -relative_files = true - -[report] -exclude_lines = -    pragma: no cover -    if DEBUG diff --git a/.dockerignore b/.dockerignore index 30ecfd4..6a360ff 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,13 @@  *  # Make exceptions for what's needed -!snekbox +!.git/  !config/  !requirements/ -!tests +!scripts/ +!snekbox/ +!tests/  !LICENSE +!NOTICE +!pyproject.toml +!README.md diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 28e5b69..e5791c9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,9 +4,9 @@ on:        artifact:          description: The name of the uploaded image aretfact.          value: ${{ jobs.build.outputs.artifact }} -      tag: -        description: The tag used for the built image. -        value: ${{ jobs.build.outputs.tag }} +      version: +        description: The package's version. +        value: ${{ jobs.build.outputs.version }}  jobs:    build: @@ -14,21 +14,21 @@ jobs:      runs-on: ubuntu-latest      outputs:        artifact: ${{ env.artifact }} -      tag: ${{ steps.sha_tag.outputs.tag }} +      version: ${{ steps.version.outputs.version }}      env:        artifact: image_artifact_snekbox-venv      steps: -      # Create a short SHA with which to tag built images. -      - name: Create SHA Container Tag -        id: sha_tag -        run: | -          tag=$(cut -c 1-7 <<< $GITHUB_SHA) -          echo "::set-output name=tag::$tag" -        - name: Checkout code          uses: actions/checkout@v2 +      - name: Get version +        id: version +        run: | +          set -eu +          version=$(python scripts/version.py) +          echo "::set-output name=version::version" +        # The current version (v2) of Docker's build-push action uses buildx,        # which comes with BuildKit. It has cache features which can speed up        # the builds. See https://github.com/docker/build-push-action @@ -83,7 +83,7 @@ jobs:              ghcr.io/python-discord/snekbox-base:latest              ghcr.io/python-discord/snekbox-venv:latest            cache-to: ${{ steps.cache_config.outputs.cache_to }} -          tags: ghcr.io/python-discord/snekbox-venv:${{ steps.sha_tag.outputs.tag }} +          tags: ghcr.io/python-discord/snekbox-venv:${{ steps.version.outputs.version }}        # Make the image available as an artifact so other jobs will be able to        # download it. diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3b12921..9113188 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,7 +4,7 @@ on:        artifact:          required: true          type: string -      tag: +      version:          required: true          type: string      secrets: @@ -55,8 +55,7 @@ jobs:            cache-to: type=inline            tags: |              ghcr.io/python-discord/snekbox:latest -            ghcr.io/python-discord/snekbox:${{ inputs.tag }} -          build-args: git_sha=${{ github.sha }} +            ghcr.io/python-discord/snekbox:${{ inputs.version }}        # Deploy to Kubernetes.        - name: Authenticate with Kubernetes @@ -69,7 +68,7 @@ jobs:          uses: Azure/k8s-deploy@v1          with:            manifests: deployment.yaml -          images: 'ghcr.io/python-discord/snekbox:${{ inputs.tag }}' +          images: 'ghcr.io/python-discord/snekbox:${{ inputs.version }}'            kubectl-version: 'latest'        # Push the base image to GHCR, with an inline cache manifest. @@ -82,7 +81,9 @@ jobs:            push: true            cache-from: ghcr.io/python-discord/snekbox-base:latest            cache-to: type=inline -          tags: ghcr.io/python-discord/snekbox-base:latest +          tags: | +            ghcr.io/python-discord/snekbox-base:latest +            ghcr.io/python-discord/snekbox-base:${{ inputs.version }}        # Push the venv image to GHCR, with an inline cache manifest.        - name: Push venv image @@ -96,4 +97,6 @@ jobs:              ghcr.io/python-discord/snekbox-base:latest              ghcr.io/python-discord/snekbox-venv:latest            cache-to: type=inline -          tags: ghcr.io/python-discord/snekbox-venv:latest +          tags: | +            ghcr.io/python-discord/snekbox-venv:latest +            ghcr.io/python-discord/snekbox-venv:${{ inputs.version }} diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f28ab61..b581ba3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,13 +16,13 @@ jobs:      needs: build      with:        artifact: ${{ needs.build.outputs.artifact }} -      tag: ${{ needs.build.outputs.tag }} +      version: ${{ needs.build.outputs.version }}    deploy:      uses: ./.github/workflows/deploy.yaml      if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}      needs: [build, lint, test]      with:        artifact: ${{ needs.build.outputs.artifact }} -      tag: ${{ needs.build.outputs.tag }} +      version: ${{ needs.build.outputs.version }}      secrets:        KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f7f3635..30e6ba3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ on:        artifact:          required: true          type: string -      tag: +      version:          required: true          type: string @@ -38,11 +38,12 @@ jobs:        - name: Run tests          id: run_tests          run: | -          export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' +          export IMAGE_SUFFIX='-venv:${{ inputs.version }}'            docker-compose run \              --rm -T -e COVERAGE_DATAFILE=.coverage.${{ matrix.os }} \ +            --entrypoint coverage \              snekbox \ -            coverage run -m unittest +            run -m unittest        # Upload it so the coverage from all matrix jobs can be combined later.        - name: Upload coverage data @@ -57,7 +58,7 @@ jobs:        - name: Docker cleanup          if: matrix.os == 'self-hosted' && always()          run: | -          export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' +          export IMAGE_SUFFIX='-venv:${{ inputs.version }}'            docker-compose down --rmi all --remove-orphans -v -t 0    report: @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1  FROM python:3.10-slim-buster as builder  WORKDIR /nsjail @@ -31,6 +32,7 @@ ENV PATH=/root/.local/bin:$PATH \  RUN apt-get -y update \      && apt-get install -y \          gcc=4:8.3.* \ +        git=1:2.20.* \          libnl-route-3-200=3.4.* \          libprotobuf17=3.6.* \      && rm -rf /var/lib/apt/lists/* @@ -60,18 +62,17 @@ RUN if [ -n "${DEV}" ]; \      fi  # At the end to avoid re-installing dependencies when only a config changes. -# It's in the venv image because the final image is not used during development.  COPY config/ /snekbox/config/ -# ------------------------------------------------------------------------------ -FROM venv -  ENTRYPOINT ["gunicorn"] -CMD ["-c", "config/gunicorn.conf.py", "snekbox.api.app"] +CMD ["-c", "config/gunicorn.conf.py"] -COPY . /snekbox -WORKDIR /snekbox +# ------------------------------------------------------------------------------ +FROM venv -# At the end to prevent it from invalidating the layer cache. -ARG git_sha="development" -ENV GIT_SHA=$git_sha +# Use a separate directory to avoid importing the source over the installed pkg. +# The venv already installed dependencies, so nothing besides snekbox itself +# will be installed. Note requirements.pip cannot be used as a constraint file +# because it contains extras, which pip disallows. +RUN --mount=source=.,target=/snekbox_src,rw \ +    pip install /snekbox_src[gunicorn,sentry] \ @@ -13,7 +13,8 @@ setup: install-piptools  .PHONY: upgrade  upgrade: install-piptools -	$(PIP_COMPILE_CMD) -o requirements/requirements.pip requirements/requirements.in +	$(PIP_COMPILE_CMD) -o requirements/requirements.pip \ +		--extra gunicorn --extra sentry pyproject.toml  	$(PIP_COMPILE_CMD) -o requirements/coverage.pip requirements/coverage.in  	$(PIP_COMPILE_CMD) -o requirements/coveralls.pip requirements/coveralls.in  	$(PIP_COMPILE_CMD) -o requirements/lint.pip requirements/lint.in @@ -24,12 +25,11 @@ lint: setup  	pre-commit run --all-files  # Fix ownership of the coverage file even if tests fail & preserve exit code -# Install numpy because a test checks if it's importable  .PHONY: test  test:  	docker-compose build -q --force-rm  	docker-compose run --entrypoint /bin/bash --rm snekbox -c \ -    'coverage run -m unittest; e=$?; chown --reference=. .coverage; exit $e' +    	'coverage run -m unittest; e=$?; chown --reference=. .coverage; exit $e'  .PHONY: report  report: setup diff --git a/config/gunicorn.conf.py b/config/gunicorn.conf.py index 5ab11f4..563f8ea 100644 --- a/config/gunicorn.conf.py +++ b/config/gunicorn.conf.py @@ -1,5 +1,6 @@  workers = 2  bind = "0.0.0.0:8060" -logger_class = "snekbox.GunicornLogger" +logger_class = "snekbox.utils.gunicorn.GunicornLogger"  access_logformat = "%(m)s %(U)s%(q)s %(s)s %(b)s %(L)ss"  access_logfile = "-" +wsgi_app = "snekbox:SnekAPI" diff --git a/docker-compose.yml b/docker-compose.yml index 3854825..aa1a0f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services:      container_name: snekbox_dev      hostname: snekbox_dev      privileged: true -    image: ghcr.io/python-discord/snekbox${IMAGE_SUFFIX:-:dev} +    image: ghcr.io/python-discord/snekbox${IMAGE_SUFFIX:--venv:dev}      ports:       - 8060:8060      init: true @@ -17,6 +17,7 @@ services:      build:        context: .        dockerfile: Dockerfile +      target: venv        args:          DEV: 1        cache_from: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..08aeb8c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["setuptools>=61", "setuptools-git-versioning>=1.8"] +build-backend = "setuptools.build_meta:__legacy__" + +[project] +name = "snekbox" +description = "HTTP REST API for sandboxed execution of arbitrary Python code." +readme = "README.md" +license = {text = "MIT"} +authors = [{name = "Python Discord", email = "[email protected]"}] +keywords = ["sandbox", "nsjail", "HTTP REST API"] +classifiers = [ +    "Development Status :: 5 - Production/Stable", +    "Environment :: Console", +    "Intended Audience :: Developers", +    "License :: OSI Approved :: MIT License", +    "Operating System :: POSIX :: Linux", +    "Programming Language :: Python :: 3 :: Only", +    "Programming Language :: Python :: 3.10", +    "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +    "Topic :: Security", +    "Topic :: Software Development :: Interpreters", +] +dynamic = ["version"] + +requires-python = ">=3.10" +dependencies = [ +    # Sentry's Falcon integration relies on api_helpers (falconry/falcon#1902). +    "falcon>=3.0.1", +    "jsonschema>=4.0", +    "protobuf>=3.19", +] + +[project.optional-dependencies] +gunicorn = ["gunicorn>=20.1"]  # Lowest which supports wsgi_app in config. +sentry = ["sentry-sdk[falcon]>=1.5.4"] + +[project.urls] +source = "https://github.com/python-discord/snekbox" +tracker = "https://github.com/python-discord/snekbox/issues" + +[project.scripts] +snekbox = "snekbox.__main__:main" + +[tool.setuptools.packages.find] +include = ["snekbox*"] + +[tool.setuptools-git-versioning] +enabled = true +version_callback = "scripts.version:get_version" + +[tool.coverage.report] +exclude_lines = [ +    "pragma: no cover", +    "if DEBUG" +] + +[tool.coverage.run] +branch = true +data_file = "${COVERAGE_DATAFILE-.coverage}" +include = ["snekbox/*"] +omit =  [ +    "snekbox/api/app.py", +    "snekbox/config_pb2.py" +] +relative_files = true diff --git a/requirements/requirements.in b/requirements/requirements.in deleted file mode 100644 index 775ad39..0000000 --- a/requirements/requirements.in +++ /dev/null @@ -1,7 +0,0 @@ -# Sentry's Falcon integration relies on api_helpers. See falconry/falcon#1902 -falcon>=3.0.1 - -gunicorn>=20 -jsonschema>=4.0 -protobuf>=3.19 -sentry-sdk[falcon]>=1.5.4 diff --git a/requirements/requirements.pip b/requirements/requirements.pip index 21b6678..034f104 100644 --- a/requirements/requirements.pip +++ b/requirements/requirements.pip @@ -2,7 +2,7 @@  # This file is autogenerated by pip-compile with python 3.10  # To update, run:  # -#    pip-compile --output-file=requirements/requirements.pip requirements/requirements.in +#    pip-compile --extra=gunicorn --extra=sentry --output-file=requirements/requirements.pip pyproject.toml  #  attrs==21.4.0      # via jsonschema @@ -10,18 +10,18 @@ certifi==2022.5.18.1      # via sentry-sdk  falcon==3.1.0      # via -    #   -r requirements/requirements.in      #   sentry-sdk +    #   snekbox (pyproject.toml)  gunicorn==20.1.0 -    # via -r requirements/requirements.in +    # via snekbox (pyproject.toml)  jsonschema==4.5.1 -    # via -r requirements/requirements.in +    # via snekbox (pyproject.toml)  protobuf==4.21.1 -    # via -r requirements/requirements.in +    # via snekbox (pyproject.toml)  pyrsistent==0.18.1      # via jsonschema  sentry-sdk[falcon]==1.5.12 -    # via -r requirements/requirements.in +    # via snekbox (pyproject.toml)  urllib3==1.26.9      # via sentry-sdk diff --git a/scripts/version.py b/scripts/version.py new file mode 100644 index 0000000..bf8d509 --- /dev/null +++ b/scripts/version.py @@ -0,0 +1,38 @@ +import datetime +import subprocess + +__all__ = ("get_version",) + + +def get_version() -> str: +    """ +    Return a version based on the HEAD commit's date. + +    The format is 'year.month.day.commits' and is compliant with PEP 440. 'commits' is the amount of +    commits made on the same date as HEAD, excluding HEAD. This ensures versions are unique if +    multiple release occur on the same date. +    """ +    args = ["git", "show", "-s", "--format=%ct", "HEAD"] +    stdout = subprocess.check_output(args, text=True) +    timestamp = float(stdout.strip()) +    date = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc) + +    commits = count_commits_on_date(date) - 1  # Exclude HEAD. + +    # Don't use strftime because it includes leading zeros, which are against PEP 440. +    return f"{date.year}.{date.month}.{date.day}.{commits}" + + +def count_commits_on_date(dt: datetime.datetime) -> int: +    """Return the amount of commits made on the given UTC aware datetime.""" +    dt = dt.combine(dt - datetime.timedelta(days=1), dt.max.time(), dt.tzinfo) + +    # git log uses the committer date for this, not the author date. +    args = ["git", "log", "--oneline", "--after", str(dt.timestamp())] +    stdout = subprocess.check_output(args, text=True) + +    return stdout.strip().count("\n") + + +if __name__ == "__main__": +    print(get_version()) diff --git a/snekbox/__init__.py b/snekbox/__init__.py index 42628d4..657c032 100644 --- a/snekbox/__init__.py +++ b/snekbox/__init__.py @@ -1,51 +1,18 @@ -import logging  import os -import sys - -import sentry_sdk -from gunicorn import glogging -from gunicorn.config import Config -from sentry_sdk.integrations.falcon import FalconIntegration +from importlib import metadata  DEBUG = os.environ.get("DEBUG", False) -GIT_SHA = os.environ.get("GIT_SHA", "development") - -sentry_sdk.init( -    dsn=os.environ.get("SNEKBOX_SENTRY_DSN", ""), -    integrations=[FalconIntegration()], -    send_default_pii=True, -    release=f"snekbox@{GIT_SHA}" -) - - -class GunicornLogger(glogging.Logger): -    """Logger for Gunicorn with custom formatting and support for the DEBUG environment variable.""" - -    error_fmt = "%(asctime)s | %(process)5s | %(name)30s | %(levelname)8s | %(message)s" -    access_fmt = error_fmt -    datefmt = None  # Use the default ISO 8601 format - -    def setup(self, cfg: Config) -> None: -        """ -        Set up loggers and set error logger's level to DEBUG if the DEBUG env var is set. - -        Note: Access and syslog handlers would need to be recreated to use a custom date format -        because they are created with an unspecified datefmt argument by default. -        """ -        super().setup(cfg) -        if DEBUG: -            self.loglevel = logging.DEBUG -        else: -            self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO) +try: +    __version__ = metadata.version("snekbox") +except metadata.PackageNotFoundError:  # pragma: no cover +    __version__ = "0.0.0.0+unknown" -        self.error_log.setLevel(self.loglevel) +from snekbox.api import SnekAPI  # noqa: E402 +from snekbox.nsjail import NsJail  # noqa: E402 +from snekbox.utils.logging import init_logger, init_sentry  # noqa: E402 +__all__ = ("NsJail", "SnekAPI") -log = logging.getLogger("snekbox") -log.setLevel(logging.DEBUG if DEBUG else logging.INFO) -log.propagate = True -formatter = logging.Formatter(GunicornLogger.error_fmt) -handler = logging.StreamHandler(sys.stdout) -handler.setFormatter(formatter) -log.addHandler(handler) +init_sentry(__version__) +init_logger(DEBUG) diff --git a/snekbox/__main__.py b/snekbox/__main__.py index 704ec9d..7ac10e9 100644 --- a/snekbox/__main__.py +++ b/snekbox/__main__.py @@ -1,7 +1,7 @@  import argparse  import sys -from snekbox.nsjail import NsJail +from snekbox import NsJail  def parse_args() -> argparse.Namespace: diff --git a/snekbox/api/app.py b/snekbox/api/app.py deleted file mode 100644 index c71e246..0000000 --- a/snekbox/api/app.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import SnekAPI - -application = SnekAPI() diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py index 9560d0b..0a59f2e 100644 --- a/snekbox/api/resources/eval.py +++ b/snekbox/api/resources/eval.py @@ -5,6 +5,8 @@ from falcon.media.validators.jsonschema import validate  from snekbox.nsjail import NsJail +__all__ = ("EvalResource",) +  log = logging.getLogger(__name__) diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index ac36551..1aca637 100644 --- a/snekbox/nsjail.py +++ b/snekbox/nsjail.py @@ -13,6 +13,8 @@ from google.protobuf import text_format  from snekbox import DEBUG, utils  from snekbox.config_pb2 import NsJailConfig +__all__ = ("NsJail",) +  log = logging.getLogger(__name__)  # [level][timestamp][PID]? function_signature:line_no? message diff --git a/snekbox/utils/__init__.py b/snekbox/utils/__init__.py index 5a7b632..6d6bc32 100644 --- a/snekbox/utils/__init__.py +++ b/snekbox/utils/__init__.py @@ -1,3 +1,3 @@ -from . import cgroup, swap +from . import cgroup, logging, swap -__all__ = ("cgroup", "swap") +__all__ = ("cgroup", "logging", "swap") diff --git a/snekbox/utils/cgroup.py b/snekbox/utils/cgroup.py index 3e12406..cd515ab 100644 --- a/snekbox/utils/cgroup.py +++ b/snekbox/utils/cgroup.py @@ -5,6 +5,8 @@ from snekbox.config_pb2 import NsJailConfig  log = logging.getLogger(__name__) +__all__ = ("get_version", "init", "init_v1", "init_v2") +  def get_version(config: NsJailConfig) -> int:      """ diff --git a/snekbox/utils/gunicorn.py b/snekbox/utils/gunicorn.py new file mode 100644 index 0000000..68e3ed9 --- /dev/null +++ b/snekbox/utils/gunicorn.py @@ -0,0 +1,33 @@ +import logging + +from gunicorn import glogging +from gunicorn.config import Config + +from snekbox import DEBUG +from .logging import FORMAT + +__all__ = ("GunicornLogger",) + + +class GunicornLogger(glogging.Logger): +    """Logger for Gunicorn with custom formatting and support for the DEBUG environment variable.""" + +    error_fmt = FORMAT +    access_fmt = error_fmt +    datefmt = None  # Use the default ISO 8601 format + +    def setup(self, cfg: Config) -> None: +        """ +        Set up loggers and set error logger's level to DEBUG if the DEBUG env var is set. + +        Note: Access and syslog handlers would need to be recreated to use a custom date format +        because they are created with an unspecified datefmt argument by default. +        """ +        super().setup(cfg) + +        if DEBUG: +            self.loglevel = logging.DEBUG +        else: +            self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO) + +        self.error_log.setLevel(self.loglevel) diff --git a/snekbox/utils/logging.py b/snekbox/utils/logging.py new file mode 100644 index 0000000..c15e3f1 --- /dev/null +++ b/snekbox/utils/logging.py @@ -0,0 +1,35 @@ +import logging +import os +import sys + +__all__ = ("FORMAT", "init_logger", "init_sentry") + +FORMAT = "%(asctime)s | %(process)5s | %(name)30s | %(levelname)8s | %(message)s" + + +def init_logger(debug: bool) -> None: +    """Initialise the root logger with a handler that outputs to stdout.""" +    log = logging.getLogger("snekbox") +    log.setLevel(logging.DEBUG if debug else logging.INFO) +    log.propagate = True + +    formatter = logging.Formatter(FORMAT) +    handler = logging.StreamHandler(sys.stdout) +    handler.setFormatter(formatter) +    log.addHandler(handler) + + +def init_sentry(version: str) -> None: +    """Initialise the Sentry SDK if it's installed.""" +    try: +        import sentry_sdk +        from sentry_sdk.integrations.falcon import FalconIntegration +    except ImportError: +        return + +    sentry_sdk.init( +        dsn=os.environ.get("SNEKBOX_SENTRY_DSN", ""), +        integrations=[FalconIntegration()], +        send_default_pii=True, +        release=f"snekbox@{version}" +    ) diff --git a/snekbox/utils/swap.py b/snekbox/utils/swap.py index 3e0d0aa..6a919cb 100644 --- a/snekbox/utils/swap.py +++ b/snekbox/utils/swap.py @@ -4,6 +4,8 @@ from pathlib import Path  from snekbox.config_pb2 import NsJailConfig +__all__ = ("controller_exists", "is_enabled", "should_ignore_limit") +  log = logging.getLogger(__name__) diff --git a/tests/gunicorn_utils.py b/tests/gunicorn_utils.py index f2d9b6d..b417b1b 100644 --- a/tests/gunicorn_utils.py +++ b/tests/gunicorn_utils.py @@ -17,8 +17,8 @@ class _StandaloneApplication(Application):          pass      def load(self): -        from snekbox.api.app import application -        return application +        from snekbox.api import SnekAPI +        return SnekAPI()      def load_config(self):          for key, value in self.options.items(): | 
