diff --git a/.gitignore b/.gitignore index 033df5fb..e31985f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv __pycache__ +.idea \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 96549796..e24efeb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2128,21 +2128,22 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "sentry-sdk" -version = "1.45.0" +version = "2.13.0" description = "Python client for Sentry (https://sentry.io)" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"}, - {file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"}, + {file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, + {file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, ] [package.dependencies] certifi = "*" -urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} +urllib3 = ">=1.26.11" [package.extras] aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] arq = ["arq (>=0.23)"] asyncpg = ["asyncpg (>=0.23)"] beam = ["apache-beam (>=2.12)"] @@ -2155,13 +2156,16 @@ django = ["django (>=1.8)"] falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] -grpcio = ["grpcio (>=1.21.1)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] +huggingface-hub = ["huggingface-hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] +opentelemetry-experimental = ["opentelemetry-distro"] pure-eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] @@ -2171,7 +2175,7 @@ sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] -tornado = ["tornado (>=5)"] +tornado = ["tornado (>=6)"] [[package]] name = "setuptools" @@ -3041,4 +3045,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "1fa57ce5e51900d0a41e2d29bce3f9741752db46dcc5402616dd4a1daddd8084" +content-hash = "5edc233402a46f291c80197a14b955d54dbc447fca48d055c80309d06883ef77" diff --git a/pyproject.toml b/pyproject.toml index eecb7428..0cbd57c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dependencies = { pydantic = "^1.10.4" } [tool.poetry.group.sentry] optional = true -dependencies = { sentry-sdk = "^1.11.0" } +dependencies = { sentry-sdk = "^2.13.0"} [tool.poe.tasks] format = [{cmd = "black ."}, {cmd = "isort ."}] diff --git a/sentry/README.md b/sentry/README.md index 45977731..d6bf2da3 100644 --- a/sentry/README.md +++ b/sentry/README.md @@ -1,6 +1,21 @@ # Sentry Sample -This sample shows how to configure [Sentry](https://sentry.io) to intercept and capture errors from the Temporal SDK. +This sample shows how to configure [Sentry](https://sentry.io) SDK (version 2) to intercept and capture errors from the Temporal SDK +for workflows and activities. The integration adds some useful context to the errors, such as the activity type, task queue, etc. + +### Further details + +This is a small modification of the original example Sentry integration in this repo based on SDK v1. The integration +didn't work properly with Sentry SDK v2 due to some internal changes in the Sentry SDK that broke the worker sandbox. +Additionally, the v1 SDK has been deprecated and is only receiving security patches and will reach EOL some time in the future. + +If you still need to use Sentry SDK v1, check the original example at this [commit](https://github.com/temporalio/samples-python/tree/7b3944926c3743bc0dcb3b781d8cc64e0330bac4/sentry). + +Sentry's `Hub` object is now deprecated in the v2 SDK in favour of scopes. See [Activating Current Hub Clone](https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#activating-current-hub-clone) +for more details. The changes are simple, just replace `with Hub(Hub.current):` with `with isolation_scope() as scope:`. +These changes resolve the sandbox issues. + +## Running the Sample For this sample, the optional `sentry` dependency group must be included. To include, run: @@ -16,4 +31,11 @@ This will start the worker. Then, in another terminal, run the following to exec poetry run python starter.py The workflow should complete with the hello result. If you alter the workflow or the activity to raise an -`ApplicationError` instead, it should appear in Sentry. \ No newline at end of file +`ApplicationError` instead, it should appear in Sentry. + +## Screenshot + +The screenshot below shows the extra tags and context included in the +Sentry error from the exception thrown in the activity. + +![Sentry screenshot](images/sentry.jpeg) \ No newline at end of file diff --git a/sentry/images/sentry.jpeg b/sentry/images/sentry.jpeg new file mode 100644 index 00000000..0f62825b Binary files /dev/null and b/sentry/images/sentry.jpeg differ diff --git a/sentry/interceptor.py b/sentry/interceptor.py index f1737ed2..b3595045 100644 --- a/sentry/interceptor.py +++ b/sentry/interceptor.py @@ -12,59 +12,63 @@ ) with workflow.unsafe.imports_passed_through(): - from sentry_sdk import Hub, capture_exception, set_context, set_tag + from sentry_sdk import Scope, isolation_scope -def _set_common_workflow_tags(info: Union[workflow.Info, activity.Info]): - set_tag("temporal.workflow.type", info.workflow_type) - set_tag("temporal.workflow.id", info.workflow_id) +def _set_common_workflow_tags(scope: Scope, info: Union[workflow.Info, activity.Info]): + scope.set_tag("temporal.workflow.type", info.workflow_type) + scope.set_tag("temporal.workflow.id", info.workflow_id) class _SentryActivityInboundInterceptor(ActivityInboundInterceptor): async def execute_activity(self, input: ExecuteActivityInput) -> Any: # https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues - with Hub(Hub.current): - set_tag("temporal.execution_type", "activity") - set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__) + with isolation_scope() as scope: + scope.set_tag("temporal.execution_type", "activity") + scope.set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__) activity_info = activity.info() - _set_common_workflow_tags(activity_info) - set_tag("temporal.activity.id", activity_info.activity_id) - set_tag("temporal.activity.type", activity_info.activity_type) - set_tag("temporal.activity.task_queue", activity_info.task_queue) - set_tag("temporal.workflow.namespace", activity_info.workflow_namespace) - set_tag("temporal.workflow.run_id", activity_info.workflow_run_id) + _set_common_workflow_tags(scope, activity_info) + scope.set_tag("temporal.activity.id", activity_info.activity_id) + scope.set_tag("temporal.activity.type", activity_info.activity_type) + scope.set_tag("temporal.activity.task_queue", activity_info.task_queue) + scope.set_tag( + "temporal.workflow.namespace", activity_info.workflow_namespace + ) + scope.set_tag("temporal.workflow.run_id", activity_info.workflow_run_id) try: return await super().execute_activity(input) except Exception as e: if len(input.args) == 1 and is_dataclass(input.args[0]): - set_context("temporal.activity.input", asdict(input.args[0])) - set_context("temporal.activity.info", activity.info().__dict__) - capture_exception() + scope.set_context("temporal.activity.input", asdict(input.args[0])) + scope.set_context("temporal.activity.info", activity.info().__dict__) + scope.capture_exception() raise e class _SentryWorkflowInterceptor(WorkflowInboundInterceptor): async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: # https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues - with Hub(Hub.current): - set_tag("temporal.execution_type", "workflow") - set_tag("module", input.run_fn.__module__ + "." + input.run_fn.__qualname__) + with isolation_scope() as scope: + scope.set_tag("temporal.execution_type", "workflow") + scope.set_tag( + "module", input.run_fn.__module__ + "." + input.run_fn.__qualname__ + ) workflow_info = workflow.info() - _set_common_workflow_tags(workflow_info) - set_tag("temporal.workflow.task_queue", workflow_info.task_queue) - set_tag("temporal.workflow.namespace", workflow_info.namespace) - set_tag("temporal.workflow.run_id", workflow_info.run_id) + _set_common_workflow_tags(scope, workflow_info) + scope.set_tag("temporal.workflow.task_queue", workflow_info.task_queue) + scope.set_tag("temporal.workflow.namespace", workflow_info.namespace) + scope.set_tag("temporal.workflow.run_id", workflow_info.run_id) try: return await super().execute_workflow(input) except Exception as e: if len(input.args) == 1 and is_dataclass(input.args[0]): - set_context("temporal.workflow.input", asdict(input.args[0])) - set_context("temporal.workflow.info", workflow.info().__dict__) + scope.set_context("temporal.workflow.input", asdict(input.args[0])) + scope.set_context("temporal.workflow.info", workflow.info().__dict__) if not workflow.unsafe.is_replaying(): with workflow.unsafe.sandbox_unrestricted(): - capture_exception() + scope.capture_exception() raise e diff --git a/sentry/starter.py b/sentry/starter.py index 9d0a0dc7..283f186f 100644 --- a/sentry/starter.py +++ b/sentry/starter.py @@ -1,5 +1,4 @@ import asyncio -import os from temporalio.client import Client diff --git a/sentry/worker.py b/sentry/worker.py index 1db0826b..fa6e5431 100644 --- a/sentry/worker.py +++ b/sentry/worker.py @@ -1,15 +1,17 @@ import asyncio -import logging import os +import random from dataclasses import dataclass from datetime import timedelta -import sentry_sdk from temporalio import activity, workflow from temporalio.client import Client from temporalio.worker import Worker -from sentry.interceptor import SentryInterceptor +with workflow.unsafe.imports_passed_through(): + import sentry_sdk + + from sentry.interceptor import SentryInterceptor @dataclass @@ -21,6 +23,8 @@ class ComposeGreetingInput: @activity.defn async def compose_greeting(input: ComposeGreetingInput) -> str: activity.logger.info("Running activity with parameter %s" % input) + if random.random() < 0.9: + raise Exception("Activity failed!") return f"{input.greeting}, {input.name}!"