Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.11"

- name: Install tox
run: python -m pip install --upgrade pip tox
Expand All @@ -44,7 +44,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.11"

- name: Install tox
run: python -m pip install --upgrade pip tox
Expand All @@ -59,8 +59,6 @@ jobs:
fail-fast: false
matrix:
include:
- python-version: "3.10"
tox-env: pytest-py310
- python-version: "3.11"
tox-env: pytest-py311
- python-version: "3.12"
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ docs/_build/
docs/api/
*.log
*_output.txt
docs/_build/
docs/api/
28 changes: 13 additions & 15 deletions PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,26 +135,24 @@ All GenAI instrumentations in this package (both direct dependencies and interna
- Track upstream progress and maintain a checklist of gaps between the internal implementation and the contrib version


## Phase 6: A365 Convergence
## Phase 6: A365 Integration

The A365 observability runtime will be **migrated as code into this repository**, not consumed as a PyPI dependency. This includes the A365 exporter, custom processors, and all relevant observability extensions.
~~The A365 observability runtime is consumed as **PyPI dependencies** from the `microsoft-agents-a365-observability-core` package published by the Agent365-python repository. The distro does not vendor A365 code — it imports scope classes, span processors, and the exporter directly from the external packages.~~

### A365 Code to Migrate In-Repo
### ~~Integration Approach~~

- **A365 exporter** — integrate into the distro configuration surface as an in-repo module
- **Microsoft Agent Framework instrumentation** — instrumentation for Microsoft's internal agent framework, brought in as source code
- **Baggage extensions** — A365 baggage propagation and enrichment extensions
- **Custom span processors** — processors required by A365 agent observability scenarios
- **Other internal observability extensions** — any remaining A365 runtime components needed for agent workloads
- ~~**External dependency** — `microsoft-agents-a365-observability-core` provides all A365 scope classes, data models, the Agent365 exporter, enriching batch span processor, baggage span processor, and constants~~
- ~~**Distro-specific token resolvers** — FIC and DefaultAzureCredential token resolution logic lives in the distro (`_a365_utils.py`) since it is specific to the distro's authentication strategy~~
- ~~**Identity stamping** — A lightweight `_A365IdentityProcessor` in the distro stamps `tenant_id` and `agent_id` on every span from distro configuration / environment variables~~
- ~~**Baggage propagation** — The external package's `SpanProcessor` handles copying OpenTelemetry baggage entries onto span attributes~~
- ~~**Configuration surface** — `use_microsoft_opentelemetry(enable_a365=True)` kwargs (`a365_tenant_id`, `a365_agent_id`, `a365_cluster_category`, etc.) and corresponding `A365_*` environment variables~~

### Migration and Convergence Plan
### Remaining Work

- Migrate A365 observability runtime code under a clearly defined internal module boundary (e.g., `_a365/`)
- **TODO: Investigate Spectra Collector sidecar export path.** The old standalone `config.py` supported a `SpectraExporterOptions` code path that created a standard OTLP exporter (gRPC or HTTP) to a Kubernetes sidecar on localhost (`localhost:4317`/`localhost:4318`). This was dropped during distro integration — need to determine if Spectra sidecar export is actively used, and if so, add it as a distro option (e.g., `a365_exporter_options` kwarg or a dedicated `enable_spectra` flag). The `SpectraExporterOptions` config class still exists but is currently dead code. Re-adding this path would also require restoring the `opentelemetry-exporter-otlp-proto-grpc` dependency.
- The A365 LangChain instrumentation is partially consumed in Phase 5 as one of the sources for the internal LangChain instrumentation — coordinate with the A365 team to avoid divergence
- Audit migrated A365 instrumentations and determine which can be contributed to upstream OpenTelemetry contrib — for those with existing upstream equivalents, plan a migration path and deprecation timeline; for those with no upstream equivalent (e.g., Microsoft Agent Framework), keep as Microsoft-specific extensions and evaluate contributing them
- Validate that existing A365 telemetry pipelines continue to work under the new distro setup with the in-repo code
- Coordinate with the A365 team on dual maintenance during the transition period (similar to the Azure Monitor Distro approach)
- **TODO: gRPC dependency bloat from `microsoft-agents-a365-observability-core`.** The external package hard-depends on `opentelemetry-exporter-otlp` (the meta-package), which transitively pulls in `opentelemetry-exporter-otlp-proto-grpc` and `grpcio` (~20-40 MB). This distro only uses `opentelemetry-exporter-otlp-proto-http`. File an issue / PR on Agent365-python to change their core dependency from `opentelemetry-exporter-otlp` to `opentelemetry-exporter-otlp-proto-http` and move gRPC to an optional extra (e.g., `pip install microsoft-agents-a365-observability-core[grpc]`).
- **TODO: Document and enforce provider initialization order.** The a365 `TelemetryManager.configure()` checks for an existing global `TracerProvider` via `trace.get_tracer_provider()` — if one exists with a resource, it adds its span processors to it; otherwise it creates its own and calls `set_tracer_provider()`. This means `use_microsoft_opentelemetry()` **must** be called before a365 `configure()`, or the distro's providers will overwrite a365's and its processors will be lost. Add documentation, a startup check or warning, and integration tests covering both orderings.
- Track external package version updates and update the distro's minimum version pin accordingly
- Validate that existing A365 telemetry pipelines continue to work under the new distro setup with the external packages

## Phase 7: Integration and End-to-End Testing

Expand Down
50 changes: 13 additions & 37 deletions docs/MIGRATION_A365.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ This migration covers only the **observability** packages. Other A365 packages

## Step 1 — Replace pip Dependencies

Remove the standalone A365 observability packages and install the distro:
Remove the standalone A365 observability extension packages and install the distro.
The core A365 package (`microsoft-agents-a365-observability-core`) is kept as a
dependency — the distro uses it directly:

```
# ❌ OLD — multiple observability packages
Expand All @@ -23,20 +25,20 @@ pip install microsoft-agents-a365-observability-extensions-openai
pip install microsoft-agents-a365-observability-extensions-semantickernel
pip install microsoft-agents-a365-observability-extensions-agentframework

# ✅ NEW — single package
# ✅ NEW — single package (includes observability-core as a dependency)
pip install microsoft-opentelemetry
```

## Step 2 — Rewrite Import Paths

The old packages used `microsoft_agents_a365.*` namespace.
The new distro uses `microsoft.opentelemetry.*`.
Scope classes, data models, and enums are still imported from the
`microsoft_agents_a365.observability.core` package — no import path changes
needed for those. Only the configuration and setup calls change.

### Core (observability-core)
### Configuration & Setup

| Old import path | New import path |
|-----------------|-----------------|
| `microsoft_agents_a365.observability.core` | `microsoft.opentelemetry.a365.core` |
| `from microsoft_agents_a365.observability.core import configure` | `from microsoft.opentelemetry import use_microsoft_opentelemetry` |
| `from microsoft_agents_a365.observability.core import is_configured` | Remove — not needed |
| `from microsoft_agents_a365.observability.core import get_tracer` | `from opentelemetry import trace; trace.get_tracer(...)` |
Expand All @@ -51,10 +53,12 @@ The new distro uses `microsoft.opentelemetry.*`.
| `from microsoft_agents_a365.observability.core import extract_context_from_headers` | Remove — use OTel propagation APIs |
| `from microsoft_agents_a365.observability.core import get_traceparent` | Remove — use OTel propagation APIs |

Scope classes, data models, and enums keep working — just change the package prefix:
### Scope Classes & Data Models — No Changes Needed

Scope classes, data models, and enums are imported from the same package as before:

```python
# ❌ OLD
# ✅ These imports remain unchanged
from microsoft_agents_a365.observability.core import (
AgentDetails,
BaggageBuilder,
Expand All @@ -81,34 +85,6 @@ from microsoft_agents_a365.observability.core import (
ToolType,
UserDetails,
)

# ✅ NEW — same symbols, different package path
from microsoft.opentelemetry.a365.core import (
AgentDetails,
BaggageBuilder,
CallerDetails,
Channel,
ChatMessage,
ExecuteToolScope,
InferenceCallDetails,
InferenceOperationType,
InferenceScope,
InputMessages,
InvokeAgentScope,
InvokeAgentScopeDetails,
MessageRole,
OutputMessage,
OutputMessages,
OutputScope,
Request,
Response,
ServiceEndpoint,
SpanDetails,
TextPart,
ToolCallDetails,
ToolType,
UserDetails,
)
```

### Extensions — LangChain (observability-extensions-langchain)
Expand Down Expand Up @@ -258,7 +234,7 @@ tracer = get_tracer("my-module")

# ✅ NEW — using microsoft-opentelemetry distro
from microsoft.opentelemetry import use_microsoft_opentelemetry
from microsoft.opentelemetry.a365.core import (
from microsoft_agents_a365.observability.core import (
AgentDetails,
BaggageBuilder,
InvokeAgentScope,
Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "microsoft-opentelemetry"
version = "0.1.0a1"
description = "Minimal template for the Microsoft OpenTelemetry distro for Python"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"azure-core<2.0.0,>=1.28.0",
"azure-core-tracing-opentelemetry~=1.0.0b11",
Expand All @@ -30,8 +30,7 @@ dependencies = [
"langchain-core>=0.2.0",
"wrapt>=1.0",
"opentelemetry-util-genai>=0.3b0",
"microsoft-agents-activity>=0.9.0",
"microsoft-agents-hosting-core>=0.9.0",
"microsoft-agents-a365-observability-core>=0.3.0.dev4",
"aiohttp>=3.8.0"
Comment thread
rads-1996 marked this conversation as resolved.
]

Expand Down Expand Up @@ -66,10 +65,10 @@ namespaces = true

[tool.black]
line-length = 120
target-version = ["py310"]
target-version = ["py311"]

[tool.pylint.main]
py-version = "3.10"
py-version = "3.11"

[tool.pylint.format]
max-line-length = 120
Expand Down
2 changes: 1 addition & 1 deletion samples/a365/manual_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import time

from microsoft.opentelemetry import use_microsoft_opentelemetry
from microsoft.opentelemetry.a365.core import (
from microsoft_agents_a365.observability.core import (
AgentDetails,
BaggageBuilder,
CallerDetails,
Expand Down
48 changes: 24 additions & 24 deletions samples/langchain/validate_traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def shutdown(self):
pass


def main(): # pylint: disable=too-many-statements
def main(): # pylint: disable=too-many-statements
# Enable content capture for validation (must be set before instrumentation)
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai_latest_experimental"
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "SPAN_ONLY"
Expand Down Expand Up @@ -67,10 +67,16 @@ def main(): # pylint: disable=too-many-statements
from langchain_core.output_parsers import StrOutputParser

llm2 = FakeListLLM(responses=["42 is the answer."])
chain = ChatPromptTemplate.from_messages([
("system", "You are helpful."),
("human", "{q}"),
]) | llm2 | StrOutputParser()
chain = (
ChatPromptTemplate.from_messages(
[
("system", "You are helpful."),
("human", "{q}"),
]
)
| llm2
| StrOutputParser()
)
chain.invoke({"q": "What is 6*7?"})

# ── Sample 3: Tool invocation ───────────────────────────────────────
Expand Down Expand Up @@ -110,12 +116,9 @@ def check(label, condition, detail=""):
check("gen_ai.operation.name present", "gen_ai.operation.name" in attrs)
check("gen_ai.operation.name == 'chat'", attrs.get("gen_ai.operation.name") == "chat")
check("gen_ai.provider.name present", "gen_ai.provider.name" in attrs)
check("SpanKind is CLIENT", span.kind == SpanKind.CLIENT,
f"actual: {span.kind.name}")
check("Span name is not 'chat None'", "None" not in span.name,
f"actual: '{span.name}'")
check("gen_ai.response.finish_reasons present",
"gen_ai.response.finish_reasons" in attrs)
check("SpanKind is CLIENT", span.kind == SpanKind.CLIENT, f"actual: {span.kind.name}")
check("Span name is not 'chat None'", "None" not in span.name, f"actual: '{span.name}'")
check("gen_ai.response.finish_reasons present", "gen_ai.response.finish_reasons" in attrs)

# ── Tool span checks ───────────────────────────────────────────────
print(f"\n{'='*60}")
Expand All @@ -126,22 +129,19 @@ def check(label, condition, detail=""):
attrs = span.attributes
print(f"\n Span: {span.name}")
check("gen_ai.operation.name present", "gen_ai.operation.name" in attrs)
check("gen_ai.operation.name == 'execute_tool'",
attrs.get("gen_ai.operation.name") == "execute_tool")
check("gen_ai.operation.name == 'execute_tool'", attrs.get("gen_ai.operation.name") == "execute_tool")
check("gen_ai.tool.name present", "gen_ai.tool.name" in attrs)
check("gen_ai.tool.type == 'function'",
attrs.get("gen_ai.tool.type") == "function",
f"actual: {attrs.get('gen_ai.tool.type')}")
check(
"gen_ai.tool.type == 'function'",
attrs.get("gen_ai.tool.type") == "function",
f"actual: {attrs.get('gen_ai.tool.type')}",
)
check("gen_ai.tool.description present", "gen_ai.tool.description" in attrs)
check("SpanKind is INTERNAL", span.kind == SpanKind.INTERNAL,
f"actual: {span.kind.name}")
check("Span name format 'execute_tool <name>'",
span.name.startswith("execute_tool "))
check("SpanKind is INTERNAL", span.kind == SpanKind.INTERNAL, f"actual: {span.kind.name}")
check("Span name format 'execute_tool <name>'", span.name.startswith("execute_tool "))
# Content capture enabled — args/results should be present
check("gen_ai.tool.call.arguments present (content on)",
"gen_ai.tool.call.arguments" in attrs)
check("gen_ai.tool.call.result present (content on)",
"gen_ai.tool.call.result" in attrs)
check("gen_ai.tool.call.arguments present (content on)", "gen_ai.tool.call.arguments" in attrs)
check("gen_ai.tool.call.result present (content on)", "gen_ai.tool.call.result" in attrs)

# ── All-span attribute dump ─────────────────────────────────────────
print(f"\n{'='*60}")
Expand Down
Loading
Loading