sonar-tracing-strands 0.1.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,51 @@
1
+ Metadata-Version: 2.5
2
+ Name: sonar-tracing-strands
3
+ Version: 0.1.0
4
+ Summary: AWS Strands Agents 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,opentelemetry,sonar,strands,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: sonar-tracing>=0.1
20
+ Requires-Dist: strands-agents>=1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # sonar-tracing-strands
26
+
27
+ AWS Strands Agents adapter for [`sonar-tracing`](https://pypi.org/project/sonar-tracing/).
28
+
29
+ Installing this package makes the `strands` framework available to
30
+ `sonar-tracing` — the adapter is discovered automatically via the
31
+ `sonar_tracing.adapters` entry point group, so there's nothing to import or wire
32
+ up by hand.
33
+
34
+ ```bash
35
+ pip install sonar-tracing-strands
36
+ ```
37
+
38
+ ```python
39
+ import sonar_tracing as tracing
40
+
41
+ tracing.init(service_name="support-agent", framework="strands")
42
+ ```
43
+
44
+ Strands is OpenTelemetry-native, so this adapter installs no instrumentation. Its
45
+ job is to pin the semantic-convention vocabulary Strands reads at import time and
46
+ stamp the active convention onto the OTel Resource so the ingest side never has
47
+ to guess.
48
+
49
+ ## License
50
+
51
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,27 @@
1
+ # sonar-tracing-strands
2
+
3
+ AWS Strands Agents adapter for [`sonar-tracing`](https://pypi.org/project/sonar-tracing/).
4
+
5
+ Installing this package makes the `strands` 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-strands
12
+ ```
13
+
14
+ ```python
15
+ import sonar_tracing as tracing
16
+
17
+ tracing.init(service_name="support-agent", framework="strands")
18
+ ```
19
+
20
+ Strands is OpenTelemetry-native, so this adapter installs no instrumentation. Its
21
+ job is to pin the semantic-convention vocabulary Strands reads at import time and
22
+ stamp the active convention onto the OTel Resource so the ingest side never has
23
+ to guess.
24
+
25
+ ## License
26
+
27
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "sonar-tracing-strands"
3
+ version = "0.1.0"
4
+ description = "AWS Strands Agents 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", "strands", "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
+ "strands-agents>=1.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8"]
29
+
30
+ # Discovered by sonar-tracing's registry via this entry point group. The adapter
31
+ # module path is independent of sonar_tracing's package namespace.
32
+ [project.entry-points."sonar_tracing.adapters"]
33
+ strands = "sonar_tracing_strands:StrandsAdapter"
34
+
35
+ [build-system]
36
+ requires = ["hatchling"]
37
+ build-backend = "hatchling.build"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/sonar_tracing_strands"]
@@ -0,0 +1,115 @@
1
+ """AWS Strands Agents adapter for sonar-tracing.
2
+
3
+ Strands is OpenTelemetry-native. ``strands.telemetry.tracer.Tracer`` calls
4
+ ``trace_api.get_tracer_provider()`` itself and instruments LLM calls, tool calls,
5
+ retrievers and event-loop cycles against whatever provider it finds. It only
6
+ bootstraps its own provider if the application explicitly constructs
7
+ ``StrandsTelemetry``.
8
+
9
+ So the correct integration is: own the global provider, then get out of the way.
10
+ ``attach()`` here installs nothing. That is the expected outcome for a native
11
+ framework, not a gap - and it is useful evidence that the adapter interface is
12
+ not over-fitted to instrumentor-shaped frameworks.
13
+
14
+ The real work is in ``requirements()``, phase 1. Strands' Tracer reads
15
+ OTEL_SEMCONV_STABILITY_OPT_IN in its constructor and caches the result for the
16
+ process lifetime, choosing between two incompatible attribute vocabularies:
17
+
18
+ default gen_ai.usage.prompt_tokens / completion_tokens
19
+ gen_ai_latest_experimental gen_ai.usage.input_tokens / output_tokens
20
+
21
+ We pin the newer one so the data is consistent across services, and stamp which
22
+ convention is active on the Resource so ingest never has to guess. The mapping
23
+ pack handles both vocabularies regardless, via ordered fallbacks - pinning is
24
+ about consistency, not about the mapper's capability.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import importlib.util
30
+ import os
31
+ import sys
32
+
33
+ from sonar_tracing.adapters.base import AttachContext, AttachResult, Requirements
34
+ from sonar_tracing.semconv import FAMILY_GEN_AI, SONAR_FRAMEWORK_VERSION
35
+
36
+ __all__ = ["StrandsAdapter"]
37
+
38
+ SEMCONV_OPT_IN_VAR = "OTEL_SEMCONV_STABILITY_OPT_IN"
39
+ DEFAULT_OPT_IN = "gen_ai_latest_experimental"
40
+
41
+ # Override with SONAR_STRANDS_SEMCONV_OPT_IN="" to leave Strands on its default.
42
+ OPT_IN_OVERRIDE_VAR = "SONAR_STRANDS_SEMCONV_OPT_IN"
43
+
44
+
45
+ class StrandsAdapter:
46
+ name = "strands"
47
+ semconv_family = FAMILY_GEN_AI
48
+ mapping_pack = "strands@1"
49
+
50
+ def detect(self) -> bool:
51
+ return importlib.util.find_spec("strands") is not None
52
+
53
+ def requirements(self) -> Requirements:
54
+ env: dict[str, str] = {}
55
+ warnings: list[str] = []
56
+ resource: dict[str, str] = {}
57
+
58
+ version = _strands_version()
59
+ if version:
60
+ resource[SONAR_FRAMEWORK_VERSION] = version
61
+
62
+ desired = os.environ.get(OPT_IN_OVERRIDE_VAR, DEFAULT_OPT_IN)
63
+ existing = os.environ.get(SEMCONV_OPT_IN_VAR)
64
+ if existing:
65
+ warnings.append(
66
+ f"{SEMCONV_OPT_IN_VAR}={existing!r} is already set; leaving it alone. "
67
+ "The strands@1 mapping pack normalises either vocabulary."
68
+ )
69
+ resource["sonar.strands.semconv_opt_in"] = existing
70
+ elif desired:
71
+ env[SEMCONV_OPT_IN_VAR] = desired
72
+ resource["sonar.strands.semconv_opt_in"] = desired
73
+
74
+ if _tracer_singleton_exists():
75
+ warnings.append(
76
+ "strands.telemetry.tracer was already constructed before tracing.init(). "
77
+ "Spans will still be captured (OTel's proxy provider resolves late), but "
78
+ "environment-derived Strands settings were already read and cannot be "
79
+ "changed. Call tracing.init() before creating any Agent."
80
+ )
81
+
82
+ return Requirements(env=env, resource_attributes=resource, warnings=tuple(warnings))
83
+
84
+ def attach(self, ctx: AttachContext) -> AttachResult:
85
+ version = _strands_version() or "unknown"
86
+ return AttachResult(
87
+ ok=True,
88
+ mechanism="native",
89
+ detail=(
90
+ f"strands {version}: no instrumentation installed; the framework "
91
+ f"resolves the global TracerProvider itself"
92
+ ),
93
+ emitted_span_kinds=frozenset({"agent", "llm", "tool", "retrieval"}),
94
+ )
95
+
96
+ def detach(self) -> None:
97
+ return None
98
+
99
+ def handles(self) -> list[object]:
100
+ return []
101
+
102
+
103
+ def _strands_version() -> str | None:
104
+ try:
105
+ from importlib.metadata import version
106
+
107
+ return version("strands-agents")
108
+ except Exception: # noqa: BLE001
109
+ return None
110
+
111
+
112
+ def _tracer_singleton_exists() -> bool:
113
+ """True if Strands already built its Tracer, meaning env config is frozen."""
114
+ module = sys.modules.get("strands.telemetry.tracer")
115
+ return module is not None and getattr(module, "_tracer_instance", None) is not None
@@ -0,0 +1,42 @@
1
+ """The Strands adapter, as its own package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from sonar_tracing.testing import assert_adapter_contract
7
+ from sonar_tracing_strands import StrandsAdapter
8
+
9
+
10
+ def test_strands_adapter_satisfies_the_contract() -> None:
11
+ assert_adapter_contract(StrandsAdapter())
12
+
13
+
14
+ def test_strands_installs_no_instrumentation() -> None:
15
+ """A native framework needs the provider to exist and nothing else.
16
+
17
+ An adapter that does nothing is a valid adapter. If this ever starts
18
+ wrapping or re-instrumenting Strands, something has gone wrong.
19
+ """
20
+ adapter = StrandsAdapter()
21
+ result = adapter.attach(ctx=None) # type: ignore[arg-type] # unused by this adapter
22
+ assert result.ok
23
+ assert result.mechanism == "native"
24
+
25
+
26
+ def test_strands_pins_the_semconv_vocabulary_before_the_provider_is_built(
27
+ monkeypatch: pytest.MonkeyPatch,
28
+ ) -> None:
29
+ """Phase 1 exists because Strands reads this env var in its constructor
30
+ and caches the result for the process lifetime."""
31
+ monkeypatch.delenv("OTEL_SEMCONV_STABILITY_OPT_IN", raising=False)
32
+ requirements = StrandsAdapter().requirements()
33
+ assert requirements.env["OTEL_SEMCONV_STABILITY_OPT_IN"] == "gen_ai_latest_experimental"
34
+
35
+
36
+ def test_an_operator_set_semconv_var_is_never_clobbered(
37
+ monkeypatch: pytest.MonkeyPatch,
38
+ ) -> None:
39
+ monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_span_attributes_only")
40
+ requirements = StrandsAdapter().requirements()
41
+ assert "OTEL_SEMCONV_STABILITY_OPT_IN" not in requirements.env
42
+ assert any("already set" in w for w in requirements.warnings)