Skip to content

feat: add OpenTelemetry instrumentation and improve tool lifecycle hooks - #83

Merged
ssdeanx merged 1 commit into
mainfrom
develop
Jan 9, 2026
Merged

ssdeanx merged 1 commit into
mainfrom
develop

Conversation

@ssdeanx

@ssdeanx ssdeanx commented Jan 9, 2026

Copy link
Copy Markdown
Owner
  • Introduced OpenTelemetry instrumentation in a new file instrumentation.ts to enable tracing for the service.
  • Configured NodeSDK with OTLPTraceExporter, BatchSpanProcessor, and W3CTraceContextPropagator for enhanced tracing capabilities.
  • Created detailed session documentation for CONTINUITY_ses_45e3.md, outlining goals, constraints, progress, and next steps for implementing lifecycle hooks and OpenTelemetry tracing in src/mastra/tools/e2b.ts.
  • Documented progress and analysis in CONTINUITY_ses_45e6.md, focusing on standardizing OpenTelemetry patterns across tools and ensuring proper lifecycle hook implementation.
  • Conducted a comprehensive analysis of the AgentStack project in CONTINUITY_ses_45fc.md, detailing the project's structure, technology stack, and current state for onboarding new team members.

- Introduced OpenTelemetry instrumentation in a new file `instrumentation.ts` to enable tracing for the service.
- Configured NodeSDK with OTLPTraceExporter, BatchSpanProcessor, and W3CTraceContextPropagator for enhanced tracing capabilities.
- Created detailed session documentation for `CONTINUITY_ses_45e3.md`, outlining goals, constraints, progress, and next steps for implementing lifecycle hooks and OpenTelemetry tracing in `src/mastra/tools/e2b.ts`.
- Documented progress and analysis in `CONTINUITY_ses_45e6.md`, focusing on standardizing OpenTelemetry patterns across tools and ensuring proper lifecycle hook implementation.
- Conducted a comprehensive analysis of the AgentStack project in `CONTINUITY_ses_45fc.md`, detailing the project's structure, technology stack, and current state for onboarding new team members.
Copilot AI review requested due to automatic review settings January 9, 2026 17:19
@continue

continue Bot commented Jan 9, 2026

Copy link
Copy Markdown

All Green - Keep your PRs mergeable

Learn more

All Green is an AI agent that automatically:

✅ Addresses code review comments

✅ Fixes failing CI checks

✅ Resolves merge conflicts


Unsubscribe from All Green comments

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @ssdeanx, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@github-actions

github-actions Bot commented Jan 9, 2026

Copy link
Copy Markdown

🤖 Hi @ssdeanx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai

coderabbitai Bot commented Jan 9, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added OpenTelemetry tracing integration for enhanced observability
    • Introduced file-based workflow logging with trace context
    • Enhanced tool instrumentation with telemetry and metrics support
    • Added smooth animation capabilities
  • Updates

    • Version bumped to 1.1.2
    • Multiple dependency updates including core framework packages
    • Optimized vector search configuration for improved performance
  • Chores

    • Updated dependencies across packages

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds comprehensive OpenTelemetry instrumentation across the platform: new Node.js SDK initialization, observability configuration with bridge and redaction, logging enhanced with trace context, tool execute signatures updated to accept context, and database vector search parameters tuned from HNSW to IVFFlat indexing.

Changes

