diff --git a/.changeset/dull-snakes-unite.md b/.changeset/dull-snakes-unite.md
new file mode 100644
index 0000000000..b71a1794db
--- /dev/null
+++ b/.changeset/dull-snakes-unite.md
@@ -0,0 +1,7 @@
+---
+'@e2b/python-sdk': minor
+'e2b': minor
+'@e2b/cli': minor
+---
+
+Expose sandbox metrics
diff --git a/apps/web/src/app/(docs)/docs/sandbox/installing-beta-sdks/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/installing-beta-sdks/page.mdx
index 590c593f1c..69bdca3634 100644
--- a/apps/web/src/app/(docs)/docs/sandbox/installing-beta-sdks/page.mdx
+++ b/apps/web/src/app/(docs)/docs/sandbox/installing-beta-sdks/page.mdx
@@ -1,6 +1,6 @@
# Installing beta SDKs
-To use features like [sandbox persistence](/docs/sandbox/persistence) and [metrics](/docs/sandbox/metrics), you need to install the beta version of the SDKs.
+To use features like [sandbox persistence](/docs/sandbox/persistence) you need to install the beta version of the SDKs.
### Code Interpreter SDK
diff --git a/apps/web/src/app/(docs)/docs/sandbox/metrics/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/metrics/page.mdx
index 395c386f53..f1ac71785f 100644
--- a/apps/web/src/app/(docs)/docs/sandbox/metrics/page.mdx
+++ b/apps/web/src/app/(docs)/docs/sandbox/metrics/page.mdx
@@ -1,22 +1,10 @@
# Sandbox metrics
-
-This feature is in a private beta.
-
-
-The sandbox metrics allows you to get information about the sandbox's CPU and memory usage.
-
-## Installation
-
-To get sandbox metrics, you need to install the beta version of the SDKs and CLI:
-
-- [Installing beta SDKs](/docs/sandbox/installing-beta-sdks)
-
-- [Installing beta CLI](/docs/cli#beta-cli)
+The sandbox metrics allows you to get information about the sandbox's CPU, memory and disk usage.
## Getting sandbox metrics
-Getting the metrics of a sandbox returns an array of timestamped metrics containing CPU and memory usage information.
-The metrics are collected at the start of the sandbox, then every 2 seconds, and finally right before the sandbox is deleted.
+Getting the metrics of a sandbox returns an array of timestamped metrics containing CPU, memory and disk usage information.
+The metrics are collected every 5 seconds.
### Getting sandbox metrics using the SDKs
@@ -27,6 +15,9 @@ import { Sandbox } from '@e2b/code-interpreter'
const sbx = await Sandbox.create()
console.log('Sandbox created', sbx.sandboxId)
+// Wait for a few seconds to collect some metrics
+await new Promise((resolve) => setTimeout(resolve, 10_000))
+
const metrics = await sbx.getMetrics() // $HighlightLine
// You can also get the metrics by sandbox ID:
@@ -34,30 +25,38 @@ const metrics = await sbx.getMetrics() // $HighlightLine
console.log('Sandbox metrics:', metrics)
+// Sandbox metrics:
// [
// {
+// timestamp: 2025-07-28T08:04:05.000Z,
+// cpuUsedPct: 20.33,
// cpuCount: 2,
-// cpuUsedPct: 50.05,
-// memTotalMiB: 484,
-// memUsedMiB: 37,
-// timestamp: '2025-01-23T23:44:12.222Z'
+// memUsed: 32681984, // in bytes
+// memTotal: 507592704, // in bytes
+// diskUsed: 1514856448, // in bytes
+// diskTotal: 2573185024 // in bytes
// },
// {
+// timestamp: 2025-07-28T08:04:10.000Z,
+// cpuUsedPct: 0.2,
// cpuCount: 2,
-// cpuUsedPct: 4.5,
-// memTotalMiB: 484,
-// memUsedMiB: 37,
-// timestamp: '2025-01-23T23:44:13.220Z'
+// memUsed: 33316864, // in bytes
+// memTotal: 507592704, // in bytes
+// diskUsed: 1514856448, // in bytes
+// diskTotal: 2573185024 // in bytes
// }
// ]
-
```
```python
+from time import sleep
from e2b_code_interpreter import Sandbox
sbx = Sandbox()
print('Sandbox created', sbx.sandbox_id)
+# Wait for a few seconds to collect some metrics
+sleep(10)
+
metrics = sbx.get_metrics() # $HighlightLine
# You can also get the metrics by sandbox ID:
@@ -65,21 +64,26 @@ metrics = sbx.get_metrics() # $HighlightLine
print('Sandbox metrics', metrics)
+# Sandbox metrics
# [
-# SandboxMetrics(timestamp=datetime.datetime(
-# 2025, 1, 23, 23, 58, 42, 84050, tzinfo=tzutc()),
-# cpu_count=2,
-# cpu_used_pct=50.07,
-# mem_total_mib=484
-# mem_used_mib=37,
-# ),
-# SandboxMetrics(timestamp=datetime.datetime(
-# 2025, 1, 23, 23, 58, 44, 84845, tzinfo=tzutc()),
-# cpu_count=2,
-# cpu_used_pct=4.75,
-# mem_total_mib=484
-# mem_used_mib=38,
-# ),
+# SandboxMetric(
+# cpu_count=2,
+# cpu_used_pct=13.97,
+# disk_total=2573185024, # in bytes
+# disk_used=1514856448, # in bytes
+# mem_total=507592704, # in bytes
+# mem_used=30588928, # in bytes
+# timestamp=datetime.datetime(2025, 7, 28, 8, 8, 15, tzinfo=tzutc()),
+# ),
+# SandboxMetric(
+# cpu_count=2,
+# cpu_used_pct=0.1,
+# disk_total=2573185024, # in bytes
+# disk_used=1514856448, # in bytes
+# mem_total=507592704, # in bytes
+# mem_used=31084544, # in bytes
+# timestamp=datetime.datetime(2025, 7, 28, 8, 8, 20, tzinfo=tzutc()),
+# ),
# ]
```
@@ -91,11 +95,13 @@ e2b sandbox metrics # $HighlightLine
# Metrics for sandbox
#
-# [2025-01-23 00:58:58.829Z] { cpuCount: 2, cpuUsedPct: 50.21, logger: '', memTotalMiB: 484, memUsedMiB: 38, timestamp: '2025-01-23T00:58:58.829638869Z' }
-# [2025-01-23 00:59:03.814Z] { cpuCount: 2, cpuUsedPct: 5.16, logger: '', memTotalMiB: 484, memUsedMiB: 37, timestamp: '2025-01-23T00:59:03.814028031Z' }
-# [2025-01-23 00:59:08.815Z] { cpuCount: 2, cpuUsedPct: 1.6, logger: '', memTotalMiB: 484, memUsedMiB: 37, timestamp: '2025-01-23T00:59:08.815933749Z' }
+# [2025-07-25 14:05:55Z] CPU: 8.27% / 2 Cores | Memory: 31 / 484 MiB | Disk: 1445 / 2453 MiB
+# [2025-07-25 14:06:00Z] CPU: 0.5% / 2 Cores | Memory: 32 / 484 MiB | Disk: 1445 / 2453 MiB
+# [2025-07-25 14:06:05Z] CPU: 0.1% / 2 Cores | Memory: 32 / 484 MiB | Disk: 1445 / 2453 MiB
+# [2025-07-25 14:06:10Z] CPU: 0.3% / 2 Cores | Memory: 32 / 484 MiB | Disk: 1445 / 2453 MiB
```
-## Limitations while in beta
-- It may take a second or more to get the metrics after the sandbox is created. Until the logs are collected from the sandbox, you will get an empty array.
\ No newline at end of file
+
+ It may take a second or more to get the first metrics after the sandbox is created. Until the first metrics are collected from the sandbox, you will get an empty array.
+
diff --git a/packages/cli/src/commands/sandbox/index.ts b/packages/cli/src/commands/sandbox/index.ts
index 538ad5bffa..3c61df8481 100644
--- a/packages/cli/src/commands/sandbox/index.ts
+++ b/packages/cli/src/commands/sandbox/index.ts
@@ -5,6 +5,7 @@ import { listCommand } from './list'
import { killCommand } from './kill'
import { spawnCommand } from './spawn'
import { logsCommand } from './logs'
+import { metricsCommand } from './metrics'
export const sandboxCommand = new commander.Command('sandbox')
.description('work with sandboxes')
@@ -14,3 +15,4 @@ export const sandboxCommand = new commander.Command('sandbox')
.addCommand(killCommand)
.addCommand(spawnCommand)
.addCommand(logsCommand)
+ .addCommand(metricsCommand)
diff --git a/packages/cli/src/commands/sandbox/logs.ts b/packages/cli/src/commands/sandbox/logs.ts
index 846259e430..4c5ee90f31 100644
--- a/packages/cli/src/commands/sandbox/logs.ts
+++ b/packages/cli/src/commands/sandbox/logs.ts
@@ -8,45 +8,7 @@ import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
import { listSandboxes } from './list'
import { wait } from 'src/utils/wait'
import { handleE2BRequestError } from '../../utils/errors'
-
-const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
-
-function getShortID(sandboxID: string) {
- return sandboxID.split('-')[0]
-}
-
-function waitForSandboxEnd(sandboxID: string) {
- let isRunning = true
-
- async function monitor() {
- const startTime = new Date().getTime()
-
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const currentTime = new Date().getTime()
- const elapsedTime = currentTime - startTime // Time elapsed in milliseconds
-
- // Check if 24 hours (in milliseconds) have passed
- if (elapsedTime >= maxRuntime) {
- break
- }
-
- const response = await listSandboxes()
- const sandbox = response.find(
- (s) => s.sandboxID === getShortID(sandboxID)
- )
- if (!sandbox) {
- isRunning = false
- break
- }
- await wait(5000)
- }
- }
-
- monitor()
-
- return () => isRunning
-}
+import { waitForSandboxEnd, formatEnum, Format, getShortID } from './utils'
enum LogLevel {
DEBUG = 'DEBUG',
@@ -76,17 +38,6 @@ function isLevelIncluded(level: LogLevel, allowedLevel?: LogLevel) {
}
}
-function formatEnum(e: { [key: string]: string }) {
- return Object.values(e)
- .map((level) => asBold(level))
- .join(', ')
-}
-
-enum LogFormat {
- JSON = 'json',
- PRETTY = 'pretty',
-}
-
function cleanLogger(logger?: string) {
if (!logger) {
return ''
@@ -112,8 +63,8 @@ export const logsCommand = new commander.Command('logs')
.option('-f, --follow', 'keep streaming logs until the sandbox is closed')
.option(
'--format ',
- `specify format for printing logs (${formatEnum(LogFormat)})`,
- LogFormat.PRETTY
+ `specify format for printing logs (${formatEnum(Format)})`,
+ Format.PRETTY
)
.option(
'--loggers [loggers]',
@@ -126,7 +77,7 @@ export const logsCommand = new commander.Command('logs')
opts?: {
level: string
follow: boolean
- format: LogFormat
+ format: Format
loggers?: string[]
}
) => {
@@ -136,8 +87,8 @@ export const logsCommand = new commander.Command('logs')
throw new Error(`Invalid log level: ${level}`)
}
- const format = opts?.format.toLowerCase() as LogFormat | undefined
- if (format && !Object.values(LogFormat).includes(format)) {
+ const format = opts?.format.toLowerCase() as Format | undefined
+ if (format && !Object.values(Format).includes(format)) {
throw new Error(`Invalid log format: ${format}`)
}
@@ -149,7 +100,7 @@ export const logsCommand = new commander.Command('logs')
let isFirstRun = true
let firstLogsPrinted = false
- if (format === LogFormat.PRETTY) {
+ if (format === Format.PRETTY) {
console.log(`\nLogs for sandbox ${asBold(sandboxID)}:`)
}
@@ -178,7 +129,7 @@ export const logsCommand = new commander.Command('logs')
const isRunning = await isRunningPromise
if (!isRunning && logs.length === 0 && isFirstRun) {
- if (format === LogFormat.PRETTY) {
+ if (format === Format.PRETTY) {
console.log(
`\nStopped printing logs — sandbox ${withUnderline(
'not found'
@@ -189,7 +140,7 @@ export const logsCommand = new commander.Command('logs')
}
if (!isRunning) {
- if (format === LogFormat.PRETTY) {
+ if (format === Format.PRETTY) {
console.log(
`\nStopped printing logs — sandbox is ${withUnderline(
'closed'
@@ -219,7 +170,7 @@ function printLog(
timestamp: string,
line: string,
allowedLevel: LogLevel | undefined,
- format: LogFormat | undefined,
+ format: Format | undefined,
allowedLoggers?: string[] | undefined
) {
const log = JSON.parse(line)
@@ -266,7 +217,7 @@ function printLog(
delete log['envID']
delete log['sandboxID']
- if (format === LogFormat.JSON) {
+ if (format === Format.JSON) {
console.log(
JSON.stringify({
timestamp: new Date(timestamp).toISOString(),
diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts
new file mode 100644
index 0000000000..d67ff6ab62
--- /dev/null
+++ b/packages/cli/src/commands/sandbox/metrics.ts
@@ -0,0 +1,144 @@
+import * as chalk from 'chalk'
+import * as commander from 'commander'
+
+import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
+import { wait } from 'src/utils/wait'
+import { listSandboxes } from './list'
+import { formatEnum, getShortID, Format } from './utils'
+import { Sandbox } from 'e2b'
+
+export const metricsCommand = new commander.Command('metrics')
+ .description('show metrics for sandbox')
+ .argument(
+ '',
+ `show metrics for sandbox specified by ${asBold('')}`
+ )
+ .alias('mt')
+ .option('-f, --follow', 'keep streaming metrics until the sandbox is closed')
+ .option(
+ '--format ',
+ `specify format for printing metrics (${formatEnum(Format)})`,
+ Format.PRETTY
+ )
+ .action(
+ async (
+ sandboxID: string,
+ opts?: {
+ follow: boolean
+ format: Format
+ }
+ ) => {
+ try {
+ const format = opts?.format.toLowerCase() as Format | undefined
+ if (format && !Object.values(Format).includes(format)) {
+ throw new Error(`Invalid log format: ${format}`)
+ }
+
+ let start: Date | undefined
+ let isFirstRun = true
+ let firstMetricsPrinted = false
+
+ if (format === Format.PRETTY) {
+ console.log(`\nMetrics for sandbox ${asBold(sandboxID)}:`)
+ }
+
+ const isRunningPromise = listSandboxes()
+ .then((r) => r.find((s) => s.sandboxID === getShortID(sandboxID)))
+ .then((s) => !!s)
+
+ do {
+ const metrics = await Sandbox.getMetrics(sandboxID, { start })
+
+ if (metrics.length !== 0 && !firstMetricsPrinted) {
+ firstMetricsPrinted = true
+ process.stdout.write('\n')
+ }
+
+ for (const metric of metrics) {
+ if (start && metric.timestamp <= start) {
+ // Skip the metric if it has the same timestamp as the last one
+ continue
+ }
+ start = metric.timestamp
+
+ printMetric(metric.timestamp, JSON.stringify(metric), format)
+ }
+
+ const isRunning = await isRunningPromise
+
+ if (!isRunning && metrics.length === 0 && isFirstRun) {
+ if (format === Format.PRETTY) {
+ console.log(
+ `\nStopped printing metrics — sandbox ${withUnderline(
+ 'not found'
+ )}`
+ )
+ }
+ break
+ }
+
+ if (!isRunning) {
+ if (format === Format.PRETTY) {
+ console.log(
+ `\nStopped printing metrics — sandbox is ${withUnderline(
+ 'closed'
+ )}`
+ )
+ }
+ break
+ }
+
+ await wait(400)
+ isFirstRun = false
+ } while (opts?.follow)
+ } catch (err: any) {
+ console.error(err)
+ process.exit(1)
+ }
+ }
+ )
+
+function printMetric(
+ timestamp: Date,
+ line: string,
+ format: Format | undefined
+) {
+ const metric = JSON.parse(line)
+ const level = chalk.default.green()
+
+ if (format === Format.JSON) {
+ console.log(
+ JSON.stringify({
+ timestamp: timestamp.toISOString(),
+ ...metric,
+ })
+ )
+ } else {
+ const time = `[${timestamp
+ .toISOString()
+ .replace(/\.\d{3}Z/, 'Z')
+ .replace(/T/, ' ')}]`
+ delete metric['timestamp']
+ const multipleCores = metric.cpuCount > 1
+ metric.cpuCount += 0
+ console.log(
+ `${asTimestamp(time)} ${level} ` +
+ asBold('CPU') +
+ `: ${metric.cpuUsedPct.toString().padStart(5)}% / ${metric.cpuCount
+ .toString()
+ .padStart(2)} Core${multipleCores && 's'} | ` +
+ asBold('Memory') +
+ `: ${(metric.memUsed >>> 20).toString().padStart(5)} / ${(
+ metric.memTotal >>> 20
+ )
+ .toString()
+ .padEnd(5)} MiB | ` +
+ asBold('Disk') +
+ `: ${(metric.diskUsed >>> 20).toString().padStart(5)} / ${(
+ metric.diskTotal >>> 20
+ )
+ .toString()
+ .padEnd(5)} MiB`
+ )
+ }
+}
diff --git a/packages/cli/src/commands/sandbox/utils.ts b/packages/cli/src/commands/sandbox/utils.ts
new file mode 100644
index 0000000000..1f2fc6ab1e
--- /dev/null
+++ b/packages/cli/src/commands/sandbox/utils.ts
@@ -0,0 +1,55 @@
+import { listSandboxes } from './list'
+import { wait } from '../../utils/wait'
+import {asBold} from '../../utils/format'
+
+
+export function formatEnum(e: { [key: string]: string }) {
+ return Object.values(e)
+ .map((level) => asBold(level))
+ .join(', ')
+}
+
+export enum Format {
+ JSON = 'json',
+ PRETTY = 'pretty',
+}
+
+
+const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
+
+export function waitForSandboxEnd(sandboxID: string) {
+ let isRunning = true
+
+ async function monitor() {
+ const startTime = new Date().getTime()
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const currentTime = new Date().getTime()
+ const elapsedTime = currentTime - startTime // Time elapsed in milliseconds
+
+ // Check if 24 hours (in milliseconds) have passed
+ if (elapsedTime >= maxRuntime) {
+ break
+ }
+
+ const response = await listSandboxes()
+ const sandbox = response.find(
+ (s) => s.sandboxID === getShortID(sandboxID)
+ )
+ if (!sandbox) {
+ isRunning = false
+ break
+ }
+ await wait(5000)
+ }
+ }
+
+ monitor()
+
+ return () => isRunning
+}
+
+export function getShortID(sandboxID: string) {
+ return sandboxID.split('-')[0]
+}
diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts
index 25747aedb0..f02f8ffb73 100644
--- a/packages/js-sdk/src/api/schema.gen.ts
+++ b/packages/js-sdk/src/api/schema.gen.ts
@@ -699,6 +699,7 @@ export interface paths {
get: {
parameters: {
query?: {
+ level?: components["schemas"]["LogLevel"];
/** @description Index of the starting build log that should be returned with the template */
logsOffset?: number;
};
@@ -733,6 +734,49 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/templates/{templateID}/files/{hash}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Get an upload link for a tar file containing build layer files */
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ hash: string;
+ templateID: components["parameters"]["templateID"];
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The upload link where to upload the tar file */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TemplateBuildFileUpload"];
+ };
+ };
+ 400: components["responses"]["400"];
+ 401: components["responses"]["401"];
+ 404: components["responses"]["404"];
+ 500: components["responses"]["500"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/v2/sandboxes": {
parameters: {
query?: never;
@@ -781,10 +825,107 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/v2/templates": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** @description Create a new template */
+ post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TemplateBuildRequestV2"];
+ };
+ };
+ responses: {
+ /** @description The build was requested successfully */
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Template"];
+ };
+ };
+ 400: components["responses"]["400"];
+ 401: components["responses"]["401"];
+ 500: components["responses"]["500"];
+ };
+ };
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/v2/templates/{templateID}/builds/{buildID}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** @description Start the build */
+ post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ buildID: components["parameters"]["buildID"];
+ templateID: components["parameters"]["templateID"];
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TemplateBuildStartV2"];
+ };
+ };
+ responses: {
+ /** @description The build has started */
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["401"];
+ 500: components["responses"]["500"];
+ };
+ };
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record;
export interface components {
schemas: {
+ BuildLogEntry: {
+ /** @description Log level of the entry */
+ level: components["schemas"]["LogLevel"];
+ /** @description Log message content */
+ message: string;
+ /**
+ * Format: date-time
+ * @description Timestamp of the log entry
+ */
+ timestamp: string;
+ };
/**
* Format: int32
* @description CPU cores for the sandbox
@@ -879,6 +1020,11 @@ export interface components {
/** @description Identifier of the template from which is the sandbox created */
templateID: string;
};
+ /**
+ * @description State of the sandbox
+ * @enum {string}
+ */
+ LogLevel: "debug" | "info" | "warn" | "error";
/**
* Format: int32
* @description Memory for the sandbox in MB
@@ -1084,6 +1230,16 @@ export interface components {
* @description CPU usage percentage
*/
cpuUsedPct: number;
+ /**
+ * Format: int64
+ * @description Total disk space in bytes
+ */
+ diskTotal: number;
+ /**
+ * Format: int64
+ * @description Disk used in bytes
+ */
+ diskUsed: number;
/**
* Format: int64
* @description Total memory in bytes
@@ -1186,6 +1342,11 @@ export interface components {
TemplateBuild: {
/** @description Identifier of the build */
buildID: string;
+ /**
+ * @description Build logs structured
+ * @default []
+ */
+ logEntries: components["schemas"]["BuildLogEntry"][];
/**
* @description Build logs
* @default []
@@ -1201,6 +1362,12 @@ export interface components {
/** @description Identifier of the template */
templateID: string;
};
+ TemplateBuildFileUpload: {
+ /** @description Whether the file is already present in the cache */
+ present: boolean;
+ /** @description Url where the file should be uploaded to */
+ url?: string;
+ };
TemplateBuildRequest: {
/** @description Alias of the template */
alias?: string;
@@ -1215,6 +1382,49 @@ export interface components {
/** @description Identifier of the team */
teamID?: string;
};
+ TemplateBuildRequestV2: {
+ /** @description Alias of the template */
+ alias: string;
+ cpuCount?: components["schemas"]["CPUCount"];
+ memoryMB?: components["schemas"]["MemoryMB"];
+ /** @description Identifier of the team */
+ teamID?: string;
+ };
+ TemplateBuildStartV2: {
+ /**
+ * @description Whether the whole build should be forced to run regardless of the cache
+ * @default false
+ */
+ force: boolean;
+ /** @description Image to use as a base for the template build */
+ fromImage: string;
+ /** @description Ready check command to execute in the template after the build */
+ readyCmd?: string;
+ /** @description Start command to execute in the template after the build */
+ startCmd?: string;
+ /**
+ * @description List of steps to execute in the template build
+ * @default []
+ */
+ steps: components["schemas"]["TemplateStep"][];
+ };
+ /** @description Step in the template build process */
+ TemplateStep: {
+ /**
+ * @description Arguments for the step
+ * @default []
+ */
+ args: string[];
+ /** @description Hash of the files used in the step */
+ filesHash?: string;
+ /**
+ * @description Whether the step should be forced to run regardless of the cache
+ * @default false
+ */
+ force: boolean;
+ /** @description Type of the step */
+ type: string;
+ };
TemplateUpdateRequest: {
/** @description Whether the template is public or only accessible by the team */
public?: boolean;
diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts
index 7aa151a7c7..a0469fa983 100644
--- a/packages/js-sdk/src/index.ts
+++ b/packages/js-sdk/src/index.ts
@@ -41,7 +41,7 @@ export type {
} from './sandbox/commands'
export type { SandboxOpts } from './sandbox'
-export type { SandboxInfo } from './sandbox/sandboxApi'
+export type { SandboxInfo, SandboxMetrics } from './sandbox/sandboxApi'
export { Sandbox }
import { Sandbox } from './sandbox'
export default Sandbox
diff --git a/packages/js-sdk/src/logs.ts b/packages/js-sdk/src/logs.ts
index 87422b95f8..5d6d51a2b0 100644
--- a/packages/js-sdk/src/logs.ts
+++ b/packages/js-sdk/src/logs.ts
@@ -13,6 +13,10 @@ export interface Logger {
* Info level logging method.
*/
info?: (...args: any[]) => void
+ /**
+ * Warn level logging method.
+ */
+ warn?: (...args: any[]) => void
/**
* Error level logging method.
*/
diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts
index 8dca724847..eae898942f 100644
--- a/packages/js-sdk/src/sandbox/index.ts
+++ b/packages/js-sdk/src/sandbox/index.ts
@@ -10,8 +10,10 @@ import { EnvdApiClient, handleEnvdApiError } from '../envd/api'
import { createRpcLogger } from '../logs'
import { Commands, Pty } from './commands'
import { Filesystem } from './filesystem'
-import { SandboxApi } from './sandboxApi'
+import { SandboxApi, SandboxMetricsOpts } from './sandboxApi'
import { getSignature } from './signature'
+import { compareVersions } from 'compare-versions'
+import { SandboxError } from '../errors'
/**
* Options for creating a new Sandbox.
@@ -520,6 +522,35 @@ export class Sandbox extends SandboxApi {
})
}
+ /**
+ * Get the metrics of the sandbox.
+ *
+ * @param opts connection options.
+ *
+ * @returns List of sandbox metrics containing CPU, memory and disk usage information.
+ */
+ async getMetrics(opts?: Pick) {
+ if (this.envdApi.version) {
+ if (compareVersions(this.envdApi.version, '0.1.5') < 0) {
+ throw new SandboxError(
+ 'You need to update the template to use the new SDK. ' +
+ 'You can do this by running `e2b template build` in the directory with the template.'
+ )
+ }
+
+ if (compareVersions(this.envdApi.version, '0.2.4') < 0) {
+ this.connectionConfig.logger?.warn?.(
+ 'Disk metrics are not supported in this version of the sandbox, please rebuild the template to get disk metrics.'
+ )
+ }
+ }
+
+ return await Sandbox.getMetrics(this.sandboxId, {
+ ...this.connectionConfig,
+ ...opts,
+ })
+ }
+
private fileUrl(path?: string, username?: string) {
const url = new URL('/files', this.envdApiUrl)
diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts
index 2e75cdbec8..1c35795456 100644
--- a/packages/js-sdk/src/sandbox/sandboxApi.ts
+++ b/packages/js-sdk/src/sandbox/sandboxApi.ts
@@ -21,6 +21,17 @@ export interface SandboxListOpts extends SandboxApiOpts {
query?: { metadata?: Record }
}
+export interface SandboxMetricsOpts extends SandboxApiOpts {
+ /**
+ * Start time for the metrics, defaults to the start of the sandbox
+ */
+ start?: string | Date
+ /**
+ * End time for the metrics, defaults to the current time
+ */
+ end?: string | Date
+}
+
/**
* Information about a sandbox.
*/
@@ -80,33 +91,33 @@ export interface ListedSandbox {
/**
* Template ID alias.
*/
- alias?: string;
+ alias?: string
/**
* Template ID.
*/
- templateId: string;
+ templateId: string
/**
* Client ID.
* @deprecated
*/
- clientId: string;
+ clientId: string
/**
* Sandbox state.
*/
- state: 'running' | 'paused';
+ state: 'running' | 'paused'
/**
* Sandbox CPU count.
*/
- cpuCount: number;
+ cpuCount: number
/**
* Sandbox Memory size in MB.
*/
- memoryMB: number;
+ memoryMB: number
/**
* Saved sandbox metadata.
@@ -116,14 +127,53 @@ export interface ListedSandbox {
/**
* Sandbox expected end time.
*/
- endAt: Date;
+ endAt: Date
/**
* Sandbox start time.
*/
- startedAt: Date;
+ startedAt: Date
}
+/**
+ * Sandbox resource usage metrics.
+ */
+export interface SandboxMetrics {
+ /**
+ * Timestamp of the metrics.
+ */
+ timestamp: Date
+
+ /**
+ * CPU usage in percentage.
+ */
+ cpuUsedPct: number
+
+ /**
+ * Number of CPU cores.
+ */
+ cpuCount: number
+
+ /**
+ * Memory usage in bytes.
+ */
+ memUsed: number
+
+ /**
+ * Total memory available in bytes.
+ */
+ memTotal: number
+
+ /**
+ * Used disk space in bytes.
+ */
+ diskUsed: number
+
+ /**
+ * Total disk space available in bytes.
+ */
+ diskTotal: number
+}
export class SandboxApi {
protected constructor() {}
@@ -202,7 +252,7 @@ export class SandboxApi {
return (
res.data?.map((sandbox: components['schemas']['ListedSandbox']) => ({
- sandboxId: sandbox.sandboxID,
+ sandboxId: sandbox.sandboxID,
templateId: sandbox.templateID,
clientId: sandbox.clientID,
state: sandbox.state,
@@ -262,6 +312,50 @@ export class SandboxApi {
}
}
+ /**
+ * Get the metrics of the sandbox.
+ *
+ * @param sandboxId sandbox ID.
+ * @param opts sandbox metrics options.
+ *
+ * @returns List of sandbox metrics containing CPU, memory and disk usage information.
+ */
+ static async getMetrics(
+ sandboxId: string,
+ opts?: SandboxMetricsOpts
+ ): Promise {
+ const config = new ConnectionConfig(opts)
+ const client = new ApiClient(config)
+
+ const res = await client.api.GET('/sandboxes/{sandboxID}/metrics', {
+ params: {
+ path: {
+ sandboxID: sandboxId,
+ start: opts?.start,
+ end: opts?.end,
+ },
+ },
+ signal: config.getSignal(opts?.requestTimeoutMs),
+ })
+
+ const err = handleApiError(res)
+ if (err) {
+ throw err
+ }
+
+ return (
+ res.data?.map((metric: components['schemas']['SandboxMetric']) => ({
+ timestamp: new Date(metric.timestamp),
+ cpuUsedPct: metric.cpuUsedPct,
+ cpuCount: metric.cpuCount,
+ memUsed: metric.memUsed,
+ memTotal: metric.memTotal,
+ diskUsed: metric.diskUsed,
+ diskTotal: metric.diskTotal,
+ })) ?? []
+ )
+ }
+
/**
* Set the timeout of the specified sandbox.
* After the timeout expires the sandbox will be automatically killed.
@@ -346,7 +440,7 @@ export class SandboxApi {
sandboxId: res.data!.sandboxID,
sandboxDomain: res.data!.domain || undefined,
envdVersion: res.data!.envdVersion,
- envdAccessToken: res.data!.envdAccessToken
+ envdAccessToken: res.data!.envdAccessToken,
}
}
diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts
new file mode 100644
index 0000000000..b675432436
--- /dev/null
+++ b/packages/js-sdk/tests/sandbox/metrics.test.ts
@@ -0,0 +1,24 @@
+import { expect } from 'vitest'
+
+import { SandboxMetrics } from '../../src'
+import {sandboxTest, isDebug, wait} from '../setup.js'
+
+sandboxTest.skipIf(isDebug)('sbx metrics', async ({ sandbox }) => {
+ let metrics: SandboxMetrics[]
+ for (let i = 0; i < 10; i++) {
+ metrics = await sandbox.getMetrics()
+ if (metrics.length > 0) {
+ break
+ }
+ await wait(1_000)
+ }
+
+ expect(metrics.length).toBeGreaterThan(0)
+ const metric = metrics[0]
+ expect(metric.diskTotal).toBeDefined()
+ expect(metric.diskUsed).toBeDefined()
+ expect(metric.memTotal).toBeDefined()
+ expect(metric.memUsed).toBeDefined()
+ expect(metric.cpuUsedPct).toBeDefined()
+ expect(metric.cpuCount).toBeDefined()
+})
diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py
index c0c99d8754..f95a5429d4 100644
--- a/packages/python-sdk/e2b/__init__.py
+++ b/packages/python-sdk/e2b/__init__.py
@@ -42,7 +42,7 @@
NotEnoughSpaceException,
TemplateException,
)
-from .sandbox.sandbox_api import SandboxInfo
+from .sandbox.sandbox_api import SandboxInfo, SandboxMetrics
from .sandbox.commands.main import ProcessInfo
from .sandbox.commands.command_handle import (
CommandResult,
@@ -84,6 +84,7 @@
"TemplateException",
# Sandbox API
"SandboxInfo",
+ "SandboxMetrics",
"ProcessInfo",
# Command handle
"CommandResult",
diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py
index 5dc96d9c3f..640f8f106f 100644
--- a/packages/python-sdk/e2b/api/client/models/__init__.py
+++ b/packages/python-sdk/e2b/api/client/models/__init__.py
@@ -1,10 +1,12 @@
"""Contains all the data models used in inputs/outputs"""
+from .build_log_entry import BuildLogEntry
from .created_access_token import CreatedAccessToken
from .created_team_api_key import CreatedTeamAPIKey
from .error import Error
from .identifier_masking_details import IdentifierMaskingDetails
from .listed_sandbox import ListedSandbox
+from .log_level import LogLevel
from .new_access_token import NewAccessToken
from .new_sandbox import NewSandbox
from .new_team_api_key import NewTeamAPIKey
@@ -29,17 +31,23 @@
from .team_user import TeamUser
from .template import Template
from .template_build import TemplateBuild
+from .template_build_file_upload import TemplateBuildFileUpload
from .template_build_request import TemplateBuildRequest
+from .template_build_request_v2 import TemplateBuildRequestV2
+from .template_build_start_v2 import TemplateBuildStartV2
from .template_build_status import TemplateBuildStatus
+from .template_step import TemplateStep
from .template_update_request import TemplateUpdateRequest
from .update_team_api_key import UpdateTeamAPIKey
__all__ = (
+ "BuildLogEntry",
"CreatedAccessToken",
"CreatedTeamAPIKey",
"Error",
"IdentifierMaskingDetails",
"ListedSandbox",
+ "LogLevel",
"NewAccessToken",
"NewSandbox",
"NewTeamAPIKey",
@@ -62,8 +70,12 @@
"TeamUser",
"Template",
"TemplateBuild",
+ "TemplateBuildFileUpload",
"TemplateBuildRequest",
+ "TemplateBuildRequestV2",
+ "TemplateBuildStartV2",
"TemplateBuildStatus",
+ "TemplateStep",
"TemplateUpdateRequest",
"UpdateTeamAPIKey",
)
diff --git a/packages/python-sdk/e2b/api/client/models/build_log_entry.py b/packages/python-sdk/e2b/api/client/models/build_log_entry.py
new file mode 100644
index 0000000000..7d352ea12d
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/build_log_entry.py
@@ -0,0 +1,79 @@
+import datetime
+from collections.abc import Mapping
+from typing import Any, TypeVar
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..models.log_level import LogLevel
+
+T = TypeVar("T", bound="BuildLogEntry")
+
+
+@_attrs_define
+class BuildLogEntry:
+ """
+ Attributes:
+ level (LogLevel): State of the sandbox
+ message (str): Log message content
+ timestamp (datetime.datetime): Timestamp of the log entry
+ """
+
+ level: LogLevel
+ message: str
+ timestamp: datetime.datetime
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ level = self.level.value
+
+ message = self.message
+
+ timestamp = self.timestamp.isoformat()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "level": level,
+ "message": message,
+ "timestamp": timestamp,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ d = dict(src_dict)
+ level = LogLevel(d.pop("level"))
+
+ message = d.pop("message")
+
+ timestamp = isoparse(d.pop("timestamp"))
+
+ build_log_entry = cls(
+ level=level,
+ message=message,
+ timestamp=timestamp,
+ )
+
+ build_log_entry.additional_properties = d
+ return build_log_entry
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/python-sdk/e2b/api/client/models/log_level.py b/packages/python-sdk/e2b/api/client/models/log_level.py
new file mode 100644
index 0000000000..bb5bcd9051
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/log_level.py
@@ -0,0 +1,11 @@
+from enum import Enum
+
+
+class LogLevel(str, Enum):
+ DEBUG = "debug"
+ ERROR = "error"
+ INFO = "info"
+ WARN = "warn"
+
+ def __str__(self) -> str:
+ return str(self.value)
diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py
index 3ac132ae24..ed0f637f86 100644
--- a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py
+++ b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py
@@ -16,6 +16,8 @@ class SandboxMetric:
Attributes:
cpu_count (int): Number of CPU cores
cpu_used_pct (float): CPU usage percentage
+ disk_total (int): Total disk space in bytes
+ disk_used (int): Disk used in bytes
mem_total (int): Total memory in bytes
mem_used (int): Memory used in bytes
timestamp (datetime.datetime): Timestamp of the metric entry
@@ -23,6 +25,8 @@ class SandboxMetric:
cpu_count: int
cpu_used_pct: float
+ disk_total: int
+ disk_used: int
mem_total: int
mem_used: int
timestamp: datetime.datetime
@@ -33,6 +37,10 @@ def to_dict(self) -> dict[str, Any]:
cpu_used_pct = self.cpu_used_pct
+ disk_total = self.disk_total
+
+ disk_used = self.disk_used
+
mem_total = self.mem_total
mem_used = self.mem_used
@@ -45,6 +53,8 @@ def to_dict(self) -> dict[str, Any]:
{
"cpuCount": cpu_count,
"cpuUsedPct": cpu_used_pct,
+ "diskTotal": disk_total,
+ "diskUsed": disk_used,
"memTotal": mem_total,
"memUsed": mem_used,
"timestamp": timestamp,
@@ -60,6 +70,10 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
cpu_used_pct = d.pop("cpuUsedPct")
+ disk_total = d.pop("diskTotal")
+
+ disk_used = d.pop("diskUsed")
+
mem_total = d.pop("memTotal")
mem_used = d.pop("memUsed")
@@ -69,6 +83,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
sandbox_metric = cls(
cpu_count=cpu_count,
cpu_used_pct=cpu_used_pct,
+ disk_total=disk_total,
+ disk_used=disk_used,
mem_total=mem_total,
mem_used=mem_used,
timestamp=timestamp,
diff --git a/packages/python-sdk/e2b/api/client/models/template_build.py b/packages/python-sdk/e2b/api/client/models/template_build.py
index 1d5691957d..a6247eccf4 100644
--- a/packages/python-sdk/e2b/api/client/models/template_build.py
+++ b/packages/python-sdk/e2b/api/client/models/template_build.py
@@ -1,5 +1,5 @@
from collections.abc import Mapping
-from typing import Any, TypeVar, Union, cast
+from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from attrs import define as _attrs_define
from attrs import field as _attrs_field
@@ -7,6 +7,10 @@
from ..models.template_build_status import TemplateBuildStatus
from ..types import UNSET, Unset
+if TYPE_CHECKING:
+ from ..models.build_log_entry import BuildLogEntry
+
+
T = TypeVar("T", bound="TemplateBuild")
@@ -15,6 +19,7 @@ class TemplateBuild:
"""
Attributes:
build_id (str): Identifier of the build
+ log_entries (list['BuildLogEntry']): Build logs structured
logs (list[str]): Build logs
status (TemplateBuildStatus): Status of the template
template_id (str): Identifier of the template
@@ -22,6 +27,7 @@ class TemplateBuild:
"""
build_id: str
+ log_entries: list["BuildLogEntry"]
logs: list[str]
status: TemplateBuildStatus
template_id: str
@@ -31,6 +37,11 @@ class TemplateBuild:
def to_dict(self) -> dict[str, Any]:
build_id = self.build_id
+ log_entries = []
+ for log_entries_item_data in self.log_entries:
+ log_entries_item = log_entries_item_data.to_dict()
+ log_entries.append(log_entries_item)
+
logs = self.logs
status = self.status.value
@@ -44,6 +55,7 @@ def to_dict(self) -> dict[str, Any]:
field_dict.update(
{
"buildID": build_id,
+ "logEntries": log_entries,
"logs": logs,
"status": status,
"templateID": template_id,
@@ -56,9 +68,18 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ from ..models.build_log_entry import BuildLogEntry
+
d = dict(src_dict)
build_id = d.pop("buildID")
+ log_entries = []
+ _log_entries = d.pop("logEntries")
+ for log_entries_item_data in _log_entries:
+ log_entries_item = BuildLogEntry.from_dict(log_entries_item_data)
+
+ log_entries.append(log_entries_item)
+
logs = cast(list[str], d.pop("logs"))
status = TemplateBuildStatus(d.pop("status"))
@@ -69,6 +90,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
template_build = cls(
build_id=build_id,
+ log_entries=log_entries,
logs=logs,
status=status,
template_id=template_id,
diff --git a/packages/python-sdk/e2b/api/client/models/template_build_file_upload.py b/packages/python-sdk/e2b/api/client/models/template_build_file_upload.py
new file mode 100644
index 0000000000..a7d4e44a04
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/template_build_file_upload.py
@@ -0,0 +1,70 @@
+from collections.abc import Mapping
+from typing import Any, TypeVar, Union
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+T = TypeVar("T", bound="TemplateBuildFileUpload")
+
+
+@_attrs_define
+class TemplateBuildFileUpload:
+ """
+ Attributes:
+ present (bool): Whether the file is already present in the cache
+ url (Union[Unset, str]): Url where the file should be uploaded to
+ """
+
+ present: bool
+ url: Union[Unset, str] = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ present = self.present
+
+ url = self.url
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "present": present,
+ }
+ )
+ if url is not UNSET:
+ field_dict["url"] = url
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ d = dict(src_dict)
+ present = d.pop("present")
+
+ url = d.pop("url", UNSET)
+
+ template_build_file_upload = cls(
+ present=present,
+ url=url,
+ )
+
+ template_build_file_upload.additional_properties = d
+ return template_build_file_upload
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/python-sdk/e2b/api/client/models/template_build_request_v2.py b/packages/python-sdk/e2b/api/client/models/template_build_request_v2.py
new file mode 100644
index 0000000000..714109487b
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/template_build_request_v2.py
@@ -0,0 +1,88 @@
+from collections.abc import Mapping
+from typing import Any, TypeVar, Union
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+T = TypeVar("T", bound="TemplateBuildRequestV2")
+
+
+@_attrs_define
+class TemplateBuildRequestV2:
+ """
+ Attributes:
+ alias (str): Alias of the template
+ cpu_count (Union[Unset, int]): CPU cores for the sandbox
+ memory_mb (Union[Unset, int]): Memory for the sandbox in MB
+ team_id (Union[Unset, str]): Identifier of the team
+ """
+
+ alias: str
+ cpu_count: Union[Unset, int] = UNSET
+ memory_mb: Union[Unset, int] = UNSET
+ team_id: Union[Unset, str] = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ alias = self.alias
+
+ cpu_count = self.cpu_count
+
+ memory_mb = self.memory_mb
+
+ team_id = self.team_id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "alias": alias,
+ }
+ )
+ if cpu_count is not UNSET:
+ field_dict["cpuCount"] = cpu_count
+ if memory_mb is not UNSET:
+ field_dict["memoryMB"] = memory_mb
+ if team_id is not UNSET:
+ field_dict["teamID"] = team_id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ d = dict(src_dict)
+ alias = d.pop("alias")
+
+ cpu_count = d.pop("cpuCount", UNSET)
+
+ memory_mb = d.pop("memoryMB", UNSET)
+
+ team_id = d.pop("teamID", UNSET)
+
+ template_build_request_v2 = cls(
+ alias=alias,
+ cpu_count=cpu_count,
+ memory_mb=memory_mb,
+ team_id=team_id,
+ )
+
+ template_build_request_v2.additional_properties = d
+ return template_build_request_v2
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/python-sdk/e2b/api/client/models/template_build_start_v2.py b/packages/python-sdk/e2b/api/client/models/template_build_start_v2.py
new file mode 100644
index 0000000000..6ab77c7d7c
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/template_build_start_v2.py
@@ -0,0 +1,114 @@
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, TypeVar, Union
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.template_step import TemplateStep
+
+
+T = TypeVar("T", bound="TemplateBuildStartV2")
+
+
+@_attrs_define
+class TemplateBuildStartV2:
+ """
+ Attributes:
+ from_image (str): Image to use as a base for the template build
+ force (Union[Unset, bool]): Whether the whole build should be forced to run regardless of the cache Default:
+ False.
+ ready_cmd (Union[Unset, str]): Ready check command to execute in the template after the build
+ start_cmd (Union[Unset, str]): Start command to execute in the template after the build
+ steps (Union[Unset, list['TemplateStep']]): List of steps to execute in the template build
+ """
+
+ from_image: str
+ force: Union[Unset, bool] = False
+ ready_cmd: Union[Unset, str] = UNSET
+ start_cmd: Union[Unset, str] = UNSET
+ steps: Union[Unset, list["TemplateStep"]] = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ from_image = self.from_image
+
+ force = self.force
+
+ ready_cmd = self.ready_cmd
+
+ start_cmd = self.start_cmd
+
+ steps: Union[Unset, list[dict[str, Any]]] = UNSET
+ if not isinstance(self.steps, Unset):
+ steps = []
+ for steps_item_data in self.steps:
+ steps_item = steps_item_data.to_dict()
+ steps.append(steps_item)
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "fromImage": from_image,
+ }
+ )
+ if force is not UNSET:
+ field_dict["force"] = force
+ if ready_cmd is not UNSET:
+ field_dict["readyCmd"] = ready_cmd
+ if start_cmd is not UNSET:
+ field_dict["startCmd"] = start_cmd
+ if steps is not UNSET:
+ field_dict["steps"] = steps
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ from ..models.template_step import TemplateStep
+
+ d = dict(src_dict)
+ from_image = d.pop("fromImage")
+
+ force = d.pop("force", UNSET)
+
+ ready_cmd = d.pop("readyCmd", UNSET)
+
+ start_cmd = d.pop("startCmd", UNSET)
+
+ steps = []
+ _steps = d.pop("steps", UNSET)
+ for steps_item_data in _steps or []:
+ steps_item = TemplateStep.from_dict(steps_item_data)
+
+ steps.append(steps_item)
+
+ template_build_start_v2 = cls(
+ from_image=from_image,
+ force=force,
+ ready_cmd=ready_cmd,
+ start_cmd=start_cmd,
+ steps=steps,
+ )
+
+ template_build_start_v2.additional_properties = d
+ return template_build_start_v2
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/python-sdk/e2b/api/client/models/template_step.py b/packages/python-sdk/e2b/api/client/models/template_step.py
new file mode 100644
index 0000000000..45daaecd06
--- /dev/null
+++ b/packages/python-sdk/e2b/api/client/models/template_step.py
@@ -0,0 +1,91 @@
+from collections.abc import Mapping
+from typing import Any, TypeVar, Union, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+T = TypeVar("T", bound="TemplateStep")
+
+
+@_attrs_define
+class TemplateStep:
+ """Step in the template build process
+
+ Attributes:
+ type_ (str): Type of the step
+ args (Union[Unset, list[str]]): Arguments for the step
+ files_hash (Union[Unset, str]): Hash of the files used in the step
+ force (Union[Unset, bool]): Whether the step should be forced to run regardless of the cache Default: False.
+ """
+
+ type_: str
+ args: Union[Unset, list[str]] = UNSET
+ files_hash: Union[Unset, str] = UNSET
+ force: Union[Unset, bool] = False
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ args: Union[Unset, list[str]] = UNSET
+ if not isinstance(self.args, Unset):
+ args = self.args
+
+ files_hash = self.files_hash
+
+ force = self.force
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ }
+ )
+ if args is not UNSET:
+ field_dict["args"] = args
+ if files_hash is not UNSET:
+ field_dict["filesHash"] = files_hash
+ if force is not UNSET:
+ field_dict["force"] = force
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ d = dict(src_dict)
+ type_ = d.pop("type")
+
+ args = cast(list[str], d.pop("args", UNSET))
+
+ files_hash = d.pop("filesHash", UNSET)
+
+ force = d.pop("force", UNSET)
+
+ template_step = cls(
+ type_=type_,
+ args=args,
+ files_hash=files_hash,
+ force=force,
+ )
+
+ template_step.additional_properties = d
+ return template_step
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py
index f928a40393..49ec991f31 100644
--- a/packages/python-sdk/e2b/sandbox/sandbox_api.py
+++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py
@@ -63,6 +63,26 @@ class SandboxQuery:
"""Filter sandboxes by metadata."""
+@dataclass
+class SandboxMetrics:
+ """Sandbox metrics."""
+
+ cpu_count: int
+ """Number of CPUs."""
+ cpu_used_pct: float
+ """CPU usage percentage."""
+ disk_total: int
+ """Total disk space in bytes."""
+ disk_used: int
+ """Disk used in bytes."""
+ mem_total: int
+ """Total memory in bytes."""
+ mem_used: int
+ """Memory used in bytes."""
+ timestamp: datetime
+ """Timestamp of the metric entry."""
+
+
class SandboxApiBase(ABC):
_limits = Limits(
max_keepalive_connections=10,
diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py
index 27f60b915d..69a5916587 100644
--- a/packages/python-sdk/e2b/sandbox_async/main.py
+++ b/packages/python-sdk/e2b/sandbox_async/main.py
@@ -1,14 +1,18 @@
+import datetime
import logging
import httpx
-from typing import Dict, Optional, TypedDict, overload
+from typing import Dict, Optional, TypedDict, overload, List
+
+from packaging.version import Version
from typing_extensions import Unpack
from e2b.api.client.types import Unset
from e2b.connection_config import ConnectionConfig, ProxyTypes
from e2b.envd.api import ENVD_API_HEALTH_ROUTE, ahandle_envd_api_exception
-from e2b.exceptions import format_request_timeout_error
+from e2b.exceptions import format_request_timeout_error, SandboxException
from e2b.sandbox.main import SandboxSetup
+from e2b.sandbox.sandbox_api import SandboxMetrics
from e2b.sandbox.utils import class_method_variant
from e2b.sandbox_async.filesystem.filesystem import Filesystem
from e2b.sandbox_async.commands.command import Commands
@@ -508,3 +512,85 @@ async def get_info( # type: ignore
sandbox_id=self.sandbox_id,
**config_dict,
)
+
+ @overload
+ async def get_metrics( # type: ignore
+ self,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the current sandbox.
+
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ ...
+
+ @overload
+ @staticmethod
+ async def get_metrics(
+ sandbox_id: str,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ api_key: Optional[str] = None,
+ domain: Optional[str] = None,
+ debug: Optional[bool] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the sandbox specified by sandbox ID.
+
+ :param sandbox_id: Sandbox ID
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param api_key: E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ ...
+
+ @class_method_variant("_cls_get_metrics")
+ async def get_metrics( # type: ignore
+ self,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the current sandbox.
+
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ if self._envd_version:
+ if Version(self._envd_version) < Version("0.1.5"):
+ raise SandboxException(
+ "Metrics are not supported in this version of the sandbox, please rebuild your template."
+ )
+
+ if Version(self._envd_version) < Version("0.2.4"):
+ logger.warning(
+ "Disk metrics are not supported in this version of the sandbox, please rebuild the template to get disk metrics."
+ )
+
+ config_dict = self.connection_config.__dict__
+ config_dict.pop("access_token", None)
+ config_dict.pop("api_url", None)
+ if request_timeout:
+ config_dict["request_timeout"] = request_timeout
+
+ return await self._cls_get_metrics(
+ sandbox_id=self.sandbox_id,
+ start=start,
+ end=end,
+ **config_dict,
+ )
diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
index 6ef22bb763..200fab1312 100644
--- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
+++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
@@ -1,3 +1,4 @@
+import datetime
import urllib.parse
from typing import Optional, Dict, List
@@ -9,16 +10,22 @@
SandboxApiBase,
SandboxQuery,
ListedSandbox,
+ SandboxMetrics,
)
-from e2b.exceptions import TemplateException
+from e2b.exceptions import TemplateException, SandboxException
from e2b.api import AsyncApiClient, SandboxCreateResponse
-from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody
+from e2b.api.client.models import (
+ NewSandbox,
+ PostSandboxesSandboxIDTimeoutBody,
+ Error,
+)
from e2b.api.client.api.sandboxes import (
get_sandboxes_sandbox_id,
post_sandboxes_sandbox_id_timeout,
get_sandboxes,
delete_sandboxes_sandbox_id,
post_sandboxes,
+ get_sandboxes_sandbox_id_metrics,
)
from e2b.connection_config import ConnectionConfig, ProxyTypes
from e2b.api import handle_api_exception
@@ -298,3 +305,64 @@ async def _create_sandbox(
envd_version=res.parsed.envd_version,
envd_access_token=res.parsed.envd_access_token,
)
+
+ @classmethod
+ async def _cls_get_metrics(
+ cls,
+ sandbox_id: str,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ api_key: Optional[str] = None,
+ domain: Optional[str] = None,
+ debug: Optional[bool] = None,
+ request_timeout: Optional[float] = None,
+ headers: Optional[Dict[str, str]] = None,
+ proxy: Optional[ProxyTypes] = None,
+ ) -> List[SandboxMetrics]:
+ config = ConnectionConfig(
+ api_key=api_key,
+ domain=domain,
+ debug=debug,
+ headers=headers,
+ request_timeout=request_timeout,
+ proxy=proxy,
+ )
+
+ if config.debug:
+ # Skip getting the metrics in debug mode
+ return []
+
+ async with AsyncApiClient(
+ config,
+ limits=cls._limits,
+ ) as api_client:
+ res = await get_sandboxes_sandbox_id_metrics.asyncio_detailed(
+ sandbox_id,
+ start=int(start.timestamp() * 1000) if start else None,
+ end=int(end.timestamp() * 1000) if end else None,
+ client=api_client,
+ )
+
+ if res.status_code >= 300:
+ raise handle_api_exception(res)
+
+ if res.parsed is None:
+ return []
+
+ # Check if res.parse is Error
+ if isinstance(res.parsed, Error):
+ raise SandboxException(f"{res.parsed.message}: Request failed")
+
+ # Convert to typed SandboxMetrics objects
+ return [
+ SandboxMetrics(
+ cpu_count=metric.cpu_count,
+ cpu_used_pct=metric.cpu_used_pct,
+ disk_total=metric.disk_total,
+ disk_used=metric.disk_used,
+ mem_total=metric.mem_total,
+ mem_used=metric.mem_used,
+ timestamp=metric.timestamp,
+ )
+ for metric in res.parsed
+ ]
diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py
index b09cc8190c..bbe99b01bb 100644
--- a/packages/python-sdk/e2b/sandbox_sync/main.py
+++ b/packages/python-sdk/e2b/sandbox_sync/main.py
@@ -1,13 +1,17 @@
+import datetime
import logging
import httpx
-from typing import Dict, Optional, overload
+from typing import Dict, Optional, overload, List
+
+from packaging.version import Version
from e2b.api.client.types import Unset
from e2b.connection_config import ConnectionConfig, ProxyTypes
from e2b.envd.api import ENVD_API_HEALTH_ROUTE, handle_envd_api_exception
from e2b.exceptions import SandboxException, format_request_timeout_error
from e2b.sandbox.main import SandboxSetup
+from e2b.sandbox.sandbox_api import SandboxMetrics
from e2b.sandbox.utils import class_method_variant
from e2b.sandbox_sync.filesystem.filesystem import Filesystem
from e2b.sandbox_sync.commands.command import Commands
@@ -484,3 +488,85 @@ def get_info( # type: ignore
sandbox_id=self.sandbox_id,
**config_dict,
)
+
+ @overload
+ def get_metrics( # type: ignore
+ self,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the current sandbox.
+
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ ...
+
+ @overload
+ @staticmethod
+ def get_metrics(
+ sandbox_id: str,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ api_key: Optional[str] = None,
+ domain: Optional[str] = None,
+ debug: Optional[bool] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the sandbox specified by sandbox ID.
+
+ :param sandbox_id: Sandbox ID
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param api_key: E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ ...
+
+ @class_method_variant("_cls_get_metrics")
+ def get_metrics( # type: ignore
+ self,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ request_timeout: Optional[float] = None,
+ ) -> List[SandboxMetrics]:
+ """
+ Get the metrics of the current sandbox.
+
+ :param start: Start time for the metrics, defaults to the start of the sandbox
+ :param end: End time for the metrics, defaults to current time
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: List of sandbox metrics containing CPU, memory and disk usage information
+ """
+ if self._envd_version:
+ if Version(self._envd_version) < Version("0.1.5"):
+ raise SandboxException(
+ "Metrics are not supported in this version of the sandbox, please rebuild your template."
+ )
+
+ if Version(self._envd_version) < Version("0.2.4"):
+ logger.warning(
+ "Disk metrics are not supported in this version of the sandbox, please rebuild the template to get disk metrics."
+ )
+
+ config_dict = self.connection_config.__dict__
+ config_dict.pop("access_token", None)
+ config_dict.pop("api_url", None)
+ if request_timeout:
+ config_dict["request_timeout"] = request_timeout
+
+ return self._cls_get_metrics(
+ sandbox_id=self.sandbox_id,
+ start=start,
+ end=end,
+ **config_dict,
+ )
diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
index e817276dcf..109d3235c7 100644
--- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
+++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
@@ -1,3 +1,4 @@
+import datetime
import urllib.parse
from typing import Optional, Dict, List
@@ -8,16 +9,22 @@
SandboxApiBase,
SandboxQuery,
ListedSandbox,
+ SandboxMetrics,
)
-from e2b.exceptions import TemplateException
+from e2b.exceptions import TemplateException, SandboxException
from e2b.api import ApiClient, SandboxCreateResponse
-from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody
+from e2b.api.client.models import (
+ NewSandbox,
+ PostSandboxesSandboxIDTimeoutBody,
+ Error,
+)
from e2b.api.client.api.sandboxes import (
get_sandboxes_sandbox_id,
post_sandboxes_sandbox_id_timeout,
get_sandboxes,
delete_sandboxes_sandbox_id,
post_sandboxes,
+ get_sandboxes_sandbox_id_metrics,
)
from e2b.connection_config import ConnectionConfig, ProxyTypes
from e2b.api import handle_api_exception
@@ -290,3 +297,63 @@ def _create_sandbox(
envd_version=res.parsed.envd_version,
envd_access_token=res.parsed.envd_access_token,
)
+
+ @classmethod
+ def _cls_get_metrics(
+ cls,
+ sandbox_id: str,
+ start: Optional[datetime.datetime] = None,
+ end: Optional[datetime.datetime] = None,
+ api_key: Optional[str] = None,
+ domain: Optional[str] = None,
+ debug: Optional[bool] = None,
+ request_timeout: Optional[float] = None,
+ headers: Optional[Dict[str, str]] = None,
+ proxy: Optional[ProxyTypes] = None,
+ ) -> List[SandboxMetrics]:
+ config = ConnectionConfig(
+ api_key=api_key,
+ domain=domain,
+ debug=debug,
+ request_timeout=request_timeout,
+ headers=headers,
+ proxy=proxy,
+ )
+
+ if config.debug:
+ # Skip getting the metrics in debug mode
+ return []
+
+ with ApiClient(
+ config,
+ limits=cls._limits,
+ ) as api_client:
+ res = get_sandboxes_sandbox_id_metrics.sync_detailed(
+ sandbox_id,
+ start=int(start.timestamp() * 1000) if start else None,
+ end=int(end.timestamp() * 1000) if end else None,
+ client=api_client,
+ )
+
+ if res.status_code >= 300:
+ raise handle_api_exception(res)
+
+ if res.parsed is None:
+ return []
+
+ if isinstance(res.parsed, Error):
+ raise SandboxException(f"{res.parsed.message}: Request failed")
+
+ # Convert to typed SandboxMetrics objects
+ return [
+ SandboxMetrics(
+ cpu_count=metric.cpu_count,
+ cpu_used_pct=metric.cpu_used_pct,
+ disk_total=metric.disk_total,
+ disk_used=metric.disk_used,
+ mem_total=metric.mem_total,
+ mem_used=metric.mem_used,
+ timestamp=metric.timestamp,
+ )
+ for metric in res.parsed
+ ]
diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py
new file mode 100644
index 0000000000..3d1e30a11d
--- /dev/null
+++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py
@@ -0,0 +1,26 @@
+import asyncio
+
+import pytest
+
+from e2b import AsyncSandbox
+
+
+@pytest.mark.skip_debug()
+async def test_sbx_metrics(async_sandbox: AsyncSandbox):
+ # Wait for the sandbox to have some metrics
+ metrics = []
+ for _ in range(10):
+ metrics = await async_sandbox.get_metrics()
+ if len(metrics) > 0:
+ break
+ await asyncio.sleep(1)
+
+ assert len(metrics) > 0
+
+ metric = metrics[0]
+ assert metric.cpu_count is not None
+ assert metric.cpu_used_pct is not None
+ assert metric.mem_used is not None
+ assert metric.mem_total is not None
+ assert metric.disk_used is not None
+ assert metric.disk_total is not None
diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py
new file mode 100644
index 0000000000..03c5eb92ca
--- /dev/null
+++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py
@@ -0,0 +1,26 @@
+import time
+
+import pytest
+
+from e2b import Sandbox
+
+
+@pytest.mark.skip_debug()
+def test_sbx_metrics(sandbox: Sandbox):
+ # Wait for the sandbox to have some metrics
+ metrics = []
+ for _ in range(10):
+ metrics = sandbox.get_metrics()
+ if len(metrics) > 0:
+ break
+ time.sleep(1)
+
+ assert len(metrics) > 0
+
+ metric = metrics[0]
+ assert metric.cpu_count is not None
+ assert metric.cpu_used_pct is not None
+ assert metric.mem_used is not None
+ assert metric.mem_total is not None
+ assert metric.disk_used is not None
+ assert metric.disk_total is not None