Skip to content

Commit 32f440d

Browse files
committed
Sandbox metrics in CLI
1 parent 32a21a4 commit 32f440d

4 files changed

Lines changed: 221 additions & 60 deletions

File tree

packages/cli/src/commands/sandbox/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { listCommand } from './list'
55
import { killCommand } from './kill'
66
import { spawnCommand } from './spawn'
77
import { logsCommand } from './logs'
8+
import { metricsCommand } from './metrics'
89

910
export const sandboxCommand = new commander.Command('sandbox')
1011
.description('work with sandboxes')
@@ -14,3 +15,4 @@ export const sandboxCommand = new commander.Command('sandbox')
1415
.addCommand(killCommand)
1516
.addCommand(spawnCommand)
1617
.addCommand(logsCommand)
18+
.addCommand(metricsCommand)

packages/cli/src/commands/sandbox/logs.ts

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,7 @@ import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
88
import { listSandboxes } from './list'
99
import { wait } from 'src/utils/wait'
1010
import { handleE2BRequestError } from '../../utils/errors'
11-
12-
const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
13-
14-
function getShortID(sandboxID: string) {
15-
return sandboxID.split('-')[0]
16-
}
17-
18-
function waitForSandboxEnd(sandboxID: string) {
19-
let isRunning = true
20-
21-
async function monitor() {
22-
const startTime = new Date().getTime()
23-
24-
// eslint-disable-next-line no-constant-condition
25-
while (true) {
26-
const currentTime = new Date().getTime()
27-
const elapsedTime = currentTime - startTime // Time elapsed in milliseconds
28-
29-
// Check if 24 hours (in milliseconds) have passed
30-
if (elapsedTime >= maxRuntime) {
31-
break
32-
}
33-
34-
const response = await listSandboxes()
35-
const sandbox = response.find(
36-
(s) => s.sandboxID === getShortID(sandboxID)
37-
)
38-
if (!sandbox) {
39-
isRunning = false
40-
break
41-
}
42-
await wait(5000)
43-
}
44-
}
45-
46-
monitor()
47-
48-
return () => isRunning
49-
}
11+
import { waitForSandboxEnd, formatEnum, Format, getShortID } from './utils'
5012

5113
enum LogLevel {
5214
DEBUG = 'DEBUG',
@@ -76,17 +38,6 @@ function isLevelIncluded(level: LogLevel, allowedLevel?: LogLevel) {
7638
}
7739
}
7840