Cohort / File(s) Summary
OpenTelemetry Core Setup
instrumentation.ts, package.json
New instrumentation entry point configuring NodeSDK with OTLP exporter; dependency updates adding @opentelemetry/auto-instrumentations-node, @mastra/otel-bridge, gsap/lenis packages; version bump 1.1.1 → 1.1.2
Observability & Configuration
src/mastra/index.ts, src/mastra/config/logger.ts
Switched observability from direct OtelExporter to combined DefaultExporter + OtelBridge with 50% sampling, SensitiveDataFilter redaction, and service configuration; logger enhanced with trace context attachment, baggage handling, file-based JSON logging via FileTransport
Database Configuration
src/mastra/config/pg-storage.ts
Pool connections reduced 30 → 20; memory index shifted from HNSW to IVFFlat with 3200-dim embeddings; pgQueryTool probes parameter added for vector search tuning
Tool Instrumentation & Types
src/mastra/tools/e2b.ts, src/mastra/tools/document-chunking.tool.ts
All e2b tools updated: execute signatures now accept context parameter; integrated OpenTelemetry tracing (span events, baggage, metrics) across tool lifecycle; exported new UI tool types (CreateSandboxUITool, WriteFileUITool, …, E2BUITools); document-chunking RequestContext changed to type-only import
Session Documentation
thoughts/ledgers/CONTINUITY_ses_*
Three new session ledger files documenting lifecycle hook standardization, instrumentation goals, analysis progress, and critical context tracking

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Tool as E2B Tool
    participant Tracer as OpenTelemetry Tracer
    participant Span as Active Span
    participant Logger as Logger (w/ Trace)
    participant Sandbox as E2B Sandbox

    App->>Tool: execute(inputData, context)
    Note over Tool: RequestContext available via context param
    
    Tool->>Tracer: Start span for tool operation
    Tracer-->>Span: Active span created
    
    Tool->>Span: Add attributes (tool.id, input size)
    Tool->>Logger: Log tool start + trace context
    Logger->>Logger: Attach span context + baggage
    
    Tool->>Sandbox: Execute tool operation
    Sandbox-->>Tool: Result/output
    
    Tool->>Span: Record span event
    Tool->>Logger: Log tool completion + metrics
    Logger->>Logger: Append to workflow.log with trace
    
    Tool-->>App: Return result with trace metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Develop #76: Adds OpenTelemetry Node.js SDK initialization and instrumentation entry point that complements this PR's instrumentation.ts core setup
  • Develop #69: Modifies e2b.ts tool signatures and observability instrumentation directly overlapping with this PR's tool context integration and trace propagation
  • feat: Update agent configurations and dependencies #38: Updates observability configuration in src/mastra/index.ts with exporter/bridge changes matching this PR's observability restructuring

Poem

🐰 Traces now hop through every tool and task,
Context whispers secrets in each span,
From logs to metrics, telemetry at last—
A rabbit's glow on systems, grand! ✨

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5aecc8d and 190bfb7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • instrumentation.ts
  • package.json
  • src/mastra/config/logger.ts
  • src/mastra/config/pg-storage.ts
  • src/mastra/index.ts
  • src/mastra/tools/document-chunking.tool.ts
  • src/mastra/tools/e2b.ts
  • thoughts/ledgers/CONTINUITY_ses_45e3.md
  • thoughts/ledgers/CONTINUITY_ses_45e6.md
  • thoughts/ledgers/CONTINUITY_ses_45fc.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ssdeanx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the project's observability and maintainability by integrating OpenTelemetry for distributed tracing and metrics across key components, particularly within the E2B tools. It standardizes how tools report their activity and progress, ensuring a consistent and detailed view of operations. Additionally, it refines the overall observability setup and provides crucial documentation for project understanding and future development.

Highlights

  • OpenTelemetry Instrumentation Setup: Introduced a new instrumentation.ts file to configure OpenTelemetry NodeSDK, enabling tracing for the service with OTLPTraceExporter, BatchSpanProcessor, and W3CTraceContextPropagator for comprehensive observability.
  • Enhanced Tool Lifecycle Hooks and Observability in e2b.ts: Refactored all E2B tools (createSandbox, runCode, writeFile, listFiles, etc.) to consistently implement all four lifecycle hooks (onInputStart, onInputDelta, onInputAvailable, onOutput), integrate standardized OpenTelemetry tracing (spans, attributes, events, error handling), and emit structured progress events. This includes adding metrics for tool execution and sandbox operations.
  • Centralized Logging with Trace Context: Modified src/mastra/config/logger.ts to enrich all log entries with active OpenTelemetry trace and baggage context, improving the correlation of logs with distributed traces.
  • Refined Observability Configuration: Updated the main Mastra observability configuration in src/mastra/index.ts to include sampling strategies, sensitive data filtering for span outputs, and integrate a new OpenTelemetry bridge for more robust and secure tracing.
  • Project Analysis and Documentation: Added detailed session documentation (CONTINUITY_ses_45e3.md, CONTINUITY_ses_45e6.md, CONTINUITY_ses_45fc.md) outlining the goals, progress, and next steps for implementing lifecycle hooks, OpenTelemetry tracing, and a comprehensive analysis of the AgentStack project for onboarding new team members.
  • Dependency Updates and Configuration Adjustments: Updated various package dependencies in package.json and made minor configuration adjustments in pg-storage.ts, such as reducing PostgreSQL connection pool size and updating semantic recall index configurations.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions

github-actions Bot commented Jan 9, 2026

Copy link
Copy Markdown

🤖 I'm sorry @ssdeanx, but I was unable to process your request. Please see the logs for more details.

