-
Notifications
You must be signed in to change notification settings - Fork 9.8k
feat: add Braintrust tracing integration #11677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
cfa6ceb
06963f5
fe49c18
4f1f80a
17cb447
5c3ed29
5b05217
aadc3a2
c8bd542
cfb8677
d2a9ae3
10058a5
e948e91
f63db8c
ce5f5ca
1e8783c
6e02eca
b40b5dd
921aedc
1736e4f
3b8a496
a49d9c5
ca56ac3
aa718f3
7f2e2d1
1dd0a4d
8e62c45
09bcabb
b05ebad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| --- | ||
| title: Braintrust | ||
| slug: /integrations-braintrust | ||
| --- | ||
|
|
||
| import Tabs from '@theme/Tabs'; | ||
| import TabItem from '@theme/TabItem'; | ||
|
|
||
| [Braintrust](https://www.braintrust.com) is an end-to-end platform for building AI applications, providing logging, tracing, evaluation, and prompt management. Braintrust helps developers debug, analyze, and optimize their AI systems with detailed span trees, token metrics, and scoring. | ||
|
|
||
| This guide explains how to configure Langflow to collect tracing data about your flow executions and automatically send the data to Braintrust. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - A [Braintrust account](https://www.braintrust.com) and [API key](https://www.braintrust.com/docs/reference/api-keys) | ||
| - A [running Langflow server](/get-started-installation) with a [flow](/concepts-flows) that you want to trace | ||
|
|
||
| :::tip | ||
| If you need a flow to test the Braintrust integration, see the [Langflow quickstart](/get-started-quickstart). | ||
| ::: | ||
|
|
||
| ## Set Braintrust credentials as environment variables {#braintrust-credentials} | ||
|
|
||
| 1. Get your API key from your [Braintrust settings](https://www.braintrust.com/app/settings/api-keys). | ||
|
|
||
| 2. Set your Braintrust API key as an environment variable in the same environment where you run Langflow. | ||
|
|
||
| <Tabs> | ||
| <TabItem value="linux" label="Linux/macOS" default> | ||
|
|
||
| ```bash | ||
| export BRAINTRUST_API_KEY=YOUR_API_KEY | ||
| ``` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code blocks are missing the Per coding guidelines, code blocks must include a As per coding guidelines: "Code blocks must include a title attribute and specify the language". Also applies to: 38-40, 52-58, 63-65, 75-77 🤖 Prompt for AI Agents |
||
|
|
||
| </TabItem> | ||
| <TabItem value="windows" label="Windows"> | ||
|
|
||
| ```bash | ||
| set BRAINTRUST_API_KEY=YOUR_API_KEY | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| Replace `YOUR_API_KEY` with your Braintrust API key. | ||
|
|
||
| 3. Optionally, set additional configuration: | ||
|
|
||
| <Tabs> | ||
| <TabItem value="linux" label="Linux/macOS" default> | ||
|
|
||
| ```bash | ||
| # Override the default Braintrust API URL (defaults to https://api.braintrust.dev) | ||
| export BRAINTRUST_API_URL=https://api.braintrust.dev | ||
|
|
||
| # Set a project name (defaults to "Langflow") | ||
| export BRAINTRUST_PROJECT=my-langflow-project | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="windows" label="Windows"> | ||
|
|
||
| ```bash | ||
| set BRAINTRUST_API_URL=https://api.braintrust.dev | ||
| set BRAINTRUST_PROJECT=my-langflow-project | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ## Start Langflow and view traces in Braintrust | ||
|
|
||
| 1. Start Langflow in the same environment where you set the Braintrust environment variables: | ||
|
|
||
| ```bash | ||
| uv run langflow run | ||
| ``` | ||
|
|
||
| 2. Run a flow. | ||
|
|
||
| Langflow automatically collects and sends tracing data about the flow execution to Braintrust. Each flow run creates a trace with nested spans for every component execution and LangChain operation, including LLM calls with token usage metrics. | ||
|
|
||
| 3. View the collected data in your [Braintrust dashboard](https://www.braintrust.com/app). | ||
|
|
||
| ## Disable Braintrust tracing | ||
|
|
||
| To disable the Braintrust integration, remove the [Braintrust environment variables](#braintrust-credentials), and then restart Langflow. | ||
|
|
||
| ## See also | ||
|
|
||
| * [Braintrust documentation](https://www.braintrust.com/docs) | ||
| * [Braintrust GitHub repository](https://github.com/braintrustdata/braintrust-sdk) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| """Braintrust tracer for Langflow. | ||
|
|
||
| Implements Langflow's BaseTracer interface to send component-level | ||
| traces to Braintrust. | ||
|
|
||
| Only depends on the ``braintrust`` package. | ||
|
|
||
| Activation: set the BRAINTRUST_API_KEY environment variable. | ||
| Optional: BRAINTRUST_API_URL, BRAINTRUST_PROJECT. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import types | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from langchain_core.documents import Document | ||
| from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage | ||
| from lfx.log.logger import logger | ||
| from typing_extensions import override | ||
|
|
||
| from langflow.schema.data import Data | ||
| from langflow.schema.message import Message | ||
| from langflow.services.tracing.base import BaseTracer | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Sequence | ||
| from uuid import UUID | ||
|
|
||
| from langchain.callbacks.base import BaseCallbackHandler | ||
| from lfx.graph.vertex.base import Vertex | ||
|
|
||
| from langflow.services.tracing.schema import Log | ||
|
|
||
|
|
||
| class BraintrustTracer(BaseTracer): | ||
| """Traces Langflow flow executions to Braintrust. | ||
|
|
||
| This tracer creates a root span for each flow run and child spans for | ||
| each component execution. Each Langflow component (prompt, model, | ||
| tool, retriever, etc.) is captured as a span with its inputs, outputs, | ||
| metadata, and any errors. | ||
|
|
||
| Only depends on the ``braintrust`` package. | ||
| """ | ||
|
|
||
| flow_id: str | ||
|
|
||
| def __init__( | ||
| self, | ||
| trace_name: str, | ||
| trace_type: str, | ||
| project_name: str, | ||
| trace_id: UUID, | ||
| user_id: str | None = None, | ||
| session_id: str | None = None, | ||
| ) -> None: | ||
| self.trace_name = trace_name | ||
| self.trace_type = trace_type | ||
| self.trace_id = trace_id | ||
| self.user_id = user_id | ||
| self.session_id = session_id | ||
| self.flow_id = trace_name.split(" - ")[-1] | ||
| self.spans: dict[str, Any] = {} | ||
|
|
||
| config = self._get_config() | ||
| self._ready: bool = self._setup_braintrust(config, project_name) if config else False | ||
|
Comment on lines
+57
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constructor does not accept The 🤖 Prompt for AI Agents |
||
|
|
||
| @property | ||
| def ready(self) -> bool: | ||
| return self._ready | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Setup | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def _setup_braintrust(self, config: dict[str, Any], project_name: str) -> bool: | ||
| try: | ||
| from braintrust import init_logger | ||
|
|
||
| project = config.pop("project", None) or project_name or "Langflow" | ||
| self._logger = init_logger( | ||
| project=project, | ||
| api_key=config.get("api_key"), | ||
| app_url=config.get("api_url"), | ||
| ) | ||
|
|
||
| # Create a root span for this flow execution | ||
| self._root_span = self._logger.start_span( | ||
| name=self.flow_id, | ||
| input={ | ||
| "trace_name": self.trace_name, | ||
| "trace_type": self.trace_type, | ||
| }, | ||
| metadata={ | ||
| "langflow_trace_id": str(self.trace_id), | ||
| "langflow_trace_name": self.trace_name, | ||
| "user_id": self.user_id, | ||
| "session_id": self.session_id, | ||
| "created_from": "langflow", | ||
| }, | ||
| ) | ||
| except ImportError: | ||
| logger.exception("Could not import braintrust. Please install it with `pip install braintrust`.") | ||
| return False | ||
| except Exception as e: # noqa: BLE001 | ||
| logger.debug(f"Error setting up Braintrust tracer: {e}") | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # BaseTracer interface | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @override | ||
| def add_trace( | ||
| self, | ||
| trace_id: str, | ||
| trace_name: str, | ||
| trace_type: str, | ||
| inputs: dict[str, Any], | ||
| metadata: dict[str, Any] | None = None, | ||
| vertex: Vertex | None = None, | ||
| ) -> None: | ||
| if not self._ready: | ||
| return | ||
|
|
||
| name = trace_name.removesuffix(f" ({trace_id})") | ||
| processed_inputs = self._convert_to_loggable(inputs) if inputs else {} | ||
| processed_metadata = self._convert_to_loggable(metadata) if metadata else {} | ||
|
|
||
| processed_metadata["from_langflow_component"] = True | ||
| processed_metadata["component_id"] = trace_id | ||
| if trace_type: | ||
| processed_metadata["trace_type"] = trace_type | ||
|
|
||
| span = self._root_span.start_span( | ||
| name=name, | ||
| input=processed_inputs, | ||
| metadata=processed_metadata, | ||
| ) | ||
|
|
||
| self.spans[trace_id] = span | ||
|
|
||
| @override | ||
| def end_trace( | ||
| self, | ||
| trace_id: str, | ||
| trace_name: str, | ||
| outputs: dict[str, Any] | None = None, | ||
| error: Exception | None = None, | ||
| logs: Sequence[Log | dict] = (), | ||
| ) -> None: | ||
| if not self._ready: | ||
| return | ||
|
|
||
| span = self.spans.pop(trace_id, None) | ||
| if span is None: | ||
| logger.warning(f"Braintrust: no span found for trace_id={trace_id}") | ||
| return | ||
|
|
||
| output: dict[str, Any] = {} | ||
| output |= self._convert_to_loggable(outputs) if outputs else {} | ||
| if logs: | ||
| output["logs"] = [self._convert_to_loggable(log) if isinstance(log, dict) else str(log) for log in logs] | ||
|
|
||
| span.log( | ||
| output=output, | ||
| error=str(error) if error else None, | ||
| ) | ||
| span.end() | ||
|
|
||
| @override | ||
| def end( | ||
| self, | ||
| inputs: dict[str, Any], | ||
| outputs: dict[str, Any], | ||
| error: Exception | None = None, | ||
| metadata: dict[str, Any] | None = None, | ||
| ) -> None: | ||
| if not self._ready: | ||
| return | ||
|
|
||
| self._root_span.log( | ||
| input=self._convert_to_loggable(inputs) if inputs else {}, | ||
| output=self._convert_to_loggable(outputs) if outputs else {}, | ||
| error=str(error) if error else None, | ||
| metadata=self._convert_to_loggable(metadata) if metadata else {}, | ||
| ) | ||
| self._root_span.end() | ||
|
|
||
| @override | ||
| def get_langchain_callback(self) -> BaseCallbackHandler | None: | ||
| return None | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Helpers | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def _convert_to_loggable(self, value: Any) -> Any: | ||
| """Recursively convert Langflow/LangChain types to JSON-serializable values.""" | ||
| if isinstance(value, dict): | ||
| return {str(k): self._convert_to_loggable(v) for k, v in value.items() if k is not None} | ||
| if isinstance(value, list): | ||
| return [self._convert_to_loggable(v) for v in value] | ||
| if isinstance(value, Message): | ||
| return value.text | ||
| if isinstance(value, Data): | ||
| return value.get_text() | ||
| if isinstance(value, (BaseMessage, HumanMessage, SystemMessage)): | ||
| return value.content | ||
| if isinstance(value, Document): | ||
| return value.page_content | ||
| if isinstance(value, (types.GeneratorType, types.NoneType)): | ||
| return str(value) | ||
| return value | ||
|
|
||
| @staticmethod | ||
| def _get_config() -> dict[str, Any]: | ||
| """Read Braintrust configuration from environment variables. | ||
|
|
||
| Returns an empty dict if the required BRAINTRUST_API_KEY is not set. | ||
| """ | ||
| api_key = os.getenv("BRAINTRUST_API_KEY") | ||
| if not api_key: | ||
| return {} | ||
|
|
||
| config: dict[str, Any] = {"api_key": api_key} | ||
|
|
||
| api_url = os.getenv("BRAINTRUST_API_URL") | ||
| if api_url: | ||
| config["api_url"] = api_url | ||
|
|
||
| project = os.getenv("BRAINTRUST_PROJECT") | ||
| if project: | ||
| config["project"] = project | ||
|
|
||
| return config | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,12 @@ def _get_traceloop_tracer(): | |
| return TraceloopTracer | ||
|
|
||
|
|
||
| def _get_braintrust_tracer(): | ||
| from langflow.services.tracing.braintrust import BraintrustTracer | ||
|
|
||
| return BraintrustTracer | ||
|
|
||
|
|
||
| trace_context_var: ContextVar[TraceContext | None] = ContextVar("trace_context", default=None) | ||
| component_context_var: ContextVar[ComponentTraceContext | None] = ContextVar("component_trace_context", default=None) | ||
|
|
||
|
|
@@ -220,6 +226,17 @@ def _initialize_traceloop_tracer(self, trace_context: TraceContext) -> None: | |
| session_id=trace_context.session_id, | ||
| ) | ||
|
|
||
| def _initialize_braintrust_tracer(self, trace_context: TraceContext) -> None: | ||
| if self.deactivated: | ||
| return | ||
| braintrust_tracer = _get_braintrust_tracer() | ||
| trace_context.tracers["braintrust"] = braintrust_tracer( | ||
| trace_name=trace_context.run_name, | ||
| trace_type="chain", | ||
| project_name=trace_context.project_name, | ||
| trace_id=trace_context.run_id, | ||
| ) | ||
|
Comment on lines
+229
to
+240
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Other tracers that support user/session context (e.g., Proposed fix braintrust_tracer = _get_braintrust_tracer()
trace_context.tracers["braintrust"] = braintrust_tracer(
trace_name=trace_context.run_name,
trace_type="chain",
project_name=trace_context.project_name,
trace_id=trace_context.run_id,
+ user_id=trace_context.user_id,
+ session_id=trace_context.session_id,
)🤖 Prompt for AI Agents |
||
|
|
||
| async def start_tracers( | ||
| self, | ||
| run_id: UUID, | ||
|
|
@@ -247,6 +264,7 @@ async def start_tracers( | |
| self._initialize_arize_phoenix_tracer(trace_context) | ||
| self._initialize_opik_tracer(trace_context) | ||
| self._initialize_traceloop_tracer(trace_context) | ||
| self._initialize_braintrust_tracer(trace_context) | ||
| except Exception as e: # noqa: BLE001 | ||
| await logger.adebug(f"Error initializing tracers: {e}") | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frontmatter is missing
descriptionandsidebar_position.As per coding guidelines, YAML frontmatter must include
title,description, andsidebar_position.Proposed fix
As per coding guidelines: "Markdown files must include YAML frontmatter with title, description, and sidebar_position".
📝 Committable suggestion
🤖 Prompt for AI Agents