Skip to content

Commit

Permalink
feat(api): integrate Sentry for error tracking and monitoring (#171)
Browse files Browse the repository at this point in the history
This pull request introduces several changes to integrate Sentry for
error monitoring and improve environment configuration to the API. The
key changes include adding Sentry DSN variables, updating Docker and
FastAPI configurations, and creating tests for the new settings.

### Sentry Integration:

*
[`.env.example`](diffhunk://#diff-a3046da0d15a27e89f2afe639b25748a7ad4d9290af3e7b1b6c1a5533c8f0a8cR13-R16):
Added `SENTRY_DSN_API` and `SENTRY_DSN_APP` environment variables for
Sentry integration.
*
[`api/requirements.txt`](diffhunk://#diff-f380e1392f38d13c3831f35c9f505277739b484c035bc791c21cc6d081845607R8):
Added `sentry-sdk[fastapi]==2.19.0` to dependencies.
*
[`api/src/app.py`](diffhunk://#diff-f5e838a574b5fb82ba7600e7be68894c5c9f6a24cae7c0b04fd7ae57139afa17R2-R21):
Initialized Sentry SDK with DSN and environment settings.
*
[`api/src/config.py`](diffhunk://#diff-7df7ccee5a6672bf04f67eebb5964559fbbf239d77f594c8756ba3110e56fae0R1-R26):
Added `SENTRY_DSN` and `APP_ENV` settings to the configuration.
[[1]](diffhunk://#diff-7df7ccee5a6672bf04f67eebb5964559fbbf239d77f594c8756ba3110e56fae0R1-R26)
[[2]](diffhunk://#diff-7df7ccee5a6672bf04f67eebb5964559fbbf239d77f594c8756ba3110e56fae0R58-R59)
*
[`compose.yml`](diffhunk://#diff-3493e6b5ddf34891e572f911db893efd9e46af828e011ea778a7c1eb64763588L82-R87):
Added `SENTRY_DSN` to the environment variables.

### Environment Configuration:

*
[`.env.example`](diffhunk://#diff-a3046da0d15a27e89f2afe639b25748a7ad4d9290af3e7b1b6c1a5533c8f0a8cL3-L4):
Removed the `ENV` variable.
*
[`api/Dockerfile`](diffhunk://#diff-21ee93e31c9cec7a5f33b680622da377c451b62b6c44eac6d9550eade41beb47R24):
Added `APP_ENV` environment variable for different stages (development,
test, production).
[[1]](diffhunk://#diff-21ee93e31c9cec7a5f33b680622da377c451b62b6c44eac6d9550eade41beb47R24)
[[2]](diffhunk://#diff-21ee93e31c9cec7a5f33b680622da377c451b62b6c44eac6d9550eade41beb47R33-R38)

### CI/CD Integration:

*
[`.github/workflows/main.yml`](diffhunk://#diff-7829468e86c1cc5d5133195b5cb48e1ff6c75e3e9203777f6b2e379d9e4882b3R156-R166):
Added a step to release to Sentry using GitHub Actions.

### Testing:

*
[`api/tests/test_config.py`](diffhunk://#diff-9c77b4f9a6f75032e644de8b5d501ca971379aaf4f4214f4f6e4b881959b8f00R1-R47):
Added tests for the new settings, including scenarios with and without
passwords and handling non-existent password files.
  • Loading branch information
0x1026 authored Dec 4, 2024
2 parents 2db7974 + e33a66e commit 517c426
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 9 deletions.
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
APP_NAME=Urban Tree 5.0

ENV=development

#* Database

DB_NAME=urbantree
Expand All @@ -12,3 +10,7 @@ DB_ROOT_PASS="J0¿H.MXA;'J6!XE"
# If you are using docker-compose, you can ignore the following variables
DB_HOST=database
DB_PORT=3306

#* Sentry
SENTRY_DSN_API=https://{PUBLIC_KEY}@{HOST}.ingest.de.sentry.io/{PROJECT_ID}
SENTRY_DSN_APP=https://{PUBLIC_KEY}@{HOST}.ingest.de.sentry.io/{PROJECT_ID}
11 changes: 11 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ jobs:
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

# https://github.com/getsentry/action-release/tree/e769183448303de84c5a06aaaddf9da7be26d6c7
- name: 📦 Release to Sentry
uses: getsentry/action-release@e769183448303de84c5a06aaaddf9da7be26d6c7
if: ${{ github.event_name != 'pull_request' }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
with:
projects: api
environment: production

deploy:
name: 🚀 Deploy to production
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install -r requirements-dev.txt

FROM dev-deps AS development
ENV APP_ENV=development
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
Expand All @@ -29,10 +30,12 @@ EXPOSE 8000
CMD ["python3", "-m", "uvicorn", "src.app:app", "--host=0.0.0.0", "--port=8000"]

FROM development AS test
ENV APP_ENV=test
WORKDIR /app
CMD ["pytest", "tests"]

FROM prod-deps AS final
ENV APP_ENV=production
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
Expand Down
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pydantic-settings==2.3.4
uvicorn==0.30.1
sqlmodel==0.0.19
databases[mysql]
sentry-sdk[fastapi]==2.19.0
15 changes: 15 additions & 0 deletions api/src/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
from fastapi import FastAPI
import sentry_sdk
from sqlmodel import Session, SQLModel, create_engine
from src.config import settings
from src.services.sensor_service import insert_data
from src.utils.file_loader import load_sensor_data

sentry_sdk.init(
dsn=settings.SENTRY_DSN,
environment=settings.APP_ENV,
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for tracing.
traces_sample_rate=1.0,
_experiments={
# Set continuous_profiling_auto_start to True
# to automatically start the profiler on when
# possible.
"continuous_profiling_auto_start": True,
},
)

engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))

def create_db_and_tables():
Expand Down
15 changes: 9 additions & 6 deletions api/src/config.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
import os

from typing import Any

from pydantic import (
MariaDBDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings, SettingsConfigDict

from typing import Any

class Settings(BaseSettings):
if os.path.exists('/run/secrets'):
model_config = SettingsConfigDict(secrets_dir='/run/secrets')

APP_ENV: str = "development"

MARIADB_SERVER: str
MARIADB_PORT: int = 3306
MARIADB_USER: str
MARIADB_PASSWORD: str | None = None
MARIADB_PASSWORD_FILE: str | None = None
MARIADB_DB: str

SENTRY_DSN: str

@model_validator(mode="before")
@classmethod
def check_mariadb_password(cls, data: Any) -> Any:
Expand Down Expand Up @@ -54,4 +55,6 @@ def SQLALCHEMY_DATABASE_URI(self) -> MariaDBDsn:
path=self.MARIADB_DB,
)

settings = Settings()
# instantiate the settings object if APP_ENV is production
if os.environ.get("APP_ENV") == "production":
settings = Settings()
47 changes: 47 additions & 0 deletions api/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import pytest
from pydantic import ValidationError
from src.config import Settings

def test_settings_with_password():
settings = Settings(
MARIADB_SERVER="localhost",
MARIADB_USER="user",
MARIADB_PASSWORD="password",
MARIADB_DB="test_db",
SENTRY_DSN="http://example.com"
)
assert settings.MARIADB_PASSWORD == "password"
assert str(settings.SQLALCHEMY_DATABASE_URI) == "mysql+pymysql://user:password@localhost:3306/test_db"

def test_settings_with_password_file(tmp_path):
password_file = tmp_path / "password.txt"
password_file.write_text("file_password")

settings = Settings(
MARIADB_SERVER="localhost",
MARIADB_USER="user",
MARIADB_PASSWORD_FILE=str(password_file),
MARIADB_DB="test_db",
SENTRY_DSN="http://example.com"
)
assert settings.MARIADB_PASSWORD_FILE == "file_password"
assert str(settings.SQLALCHEMY_DATABASE_URI) == "mysql+pymysql://user:file_password@localhost:3306/test_db"

def test_settings_missing_password():
with pytest.raises(ValidationError):
Settings(
MARIADB_SERVER="localhost",
MARIADB_USER="user",
MARIADB_DB="test_db",
SENTRY_DSN="http://example.com"
)

def test_password_file_does_not_exist():
with pytest.raises(ValueError, match="Password file /non/existent/path does not exist."):
Settings(
MARIADB_SERVER="localhost",
MARIADB_USER="user",
MARIADB_PASSWORD_FILE="/non/existent/path",
MARIADB_DB="test_db",
SENTRY_DSN="http://example.com"
)
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ services:
- db_pass
environment:
- APP_NAME=${APP_NAME}
- APP_ENV=${APP_ENV}
- MARIADB_SERVER=database
- MARIADB_PORT=3306
- MARIADB_DB=${DB_NAME}
- MARIADB_USER=${DB_USER}
- MARIADB_PASSWORD_FILE=/run/secrets/db_pass
- SENTRY_DSN=${SENTRY_DSN_API}
develop:
watch:
- action: rebuild
Expand Down

0 comments on commit 517c426

Please sign in to comment.