Skip to content

Commit b331315

Browse files
feat: add open telemetry for sandboxes methods (#147)
1 parent 6050dbd commit b331315

5 files changed

Lines changed: 165 additions & 70 deletions

File tree

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"@hey-api/client-fetch": "^0.7.3",
9696
"@inkjs/ui": "^2.0.0",
9797
"@msgpack/msgpack": "^3.1.0",
98+
"@opentelemetry/api": "^1.9.0",
9899
"blessed": "^0.1.81",
99100
"blessed-contrib": "^4.11.0",
100101
"chalk": "^5.4.1",

src/Sandboxes.ts

Lines changed: 156 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Sandbox } from "./Sandbox";
22
import { API } from "./API";
33
import { getDefaultTemplateTag, getStartOptions } from "./utils/api";
4+
import { Tracer, SpanStatusCode } from "@opentelemetry/api";
45

56
import {
67
CreateSandboxOpts,
@@ -16,11 +17,48 @@ import {
1617
* This class provides methods for creating and managing sandboxes.
1718
*/
1819
export class Sandboxes {
20+
private tracer?: Tracer;
21+
1922
get defaultTemplateId() {
2023
return getDefaultTemplateTag(this.api.getClient());
2124
}
2225

23-
constructor(private api: API) {}
26+
constructor(private api: API, tracer?: Tracer) {
27+
this.tracer = tracer;
28+
}
29+
30+
private async withSpan<T>(
31+
operationName: string,
32+
attributes: Record<string, string | number | boolean> = {},
33+
operation: () => Promise<T>
34+
): Promise<T> {
35+
if (!this.tracer) {
36+
return operation();
37+
}
38+
39+
return this.tracer.startActiveSpan(
40+
operationName,
41+
{ attributes },
42+
async (span) => {
43+
try {
44+
const result = await operation();
45+
span.setStatus({ code: SpanStatusCode.OK });
46+
return result;
47+
} catch (error) {
48+
span.setStatus({
49+
code: SpanStatusCode.ERROR,
50+
message: error instanceof Error ? error.message : String(error),
51+
});
52+
span.recordException(
53+
error instanceof Error ? error : new Error(String(error))
54+
);
55+
throw error;
56+
} finally {
57+
span.end();
58+
}
59+
}
60+
);
61+
}
2462

2563
private async createTemplateSandbox(
2664
opts?: CreateSandboxOpts & StartSandboxOpts
@@ -62,26 +100,44 @@ export class Sandboxes {
62100
* Note! On CLEAN bootups the setup will run again. When hibernated a new snapshot will be created.
63101
*/
64102
async resume(sandboxId: string) {
65-
const startResponse = await this.api.startVm(sandboxId);
66-
return new Sandbox(sandboxId, this.api, startResponse);
103+
return this.withSpan(
104+
"sandboxes.resume",
105+
{ "sandbox.id": sandboxId },
106+
async () => {
107+
const startResponse = await this.api.startVm(sandboxId);
108+
return new Sandbox(sandboxId, this.api, startResponse);
109+
}
110+
);
67111
}
68112

69113
/**
70114
* Shuts down a sandbox. Files will be saved, and the sandbox will be stopped.
71115
*/
72116
async shutdown(sandboxId: string): Promise<void> {
73-
await this.api.shutdown(sandboxId);
117+
return this.withSpan(
118+
"sandboxes.shutdown",
119+
{ "sandbox.id": sandboxId },
120+
async () => {
121+
await this.api.shutdown(sandboxId);
122+
}
123+
);
74124
}
75125

76126
/**
77127
* Forks a sandbox. This will create a new sandbox from the given sandbox.
78128
* @deprecated This will be removed shortly to avoid having multiple ways of doing the same thing
79129
*/
80130
public async fork(sandboxId: string, opts?: StartSandboxOpts) {
81-
return this.create({
82-
id: sandboxId,
83-
...opts,
84-
});
131+
return this.withSpan(
132+
"sandboxes.fork",
133+
{ "sandbox.id": sandboxId },
134+
async () => {
135+
return this.create({
136+
id: sandboxId,
137+
...opts,
138+
});
139+
}
140+
);
85141
}
86142

87143
/**
@@ -91,33 +147,54 @@ export class Sandboxes {
91147
* Will resolve once the sandbox is restarted with its setup running.
92148
*/
93149
public async restart(sandboxId: string, opts?: StartSandboxOpts) {
94-
try {
95-
await this.shutdown(sandboxId);
96-
} catch (e) {
97-
throw new Error("Failed to shutdown VM, " + String(e));
98-
}
150+
return this.withSpan(
151+
"sandboxes.restart",
152+
{ "sandbox.id": sandboxId },
153+
async () => {
154+
try {
155+
await this.shutdown(sandboxId);
156+
} catch (e) {
157+
throw new Error("Failed to shutdown VM, " + String(e));
158+
}
99159

100-
try {
101-
const startResponse = await this.api.startVm(sandboxId, opts);
160+
try {
161+
const startResponse = await this.api.startVm(sandboxId, opts);
102162

103-
return new Sandbox(sandboxId, this.api, startResponse);
104-
} catch (e) {
105-
throw new Error("Failed to start VM, " + String(e));
106-
}
163+
return new Sandbox(sandboxId, this.api, startResponse);
164+
} catch (e) {
165+
throw new Error("Failed to start VM, " + String(e));
166+
}
167+
}
168+
);
107169
}
108170
/**
109171
* Hibernates a sandbox. Files will be saved, and the sandbox will be put to sleep. Next time
110172
* you resume the sandbox it will continue from the last state it was in.
111173
*/
112174
async hibernate(sandboxId: string): Promise<void> {
113-
await this.api.hibernate(sandboxId);
175+
return this.withSpan(
176+
"sandboxes.hibernate",
177+
{ "sandbox.id": sandboxId },
178+
async () => {
179+
await this.api.hibernate(sandboxId);
180+
}
181+
);
114182
}
115183

116184
/**
117185
* Create a sandbox from a template. By default we will create a sandbox from the default universal template.
118186
*/
119187
async create(opts?: CreateSandboxOpts & StartSandboxOpts): Promise<Sandbox> {
120-
return this.createTemplateSandbox(opts);
188+
return this.withSpan(
189+
"sandboxes.create",
190+
{
191+
"template.id": opts?.id || this.defaultTemplateId,
192+
"sandbox.privacy": opts?.privacy || "unlisted",
193+
},
194+
async () => {
195+
return this.createTemplateSandbox(opts);
196+
}
197+
);
121198
}
122199

123200
/**
@@ -155,59 +232,70 @@ export class Sandboxes {
155232
pagination?: PaginationOpts;
156233
} = {}
157234
): Promise<SandboxListResponse> {
158-
const limit = opts.limit ?? 50;
159-
let allSandboxes: SandboxInfo[] = [];
160-
let currentPage = opts.pagination?.page ?? 1;
161-
let pageSize = opts.pagination?.pageSize ?? limit;
162-
let totalCount = 0;
163-
let nextPage: number | null = null;
235+
return this.withSpan(
236+
"sandboxes.list",
237+
{
238+
"list.limit": opts.limit ?? 50,
239+
"list.tags": opts.tags?.join(",") || "",
240+
"list.orderBy": opts.orderBy || "inserted_at",
241+
"list.direction": opts.direction || "desc",
242+
},
243+
async () => {
244+
const limit = opts.limit ?? 50;
245+
let allSandboxes: SandboxInfo[] = [];
246+
let currentPage = opts.pagination?.page ?? 1;
247+
let pageSize = opts.pagination?.pageSize ?? limit;
248+
let totalCount = 0;
249+
let nextPage: number | null = null;
164250

165-
while (true) {
166-
const info = await this.api.listSandboxes({
167-
tags: opts.tags?.join(","),
168-
page: currentPage,
169-
page_size: pageSize,
170-
order_by: opts.orderBy,
171-
direction: opts.direction,
172-
status: opts.status,
173-
});
174-
totalCount = info.pagination.total_records;
175-
nextPage = info.pagination.next_page;
251+
while (true) {
252+
const info = await this.api.listSandboxes({
253+
tags: opts.tags?.join(","),
254+
page: currentPage,
255+
page_size: pageSize,
256+
order_by: opts.orderBy,
257+
direction: opts.direction,
258+
status: opts.status,
259+
});
260+
totalCount = info.pagination.total_records;
261+
nextPage = info.pagination.next_page;
176262

177-
const sandboxes = info.sandboxes.map((sandbox) => ({
178-
id: sandbox.id,
179-
createdAt: new Date(sandbox.created_at),
180-
updatedAt: new Date(sandbox.updated_at),
181-
title: sandbox.title ?? undefined,
182-
description: sandbox.description ?? undefined,
183-
privacy: privacyFromNumber(sandbox.privacy),
184-
tags: sandbox.tags,
185-
}));
263+
const sandboxes = info.sandboxes.map((sandbox) => ({
264+
id: sandbox.id,
265+
createdAt: new Date(sandbox.created_at),
266+
updatedAt: new Date(sandbox.updated_at),
267+
title: sandbox.title ?? undefined,
268+
description: sandbox.description ?? undefined,
269+
privacy: privacyFromNumber(sandbox.privacy),
270+
tags: sandbox.tags,
271+
}));
186272

187-
const newSandboxes = sandboxes.filter(
188-
(sandbox) =>
189-
!allSandboxes.some((existing) => existing.id === sandbox.id)
190-
);
191-
allSandboxes = [...allSandboxes, ...newSandboxes];
273+
const newSandboxes = sandboxes.filter(
274+
(sandbox) =>
275+
!allSandboxes.some((existing) => existing.id === sandbox.id)
276+
);
277+
allSandboxes = [...allSandboxes, ...newSandboxes];
192278

193-
// Stop if we've hit the limit or there are no more pages
194-
if (!nextPage || allSandboxes.length >= limit) {
195-
break;
196-
}
279+
// Stop if we've hit the limit or there are no more pages
280+
if (!nextPage || allSandboxes.length >= limit) {
281+
break;
282+
}
197283

198-
currentPage = nextPage;
199-
}
284+
currentPage = nextPage;
285+
}
200286

201-
return {
202-
sandboxes: allSandboxes,
203-
hasMore: totalCount > allSandboxes.length,
204-
totalCount,
205-
pagination: {
206-
currentPage,
207-
nextPage: allSandboxes.length >= limit ? nextPage : null,
208-
pageSize,
209-
},
210-
};
287+
return {
288+
sandboxes: allSandboxes,
289+
hasMore: totalCount > allSandboxes.length,
290+
totalCount,
291+
pagination: {
292+
currentPage,
293+
nextPage: allSandboxes.length >= limit ? nextPage : null,
294+
pageSize,
295+
},
296+
};
297+
}
298+
);
211299
}
212300

213301
/**

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class CodeSandbox {
2828
const apiKey = apiToken || getInferredApiKey();
2929
const api = new API({ apiKey, config: opts });
3030

31-
this.sandboxes = new Sandboxes(api);
31+
this.sandboxes = new Sandboxes(api, opts.tracer);
3232
this.hosts = new HostTokens(api);
3333
}
3434
}

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { VMTier } from "./VMTier";
22
import { HostToken } from "./HostTokens";
33
import { Config } from "@hey-api/client-fetch";
4+
import { Tracer } from "@opentelemetry/api";
45

56
export interface PitcherManagerResponse {
67
bootupType: "RUNNING" | "CLEAN" | "RESUME" | "FORK";
@@ -80,6 +81,11 @@ export interface ClientOpts {
8081
* Additional headers to send with each request
8182
*/
8283
headers?: Record<string, string>;
84+
85+
/**
86+
* Optional OpenTelemetry tracer for instrumenting SDK operations
87+
*/
88+
tracer?: Tracer;
8389
}
8490

8591
export const DEFAULT_SUBSCRIPTIONS = {

0 commit comments

Comments
 (0)