@ssdeanx
ssdeanx merged commit 2f617e6 into main Jan 9, 2026
24 of 29 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive OpenTelemetry (OTEL) instrumentation and logging enhancements across the codebase, particularly for E2B sandbox tools. A new instrumentation.ts file sets up the OTEL NodeSDK, while logger.ts is updated with OTEL helpers to enrich logs with trace context and baggage, although the main log formatter for this is currently commented out. The Mastra observability configuration in index.ts has been refactored to use a DefaultExporter with sensitive data filtering and an OtelBridge, replacing a previous OtelExporter setup. The package.json reflects a version bump and numerous dependency updates, including new OTEL-related packages. In pg-storage.ts, PostgreSQL connection limits and vector index configurations were adjusted, shifting from HNSW to IVFFlat for some indexes. Most significantly, the e2b.ts file underwent a major refactoring to integrate OTEL tracing and metrics into all its tools, adding spans, attributes, events, and error handling, and standardizing tool lifecycle hooks. However, the readFile tool was accidentally removed, the runCode tool's implementation was fundamentally changed from sandbox.runCode to sandbox.commands.run leading to breaking changes in output and supported options, and the contextManager in instrumentation.ts is set to undefined, which disables asynchronous context propagation for tracing. Additionally, there is commented-out code in the observability configuration and a potential loss of baggage metadata in the mergeAndSetBaggage helper.

Comment thread instrumentation.ts
})
),
],
contextManager: undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Setting contextManager to undefined disables automatic asynchronous context propagation. This means that trace.getActiveSpan() and otelContext.active() will not work correctly across async/await calls, which will break the tracing instrumentation implemented in logger.ts and e2b.ts. To fix this, you should remove this line entirely to let the SDK use the default AsyncLocalStorageContextManager.

Comment thread src/mastra/tools/e2b.ts
import { createTool } from '@mastra/core/tools';
import z from 'zod';
import { log } from '../config/logger';
import { FilesystemEventType, FileType, Sandbox } from '@e2b/code-interpreter'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The readFile tool, which existed in the previous version of this file, appears to have been accidentally removed during this refactoring. All other tools were preserved and instrumented, but readFile is now missing, which constitutes a breaking change.

Comment on lines +65 to +67
// log(object) {
// return attachTraceAndBaggage(object)
// },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The log formatter, which is responsible for calling attachTraceAndBaggage to enrich all logs with trace context, is commented out. This means that no logs will be automatically correlated with traces, which undermines a key benefit of this instrumentation. Please uncomment this block to enable automatic log enrichment.

Suggested change
// log(object) {
// return attachTraceAndBaggage(object)
// },
log(object) {
return attachTraceAndBaggage(object)
},

Comment thread src/mastra/tools/e2b.ts
Comment on lines +1538 to +1649
export const runCode = createTool({
id: 'runCode',
description: 'Execute code in the e2b sandbox',
inputSchema: z.object({
sandboxId: z
.string()
.describe('The sandboxId for the sandbox to run code in'),
code: z.string().describe('The code to execute'),
runCodeOpts: z
.object({
language: z
.enum(['python', 'javascript', 'typescript'])
.default('python')
.describe('The programming language of the code'),
})
.optional()
.describe('Options for code execution'),
}),
outputSchema: z.object({
execution: z.string().describe('JSON string containing execution results'),
}),
execute: async (inputData, context) => {
const span = tracer.startSpan('runCode')
const startTime = Date.now()

try {
logToolStart('runCode', inputData, context)

const sandbox = await Sandbox.connect(inputData.sandboxId)

// Execute code using the appropriate interpreter based on language
const language = inputData.runCodeOpts?.language ?? 'python'
let command: string

if (language === 'python') {
command = `python -c "${inputData.code.replace(/"/g, '\\"')}"`
} else if (language === 'javascript') {
command = `node -e "${inputData.code.replace(/"/g, '\\"')}"`
} else if (language === 'typescript') {
// For TypeScript, we need to compile and run
command = `npx tsx -e "${inputData.code.replace(/"/g, '\\"')}"`
} else {
throw new Error(`Unsupported language: ${language}`)
}

const result = await sandbox.commands.run(command)

span.setStatus({ code: SpanStatusCode.OK })
span.setAttribute('sandbox.id', inputData.sandboxId)
span.setAttribute('code.language', language)
span.setAttribute('code.length', inputData.code.length)

// Format result to match expected structure with logs.stdout and logs.stderr as arrays
const executionResult = {
logs: {
stdout: result.stdout ? result.stdout.split('\n') : [],
stderr: result.stderr ? result.stderr.split('\n') : [],
},
exitCode: result.exitCode,
success: result.exitCode === 0,
}

const output = {
execution: JSON.stringify(executionResult),
}

logToolComplete('runCode', output, Date.now() - startTime, context)
return output
} catch (e) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: e instanceof Error ? e.message : String(e),
})
logToolError('runCode', e, context)
const errObj =
e instanceof Error
? { message: e.message, stack: e.stack }
: { message: String(e) }
throw new Error(JSON.stringify(errObj))
} finally {
span.end()
}
},
onInputStart: ({ toolCallId, messages, abortSignal }) => {
log.info('Run code tool input streaming started', {
toolCallId,
messageCount: messages.length,
abortSignal: abortSignal?.aborted,
hook: 'onInputStart',
})
},
onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => {
log.info('Run code received complete input', {
toolCallId,
messageCount: messages.length,
sandboxId: input.sandboxId,
codeLength: input.code.length,
language: input.runCodeOpts?.language ?? 'python',
abortSignal: abortSignal?.aborted,
hook: 'onInputAvailable',
})
},
onOutput: ({ output, toolCallId, toolName, abortSignal }) => {
log.info('Run code completed', {
toolCallId,
toolName,
executionLength: output.execution.length,
abortSignal: abortSignal?.aborted,
hook: 'onOutput',
})
},
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The implementation of the runCode tool has been fundamentally changed from using sandbox.runCode to sandbox.commands.run. This introduces several breaking changes:

  1. Different Output: The output format is now a simple object with stdout/stderr arrays, which differs from the richer object returned by the original runCode method.
  2. Unsupported Options: The runCodeOpts no longer supports envs, timeoutMS, or requestTimeoutMs.
  3. Language Aliases: The supported languages have changed (e.g., js is no longer an alias for javascript).

