Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add example using Sentry V2 SDK #140

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.venv
__pycache__
.idea
22 changes: 13 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ."}]
Expand Down
26 changes: 24 additions & 2 deletions sentry/README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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.
`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)
Binary file added sentry/images/sentry.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 30 additions & 26 deletions sentry/interceptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
1 change: 0 additions & 1 deletion sentry/starter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import os

from temporalio.client import Client

Expand Down
10 changes: 7 additions & 3 deletions sentry/worker.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}!"


Expand Down