sonar-tracing-langchain 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .mypy_cache/
9
+ dist/
10
+ build/
11
+ .env
12
+ *.local.yaml
13
+ .DS_Store
14
+
15
+ # Collector persistent queue (runtime state)
16
+ collector/queue/*
17
+ !collector/queue/.gitkeep
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Ahmon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.5
2
+ Name: sonar-tracing-langchain
3
+ Version: 0.2.0
4
+ Summary: LangChain / LangGraph adapter for sonar-tracing.
5
+ Author-email: Matthew Ahmon <matthew.ahmon@gmail.com>
6
+ Maintainer-email: Matthew Ahmon <matthew.ahmon@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,ai,langchain,langgraph,opentelemetry,sonar,tracing
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: langchain-core>=0.3
20
+ Requires-Dist: openinference-instrumentation-langchain>=0.1
21
+ Requires-Dist: sonar-tracing>=0.1
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # sonar-tracing-langchain
27
+
28
+ LangChain / LangGraph adapter for [`sonar-tracing`](https://pypi.org/project/sonar-tracing/).
29
+
30
+ Installing this package makes the `langchain` framework available to
31
+ `sonar-tracing` — the adapter is discovered automatically via the
32
+ `sonar_tracing.adapters` entry point group, so there's nothing to import or wire
33
+ up by hand.
34
+
35
+ ```bash
36
+ pip install sonar-tracing-langchain
37
+ ```
38
+
39
+ ```python
40
+ import sonar_tracing as tracing
41
+
42
+ tracing.init(service_name="support-agent", framework="langchain")
43
+ ```
44
+
45
+ Unlike Strands, LangChain is not OpenTelemetry-native. This adapter installs the
46
+ OpenInference `LangChainInstrumentor`, handing it the tracer provider
47
+ `sonar-tracing` owns. The instrumentor patches `langchain-core`'s callback
48
+ manager, so the same integration captures both LangChain and LangGraph — there
49
+ is no separate LangGraph package to install.
50
+
51
+ Spans are emitted under the OpenInference semantic conventions and normalised by
52
+ the `openinference@1` mapping pack on the ingest side.
53
+
54
+ ## License
55
+
56
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,31 @@
1
+ # sonar-tracing-langchain
2
+
3
+ LangChain / LangGraph adapter for [`sonar-tracing`](https://pypi.org/project/sonar-tracing/).
4
+
5
+ Installing this package makes the `langchain` framework available to
6
+ `sonar-tracing` — the adapter is discovered automatically via the
7
+ `sonar_tracing.adapters` entry point group, so there's nothing to import or wire
8
+ up by hand.
9
+
10
+ ```bash
11
+ pip install sonar-tracing-langchain
12
+ ```
13
+
14
+ ```python
15
+ import sonar_tracing as tracing
16
+
17
+ tracing.init(service_name="support-agent", framework="langchain")
18
+ ```
19
+
20
+ Unlike Strands, LangChain is not OpenTelemetry-native. This adapter installs the
21
+ OpenInference `LangChainInstrumentor`, handing it the tracer provider
22
+ `sonar-tracing` owns. The instrumentor patches `langchain-core`'s callback
23
+ manager, so the same integration captures both LangChain and LangGraph — there
24
+ is no separate LangGraph package to install.
25
+
26
+ Spans are emitted under the OpenInference semantic conventions and normalised by
27
+ the `openinference@1` mapping pack on the ingest side.
28
+
29
+ ## License
30
+
31
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "sonar-tracing-langchain"
3
+ version = "0.2.0"
4
+ description = "LangChain / LangGraph adapter for sonar-tracing."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ authors = [{ name = "Matthew Ahmon", email = "matthew.ahmon@gmail.com" }]
10
+ maintainers = [{ name = "Matthew Ahmon", email = "matthew.ahmon@gmail.com" }]
11
+ keywords = ["opentelemetry", "tracing", "langchain", "langgraph", "ai", "agents", "sonar"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ "Topic :: System :: Monitoring",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "sonar-tracing>=0.1",
24
+ "openinference-instrumentation-langchain>=0.1",
25
+ "langchain-core>=0.3",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest>=8"]
30
+
31
+ # Discovered by sonar-tracing's registry via this entry point group. The adapter
32
+ # module path is independent of sonar_tracing's package namespace.
33
+ [project.entry-points."sonar_tracing.adapters"]
34
+ langchain = "sonar_tracing_langchain:LangChainAdapter"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/sonar_tracing_langchain"]
@@ -0,0 +1,98 @@
1
+ """LangChain / LangGraph adapter for sonar-tracing.
2
+
3
+ LangChain is not OpenTelemetry-native. Tracing comes from a third-party
4
+ instrumentor, ``openinference-instrumentation-langchain``, which patches
5
+ ``langchain-core``'s callback manager. Because LangGraph runs on the same
6
+ ``langchain-core`` foundation, one instrumentor covers both - there is no
7
+ separate LangGraph adapter to write.
8
+
9
+ This is the ``instrumentor`` shape (contrast the ``native`` Strands adapter):
10
+ the real work is in ``attach()``, phase 2, because the instrumentor must be
11
+ handed a live ``TracerProvider``. ``requirements()`` has nothing to pin - the
12
+ instrumentor reads no environment configuration at import time - so it only
13
+ stamps the framework version for provenance.
14
+
15
+ Identity threading, redaction, sampling and OTLP export are all handled by core
16
+ and are framework-agnostic; the OpenInference content keys are already covered
17
+ by the default redaction patterns. Ingest normalises the emitted OpenInference
18
+ attributes via the ``openinference@1`` mapping pack.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib.util
24
+
25
+ from sonar_tracing.adapters.base import AttachContext, AttachResult, Requirements
26
+ from sonar_tracing.semconv import FAMILY_OPENINFERENCE, SONAR_FRAMEWORK_VERSION
27
+
28
+ __all__ = ["LangChainAdapter"]
29
+
30
+
31
+ class LangChainAdapter:
32
+ name = "langchain"
33
+ semconv_family = FAMILY_OPENINFERENCE
34
+ mapping_pack = "openinference@1"
35
+
36
+ def __init__(self) -> None:
37
+ self._instrumentor: object | None = None
38
+
39
+ def detect(self) -> bool:
40
+ return importlib.util.find_spec("langchain_core") is not None
41
+
42
+ def requirements(self) -> Requirements:
43
+ resource: dict[str, str] = {}
44
+ version = _langchain_core_version()
45
+ if version:
46
+ resource[SONAR_FRAMEWORK_VERSION] = version
47
+ return Requirements(resource_attributes=resource)
48
+
49
+ def attach(self, ctx: AttachContext) -> AttachResult:
50
+ try:
51
+ from openinference.instrumentation.langchain import LangChainInstrumentor
52
+ except ImportError as exc:
53
+ return AttachResult(
54
+ ok=False,
55
+ mechanism="instrumentor",
56
+ detail=(
57
+ "openinference-instrumentation-langchain is not installed; "
58
+ f"cannot instrument LangChain ({exc})"
59
+ ),
60
+ )
61
+
62
+ instrumentor = LangChainInstrumentor()
63
+ instrumentor.instrument(tracer_provider=ctx.tracer_provider)
64
+ self._instrumentor = instrumentor
65
+
66
+ version = _langchain_core_version() or "unknown"
67
+ return AttachResult(
68
+ ok=True,
69
+ mechanism="instrumentor",
70
+ detail=(
71
+ f"langchain-core {version}: OpenInference LangChainInstrumentor installed; "
72
+ "covers LangChain and LangGraph via the shared callback manager"
73
+ ),
74
+ emitted_span_kinds=frozenset(
75
+ {"llm", "tool", "agent", "retrieval", "embedding", "chain"}
76
+ ),
77
+ )
78
+
79
+ def detach(self) -> None:
80
+ instrumentor = self._instrumentor
81
+ if instrumentor is None:
82
+ return
83
+ self._instrumentor = None
84
+ instrumentor.uninstrument()
85
+
86
+ def handles(self) -> list[object]:
87
+ # The instrumentor auto-patches langchain-core's callback manager, so
88
+ # LangGraph call sites need no hand-wired callback. Nothing to hand back.
89
+ return []
90
+
91
+
92
+ def _langchain_core_version() -> str | None:
93
+ try:
94
+ from importlib.metadata import version
95
+
96
+ return version("langchain-core")
97
+ except Exception: # noqa: BLE001
98
+ return None
@@ -0,0 +1,110 @@
1
+ """The LangChain adapter, as its own package.
2
+
3
+ The instrumentor is mocked: these tests pin the adapter's contract and its
4
+ attach/detach behaviour without requiring a live LangChain runtime.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+ import logging
11
+ import sys
12
+ import types
13
+
14
+ import pytest
15
+ from sonar_tracing.adapters.base import AttachContext
16
+ from sonar_tracing.config import Settings
17
+ from sonar_tracing.testing import assert_adapter_contract
18
+ from sonar_tracing_langchain import LangChainAdapter
19
+
20
+
21
+ class _FakeInstrumentor:
22
+ """Records the OpenTelemetry instrumentor lifecycle."""
23
+
24
+ last: _FakeInstrumentor | None = None
25
+
26
+ def __init__(self) -> None:
27
+ self.instrument_calls: list[object] = []
28
+ self.uninstrument_calls = 0
29
+ _FakeInstrumentor.last = self
30
+
31
+ def instrument(self, *, tracer_provider: object) -> None:
32
+ self.instrument_calls.append(tracer_provider)
33
+
34
+ def uninstrument(self) -> None:
35
+ self.uninstrument_calls += 1
36
+
37
+
38
+ @pytest.fixture
39
+ def fake_instrumentor(monkeypatch: pytest.MonkeyPatch) -> type[_FakeInstrumentor]:
40
+ """Install a fake ``openinference.instrumentation.langchain`` module."""
41
+ module = types.ModuleType("openinference.instrumentation.langchain")
42
+ module.LangChainInstrumentor = _FakeInstrumentor # type: ignore[attr-defined]
43
+ monkeypatch.setitem(sys.modules, "openinference.instrumentation.langchain", module)
44
+ _FakeInstrumentor.last = None
45
+ return _FakeInstrumentor
46
+
47
+
48
+ def _ctx() -> AttachContext:
49
+ provider = object()
50
+ return AttachContext(
51
+ tracer_provider=provider, # type: ignore[arg-type] # only passed through
52
+ settings=Settings(service_name="lc-test"),
53
+ logger=logging.getLogger("test"),
54
+ )
55
+
56
+
57
+ def test_langchain_adapter_satisfies_the_contract() -> None:
58
+ assert_adapter_contract(LangChainAdapter())
59
+
60
+
61
+ def test_attach_installs_the_openinference_instrumentor(
62
+ fake_instrumentor: type[_FakeInstrumentor],
63
+ ) -> None:
64
+ ctx = _ctx()
65
+ adapter = LangChainAdapter()
66
+ result = adapter.attach(ctx)
67
+
68
+ assert result.ok
69
+ assert result.mechanism == "instrumentor"
70
+ assert result.emitted_span_kinds # non-empty
71
+ assert fake_instrumentor.last is not None
72
+ assert fake_instrumentor.last.instrument_calls == [ctx.tracer_provider]
73
+
74
+
75
+ def test_detach_uninstruments_and_is_idempotent(
76
+ fake_instrumentor: type[_FakeInstrumentor],
77
+ ) -> None:
78
+ adapter = LangChainAdapter()
79
+
80
+ # Safe before any attach().
81
+ adapter.detach()
82
+
83
+ adapter.attach(_ctx())
84
+ instrumentor = fake_instrumentor.last
85
+ assert instrumentor is not None
86
+
87
+ adapter.detach()
88
+ adapter.detach() # second call must not uninstrument again
89
+ assert instrumentor.uninstrument_calls == 1
90
+
91
+
92
+ def test_attach_reports_failure_when_the_instrumentor_is_missing(
93
+ monkeypatch: pytest.MonkeyPatch,
94
+ ) -> None:
95
+ """framework="langchain" loads the adapter regardless of detect(); a missing
96
+ instrumentor must degrade to ok=False, not raise."""
97
+ monkeypatch.setitem(sys.modules, "openinference.instrumentation.langchain", None)
98
+ result = LangChainAdapter().attach(_ctx())
99
+ assert result.ok is False
100
+ assert result.mechanism == "instrumentor"
101
+
102
+
103
+ def test_requirements_pins_no_env_and_reports_a_version() -> None:
104
+ requirements = LangChainAdapter().requirements()
105
+ # Nothing to pin at import time - the instrumentor reads no env config.
106
+ assert requirements.env == {}
107
+ # When langchain-core is installed (a declared dependency), its version is
108
+ # stamped for provenance.
109
+ if importlib.util.find_spec("langchain_core") is not None:
110
+ assert "sonar.framework.version" in requirements.resource_attributes