Repository navigation
fix(ollama): record failed spans when API raises exceptions #4127
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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))) | ||
|
Comment on lines
344
to
346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Record request exceptions only once. The existing handlers already call Also applies to: 428-430
|
||
|
|
@@ -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))) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the invalid streaming exception blocks.
Both helpers add an extra
)after theresponse_dataexpression. They also place a secondexceptafter theelsesuite. Python requires allexceptclauses to appear beforeelseandfinally, so this module cannot be imported. Remove the extra parenthesis and restructure the response-handling exception path as a nestedtryor as one outertry/except/finallyblock. (raw.githubusercontent.com)Also applies to: 228-228
Ruff (0.17.0)
[warning] 151-151: Expected a statement
(invalid-syntax)
[warning] 151-151: Expected a statement
(invalid-syntax)
Source: Linters/SAST tools