feat(telemetry): add native OpenTelemetry tracing for Crews, Flows, and Tools - #6691
feat(telemetry): add native OpenTelemetry tracing for Crews, Flows, and Tools#6691glatinone wants to merge 1 commit into
Conversation
…op tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📝 WalkthroughWalkthroughAdds an optional OpenTelemetry telemetry utility with lazy provider configuration, OTLP export support, no-op fallback behavior, exception recording, and tests for disabled telemetry or missing OpenTelemetry. ChangesTelemetry tracing
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai/src/crewai/utilities/telemetry.py`:
- Around line 39-42: Make _ensure_provider() thread-safe by guarding the
_PROVIDER_READY check and the complete provider construction/registration
sequence with a module-level threading lock or equivalent one-time
initialization primitive. Keep the existing early exits, but ensure concurrent
first calls cannot initialize or register duplicate providers.
- Around line 79-81: Update _NoopTracer.start_as_current_span to accept the same
optional OpenTelemetry span arguments as the real tracer, or flexibly accept
ignored positional and keyword arguments, while preserving its existing
_NoopSpanCtx return behavior.
- Around line 44-56: The _ensure_provider() setup must remain non-fatal when
OTLP is unavailable or exporter construction fails. Guard OTLPSpanExporter
creation and BatchSpanProcessor registration, catch setup failures, and call
trace.set_tracer_provider only when the provider has a configured exporter;
otherwise leave _PROVIDER_READY unset so get_tracer() returns the no-op tracer.
In `@lib/crewai/tests/test_telemetry.py`:
- Around line 4-21: Make test_get_tracer_noop_when_disabled and
test_get_tracer_no_crash_when_enabled_without_otel deterministic by isolating or
mocking the OpenTelemetry-unavailable path and resetting module-global tracer
state before each assertion. Ensure the tests exercise the fallback
implementation rather than inheriting a real provider or cached state from
crewai.utilities.telemetry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cb07f247-6781-4d1b-8b20-5e7eaaff4b16
📒 Files selected for processing (2)
lib/crewai/src/crewai/utilities/telemetry.pylib/crewai/tests/test_telemetry.py
| def _ensure_provider() -> None: | ||
| global _PROVIDER_READY | ||
| if _PROVIDER_READY or not _OTEL_AVAILABLE or not _enabled(): | ||
| return |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the telemetry file and relevant symbols.
if [ -f lib/crewai/src/crewai/utilities/telemetry.py ]; then
echo "== file exists =="
wc -l lib/crewai/src/crewai/utilities/telemetry.py
echo "== outline =="
ast-grep outline lib/crewai/src/crewai/utilities/telemetry.py || true
echo "== lines 1-120 =="
sed -n '1,120p' lib/crewai/src/crewai/utilities/telemetry.py | nl -ba
else
echo "file missing"
fd -i 'telemetry.py' .
fi
echo "== usages of _ensure_provider/_PROVIDER_READY =="
rg -n "_ensure_provider|_PROVIDER_READY|set_tracer_provider|trace.set_tracer_provider|_OTEL_AVAILABLE" lib/crewai/src/crewai/utilities/telemetry.py || trueRepository: crewAIInc/crewAI
Length of output: 973
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== telemetry.py contents =="
awk '{printf "%5d\t%s\n", NR, $0}' lib/crewai/src/crewai/utilities/telemetry.py
echo "== usages =="
rg -n "_ensure_provider|_PROVIDER_READY|set_tracer_provider|trace.set_tracer_provider|_OTEL_AVAILABLE|OTLPLoader|telemetry" . -g '!**/.git/**' || true
echo "== simple race probe of source pattern =="
python3 - <<'PY'
from pathlib import Path
p = Path('lib/crewai/src/crewai/utilities/telemetry.py')
s = p.read_text()
for term in ['_PROVIDER_READY = False', 'if _PROVIDER_READY', 'lock', 'Lock', 'set_tracer_provider', 'def _ensure_provider']:
print(f'{term}:', term in s)
PYRepository: crewAIInc/crewAI
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== telemetry.py target file contents =="
printf '%8s\t%s\n' "$(sed -n '1p' lib/crewai/src/crewai/utilities/telemetry.py | wc -c)" lines
awk '{printf "%5d\t%s\n", NR, $0}' lib/crewai/src/crewai/utilities/telemetry.py
echo "== local references to _ensure_provider/get_tracer in lib/crewai =="
rg -n "from .*telemetry import|import .*telemetry|_ensure_provider|get_tracer\(" lib/crewai/src/crewai -g '*.py' || true
echo "== minimal source invariant check =="
python3 - <<'PY'
from pathlib import Path
p = Path('lib/crewai/src/crewai/utilities/telemetry.py')
text = p.read_text()
checks = {
"has_plain_ready_flag": "_PROVIDER_READY = False" in text,
"checks_ready_before_init": "if _PROVIDER_READY or not _OTEL_AVAILABLE or not _enabled():" in text,
"has_lock_import": "from threading import Lock" in text or "import threading" in text,
"uses_lock": "Lock(" in text or "threading.Lock" in text,
"has_set_tracer_provider": "trace.set_tracer_provider" in text,
"has_trace_get_tracer": "trace.get_tracer" in text,
}
for k, v in checks.items():
print(f"{k}: {v}")
PYRepository: crewAIInc/crewAI
Length of output: 7568
Make provider initialization thread-safe.
_ensure_provider() checks _PROVIDER_READY without holding a lock, then constructs/registers the provider; concurrent first calls can create duplicate exporters and race during initialization. Guard the check-and-initialize block with a threading lock or another one-time initialization primitive.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/utilities/telemetry.py` around lines 39 - 42, Make
_ensure_provider() thread-safe by guarding the _PROVIDER_READY check and the
complete provider construction/registration sequence with a module-level
threading lock or equivalent one-time initialization primitive. Keep the
existing early exits, but ensure concurrent first calls cannot initialize or
register duplicate providers.
| service_name = os.getenv("SERVICE_NAME", "crewai") | ||
| otlp_endpoint = os.getenv("OTLP_ENDPOINT") # e.g., http://localhost:4318/v1/traces | ||
|
|
||
| resource = Resource.create({"service.name": service_name}) if Resource else None | ||
| provider = TracerProvider(resource=resource) if TracerProvider else None | ||
|
|
||
| if provider and BatchSpanProcessor and OTLPSpanExporter and otlp_endpoint: | ||
| exporter = OTLPSpanExporter(endpoint=otlp_endpoint) | ||
| provider.add_span_processor(BatchSpanProcessor(exporter)) | ||
|
|
||
| if provider and trace: | ||
| trace.set_tracer_provider(provider) | ||
| _PROVIDER_READY = True |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate telemetry.py =="
fd -a 'telemetry.py' . || true
echo "== outline =="
ast-grep outline lib/crewai/src/crewai/utilities/telemetry.py || true
echo "== relevant lines 1-130 =="
cat -n lib/crewai/src/crewai/utilities/telemetry.py | sed -n '1,130p'
echo "== usages of telemetry module/get_tracer =="
rg -n "from.*telemetry|import.*telemetry|get_tracer|_PROVIDER_READY|setup_provider" lib/crewai/src/crewai || trueRepository: crewAIInc/crewAI
Length of output: 10501
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic control-flow probe for _ensure_provider cases =="
python3 - <<'PY'
import dataclasses
`@dataclasses.dataclass`
class Case:
enabled: bool
otlp_endpoint: str | None
has_provider: bool
has_batch: bool
has_exporter: bool
has_trace: bool
exporter_raises: bool | None = None
cases = [
Case(True, None, True, True, True, True),
Case(True, "http://localhost:4318/v1/traces", True, True, True, True),
Case(True, None, False, True, True, True),
Case(True, "?", True, True, True, True),
Case(True, "x", True, False, True, True),
Case(True, "x", True, True, False, True),
Case(True, "x", True, True, True, False),
]
for case in cases:
_PROVIDER_READY = False
_OTEL_AVAILABLE = True
Resource = bool(case.has_provider)
TracerProvider = bool(case.has_provider)
BatchSpanProcessor = bool(case.has_batch)
OTLPSpanExporter = bool(case.has_exporter)
trace = bool(case.has_trace)
service_name = "crewai"
otlp_endpoint = case.otlp_endpoint
provider = None
exporter_error = None
if provider and BatchSpanProcessor and OTLPSpanExporter and case.otlp_endpoint:
provider = object()
try:
exporter = "exporter"
if case.exporter_raises:
raise ValueError("bad")
provider = object()
except ValueError as exc:
exporter_error = str(exc)
provider_ready_before_check = _PROVIDER_READY
if provider and trace:
_PROVIDER_READY = True
print("case", case)
print(" provider_ready_before_condition=", provider_ready_before_check)
print(" exporter_error=", exporter_error)
print(" _PROVIDER_READY=", _PROVIDER_READY)
print(" will_return_real_tracer=", bool(not trace or _OTEL_AVAILABLE and case.enabled and trace and _PROVIDER_READY))
PY
echo "== inspect tests for get_tracer / ensure_provider behavior =="
rg -n "_ensure_provider|get_tracer|OTLP|NoopTracer|tracer_provider|BatchSpanProcessor|OTLPSpanExporter" lib/crewai/tests lib/crewai/src/crewai/utilities/telemetry.py -C 2Repository: crewAIInc/crewAI
Length of output: 395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 -c "print(sys.version)"
python3 <<'PY'
from __future__ import annotations
import dataclasses
from typing import Optional
`@dataclasses.dataclass`
class Case:
enabled: bool
otlp_endpoint: Optional[str]
has_provider: bool
has_exporter: bool
exporter_raises: bool = False
cases = [
Case(True, None, True, True, False),
Case(True, "http://localhost:4318/v1/traces", True, True, False),
Case(True, None, True, True, True),
Case(True, "x", True, True, True),
]
for case in cases:
PROVIDER_READY = False
TraceExists = True
provider = None
exporter_configured = False
exporter_error = None
if case.has_provider and True and case.has_exporter and case.otlp_endpoint:
provider = object()
try:
exporter = object()
exporter_configured = True
provider = object()
except Exception as exc:
exporter_error = str(exc)
if provider and TraceExists:
PROVIDER_READY = True
real_tracer = TraceExists and case.enabled and PROVIDER_READY
print({"case": dataclasses.asdict(case), "exporter_configured": exporter_configured, "exporter_error": exporter_error, "provider_ready": PROVIDER_READY, "returns_real_tracer": real_tracer})
PY
echo "== tests coverage around setup/get_tracer =="
git diff -- lib/crewai/src/crewai/utilities/telemetry.py lib/crewai/tests/test_telemetry.py || true
sed -n '1,260p' lib/crewai/tests/test_telemetry.pyRepository: crewAIInc/crewAI
Length of output: 265
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 <<'PY'
from __future__ import annotations
import dataclasses
from typing import Optional
# Model lines 39-59 as data flow: provider exists only when OTEL classes are present;
# exporter is only constructed when provider, batch, exporter class, and endpoint are present.
`@dataclasses.dataclass`
class Case:
enabled: bool
otlp_endpoint: Optional[str]
has_provider: bool
has_exporter: bool
exporter_raises: bool = False
cases = [
Case(True, None, True, True, False),
Case(True, "http://localhost:4318/v1/traces", True, True, False),
Case(True, None, True, True, True),
Case(True, "x", True, True, True),
]
for case in cases:
PROVIDER_READY = False
TraceExists = True
provider = None
exporter_configured = False
exporter_error = None
if case.has_provider and True and case.has_exporter and case.otlp_endpoint:
provider = object()
try:
exporter = object()
exporter_configured = True
provider = object()
except Exception as exc:
exporter_error = str(exc)
if provider and TraceExists:
PROVIDER_READY = True
real_tracer = TraceExists and case.enabled and PROVIDER_READY
print({"case": dataclasses.asdict(case), "exporter_configured": exporter_configured, "exporter_error": exporter_error, "provider_ready": PROVIDER_READY, "returns_real_tracer": real_tracer})
PY
echo "== focused tests and any mocks =="
rg -n "get_tracer|_ensure_provider|_NOOP_TRACER|OTLP|TraceExporter|Provider|telemetry.get_tracer|utilities.telemetry" lib/crewai/tests test_telemetry.py || trueRepository: crewAIInc/crewAI
Length of output: 43927
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tests/test_telemetry.py =="
cat -n lib/crewai/tests/test_telemetry.py | sed -n '1,180p'
echo "== trace module behavior in installed OTel =="
python3 - <<'PY'
try:
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
except Exception as exc:
print("IMPORT_ERROR", type(exc).__name__, str(exc))
raise SystemExit
class TracerProviderThatRaisesDuringResourceCreation(Exception):
pass
class CustomResource(Resource):
pass
class CustomTracerProvider(TracerProvider):
def __init__(self, *_, **__):
raise TracerResourceConstructionError
class CustomOTLPSpanExporter(OTLPSpanExporter):
def __init__(self, *_, **__):
raise ValueError("bad endpoint")
print("base_provider", trace.get_tracer_provider())
# This simulates provider creation raising before set_tracer_provider:
try:
CustomTracerProvider()
except Exception as e1:
print("provider_init_error", type(e1).__name__, str(e1))
try:
trace.set_tracer_provider(CustomTracerProvider())
except Exception as e2:
print("set_tracer_provider_error", type(e2).__name__, str(e2))
print("get_tracer_after_failed_provider_init", trace.get_tracer("test"))
# This simulates exporter construction raising after get_tracer_provider and provider exists:
try:
CustomOTLPSpanExporter()
except Exception as e3:
print("exporter_init_error", type(e3).__name__, str(e3))
PYRepository: crewAIInc/crewAI
Length of output: 1138
🌐 Web query:
OpenTelemetry Python TracerProvider set_tracer_provider source code ProxyTracerProvider class
💡 Result:
In the OpenTelemetry Python API, the set_tracer_provider function is used to configure the global TracerProvider [1][2]. The implementation of this function relies on a helper method, _set_tracer_provider, which uses a thread-safe mechanism to ensure the provider is set only once [1][3]. If an attempt is made to set the provider more than once, the system logs a warning [1][4][2]. The ProxyTracerProvider class exists to enable the lazy initialization of the tracing pipeline [5]. When the application starts, if a global TracerProvider has not been set via code or environment variables, the get_tracer_provider function returns the _PROXY_TRACER_PROVIDER instance instead [1][2][3]. This proxy provider initially returns no-op implementations for tracers [5]. Once a user provides a concrete TracerProvider via set_tracer_provider, the proxies are designed to delegate operations to the actual implementation [5]. This allows instrumentation to be active and non-breaking even before the full tracing infrastructure is configured [5].
Citations:
- 1: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/trace/__init__.py
- 2: https://opentelemetry-python.readthedocs.io/en/stable/%5Fmodules/opentelemetry/trace.html
- 3: https://opentelemetry-python.readthedocs.io/en/latest/_modules/opentelemetry/trace.html
- 4: https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html
- 5: Proxy tracer/provider to enable lazy setup of tracing pipeline open-telemetry/opentelemetry-python#1726
Keep telemetry provider setup non-fatal and return the no-op tracer when OTLP is unavailable.
Currently _ensure_provider() only checks OTLP_ENDPOINT and OTLPSpanExporter before constructing the exporter, so a provider without a processor can still be registered. It also doesn’t catch failures during OTLPSpanExporter(...), which would make get_tracer() crash when telemetry is enabled. Wrap the exporter setup in a guard and only call trace.set_tracer_provider(...) after a provider with a configured exporter is ready.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/utilities/telemetry.py` around lines 44 - 56, The
_ensure_provider() setup must remain non-fatal when OTLP is unavailable or
exporter construction fails. Guard OTLPSpanExporter creation and
BatchSpanProcessor registration, catch setup failures, and call
trace.set_tracer_provider only when the provider has a configured exporter;
otherwise leave _PROVIDER_READY unset so get_tracer() returns the no-op tracer.
| class _NoopTracer: | ||
| def start_as_current_span(self, _name: str): | ||
| return _NoopSpanCtx() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate telemetry.py =="
fd -a 'telemetry\.py$' . | sed 's#^\./##'
echo
echo "== File outline =="
ast-grep outline lib/crewai/src/crewai/utilities/telemetry.py --view expanded || true
echo
echo "== Relevant content =="
cat -n lib/crewai/src/crewai/utilities/telemetry.py | sed -n '1,180p'
echo
echo "== Search start_as_current_span usages in telemetry =="
rg -n "start_as_current_span|_NoopTracer|OTel|opentelemetry" lib/crewai/src/crewai/utilities/telemetry.pyRepository: crewAIInc/crewAI
Length of output: 6138
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search get_tracer usages (current repo) =="
rg -n "get_tracer\(\)|start_as_current_span\(" -g '*.py' .
echo
echo "== Inspect telemetry.py (core variant) =="
ast-grep outline lib/crewai-core/src/crewai_core/telemetry.py --view expanded || true
cat -n lib/crewai-core/src/crewai_core/telemetry.py | sed -n '1,180p' || true
echo
echo "== Probe current installed opentelemetry signature if available =="
python3 - <<'PY'
import inspect
try:
from opentelemetry import trace
method = getattr(trace.Tracer, "start_as_current_span", None)
print("opentelemetry_installed:", bool(trace))
print("signature:", inspect.signature(method) if method else None)
print("method_source:")
print(inspect.getsource(method) if method else "not available")
except Exception as exc:
print("opentelemetry_probe_error:", type(exc).__name__, exc)
PYRepository: crewAIInc/crewAI
Length of output: 9799
🌐 Web query:
opentelemetry python trace TraceContextTracer start_as_current_span signature
💡 Result:
In OpenTelemetry Python, there is no class named TraceContextTracer [1][2][3]. The functionality often associated with trace context management and span creation involves the Tracer class and the TraceContextTextMapPropagator [1][4][3]. The method start_as_current_span is a member of the Tracer class, which is used to create a span and set it as the current span in the active context [3][5][6]. The signature for start_as_current_span is as follows [5][7]: tracer.start_as_current_span( name: str, context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True) -> Iterator[Span] To use a specific trace context (such as one extracted from incoming headers using TraceContextTextMapPropagator), you pass that context object into the context parameter of this method [1][2][8]: from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator # Extract context from a carrier (e.g., HTTP headers) propagator = TraceContextTextMapPropagator ctx = propagator.extract(carrier=headers) # Use the extracted context when starting a new span with tracer.start_as_current_span("my-span", context=ctx) as span: # span is now a child of the context extracted from the headers...
Citations:
- 1: https://opentelemetry.io/docs/languages/python/propagation/
- 2: https://opentelemetry.io/docs/languages/python/cookbook/
- 3: https://opentelemetry-python.readthedocs.io/en/stable/api/trace.html
- 4: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py
- 5: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/trace/__init__.py
- 6: https://opentelemetry-python.readthedocs.io/en/stable/%5Fmodules/opentelemetry/trace.html
- 7: https://github.com/open-telemetry/opentelemetry-python/blob/eed100c3/opentelemetry-api/src/opentelemetry/trace/__init__.py
- 8: https://stackoverflow.com/questions/68530363/opentelemetry-python-how-to-instanciate-a-new-span-as-a-child-span-for-a-given
🌐 Web query:
opentelemetry-python Tracer start_as_current_span signature record_exception set_status_on_exception end_on_exit Span
💡 Result:
In the OpenTelemetry Python API, the Tracer.start_as_current_span method is a context manager used to create a new span, make it the current span in the context, and optionally manage its lifecycle [1][2][3]. The signature for start_as_current_span is: tracer.start_as_current_span( name: str, context: Context | None = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True) -> Iterator[Span] Parameters: - record_exception (bool): When True (default), any exception raised within the context manager's block is automatically recorded on the span using the record_exception method [1][2][3]. - set_status_on_exception (bool): When True (default), the span's status is automatically set to StatusCode.ERROR if an uncaught exception is raised within the block. This does not override a status that has been set manually [2][3][4]. - end_on_exit (bool): When True (default), the span is automatically ended when the context manager exits [1][3]. The Span.record_exception method is used to record exception events on a span. It typically captures the exception object, its stack trace, and timestamps [2][5]. While the context manager can automate this (via record_exception), you can also call it manually [6][7]: span.record_exception(exception, attributes=None, timestamp=None, escaped=False) It is recommended to use record_exception in conjunction with setting the span status to ERROR using span.set_status(Status(StatusCode.ERROR)) when handling exceptions manually [6][7].
Citations:
- 1: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/trace/__init__.py
- 2: https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html
- 3: https://opentelemetry-python.readthedocs.io/en/stable/api/trace.html
- 4: https://opentelemetry-python.readthedocs.io/en/stable/%5Fmodules/opentelemetry/trace.html
- 5: https://opentelemetry-python.readthedocs.io/en/latest/_modules/opentelemetry/sdk/trace.html#Span.record_exception
- 6: https://opentelemetry.io/docs/languages/python/instrumentation/
- 7: https://opentelemetry.io/docs/languages/python/instrumentation
Make the no-op tracer accept OpenTelemetry span arguments.
_NoopTracer.start_as_current_span() only accepts name, while the real tracer.start_as_current_span() API also accepts parameters such as context, kind, attributes, links, start_time, record_exception, set_status_on_exception, and end_on_exit. Existing and future callers that pass these options will work with OTel enabled but raise TypeError when telemetry is disabled or unavailable. Mirror the real signature or accept ignored *args/**kwargs.
Suggested fix
- def start_as_current_span(self, _name: str):
+ def start_as_current_span(
+ self, _name: str, *args: Any, **kwargs: Any
+ ) -> "_NoopSpanCtx":
return _NoopSpanCtx()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class _NoopTracer: | |
| def start_as_current_span(self, _name: str): | |
| return _NoopSpanCtx() | |
| class _NoopTracer: | |
| def start_as_current_span( | |
| self, _name: str, *args: Any, **kwargs: Any | |
| ) -> "_NoopSpanCtx": | |
| return _NoopSpanCtx() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/utilities/telemetry.py` around lines 79 - 81, Update
_NoopTracer.start_as_current_span to accept the same optional OpenTelemetry span
arguments as the real tracer, or flexibly accept ignored positional and keyword
arguments, while preserving its existing _NoopSpanCtx return behavior.
| def test_get_tracer_noop_when_disabled(monkeypatch): | ||
| monkeypatch.delenv("CREWAI_OTEL_ENABLED", raising=False) | ||
| from crewai.utilities.telemetry import get_tracer | ||
|
|
||
| tracer = get_tracer() | ||
| # Should not raise when creating a span even without opentelemetry installed | ||
| with tracer.start_as_current_span("unit-test-noop"): | ||
| pass | ||
|
|
||
|
|
||
| def test_get_tracer_no_crash_when_enabled_without_otel(monkeypatch): | ||
| # Even if enabled, absence of OTel must not crash | ||
| monkeypatch.setenv("CREWAI_OTEL_ENABLED", "1") | ||
| from crewai.utilities.telemetry import get_tracer | ||
|
|
||
| tracer = get_tracer() | ||
| with tracer.start_as_current_span("unit-test-enabled-no-otel"): | ||
| pass |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)test_telemetry\.py$|(^|/)telemetry\.py$' || true
echo "== test file =="
for f in $(git ls-files | rg '(^|/)test_telemetry\.py$'); do
echo "### $f"
cat -n "$f"
done
echo "== telemetry file outline/contents =="
for f in $(git ls-files | rg '(^|/)telemetry\.py$'); do
echo "### $f"
wc -l "$f"
ast-grep outline "$f" || true
cat -n "$f"
done
echo "== other telemetry uses/tests =="
rg -n "get_tracer|CREWAI_OTEL_ENABLED|opentelemetry|StartSpan|NoOp|TracerProvider" -S .Repository: crewAIInc/crewAI
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate telemetry files =="
git ls-files | rg '(^|/)(telemetry\.py|test_telemetry\.py)$' || true
echo "== test_telemetry.py =="
f=$(git ls-files | rg '(^|/)test_telemetry\.py$' | head -n1 || true)
if [ -z "$f" ]; then
echo "NO test_telemetry.py found"
else
wc -l "$f"
cat -n "$f"
fi
echo "== candidate telemetry source files =="
while IFS= read -r f; do
echo "### $f"
wc -l "$f"
ast-grep outline "$f" || true
sed -n '1,240p' "$f" | cat -n
done < <(git ls-files | rg '(^|/)telemetry\.py$')
echo "== focused telemetry references =="
rg -n '(^from|/^import)|^def get_tracer|^class .*Tracer|^def .*Tracer|NoOp|OPENTELEMETRY|CREWAI_OTEL_ENABLED|start_as_current_span|TracerProvider' $(git ls-files | rg '(^|/)(telemetry\.py|test_telemetry\.py)$') || true
echo "== package dependencies mentioning opentelemetry =="
rg -n 'opentelemetry|otel|telemetry' pyproject.toml uv.lock requirements*.txt setup.cfg setup.py MANIFEST.in 2>/dev/null || trueRepository: crewAIInc/crewAI
Length of output: 9707
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== old test_telemetry.py content =="
wc -l lib/crewai/tests/test_telemetry.py
cat -n lib/crewai/tests/test_telemetry.py
echo "== telemetry source files length =="
for f in lib/crewai-core/src/crewai_core/telemetry.py lib/crewai/src/crewai/telemetry/telemetry.py lib/crewai/src/crewai/utilities/telemetry.py; do
if [ -f "$f" ]; then
echo "### $f"
wc -l "$f"
fi
done
echo "== telemetry source slices containing get_tracer/otel/NoOp =="
for f in lib/crewai-core/src/crewai_core/telemetry.py lib/crewai/src/crewai/telemetry/telemetry.py lib/crewai/src/crewai/utilities/telemetry.py; do
if [ -f "$f" ]; then
echo "### $f"
rg -n -C 4 'def get_tracer|get_tracer\(|NoOp|opentelemetry|OTEL_SDK_DISABLED|CREWAI_OTEL_ENABLED|start_as_current_span|TraceProvider|TracerProvider' "$f" || true
fi
done
echo "== focused diff/stat =="
git diff --stat || true
git diff -- lib/crewai/tests/test_telemetry.py lib/crewai-core/src/crewai_core/telemetry.py lib/crewai/src/crewai/telemetry/telemetry.py lib/crewai/src/crewai/utilities/telemetry.py | sed -n '1,260p' || trueRepository: crewAIInc/crewAI
Length of output: 19231
Make the telemetry fallback tests deterministic.
Both tests use the module-global tracer state, so a real OTel provider can make the “noop” test pass even when the fallback path is not exercised. Mock the unavailable dependency path explicitly, or import crewai.utilities.telemetry into an isolated namespace before asserting the no-op behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/tests/test_telemetry.py` around lines 4 - 21, Make
test_get_tracer_noop_when_disabled and
test_get_tracer_no_crash_when_enabled_without_otel deterministic by isolating or
mocking the OpenTelemetry-unavailable path and resetting module-global tracer
state before each assertion. Ensure the tests exercise the fallback
implementation rather than inheriting a real provider or cached state from
crewai.utilities.telemetry.
Introduces optional OpenTelemetry tracer wrapper and minimal no-op tests.