This change is likely to break integrations that rely on this tool. It's recommended to revert to using sandbox.runCode and wrap it with the new tracing and logging logic to maintain compatibility.

Comment thread instrumentation.ts
import { W3CTraceContextPropagator } from "@opentelemetry/core";

const sdk = new NodeSDK({
serviceName: "my-service",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The serviceName is hardcoded. It's a best practice to make this configurable via an environment variable to easily identify the service in different observability environments (e.g., development, staging, production).

Suggested change
serviceName: "my-service",
serviceName: process.env.OTEL_SERVICE_NAME ?? "agentstack",

if (!span) {
return { traceId: undefined, spanId: undefined }
}
const sc = (span as any).spanContext ? (span as any).spanContext() : undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast (span as any) is unnecessary and bypasses type safety. The spanContext() method is a standard part of the Span interface from @opentelemetry/api. You can call it directly on the span object.

Suggested change
const sc = (span as any).spanContext ? (span as any).spanContext() : undefined
const sc = span.spanContext();

Comment on lines +26 to +35
const BaggageToObject = (b: unknown) => {
if (b === null || b === undefined) {
return {}
}
const obj: Record<string, string> = {}
for (const [k, v] of (b as any).getAllEntries()) {
obj[k] = String(v.value)
}
return obj
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The BaggageToObject function takes b as unknown and then casts it to any. This can be made more type-safe by typing b as Baggage | undefined | null and avoiding the any cast, as getAllEntries() is a standard method on the Baggage interface.

Suggested change
const BaggageToObject = (b: unknown) => {
if (b === null || b === undefined) {
return {}
}
const obj: Record<string, string> = {}
for (const [k, v] of (b as any).getAllEntries()) {
obj[k] = String(v.value)
}
return obj
}
const BaggageToObject = (b: Baggage | undefined | null) => {
if (!b) {
return {}
}
const obj: Record<string, string> = {}
for (const [k, v] of b.getAllEntries()) {
obj[k] = String(v.value)
}
return obj
}

Comment on lines +90 to +101
let data: {
workflowId: string
input: Record<string, unknown>
timestamp: string
_trace?: Record<string, unknown>
_Baggage?: Record<string, string>
} = {
workflowId,
input,
timestamp: new Date().toISOString(),
}
data = attachTraceAndBaggage(data) as typeof data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is significant code duplication across the logWorkflowStart, logWorkflowEnd, logStepStart, and other logging functions. Each one re-defines a data type and manually calls attachTraceAndBaggage. If you enable the Pino log formatter (as suggested in another comment), it will handle this automatically for all logs, and you can remove the manual enrichment from each of these functions, greatly simplifying the code.

Comment thread src/mastra/index.ts
Comment on lines +269 to +322
// strategy: 'realtime',
// maxBatchSize: 500,
// maxBufferSize: 1000,
// maxBatchWaitMs: 6000,
maxRetries: 5,
}
), // Studio access0
],
includeInternalSpans: true,

