mirror of
https://github.com/langgenius/dify.git
synced 2025-12-25 01:00:42 -05:00
add service layer OTel Span (#28582)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
0
api/tests/unit_tests/extensions/otel/__init__.py
Normal file
0
api/tests/unit_tests/extensions/otel/__init__.py
Normal file
96
api/tests/unit_tests/extensions/otel/conftest.py
Normal file
96
api/tests/unit_tests/extensions/otel/conftest.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Shared fixtures for OTel tests.
|
||||
|
||||
Provides:
|
||||
- Mock TracerProvider with MemorySpanExporter
|
||||
- Mock configurations
|
||||
- Test data factories
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, create_autospec
|
||||
|
||||
import pytest
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
||||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
||||
from opentelemetry.trace import set_tracer_provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_span_exporter():
|
||||
"""Provide an in-memory span exporter for testing."""
|
||||
return InMemorySpanExporter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tracer_provider_with_memory_exporter(memory_span_exporter):
|
||||
"""Provide a TracerProvider configured with memory exporter."""
|
||||
import opentelemetry.trace as trace_api
|
||||
|
||||
trace_api._TRACER_PROVIDER = None
|
||||
trace_api._TRACER_PROVIDER_SET_ONCE._done = False
|
||||
|
||||
provider = TracerProvider()
|
||||
processor = SimpleSpanProcessor(memory_span_exporter)
|
||||
provider.add_span_processor(processor)
|
||||
set_tracer_provider(provider)
|
||||
|
||||
yield provider
|
||||
|
||||
provider.force_flush()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app_model():
|
||||
"""Create a mock App model."""
|
||||
app = MagicMock()
|
||||
app.id = "test-app-id"
|
||||
app.tenant_id = "test-tenant-id"
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_account_user():
|
||||
"""Create a mock Account user."""
|
||||
from models.model import Account
|
||||
|
||||
user = create_autospec(Account, instance=True)
|
||||
user.id = "test-user-id"
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_end_user():
|
||||
"""Create a mock EndUser."""
|
||||
from models.model import EndUser
|
||||
|
||||
user = create_autospec(EndUser, instance=True)
|
||||
user.id = "test-end-user-id"
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workflow_runner():
|
||||
"""Create a mock WorkflowAppRunner."""
|
||||
runner = MagicMock()
|
||||
runner.application_generate_entity = MagicMock()
|
||||
runner.application_generate_entity.user_id = "test-user-id"
|
||||
runner.application_generate_entity.stream = True
|
||||
runner.application_generate_entity.app_config = MagicMock()
|
||||
runner.application_generate_entity.app_config.app_id = "test-app-id"
|
||||
runner.application_generate_entity.app_config.tenant_id = "test-tenant-id"
|
||||
runner.application_generate_entity.app_config.workflow_id = "test-workflow-id"
|
||||
return runner
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_handler_instances():
|
||||
"""Reset handler singleton instances before each test."""
|
||||
from extensions.otel.decorators.base import _HANDLER_INSTANCES
|
||||
|
||||
_HANDLER_INSTANCES.clear()
|
||||
from extensions.otel.decorators.handler import SpanHandler
|
||||
|
||||
_HANDLER_INSTANCES[SpanHandler] = SpanHandler()
|
||||
yield
|
||||
_HANDLER_INSTANCES.clear()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Tests for AppGenerateHandler.
|
||||
|
||||
Test objectives:
|
||||
1. Verify handler compatibility with real function signature (fails when parameters change)
|
||||
2. Verify span attribute mapping correctness
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.otel.decorators.handlers.generate_handler import AppGenerateHandler
|
||||
from extensions.otel.semconv import DifySpanAttributes, GenAIAttributes
|
||||
|
||||
|
||||
class TestAppGenerateHandler:
|
||||
"""Core tests for AppGenerateHandler"""
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_compatible_with_real_function_signature(
|
||||
self, tracer_provider_with_memory_exporter, mock_app_model, mock_account_user
|
||||
):
|
||||
"""
|
||||
Verify handler compatibility with real AppGenerateService.generate signature.
|
||||
|
||||
If AppGenerateService.generate parameters change, this test will fail,
|
||||
prompting developers to update the handler's parameter extraction logic.
|
||||
"""
|
||||
from services.app_generate_service import AppGenerateService
|
||||
|
||||
handler = AppGenerateHandler()
|
||||
|
||||
kwargs = {
|
||||
"app_model": mock_app_model,
|
||||
"user": mock_account_user,
|
||||
"args": {"workflow_id": "test-wf-123"},
|
||||
"invoke_from": InvokeFrom.DEBUGGER,
|
||||
"streaming": True,
|
||||
"root_node_id": None,
|
||||
}
|
||||
|
||||
arguments = handler._extract_arguments(AppGenerateService.generate, (), kwargs)
|
||||
|
||||
assert arguments is not None, "Failed to extract arguments from AppGenerateService.generate"
|
||||
assert "app_model" in arguments, "Handler uses app_model but parameter is missing"
|
||||
assert "user" in arguments, "Handler uses user but parameter is missing"
|
||||
assert "args" in arguments, "Handler uses args but parameter is missing"
|
||||
assert "streaming" in arguments, "Handler uses streaming but parameter is missing"
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_all_span_attributes_set_correctly(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_app_model, mock_account_user
|
||||
):
|
||||
"""Verify all span attributes are mapped correctly"""
|
||||
handler = AppGenerateHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
test_app_id = "app-456"
|
||||
test_tenant_id = "tenant-789"
|
||||
test_user_id = "user-111"
|
||||
test_workflow_id = "wf-222"
|
||||
|
||||
mock_app_model.id = test_app_id
|
||||
mock_app_model.tenant_id = test_tenant_id
|
||||
mock_account_user.id = test_user_id
|
||||
|
||||
def dummy_func(app_model, user, args, invoke_from, streaming=True):
|
||||
return "result"
|
||||
|
||||
handler.wrapper(
|
||||
tracer,
|
||||
dummy_func,
|
||||
(),
|
||||
{
|
||||
"app_model": mock_app_model,
|
||||
"user": mock_account_user,
|
||||
"args": {"workflow_id": test_workflow_id},
|
||||
"invoke_from": InvokeFrom.DEBUGGER,
|
||||
"streaming": False,
|
||||
},
|
||||
)
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
attrs = spans[0].attributes
|
||||
|
||||
assert attrs[DifySpanAttributes.APP_ID] == test_app_id
|
||||
assert attrs[DifySpanAttributes.TENANT_ID] == test_tenant_id
|
||||
assert attrs[GenAIAttributes.USER_ID] == test_user_id
|
||||
assert attrs[DifySpanAttributes.WORKFLOW_ID] == test_workflow_id
|
||||
assert attrs[DifySpanAttributes.USER_TYPE] == "Account"
|
||||
assert attrs[DifySpanAttributes.STREAMING] is False
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Tests for WorkflowAppRunnerHandler.
|
||||
|
||||
Test objectives:
|
||||
1. Verify handler compatibility with real WorkflowAppRunner structure (fails when structure changes)
|
||||
2. Verify span attribute mapping correctness
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from extensions.otel.decorators.handlers.workflow_app_runner_handler import WorkflowAppRunnerHandler
|
||||
from extensions.otel.semconv import DifySpanAttributes, GenAIAttributes
|
||||
|
||||
|
||||
class TestWorkflowAppRunnerHandler:
|
||||
"""Core tests for WorkflowAppRunnerHandler"""
|
||||
|
||||
def test_handler_structure_dependencies(self):
|
||||
"""
|
||||
Verify handler dependencies on WorkflowAppRunner structure.
|
||||
|
||||
Handler depends on:
|
||||
- runner.application_generate_entity (WorkflowAppGenerateEntity)
|
||||
- entity.app_config (WorkflowAppConfig)
|
||||
- entity.user_id, entity.stream
|
||||
- app_config.app_id, app_config.tenant_id, app_config.workflow_id
|
||||
|
||||
If these attribute paths change in real types, this test will fail,
|
||||
prompting developers to update the handler's attribute access logic.
|
||||
"""
|
||||
from core.app.app_config.entities import WorkflowUIBasedAppConfig
|
||||
from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity
|
||||
|
||||
required_entity_fields = ["user_id", "stream", "app_config"]
|
||||
entity_fields = WorkflowAppGenerateEntity.model_fields
|
||||
for field in required_entity_fields:
|
||||
assert field in entity_fields, f"Handler expects WorkflowAppGenerateEntity.{field} but field is missing"
|
||||
|
||||
required_config_fields = ["app_id", "tenant_id", "workflow_id"]
|
||||
config_fields = WorkflowUIBasedAppConfig.model_fields
|
||||
for field in required_config_fields:
|
||||
assert field in config_fields, f"Handler expects app_config.{field} but field is missing"
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_all_span_attributes_set_correctly(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_workflow_runner
|
||||
):
|
||||
"""Verify all span attributes are mapped correctly"""
|
||||
handler = WorkflowAppRunnerHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
test_app_id = "app-999"
|
||||
test_tenant_id = "tenant-888"
|
||||
test_user_id = "user-777"
|
||||
test_workflow_id = "wf-666"
|
||||
|
||||
mock_workflow_runner.application_generate_entity.user_id = test_user_id
|
||||
mock_workflow_runner.application_generate_entity.stream = False
|
||||
mock_workflow_runner.application_generate_entity.app_config.app_id = test_app_id
|
||||
mock_workflow_runner.application_generate_entity.app_config.tenant_id = test_tenant_id
|
||||
mock_workflow_runner.application_generate_entity.app_config.workflow_id = test_workflow_id
|
||||
|
||||
def runner_run(self):
|
||||
return "result"
|
||||
|
||||
handler.wrapper(tracer, runner_run, (mock_workflow_runner,), {})
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
attrs = spans[0].attributes
|
||||
|
||||
assert attrs[DifySpanAttributes.APP_ID] == test_app_id
|
||||
assert attrs[DifySpanAttributes.TENANT_ID] == test_tenant_id
|
||||
assert attrs[GenAIAttributes.USER_ID] == test_user_id
|
||||
assert attrs[DifySpanAttributes.WORKFLOW_ID] == test_workflow_id
|
||||
assert attrs[DifySpanAttributes.STREAMING] is False
|
||||
119
api/tests/unit_tests/extensions/otel/decorators/test_base.py
Normal file
119
api/tests/unit_tests/extensions/otel/decorators/test_base.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Tests for trace_span decorator.
|
||||
|
||||
Test coverage:
|
||||
- Decorator basic functionality
|
||||
- Enable/disable logic
|
||||
- Handler singleton management
|
||||
- Integration with OpenTelemetry SDK
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from opentelemetry.trace import StatusCode
|
||||
|
||||
from extensions.otel.decorators.base import trace_span
|
||||
|
||||
|
||||
class TestTraceSpanDecorator:
|
||||
"""Test trace_span decorator basic functionality."""
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_decorated_function_executes_normally(self, tracer_provider_with_memory_exporter):
|
||||
"""Test that decorated function executes and returns correct value."""
|
||||
|
||||
@trace_span()
|
||||
def test_func(x, y):
|
||||
return x + y
|
||||
|
||||
result = test_func(2, 3)
|
||||
assert result == 5
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_decorator_with_args_and_kwargs(self, tracer_provider_with_memory_exporter):
|
||||
"""Test that decorator correctly handles args and kwargs."""
|
||||
|
||||
@trace_span()
|
||||
def test_func(a, b, c=10):
|
||||
return a + b + c
|
||||
|
||||
result = test_func(1, 2, c=3)
|
||||
assert result == 6
|
||||
|
||||
|
||||
class TestTraceSpanWithMemoryExporter:
|
||||
"""Test trace_span with MemorySpanExporter to verify span creation."""
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_span_is_created_and_exported(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that span is created and exported to memory exporter."""
|
||||
|
||||
@trace_span()
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
test_func()
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_span_name_matches_function(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that span name matches the decorated function."""
|
||||
|
||||
@trace_span()
|
||||
def my_test_function():
|
||||
return "result"
|
||||
|
||||
my_test_function()
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert "my_test_function" in spans[0].name
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_span_status_is_ok_on_success(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that span status is OK when function succeeds."""
|
||||
|
||||
@trace_span()
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
test_func()
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert spans[0].status.status_code == StatusCode.OK
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_span_status_is_error_on_exception(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that span status is ERROR when function raises exception."""
|
||||
|
||||
@trace_span()
|
||||
def test_func():
|
||||
raise ValueError("test error")
|
||||
|
||||
with pytest.raises(ValueError, match="test error"):
|
||||
test_func()
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert spans[0].status.status_code == StatusCode.ERROR
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_exception_is_recorded_in_span(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that exception details are recorded in span events."""
|
||||
|
||||
@trace_span()
|
||||
def test_func():
|
||||
raise ValueError("test error")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
test_func()
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
events = spans[0].events
|
||||
assert len(events) > 0
|
||||
assert any("exception" in event.name.lower() for event in events)
|
||||
258
api/tests/unit_tests/extensions/otel/decorators/test_handler.py
Normal file
258
api/tests/unit_tests/extensions/otel/decorators/test_handler.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Tests for SpanHandler base class.
|
||||
|
||||
Test coverage:
|
||||
- _build_span_name method
|
||||
- _extract_arguments method
|
||||
- wrapper method default implementation
|
||||
- Signature caching
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from opentelemetry.trace import StatusCode
|
||||
|
||||
from extensions.otel.decorators.handler import SpanHandler
|
||||
|
||||
|
||||
class TestSpanHandlerExtractArguments:
|
||||
"""Test SpanHandler._extract_arguments method."""
|
||||
|
||||
def test_extract_positional_arguments(self):
|
||||
"""Test extracting positional arguments."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b, c):
|
||||
pass
|
||||
|
||||
args = (1, 2, 3)
|
||||
kwargs = {}
|
||||
result = handler._extract_arguments(func, args, kwargs)
|
||||
|
||||
assert result is not None
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 2
|
||||
assert result["c"] == 3
|
||||
|
||||
def test_extract_keyword_arguments(self):
|
||||
"""Test extracting keyword arguments."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b, c):
|
||||
pass
|
||||
|
||||
args = ()
|
||||
kwargs = {"a": 1, "b": 2, "c": 3}
|
||||
result = handler._extract_arguments(func, args, kwargs)
|
||||
|
||||
assert result is not None
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 2
|
||||
assert result["c"] == 3
|
||||
|
||||
def test_extract_mixed_arguments(self):
|
||||
"""Test extracting mixed positional and keyword arguments."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b, c):
|
||||
pass
|
||||
|
||||
args = (1,)
|
||||
kwargs = {"b": 2, "c": 3}
|
||||
result = handler._extract_arguments(func, args, kwargs)
|
||||
|
||||
assert result is not None
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 2
|
||||
assert result["c"] == 3
|
||||
|
||||
def test_extract_arguments_with_defaults(self):
|
||||
"""Test extracting arguments with default values."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b=10, c=20):
|
||||
pass
|
||||
|
||||
args = (1,)
|
||||
kwargs = {}
|
||||
result = handler._extract_arguments(func, args, kwargs)
|
||||
|
||||
assert result is not None
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 10
|
||||
assert result["c"] == 20
|
||||
|
||||
def test_extract_arguments_handles_self(self):
|
||||
"""Test extracting arguments from instance method (with self)."""
|
||||
handler = SpanHandler()
|
||||
|
||||
class MyClass:
|
||||
def method(self, a, b):
|
||||
pass
|
||||
|
||||
instance = MyClass()
|
||||
args = (1, 2)
|
||||
kwargs = {}
|
||||
result = handler._extract_arguments(instance.method, args, kwargs)
|
||||
|
||||
assert result is not None
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 2
|
||||
|
||||
def test_extract_arguments_returns_none_on_error(self):
|
||||
"""Test that _extract_arguments returns None when extraction fails."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b):
|
||||
pass
|
||||
|
||||
args = (1,)
|
||||
kwargs = {}
|
||||
result = handler._extract_arguments(func, args, kwargs)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_signature_caching(self):
|
||||
"""Test that function signatures are cached."""
|
||||
handler = SpanHandler()
|
||||
|
||||
def func(a, b):
|
||||
pass
|
||||
|
||||
assert func not in handler._signature_cache
|
||||
|
||||
handler._extract_arguments(func, (1, 2), {})
|
||||
assert func in handler._signature_cache
|
||||
|
||||
cached_sig = handler._signature_cache[func]
|
||||
handler._extract_arguments(func, (3, 4), {})
|
||||
assert handler._signature_cache[func] is cached_sig
|
||||
|
||||
|
||||
class TestSpanHandlerWrapper:
|
||||
"""Test SpanHandler.wrapper default implementation."""
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_creates_span(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper creates a span."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
result = handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
assert result == "result"
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_sets_span_kind_internal(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper sets SpanKind to INTERNAL."""
|
||||
from opentelemetry.trace import SpanKind
|
||||
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert spans[0].kind == SpanKind.INTERNAL
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_sets_status_ok_on_success(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper sets status to OK when function succeeds."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert spans[0].status.status_code == StatusCode.OK
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_records_exception_on_error(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper records exception when function raises."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
raise ValueError("test error")
|
||||
|
||||
with pytest.raises(ValueError, match="test error"):
|
||||
handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
events = spans[0].events
|
||||
assert len(events) > 0
|
||||
assert any("exception" in event.name.lower() for event in events)
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_sets_status_error_on_exception(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper sets status to ERROR when function raises exception."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
raise ValueError("test error")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert spans[0].status.status_code == StatusCode.ERROR
|
||||
assert "test error" in spans[0].status.description
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_re_raises_exception(self, tracer_provider_with_memory_exporter):
|
||||
"""Test that wrapper re-raises exception after recording it."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func():
|
||||
raise ValueError("test error")
|
||||
|
||||
with pytest.raises(ValueError, match="test error"):
|
||||
handler.wrapper(tracer, test_func, (), {})
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_passes_arguments_correctly(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test that wrapper correctly passes arguments to wrapped function."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def test_func(a, b, c=10):
|
||||
return a + b + c
|
||||
|
||||
result = handler.wrapper(tracer, test_func, (1, 2), {"c": 3})
|
||||
|
||||
assert result == 6
|
||||
|
||||
@patch("extensions.otel.decorators.base.dify_config.ENABLE_OTEL", True)
|
||||
def test_wrapper_with_memory_exporter(self, tracer_provider_with_memory_exporter, memory_span_exporter):
|
||||
"""Test wrapper end-to-end with memory exporter."""
|
||||
handler = SpanHandler()
|
||||
tracer = tracer_provider_with_memory_exporter.get_tracer(__name__)
|
||||
|
||||
def my_function(x):
|
||||
return x * 2
|
||||
|
||||
result = handler.wrapper(tracer, my_function, (5,), {})
|
||||
|
||||
assert result == 10
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
assert "my_function" in spans[0].name
|
||||
assert spans[0].status.status_code == StatusCode.OK
|
||||
Reference in New Issue
Block a user