Boundary Studio
For 2025 Q1, Boundary Studio is free for new accounts!
Boundary Studio 2 will be released in 2025 Q2 with a new pricing model.
To enable observability with BAML, you’ll first need to sign up for a Boundary Studio account.
Once you’ve signed up, you’ll be able to create a new project and get your API key.
Then simply add the following environment variable prior to running your application:
There you’ll be able to see all the metrics and logs from your application including:
- Cost
- Function calls
- Execution time
- Token Usage
- Prompt Logs
- and more…
Tracing Custom Events
BAML allows you to trace any function with the @trace decorator. This will make the function’s input and output show up in the Boundary dashboard. This works for any python function you define yourself. BAML LLM functions (or any other function declared in a .baml file) are already traced by default. Logs are only sent to the Dashboard if you setup your environment variables correctly.
Example
In the example below, we trace each of the two functions pre_process_text and full_analysis:
This allows us to see each function invocation, as well as all its children in the dashboard:
Adding custom tags
The dashboard view allows you to see custom tags for each of the function calls. This is useful for adding metadata to your traces and allow you to query your generated logs more easily.
To add a custom tag, you can import set_tags(..) as below:
Tags on BAML calls and retrieving them with the Collector
You can also set tags directly on a BAML function call and then retrieve them from the Collector. Tags from a parent trace are inherited by the BAML function call and merged with any function-specific tags you pass.
Python
TypeScript
Go
Notes:
- Tags from
set_tags/setTagson a parenttraceare merged into the BAML function’s tags. - Per-call tags are provided via
baml_optionsin Python and the options object in TypeScript; in Go useb.WithTags(map[string]string). - Retrieve tags from a
FunctionLogusinglog.tags(Python/TypeScript) orlog.Tags()(Go).
Tracing with ThreadPoolExecutor (Python)
When using Python’s concurrent.futures.ThreadPoolExecutor, traced functions submitted to the thread pool will start with fresh, independent tracing contexts. This is by design and differs from async/await execution.
Expected Behavior
In the trace hierarchy, you’ll see:
parent_functionas a root trace (depth 1)worker_functionas an independent root trace (depth 1) - not a childprocess_dataas a child ofworker_function(depth 2)
Why This Happens
Python’s contextvars (used for tracing context) don’t automatically propagate to thread pool threads. Each worker thread starts with a fresh context to:
- Avoid complexity with context sharing across threads
- Prevent potential race conditions
- Maintain clear thread boundaries
Best Practices
- Use async/await for related work: If you need to maintain parent-child relationships for parallel execution, use
asyncioinstead of thread pools:
-
Understand the trace hierarchy: When debugging, remember that thread pool workers appear as separate root traces in your observability dashboard.
-
Tags don’t propagate: Tags set in the parent function won’t automatically appear in thread pool workers since they have independent contexts.