From 79c26143f87242c112808d536a526bfcc7b9e812 Mon Sep 17 00:00:00 2001 From: ionmincu Date: Fri, 16 Jan 2026 16:34:14 +0200 Subject: [PATCH] fix(traces): fix traced otel supression --- .github/workflows/publish-dev.yml | 20 +++- pyproject.toml | 2 +- src/uipath/core/tracing/decorators.py | 14 +++ tests/tracing/test_traced.py | 158 ++++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 190 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index ae0907d..4ad7520 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -60,9 +60,14 @@ jobs: Write-Output "Package version set to $DEV_VERSION" + $startMarker = "" + $endMarker = "" + $dependencyMessage = @" + $startMarker ## Development Package + - Use ``uipath pack --nolock`` to get the latest dev build from this PR (requires version range). - Add this package as a dependency in your pyproject.toml: ``````toml @@ -83,7 +88,13 @@ jobs: [tool.uv.sources] $PROJECT_NAME = { index = "testpypi" } + + [tool.uv] + override-dependencies = [ + "$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION", + ] `````` + $endMarker "@ # Get the owner and repo from the GitHub repository @@ -101,10 +112,11 @@ jobs: $pr = Invoke-RestMethod -Uri $prUri -Method Get -Headers $headers $currentBody = $pr.body - # Check if there's already a development package section - if ($currentBody -match '## Development Package') { - # Replace the existing section with the new dependency message - $newBody = $currentBody -replace '## Development Package(\r?\n|.)*?(?=##|$)', $dependencyMessage + # Check if markers already exist in the PR description + $markerPattern = "(?s)$([regex]::Escape($startMarker)).*?$([regex]::Escape($endMarker))" + if ($currentBody -match $markerPattern) { + # Replace everything between markers (including markers) + $newBody = $currentBody -replace $markerPattern, $dependencyMessage } else { # Append the dependency message to the end of the description $newBody = if ($currentBody) { "$currentBody`n`n$dependencyMessage" } else { $dependencyMessage } diff --git a/pyproject.toml b/pyproject.toml index 95cf502..5641048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.1.6" +version = "0.1.7" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/core/tracing/decorators.py b/src/uipath/core/tracing/decorators.py index 31ac8a4..74bb17e 100644 --- a/src/uipath/core/tracing/decorators.py +++ b/src/uipath/core/tracing/decorators.py @@ -6,7 +6,9 @@ from functools import wraps from typing import Any, Callable, Optional +from opentelemetry import context as context_api from opentelemetry import trace +from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags from opentelemetry.trace.status import StatusCode @@ -78,6 +80,8 @@ def get_span(): # --------- Sync wrapper --------- @wraps(func) def sync_wrapper(*args: Any, **kwargs: Any) -> Any: + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return func(*args, **kwargs) span_cm, span = get_span() try: # Set input attributes BEFORE execution @@ -113,6 +117,8 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any: # --------- Async wrapper --------- @wraps(func) async def async_wrapper(*args: Any, **kwargs: Any) -> Any: + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return await func(*args, **kwargs) span_cm, span = get_span() try: # Set input attributes BEFORE execution @@ -148,6 +154,10 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Any: # --------- Generator wrapper --------- @wraps(func) def generator_wrapper(*args: Any, **kwargs: Any) -> Any: + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + for item in func(*args, **kwargs): + yield item + return span_cm, span = get_span() try: # Set input attributes BEFORE execution @@ -186,6 +196,10 @@ def generator_wrapper(*args: Any, **kwargs: Any) -> Any: # --------- Async generator wrapper --------- @wraps(func) async def async_generator_wrapper(*args: Any, **kwargs: Any) -> Any: + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + async for item in func(*args, **kwargs): + yield item + return span_cm, span = get_span() try: # Set input attributes BEFORE execution diff --git a/tests/tracing/test_traced.py b/tests/tracing/test_traced.py index cadf761..1009f10 100644 --- a/tests/tracing/test_traced.py +++ b/tests/tracing/test_traced.py @@ -816,3 +816,161 @@ async def sample_async_generator_function(n): spans = exporter.get_exported_spans() assert len(spans) == 0 + + +# --------- Suppress Instrumentation Tests --------- + + +def test_suppress_instrumentation_sync_function(setup_tracer): + """Test that suppress_instrumentation prevents spans from being created for sync functions.""" + from opentelemetry.instrumentation.utils import suppress_instrumentation + + exporter, provider = setup_tracer + + @traced() + def sample_function(x, y): + return x + y + + # Call without suppression - should create span + result = sample_function(2, 3) + assert result == 5 + + spans = exporter.get_exported_spans() + assert len(spans) == 1 + + exporter.clear_exported_spans() + + # Call with suppression - should NOT create span + with suppress_instrumentation(): + result = sample_function(4, 5) + assert result == 9 + + provider.shutdown() + spans = exporter.get_exported_spans() + assert len(spans) == 0 + + +@pytest.mark.asyncio +async def test_suppress_instrumentation_async_function(setup_tracer): + """Test that suppress_instrumentation prevents spans from being created for async functions.""" + from opentelemetry.instrumentation.utils import suppress_instrumentation + + exporter, provider = setup_tracer + + @traced() + async def sample_async_function(x, y): + return x * y + + # Call without suppression - should create span + result = await sample_async_function(2, 3) + assert result == 6 + + await sleep(0.1) + spans = exporter.get_exported_spans() + assert len(spans) == 1 + + exporter.clear_exported_spans() + + # Call with suppression - should NOT create span + with suppress_instrumentation(): + result = await sample_async_function(4, 5) + assert result == 20 + + provider.shutdown() + await sleep(0.1) + spans = exporter.get_exported_spans() + assert len(spans) == 0 + + +def test_suppress_instrumentation_generator_function(setup_tracer): + """Test that suppress_instrumentation prevents spans from being created for generator functions.""" + from opentelemetry.instrumentation.utils import suppress_instrumentation + + exporter, provider = setup_tracer + + @traced() + def sample_generator_function(n): + for i in range(n): + yield i + + # Call without suppression - should create span + results = list(sample_generator_function(3)) + assert results == [0, 1, 2] + + spans = exporter.get_exported_spans() + assert len(spans) == 1 + + exporter.clear_exported_spans() + + # Call with suppression - should NOT create span + with suppress_instrumentation(): + results = list(sample_generator_function(4)) + assert results == [0, 1, 2, 3] + + provider.shutdown() + spans = exporter.get_exported_spans() + assert len(spans) == 0 + + +@pytest.mark.asyncio +async def test_suppress_instrumentation_async_generator_function(setup_tracer): + """Test that suppress_instrumentation prevents spans from being created for async generator functions.""" + from opentelemetry.instrumentation.utils import suppress_instrumentation + + exporter, provider = setup_tracer + + @traced() + async def sample_async_generator_function(n): + for i in range(n): + yield i + + # Call without suppression - should create span + results = [item async for item in sample_async_generator_function(3)] + assert results == [0, 1, 2] + + spans = exporter.get_exported_spans() + assert len(spans) == 1 + + exporter.clear_exported_spans() + + # Call with suppression - should NOT create span + with suppress_instrumentation(): + results = [item async for item in sample_async_generator_function(4)] + assert results == [0, 1, 2, 3] + + provider.shutdown() + spans = exporter.get_exported_spans() + assert len(spans) == 0 + + +def test_suppress_instrumentation_nested_functions(setup_tracer): + """Test that suppress_instrumentation prevents spans for nested traced function calls.""" + from opentelemetry.instrumentation.utils import suppress_instrumentation + + exporter, provider = setup_tracer + + @traced() + def inner_function(x): + return x * 2 + + @traced() + def outer_function(x): + return inner_function(x) + 1 + + # Call without suppression - should create 2 spans + result = outer_function(5) + assert result == 11 + + spans = exporter.get_exported_spans() + assert len(spans) == 2 + + exporter.clear_exported_spans() + + # Call with suppression - should NOT create any spans + with suppress_instrumentation(): + result = outer_function(10) + assert result == 21 + + provider.shutdown() + spans = exporter.get_exported_spans() + assert len(spans) == 0 diff --git a/uv.lock b/uv.lock index 4e8be57..b9b3529 100644 --- a/uv.lock +++ b/uv.lock @@ -991,7 +991,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.1.6" +version = "0.1.7" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" },