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
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
OpenTelemetry CrewAI Instrumentation
====================================

This package provides the setup for instrumenting CrewAI with OpenTelemetry
Generative AI semantic conventions. CrewAI operation instrumentation will be
added in follow-up changes.
This package instruments CrewAI LLM calls using the OpenTelemetry Generative AI
semantic conventions. It emits inference spans and client duration and token
usage metrics from CrewAI's public LLM lifecycle events.

Installation
------------
Expand All @@ -24,10 +24,39 @@ Usage
Configuration
-------------

CrewAI's native telemetry is disabled while this instrumentation is active to
avoid emitting two independent sets of spans. To retain CrewAI's native
telemetry, explicitly enable it before instrumenting::

export CREWAI_DISABLE_TELEMETRY=false

An existing ``CREWAI_DISABLE_TELEMETRY`` value is always preserved. When the
instrumentation supplies the default value, it removes that value again during
``uninstrument()``.

By default, prompts and completions are not captured. To capture message content, set the
environment variable ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` to one of
``NO_CONTENT``, ``SPAN_ONLY``, ``EVENT_ONLY``, or ``SPAN_AND_EVENT``:

::

export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT

Completion hooks
----------------

To forward captured prompts and completions to the built-in upload hook, set
``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload`` and configure an
``fsspec``-compatible destination with
``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH``::

export OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload
export OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=/path/to/prompts

Install ``opentelemetry-util-genai[upload]`` to use the upload hook. A hook can
also be supplied programmatically; it takes precedence over the environment
variable::

CrewAIInstrumentor().instrument(completion_hook=my_hook)

See ``examples/custom_hook.py`` for a minimal custom hook.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Run a CrewAI agent with a custom completion hook."""

from crewai import Agent

from opentelemetry.instrumentation.genai.crewai import CrewAIInstrumentor
from opentelemetry.util.genai.completion_hook import CompletionHook
from opentelemetry.util.genai.types import (
InputMessage,
MessagePart,
OutputMessage,
ToolDefinition,
)


class PrintCompletionHook(CompletionHook):
"""Print content after each CrewAI LLM call."""

def on_completion(
self,
*,
inputs: list[InputMessage],
outputs: list[OutputMessage],
system_instruction: list[MessagePart],
tool_definitions: list[ToolDefinition] | None = None,
span=None,
log_record=None,
) -> None:
print(f"inputs: {inputs}")
print(f"outputs: {outputs}")


CrewAIInstrumentor().instrument(completion_hook=PrintCompletionHook())

agent = Agent(
role="Assistant",
goal="Answer questions concisely",
backstory="You are a helpful assistant.",
)
print(agent.kickoff("What is OpenTelemetry?"))
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import os
from collections.abc import Collection
from typing import Any

Expand All @@ -15,6 +16,8 @@

__all__ = ["CrewAIInstrumentor"]

_CREWAI_DISABLE_TELEMETRY = "CREWAI_DISABLE_TELEMETRY"


class CrewAIInstrumentor(BaseInstrumentor):
"""An instrumentor for CrewAI."""
Expand All @@ -24,17 +27,42 @@ def instrumentation_dependencies(self) -> Collection[str]:

def _instrument(self, **kwargs: Any) -> None:
"""Enable CrewAI instrumentation."""
completion_hook = (
kwargs.get("completion_hook") or load_completion_hook()
)
TelemetryHandler(
tracer_provider=kwargs.get("tracer_provider"),
meter_provider=kwargs.get("meter_provider"),
logger_provider=kwargs.get("logger_provider"),
completion_hook=completion_hook,
self._disabled_crewai_telemetry = (
_CREWAI_DISABLE_TELEMETRY not in os.environ
)
# CrewAI patching will be added in a follow-up change.
if self._disabled_crewai_telemetry:
os.environ[_CREWAI_DISABLE_TELEMETRY] = "true"

try:
completion_hook = (
kwargs.get("completion_hook") or load_completion_hook()
)
telemetry_handler = TelemetryHandler(
tracer_provider=kwargs.get("tracer_provider"),
meter_provider=kwargs.get("meter_provider"),
logger_provider=kwargs.get("logger_provider"),
completion_hook=completion_hook,
)
from opentelemetry.instrumentation.genai.crewai.event_listener import (
CrewAIInferenceEventListener,
)

self._event_listener = CrewAIInferenceEventListener(
telemetry_handler
)
except BaseException:
self._restore_crewai_telemetry()
raise

def _uninstrument(self, **kwargs: Any) -> None:
"""Disable CrewAI instrumentation."""
# CrewAI unpatching will be added in a follow-up change.
listener = getattr(self, "_event_listener", None)
if listener is not None:
listener.shutdown()
self._event_listener = None
self._restore_crewai_telemetry()

def _restore_crewai_telemetry(self) -> None:
if getattr(self, "_disabled_crewai_telemetry", False):
os.environ.pop(_CREWAI_DISABLE_TELEMETRY, None)
self._disabled_crewai_telemetry = False
Loading
Loading