Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit 2cc3d2c

Browse files
committed
add laminar observability
1 parent 3a6a4be commit 2cc3d2c

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

src/observability/laminar.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/**
2+
* Laminar observability integration for MCP-use.
3+
*
4+
* This module provides automatic instrumentation and optional callback handler
5+
* for Laminar AI observability platform.
6+
*/
7+
8+
import type { AgentAction, AgentFinish } from '@langchain/core/agents'
9+
import type { Serialized } from '@langchain/core/load/serializable'
10+
import type { LLMResult } from '@langchain/core/outputs'
11+
import type { ChainValues } from '@langchain/core/utils/types'
12+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base'
13+
import { config } from 'dotenv'
14+
import { logger } from '../logging.js'
15+
16+
config()
17+
18+
// Check if Laminar is disabled via environment variable
19+
const laminarDisabled = process.env.MCP_USE_LAMINAR?.toLowerCase() === 'false'
20+
21+
// Track if Laminar is initialized for other modules to check
22+
let laminarInitialized = false
23+
let laminarHandler: BaseCallbackHandler | null = null
24+
// Initialize based on environment
25+
const initializationPromise: Promise<void> | null = (() => {
26+
// Only initialize if not disabled and API key is present
27+
if (laminarDisabled) {
28+
logger.debug('Laminar tracing disabled via MCP_USE_LAMINAR environment variable')
29+
return null
30+
}
31+
else if (!process.env.LAMINAR_PROJECT_API_KEY && !process.env.LMNR_PROJECT_API_KEY) {
32+
logger.debug(
33+
'Laminar API key not found - tracing disabled. Set LAMINAR_PROJECT_API_KEY or LMNR_PROJECT_API_KEY to enable',
34+
)
35+
return null
36+
}
37+
else {
38+
// Create initialization promise to ensure handlers are ready when needed
39+
return (async () => {
40+
try {
41+
// Dynamically import to avoid errors if package not installed
42+
const laminarModule = await import('@lmnr-ai/lmnr').catch(() => null)
43+
if (!laminarModule) {
44+
logger.debug('Laminar package not installed - tracing disabled. Install with: npm install @lmnr-ai/lmnr')
45+
return
46+
}
47+
48+
const { Laminar, observe } = laminarModule
49+
// Initialize Laminar with automatic instrumentation
50+
logger.debug('Laminar: Initializing automatic instrumentation for LangChain')
51+
52+
const config = {
53+
projectApiKey: process.env.LAMINAR_PROJECT_API_KEY || process.env.LMNR_PROJECT_API_KEY,
54+
baseUrl: process.env.LAMINAR_BASE_URL || process.env.LMNR_BASE_URL,
55+
}
56+
57+
Laminar.initialize(config)
58+
laminarInitialized = true
59+
logger.debug('Laminar observability initialized successfully with LangChain instrumentation')
60+
61+
// Create a simple LangChain callback handler for Laminar (optional, as auto-instrumentation handles most)
62+
class LaminarCallbackHandler extends BaseCallbackHandler {
63+
name = 'LaminarCallbackHandler'
64+
65+
constructor() {
66+
super()
67+
logger.debug('Laminar: Created custom LangChain callback handler')
68+
}
69+
70+
async handleLLMStart(
71+
llm: Serialized,
72+
prompts: string[],
73+
_runId?: string,
74+
_parentRunId?: string,
75+
_extraParams?: Record<string, unknown>,
76+
_tags?: string[],
77+
_metadata?: Record<string, unknown>,
78+
): Promise<void> {
79+
logger.debug('Laminar CallbackHandler: LLM start')
80+
if (prompts && prompts.length > 0) {
81+
const preview = prompts[0].slice(0, 100)
82+
logger.debug(`Laminar CallbackHandler: Prompts preview: ${preview}${prompts[0].length > 100 ? '...' : ''}`)
83+
}
84+
}
85+
86+
async handleLLMEnd(
87+
_output: LLMResult,
88+
_runId?: string,
89+
_parentRunId?: string,
90+
): Promise<void> {
91+
logger.debug('Laminar CallbackHandler: LLM end')
92+
}
93+
94+
async handleLLMError(
95+
err: Error | unknown,
96+
_runId?: string,
97+
_parentRunId?: string,
98+
): Promise<void> {
99+
logger.debug(`Laminar CallbackHandler: LLM error: ${err}`)
100+
}
101+
102+
async handleChainStart(
103+
chain: Serialized,
104+
inputs: ChainValues,
105+
_runId?: string,
106+
_parentRunId?: string,
107+
_tags?: string[],
108+
_metadata?: Record<string, unknown>,
109+
): Promise<void> {
110+
logger.debug('Laminar CallbackHandler: Chain start')
111+
if (inputs) {
112+
const inputStr = JSON.stringify(inputs).slice(0, 100)
113+
logger.debug(`Laminar CallbackHandler: Inputs preview: ${inputStr}${JSON.stringify(inputs).length > 100 ? '...' : ''}`)
114+
}
115+
}
116+
117+
async handleChainEnd(
118+
_outputs: ChainValues,
119+
_runId?: string,
120+
_parentRunId?: string,
121+
): Promise<void> {
122+
logger.debug('Laminar CallbackHandler: Chain end')
123+
}
124+
125+
async handleChainError(
126+
err: Error | unknown,
127+
_runId?: string,
128+
_parentRunId?: string,
129+
): Promise<void> {
130+
logger.debug(`Laminar CallbackHandler: Chain error: ${err}`)
131+
}
132+
133+
async handleToolStart(
134+
tool: Serialized,
135+
input: string,
136+
_runId?: string,
137+
_parentRunId?: string,
138+
_tags?: string[],
139+
_metadata?: Record<string, unknown>,
140+
): Promise<void> {
141+
logger.debug('Laminar CallbackHandler: Tool start')
142+
if (input) {
143+
const preview = input.slice(0, 100)
144+
logger.debug(`Laminar CallbackHandler: Tool input: ${preview}${input.length > 100 ? '...' : ''}`)
145+
}
146+
}
147+
148+
async handleToolEnd(
149+
_output: string,
150+
_runId?: string,
151+
_parentRunId?: string,
152+
): Promise<void> {
153+
logger.debug('Laminar CallbackHandler: Tool end')
154+
}
155+
156+
async handleToolError(
157+
err: Error | unknown,
158+
_runId?: string,
159+
_parentRunId?: string,
160+
): Promise<void> {
161+
logger.debug(`Laminar CallbackHandler: Tool error: ${err}`)
162+
}
163+
164+
async handleAgentAction(
165+
action: AgentAction,
166+
_runId?: string,
167+
_parentRunId?: string,
168+
): Promise<void> {
169+
logger.debug('Laminar CallbackHandler: Agent action')
170+
const actionStr = JSON.stringify(action).slice(0, 100)
171+
logger.debug(`Laminar CallbackHandler: Action: ${actionStr}...`)
172+
}
173+
174+
async handleAgentEnd(
175+
_action: AgentFinish,
176+
_runId?: string,
177+
_parentRunId?: string,
178+
): Promise<void> {
179+
logger.debug('Laminar CallbackHandler: Agent end')
180+
}
181+
182+
async handleRetrieverStart(
183+
_retriever: Serialized,
184+
_query: string,
185+
_runId?: string,
186+
_parentRunId?: string,
187+
_tags?: string[],
188+
_metadata?: Record<string, unknown>,
189+
): Promise<void> {
190+
logger.debug('Laminar CallbackHandler: Retriever start')
191+
}
192+
193+
async handleRetrieverEnd(
194+
_documents: any[],
195+
_runId?: string,
196+
_parentRunId?: string,
197+
): Promise<void> {
198+
logger.debug('Laminar CallbackHandler: Retriever end')
199+
}
200+
201+
async handleRetrieverError(
202+
err: Error | unknown,
203+
_runId?: string,
204+
_parentRunId?: string,
205+
): Promise<void> {
206+
logger.debug(`Laminar CallbackHandler: Retriever error: ${err}`)
207+
}
208+
}
209+
210+
// Export the handler for use in MCPAgent
211+
laminarHandler = new LaminarCallbackHandler()
212+
logger.debug('Laminar: Custom callback handler created and ready for use');
213+
214+
// Export observe function for wrapping custom functions
215+
(globalThis as any).laminarObserve = observe
216+
}
217+
catch (error) {
218+
logger.error(`Failed to initialize Laminar: ${error}`)
219+
}
220+
})()
221+
}
222+
})()
223+
224+
// Export getters to avoid exporting mutable bindings
225+
export const getLaminarHandler = () => laminarHandler
226+
export const isLaminarInitialized = () => laminarInitialized
227+
export const getLaminarInitPromise = () => initializationPromise

0 commit comments

Comments
 (0)