diff --git a/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py b/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py index 76336b94f2..93d5c6a176 100644 --- a/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py +++ b/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py @@ -125,6 +125,7 @@ def _accumulate_streaming_response( elif llm_request_type == LLMRequestTypeValues.COMPLETION: text = res.get("response", "") accumulated_response["response"] += text + except Exception as e: span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) @@ -146,6 +147,7 @@ def _accumulate_streaming_response( last_response.model_dump() if last_response and hasattr(last_response, 'model_dump') else last_response + ) ) or {} _handle_response( span=span, @@ -154,6 +156,10 @@ def _accumulate_streaming_response( token_histogram=token_histogram, response=response_data | accumulated_response, ) + except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + span.record_exception(e) + raise finally: span.end() @@ -196,6 +202,7 @@ async def _aaccumulate_streaming_response( elif llm_request_type == LLMRequestTypeValues.COMPLETION: text = res.get("response", "") accumulated_response["response"] += text + except Exception as e: span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) @@ -217,6 +224,7 @@ async def _aaccumulate_streaming_response( last_response.model_dump() if last_response and hasattr(last_response, 'model_dump') else last_response + ) ) or {} _handle_response( span, @@ -225,6 +233,10 @@ async def _aaccumulate_streaming_response( token_histogram, response_data | accumulated_response, ) + except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + span.record_exception(e) + raise finally: span.end() @@ -327,6 +339,8 @@ def _wrap( try: response = wrapped(*args, **kwargs) except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + span.record_exception(e) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) span.set_status(Status(StatusCode.ERROR, str(e))) @@ -409,6 +423,8 @@ async def _awrap( try: response = await wrapped(*args, **kwargs) except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + span.record_exception(e) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) span.set_status(Status(StatusCode.ERROR, str(e))) diff --git a/packages/opentelemetry-instrumentation-ollama/tests/test_exceptions.py b/packages/opentelemetry-instrumentation-ollama/tests/test_exceptions.py new file mode 100644 index 0000000000..39d8a37d02 --- /dev/null +++ b/packages/opentelemetry-instrumentation-ollama/tests/test_exceptions.py @@ -0,0 +1,97 @@ +"""Tests that HTTP errors from the Ollama API are recorded as failed spans.""" + +import pytest +from unittest.mock import patch +from ollama._client import AsyncClient as OllamaAsyncClient +from ollama._client import Client as OllamaClient +from opentelemetry.instrumentation.ollama import OllamaInstrumentor +from opentelemetry.trace import StatusCode + + +def test_chat_exception_marks_span_error(span_exporter, tracer_provider, meter_provider): + # Patch _request on the class BEFORE instrumenting so wrapt wraps the mock. + # Call chain: ollama.chat → Client._request (wrapt wrapper) + # → _dispatch_wrap → _wrap → wrapped() = mock → raises + # → our except: set_status(ERROR), record_exception, span.end(), re-raise + with patch.object( + OllamaClient, "_request", side_effect=Exception("connection refused") + ): + instrumentor = OllamaInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + try: + import ollama + + with pytest.raises(Exception, match="connection refused"): + ollama.chat( + model="llama3", + messages=[{"role": "user", "content": "Hello"}], + ) + finally: + instrumentor.uninstrument() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.status.status_code == StatusCode.ERROR + assert "connection refused" in span.status.description + assert any(e.name == "exception" for e in span.events) + + +def test_generate_exception_marks_span_error(span_exporter, tracer_provider, meter_provider): + with patch.object( + OllamaClient, "_request", side_effect=Exception("model not found") + ): + instrumentor = OllamaInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + try: + import ollama + + with pytest.raises(Exception, match="model not found"): + ollama.generate(model="llama3", prompt="Hello") + finally: + instrumentor.uninstrument() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.status.status_code == StatusCode.ERROR + assert "model not found" in span.status.description + + +@pytest.mark.asyncio +async def test_async_chat_exception_marks_span_error( + span_exporter, tracer_provider, meter_provider +): + with patch.object( + OllamaAsyncClient, + "_request", + side_effect=Exception("async connection refused"), + ): + instrumentor = OllamaInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + try: + import ollama + + with pytest.raises(Exception, match="async connection refused"): + await ollama.AsyncClient().chat( + model="llama3", + messages=[{"role": "user", "content": "Hello"}], + ) + finally: + instrumentor.uninstrument() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.status.status_code == StatusCode.ERROR + assert "async connection refused" in span.status.description + assert any(e.name == "exception" for e in span.events)