bridge: new OtelBridge(
{
name: "mastra-otel-bridge",
// otelTracer: {
// startSpan: (name: string, options: SpanOptions | undefined) => {
// const tracer = trace.getTracer("mastra-otel-bridge-tracer");
// return tracer.startSpan(name, options);
// },
// },
logger: log,
logLevel: 'debug',
logLevel: 'info',
// batchsize: 500,
timeout: 50000,
serializationOptions: {
maxStringLength: 1024, // Maximum length for string values (default: 1024)
maxDepth: 6, // Maximum depth for nested objects (default: 6)
maxArrayLength: 50, // Maximum number of items in arrays (default: 50)
maxObjectKeys: 50, // Maximum number of keys in objects (default: 50)
},
resourceAttributes: {
'deployment.environment': 'dev',
},
}),
],
}
),
},
// exporters: [
// new DefaultExporter(), // Studio access

// new OtelExporter({
// provider: {
// traceloop: {
// apiKey: process.env.TRACELOOP_API_KEY,
// destinationId: 'my-destination',
// },
// },
// batchSize: 500,
// timeout: 50000,
// logger: log,
// logLevel: 'debug',
// resourceAttributes: {
// 'deployment.environment': 'dev',
// },
// }),
// ],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a large amount of commented-out code in the observability configuration. This makes the file difficult to read and maintain. Please remove this dead code before merging.

Comment thread src/mastra/tools/e2b.ts
Comment on lines +74 to +76
for (const k of Object.keys(entries)) {
merged[k] = { value: entries[k] }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the mergeAndSetBaggage helper, when adding or updating baggage entries, you are creating a new entry with only a value. If an entry with the same key already existed and had associated metadata, that metadata will be lost. This could lead to a loss of context if other services rely on that metadata.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces OpenTelemetry instrumentation and enhances lifecycle hooks for E2B sandbox tools. The changes include comprehensive tracing, metrics collection, and improved observability across the application.

Key Changes

  • Added OpenTelemetry auto-instrumentation with OTLP exporter for distributed tracing
  • Refactored 13 E2B tools with enhanced OpenTelemetry spans, attributes, events, and metrics
  • Integrated trace context and baggage into the logging system for correlated observability
  • Updated observability configuration with sampling strategies and sensitive data filtering

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 33 comments.

Show a summary per file
File Description
instrumentation.ts New OpenTelemetry NodeSDK configuration with auto-instrumentation
src/mastra/tools/e2b.ts Major refactor (823→1697 lines): Added comprehensive OTEL tracing, metrics, lifecycle hooks, and helper functions
src/mastra/config/logger.ts Enhanced with trace context and baggage propagation
src/mastra/index.ts Updated observability config with DefaultExporter, sampling, and OtelBridge
src/mastra/config/pg-storage.ts Adjusted connection pool size, IVFFlat configuration, and title generation
src/mastra/tools/document-chunking.tool.ts Minor: Added type keyword to RequestContext import
package.json Version bump and dependency updates
thoughts/ledgers/*.md Session documentation for development context

Comment thread src/mastra/tools/e2b.ts
Comment on lines +1323 to +1378
const handle = await sandbox.files.watchDir(
inputData.path,
async (event) => {
events.push({
type: event.type,
name: event.name,
timestamp: new Date().toISOString(),
})
},
{
recursive: inputData.recursive,
}
)

// Watch for the specified duration
await new Promise((resolve) =>
setTimeout(resolve, inputData.watchDuration)
)

// Stop watching
await handle.stop()

span.setStatus({ code: SpanStatusCode.OK })
span.setAttribute('sandbox.id', inputData.sandboxId)
span.setAttribute('watch.path', inputData.path)
span.setAttribute('watch.recursive', inputData.recursive)
span.setAttribute('watch.duration', inputData.watchDuration)
span.setAttribute('events.count', events.length)

const result = {
watchStarted: true,
path: inputData.path,
events,
}

logToolComplete(
'watchDirectory',
result,
Date.now() - startTime,
context
)
return result
} catch (e) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: e instanceof Error ? e.message : String(e),
})
logToolError('watchDirectory', e, context)
const errObj =
e instanceof Error
? { message: e.message, stack: e.stack }
: { message: String(e) }
throw new Error(JSON.stringify(errObj))
} finally {
span.end()
}

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential memory leak in watchDirectory tool. The tool creates a file watcher at line 1323 that captures events in a closure, but if an error occurs during the watch setup or execution, the watcher handle may not be properly cleaned up. The handle.stop() call is only in the success path (line 1343), not in the catch block. If an exception occurs after the watcher is created but before it's stopped, the watcher will continue running and leaking resources.

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
Comment on lines +1027 to +1070
try {
const info = await sandbox.files.getInfo(inputData.path)

span.setStatus({ code: SpanStatusCode.OK })
span.setAttribute('sandbox.id', inputData.sandboxId)
span.setAttribute('file.path', inputData.path)
span.setAttribute('file.exists', true)
if (info.type !== null && info.type !== undefined) {
span.setAttribute('file.type', info.type)
}

const result = {
exists: true,
path: inputData.path,
type: info.type,
}

logToolComplete(
'checkFileExists',
result,
Date.now() - startTime,
context
)
return result
} catch {
// If getInfo fails, the file doesn't exist
span.setStatus({ code: SpanStatusCode.OK })
span.setAttribute('sandbox.id', inputData.sandboxId)
span.setAttribute('file.path', inputData.path)
span.setAttribute('file.exists', false)

const result = {
exists: false,
path: inputData.path,
}

logToolComplete(
'checkFileExists',
result,
Date.now() - startTime,
context
)
return result
}

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling pattern in checkFileExists. The inner try-catch block at line 1027 catches errors from sandbox.files.getInfo() silently without any error recording or logging (line 1051), treating file-not-found as a normal case. However, if this inner catch triggers due to a different error (e.g., network failure, permission issues), it will be incorrectly interpreted as "file doesn't exist". The implementation should distinguish between "file not found" errors and other types of errors.

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
Comment on lines +95 to +296
'tool.input_size': JSON.stringify(input).length,
'user.id': toolContext?.requestContext?.userId,
})

// Merge into baggage for downstream services and traces
mergeAndSetBaggage({ 'tool.name': toolName, 'tool.start_time': Date.now().toString() })

// Also set attributes on the span so it's available in OTEL exporters
try {
span.setAttribute('tool.name', toolName)
span.setAttribute('tool.input_size', JSON.stringify(input).length)
} catch (err) {
// ignore if span does not support setAttribute
}
}

// Include trace info and baggage in the structured log
const traceInfo = getActiveSpanContextInfo()
const bag = propagation.getBaggage(otelContext.active())
logToolExecution(toolName, { originalInput: input, _trace: traceInfo, _Baggage: BaggageToObject(bag), _userId: toolContext?.requestContext?.userId })
}

const logToolComplete = (
toolName: string,
output: any,
duration: number,
toolContext?: any
) => {
const span = trace.getActiveSpan()
if (span) {
// Add span event for tool completion
span.addEvent('tool.complete', {
'tool.name': toolName,
'tool.duration_ms': duration,
'tool.output_size': JSON.stringify(output).length,
'user.id': toolContext?.requestContext?.userId,
})

// Update baggage
mergeAndSetBaggage({ 'tool.duration': duration.toString(), 'tool.completed_at': Date.now().toString() })

// Set attributes on span
try {
span.setAttribute('tool.duration_ms', duration)
span.setAttribute('tool.output_size', JSON.stringify(output).length)
} catch (err) {
// ignore
}
}

// Record metrics
toolExecutionCounter.add(1, {
'tool.name': toolName,
'tool.status': 'success',
'user.id': toolContext?.requestContext?.userId,
})
toolExecutionDuration.record(duration, {
'tool.name': toolName,
'tool.status': 'success',
'user.id': toolContext?.requestContext?.userId,
})

const traceInfo = getActiveSpanContextInfo()
const bag = propagation.getBaggage(otelContext.active())

logToolExecution(
toolName,
{ _trace: traceInfo, _Baggage: BaggageToObject(bag), _userId: toolContext?.requestContext?.userId },
output
)
}

const logToolError = (toolName: string, error: any, context?: any) => {
const span = trace.getActiveSpan()
if (span) {
// Add span event for tool error
span.addEvent('tool.error', {
'tool.name': toolName,
'error.message': error instanceof Error ? error.message : String(error),
'error.stack': error instanceof Error ? error.stack : undefined,
'user.id': context?.requestContext?.userId
})

// Record error in span
span.recordException(error instanceof Error ? error : new Error(String(error)))
}

// Record error metrics
toolExecutionCounter.add(1, {
'tool.name': toolName,
'tool.status': 'error',
'error.type': error instanceof Error ? error.constructor.name : 'Unknown'
})

logError(toolName, error, {
toolName,
userId: context?.requestContext?.userId
})
}

export const createSandbox = createTool({
id: 'createSandbox',
description: 'Create an e2b sandbox',
inputSchema: z.object({
metadata: z.record(z.string(), z.string()).optional().describe('Custom metadata for the sandbox'),
envs: z.record(z.string(), z.string()).optional().describe(`
id: 'createSandbox',
description: 'Create an e2b sandbox',
inputSchema: z.object({
metadata: z
.record(z.string(), z.string())
.optional()
.describe('Custom metadata for the sandbox'),
envs: z.record(z.string(), z.string()).optional().describe(`
Custom environment variables for the sandbox.
Used when executing commands and code in the sandbox.
Can be overridden with the \`envs\` argument when executing commands or code.
`),
timeoutMS: z.number().optional().describe(`
timeoutMS: z.number().optional().describe(`
Timeout for the sandbox in **milliseconds**.
Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users.
@default 300_000 // 5 minutes
`),
}),
outputSchema: z.object({
sandboxId: z.string(),
}),
onInputStart: ({ toolCallId, messages, abortSignal }) => {
log.info('Create sandbox tool input streaming started', {
toolCallId,
hook: 'onInputStart',
})
},
onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => {
log.info('Create sandbox received complete input', {
toolCallId,
hasMetadata: !!input.metadata,
hasEnvs: !!input.envs,
hook: 'onInputAvailable',
})
},
onOutput: ({ output, toolCallId, toolName, abortSignal }) => {
log.info('Create sandbox completed', {
toolCallId,
toolName,
sandboxId: output.sandboxId,
hook: 'onOutput',
})
},
execute: async (sandboxOptions) => {
try {
const sandbox = await Sandbox.create(sandboxOptions);

return {
sandboxId: sandbox.sandboxId,
};
} catch (e) {
const errObj = e instanceof Error ? { message: e.message, stack: e.stack } : { message: String(e) };
throw new Error(JSON.stringify(errObj));
}
},
});
}),
outputSchema: z.object({
sandboxId: z.string(),
}),
execute: async (inputData, context) => {
const span = tracer.startSpan('createSandbox')
const startTime = Date.now()
const requestContext = context?.requestContext as E2BRequestContext
const userId = requestContext?.userId
if (userId && userId.length > 0) {
span.setAttribute('user.id', userId)
}

export const runCode = createTool({
id: 'runCode',
description: 'Run code in an e2b sandbox',
inputSchema: z.object({
sandboxId: z.string().describe('The sandboxId for the sandbox to run the code'),
code: z.string().describe('The code to run in the sandbox'),
runCodeOpts: z
.object({
language: z
.enum(['ts', 'js', 'python'])
.default('python')
.describe('language used for code execution. If not provided, default python context is used'),
envs: z.record(z.string(), z.string()).optional().describe('Custom environment variables for code execution.'),
timeoutMS: z.number().optional().describe(`
Timeout for the code execution in **milliseconds**.
@default 60_000 // 60 seconds
`),
requestTimeoutMs: z.number().optional().describe(`
Timeout for the request in **milliseconds**.
@default 30_000 // 30 seconds
`),
})
.optional()
.describe('Run code options'),
}),
outputSchema: z.object({
execution: z.string().describe('Serialized representation of the execution results'),
}),
onInputStart: ({ toolCallId, messages, abortSignal }) => {
log.info('Run code tool input streaming started', {
toolCallId,
hook: 'onInputStart',
})
},
onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => {
log.info('Run code received complete input', {
toolCallId,
sandboxId: input.sandboxId,
codeLength: input.code.length,
language: input.runCodeOpts?.language,
hook: 'onInputAvailable',
})
},
onOutput: ({ output, toolCallId, toolName, abortSignal }) => {
log.info('Run code completed', {
toolCallId,
toolName,
executionLength: output.execution.length,
hook: 'onOutput',
})
},
execute: async (inputData) => {
try {
const sandbox = await Sandbox.connect(inputData.sandboxId);
// Add custom resource attributes
span.setAttribute('service.name', 'e2b-tools')
span.setAttribute('service.version', '1.0.0')
span.setAttribute('sandbox.operation', 'create')

const execution = await sandbox.runCode(inputData.code, inputData.runCodeOpts);
// Set baggage for cross-service context propagation
const currentBaggage = propagation.getBaggage(otelContext.active())
if (currentBaggage) {
const keys = Array.from(currentBaggage.getAllEntries()).map(([key]) => key)
span.setAttribute('baggage.keys', keys.join(','))
}

return {
execution: JSON.stringify(execution.toJSON()),
};
} catch (e) {
const errObj = e instanceof Error ? { message: e.message, stack: e.stack } : { message: String(e) };
throw new Error(JSON.stringify(errObj));
}
},
});

export const readFile = createTool({
id: 'readFile',
description: 'Read a file from the e2b sandbox',
inputSchema: z.object({
sandboxId: z.string().describe('The sandboxId for the sandbox to read the file from'),
path: z.string().describe('The path to the file to read'),
}),
outputSchema: z.object({
content: z.string().describe('The content of the file'),
path: z.string().describe('The path of the file that was read'),
}),
onInputStart: ({ toolCallId, messages, abortSignal }) => {
log.info('Read file tool input streaming started', {
toolCallId,
hook: 'onInputStart',
})
},
onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => {
log.info('Read file received complete input', {
toolCallId,
sandboxId: input.sandboxId,
path: input.path,
hook: 'onInputAvailable',
})
},
onOutput: ({ output, toolCallId, toolName, abortSignal }) => {
log.info('Read file completed', {
toolCallId,
toolName,
contentLength: output.content.length,
path: output.path,
hook: 'onOutput',
})
},
execute: async (inputData) => {
try {
const sandbox = await Sandbox.connect(inputData.sandboxId);
const fileContent = await sandbox.files.read(inputData.path);

return {
content: fileContent,
path: inputData.path,
};
} catch (e) {
const errObj = e instanceof Error ? { message: e.message, stack: e.stack } : { message: String(e) };
throw new Error(JSON.stringify(errObj));
}
},
});
// Add span event for input validation
span.addEvent('input.validation', {
'input.has_metadata': !!inputData.metadata,
'input.has_envs': !!inputData.envs,
'input.timeout_ms': inputData.timeoutMS ?? 300000
})

try {
// Add start event and input size
const inputSize = JSON.stringify(inputData).length
span.addEvent('tool.start', {
ts: Date.now(),
input_size: inputSize,
})
span.setAttribute('tool.input_size', inputSize)

await context?.writer?.custom({
type: 'data-tool-progress',
data: {
status: 'in-progress',
message: `Creating sandbox...`,
stage: 'createSandbox',
},
id: 'createSandbox',
})

logToolStart('createSandbox', inputData, context)

const sandbox = await Sandbox.create(inputData)

// Record successful sandbox creation metrics
const creationDuration = Date.now() - startTime
sandboxOperationsCounter.add(1, {
'operation.type': 'create',
'operation.status': 'success',
'sandbox.id': sandbox.sandboxId,
})
sandboxOperationDuration.record(creationDuration, {
'operation.type': 'create',
'operation.status': 'success',
'sandbox.id': sandbox.sandboxId,
})

span.setStatus({ code: SpanStatusCode.OK })
span.setAttribute('sandbox.id', sandbox.sandboxId)
span.setAttribute('sandbox.created', true)

// Add span event for successful creation
span.addEvent('sandbox.created', {
'sandbox.id': sandbox.sandboxId,
'creation.time_ms': Date.now() - startTime
})

const result = {
sandboxId: sandbox.sandboxId,
}

// Output size attribute & complete event
const outputSize = JSON.stringify(result).length

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: Synchronous JSON.stringify calls on potentially large objects. Throughout the e2b.ts file, JSON.stringify() is called synchronously on input/output data for calculating sizes (e.g., lines 95, 129, 247, 252, 296). For large inputs or outputs, this can block the event loop. Consider using streaming or chunked approaches for size calculation, or at minimum add size limits to prevent performance degradation.

Copilot uses AI. Check for mistakes.
// Message management
lastMessages: parseInt(process.env.MEMORY_LAST_MESSAGES ?? '500'),
generateTitle: process.env.THREAD_GENERATE_TITLE !== 'true',
generateTitle: true,

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateTitle configuration change on line 87 in pg-storage.ts appears to have the logic inverted. The original code was generateTitle: process.env.THREAD_GENERATE_TITLE !== 'true' (meaning it defaults to false and only generates when env var is NOT 'true'), but the new code is generateTitle: true (always generate). This behavioral change should be intentional and documented, as it may impact performance by always generating titles for threads.

Suggested change
generateTitle: true,
generateTitle: process.env.THREAD_GENERATE_TITLE !== 'false',

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
Comment on lines +1681 to +1694
export const e2bToolSet = {
createSandbox,
writeFile,
writeFiles,
listFiles,
deleteFile,
createDirectory,
getFileInfo,
checkFileExists,
getFileSize,
watchDirectory,
runCommand,
runCode,
} as const

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runCode tool is missing the readFile tool from the original implementation. Comparing the original code (which had readFile exported) with the new code, the readFile tool has been completely removed from the exports and tool definitions. This is a breaking change that removes existing functionality users may depend on.

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts

const logToolComplete = (
toolName: string,
output: any,

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Suggested change
output: any,
output: unknown,

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
toolName: string,
output: any,
duration: number,
toolContext?: any

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
Comment on lines +140 to +141
} catch (err) {
// ignore

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'err' is defined but never used.

Suggested change
} catch (err) {
// ignore
} catch (error) {
logError('Failed to set span attributes in logToolComplete', { error })

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
)
}

const logToolError = (toolName: string, error: any, context?: any) => {

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Copilot uses AI. Check for mistakes.
Comment thread src/mastra/tools/e2b.ts
)
}

const logToolError = (toolName: string, error: any, context?: any) => {

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Suggested change
const logToolError = (toolName: string, error: any, context?: any) => {
const logToolError = (toolName: string, error: unknown, context?: { requestContext?: E2BRequestContext }) => {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants