mirror of
https://github.com/zoriya/drizzle-otel.git
synced 2025-12-06 00:46:09 +00:00
Initial release of @kubiks/otel-e2b package for OpenTelemetry instrumentation of E2B Sandboxes, featuring comprehensive tracing for sandbox lifecycle, code execution, file operations, and command execution. Includes configuration options for metadata capture and full TypeScript support.
This commit is contained in:
28
packages/otel-e2b/CHANGELOG.md
Normal file
28
packages/otel-e2b/CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# @kubiks/otel-e2b
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Features
|
||||
|
||||
- Initial release of OpenTelemetry instrumentation for E2B Sandboxes
|
||||
- **Core instrumentation functions**:
|
||||
- `instrumentSandbox()` - Instrument existing sandbox instances
|
||||
- `instrumentSandboxClass()` - Instrument Sandbox class for automatic instrumentation
|
||||
- **Sandbox lifecycle tracing**:
|
||||
- `Sandbox.create()` - Sandbox creation with template information
|
||||
- `sandbox.kill()` - Sandbox termination
|
||||
- **Code execution tracing**:
|
||||
- `sandbox.runCode()` - Code execution with language, error status, and execution count
|
||||
- **File operations tracing**:
|
||||
- `sandbox.files.read()` - Read files with path, size, and format
|
||||
- `sandbox.files.write()` - Write single or multiple files
|
||||
- `sandbox.files.list()` - List directory contents with file counts
|
||||
- `sandbox.files.remove()` - Delete files or directories
|
||||
- `sandbox.files.makeDir()` - Create directories
|
||||
- **Command execution tracing**:
|
||||
- `sandbox.commands.run()` - Execute shell commands with exit codes and output line counts
|
||||
- **Security-first design**: Never captures sensitive data (code content, file contents, command arguments, etc.)
|
||||
- **Comprehensive configuration options**: Control what metadata to capture
|
||||
- **Idempotent instrumentation**: Safe to call multiple times
|
||||
- **Full TypeScript support** with complete type definitions
|
||||
- **OpenTelemetry semantic conventions** for all span attributes
|
||||
22
packages/otel-e2b/LICENSE
Normal file
22
packages/otel-e2b/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Kubiks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
83
packages/otel-e2b/README.md
Normal file
83
packages/otel-e2b/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# @kubiks/otel-e2b
|
||||
|
||||
OpenTelemetry instrumentation for [E2B Sandboxes](https://e2b.dev).
|
||||
Capture spans for sandbox lifecycle, code execution, file operations, and command execution to monitor and debug your E2B sandbox operations.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @kubiks/otel-e2b
|
||||
# or
|
||||
pnpm add @kubiks/otel-e2b
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { instrumentSandbox } from "@kubiks/otel-e2b";
|
||||
|
||||
const sandbox = await Sandbox.create();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
// All operations are now traced
|
||||
await sandbox.runCode('print("Hello from E2B")');
|
||||
await sandbox.files.write("/app/data.txt", "some data");
|
||||
await sandbox.commands.run("ls -la");
|
||||
await sandbox.kill();
|
||||
```
|
||||
|
||||
`instrumentSandbox` wraps the sandbox you already use — no configuration changes needed. Every operation creates a client span with useful attributes.
|
||||
|
||||
You can also use `instrumentSandboxClass(Sandbox)` to automatically instrument all sandboxes created after setup.
|
||||
|
||||
## What Gets Traced
|
||||
|
||||
This instrumentation automatically traces all E2B sandbox operations including `Sandbox.create()`, `sandbox.kill()`, `sandbox.runCode()` (code execution), `sandbox.commands.run()` (shell commands), and all file operations (`files.read()`, `files.write()`, `files.list()`, `files.remove()`, `files.makeDir()`).
|
||||
|
||||
## Span Attributes
|
||||
|
||||
Each span includes relevant attributes for debugging and monitoring:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| -------------------------- | ------------------------------------- | ---------------------------- |
|
||||
| `e2b.operation` | Operation type | `sandbox.create`, `code.run` |
|
||||
| `e2b.sandbox.id` | Unique sandbox identifier | `sb_abc123def456` |
|
||||
| `e2b.sandbox.template` | Template used for creation | `custom-template` |
|
||||
| `e2b.code.language` | Programming language | `python`, `javascript` |
|
||||
| `e2b.code.has_error` | Whether execution had errors | `true`, `false` |
|
||||
| `e2b.code.execution_count` | Execution count from result | `1`, `2`, `3` |
|
||||
| `e2b.command.exit_code` | Process exit code | `0`, `1`, `127` |
|
||||
| `e2b.command.stdout_lines` | Number of stdout lines (when enabled) | `5` |
|
||||
| `e2b.command.stderr_lines` | Number of stderr lines (when enabled) | `2` |
|
||||
| `e2b.command.background` | Whether command ran in background | `true`, `false` |
|
||||
| `e2b.file.operation` | File operation type | `read`, `write`, `list` |
|
||||
| `e2b.file.path` | File or directory path | `/app/data.txt` |
|
||||
| `e2b.file.size_bytes` | File size in bytes | `1024` |
|
||||
| `e2b.file.format` | Read format (for read ops) | `text`, `bytes` |
|
||||
| `e2b.file.count` | Number of files (list/write multiple) | `10` |
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The instrumentation accepts optional configuration to control what metadata to capture:
|
||||
|
||||
- `tracerName` - Custom tracer name (default: `"@kubiks/otel-e2b"`)
|
||||
- `captureFilePaths` - Capture file paths, not content (default: `true`)
|
||||
- `captureFileSize` - Capture file sizes (default: `true`)
|
||||
- `captureCodeLanguage` - Capture code execution language (default: `true`)
|
||||
- `captureCommandOutput` - Capture command output line counts, not content (default: `false`)
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
instrumentSandbox(sandbox, {
|
||||
captureFilePaths: true,
|
||||
captureCommandOutput: true,
|
||||
});
|
||||
```
|
||||
|
||||
The instrumentation never captures sensitive data like code content, command arguments, file contents, or environment variables — only safe metadata like paths, sizes, exit codes, and language types.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
65
packages/otel-e2b/package.json
Normal file
65
packages/otel-e2b/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@kubiks/otel-e2b",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "OpenTelemetry instrumentation for E2B Sandboxes",
|
||||
"keywords": [
|
||||
"opentelemetry",
|
||||
"otel",
|
||||
"instrumentation",
|
||||
"e2b",
|
||||
"sandbox",
|
||||
"code-interpreter",
|
||||
"observability",
|
||||
"tracing",
|
||||
"monitoring",
|
||||
"telemetry"
|
||||
],
|
||||
"author": "Kubiks",
|
||||
"license": "MIT",
|
||||
"repository": "kubiks-inc/otel",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && tsc",
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "pnpm build",
|
||||
"type-check": "tsc --noEmit",
|
||||
"unit-test": "vitest --run",
|
||||
"unit-test-watch": "vitest"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@e2b/code-interpreter": "^2.0.0",
|
||||
"@types/node": "18.15.11",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "^5",
|
||||
"vitest": "0.33.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <2.0.0",
|
||||
"@e2b/code-interpreter": ">=2.0.0"
|
||||
}
|
||||
}
|
||||
505
packages/otel-e2b/src/index.test.ts
Normal file
505
packages/otel-e2b/src/index.test.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
||||
import {
|
||||
BasicTracerProvider,
|
||||
InMemorySpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
import type { Sandbox } from "@e2b/code-interpreter";
|
||||
import {
|
||||
instrumentSandbox,
|
||||
instrumentSandboxClass,
|
||||
SEMATTRS_E2B_OPERATION,
|
||||
SEMATTRS_E2B_SANDBOX_ID,
|
||||
SEMATTRS_E2B_CODE_LANGUAGE,
|
||||
SEMATTRS_E2B_CODE_HAS_ERROR,
|
||||
SEMATTRS_E2B_CODE_EXECUTION_COUNT,
|
||||
SEMATTRS_E2B_COMMAND_EXIT_CODE,
|
||||
SEMATTRS_E2B_COMMAND_STDOUT_LINES,
|
||||
SEMATTRS_E2B_COMMAND_STDERR_LINES,
|
||||
SEMATTRS_E2B_COMMAND_BACKGROUND,
|
||||
SEMATTRS_E2B_FILE_OPERATION,
|
||||
SEMATTRS_E2B_FILE_PATH,
|
||||
SEMATTRS_E2B_FILE_SIZE_BYTES,
|
||||
SEMATTRS_E2B_FILE_COUNT,
|
||||
SEMATTRS_E2B_FILE_FORMAT,
|
||||
SEMATTRS_E2B_SANDBOX_TEMPLATE,
|
||||
} from "./index";
|
||||
|
||||
describe("instrumentSandbox", () => {
|
||||
let provider: BasicTracerProvider;
|
||||
let exporter: InMemorySpanExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
exporter = new InMemorySpanExporter();
|
||||
provider = new BasicTracerProvider({
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
});
|
||||
trace.setGlobalTracerProvider(provider);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await provider.shutdown();
|
||||
exporter.reset();
|
||||
trace.disable();
|
||||
});
|
||||
|
||||
const createMockSandbox = (): Sandbox => {
|
||||
const mockSandbox = {
|
||||
sandboxId: "test-sandbox-123",
|
||||
kill: vi.fn(async () => {}),
|
||||
runCode: vi.fn(async () => ({
|
||||
results: [],
|
||||
logs: { stdout: [], stderr: [] },
|
||||
error: undefined,
|
||||
executionCount: 1,
|
||||
})),
|
||||
files: {
|
||||
read: vi.fn(async () => "file content"),
|
||||
write: vi.fn(async () => ({
|
||||
path: "/test/file.txt",
|
||||
name: "file.txt",
|
||||
})),
|
||||
list: vi.fn(async () => [
|
||||
{ name: "file1.txt", type: "file" },
|
||||
{ name: "file2.txt", type: "file" },
|
||||
]),
|
||||
remove: vi.fn(async () => {}),
|
||||
makeDir: vi.fn(async () => true),
|
||||
},
|
||||
commands: {
|
||||
run: vi.fn(async () => ({
|
||||
exitCode: 0,
|
||||
stdout: ["output line 1", "output line 2"],
|
||||
stderr: [],
|
||||
})),
|
||||
},
|
||||
} as unknown as Sandbox;
|
||||
|
||||
return mockSandbox;
|
||||
};
|
||||
|
||||
it("instruments sandbox.kill() and records spans", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.kill();
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.kill");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.kill");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.runCode() and captures execution details", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const result = await (sandbox as any).runCode('print("hello")', {
|
||||
language: "python",
|
||||
});
|
||||
|
||||
expect(result.executionCount).toBe(1);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.code.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("code.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123");
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBe("python");
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_HAS_ERROR]).toBe(false);
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_EXECUTION_COUNT]).toBe(1);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.read() and captures file details", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const content = await sandbox.files.read("/path/to/file.txt");
|
||||
|
||||
expect(content).toBe("file content");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("files.read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "file content".length
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.read() with format option", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.read("/path/to/file.txt", { format: "bytes" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_FORMAT]).toBe("bytes");
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.write() for single file", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.write("/path/to/file.txt", "test content");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "test content".length
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.write() for multiple files", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const files = [
|
||||
{ path: "/file1.txt", data: "content1" },
|
||||
{ path: "/file2.txt", data: "content2" },
|
||||
{ path: "/file3.txt", data: "content3" },
|
||||
];
|
||||
|
||||
await (sandbox.files as any).write(files);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(3);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.list() and captures file count", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const files = await sandbox.files.list("/path/to/dir");
|
||||
|
||||
expect(files).toHaveLength(2);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.list");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("list");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/dir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(2);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.remove()", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.remove("/path/to/file.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.remove");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("remove");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.makeDir()", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.makeDir("/path/to/new/dir");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.makeDir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("makeDir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/new/dir");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.commands.run() and captures command results", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCommandOutput: true });
|
||||
|
||||
const result = await sandbox.commands.run("echo hello");
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.command.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("command.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_EXIT_CODE]).toBe(0);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBe(2);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBe(0);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(false);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.commands.run() in background mode", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.commands.run("long-running-command", { background: true });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(true);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("captures errors and marks span status", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.kill = vi.fn().mockRejectedValue(new Error("Failed to kill"));
|
||||
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await expect(async () => sandbox.kill()).rejects.toThrowError(
|
||||
"Failed to kill"
|
||||
);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.status.code).toBe(SpanStatusCode.ERROR);
|
||||
const hasException = span?.events.some(
|
||||
(event) => event.name === "exception"
|
||||
);
|
||||
expect(hasException).toBe(true);
|
||||
});
|
||||
|
||||
it("is idempotent - calling instrument twice doesn't double-wrap", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
|
||||
const first = instrumentSandbox(sandbox);
|
||||
const second = instrumentSandbox(first);
|
||||
|
||||
expect(first).toBe(second);
|
||||
|
||||
await sandbox.kill();
|
||||
|
||||
// Should only create one span, not two
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("respects configuration options - captureFilePaths", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureFilePaths: false });
|
||||
|
||||
await sandbox.files.read("/secret/path.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureFileSize", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureFileSize: false });
|
||||
|
||||
await sandbox.files.read("/path/file.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureCodeLanguage", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCodeLanguage: false });
|
||||
|
||||
await (sandbox as any).runCode('print("test")', { language: "python" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureCommandOutput disabled", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCommandOutput: false });
|
||||
|
||||
await sandbox.commands.run("echo test");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBeUndefined();
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("instrumentSandboxClass", () => {
|
||||
let provider: BasicTracerProvider;
|
||||
let exporter: InMemorySpanExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
exporter = new InMemorySpanExporter();
|
||||
provider = new BasicTracerProvider({
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
});
|
||||
trace.setGlobalTracerProvider(provider);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await provider.shutdown();
|
||||
exporter.reset();
|
||||
trace.disable();
|
||||
});
|
||||
|
||||
const createMockSandboxClass = () => {
|
||||
const mockSandbox = {
|
||||
sandboxId: "created-sandbox-456",
|
||||
kill: vi.fn(async () => {}),
|
||||
runCode: vi.fn(async () => ({
|
||||
results: [],
|
||||
logs: { stdout: [], stderr: [] },
|
||||
})),
|
||||
files: {
|
||||
read: vi.fn(async () => "content"),
|
||||
write: vi.fn(async () => ({ path: "/test.txt" })),
|
||||
list: vi.fn(async () => []),
|
||||
remove: vi.fn(async () => {}),
|
||||
makeDir: vi.fn(async () => true),
|
||||
},
|
||||
commands: {
|
||||
run: vi.fn(async () => ({ exitCode: 0, stdout: [], stderr: [] })),
|
||||
},
|
||||
} as unknown as Sandbox;
|
||||
|
||||
return {
|
||||
create: vi.fn(async () => mockSandbox),
|
||||
} as unknown as typeof Sandbox;
|
||||
};
|
||||
|
||||
it("instruments Sandbox.create() and records span", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
const sandbox = await SandboxClass.create();
|
||||
|
||||
expect(sandbox.sandboxId).toBe("created-sandbox-456");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.create");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.create");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe(
|
||||
"created-sandbox-456"
|
||||
);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("captures template from options", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
await SandboxClass.create({ template: "custom-template" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_TEMPLATE]).toBe(
|
||||
"custom-template"
|
||||
);
|
||||
});
|
||||
|
||||
it("automatically instruments created sandbox instances", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
const sandbox = await SandboxClass.create();
|
||||
|
||||
// Clear spans from create operation
|
||||
exporter.reset();
|
||||
|
||||
// Use the sandbox - should be automatically instrumented
|
||||
await sandbox.kill();
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.kill");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("is idempotent - calling instrumentSandboxClass twice doesn't double-wrap", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
|
||||
const first = instrumentSandboxClass(SandboxClass);
|
||||
const second = instrumentSandboxClass(first);
|
||||
|
||||
expect(first).toBe(second);
|
||||
|
||||
await SandboxClass.create();
|
||||
|
||||
// Should only create one span for create, not two
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
expect(spans[0]?.name).toBe("e2b.sandbox.create");
|
||||
});
|
||||
|
||||
it("handles errors during sandbox creation", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
(SandboxClass as any).create = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Failed to create sandbox"));
|
||||
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
await expect(async () => SandboxClass.create()).rejects.toThrowError(
|
||||
"Failed to create sandbox"
|
||||
);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.create");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.ERROR);
|
||||
const hasException = span?.events.some(
|
||||
(event) => event.name === "exception"
|
||||
);
|
||||
expect(hasException).toBe(true);
|
||||
});
|
||||
});
|
||||
672
packages/otel-e2b/src/index.ts
Normal file
672
packages/otel-e2b/src/index.ts
Normal file
@@ -0,0 +1,672 @@
|
||||
import {
|
||||
context,
|
||||
SpanKind,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
type Span,
|
||||
} from "@opentelemetry/api";
|
||||
import type { Sandbox } from "@e2b/code-interpreter";
|
||||
|
||||
const DEFAULT_TRACER_NAME = "@kubiks/otel-e2b";
|
||||
const INSTRUMENTED_FLAG = Symbol("kubiksOtelE2BInstrumented");
|
||||
const INSTRUMENTED_FILES_FLAG = Symbol("kubiksOtelE2BFilesInstrumented");
|
||||
const INSTRUMENTED_COMMANDS_FLAG = Symbol("kubiksOtelE2BCommandsInstrumented");
|
||||
|
||||
// Semantic attribute constants following OpenTelemetry conventions
|
||||
export const SEMATTRS_E2B_OPERATION = "e2b.operation" as const;
|
||||
export const SEMATTRS_E2B_SANDBOX_ID = "e2b.sandbox.id" as const;
|
||||
export const SEMATTRS_E2B_SANDBOX_TEMPLATE = "e2b.sandbox.template" as const;
|
||||
|
||||
// Code execution attributes
|
||||
export const SEMATTRS_E2B_CODE_LANGUAGE = "e2b.code.language" as const;
|
||||
export const SEMATTRS_E2B_CODE_HAS_ERROR = "e2b.code.has_error" as const;
|
||||
export const SEMATTRS_E2B_CODE_EXECUTION_COUNT =
|
||||
"e2b.code.execution_count" as const;
|
||||
|
||||
// Command execution attributes
|
||||
export const SEMATTRS_E2B_COMMAND_EXIT_CODE = "e2b.command.exit_code" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_STDOUT_LINES =
|
||||
"e2b.command.stdout_lines" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_STDERR_LINES =
|
||||
"e2b.command.stderr_lines" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_BACKGROUND =
|
||||
"e2b.command.background" as const;
|
||||
|
||||
// File operation attributes
|
||||
export const SEMATTRS_E2B_FILE_OPERATION = "e2b.file.operation" as const;
|
||||
export const SEMATTRS_E2B_FILE_PATH = "e2b.file.path" as const;
|
||||
export const SEMATTRS_E2B_FILE_SIZE_BYTES = "e2b.file.size_bytes" as const;
|
||||
export const SEMATTRS_E2B_FILE_FORMAT = "e2b.file.format" as const;
|
||||
export const SEMATTRS_E2B_FILE_COUNT = "e2b.file.count" as const;
|
||||
|
||||
/**
|
||||
* Configuration options for E2B instrumentation.
|
||||
*/
|
||||
export interface InstrumentE2BConfig {
|
||||
/**
|
||||
* Custom tracer name. Defaults to "@kubiks/otel-e2b".
|
||||
*/
|
||||
tracerName?: string;
|
||||
|
||||
/**
|
||||
* Whether to capture file paths in spans.
|
||||
* Paths only, not content.
|
||||
* @default true
|
||||
*/
|
||||
captureFilePaths?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture file sizes in spans.
|
||||
* @default true
|
||||
*/
|
||||
captureFileSize?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture code language in spans.
|
||||
* @default true
|
||||
*/
|
||||
captureCodeLanguage?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture command output line counts.
|
||||
* Only counts, not actual content.
|
||||
* @default false
|
||||
*/
|
||||
captureCommandOutput?: boolean;
|
||||
}
|
||||
|
||||
interface InstrumentedSandbox {
|
||||
[INSTRUMENTED_FLAG]?: true;
|
||||
}
|
||||
|
||||
interface InstrumentedFilesystem {
|
||||
[INSTRUMENTED_FILES_FLAG]?: true;
|
||||
}
|
||||
|
||||
interface InstrumentedCommands {
|
||||
[INSTRUMENTED_COMMANDS_FLAG]?: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes a span with status, timing, and optional error.
|
||||
*/
|
||||
function finalizeSpan(span: Span, error?: unknown): void {
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
span.recordException(error);
|
||||
} else {
|
||||
span.recordException(new Error(String(error)));
|
||||
}
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
}
|
||||
span.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the filesystem module of a sandbox.
|
||||
*/
|
||||
function instrumentFilesystem(
|
||||
files: any,
|
||||
sandboxId: string,
|
||||
tracer: ReturnType<typeof trace.getTracer>,
|
||||
config?: InstrumentE2BConfig
|
||||
): any {
|
||||
if (!files) {
|
||||
return files;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG]) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const { captureFilePaths = true, captureFileSize = true } = config ?? {};
|
||||
|
||||
// Instrument read
|
||||
const originalRead = files.read?.bind(files);
|
||||
if (originalRead) {
|
||||
files.read = async function instrumentedRead(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.read", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.read",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "read",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
if (opts?.format) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_FORMAT, opts.format);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRead(path, opts)
|
||||
);
|
||||
|
||||
// Capture size if result is a string or has a length/size property
|
||||
if (captureFileSize) {
|
||||
if (typeof result === "string") {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length);
|
||||
} else if (result?.length !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length);
|
||||
} else if (result?.size !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.size);
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument write (handles both single and multiple file writes)
|
||||
const originalWrite = files.write?.bind(files);
|
||||
if (originalWrite) {
|
||||
files.write = async function instrumentedWrite(
|
||||
pathOrFiles: string | any[],
|
||||
dataOrOpts?: any,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.write", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.write",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "write",
|
||||
});
|
||||
|
||||
const isArray = Array.isArray(pathOrFiles);
|
||||
|
||||
if (isArray) {
|
||||
// Multiple files
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_COUNT, pathOrFiles.length);
|
||||
} else if (captureFilePaths && typeof pathOrFiles === "string") {
|
||||
// Single file
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, pathOrFiles);
|
||||
|
||||
// Try to capture size
|
||||
if (captureFileSize && dataOrOpts) {
|
||||
if (typeof dataOrOpts === "string") {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length);
|
||||
} else if (dataOrOpts?.length !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length);
|
||||
} else if (dataOrOpts?.size !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalWrite(pathOrFiles, dataOrOpts, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument list
|
||||
const originalList = files.list?.bind(files);
|
||||
if (originalList) {
|
||||
files.list = async function instrumentedList(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.list", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.list",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "list",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalList(path, opts)
|
||||
);
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_COUNT, result.length);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument remove
|
||||
const originalRemove = files.remove?.bind(files);
|
||||
if (originalRemove) {
|
||||
files.remove = async function instrumentedRemove(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.remove", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.remove",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "remove",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRemove(path, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument makeDir
|
||||
const originalMakeDir = files.makeDir?.bind(files);
|
||||
if (originalMakeDir) {
|
||||
files.makeDir = async function instrumentedMakeDir(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.makeDir", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.makeDir",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "makeDir",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalMakeDir(path, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG] = true;
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the commands module of a sandbox.
|
||||
*/
|
||||
function instrumentCommands(
|
||||
commands: any,
|
||||
sandboxId: string,
|
||||
tracer: ReturnType<typeof trace.getTracer>,
|
||||
config?: InstrumentE2BConfig
|
||||
): any {
|
||||
if (!commands) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG]) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
const { captureCommandOutput = false } = config ?? {};
|
||||
|
||||
// Instrument run
|
||||
const originalRun = commands.run?.bind(commands);
|
||||
if (originalRun) {
|
||||
commands.run = async function instrumentedRun(
|
||||
cmd: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.command.run", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
const isBackground = opts?.background === true;
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "command.run",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_COMMAND_BACKGROUND]: isBackground,
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRun(cmd, opts)
|
||||
);
|
||||
|
||||
// For non-background commands, capture result details
|
||||
if (!isBackground && result) {
|
||||
if (result.exitCode !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_EXIT_CODE, result.exitCode);
|
||||
}
|
||||
|
||||
if (captureCommandOutput) {
|
||||
if (result.stdout !== undefined) {
|
||||
const stdoutLines = Array.isArray(result.stdout)
|
||||
? result.stdout.length
|
||||
: typeof result.stdout === "string"
|
||||
? result.stdout.split("\n").length
|
||||
: 0;
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_STDOUT_LINES, stdoutLines);
|
||||
}
|
||||
|
||||
if (result.stderr !== undefined) {
|
||||
const stderrLines = Array.isArray(result.stderr)
|
||||
? result.stderr.length
|
||||
: typeof result.stderr === "string"
|
||||
? result.stderr.split("\n").length
|
||||
: 0;
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_STDERR_LINES, stderrLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG] = true;
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments an E2B Sandbox instance with OpenTelemetry tracing.
|
||||
*
|
||||
* This function wraps sandbox methods to create spans for each operation.
|
||||
* The instrumentation is idempotent - calling it multiple times on the same
|
||||
* sandbox will only instrument it once.
|
||||
*
|
||||
* @param sandbox - The E2B Sandbox to instrument
|
||||
* @param config - Optional configuration for instrumentation behavior
|
||||
* @returns The instrumented sandbox (same instance, modified in place)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Sandbox } from '@e2b/code-interpreter';
|
||||
* import { instrumentSandbox } from '@kubiks/otel-e2b';
|
||||
*
|
||||
* const sandbox = await Sandbox.create();
|
||||
* instrumentSandbox(sandbox, {
|
||||
* captureFilePaths: true,
|
||||
* captureFileSize: true,
|
||||
* });
|
||||
*
|
||||
* // All operations are now traced
|
||||
* await sandbox.runCode('print("Hello")');
|
||||
* ```
|
||||
*/
|
||||
export function instrumentSandbox<T extends Sandbox>(
|
||||
sandbox: T,
|
||||
config?: InstrumentE2BConfig
|
||||
): T {
|
||||
if (!sandbox) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((sandbox as any)[INSTRUMENTED_FLAG]) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
const { tracerName = DEFAULT_TRACER_NAME, captureCodeLanguage = true } =
|
||||
config ?? {};
|
||||
|
||||
const tracer = trace.getTracer(tracerName);
|
||||
const sandboxId = sandbox.sandboxId || "unknown";
|
||||
|
||||
// Instrument kill method
|
||||
const originalKill = sandbox.kill?.bind(sandbox);
|
||||
if (originalKill) {
|
||||
(sandbox as any).kill = async function instrumentedKill(
|
||||
opts?: any
|
||||
): Promise<void> {
|
||||
const span = tracer.startSpan("e2b.sandbox.kill", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "sandbox.kill",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
await context.with(activeContext, () => originalKill(opts));
|
||||
finalizeSpan(span);
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument runCode method (code-interpreter specific)
|
||||
const originalRunCode = (sandbox as any).runCode?.bind(sandbox);
|
||||
if (originalRunCode) {
|
||||
(sandbox as any).runCode = async function instrumentedRunCode(
|
||||
code: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.code.run", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "code.run",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
});
|
||||
|
||||
// Capture language if available
|
||||
if (captureCodeLanguage) {
|
||||
const language = opts?.language || opts?.context?.language || "python";
|
||||
span.setAttribute(SEMATTRS_E2B_CODE_LANGUAGE, language);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRunCode(code, opts)
|
||||
);
|
||||
|
||||
// Capture execution details
|
||||
if (result) {
|
||||
// Always set has_error attribute (true if error exists, false otherwise)
|
||||
span.setAttribute(SEMATTRS_E2B_CODE_HAS_ERROR, !!result.error);
|
||||
|
||||
if (result.executionCount !== undefined) {
|
||||
span.setAttribute(
|
||||
SEMATTRS_E2B_CODE_EXECUTION_COUNT,
|
||||
result.executionCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument filesystem module
|
||||
if (sandbox.files) {
|
||||
instrumentFilesystem(sandbox.files, sandboxId, tracer, config);
|
||||
}
|
||||
|
||||
// Instrument commands module
|
||||
if (sandbox.commands) {
|
||||
instrumentCommands(sandbox.commands, sandboxId, tracer, config);
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(sandbox as any)[INSTRUMENTED_FLAG] = true;
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the Sandbox class itself to automatically instrument all created sandboxes.
|
||||
*
|
||||
* This function wraps the static `Sandbox.create()` method to automatically
|
||||
* instrument any sandbox instances it creates.
|
||||
*
|
||||
* @param SandboxClass - The Sandbox class to instrument
|
||||
* @param config - Optional configuration for instrumentation behavior
|
||||
* @returns The instrumented Sandbox class (same class, modified in place)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Sandbox } from '@e2b/code-interpreter';
|
||||
* import { instrumentSandboxClass } from '@kubiks/otel-e2b';
|
||||
*
|
||||
* // Instrument the class once at startup
|
||||
* instrumentSandboxClass(Sandbox, {
|
||||
* captureFilePaths: true,
|
||||
* captureFileSize: true,
|
||||
* });
|
||||
*
|
||||
* // All sandboxes created after this are automatically instrumented
|
||||
* const sandbox = await Sandbox.create();
|
||||
* await sandbox.runCode('print("Hello")'); // Automatically traced
|
||||
* ```
|
||||
*/
|
||||
export function instrumentSandboxClass<T extends typeof Sandbox>(
|
||||
SandboxClass: T,
|
||||
config?: InstrumentE2BConfig
|
||||
): T {
|
||||
if (!SandboxClass) {
|
||||
return SandboxClass;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((SandboxClass as any)[INSTRUMENTED_FLAG]) {
|
||||
return SandboxClass;
|
||||
}
|
||||
|
||||
const { tracerName = DEFAULT_TRACER_NAME } = config ?? {};
|
||||
const tracer = trace.getTracer(tracerName);
|
||||
|
||||
// Instrument the static create method
|
||||
const originalCreate = SandboxClass.create?.bind(SandboxClass);
|
||||
if (originalCreate) {
|
||||
(SandboxClass as any).create = async function instrumentedCreate(
|
||||
opts?: any
|
||||
): Promise<Sandbox> {
|
||||
const span = tracer.startSpan("e2b.sandbox.create", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "sandbox.create",
|
||||
});
|
||||
|
||||
// Capture template if available
|
||||
if (opts?.template) {
|
||||
span.setAttribute(SEMATTRS_E2B_SANDBOX_TEMPLATE, opts.template);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const sandbox = await context.with(activeContext, () =>
|
||||
originalCreate(opts)
|
||||
);
|
||||
|
||||
// Add sandbox ID to span
|
||||
if (sandbox?.sandboxId) {
|
||||
span.setAttribute(SEMATTRS_E2B_SANDBOX_ID, sandbox.sandboxId);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
|
||||
// Automatically instrument the created sandbox
|
||||
instrumentSandbox(sandbox as Sandbox, config);
|
||||
|
||||
return sandbox as Sandbox;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(SandboxClass as any)[INSTRUMENTED_FLAG] = true;
|
||||
|
||||
return SandboxClass;
|
||||
}
|
||||
22
packages/otel-e2b/tsconfig.json
Normal file
22
packages/otel-e2b/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declarationDir": "dist/types",
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user