Skip to content

feat(telemetry): add native OpenTelemetry tracing for Crews, Flows, and Tools - #6691

Open
glatinone wants to merge 1 commit into
crewAIInc:mainfrom
glatinone:feat/opentelemetry-tracing
Open

feat(telemetry): add native OpenTelemetry tracing for Crews, Flows, and Tools#6691
glatinone wants to merge 1 commit into
crewAIInc:mainfrom
glatinone:feat/opentelemetry-tracing

Conversation

@glatinone

Copy link
Copy Markdown

Introduces optional OpenTelemetry tracer wrapper and minimal no-op tests.

…op tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Telemetry tracing

Layer / File(s) Summary
Provider initialization and no-op fallback
lib/crewai/src/crewai/utilities/telemetry.py
Optional OpenTelemetry imports, environment-controlled provider setup, OTLP exporter configuration, and no-op tracing primitives are added.
Tracing API and validation
lib/crewai/src/crewai/utilities/telemetry.py, lib/crewai/tests/test_telemetry.py
get_tracer() and record_exception() are added, with tests covering disabled telemetry and enabled telemetry without OpenTelemetry installed.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the new OpenTelemetry telemetry support added in this PR.
Description check ✅ Passed The description matches the changeset by describing the optional tracer wrapper and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (trivial_assertion). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 97981ed and ff745ba.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/utilities/telemetry.py
  • lib/crewai/tests/test_telemetry.py

Comment on lines +39 to +42
def _ensure_provider() -> None:
global _PROVIDER_READY
if _PROVIDER_READY or not _OTEL_AVAILABLE or not _enabled():
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 || true

Repository: 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)
PY

Repository: 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}")
PY

Repository: 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.

Comment on lines +44 to +56
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 || true

Repository: 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 2

Repository: 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.py

Repository: 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 || true

Repository: 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))
PY

Repository: 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:


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.

Comment on lines +79 to +81
class _NoopTracer:
def start_as_current_span(self, _name: str):
return _NoopSpanCtx()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.py

Repository: 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)
PY

Repository: 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:


🌐 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:


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.

Suggested change
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.

Comment on lines +4 to +21
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 || true

Repository: 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' || true

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant