-
Notifications
You must be signed in to change notification settings - Fork 25
fix: pin opentelemetry-instrumentation-langchain and add telemetry integration tests #143
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
Open
LucasAlvesSoares
wants to merge
22
commits into
main
Choose a base branch
from
telemetry-integration-tests
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,630
−11
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
5a82654
.env example
LucasAlvesSoares f1c3854
Added langgraph and langchain as dev dependencies for telemetry integ…
LucasAlvesSoares 67699e2
Added langgraph and langchain as dev dependencies for telemetry integ…
LucasAlvesSoares 8d3952c
Init
LucasAlvesSoares e7ce59d
Exporter config and setup
LucasAlvesSoares 1ebd6e7
Feature file
LucasAlvesSoares 600b0dd
test drill
LucasAlvesSoares 8a2aeac
resource attribute checks and more traceloop scenarios
LucasAlvesSoares 16ea043
More traceloop scenarios
LucasAlvesSoares 6705c9c
Less but more meaningful scenarios
LucasAlvesSoares 57eba52
Removed verbose
LucasAlvesSoares 9a15857
Linting and logging
LucasAlvesSoares 5a41f3d
Added AICORE* pattern to CI
LucasAlvesSoares 6bd88b6
Upgrade uv for compatibility
LucasAlvesSoares 0e8c68e
Regenerate uv.lock from scratch to fix parse error
LucasAlvesSoares 9ea291c
Revert "Upgrade uv for compatibility"
LucasAlvesSoares 68f1a6a
Merge branch 'main' into telemetry-integration-tests
LucasAlvesSoares f898534
merge conflicts
LucasAlvesSoares d54b9c1
Fix ty type errors in integration tests
LucasAlvesSoares cd1409c
Use dataclass instead of TypedDict for LangGraph State
LucasAlvesSoares 227796e
merge conflicts
LucasAlvesSoares 3ece6c6
Pinned opentelemetry-instrumentation-langchain to >= 0.61.0
LucasAlvesSoares File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """LangGraph test agent used by the telemetry integration tests.""" | ||
|
|
||
| import os | ||
| from dataclasses import dataclass | ||
| from typing import Annotated | ||
|
|
||
| import pytest | ||
|
|
||
| from langchain_core.messages import BaseMessage | ||
| from langgraph.graph.message import add_messages | ||
|
|
||
|
|
||
| @dataclass | ||
| class State: | ||
| messages: Annotated[list[BaseMessage], add_messages] | ||
|
|
||
|
|
||
| def build_langgraph_agent(): | ||
| """Build a minimal single-node LangGraph agent backed by LiteLLM via AI Core. | ||
|
|
||
| Requires AICORE_MODEL env var (e.g. "anthropic--claude-3-5-haiku") in addition | ||
| to the AICORE_* credentials set by set_aicore_config(). LiteLLM uses the | ||
| "sap/<model>" prefix to route through the SAP AI Core provider. | ||
| """ | ||
| try: | ||
| from langchain_litellm import ChatLiteLLM | ||
| from langgraph.graph import END, StateGraph | ||
| except ImportError: | ||
| pytest.skip("langchain-litellm or langgraph not installed") | ||
|
|
||
| model_name = os.environ.get("AICORE_MODEL") or "anthropic--claude-4.5-sonnet" | ||
| llm = ChatLiteLLM(model=f"sap/{model_name}") | ||
|
|
||
| def call_llm(state: State) -> State: | ||
| return State(messages=[llm.invoke(state.messages)]) | ||
|
|
||
| graph = StateGraph(State) | ||
| graph.add_node("llm", call_llm) | ||
| graph.set_entry_point("llm") | ||
| graph.add_edge("llm", END) | ||
| return graph.compile() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| """Fixtures for telemetry integration tests.""" | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| from dotenv import load_dotenv | ||
| from opentelemetry import trace | ||
| 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 sap_cloud_sdk.aicore import set_aicore_config | ||
| from sap_cloud_sdk.core.telemetry.auto_instrument import auto_instrument | ||
| from sap_cloud_sdk.core.telemetry.genai_attribute_transformer import ( | ||
| GenAIAttributeTransformer, | ||
| ) | ||
|
|
||
| _env_file = Path(__file__).parent.parent.parent.parent.parent / ".env_integration_tests" | ||
| if _env_file.exists(): | ||
| load_dotenv(_env_file, override=True) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def memory_exporter() -> InMemorySpanExporter: | ||
| """Initialize auto_instrument once per session and inject an in-memory exporter. | ||
|
|
||
| Uses OTEL_TRACES_EXPORTER=console so Traceloop.init runs for real without | ||
| needing a real collector endpoint. A second SimpleSpanProcessor backed by | ||
| InMemorySpanExporter is added afterward for test assertions on raw spans. | ||
| """ | ||
| raw_exporter = InMemorySpanExporter() | ||
| with patch.dict(os.environ, {"OTEL_TRACES_EXPORTER": "console"}, clear=False): | ||
| auto_instrument(disable_batch=True) | ||
| provider = trace.get_tracer_provider() | ||
| assert isinstance(provider, TracerProvider) | ||
| provider.add_span_processor(SimpleSpanProcessor(raw_exporter)) | ||
| return raw_exporter | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def transforming_exporter(memory_exporter: InMemorySpanExporter) -> InMemorySpanExporter: | ||
| """Inject a second in-memory exporter wrapped in GenAIAttributeTransformer. | ||
|
|
||
| Used by traceloop.feature tests to assert on transformer output | ||
| (gen_ai.* attributes present, llm.usage.* and traceloop.* absent). | ||
| """ | ||
| transformed_sink = InMemorySpanExporter() | ||
| provider = trace.get_tracer_provider() | ||
| assert isinstance(provider, TracerProvider) | ||
| provider.add_span_processor( | ||
| SimpleSpanProcessor(GenAIAttributeTransformer(transformed_sink)) | ||
| ) | ||
| return transformed_sink | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def clear_spans(memory_exporter: InMemorySpanExporter, transforming_exporter: InMemorySpanExporter): | ||
| """Clear both exporters before each test so spans don't bleed between scenarios.""" | ||
| memory_exporter.clear() | ||
| transforming_exporter.clear() | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def aicore_configured(): | ||
| """Call set_aicore_config() once per session. | ||
|
|
||
| Skips if AICORE_BASE_URL is absent — tests depending on this fixture are | ||
| skipped automatically. | ||
| """ | ||
| if not os.environ.get("AICORE_BASE_URL"): | ||
| pytest.skip("AICORE_BASE_URL not set — skipping AI Core integration tests") | ||
| set_aicore_config() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| Feature: SDK telemetry instrumentation | ||
|
|
||
| Background: | ||
| Given auto_instrument is initialized | ||
|
|
||
| Scenario: invoke_agent_span emits a span with required GenAI attributes | ||
| When I invoke an agent with provider "test" and name "bot" and conversation_id "c1" | ||
| Then a span named "invoke_agent bot" is recorded | ||
| And the span has attribute "gen_ai.operation.name" equal to "invoke_agent" | ||
| And the span has attribute "gen_ai.provider.name" equal to "test" | ||
| And the span has attribute "gen_ai.agent.name" equal to "bot" | ||
| And the span has attribute "gen_ai.conversation.id" equal to "c1" | ||
|
|
||
| Scenario: invoke_agent_span records errors | ||
| When I invoke an agent that raises an exception | ||
| Then the span status is ERROR | ||
| And the span has an exception event | ||
|
|
||
| Scenario: spans carry SDK resource attributes | ||
| When I invoke an agent with provider "test" and name "sdk-resource-test" | ||
| Then a span named "invoke_agent sdk-resource-test" is recorded | ||
| And the span resource has attribute "sap.cloud_sdk.name" equal to "SAP Cloud SDK for Python" | ||
| And the span resource has attribute "sap.cloud_sdk.language" equal to "python" | ||
| And the span resource has attribute "sap.cloud_sdk.version" set | ||
|
|
||
| # Real LLM call scenarios — require AI Core credentials | ||
|
|
||
| @aicore | ||
| Scenario: invoke_agent_span wrapping a real LLM call produces a complete trace | ||
| Given AI Core is configured via set_aicore_config | ||
| When I invoke an agent wrapping a direct LLM call | ||
| Then a span named "invoke_agent llm-agent" is recorded | ||
| And a span with operation "chat" is a child of "invoke_agent llm-agent" | ||
| And that span has attribute "gen_ai.usage.input_tokens" set | ||
| And that span has attribute "gen_ai.usage.output_tokens" set | ||
| And the span "invoke_agent llm-agent" has resource attribute "sap.cloud_sdk.name" equal to "SAP Cloud SDK for Python" | ||
| And the span "invoke_agent llm-agent" has resource attribute "sap.cloud_sdk.language" equal to "python" | ||
| And the span "invoke_agent llm-agent" has resource attribute "sap.cloud_sdk.version" set | ||
|
|
||
| @aicore | ||
| Scenario: invoke_agent_span wrapping LLM call then tool produces a full agentic trace | ||
| Given AI Core is configured via set_aicore_config | ||
| When I invoke an agent that calls an LLM then executes a tool | ||
| Then a span named "invoke_agent agent-with-tool" is recorded | ||
| And a span with operation "chat" is a child of "invoke_agent agent-with-tool" | ||
| And that span has attribute "gen_ai.usage.input_tokens" set | ||
| And the span "execute_tool search" is a child of "invoke_agent agent-with-tool" | ||
| And the span "execute_tool search" has attribute "gen_ai.tool.name" equal to "search" | ||
|
|
||
| @aicore | ||
| Scenario: propagate=True flows invoke_agent attributes to nested LLM span | ||
| Given AI Core is configured via set_aicore_config | ||
| When I invoke an agent with propagate=True wrapping a real LLM call | ||
| Then a span with operation "chat" is a child of "invoke_agent propagate-llm-agent" | ||
| And that span has attribute "custom.session" equal to "s42" | ||
| And that span has attribute "gen_ai.usage.input_tokens" set | ||
|
|
||
| @aicore | ||
| Scenario: propagate=False does not leak invoke_agent attributes to nested LLM span | ||
| Given AI Core is configured via set_aicore_config | ||
| When I invoke an agent with propagate=False wrapping a real LLM call | ||
| Then a span with operation "chat" is a child of "invoke_agent no-propagate-llm-agent" | ||
| And that span does not have attribute "custom.session" | ||
|
|
||
| @aicore | ||
| Scenario: baggage attributes propagate to Traceloop-instrumented LLM spans | ||
| Given baggage key "sap.extension.capabilityId" is set to "cap-traceloop" | ||
| And AI Core is configured via set_aicore_config | ||
| When I invoke an agent wrapping a direct LLM call with baggage | ||
| Then a span with operation "chat" is a child of "invoke_agent baggage-llm-agent" | ||
| And that span has attribute "sap.extension.capabilityId" equal to "cap-traceloop" | ||
|
|
||
| @aicore | ||
| Scenario: LangGraph agent run produces an invoke_agent span with LangChain child spans | ||
| Given AI Core is configured via set_aicore_config | ||
| When I run a LangGraph agent with provider "sap-aicore" and name "test-agent" | ||
| Then a span named "invoke_agent test-agent" is recorded | ||
| And at least one descendant span with attribute "gen_ai.operation.name" equal to "chat" is recorded | ||
| And at least one descendant span has attribute "gen_ai.request.model" set | ||
| And at least one descendant span has attribute "gen_ai.usage.input_tokens" set | ||
| And at least one descendant span has attribute "gen_ai.usage.output_tokens" set | ||
| And no descendant span has an attribute starting with "llm.usage." | ||
| And no descendant span has attribute "traceloop.association.properties.ls_model_name" | ||
| And no descendant span has attribute "traceloop.association.properties.ls_provider" | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.