diff --git a/.github/workflows/build_api/action.yml b/.github/workflows/build_api/action.yml index 6880004..dacb001 100644 --- a/.github/workflows/build_api/action.yml +++ b/.github/workflows/build_api/action.yml @@ -48,6 +48,7 @@ runs: shell: bash run: > docker build ${{ inputs.docker_build_context }} + --build-arg SENTRY_RELEASE="opensensor-api@${GITHUB_SHA}" -f ${{ inputs.dockerfile }} -t ${{ steps.image_tags.outputs.sha }} -t ${{ steps.image_tags.outputs.latest }} diff --git a/Dockerfile b/Dockerfile index cc226dd..b179bad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,4 +39,10 @@ USER opensensor-api ENV TZ=UTC # Start opensensor API +# Public Sentry ingestion key; upload credentials are never part of the image. +ARG SENTRY_RELEASE +ENV SENTRY_DSN=https://f342f997ac7c582759f18b6d46a71956@o4512337837686784.ingest.us.sentry.io/4512337894637568 \ + SENTRY_ENVIRONMENT=production \ + SENTRY_RELEASE=${SENTRY_RELEASE} + CMD ["/usr/src/.venv/bin/uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8010", "--timeout-keep-alive", "30", "--log-level", "info"] diff --git a/Pipfile b/Pipfile index a91668c..7c65dd6 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ verify_ssl = true name = "pypi" [packages] +sentry-sdk = "==2.73.0" fastapi = "*" uvicorn = "*" pydantic = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 368476c..67646ac 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b556e059a8c278493da006a0f3a3b10a26d0de3aa2c9fc6fc8e90ade8b903a4f" + "sha256": "ffef7f19f57c32d0b0be0418165207278e7136fd8c28010570e7bf12120b5150" }, "pipfile-spec": 6, "requires": { @@ -725,6 +725,15 @@ "markers": "python_version >= '3.9'", "version": "==6.2.0" }, + "sentry-sdk": { + "hashes": [ + "sha256:0bcfa6928ea5adbb89591858b514d27adac27c7a3cccc31b20ed10e0b09eda9e", + "sha256:4a2ab97b16e4693c325ab361c7bbbb346dbc7d0bc0150922d795a25bac8d5280" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==2.73.0" + }, "simplejson": { "hashes": [ "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb", @@ -898,6 +907,14 @@ "markers": "python_version >= '3.9'", "version": "==0.4.1" }, + "urllib3": { + "hashes": [ + "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", + "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63" + ], + "markers": "python_version >= '3.10'", + "version": "==2.8.0" + }, "uvicorn": { "hashes": [ "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", diff --git a/main.py b/main.py index d8d5b0a..bd2f5c0 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,9 @@ from opensensor.app import app from opensensor.collection_apis import router as collection_apis_router from opensensor.devices import router as device_apis_router +from opensensor.observability import init_sentry + +init_sentry() @app.get("/") diff --git a/opensensor/observability.py b/opensensor/observability.py new file mode 100644 index 0000000..e94258d --- /dev/null +++ b/opensensor/observability.py @@ -0,0 +1,46 @@ +"""Opt-in Sentry error and trace capture; no request payloads or local variables.""" + +import os +from urllib.parse import urlsplit, urlunsplit + +import sentry_sdk + + +def scrub_event(event, hint): + request = event.get("request") + if request: + for key in ("data", "cookies", "headers", "query_string"): + request.pop(key, None) + if request.get("url"): + parts = urlsplit(request["url"]) + request["url"] = urlunsplit((parts.scheme, parts.netloc, parts.path, "", "")) + event.pop("user", None) + event.pop("extra", None) + event["breadcrumbs"] = {"values": []} + return event + + +def init_sentry() -> None: + dsn = os.environ.get("SENTRY_DSN") + if not dsn or os.environ.get("SENTRY_ENABLED", "true").lower() == "false": + return + sentry_sdk.init( + dsn=dsn, + environment=os.environ.get("SENTRY_ENVIRONMENT", "production"), + release=os.environ.get("SENTRY_RELEASE") or None, + traces_sample_rate=0.1, + data_collection={ + "user_info": False, + "cookies": {"mode": "off"}, + "http_headers": {"request": {"mode": "off"}}, + "http_bodies": [], + "url_query_params": {"mode": "off"}, + "database_query_data": False, + "queues": False, + "stack_frame_variables": False, + "gen_ai": {"inputs": False, "outputs": False}, + "graphql": {"document": False, "variables": False}, + }, + before_send=scrub_event, + before_send_transaction=scrub_event, + )