Skip to content

Commit fbf0503

Browse files
joaomdmouraclaude
andcommitted
fix: emit coding_agent as a span attribute, not a Resource attribute
The previous commit set coding_agent on the OTel Resource. Verified against the telemetry ClickHouse instance that this would have silently produced nothing: across 2,000,000 sampled spans the `process` column contains exactly one key, `serviceName`, and no row has more than one. The ingestion pipeline discards every other resource attribute. Replace it with CommonAttributesSpanProcessor, whose on_start hook applies the attribute to every span the provider emits. Span attributes are preserved through ingestion and land in the `tags` array alongside crew_key and crewai_version, which is where existing extraction reads from. - Add CommonAttributesSpanProcessor, a SpanProcessor that applies a fixed attribute set at span start. Attribute application is wrapped so a failure can never propagate into user execution. - Remove the now-redundant explicit coding_agent attributes from the Crew Created and Flow Creation spans; the processor covers all spans. - Replace the resource-attribute test with an end-to-end one that exports four differently-named spans through a real TracerProvider and asserts coding_agent survives on each, and that it is NOT on the resource. - Add a test asserting the processor swallows attribute-application errors. Verified at runtime that the real Telemetry provider installs the processor and that an emitted span carries {'coding_agent': 'claude_code', 'crew_key': 'abc'} with resource {'service.name': 'crewAI-telemetry'}. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
1 parent 98e7d48 commit fbf0503

2 files changed

Lines changed: 109 additions & 35 deletions

File tree

lib/crewai/src/crewai/telemetry/telemetry.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
from typing import TYPE_CHECKING, Any
2222

