Laminar is an open-source observability platform purpose-built for AI agents.
- Tracing. Docs
- OpenTelemetry-native powerful tracing SDK - 1 line of code to automatically trace Vercel AI SDK, Browser Use, Stagehand, LangChain, OpenAI, Anthropic, Gemini, and more.
- Signals. Docs
- Describe any behavior of your agent that you want to track in plain English (e.g. "agent is stuck in a loop")
- Laminar reads every agent run and pings you in Slack when it happens.
- Evals. Docs
- Unopinionated, extensible SDK and CLI for running evals locally or in CI/CD pipeline.
- UI for visualizing evals and comparing results.
- MCP / CLI access for your coding agent
- Query traces, spans, metrics, and events with SQL
- Let your coding agent investigate and debug issues based on your traces
- Dashboards. Docs
- Powerful dashboard builder for traces, metrics, and events with support of custom SQL queries.
- Data annotation & Datasets. Docs
- Custom data rendering UI for fast data annotation and dataset creation for evals.
- Extremely high performance.
- Written in Rust 🦀
- 20x trace compression for efficient ingestion and storage. Read more about it here.
- Custom realtime engine for viewing traces as they happen.
- Ultra-fast full-text search over span data.
- gRPC exporter for tracing data.
Check out the full documentation here laminar.sh/docs.
The fastest and easiest way to get started is with our managed platform -> laminar.sh
Laminar is very easy to self-host locally. For a quick start, clone the repo and start the services with docker compose:
git clone https://github.com/lmnr-ai/lmnr
cd lmnr
docker compose up -dThis will spin up a lightweight but full-featured version of the stack. This is good for a quickstart or for lightweight usage. You can access the UI at http://localhost:5667 in your browser.
You will also need to properly configure the SDK, with baseUrl and correct ports. See guide on self-hosting.
For production environment, we recommend using our managed platform or docker compose -f docker-compose-full.yml up -d.
The ClickHouse configuration disables internal telemetry logs that are not useful
for a single-node installation and retains query and error logs for three days.
After pulling this change, recreate the ClickHouse container to apply the
configuration. The examples below use docker-compose.yml; pass -f with the
Compose file used by your deployment when it differs:
docker compose up -d --force-recreate clickhouseDisabling a system log stops new writes but does not clear an existing table. To reclaim its disk space while preserving the table structure, truncate the disabled log tables after ClickHouse restarts:
docker compose exec -T clickhouse clickhouse-client --multiquery <<'SQL'
TRUNCATE TABLE IF EXISTS system.trace_log;
TRUNCATE TABLE IF EXISTS system.text_log;
TRUNCATE TABLE IF EXISTS system.part_log;
TRUNCATE TABLE IF EXISTS system.metric_log;
TRUNCATE TABLE IF EXISTS system.asynchronous_metric_log;
TRUNCATE TABLE IF EXISTS system.background_schedule_pool_log;
TRUNCATE TABLE IF EXISTS system.query_metric_log;
TRUNCATE TABLE IF EXISTS system.query_thread_log;
TRUNCATE TABLE IF EXISTS system.query_views_log;
TRUNCATE TABLE IF EXISTS system.session_log;
TRUNCATE TABLE IF EXISTS system.crash_log;
TRUNCATE TABLE IF EXISTS system.opentelemetry_span_log;
TRUNCATE TABLE IF EXISTS system.zookeeper_log;
TRUNCATE TABLE IF EXISTS system.blob_storage_log;
TRUNCATE TABLE IF EXISTS system.processors_profile_log;
TRUNCATE TABLE IF EXISTS system.asynchronous_insert_log;
SQLWhen ClickHouse applies the new TTL to an existing query_log or error_log,
it may rename the previous table with a numeric suffix such as query_log_0.
Inspect any suffixed tables before dropping them if you also want to reclaim
their historical data.
Frontend AI features (chat-with-trace, SQL-with-AI) and server-side AI workers require an LLM provider. Configure one in your .env file at the repo root.
Pick one of the following provider setups. LLM_MODEL_SMALL|MEDIUM|LARGE are optional — per-provider defaults apply when unset. LLM_DEFAULT_HEADERS_JSON is optional for any provider or gateway that requires static headers.
# Optional for any provider/gateway that requires static headers
# LLM_DEFAULT_HEADERS_JSON='{"X-Gateway-Tenant":"tenant"}'
# Option A: Gemini
LLM_PROVIDER=gemini
LLM_API_KEY=your_gemini_key
# Option B: OpenAI (or any OpenAI-compatible gateway such as LiteLLM, OpenRouter, vLLM)
LLM_PROVIDER=openai
# LLM_BASE_URL=http://localhost:4000 # optional, for OpenAI-compatible gateways
LLM_API_KEY=your_openai_key
# Option C: AWS Bedrock (Anthropic Claude). Uses AWS credentials instead of LLM_API_KEY.
LLM_PROVIDER=bedrock
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
# Option D: Azure AI Foundry. One resource serves three API shapes; pick the one
# matching your deployment. Model ids are deployment names, so set LLM_MODEL_*
# unless your deployments are named after the models.
# azure_chat_completions -> <resource>.services.ai.azure.com/openai/v1/chat/completions
# azure_responses -> <resource>.services.ai.azure.com/openai/v1/responses
# azure_anthropic -> <resource>.services.ai.azure.com/anthropic/v1/messages (Claude)
LLM_PROVIDER=azure_chat_completions
LLM_API_KEY=your_azure_key
AZURE_RESOURCE_ID=your_resource_name # or AZURE_BASE_URL for private endpoints
# AZURE_API_VERSION=preview # optional; only for resources that require itBy default Laminar uses the public schema. To target a different schema (e.g.
when deploying alongside other services in a shared Postgres instance), set the
same value for both the frontend and the app-server:
POSTGRES_SCHEMA=laminar
# Set to false if the schema is pre-provisioned or the DB role lacks CREATE.
# POSTGRES_CREATE_SCHEMA=trueThe schema is applied as the connection search_path, so all tables, foreign
keys, and migrations target it. When a non-public schema is set, the frontend
also tracks migrations inside that schema (<schema>.__drizzle_migrations)
rather than the shared drizzle schema. Note that running Laminar alongside
another Drizzle-managed service in the same database may still require manual
intervention, since Drizzle's migration journal is versioned per-schema.
Self-hosted deployments collect anonymized usage telemetry. To opt out, set LAMINAR_TELEMETRY_DISABLED=true in your .env.
For shipped Docker Compose stacks, set the variable in the repository-root .env and recreate the frontend container. For a manually run frontend or Helm deployment, set it in the frontend process or pod environment.
For running and building Laminar locally, or to learn more about docker compose files, follow the guide in Contributing.
First, create a project and generate a project API key. Then,
npm add @lmnr-ai/lmnrIt will install Laminar TS SDK and all instrumentation packages (OpenAI, Anthropic, LangChain ...)
To start tracing LLM calls just add
import { Laminar } from '@lmnr-ai/lmnr';
Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY });To trace inputs / outputs of functions use observe wrapper.
import { OpenAI } from 'openai';
import { observe } from '@lmnr-ai/lmnr';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const poemWriter = observe({name: 'poemWriter'}, async (topic) => {
const response = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: `write a poem about ${topic}` }],
});
return response.choices[0].message.content;
});
await poemWriter();First, create a project and generate a project API key. Then,
pip install --upgrade 'lmnr[all]'It will install Laminar Python SDK and all instrumentation packages. See list of all instruments here
To start tracing LLM calls just add
from lmnr import Laminar
Laminar.initialize(project_api_key="<LMNR_PROJECT_API_KEY>")To trace inputs / outputs of functions use @observe() decorator.
import os
from openai import OpenAI
from lmnr import observe, Laminar
Laminar.initialize(project_api_key="<LMNR_PROJECT_API_KEY>")
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
@observe() # annotate all functions you want to trace
def poem_writer(topic):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": f"write a poem about {topic}"},
],
)
poem = response.choices[0].message.content
return poem
if __name__ == "__main__":
print(poem_writer(topic="laminar flow"))To learn more about instrumenting your code, check out our client libraries:

