Skip to content

Commit 653f9c7

Browse files
authored
Make langchain core optional (#80)
* Make langchain-core an optional dependency * Improve missing package logs * Add CHANGELOG * Fix log level * Fix tests
1 parent 62f505d commit 653f9c7

6 files changed

Lines changed: 75 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,29 @@
55
## Bugs Fixed
66
- Reverted [#81](https://github.com/microsoft/opentelemetry-distro-python/pull/81): baggage propagation now requires `enable_a365=True` and respects `ENABLE_A365_OBSERVABILITY_EXPORTER` as before.
77

8+
### Other Changes
9+
- Make `langchain-core` an optional dependency. The LangChain instrumentation
10+
is now installable via `pip install microsoft-opentelemetry[langchain]` and
11+
fails silently with a one-time warning when `langchain-core` is not
12+
installed.
13+
14+
815
## 0.1.0b2 (2026-04-28)
916

1017
### Bugs Fixed
1118
- Ensure baggage properties propagate to child spans for all exporters
1219
([#81](https://github.com/microsoft/opentelemetry-distro-python/pull/81))
1320

21+
1422
## 0.1.0b1 (2026-04-27)
1523

1624
### Features Added
1725

1826
- Ensure baggage properties propagate to child spans when the console exporter is chosen and A365 exporter is disabled
1927
([#74](https://github.com/microsoft/opentelemetry-distro-python/pull/74))
2028
- Disable web-framework / HTTP-client instrumentations
21-
(`django`, `fastapi`, `flask`, `psycopg2`, `requests`, `urllib`, `urllib3`)
22-
by default when A365 is enabled. GenAI instrumentations
29+
(`django`, `fastapi`, `flask`, `psycopg2`, `requests`, `urllib`, `urllib3`,
30+
`azure_sdk`) by default when A365 is enabled. GenAI instrumentations
2331
(`langchain`, `openai`, `openai_agents`, `semantic_kernel`,
2432
`agent_framework`) remain enabled. Users can override either default via
2533
`instrumentation_options`.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ dependencies = [
4040
"opentelemetry-instrumentation-openai-agents-v2==0.1.0",
4141
"opentelemetry-instrumentation-openai-v2==2.3b0",
4242
"opentelemetry-resource-detector-azure<1.0.0,>=0.1.5",
43-
"langchain-core>=0.2.0",
4443
"wrapt>=1.0",
4544
"opentelemetry-util-genai>=0.3b0",
4645
"microsoft-agents-activity>=0.9.0",
@@ -51,6 +50,9 @@ dependencies = [
5150
]
5251

5352
[project.optional-dependencies]
53+
langchain = [
54+
"langchain-core>=0.2.0",
55+
]
5456
test = [
5557
"pytest>=8.0",
5658
"pytest-cov>=5.0",

src/microsoft/opentelemetry/_genai/_langchain/_tracer_instrumentor.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,33 @@
1616
from typing import Any
1717
from uuid import UUID
1818

19-
import langchain_core
20-
import langchain_core.callbacks
21-
import langchain_core.runnables.config
2219
import opentelemetry.trace as trace_api
23-
from langchain_core.callbacks import BaseCallbackManager
2420
from opentelemetry._logs import get_logger as get_otel_logger
2521
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
2622
from opentelemetry.trace import Span
27-
from wrapt import wrap_function_wrapper
28-
29-
from microsoft.opentelemetry._genai._langchain._tracer import LangChainTracer
3023

3124
logger = logging.getLogger(__name__)
3225

3326
_INSTRUMENTS: str = "langchain-core >= 0.2.0"
3427

28+
try:
29+
import langchain_core
30+
import langchain_core.callbacks
31+
import langchain_core.runnables.config
32+
from langchain_core.callbacks import BaseCallbackManager
33+
from wrapt import wrap_function_wrapper
34+
35+
from microsoft.opentelemetry._genai._langchain._tracer import LangChainTracer
36+
37+
langchain_available = True
38+
except ImportError: # pragma: no cover - exercised only when langchain-core absent
39+
langchain_available = False
40+
logger.warning(
41+
"LangChain instrumentation is disabled because 'langchain-core' is not "
42+
"installed. Install the optional extra with "
43+
"`pip install microsoft-opentelemetry[langchain]` to enable it."
44+
)
45+
3546

3647
class LangChainInstrumentor(BaseInstrumentor):
3748
"""Attaches a LangChainTracer to every new BaseCallbackManager."""
@@ -47,6 +58,8 @@ def instrumentation_dependencies(self) -> Collection[str]:
4758
return (_INSTRUMENTS,)
4859

4960
def _instrument(self, **kwargs: Any) -> None:
61+
if not langchain_available:
62+
return
5063
tracer_provider = kwargs.get("tracer_provider")
5164
tracer = trace_api.get_tracer(
5265
__name__,
@@ -83,6 +96,8 @@ def _instrument(self, **kwargs: Any) -> None:
8396
)
8497

8598
def _uninstrument(self, **kwargs: Any) -> None:
99+
if not langchain_available:
100+
return
86101
if self._original_cb_init is not None:
87102
langchain_core.callbacks.BaseCallbackManager.__init__ = self._original_cb_init # type: ignore[assignment]
88103
self._original_cb_init = None

tests/langchain/test_tracer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77
from unittest.mock import MagicMock, patch
88
from uuid import uuid4
99

10-
from microsoft.opentelemetry._genai._langchain._tracer import (
10+
import pytest
11+
12+
pytest.importorskip("langchain_core")
13+
14+
from microsoft.opentelemetry._genai._langchain._tracer import ( # noqa: E402 # pylint: disable=wrong-import-position
1115
LangChainTracer,
1216
_update_span,
1317
get_attributes_from_context,
1418
)
15-
from microsoft.opentelemetry._genai._langchain._utils import (
19+
from microsoft.opentelemetry._genai._langchain._utils import ( # noqa: E402 # pylint: disable=wrong-import-position
1620
EXECUTE_TOOL_OPERATION_NAME,
1721
INVOKE_AGENT_OPERATION_NAME,
1822
)
1923

24+
pytest.importorskip("langchain_core")
25+
2026
_NOW = datetime.datetime(2024, 6, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
2127
_NOW_END = datetime.datetime(2024, 6, 1, 12, 0, 1, tzinfo=datetime.timezone.utc)
2228

tests/langchain/test_tracer_instrumentor.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
_BaseCallbackManagerInit,
1111
)
1212

13+
import pytest
14+
15+
pytest.importorskip("langchain_core")
1316

1417
class TestLangChainInstrumentor(TestCase):
1518
def setUp(self):
@@ -95,6 +98,30 @@ def test_get_ancestors_returns_empty_when_not_instrumented(self, mock_wrap, mock
9598
inst = LangChainInstrumentor()
9699
self.assertEqual(inst.get_ancestors(uuid4()), [])
97100

101+
@patch("microsoft.opentelemetry._genai._langchain._tracer_instrumentor.langchain_available", False)
102+
@patch("microsoft.opentelemetry._genai._langchain._tracer_instrumentor.wrap_function_wrapper")
103+
@patch("microsoft.opentelemetry._genai._langchain._tracer_instrumentor.logger")
104+
def test_instrument_skips_when_langchain_unavailable(self, mock_logger, mock_wrap):
105+
"""When langchain-core is not installed, _instrument logs a debug message and no-ops."""
106+
inst = LangChainInstrumentor()
107+
inst._instrument()
108+
mock_wrap.assert_not_called()
109+
self.assertIsNone(inst._tracer)
110+
mock_logger.debug.assert_called_once()
111+
debug_msg = mock_logger.debug.call_args[0][0]
112+
self.assertIn("langchain-core", debug_msg)
113+
# Should NOT log a warning here (the import-time warning already fired).
114+
mock_logger.warning.assert_not_called()
115+
116+
@patch("microsoft.opentelemetry._genai._langchain._tracer_instrumentor.langchain_available", False)
117+
def test_uninstrument_no_op_when_langchain_unavailable(self):
118+
"""_uninstrument is a safe no-op when langchain-core is not installed."""
119+
inst = LangChainInstrumentor()
120+
# Should not raise
121+
inst._uninstrument()
122+
self.assertIsNone(inst._tracer)
123+
self.assertIsNone(inst._original_cb_init)
124+
98125

99126
class TestBaseCallbackManagerInit(TestCase):
100127
def test_adds_tracer_to_handlers(self):

tests/langchain/test_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
from unittest import TestCase
88
from unittest.mock import MagicMock, patch
99

10-
from microsoft.opentelemetry._genai._langchain._utils import (
10+
import pytest
11+
12+
pytest.importorskip("langchain_core")
13+
14+
from microsoft.opentelemetry._genai._langchain._utils import ( # noqa: E402 # pylint: disable=wrong-import-position
1115
DictWithLock,
1216
CHAT_OPERATION_NAME,
1317
EXECUTE_TOOL_OPERATION_NAME,

0 commit comments

Comments
 (0)