2323
from opentelemetry import trace
24+
from opentelemetry.context import Context
2425
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
2526
OTLPSpanExporter,
2627
)
2728
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
28-
from opentelemetry.sdk.trace import TracerProvider
29+
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider
2930
from opentelemetry.sdk.trace.export import (
3031
BatchSpanProcessor,
3132
SpanExportResult,
@@ -88,6 +89,57 @@ def export(self, spans: Any) -> SpanExportResult:
8889
return SpanExportResult.FAILURE
8990

9091

92+
class CommonAttributesSpanProcessor(SpanProcessor):
93+
"""Applies a fixed set of attributes to every span at start.
94+
95+
Used for process-wide context that should appear on all spans (e.g. which
96+
AI coding assistant is running the process) without each span-emitting
97+
method having to set it. Attributes are applied as span attributes rather
98+
than Resource attributes because the ingestion pipeline preserves only
99+
serviceName from the resource.
100+
"""
101+
102+
def __init__(self, attributes: dict[str, str]) -> None:
103+
"""Initialize the processor.
104+
105+
Args:
106+
attributes: Attributes applied to every span. Values must not
107+
contain user data - this is process-wide context only.
108+
"""
109+
self._attributes = attributes
110+
111+
def on_start(
112+
self, span: Span, parent_context: Context | None = None
113+
) -> None:
114+
"""Apply the common attributes to a span as it starts.
115+
116+
Args:
117+
span: The span being started.
118+
parent_context: Parent context, unused.
119+
"""
120+
try:
121+
span.set_attributes(self._attributes)
122+
except Exception: # noqa: S110 - telemetry must never break execution
123+
pass
124+
125+
def on_end(self, span: Any) -> None:
126+
"""No-op; export is handled by the batch processor."""
127+
128+
def shutdown(self) -> None:
129+
"""No-op; this processor holds no resources."""
130+
131+
def force_flush(self, timeout_millis: int = 30000) -> bool:
132+
"""No-op flush.
133+
134+
Args:
135+
timeout_millis: Unused.
136+
137+
Returns:
138+
Always True.
139+
"""
140+
return True
141+
142+
91143
class Telemetry:
92144
"""Handle anonymous telemetry for the CrewAI package.
93145
@@ -123,19 +175,20 @@ def __init__(self) -> None:
123175
return
124176

125177
try:
126-
# coding_agent is set on the Resource so it is attached to *every*
127-
# span this provider emits, without per-method duplication. The value
128-
# is one of a fixed set of literals from detect_coding_agent() and
129-
# never contains environment values or any user data.
130178
self.resource = Resource(
131-
attributes={
132-
SERVICE_NAME: CREWAI_TELEMETRY_SERVICE_NAME,
133-
"coding_agent": detect_coding_agent(),
134-
},
179+
attributes={SERVICE_NAME: CREWAI_TELEMETRY_SERVICE_NAME},
135180
)
136181
with suppress_warnings():
137182
self.provider = TracerProvider(resource=self.resource)
138183

184+
# coding_agent is applied as a *span attribute* via on_start, not as
185+
# a Resource attribute: the ingestion pipeline only preserves
186+
# serviceName from the resource, so anything else set there is
187+
# dropped before it reaches storage. Span attributes are preserved.
188+
self.provider.add_span_processor(
189+
CommonAttributesSpanProcessor({"coding_agent": detect_coding_agent()})
190+
)
191+
139192
processor = BatchSpanProcessor(
140193
SafeOTLPSpanExporter(
141194
endpoint=f"{CREWAI_TELEMETRY_BASE_URL}/v1/traces",
@@ -293,7 +346,6 @@ def _operation() -> None:
293346
version("crewai"),
294347
)
295348
self._add_attribute(span, "python_version", platform.python_version())
296-
self._add_attribute(span, "coding_agent", detect_coding_agent())
297349
add_crew_attributes(span, crew, self._add_attribute)
298350
self._add_attribute(span, "crew_process", crew.process)
299351
self._add_attribute(span, "crew_memory", crew.memory)
@@ -963,7 +1015,6 @@ def _operation() -> None:
9631015
span = tracer.start_span("Flow Creation")
9641016
self._add_attribute(span, "crewai_version", version("crewai"))
9651017
self._add_attribute(span, "flow_name", flow_name)
966-
self._add_attribute(span, "coding_agent", detect_coding_agent())
9671018
close_span(span)
9681019

9691020
self._safe_telemetry_operation(_operation)

lib/crewai/tests/telemetry/test_coding_agent_detection.py

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -127,31 +127,54 @@ def test_known_agents_contains_no_pii_shaped_values():
127127
assert len(name) <= 32, name
128128

129129

130-
def test_coding_agent_attached_to_telemetry_resource(clean_env, monkeypatch):
131-
"""The attribute must land on the Resource, so it reaches every span."""
132-
import os
133-
from unittest.mock import patch
130+
def test_coding_agent_lands_on_every_exported_span(clean_env):
131+
"""End-to-end: the attribute must appear as a *span attribute* on any span.
134132
135-
from crewai.telemetry.telemetry import Telemetry
136-
137-
clean_env.setenv("CLAUDECODE", "1")
138-
139-
with (
140-
patch.dict(
141-
os.environ,
142-
{
143-
"CREWAI_DISABLE_TELEMETRY": "false",
144-
"CREWAI_DISABLE_TRACKING": "false",
145-
"OTEL_SDK_DISABLED": "false",
146-
},
147-
),
148-
patch("crewai.telemetry.telemetry.TracerProvider"),
149-
):
150-
telemetry = Telemetry()
151-
telemetry._initialized = False
152-
telemetry.__init__()
153-
154-
assert telemetry.resource.attributes["coding_agent"] == "claude_code"
133+
It cannot be a Resource attribute - the ingestion pipeline preserves only
134+
serviceName from the resource, so anything else set there is dropped before
135+
it reaches storage. This test exports through a real TracerProvider and
136+
asserts the attribute survives on arbitrary spans.
137+
"""
138+
from opentelemetry.sdk.trace import TracerProvider
139+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
140+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
141+
InMemorySpanExporter,
142+
)
143+
144+
from crewai.telemetry.telemetry import CommonAttributesSpanProcessor
145+
146+
exporter = InMemorySpanExporter()
147+
provider = TracerProvider()
148+
provider.add_span_processor(
149+
CommonAttributesSpanProcessor({"coding_agent": "claude_code"})
150+
)
151+
provider.add_span_processor(SimpleSpanProcessor(exporter))
152+
153+
tracer = provider.get_tracer("crewai.telemetry")
154+
for name in ("Crew Created", "Task Execution", "Tool Usage", "Feature Usage"):
155+
span = tracer.start_span(name)
156+
span.end()
157+
158+
exported = exporter.get_finished_spans()
159+
assert len(exported) == 4
160+
for span in exported:
161+
assert span.attributes["coding_agent"] == "claude_code", span.name
162+
163+
# It must be a span attribute, not a resource attribute, or ingestion drops it.
164+
assert "coding_agent" not in exported[0].resource.attributes
165+
166+
167+
def test_common_attributes_processor_never_breaks_span_creation(clean_env):
168+
"""A failure applying attributes must not propagate into user execution."""
169+
from crewai.telemetry.telemetry import CommonAttributesSpanProcessor
170+
171+
class ExplodingSpan:
172+
def set_attributes(self, _):
173+
raise RuntimeError("boom")
174+
175+
CommonAttributesSpanProcessor({"coding_agent": "cursor"}).on_start(
176+
ExplodingSpan() # type: ignore[arg-type]
177+
)
155178

156179

157180
def test_coding_agent_span_emits_once(clean_env, monkeypatch):

0 commit comments

Comments
 (0)