Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics methods to SDKs #543

Merged
merged 54 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f4c4bbd
created boilerplate for integration tests within js-sdk
0div Dec 23, 2024
4116313
rm integration test template work
0div Dec 23, 2024
d90fc02
add stress tests
0div Dec 23, 2024
7dc89ac
boilerplate for integration test template build
0div Dec 23, 2024
39dfd38
Merge branch 'create-integration-test-template-e2b-1361' into create-…
0div Dec 26, 2024
3245acd
fix template build command to include npm start command and add nextj…
0div Dec 26, 2024
e84bf8d
improve nextjs stress test
0div Dec 26, 2024
b1d02df
add get_metrics method to sandbox sync & async
0div Dec 27, 2024
198ac2e
add metrics command to CLI
0div Dec 30, 2024
c7ae716
Update packages/js-sdk/tests/setup.ts
0div Jan 17, 2025
a76ce3f
used create-next-app in template; remove old draft test
0div Jan 17, 2025
5c4207b
Merge branch 'main' of https://github.com/e2b-dev/E2B into create-str…
0div Jan 17, 2025
0b1d1a0
Merge branch 'add-get_metrics-method-to-python-sdk-e2b-1313' of https…
0div Jan 17, 2025
61b3630
Merge branch 'add-metrics-command-to-cli-e2b-1314' of https://github.…
0div Jan 17, 2025
7beccd7
Boilerplate for integration tests (within js-sdk) (#519)
0div Jan 18, 2025
e855adb
Add docs about filter on Sandbox.list
jakubno Jan 18, 2025
fb89e96
Add slight changes to wording in docs
mlejva Jan 18, 2025
2f22248
Add docs about filter on Sandbox.list (#544)
mlejva Jan 18, 2025
6aff0a1
Clarify how filtering works with multiple key-value pairs
mlejva Jan 18, 2025
8703049
Update text
mlejva Jan 18, 2025
69025a7
Clarify how filtering works with multiple key-value pairs (#545)
mlejva Jan 18, 2025
1c1a681
Fix default user for the user
jakubno Jan 20, 2025
61cc00a
Fix default user for the user (#546)
jakubno Jan 20, 2025
30a1cc9
merge main & update openapi spec to latest
0div Jan 21, 2025
fcd40f1
fix js-sdk getMetrics
0div Jan 21, 2025
91b06e3
Fix create checkout for users on another clusters
jakubno Jan 22, 2025
f59f2cb
update openapi spec for updated metrics type
0div Jan 23, 2025
a3a61e0
update python-sdk models
0div Jan 23, 2025
881b4c8
Regenerate python
jakubno Jan 23, 2025
f2ab7b1
add missing gen files
0div Jan 23, 2025
da92b26
Merge branch 'add-metrics-methods-to-sdks' of https://github.com/e2b-…
0div Jan 23, 2025
41eb007
fix get_metrics tests in python-sdk
0div Jan 23, 2025
f25519f
update sleep time in python sdk metrics test
0div Jan 23, 2025
5da2d4f
adress PR comments
0div Jan 23, 2025
9282976
Add envd version
jakubno Jan 23, 2025
2e5398c
Add python tests
jakubno Jan 23, 2025
323c221
Add TS test
jakubno Jan 23, 2025
e3f953d
Add envd version to SDKs (#548)
jakubno Jan 23, 2025
02b4e9e
merge main
0div Jan 23, 2025
ef11a49
adress PR comments; check envd version when calling get metrics
0div Jan 23, 2025
fba9841
adress nits
0div Jan 23, 2025
4605a62
overload get_metircs with staticmethod
0div Jan 23, 2025
9cb1662
add class method test in js-sdk
0div Jan 23, 2025
61d1479
Add missing backticks
jakubno Jan 23, 2025
b24a39e
add missing docstring
0div Jan 23, 2025
66de628
Update packages/js-sdk/src/sandbox/sandboxApi.ts
0div Jan 23, 2025
9f3018c
Merge 'main'
jakubno Jan 23, 2025
94396b0
Merge remote-tracking branch 'origin/add-metrics-methods-to-sdks' int…
jakubno Jan 23, 2025
c2c8065
Fix docstrings
jakubno Jan 24, 2025
3055d0e
add stress test for getMetrics
0div Jan 24, 2025
fa32e39
update cpuPct to cpuUsedPCt
0div Jan 24, 2025
eafaab1
Merge branch 'main' of https://github.com/e2b-dev/E2B into add-metric…
0div Jan 24, 2025
58fa7d4
Generate API
jakubno Jan 24, 2025
d920f54
Update spec
jakubno Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ generate-js:
cd packages/js-sdk && pnpm generate-envd-api
cd spec/envd && buf generate --template buf-js.gen.yaml

# `brew install protobuf` beforehand
generate-python:
$(MAKE) -C packages/connect-python build
cd packages/python-sdk && make generate-api
Expand Down
50 changes: 50 additions & 0 deletions apps/web/src/app/(docs)/docs/sandbox/list/page.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Link from 'next/link'

# List running sandboxes

You can list all running sandboxes using the `Sandbox.list()` method.
Expand Down Expand Up @@ -58,3 +60,51 @@ Running sandbox started at: 2024-10-15 21:13:07.311861+00:00
Running sandbox template id: 3e4rngfa34txe0gxc1zf
```
</CodeGroup>


## Filtering sandboxes
<Note>
This feature is in a private beta.
</Note>

You can filter sandboxes by specifying <Link href="/docs/sandbox/metadata">Metadata</Link> key value pairs.
Specifying multiple key value pairs will return sandboxes that match all of them.

This can be useful when you have a large number of sandboxes and want to find only specific ones. The filtering is performed on the server.

<CodeGroup>
```js
import { Sandbox } from '@e2b/code-interpreter'

// Create sandbox with metadata.
const sandbox = await Sandbox.create({
metadata: {
env: 'dev', // $HighlightLine
app: 'my-app', // $HighlightLine
userId: '123', // $HighlightLine
},
})

// List running sandboxes that has `userId` key with value `123` and `env` key with value `dev`.
const runningSandboxes = await Sandbox.list({
filters: { userId: '123', env: 'dev' } // $HighlightLine
})
```
```python
from e2b_code_interpreter import Sandbox

# Create sandbox with metadata.
sandbox = Sandbox(
metadata={
"env": "dev", # $HighlightLine
"app": "my-app", # $HighlightLine
"user_id": "123", # $HighlightLine
},
)

# List running sandboxes that has `userId` key with value `123` and `env` key with value `dev`.
running_sandboxes = Sandbox.list(filters={
"userId": "123", "env": "dev" # $HighlightLine
})
```
</CodeGroup>
5 changes: 5 additions & 0 deletions apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Link from 'next/link'

# Sandbox metadata

Metadata is a way to attach arbitrary key-value pairs for a sandbox.
Expand Down Expand Up @@ -47,3 +49,6 @@ running_sandboxes = Sandbox.list()
print(running_sandboxes[0].metadata)
```
</CodeGroup>

## Filtering sandboxes by metadata
You can also filter sandboxes by metadata, you can find more about it <Link href="/docs/sandbox/list#filtering-sandboxes">here</Link>.
11 changes: 1 addition & 10 deletions apps/web/src/components/Pricing/SwitchToProButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,9 @@ import Spinner from '@/components/Spinner'

import { TierActiveTag } from './TierActiveTag'
import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils'
import { toast } from '@/components/ui/use-toast'

function createCheckout(domain: string, tierID: string, teamID: string) {
if (domain !== 'e2b.dev') {
console.error('Managing billing is allowed only at e2b.dev.')
toast({
title: 'Error',
description: 'Managing billing is allowed only at e2b.dev.',
})
}

return fetch(getBillingUrl(domain, '/checkouts'), {
return fetch(getBillingUrl('e2b.dev', '/checkouts'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
41 changes: 27 additions & 14 deletions apps/web/src/utils/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ export type Team = {
apiKeys: string[]
}

interface APIKey { api_key: string; }
interface UserTeam {
id: string;
name: string;
is_default: boolean;
tier: string;
email: string;
team_api_keys: { api_key: string; }[];
teams: {
tier: string;
email: string;
team_api_keys: { api_key: string; }[];
id: string;
name: string;
}
}

export type E2BUser = (User & {
Expand Down Expand Up @@ -110,20 +113,30 @@ export const CustomUserContextProvider = (props) => {
if (!session) return
if (!session.user.id) return

// @ts-ignore
const { data: userTeams, teamsError } = await supabase
const { data: userTeams, error: teamsError } = await supabase
.from('users_teams')
.select('teams (id, name, is_default, tier, email, team_api_keys (api_key))')
.select('is_default, teams (id, name, tier, email, team_api_keys (api_key))')
.eq('user_id', session?.user.id) // Due to RLS, we could also safely just fetch all, but let's be explicit for sure

if (teamsError) Sentry.captureException(teamsError)
// TODO: Adjust when user can be part of multiple teams
// @ts-ignore
const teams = userTeams?.map(userTeam => userTeam.teams).map((team: UserTeam) => ({
...team,
apiKeys: team.team_api_keys.map(apiKey => apiKey.api_key)
} as Team))

if (userTeams === undefined || userTeams === null) {
console.log('No user teams found')
Sentry.captureEvent({ message: 'No user teams found' })
return
}

const typedUserTeams = userTeams as unknown as UserTeam[]
const teams: Team[] = typedUserTeams.map((userTeam: UserTeam): Team => {
return {
id: userTeam.teams.id,
name: userTeam.teams.name,
tier: userTeam.teams.tier,
is_default: userTeam.is_default,
email: userTeam.teams.email,
apiKeys: userTeam.teams.team_api_keys.map((apiKey: APIKey) => apiKey.api_key),
}
})
const defaultTeam = teams?.find(team => team.is_default)

if (!defaultTeam) {
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/commands/sandbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as commander from 'commander'

import { connectCommand } from './connect'
import { listCommand } from './list'
import { killCommand } from './kill'
import { spawnCommand } from './spawn'
import { listCommand } from './list'
import { logsCommand } from './logs'
import { metricsCommand } from './metrics'
import { spawnCommand } from './spawn'

export const sandboxCommand = new commander.Command('sandbox')
.description('work with sandboxes')
Expand All @@ -14,3 +15,4 @@ export const sandboxCommand = new commander.Command('sandbox')
.addCommand(killCommand)
.addCommand(spawnCommand)
.addCommand(logsCommand)
.addCommand(metricsCommand)
2 changes: 1 addition & 1 deletion packages/cli/src/commands/sandbox/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function getShortID(sandboxID: string) {
return sandboxID.split('-')[0]
}

function waitForSandboxEnd(sandboxID: string) {
export function waitForSandboxEnd(sandboxID: string) {
let isRunning = true

async function monitor() {
Expand Down
199 changes: 199 additions & 0 deletions packages/cli/src/commands/sandbox/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import * as chalk from 'chalk'
import * as commander from 'commander'
import * as e2b from 'e2b'
import * as util from 'util'

import { client, connectionConfig } from 'src/api'
import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
import { wait } from 'src/utils/wait'
import { handleE2BRequestError } from '../../utils/errors'
import { listSandboxes } from './list'
import { waitForSandboxEnd } from './logs'

const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
0div marked this conversation as resolved.
Show resolved Hide resolved

function getShortID(sandboxID: string) {
return sandboxID.split('-')[0]
}

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 ''
}

return logger.replaceAll('Svc', '')
}
0div marked this conversation as resolved.
Show resolved Hide resolved

export const metricsCommand = new commander.Command('metrics')
.description('show metrics for sandbox')
.argument(
'<sandboxID>',
`show metrics for sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('mt')
.option('-f, --follow', 'keep streaming metrics until the sandbox is closed')
.option(
0div marked this conversation as resolved.
Show resolved Hide resolved
'--format <format>',
`specify format for printing metrics (${formatEnum(LogFormat)})`,
LogFormat.PRETTY
)
.action(
async (
sandboxID: string,
opts?: {
level: string
follow: boolean
format: LogFormat
loggers?: string[]
}
) => {
try {
const format = opts?.format.toLowerCase() as LogFormat | undefined
if (format && !Object.values(LogFormat).includes(format)) {
throw new Error(`Invalid log format: ${format}`)
}

const getIsRunning = opts?.follow
? waitForSandboxEnd(sandboxID)
: () => false

let start: number | undefined
let isFirstRun = true
let firstMetricsPrinted = false

if (format === LogFormat.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 getSandboxMetrics({ sandboxID })

if (metrics.length !== 0 && firstMetricsPrinted === false) {
firstMetricsPrinted = true
process.stdout.write('\n')
}

for (const metric of metrics) {
printMetric(metric.timestamp, JSON.stringify(metric), format)
}

const isRunning = await isRunningPromise

if (!isRunning && metrics.length === 0 && isFirstRun) {
if (format === LogFormat.PRETTY) {
console.log(
`\nStopped printing metrics — sandbox ${withUnderline(
'not found'
)}`
)
}
break
}

if (!isRunning) {
if (format === LogFormat.PRETTY) {
console.log(
`\nStopped printing metrics — sandbox is ${withUnderline(
'closed'
)}`
)
}
break
}

const lastMetric =
metrics.length > 0 ? metrics[metrics.length - 1] : undefined
if (lastMetric) {
// TODO: Use the timestamp from the last metric instead of the current time?
0div marked this conversation as resolved.
Show resolved Hide resolved
0div marked this conversation as resolved.
Show resolved Hide resolved
start = new Date(lastMetric.timestamp).getTime() + 1
}

await wait(400)
isFirstRun = false
} while (getIsRunning() && opts?.follow)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)

function printMetric(
timestamp: string,
line: string,
format: LogFormat | undefined
) {
const metric = JSON.parse(line)
const level = chalk.default.green()

metric.logger = cleanLogger(metric.logger)

delete metric['traceID']
delete metric['instanceID']
delete metric['source_type']
delete metric['teamID']
delete metric['source']
delete metric['service']
delete metric['envID']
delete metric['sandboxID']
0div marked this conversation as resolved.
Show resolved Hide resolved
delete metric['logger']

if (format === LogFormat.JSON) {
console.log(
JSON.stringify({
timestamp: new Date(timestamp).toISOString(),
level,
...metric,
})
)
} else {
const time = `[${new Date(timestamp).toISOString().replace(/T/, ' ')}]`
delete metric['level']
console.log(
`${asTimestamp(time)} ${level} ` +
util.inspect(metric, {
colors: true,
depth: null,
maxArrayLength: Infinity,
sorted: true,
compact: true,
breakLength: Infinity,
})
)
}
}

export async function getSandboxMetrics({
sandboxID,
}: {
sandboxID: string
}): Promise<e2b.components['schemas']['SandboxMetric'][]> {
const signal = connectionConfig.getSignal()
const res = await client.api.GET('/sandboxes/{sandboxID}/metrics', {
signal,
params: {
path: {
sandboxID,
},
},
})

handleE2BRequestError(res.error, 'Error while getting sandbox metrics')

return res.data as e2b.components['schemas']['SandboxMetric'][]
}
3 changes: 2 additions & 1 deletion packages/js-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"update-deps": "ncu -u && pnpm i",
"postPublish": "./scripts/post-publish.sh || true",
"test:bun": "bun test tests/runtimes/bun --env-file=.env",
"test:deno": "deno test tests/runtimes/deno/ --allow-net --allow-read --allow-env --unstable-sloppy-imports --trace-leaks"
"test:deno": "deno test tests/runtimes/deno/ --allow-net --allow-read --allow-env --unstable-sloppy-imports --trace-leaks",
"test:integration": "E2B_INTEGRATION_TEST=1 vitest run tests/integration/**"
},
"devDependencies": {
"@testing-library/react": "^16.0.1",
Expand Down
Loading
Loading