79-
function formatEnum(e: { [key: string]: string }) {
80-
return Object.values(e)
81-
.map((level) => asBold(level))
82-
.join(', ')
83-
}
84-
85-
enum LogFormat {
86-
JSON = 'json',
87-
PRETTY = 'pretty',
88-
}
89-
9041
function cleanLogger(logger?: string) {
9142
if (!logger) {
9243
return ''
@@ -112,8 +63,8 @@ export const logsCommand = new commander.Command('logs')
11263
.option('-f, --follow', 'keep streaming logs until the sandbox is closed')
11364
.option(
11465
'--format <format>',
115-
`specify format for printing logs (${formatEnum(LogFormat)})`,
116-
LogFormat.PRETTY
66+
`specify format for printing logs (${formatEnum(Format)})`,
67+
Format.PRETTY
11768
)
11869
.option(
11970
'--loggers [loggers]',
@@ -126,7 +77,7 @@ export const logsCommand = new commander.Command('logs')
12677
opts?: {
12778
level: string
12879
follow: boolean
129-
format: LogFormat
80+
format: Format
13081
loggers?: string[]
13182
}
13283
) => {
@@ -136,8 +87,8 @@ export const logsCommand = new commander.Command('logs')
13687
throw new Error(`Invalid log level: ${level}`)
13788
}
13889

139-
const format = opts?.format.toLowerCase() as LogFormat | undefined
140-
if (format && !Object.values(LogFormat).includes(format)) {
90+
const format = opts?.format.toLowerCase() as Format | undefined
91+
if (format && !Object.values(Format).includes(format)) {
14192
throw new Error(`Invalid log format: ${format}`)
14293
}
14394

@@ -149,7 +100,7 @@ export const logsCommand = new commander.Command('logs')
149100
let isFirstRun = true
150101
let firstLogsPrinted = false
151102

152-
if (format === LogFormat.PRETTY) {
103+
if (format === Format.PRETTY) {
153104
console.log(`\nLogs for sandbox ${asBold(sandboxID)}:`)
154105
}
155106

@@ -178,7 +129,7 @@ export const logsCommand = new commander.Command('logs')
178129
const isRunning = await isRunningPromise
179130

180131
if (!isRunning && logs.length === 0 && isFirstRun) {
181-
if (format === LogFormat.PRETTY) {
132+
if (format === Format.PRETTY) {
182133
console.log(
183134
`\nStopped printing logs — sandbox ${withUnderline(
184135
'not found'
@@ -189,7 +140,7 @@ export const logsCommand = new commander.Command('logs')
189140
}
190141

191142
if (!isRunning) {
192-
if (format === LogFormat.PRETTY) {
143+
if (format === Format.PRETTY) {
193144
console.log(
194145
`\nStopped printing logs — sandbox is ${withUnderline(
195146
'closed'
@@ -219,7 +170,7 @@ function printLog(
219170
timestamp: string,
220171
line: string,
221172
allowedLevel: LogLevel | undefined,
222-
format: LogFormat | undefined,
173+
format: Format | undefined,
223174
allowedLoggers?: string[] | undefined
224175
) {
225176
const log = JSON.parse(line)
@@ -266,7 +217,7 @@ function printLog(
266217
delete log['envID']
267218
delete log['sandboxID']
268219

269-
if (format === LogFormat.JSON) {
220+
if (format === Format.JSON) {
270221
console.log(
271222
JSON.stringify({
272223
timestamp: new Date(timestamp).toISOString(),
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as chalk from 'chalk'
2+
import * as commander from 'commander'
3+
import * as e2b from 'e2b'
4+
5+
import { client, connectionConfig } from 'src/api'
6+
import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
7+
import { wait } from 'src/utils/wait'
8+
import { handleE2BRequestError } from '../../utils/errors'
9+
import { listSandboxes } from './list'
10+
import { formatEnum, getShortID, Format } from './utils'
11+
12+
export const metricsCommand = new commander.Command('metrics')
13+
.description('show metrics for sandbox')
14+
.argument(
15+
'<sandboxID>',
16+
`show metrics for sandbox specified by ${asBold('<sandboxID>')}`
17+
)
18+
.alias('mt')
19+
.option('-f, --follow', 'keep streaming metrics until the sandbox is closed')
20+
.option(
21+
'--format <format>',
22+
`specify format for printing logs (${formatEnum(Format)})`,
23+
Format.PRETTY
24+
)
25+
.action(
26+
async (
27+
sandboxID: string,
28+
opts?: {
29+
follow: boolean
30+
format: Format
31+
}
32+
) => {
33+
try {
34+
const format = opts?.format.toLowerCase() as Format | undefined
35+
if (format && !Object.values(Format).includes(format)) {
36+
throw new Error(`Invalid log format: ${format}`)
37+
}
38+
39+
let start : string | undefined
40+
let isFirstRun = true
41+
let firstMetricsPrinted = false
42+
43+
if (format === Format.PRETTY) {
44+
console.log(`\nMetrics for sandbox ${asBold(sandboxID)}:`)
45+
}
46+
47+
const isRunningPromise = listSandboxes()
48+
.then((r) => r.find((s) => s.sandboxID === getShortID(sandboxID)))
49+
.then((s) => !!s)
50+
51+
do {
52+
const metrics = await getSandboxMetrics({ sandboxID, start })
53+
54+
if (metrics.length !== 0 && !firstMetricsPrinted) {
55+
firstMetricsPrinted = true
56+
process.stdout.write('\n')
57+
}
58+
59+
for (const metric of metrics) {
60+
if (start && metric.timestamp <= start) {
61+
// Skip the metric if it has the same timestamp as the last one
62+
continue
63+
}
64+
start = metric.timestamp
65+
66+
printMetric(metric.timestamp, JSON.stringify(metric), format)
67+
}
68+
69+
const isRunning = await isRunningPromise
70+
71+
if (!isRunning && metrics.length === 0 && isFirstRun) {
72+
if (format === Format.PRETTY) {
73+
console.log(
74+
`\nStopped printing metrics — sandbox ${withUnderline(
75+
'not found'
76+
)}`
77+
)
78+
}
79+
break
80+
}
81+
82+
if (!isRunning) {
83+
if (format === Format.PRETTY) {
84+
console.log(
85+
`\nStopped printing metrics — sandbox is ${withUnderline(
86+
'closed'
87+
)}`
88+
)
89+
}
90+
break
91+
}
92+
93+
await wait(400)
94+
isFirstRun = false
95+
} while (opts?.follow)
96+
} catch (err: any) {
97+
console.error(err)
98+
process.exit(1)
99+
}
100+
}
101+
)
102+
103+
function printMetric(
104+
timestamp: string,
105+
line: string,
106+
format: Format | undefined
107+
) {
108+
const metric = JSON.parse(line)
109+
const level = chalk.default.green()
110+
111+
if (format === Format.JSON) {
112+
console.log(
113+
JSON.stringify({
114+
timestamp: new Date(timestamp).toISOString(),
115+
...metric,
116+
})
117+
)
118+
} else {
119+
const time = `[${new Date(timestamp).toISOString().replace(/\.\d{3}Z/, 'Z').replace(/T/, ' ')}]`
120+
delete metric['timestamp']
121+
const multipleCores = metric.cpuCount > 1
122+
metric.cpuCount += 0
123+
console.log(
124+
`${asTimestamp(time)} ${level} ` +
125+
asBold('CPU') + `: ${(metric.cpuUsedPct).toString().padStart(5)}% / ${metric.cpuCount.toString().padStart(2)} Core${multipleCores && 's'} | ` +
126+
asBold('Memory') + `: ${(metric.memUsed >>> 20).toString().padStart(5)} / ${(metric.memTotal >>> 20).toString().padEnd(5)} MiB | ` +
127+
asBold('Disk') + `: ${(metric.diskUsed >>> 20).toString().padStart(5)} / ${(metric.diskTotal >>> 20).toString().padEnd(5)} MiB`
128+
)
129+
}
130+
}
131+
132+
export async function getSandboxMetrics({
133+
sandboxID,
134+
start,
135+
}: {
136+
sandboxID: string
137+
start?: string
138+
}): Promise<e2b.components['schemas']['SandboxMetric'][]> {
139+
const signal = connectionConfig.getSignal()
140+
const res = await client.api.GET('/sandboxes/{sandboxID}/metrics', {
141+
signal,
142+
params: {
143+
path: {
144+
sandboxID,
145+
start,
146+
},
147+
},
148+
})
149+
150+
handleE2BRequestError(res, 'Error while getting sandbox metrics')
151+
152+
return res.data as unknown as e2b.components['schemas']['SandboxMetric'][]
153+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { listSandboxes } from './list'
2+
import { wait } from '../../utils/wait'
3+
import {asBold} from '../../utils/format'
4+
5+
6+
export function formatEnum(e: { [key: string]: string }) {
7+
return Object.values(e)
8+
.map((level) => asBold(level))
9+
.join(', ')
10+
}
11+
12+
export enum Format {
13+
JSON = 'json',
14+
PRETTY = 'pretty',
15+
}
16+
17+
18+
const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
19+
20+
export function waitForSandboxEnd(sandboxID: string) {
21+
let isRunning = true
22+
23+
async function monitor() {
24+
const startTime = new Date().getTime()
25+
26+
// eslint-disable-next-line no-constant-condition
27+
while (true) {
28+
const currentTime = new Date().getTime()
29+
const elapsedTime = currentTime - startTime // Time elapsed in milliseconds
30+
31+
// Check if 24 hours (in milliseconds) have passed
32+
if (elapsedTime >= maxRuntime) {
33+
break
34+
}
35+
36+
const response = await listSandboxes()
37+
const sandbox = response.find(
38+
(s) => s.sandboxID === getShortID(sandboxID)
39+
)
40+
if (!sandbox) {
41+
isRunning = false
42+
break
43+
}
44+
await wait(5000)
45+
}
46+
}
47+
48+
monitor()
49+
50+
return () => isRunning
51+
}
52+
53+
export function getShortID(sandboxID: string) {
54+
return sandboxID.split('-')[0]
55+
}

0 commit comments

Comments
 (0)