Skip to content

Commit 101d89d

Browse files
authored
Don't set error status on otel spans for benign exceptions (#1085)
* Experiment with new tracer * Limit to only handling in our own spans * Linting * Update test_opentelemetry.py * Refactor * Formatting * Remove application error catch
1 parent 44f0c42 commit 101d89d

File tree

2 files changed

+84
-4
lines changed

2 files changed

+84
-4
lines changed

temporalio/contrib/opentelemetry.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import opentelemetry.trace
2626
import opentelemetry.trace.propagation.tracecontext
2727
import opentelemetry.util.types
28+
from opentelemetry.context import Context
29+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode, _Links
30+
from opentelemetry.util import types
2831
from typing_extensions import Protocol, TypeAlias, TypedDict
2932

3033
import temporalio.activity
@@ -34,6 +37,7 @@
3437
import temporalio.exceptions
3538
import temporalio.worker
3639
import temporalio.workflow
40+
from temporalio.exceptions import ApplicationError, ApplicationErrorCategory
3741

3842
# OpenTelemetry dynamically, lazily chooses its context implementation at
3943
# runtime. When first accessed, they use pkg_resources.iter_entry_points + load.
@@ -167,11 +171,31 @@ def _start_as_current_span(
167171
attributes: opentelemetry.util.types.Attributes,
168172
input: Optional[_InputWithHeaders] = None,
169173
kind: opentelemetry.trace.SpanKind,
174+
context: Optional[Context] = None,
170175
) -> Iterator[None]:
171-
with self.tracer.start_as_current_span(name, attributes=attributes, kind=kind):
176+
with self.tracer.start_as_current_span(
177+
name,
178+
attributes=attributes,
179+
kind=kind,
180+
context=context,
181+
set_status_on_exception=False,
182+
) as span:
172183
if input:
173184
input.headers = self._context_to_headers(input.headers)
174-
yield None
185+
try:
186+
yield None
187+
except Exception as exc:
188+
if (
189+
not isinstance(exc, ApplicationError)
190+
or exc.category != ApplicationErrorCategory.BENIGN
191+
):
192+
span.set_status(
193+
Status(
194+
status_code=StatusCode.ERROR,
195+
description=f"{type(exc).__name__}: {exc}",
196+
)
197+
)
198+
raise
175199

176200
def _completed_workflow_span(
177201
self, params: _CompletedWorkflowSpanParams
@@ -282,7 +306,7 @@ async def execute_activity(
282306
self, input: temporalio.worker.ExecuteActivityInput
283307
) -> Any:
284308
info = temporalio.activity.info()
285-
with self.root.tracer.start_as_current_span(
309+
with self.root._start_as_current_span(
286310
f"RunActivity:{info.activity_type}",
287311
context=self.root._context_from_headers(input.headers),
288312
attributes={

tests/contrib/test_opentelemetry.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33
import asyncio
44
import logging
55
import uuid
6+
from concurrent.futures import ThreadPoolExecutor
67
from dataclasses import dataclass
78
from datetime import timedelta
89
from typing import Iterable, List, Optional
910

1011
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
1112
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
1213
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
13-
from opentelemetry.trace import get_tracer
14+
from opentelemetry.trace import StatusCode, get_tracer
1415

1516
from temporalio import activity, workflow
1617
from temporalio.client import Client
1718
from temporalio.common import RetryPolicy
1819
from temporalio.contrib.opentelemetry import TracingInterceptor
1920
from temporalio.contrib.opentelemetry import workflow as otel_workflow
21+
from temporalio.exceptions import ApplicationError, ApplicationErrorCategory
2022
from temporalio.testing import WorkflowEnvironment
2123
from temporalio.worker import UnsandboxedWorkflowRunner, Worker
2224

@@ -386,6 +388,60 @@ async def test_opentelemetry_always_create_workflow_spans(client: Client):
386388
assert spans[0].name == "RunWorkflow:SimpleWorkflow"
387389

388390

391+
attempted = False
392+
393+
394+
@activity.defn
395+
def benign_activity() -> str:
396+
global attempted
397+
if attempted:
398+
return "done"
399+
attempted = True
400+
raise ApplicationError(
401+
category=ApplicationErrorCategory.BENIGN, message="Benign Error"
402+
)
403+
404+
405+
@workflow.defn
406+
class BenignWorkflow:
407+
@workflow.run
408+
async def run(self) -> str:
409+
return await workflow.execute_activity(
410+
benign_activity, schedule_to_close_timeout=timedelta(seconds=1)
411+
)
412+
413+
414+
async def test_opentelemetry_benign_exception(client: Client):
415+
# Create a tracer that has an in-memory exporter
416+
exporter = InMemorySpanExporter()
417+
provider = TracerProvider()
418+
provider.add_span_processor(SimpleSpanProcessor(exporter))
419+
tracer = get_tracer(__name__, tracer_provider=provider)
420+
421+
# Create new client with tracer interceptor
422+
client_config = client.config()
423+
client_config["interceptors"] = [TracingInterceptor(tracer)]
424+
client = Client(**client_config)
425+
426+
async with Worker(
427+
client,
428+
task_queue=f"task_queue_{uuid.uuid4()}",
429+
workflows=[BenignWorkflow],
430+
activities=[benign_activity],
431+
activity_executor=ThreadPoolExecutor(max_workers=1),
432+
) as worker:
433+
assert "done" == await client.execute_workflow(
434+
BenignWorkflow.run,
435+
id=f"workflow_{uuid.uuid4()}",
436+
task_queue=worker.task_queue,
437+
retry_policy=RetryPolicy(
438+
maximum_attempts=2, initial_interval=timedelta(milliseconds=10)
439+
),
440+
)
441+
spans = exporter.get_finished_spans()
442+
assert all(span.status.status_code == StatusCode.UNSET for span in spans)
443+
444+
389445
# TODO(cretz): Additional tests to write
390446
# * query without interceptor (no headers)
391447
# * workflow without interceptor (no headers) but query with interceptor (headers)

0 commit comments

Comments
 (0)