Skip to content
Open
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
8 changes: 8 additions & 0 deletions lib/crewai/src/crewai/telemetry/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ def _register_shutdown_handlers(self) -> None:

self._original_handlers: dict[int, Any] = {}

if threading.current_thread() is not threading.main_thread():
logger.debug(
"CrewAI telemetry: Skipping signal handler registration (not running in main thread). "
"To disable this warning or disable crewai telemetry entirely, set CREWAI_DISABLE_TELEMETRY=true. "
"See: https://docs.crewai.com/telemetry"
)
return

self._register_signal_handler(signal.SIGTERM, SigTermEvent, shutdown=True)
self._register_signal_handler(signal.SIGINT, SigIntEvent, shutdown=True)
if hasattr(signal, "SIGHUP"):
Expand Down
104 changes: 103 additions & 1 deletion lib/crewai/tests/telemetry/test_telemetry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import threading
from unittest.mock import patch
from unittest.mock import MagicMock ,patch

import pytest
from crewai import Agent, Crew, Task
Expand Down Expand Up @@ -121,3 +121,105 @@ def create_instance():
thread.join()

assert all(instance is telemetry1 for instance in instances)

def test_signal_handler_registration_skipped_in_non_main_thread():
"""Test that signal handler registration is skipped when running from a non-main thread.

This test verifies that when Telemetry is initialized from a non-main thread,
the signal handler registration is skipped without raising noisy ValueError tracebacks.
See: https://github.com/crewAIInc/crewAI/issues/4289
"""
Telemetry._instance = None

result = {"register_signal_handler_called": False, "error": None}

def init_telemetry_in_thread():
try:
with patch("crewai.telemetry.telemetry.TracerProvider"):
with patch.object(
Telemetry,
"_register_signal_handler",
wraps=lambda *args, **kwargs: None,
) as mock_register:
telemetry = Telemetry()
result["register_signal_handler_called"] = mock_register.called
result["telemetry"] = telemetry
except Exception as e:
result["error"] = e

thread = threading.Thread(target=init_telemetry_in_thread)
thread.start()
thread.join()

assert result["error"] is None, f"Unexpected error: {result['error']}"
assert (
result["register_signal_handler_called"] is False
), "Signal handler should not be registered in non-main thread"


def test_signal_handler_registration_skipped_logs_debug_message():
"""Test that a debug message is logged when signal handler registration is skipped.

This test verifies that when Telemetry is initialized from a non-main thread,
a debug message is logged indicating that signal handler registration was skipped.
"""
Telemetry._instance = None

result = {"telemetry": None, "error": None, "debug_calls": []}

mock_logger_debug = MagicMock()

def init_telemetry_in_thread():
try:
with patch("crewai.telemetry.telemetry.TracerProvider"):
with patch(
"crewai.telemetry.telemetry.logger.debug", mock_logger_debug
):
result["telemetry"] = Telemetry()
result["debug_calls"] = [
args[0] for args, _ in mock_logger_debug.call_args_list
]
except Exception as e:
result["error"] = e

thread = threading.Thread(target=init_telemetry_in_thread)
thread.start()
thread.join()

assert result["error"] is None, f"Unexpected error: {result['error']}"
assert result["telemetry"] is not None

debug_calls = result["debug_calls"]
assert any(
"Skipping signal handler registration (not running in main thread)." in call
for call in debug_calls
), (
"Expected debug message about skipping signal handler registration in non-main "
f"thread, got: {debug_calls}"
)
assert any(
"CREWAI_DISABLE_TELEMETRY=true" in call for call in debug_calls
), (
"Expected debug message to mention CREWAI_DISABLE_TELEMETRY=true, "
f"got: {debug_calls}"
)
assert any(
"https://docs.crewai.com/telemetry" in call for call in debug_calls
), (
"Expected debug message to include link to telemetry docs, "
f"got: {debug_calls}"
)


def test_signal_handlers_registered_in_main_thread():
"""Test that signal handlers are registered when running from the main thread."""
Telemetry._instance = None

with patch("crewai.telemetry.telemetry.TracerProvider"):
with patch(
"crewai.telemetry.telemetry.Telemetry._register_signal_handler"
) as mock_register:
telemetry = Telemetry()

assert telemetry.ready is True
assert mock_register.call_count >= 2