History

Merge pull request #326 from RhysSullivan/rs/instrument-everywhere

feat(otel): instrument tool dispatch, plugins, storage, schema, transport

Rhys Sullivan committed 6 months ago

Commit 03340142 parents db53677 cb00c7116 files · +784 −269Browse files

Unified · Split

16 files
  1. apps/cloud/src/api/autumn.ts +1 −3
  2. apps/cloud/src/api/protected.ts +1 −3
  3. apps/cloud/src/mcp-session.ts +74 −37
  4. packages/core/execution/src/description.ts +13 −2
  5. packages/core/execution/src/engine.ts +19 −3
  6. packages/core/execution/src/tool-invoker.ts +14 −2
  7. packages/core/sdk/src/executor.ts +91 −31
  8. packages/core/sdk/src/schema-types.ts +8 −5
  9. packages/core/storage-core/src/factory.ts +82 −14
  10. packages/core/storage-drizzle/src/adapter.ts +70 −10
  11. packages/hosts/mcp/src/server.ts +81 −40
  12. packages/plugins/graphql/src/sdk/invoke.ts +38 −4
  13. packages/plugins/mcp/src/sdk/connection.ts +5 −1
  14. packages/plugins/mcp/src/sdk/invoke.ts +40 −6
  15. packages/plugins/mcp/src/sdk/plugin.ts +210 −103
  16. packages/plugins/openapi/src/sdk/invoke.ts +37 −5
apps/cloud/src/api/autumn.ts+1−3
@@ -7,7 +7,7 @@ import { server } from "../env"; · 6 unmodified lines
77 import { HttpResponseError, isServerError, toErrorServerResponse } from "./error-response";
88 import { SharedServices } from "./layers";
99
10−const handleAutumnRequestEffect = Effect.gen(function* () {
10+export const AutumnApiApp = Effect.gen(function* () {
1111 const request = yield* HttpServerRequest.HttpServerRequest;
1212 const webRequest = yield* Effect.mapError(
1313 HttpServerRequest.toWeb(request),
@@ -86,5 +86,3 @@ const handleAutumnRequestEffect = Effect.gen(function* () { · 72 unmodified lines
8686 return Effect.succeed(toErrorServerResponse(err));
8787 }),
8888 );
89−
90−export const AutumnApiApp = handleAutumnRequestEffect;
apps/cloud/src/api/protected.ts+1−3
@@ -56,7 +56,7 @@ const createProtectedApp = (organizationId: string, organizationName: string) => · 55 unmodified lines
5656 );
5757 });
5858
59−const handleProtectedRequestEffect = Effect.gen(function* () {
59+export const ProtectedApiApp = Effect.gen(function* () {
6060 const request = yield* HttpServerRequest.HttpServerRequest;
6161 const org = yield* lookupOrgForRequest(request);
6262 if (!org) {
@@ -80,5 +80,3 @@ const handleProtectedRequestEffect = Effect.gen(function* () { · 17 unmodified lines
8080 return Effect.succeed(toErrorServerResponse(err));
8181 }),
8282 );
83−
84−export const ProtectedApiApp = handleProtectedRequestEffect;
apps/cloud/src/mcp-session.ts+74−37
@@ -204,11 +204,13 @@ export class McpSessionDO extends DurableObject { · 203 unmodified lines
204204 };
205205 }
206206
207− private async loadSessionMeta(): Promise<SessionMeta | null> {
208− if (this.sessionMeta) return this.sessionMeta;
209− const stored = await this.ctx.storage.get<SessionMeta>(SESSION_META_KEY);
210− this.sessionMeta = stored ?? null;
211− return this.sessionMeta;
207+ private loadSessionMeta(): Effect.Effect<SessionMeta | null> {
208+ return Effect.promise(async () => {
209+ if (this.sessionMeta) return this.sessionMeta;
210+ const stored = await this.ctx.storage.get<SessionMeta>(SESSION_META_KEY);
211+ this.sessionMeta = stored ?? null;
212+ return this.sessionMeta;
213+ }).pipe(Effect.withSpan("mcp.session.load_meta"));
212214 }
213215
214216 private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> {
@@ -216,18 +218,20 @@ export class McpSessionDO extends DurableObject { · 1 unmodified line
216218 await this.ctx.storage.put(SESSION_META_KEY, sessionMeta);
217219 }
218220
219− private async clearSessionState(): Promise<void> {
220− this.sessionMeta = null;
221− this.initialized = false;
222− this.lastActivityMs = 0;
223−
224− await Promise.all([
225− this.ctx.storage.delete(TRANSPORT_STATE_KEY).catch(() => false),
226− this.ctx.storage.delete(SESSION_META_KEY).catch(() => false),
227− ]);
221+ private clearSessionState(): Effect.Effect<void> {
222+ return Effect.promise(async () => {
223+ this.sessionMeta = null;
224+ this.initialized = false;
225+ this.lastActivityMs = 0;
226+
227+ await Promise.all([
228+ this.ctx.storage.delete(TRANSPORT_STATE_KEY).catch(() => false),
229+ this.ctx.storage.delete(SESSION_META_KEY).catch(() => false),
230+ ]);
231+ }).pipe(Effect.withSpan("mcp.session.clear_state"));
228232 }
229233
230− private createConnectedRuntimeEffect(
234+ private createConnectedRuntime(
231235 sessionMeta: SessionMeta,
232236 options: { readonly dbHandle: DbHandle; readonly enableJsonResponse?: boolean },
233237 ) {
@@ -263,7 +267,7 @@ export class McpSessionDO extends DurableObject { · 29 unmodified lines
263267 );
264268 }
265269
266− private resolveAndStoreSessionMetaEffect(token: McpSessionInit) {
270+ private resolveAndStoreSessionMeta(token: McpSessionInit) {
267271 const self = this;
268272 return Effect.gen(function* () {
269273 const dbHandle = makeRequestScopedDb();
@@ -271,18 +275,20 @@ export class McpSessionDO extends DurableObject { · 1 unmodified line
271275 const sessionMeta = yield* resolveSessionMeta(token.organizationId).pipe(
272276 Effect.provide(makeResolveOrganizationServices(dbHandle)),
273277 );
274− yield* Effect.promise(() => self.saveSessionMeta(sessionMeta));
278+ yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe(
279+ Effect.withSpan("mcp.session.save_meta"),
280+ );
275281 return sessionMeta;
276282 } finally {
277283 yield* Effect.promise(() => dbHandle.end());
278284 }
279− });
285+ }).pipe(Effect.withSpan("mcp.session.resolve_and_store_meta"));
280286 }
281287
282288 async init(token: McpSessionInit, incoming?: IncomingTraceHeaders): Promise<void> {
283289 if (this.initialized) return;
284290 return Effect.runPromise(
285− this.doInitEffect(token).pipe(
291+ this.doInit(token).pipe(
286292 Effect.withSpan("McpSessionDO.init", {
287293 attributes: { "mcp.auth.organization_id": token.organizationId },
288294 }),
@@ -292,7 +298,7 @@ export class McpSessionDO extends DurableObject { · 3 unmodified lines
292298 );
293299 }
294300
295− private doInitEffect(token: McpSessionInit) {
301+ private doInit(token: McpSessionInit) {
296302 const self = this;
297303 // Single Effect chain so every sub-span (resolveSessionMeta,
298304 // createRuntime, createScopedExecutor, createExecutorMcpServer,
@@ -302,7 +308,7 @@ export class McpSessionDO extends DurableObject { · 3 unmodified lines
302308 // each sub-span into its own root trace and made init opaque —
303309 // dashboard saw one 2.77s span with nothing under it.
304310 return Effect.gen(function* () {
305− const sessionMeta = yield* self.resolveAndStoreSessionMetaEffect(token);
311+ const sessionMeta = yield* self.resolveAndStoreSessionMeta(token);
306312
307313 if (!requestScopedRuntimeEnabled) {
308314 self.dbHandle = makeLongLivedDb();
@@ -313,7 +319,7 @@ export class McpSessionDO extends DurableObject { · 4 unmodified lines
313319 // POSTs the callback fires after `Effect.ensuring` clears the field
314320 // and engine spans orphan into new root traces. GET still streams
315321 // (the GET handler doesn't consult `enableJsonResponse`).
316− const runtime = yield* self.createConnectedRuntimeEffect(sessionMeta, {
322+ const runtime = yield* self.createConnectedRuntime(sessionMeta, {
317323 dbHandle: self.dbHandle,
318324 enableJsonResponse: true,
319325 });
@@ -343,10 +349,10 @@ export class McpSessionDO extends DurableObject { · 23 unmodified lines
343349 );
344350 }
345351
346− private handleRequestWithRequestScopedRuntimeEffect(request: Request) {
352+ private handleRequestWithRequestScopedRuntime(request: Request) {
347353 const self = this;
348354 return Effect.gen(function* () {
349− const sessionMeta = yield* Effect.promise(() => self.loadSessionMeta());
355+ const sessionMeta = yield* self.loadSessionMeta();
350356 if (!sessionMeta) {
351357 return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect");
352358 }
@@ -355,30 +361,48 @@ export class McpSessionDO extends DurableObject { · 2 unmodified lines
355361 self.lastActivityMs = Date.now();
356362
357363 const dbHandle = makeRequestScopedDb();
358− const cleanupDb = Effect.promise(() => dbHandle.end());
364+ const cleanupDb = Effect.promise(() => dbHandle.end()).pipe(
365+ Effect.withSpan("mcp.session.db.close"),
366+ );
359367 return yield* Effect.acquireUseRelease(
360− self.createConnectedRuntimeEffect(sessionMeta, {
368+ self.createConnectedRuntime(sessionMeta, {
361369 dbHandle,
362370 enableJsonResponse: request.method !== "GET",
363371 }),
364372 ({ transport }) =>
365373 Effect.gen(function* () {
366374 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(
367− Effect.withSpan("McpSessionDO.transport.handleRequest"),
375+ Effect.withSpan("McpSessionDO.transport.handleRequest", {
376+ attributes: {
377+ "mcp.request.method": request.method,
378+ "mcp.request.content_type":
379+ request.headers.get("content-type") ?? "",
380+ "mcp.request.content_length":
381+ request.headers.get("content-length") ?? "",
382+ },
383+ }),
368384 );
385+ yield* Effect.annotateCurrentSpan({
386+ "mcp.response.status_code": response.status,
387+ });
369388 if (request.method === "DELETE") {
370− yield* Effect.promise(() => self.clearSessionState());
389+ yield* self.clearSessionState();
371390 }
372391 return response;
373392 }),
374393 ({ mcpServer, transport }) =>
375394 Effect.gen(function* () {
376− yield* Effect.promise(() => transport.close().catch(() => undefined));
377− yield* Effect.promise(() => mcpServer.close().catch(() => undefined));
395+ yield* Effect.promise(() => transport.close().catch(() => undefined)).pipe(
396+ Effect.withSpan("mcp.session.transport.close"),
397+ );
398+ yield* Effect.promise(() => mcpServer.close().catch(() => undefined)).pipe(
399+ Effect.withSpan("mcp.session.server.close"),
400+ );
378401 yield* cleanupDb;
379− }),
402+ }).pipe(Effect.withSpan("mcp.session.runtime.release")),
380403 );
381404 }).pipe(
405+ Effect.withSpan("mcp.session.request_scoped_runtime"),
382406 Effect.catchAllCause((cause) =>
383407 Effect.sync(() => {
384408 console.error("[mcp-session] request-scoped handleRequest error:", cause);
@@ -410,7 +434,7 @@ export class McpSessionDO extends DurableObject { · 25 unmodified lines
410434 const span = yield* Effect.currentSpan;
411435 self.currentRequestSpan = span;
412436
413− return yield* self.dispatchRequestEffect(request).pipe(
437+ return yield* self.dispatchRequest(request).pipe(
414438 Effect.tap((response) =>
415439 Effect.annotateCurrentSpan({
416440 "mcp.response.status_code": response.status,
@@ -435,9 +459,9 @@ export class McpSessionDO extends DurableObject { · 18 unmodified lines
435459 return Effect.runPromise(program);
436460 }
437461
438− private dispatchRequestEffect(request: Request): Effect.Effect<Response> {
462+ private dispatchRequest(request: Request): Effect.Effect<Response> {
439463 if (requestScopedRuntimeEnabled) {
440− return this.handleRequestWithRequestScopedRuntimeEffect(request);
464+ return this.handleRequestWithRequestScopedRuntime(request);
441465 }
442466
443467 if (!this.initialized || !this.transport) {
@@ -451,10 +475,23 @@ export class McpSessionDO extends DurableObject { · 7 unmodified lines
451475 const self = this;
452476 return Effect.gen(function* () {
453477 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(
454− Effect.withSpan("McpSessionDO.transport.handleRequest"),
478+ Effect.withSpan("McpSessionDO.transport.handleRequest", {
479+ attributes: {
480+ "mcp.request.method": request.method,
481+ "mcp.request.content_type":
482+ request.headers.get("content-type") ?? "",
483+ "mcp.request.content_length":
484+ request.headers.get("content-length") ?? "",
485+ },
486+ }),
455487 );
488+ yield* Effect.annotateCurrentSpan({
489+ "mcp.response.status_code": response.status,
490+ });
456491 if (request.method === "DELETE") {
457− yield* Effect.promise(() => self.cleanup());
492+ yield* Effect.promise(() => self.cleanup()).pipe(
493+ Effect.withSpan("mcp.session.cleanup"),
494+ );
458495 }
459496 return response;
460497 }).pipe(
@@ -498,6 +535,6 @@ export class McpSessionDO extends DurableObject { · 37 unmodified lines
498535 await this.dbHandle.end();
499536 this.dbHandle = null;
500537 }
501− await this.clearSessionState();
538+ await Effect.runPromise(this.clearSessionState());
502539 }
503540 }
packages/core/execution/src/description.ts+13−2
@@ -14,8 +14,19 @@ export const buildExecuteDescription = (executor: Executor): Effect.Effect<strin · 13 unmodified lines
1414 .list()
1515 .pipe(Effect.orDie, Effect.withSpan("executor.sources.list"));
1616
17− return formatDescription(sources);
18− }).pipe(Effect.withSpan("buildExecuteDescription"));
17+ const description = yield* Effect.sync(() => formatDescription(sources)).pipe(
18+ Effect.withSpan("schema.compile.description", {
19+ attributes: { "executor.source_count": sources.length },
20+ }),
21+ );
22+
23+ yield* Effect.annotateCurrentSpan({
24+ "executor.source_count": sources.length,
25+ "schema.kind": "execute",
26+ });
27+
28+ return description;
29+ }).pipe(Effect.withSpan("schema.describe.execute"));
1930
2031 const formatDescription = (sources: readonly Source[]): string => {
2132 const lines: string[] = [
packages/core/execution/src/engine.ts+19−3
@@ -194,7 +194,11 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 193 unmodified lines
194194
195195 return searchTools(executor, args.query ?? "", limit, {
196196 namespace: args.namespace,
197− });
197+ }).pipe(
198+ Effect.withSpan("mcp.tool.dispatch", {
199+ attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
200+ }),
201+ );
198202 }
199203 if (path === "executor.sources.list") {
200204 if (args !== undefined && !isRecord(args)) {
@@ -225,7 +229,11 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 24 unmodified lines
225229 return listExecutorSources(executor, {
226230 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,
227231 limit,
228− });
232+ }).pipe(
233+ Effect.withSpan("mcp.tool.dispatch", {
234+ attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
235+ }),
236+ );
229237 }
230238 if (path === "describe.tool") {
231239 if (!isRecord(args)) {
@@ -248,7 +256,15 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 16 unmodified lines
248256 );
249257 }
250258
251− return describeTool(executor, args.path);
259+ return describeTool(executor, args.path).pipe(
260+ Effect.withSpan("mcp.tool.dispatch", {
261+ attributes: {
262+ "mcp.tool.name": path,
263+ "executor.tool.builtin": true,
264+ "executor.tool.target_path": args.path,
265+ },
266+ }),
267+ );
252268 }
253269 return base.invoke({ path, args });
254270 },
packages/core/execution/src/tool-invoker.ts+14−2
@@ -288,12 +288,18 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 287 unmodified lines
288288 }
289289
290290 const all = yield* executor.tools.list().pipe(Effect.orDie);
291− return all
291+ const results = all
292292 .filter((tool: Tool) => matchesNamespace(tool, options?.namespace))
293293 .map((tool: Tool) => scoreToolMatch(tool, query))
294294 .filter((tool): tool is ToolDiscoveryResult => tool !== null)
295295 .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
296296 .slice(0, limit);
297+
298+ yield* Effect.annotateCurrentSpan({
299+ "executor.search.candidate_count": all.length,
300+ "executor.search.result_count": results.length,
301+ });
302+ return results;
297303 });
298304
299305 /** What `tools.executor.sources.list()` calls inside the sandbox. */
@@ -333,9 +339,15 @@ export const listExecutorSources = Effect.fn("executor.sources.list")(function* · 33 unmodified lines
333339 }) satisfies ExecutorSourceListItem,
334340 );
335341
336− return withCounts
342+ const results = withCounts
337343 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))
338344 .slice(0, limit);
345+
346+ yield* Effect.annotateCurrentSpan({
347+ "executor.sources.candidate_count": sources.length,
348+ "executor.sources.result_count": results.length,
349+ });
350+ return results;
339351 });
340352
341353 /** What `tools.describe.tool()` calls inside the sandbox. */
packages/core/sdk/src/executor.ts+91−31
@@ -892,8 +892,13 @@ export const createExecutor = < · 891 unmodified lines
892892 for (const { source, pluginId } of staticSources.values()) {
893893 staticList.push(staticDeclToSource(source, pluginId));
894894 }
895− return [...staticList, ...dynamic.map(rowToSource)];
896− });
895+ const merged = [...staticList, ...dynamic.map(rowToSource)];
896+ yield* Effect.annotateCurrentSpan({
897+ "executor.sources.static_count": staticList.length,
898+ "executor.sources.dynamic_count": dynamic.length,
899+ });
900+ return merged;
901+ }).pipe(Effect.withSpan("executor.sources.list"));
897902
898903 // Bulk-resolve annotations across a set of dynamic tool rows by
899904 // grouping them under their owning plugin's resolveAnnotations
@@ -936,7 +941,9 @@ export const createExecutor = < · 36 unmodified lines
936941 const listTools = (filter?: ToolListFilter) =>
937942 Effect.gen(function* () {
938943 const dynamic = yield* core.findMany({ model: "tool" });
939− const annotations = yield* resolveAnnotationsFor(dynamic);
944+ const annotations = yield* resolveAnnotationsFor(dynamic).pipe(
945+ Effect.withSpan("executor.tools.list.annotations"),
946+ );
940947
941948 const out: Tool[] = [];
942949 // Static tools — annotations from the declaration, not a resolver.
@@ -946,9 +953,14 @@ export const createExecutor = < · 3 unmodified lines
946953 for (const row of dynamic) {
947954 out.push(rowToTool(row, annotations.get(row.id)));
948955 }
949− if (!filter) return out;
950− return out.filter((t) => toolMatchesFilter(t, filter));
951− });
956+ const result = filter ? out.filter((t) => toolMatchesFilter(t, filter)) : out;
957+ yield* Effect.annotateCurrentSpan({
958+ "executor.tools.static_count": staticTools.size,
959+ "executor.tools.dynamic_count": dynamic.length,
960+ "executor.tools.result_count": result.length,
961+ });
962+ return result;
963+ }).pipe(Effect.withSpan("executor.tools.list"));
952964
953965 // Load all definitions for a single source as a plain map.
954966 const loadDefinitionsForSource = (sourceId: string) =>
@@ -975,7 +987,9 @@ export const createExecutor = < · 20 unmodified lines
975987 }) =>
976988 Effect.gen(function* () {
977989 const defs: Record<string, unknown> = opts.sourceId
978− ? yield* loadDefinitionsForSource(opts.sourceId)
990+ ? yield* loadDefinitionsForSource(opts.sourceId).pipe(
991+ Effect.withSpan("executor.tool.schema.load_defs"),
992+ )
979993 : {};
980994
981995 const attachDefs = (schema: unknown): unknown => {
@@ -988,11 +1002,18 @@ export const createExecutor = < · 6 unmodified lines
9881002 const outputSchema = attachDefs(opts.rawOutput);
9891003
9901004 const defsMap = new Map<string, unknown>(Object.entries(defs));
991− const preview = buildToolTypeScriptPreview({
992− inputSchema,
993− outputSchema,
994− defs: defsMap,
995− });
1005+ const preview = yield* Effect.sync(() =>
1006+ buildToolTypeScriptPreview({ inputSchema, outputSchema, defs: defsMap }),
1007+ ).pipe(
1008+ Effect.withSpan("schema.compile.preview", {
1009+ attributes: {
1010+ "schema.kind": "tool.preview",
1011+ "schema.has_input": inputSchema !== undefined,
1012+ "schema.has_output": outputSchema !== undefined,
1013+ "schema.def_count": defsMap.size,
1014+ },
1015+ }),
1016+ );
9961017
9971018 return new ToolSchema({
9981019 id: ToolId.make(opts.toolId),
@@ -1012,6 +1033,11 @@ export const createExecutor = < · 13 unmodified lines
10121033 // no `$defs` attach; just wrap the declared schemas.
10131034 const staticEntry = staticTools.get(toolId);
10141035 if (staticEntry) {
1036+ yield* Effect.annotateCurrentSpan({
1037+ "executor.tool.dispatch_path": "static",
1038+ "executor.source_id": staticEntry.source.id,
1039+ "executor.source_kind": staticEntry.source.kind,
1040+ });
10151041 return yield* buildToolSchemaView({
10161042 toolId,
10171043 name: staticEntry.tool.name,
@@ -1021,11 +1047,18 @@ export const createExecutor = < · 3 unmodified lines
10211047 rawOutput: staticEntry.tool.outputSchema,
10221048 });
10231049 }
1024− const row = yield* core.findOne({
1025− model: "tool",
1026− where: [{ field: "id", value: toolId }],
1027− });
1050+ const row = yield* core
1051+ .findOne({
1052+ model: "tool",
1053+ where: [{ field: "id", value: toolId }],
1054+ })
1055+ .pipe(Effect.withSpan("executor.tool.resolve"));
10281056 if (!row) return null;
1057+ yield* Effect.annotateCurrentSpan({
1058+ "executor.tool.dispatch_path": "dynamic",
1059+ "executor.source_id": row.source_id,
1060+ "executor.plugin_id": row.plugin_id,
1061+ });
10291062 return yield* buildToolSchemaView({
10301063 toolId,
10311064 name: row.name,
@@ -1034,7 +1067,11 @@ export const createExecutor = < · 2 unmodified lines
10341067 rawInput: decodeJsonColumn(row.input_schema),
10351068 rawOutput: decodeJsonColumn(row.output_schema),
10361069 });
1037− });
1070+ }).pipe(
1071+ Effect.withSpan("executor.tool.schema", {
1072+ attributes: { "mcp.tool.name": toolId },
1073+ }),
1074+ );
10381075
10391076 // Bulk definitions accessor — every source's $defs, grouped by
10401077 // source id. One query against the definition table, plus an
@@ -1121,31 +1158,44 @@ export const createExecutor = < · 80 unmodified lines
11211158 // Static path — O(1) map lookup, no DB hit.
11221159 const staticEntry = staticTools.get(toolId);
11231160 if (staticEntry) {
1161+ yield* Effect.annotateCurrentSpan({
1162+ "executor.tool.dispatch_path": "static",
1163+ "executor.source_id": staticEntry.source.id,
1164+ "executor.source_kind": staticEntry.source.kind,
1165+ "executor.plugin_id": staticEntry.pluginId,
1166+ });
11241167 yield* enforceApproval(
11251168 staticEntry.tool.annotations,
11261169 toolId,
11271170 args,
11281171 options,
1129− );
1172+ ).pipe(Effect.withSpan("executor.tool.enforce_approval"));
11301173 return yield* wrapInvocationError(
11311174 staticEntry.tool.handler({
11321175 ctx: staticEntry.ctx,
11331176 args,
11341177 elicit: buildElicit(toolId, args, options),
11351178 }),
1136− );
1179+ ).pipe(Effect.withSpan("executor.tool.handler"));
11371180 }
11381181
11391182 // Dynamic path — DB lookup + delegate to owning plugin.
1140− const row = yield* core.findOne({
1141− model: "tool",
1142− where: [{ field: "id", value: toolId }],
1143− });
1183+ const row = yield* core
1184+ .findOne({
1185+ model: "tool",
1186+ where: [{ field: "id", value: toolId }],
1187+ })
1188+ .pipe(Effect.withSpan("executor.tool.resolve"));
11441189 if (!row) {
11451190 return yield* new ToolNotFoundError({
11461191 toolId: ToolId.make(toolId),
11471192 });
11481193 }
1194+ yield* Effect.annotateCurrentSpan({
1195+ "executor.tool.dispatch_path": "dynamic",
1196+ "executor.source_id": row.source_id,
1197+ "executor.plugin_id": row.plugin_id,
1198+ });
11491199 const runtime = runtimes.get(row.plugin_id);
11501200 if (!runtime) {
11511201 return yield* new PluginNotLoadedError({
@@ -1167,14 +1217,18 @@ export const createExecutor = < · 15 unmodified lines
11671217 // around a single storage read.
11681218 let annotations: ToolAnnotations | undefined;
11691219 if (runtime.plugin.resolveAnnotations) {
1170− const map = yield* runtime.plugin.resolveAnnotations({
1171− ctx: runtime.ctx,
1172− sourceId: row.source_id,
1173− toolRows: [row],
1174− });
1220+ const map = yield* runtime.plugin
1221+ .resolveAnnotations({
1222+ ctx: runtime.ctx,
1223+ sourceId: row.source_id,
1224+ toolRows: [row],
1225+ })
1226+ .pipe(Effect.withSpan("executor.tool.resolve_annotations"));
11751227 annotations = map[toolId];
11761228 }
1177− yield* enforceApproval(annotations, toolId, args, options);
1229+ yield* enforceApproval(annotations, toolId, args, options).pipe(
1230+ Effect.withSpan("executor.tool.enforce_approval"),
1231+ );
11781232
11791233 return yield* wrapInvocationError(
11801234 runtime.plugin.invokeTool({
@@ -1183,8 +1237,14 @@ export const createExecutor = < · 2 unmodified lines
11831237 args,
11841238 elicit: buildElicit(toolId, args, options),
11851239 }),
1186− );
1187− });
1240+ ).pipe(Effect.withSpan("executor.tool.handler"));
1241+ }).pipe(
1242+ Effect.withSpan("executor.tool.invoke", {
1243+ attributes: {
1244+ "mcp.tool.name": toolId,
1245+ },
1246+ }),
1247+ );
11881248
11891249 const removeSource = (sourceId: string) =>
11901250 Effect.gen(function* () {
packages/core/sdk/src/schema-types.ts+8−5
@@ -364,16 +364,18 @@ export const schemaToTypeScriptPreviewWithDefs = ( · 363 unmodified lines
364364 };
365365 };
366366
367+export type ToolTypeScriptPreview = {
368+ inputTypeScript?: string;
369+ outputTypeScript?: string;
370+ typeScriptDefinitions?: Record<string, string>;
371+};
372+
367373 export const buildToolTypeScriptPreview = (input: {
368374 inputSchema?: unknown;
369375 outputSchema?: unknown;
370376 defs: ReadonlyMap<string, unknown>;
371377 options?: TypeScriptRenderOptions;
372−}): {
373− inputTypeScript?: string;
374− outputTypeScript?: string;
375− typeScriptDefinitions?: Record<string, string>;
376−} => {
378+}): ToolTypeScriptPreview => {
377379 const inputPreview =
378380 input.inputSchema !== undefined
379381 ? schemaToTypeScriptPreviewWithDefs(input.inputSchema, input.defs, input.options)
@@ -396,3 +398,4 @@ export const buildToolTypeScriptPreview = (input: { · 16 unmodified lines
396398 : {}),
397399 };
398400 };
401+
packages/core/storage-core/src/factory.ts+82−14
@@ -638,7 +638,14 @@ export const createAdapter = ( · 637 unmodified lines
638638 data.select,
639639 );
640640 return out as unknown as R;
641− }),
641+ }).pipe(
642+ Effect.withSpan("executor.storage.create", {
643+ attributes: {
644+ "executor.storage.adapter_id": config.adapterId,
645+ "executor.storage.table": data.model,
646+ },
647+ }),
648+ ),
642649
643650 createMany: <T extends Record<string, unknown>, R = T>(data: {
644651 model: string;
@@ -678,7 +685,15 @@ export const createAdapter = ( · 33 unmodified lines
678685 );
679686 }
680687 return out as unknown as readonly R[];
681− }),
688+ }).pipe(
689+ Effect.withSpan("executor.storage.create_many", {
690+ attributes: {
691+ "executor.storage.adapter_id": config.adapterId,
692+ "executor.storage.table": data.model,
693+ "executor.storage.row_count": data.data.length,
694+ },
695+ }),
696+ ),
682697
683698 findOne: <T>(data: {
684699 model: string;
@@ -698,7 +713,14 @@ export const createAdapter = ( · 13 unmodified lines
698713 const out = yield* maybeTransformOutput(data.model, res, data.select);
699714 const merged = yield* attachJoinedRows(out, res, data.join);
700715 return merged as unknown as T | null;
701− }),
716+ }).pipe(
717+ Effect.withSpan("executor.storage.find_one", {
718+ attributes: {
719+ "executor.storage.adapter_id": config.adapterId,
720+ "executor.storage.table": data.model,
721+ },
722+ }),
723+ ),
702724
703725 findMany: <T>(data: {
704726 model: string;
@@ -734,7 +756,14 @@ export const createAdapter = ( · 29 unmodified lines
734756 out.push(merged);
735757 }
736758 return out as readonly T[];
737− }),
759+ }).pipe(
760+ Effect.withSpan("executor.storage.find_many", {
761+ attributes: {
762+ "executor.storage.adapter_id": config.adapterId,
763+ "executor.storage.table": data.model,
764+ },
765+ }),
766+ ),
738767
739768 count: (data: { model: string; where?: Where[] | undefined }) =>
740769 Effect.gen(function* () {
@@ -743,7 +772,14 @@ export const createAdapter = ( · 2 unmodified lines
743772 model: getModelName(data.model),
744773 where,
745774 });
746− }),
775+ }).pipe(
776+ Effect.withSpan("executor.storage.count", {
777+ attributes: {
778+ "executor.storage.adapter_id": config.adapterId,
779+ "executor.storage.table": data.model,
780+ },
781+ }),
782+ ),
747783
748784 update: <T>(data: {
749785 model: string;
@@ -765,7 +801,14 @@ export const createAdapter = ( · 15 unmodified lines
765801 });
766802 const out = yield* maybeTransformOutput(data.model, res);
767803 return out as unknown as T | null;
768− }),
804+ }).pipe(
805+ Effect.withSpan("executor.storage.update", {
806+ attributes: {
807+ "executor.storage.adapter_id": config.adapterId,
808+ "executor.storage.table": data.model,
809+ },
810+ }),
811+ ),
769812
770813 updateMany: (data: {
771814 model: string;
@@ -785,7 +828,14 @@ export const createAdapter = ( · 13 unmodified lines
785828 where,
786829 update,
787830 });
788− }),
831+ }).pipe(
832+ Effect.withSpan("executor.storage.update_many", {
833+ attributes: {
834+ "executor.storage.adapter_id": config.adapterId,
835+ "executor.storage.table": data.model,
836+ },
837+ }),
838+ ),
789839
790840 delete: (data: { model: string; where: Where[] }) =>
791841 Effect.gen(function* () {
@@ -794,7 +844,14 @@ export const createAdapter = ( · 2 unmodified lines
794844 model: getModelName(data.model),
795845 where,
796846 });
797− }),
847+ }).pipe(
848+ Effect.withSpan("executor.storage.delete", {
849+ attributes: {
850+ "executor.storage.adapter_id": config.adapterId,
851+ "executor.storage.table": data.model,
852+ },
853+ }),
854+ ),
798855
799856 deleteMany: (data: { model: string; where: Where[] }) =>
800857 Effect.gen(function* () {
@@ -803,17 +860,28 @@ export const createAdapter = ( · 2 unmodified lines
803860 model: getModelName(data.model),
804861 where,
805862 });
806− }),
863+ }).pipe(
864+ Effect.withSpan("executor.storage.delete_many", {
865+ attributes: {
866+ "executor.storage.adapter_id": config.adapterId,
867+ "executor.storage.table": data.model,
868+ },
869+ }),
870+ ),
807871
808872 transaction: <R, E>(
809873 callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,
810874 ) => {
811875 const txFn = config.transaction;
812− if (!txFn) {
813− // No real transaction support — just run the callback against self.
814− return callback(self);
815− }
816− return txFn(callback);
876+ const ran = !txFn ? callback(self) : txFn(callback);
877+ return ran.pipe(
878+ Effect.withSpan("executor.storage.transaction", {
879+ attributes: {
880+ "executor.storage.adapter_id": config.adapterId,
881+ "executor.storage.transaction.native": txFn !== undefined,
882+ },
883+ }),
884+ );
817885 },
818886
819887 // Forward the backend's createSchema verbatim. Upstream better-auth
packages/core/storage-drizzle/src/adapter.ts+70−10
@@ -361,6 +361,12 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 360 unmodified lines
361361 return t;
362362 };
363363
364+ const backendAttrs = (model: string) => ({
365+ "executor.storage.backend": "drizzle" as const,
366+ "executor.storage.drizzle.provider": provider,
367+ "executor.storage.table": model,
368+ });
369+
364370 const custom: CustomAdapter = {
365371 create: ({ model, data }) =>
366372 Effect.gen(function* () {
@@ -375,7 +381,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
375381 model,
376382 );
377383 return row as never;
378− }),
384+ }).pipe(
385+ Effect.withSpan("executor.storage.backend.create", {
386+ attributes: backendAttrs(model),
387+ }),
388+ ),
379389
380390 // Real multi-row INSERT in fixed-size chunks. One statement per
381391 // chunk, not one per row — per-row loops blow the Hyperdrive
@@ -401,7 +411,14 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 19 unmodified lines
401411 for (const row of rows) all.push(row);
402412 }
403413 return all as never;
404− }),
414+ }).pipe(
415+ Effect.withSpan("executor.storage.backend.create_many", {
416+ attributes: {
417+ ...backendAttrs(model),
418+ "executor.storage.row_count": data.length,
419+ },
420+ }),
421+ ),
405422
406423 findOne: ({ model, where, join }) =>
407424 Effect.gen(function* () {
@@ -430,7 +447,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 22 unmodified lines
430447 model,
431448 )) as Record<string, unknown>[];
432449 return (rows[0] ?? null) as never;
433− }),
450+ }).pipe(
451+ Effect.withSpan("executor.storage.backend.find_one", {
452+ attributes: backendAttrs(model),
453+ }),
454+ ),
434455
435456 findMany: ({ model, where, limit, sortBy, offset, join }) =>
436457 Effect.gen(function* () {
@@ -477,7 +498,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 40 unmodified lines
477498 model,
478499 )) as Record<string, unknown>[];
479500 return rows as never[];
480− }),
501+ }).pipe(
502+ Effect.withSpan("executor.storage.backend.find_many", {
503+ attributes: backendAttrs(model),
504+ }),
505+ ),
481506
482507 count: ({ model, where }) =>
483508 Effect.gen(function* () {
@@ -492,7 +517,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
492517 )) as { c: number | string | bigint }[];
493518 const raw = rows[0]?.c ?? 0;
494519 return typeof raw === "number" ? raw : Number(raw);
495− }),
520+ }).pipe(
521+ Effect.withSpan("executor.storage.backend.count", {
522+ attributes: backendAttrs(model),
523+ }),
524+ ),
496525
497526 update: ({ model, where, update }) =>
498527 Effect.gen(function* () {
@@ -529,7 +558,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 30 unmodified lines
529558 model,
530559 )) as Record<string, unknown>[];
531560 return (reread[0] ?? null) as never;
532− }),
561+ }).pipe(
562+ Effect.withSpan("executor.storage.backend.update", {
563+ attributes: backendAttrs(model),
564+ }),
565+ ),
533566
534567 updateMany: ({ model, where, update }) =>
535568 Effect.gen(function* () {
@@ -554,7 +587,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 18 unmodified lines
554587 model,
555588 );
556589 return n;
557− }),
590+ }).pipe(
591+ Effect.withSpan("executor.storage.backend.update_many", {
592+ attributes: backendAttrs(model),
593+ }),
594+ ),
558595
559596 delete: ({ model, where }) =>
560597 Effect.gen(function* () {
@@ -575,7 +612,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 14 unmodified lines
575612 () => Promise.resolve(db.delete(table).where(eq(table.id, first.id))),
576613 model,
577614 );
578− }),
615+ }).pipe(
616+ Effect.withSpan("executor.storage.backend.delete", {
617+ attributes: backendAttrs(model),
618+ }),
619+ ),
579620
580621 deleteMany: ({ model, where }) =>
581622 Effect.gen(function* () {
@@ -598,7 +639,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 16 unmodified lines
598639 model,
599640 );
600641 return n;
601− }),
642+ }).pipe(
643+ Effect.withSpan("executor.storage.backend.delete_many", {
644+ attributes: backendAttrs(model),
645+ }),
646+ ),
602647 };
603648
604649 // Transaction strategy differs by dialect:
@@ -659,6 +704,13 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 54 unmodified lines
659704 if (e instanceof TxFailure) return e.inner;
660705 return classifyError("pg transaction", undefined, e);
661706 }),
707+ Effect.withSpan("executor.storage.backend.transaction", {
708+ attributes: {
709+ "executor.storage.backend": "drizzle",
710+ "executor.storage.drizzle.provider": provider,
711+ "executor.storage.transaction.strategy": "drizzle_native",
712+ },
713+ }),
662714 ) as Effect.Effect<R, E | StorageFailure>;
663715 }
664716
@@ -711,7 +763,15 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 46 unmodified lines
711763 });
712764 }
713765 return result;
714− });
766+ }).pipe(
767+ Effect.withSpan("executor.storage.backend.transaction", {
768+ attributes: {
769+ "executor.storage.backend": "drizzle",
770+ "executor.storage.drizzle.provider": provider,
771+ "executor.storage.transaction.strategy": "raw_begin_commit",
772+ },
773+ }),
774+ );
715775 }
716776 : undefined;
717777
packages/hosts/mcp/src/server.ts+81−40
@@ -195,7 +195,11 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 194 unmodified lines
195195 ): Effect.Effect<McpServer> =>
196196 Effect.gen(function* () {
197197 const engine = "engine" in config ? config.engine : createExecutionEngine(config);
198− const description = config.description ?? (yield* engine.getDescription);
198+ const description =
199+ config.description ??
200+ (yield* engine.getDescription.pipe(
201+ Effect.withSpan("mcp.host.get_description"),
202+ ));
199203
200204 // Captured at construction time. SDK callbacks fire later (often
201205 // deferred past the outer Effect's await), so we use the runtime to
@@ -211,10 +215,16 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 9 unmodified lines
211215 return parent ? Effect.withParentSpan(effect, parent) : effect;
212216 };
213217
214− const server = new McpServer(
215− { name: "executor", version: "1.0.0" },
216− { capabilities: { tools: {} }, jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },
217− );
218+ const server = yield* Effect.sync(
219+ () =>
220+ new McpServer(
221+ { name: "executor", version: "1.0.0" },
222+ {
223+ capabilities: { tools: {} },
224+ jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
225+ },
226+ ),
227+ ).pipe(Effect.withSpan("mcp.host.create_server"));
218228
219229 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>
220230 Effect.gen(function* () {
@@ -228,7 +238,14 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
228238 return outcome.status === "completed"
229239 ? toMcpResult(formatExecuteResult(outcome.result))
230240 : toMcpPausedResult(formatPausedExecution(outcome.execution));
231− });
241+ }).pipe(
242+ Effect.withSpan("mcp.host.tool.execute", {
243+ attributes: {
244+ "mcp.tool.name": "execute",
245+ "mcp.execute.code_length": code.length,
246+ },
247+ }),
248+ );
232249
233250 const resumeExecution = (
234251 executionId: string,
@@ -239,48 +256,70 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 4 unmodified lines
239256 const outcome = yield* engine.resume(executionId, { action, content });
240257 if (!outcome) {
241258 return {
242− content: [{ type: "text", text: `No paused execution: ${executionId}` }],
259+ content: [
260+ { type: "text" as const, text: `No paused execution: ${executionId}` },
261+ ],
243262 isError: true,
244− };
263+ } satisfies McpToolResult;
245264 }
246265 return outcome.status === "completed"
247266 ? toMcpResult(formatExecuteResult(outcome.result))
248267 : toMcpPausedResult(formatPausedExecution(outcome.execution));
249− });
268+ }).pipe(
269+ Effect.withSpan("mcp.host.tool.resume", {
270+ attributes: {
271+ "mcp.tool.name": "resume",
272+ "mcp.execute.resume.action": action,
273+ "mcp.execute.execution_id": executionId,
274+ },
275+ }),
276+ );
250277
251278 // --- tools ---
252279
253− const executeTool = server.registerTool(
254− "execute",
255− {
256− description,
257− inputSchema: { code: z.string().trim().min(1) },
258− },
259− ({ code }) => Runtime.runPromise(runtime)(anchor(executeCode(code))),
280+ const executeTool = yield* Effect.sync(() =>
281+ server.registerTool(
282+ "execute",
283+ {
284+ description,
285+ inputSchema: { code: z.string().trim().min(1) },
286+ },
287+ ({ code }) => Runtime.runPromise(runtime)(anchor(executeCode(code))),
288+ ),
289+ ).pipe(
290+ Effect.withSpan("mcp.host.register_tool", {
291+ attributes: { "mcp.tool.name": "execute" },
292+ }),
260293 );
261294
262− const resumeTool = server.registerTool(
263− "resume",
264− {
265− description: [
266− "Resume a paused execution using the executionId returned by execute.",
267− "Never call this without user approval unless they explicitly state otherwise.",
268− ].join("\n"),
269− inputSchema: {
270− executionId: z.string().describe("The execution ID from the paused result"),
271− action: z
272− .enum(["accept", "decline", "cancel"])
273− .describe("How to respond to the interaction"),
274− content: z
275− .string()
276− .describe("Optional JSON-encoded response content for form elicitations")
277− .default("{}"),
295+ const resumeTool = yield* Effect.sync(() =>
296+ server.registerTool(
297+ "resume",
298+ {
299+ description: [
300+ "Resume a paused execution using the executionId returned by execute.",
301+ "Never call this without user approval unless they explicitly state otherwise.",
302+ ].join("\n"),
303+ inputSchema: {
304+ executionId: z.string().describe("The execution ID from the paused result"),
305+ action: z
306+ .enum(["accept", "decline", "cancel"])
307+ .describe("How to respond to the interaction"),
308+ content: z
309+ .string()
310+ .describe("Optional JSON-encoded response content for form elicitations")
311+ .default("{}"),
312+ },
278313 },
279− },
280− ({ executionId, action, content: rawContent }) =>
281− Runtime.runPromise(runtime)(
282− anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),
283− ),
314+ ({ executionId, action, content: rawContent }) =>
315+ Runtime.runPromise(runtime)(
316+ anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),
317+ ),
318+ ),
319+ ).pipe(
320+ Effect.withSpan("mcp.host.register_tool", {
321+ attributes: { "mcp.tool.name": "resume" },
322+ }),
284323 );
285324
286325 // --- capability-based tool visibility ---
@@ -294,8 +333,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
294333 }
295334 };
296335
297− syncToolAvailability();
298− server.server.oninitialized = syncToolAvailability;
336+ yield* Effect.sync(() => {
337+ syncToolAvailability();
338+ server.server.oninitialized = syncToolAvailability;
339+ }).pipe(Effect.withSpan("mcp.host.sync_tool_availability"));
299340
300341 return server;
301− });
342+ }).pipe(Effect.withSpan("mcp.host.create_executor_server"));
packages/plugins/graphql/src/sdk/invoke.ts+38−4
@@ -15,9 +15,13 @@ import { · 14 unmodified lines
1515 export const resolveHeaders = (
1616 headers: Record<string, HeaderValue>,
1717 secrets: { readonly get: (id: string) => Effect.Effect<string | null, unknown> },
18−): Effect.Effect<Record<string, string>> =>
19− Effect.gen(function* () {
20− const entries = Object.entries(headers);
18+): Effect.Effect<Record<string, string>> => {
19+ const entries = Object.entries(headers);
20+ const secretCount = entries.reduce(
21+ (acc, [, value]) => (typeof value === "string" ? acc : acc + 1),
22+ 0,
23+ );
24+ return Effect.gen(function* () {
2125 // Resolve secret-backed headers in parallel. Missing / failing
2226 // lookups drop the header rather than fail the invocation, same
2327 // as the serial version.
@@ -48,7 +52,15 @@ export const resolveHeaders = ( · 24 unmodified lines
4852 if (value !== null) resolved[name] = value;
4953 }
5054 return resolved;
51− });
55+ }).pipe(
56+ Effect.withSpan("plugin.graphql.secret.resolve", {
57+ attributes: {
58+ "plugin.graphql.headers.total": entries.length,
59+ "plugin.graphql.headers.secret_count": secretCount,
60+ },
61+ }),
62+ );
63+};
5264
5365 // ---------------------------------------------------------------------------
5466 // Response helpers
@@ -74,6 +86,15 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 19 unmodified lines
7486 ) {
7587 const client = yield* HttpClient.HttpClient;
7688
89+ yield* Effect.annotateCurrentSpan({
90+ "http.method": "POST",
91+ "http.url": endpoint,
92+ "plugin.graphql.endpoint": endpoint,
93+ "plugin.graphql.operation_kind": operation.kind,
94+ "plugin.graphql.field_name": operation.fieldName,
95+ "plugin.graphql.headers.resolved_count": Object.keys(resolvedHeaders).length,
96+ });
97+
7798 // Build the GraphQL request body
7899 const variables: Record<string, unknown> = {};
79100 for (const varName of operation.variableNames) {
@@ -121,6 +142,12 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 41 unmodified lines
121142 const gqlBody = body as { data?: unknown; errors?: unknown[] } | null;
122143 const hasErrors = Array.isArray(gqlBody?.errors) && gqlBody.errors.length > 0;
123144
145+ yield* Effect.annotateCurrentSpan({
146+ "http.status_code": status,
147+ "plugin.graphql.has_errors": hasErrors,
148+ "plugin.graphql.error_count": hasErrors ? gqlBody!.errors!.length : 0,
149+ });
150+
124151 return new InvocationResult({
125152 status,
126153 data: gqlBody?.data ?? null,
@@ -150,4 +177,11 @@ export const invokeWithLayer = ( · 23 unmodified lines
150177 cause: err,
151178 }),
152179 ),
180+ Effect.withSpan("plugin.graphql.invoke", {
181+ attributes: {
182+ "plugin.graphql.endpoint": endpoint,
183+ "plugin.graphql.operation_kind": operation.kind,
184+ "plugin.graphql.field_name": operation.fieldName,
185+ },
186+ }),
153187 );
packages/plugins/mcp/src/sdk/connection.ts+5−1
@@ -79,7 +79,11 @@ const connectClient = (input: { · 78 unmodified lines
7979 cause instanceof Error ? cause.message : String(cause)
8080 }`,
8181 }),
82− });
82+ }).pipe(
83+ Effect.withSpan("plugin.mcp.connection.handshake", {
84+ attributes: { "plugin.mcp.transport": input.transport },
85+ }),
86+ );
8387
8488 return connectionFromClient(client);
8589 });
packages/plugins/mcp/src/sdk/invoke.ts+40−6
@@ -136,7 +136,11 @@ const useConnection = ( · 135 unmodified lines
136136 cause instanceof Error ? cause.message : String(cause)
137137 }`,
138138 }),
139− });
139+ }).pipe(
140+ Effect.withSpan("plugin.mcp.client.call_tool", {
141+ attributes: { "mcp.tool.name": toolName },
142+ }),
143+ );
140144 });
141145
142146 // ---------------------------------------------------------------------------
@@ -163,8 +167,12 @@ export interface InvokeMcpToolInput { · 20 unmodified lines
163167
164168 export const invokeMcpTool = (
165169 input: InvokeMcpToolInput,
166−): Effect.Effect<unknown, McpConnectionError | McpInvocationError> =>
167− Effect.gen(function* () {
170+): Effect.Effect<unknown, McpConnectionError | McpInvocationError> => {
171+ const transport: string =
172+ input.sourceData.transport === "stdio"
173+ ? "stdio"
174+ : (input.sourceData.remoteTransport ?? "auto");
175+ return Effect.gen(function* () {
168176 const cacheKey = connectionCacheKey(input.sourceData);
169177 const args = asRecord(input.args);
170178
@@ -173,7 +181,15 @@ export const invokeMcpTool = ( · 2 unmodified lines
173181 const connector = input.resolveConnector();
174182 input.pendingConnectors.set(cacheKey, connector);
175183
176− const firstConnection = yield* input.connectionCache.get(cacheKey);
184+ const firstConnection = yield* input.connectionCache.get(cacheKey).pipe(
185+ Effect.withSpan("plugin.mcp.connection.acquire", {
186+ attributes: {
187+ "plugin.mcp.transport": transport,
188+ "plugin.mcp.cache_key": cacheKey,
189+ "plugin.mcp.attempt": 1,
190+ },
191+ }),
192+ );
177193
178194 return yield* useConnection(
179195 firstConnection,
@@ -194,7 +210,25 @@ export const invokeMcpTool = ( · 14 unmodified lines
194210 args,
195211 input.elicit,
196212 );
197− }),
213+ }).pipe(
214+ Effect.withSpan("plugin.mcp.invoke.retry", {
215+ attributes: {
216+ "plugin.mcp.transport": transport,
217+ "plugin.mcp.cache_key": cacheKey,
218+ "mcp.tool.name": input.toolName,
219+ },
220+ }),
221+ ),
198222 ),
199223 );
200− }).pipe(Effect.scoped);
224+ }).pipe(
225+ Effect.scoped,
226+ Effect.withSpan("plugin.mcp.invoke", {
227+ attributes: {
228+ "mcp.tool.name": input.toolName,
229+ "plugin.mcp.tool_id": input.toolId,
230+ "plugin.mcp.transport": transport,
231+ },
232+ }),
233+ );
234+};
packages/plugins/mcp/src/sdk/plugin.ts+210−103
@@ -445,6 +445,7 @@ export const mcpPlugin = definePlugin( · 444 unmodified lines
445445 Effect.catchAll(() =>
446446 Effect.succeed({ ok: false as const, manifest: null }),
447447 ),
448+ Effect.withSpan("mcp.plugin.discover_tools"),
448449 );
449450
450451 if (result.ok && result.manifest) {
@@ -465,6 +466,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
465466 }).pipe(
466467 Effect.map(() => true),
467468 Effect.catchAll(() => Effect.succeed(false)),
469+ Effect.withSpan("mcp.plugin.probe_oauth"),
468470 );
469471
470472 if (hasOAuth) {
@@ -483,7 +485,11 @@ export const mcpPlugin = definePlugin( · 12 unmodified lines
483485 "Could not connect to MCP endpoint and no OAuth was detected",
484486 ),
485487 );
486− });
488+ }).pipe(
489+ Effect.withSpan("mcp.plugin.probe_endpoint", {
490+ attributes: { "mcp.endpoint": endpoint },
491+ }),
492+ );
487493
488494 const configFile = options?.configFile;
489495
@@ -493,7 +499,14 @@ export const mcpPlugin = definePlugin( · 3 unmodified lines
493499 const sd = toStoredSourceData(config);
494500
495501 // Resolve auth (may fail if stdio gate is off)
496− const ci = yield* resolveConnectorInput(sd, ctx, allowStdio);
502+ const ci = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
503+ Effect.withSpan("mcp.plugin.resolve_connector", {
504+ attributes: {
505+ "mcp.source.namespace": namespace,
506+ "mcp.source.transport": sd.transport,
507+ },
508+ }),
509+ );
497510
498511 const connector = createMcpConnector(ci);
499512 // Try discovery. If it fails (auth, network, bad spec), we still
@@ -505,6 +518,9 @@ export const mcpPlugin = definePlugin( · 5 unmodified lines
505518 mcpDiscoveryError(`MCP discovery failed: ${err.message}`),
506519 ),
507520 Effect.either,
521+ Effect.withSpan("mcp.plugin.discover_tools", {
522+ attributes: { "mcp.source.namespace": namespace },
523+ }),
508524 );
509525 const manifest =
510526 discovery._tag === "Right"
@@ -513,73 +529,101 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
513529
514530 const sourceName = manifest.server?.name ?? config.name ?? namespace;
515531
516− yield* ctx.transaction(
517− Effect.gen(function* () {
518− // Remove stale rows for this namespace (plugin-owned)
519− yield* ctx.storage.removeBindingsByNamespace(namespace);
520− yield* ctx.storage.removeSource(namespace);
521−
522− yield* ctx.storage.putSource({
523− namespace,
524− name: sourceName,
525− config: sd,
526− });
527−
528− yield* ctx.storage.putBindings(
529− namespace,
530− manifest.tools.map((e) => ({
531− toolId: `${namespace}.${e.toolId}`,
532− binding: toBinding(e),
533− })),
534− );
535−
536− yield* ctx.core.sources.register({
537− id: namespace,
538− kind: "mcp",
539− name: sourceName,
540− url: sd.transport === "remote" ? sd.endpoint : undefined,
541− canRemove: true,
542− canRefresh: true,
543− canEdit: sd.transport === "remote",
544− tools: manifest.tools.map((e) => ({
545− name: e.toolId,
546− description: e.description ?? `MCP tool: ${e.toolName}`,
547− inputSchema: e.inputSchema,
548− outputSchema: e.outputSchema,
549− })),
550− });
551− }),
552− );
532+ yield* ctx
533+ .transaction(
534+ Effect.gen(function* () {
535+ // Remove stale rows for this namespace (plugin-owned)
536+ yield* ctx.storage.removeBindingsByNamespace(namespace);
537+ yield* ctx.storage.removeSource(namespace);
538+
539+ yield* ctx.storage.putSource({
540+ namespace,
541+ name: sourceName,
542+ config: sd,
543+ });
544+
545+ yield* ctx.storage.putBindings(
546+ namespace,
547+ manifest.tools.map((e) => ({
548+ toolId: `${namespace}.${e.toolId}`,
549+ binding: toBinding(e),
550+ })),
551+ );
552+
553+ yield* ctx.core.sources.register({
554+ id: namespace,
555+ kind: "mcp",
556+ name: sourceName,
557+ url: sd.transport === "remote" ? sd.endpoint : undefined,
558+ canRemove: true,
559+ canRefresh: true,
560+ canEdit: sd.transport === "remote",
561+ tools: manifest.tools.map((e) => ({
562+ name: e.toolId,
563+ description: e.description ?? `MCP tool: ${e.toolName}`,
564+ inputSchema: e.inputSchema,
565+ outputSchema: e.outputSchema,
566+ })),
567+ });
568+ }),
569+ )
570+ .pipe(
571+ Effect.withSpan("mcp.plugin.persist_source", {
572+ attributes: {
573+ "mcp.source.namespace": namespace,
574+ "mcp.source.tool_count": manifest.tools.length,
575+ },
576+ }),
577+ );
553578
554579 if (configFile) {
555− yield* configFile.upsertSource(
556− toMcpConfigEntry(namespace, sourceName, config),
557− );
580+ yield* configFile
581+ .upsertSource(toMcpConfigEntry(namespace, sourceName, config))
582+ .pipe(Effect.withSpan("mcp.plugin.config_file.upsert"));
558583 }
559584
560585 if (discovery._tag === "Left") {
561586 return yield* Effect.fail(discovery.left);
562587 }
563588 return { toolCount: manifest.tools.length, namespace };
564− });
589+ }).pipe(
590+ Effect.withSpan("mcp.plugin.add_source", {
591+ attributes: {
592+ "mcp.source.transport": config.transport,
593+ "mcp.source.name": config.name,
594+ },
595+ }),
596+ );
565597
566598 const removeSource = (namespace: string) =>
567599 Effect.gen(function* () {
568− yield* ctx.transaction(
569− Effect.gen(function* () {
570− yield* ctx.storage.removeBindingsByNamespace(namespace);
571− yield* ctx.storage.removeSource(namespace);
572− yield* ctx.core.sources.unregister(namespace);
573− }),
574− );
600+ yield* ctx
601+ .transaction(
602+ Effect.gen(function* () {
603+ yield* ctx.storage.removeBindingsByNamespace(namespace);
604+ yield* ctx.storage.removeSource(namespace);
605+ yield* ctx.core.sources.unregister(namespace);
606+ }),
607+ )
608+ .pipe(Effect.withSpan("mcp.plugin.persist_remove"));
575609 if (configFile) {
576− yield* configFile.removeSource(namespace);
610+ yield* configFile
611+ .removeSource(namespace)
612+ .pipe(Effect.withSpan("mcp.plugin.config_file.remove"));
577613 }
578− });
614+ }).pipe(
615+ Effect.withSpan("mcp.plugin.remove_source", {
616+ attributes: { "mcp.source.namespace": namespace },
617+ }),
618+ );
579619
580620 const refreshSource = (namespace: string) =>
581621 Effect.gen(function* () {
582− const sd = yield* ctx.storage.getSourceConfig(namespace);
622+ const sd = yield* ctx.storage.getSourceConfig(namespace).pipe(
623+ Effect.withSpan("mcp.plugin.load_source_config", {
624+ attributes: { "mcp.source.namespace": namespace },
625+ }),
626+ );
583627 if (!sd) {
584628 return yield* Effect.fail(
585629 remoteConnectionError(
@@ -588,49 +632,72 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
588632 );
589633 }
590634
591− const ci = yield* resolveConnectorInput(sd, ctx, allowStdio);
635+ const ci = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
636+ Effect.withSpan("mcp.plugin.resolve_connector", {
637+ attributes: {
638+ "mcp.source.namespace": namespace,
639+ "mcp.source.transport": sd.transport,
640+ },
641+ }),
642+ );
592643 const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(
593644 Effect.mapError((err) =>
594645 mcpDiscoveryError(`MCP refresh failed: ${err.message}`),
595646 ),
647+ Effect.withSpan("mcp.plugin.discover_tools", {
648+ attributes: { "mcp.source.namespace": namespace },
649+ }),
596650 );
597651
598652 const existing = yield* ctx.storage.getSource(namespace);
599653 const sourceName =
600654 manifest.server?.name ?? existing?.name ?? namespace;
601655
602− yield* ctx.transaction(
603− Effect.gen(function* () {
604− yield* ctx.storage.removeBindingsByNamespace(namespace);
605− yield* ctx.core.sources.unregister(namespace);
606−
607− yield* ctx.storage.putBindings(
608− namespace,
609− manifest.tools.map((e) => ({
610− toolId: `${namespace}.${e.toolId}`,
611− binding: toBinding(e),
612− })),
613− );
614− yield* ctx.core.sources.register({
615− id: namespace,
616− kind: "mcp",
617− name: sourceName,
618− url: sd.transport === "remote" ? sd.endpoint : undefined,
619− canRemove: true,
620− canRefresh: true,
621− canEdit: sd.transport === "remote",
622− tools: manifest.tools.map((e) => ({
623− name: e.toolId,
624− description: e.description ?? `MCP tool: ${e.toolName}`,
625− inputSchema: e.inputSchema,
626− outputSchema: e.outputSchema,
627− })),
628− });
629− }),
630− );
656+ yield* ctx
657+ .transaction(
658+ Effect.gen(function* () {
659+ yield* ctx.storage.removeBindingsByNamespace(namespace);
660+ yield* ctx.core.sources.unregister(namespace);
661+
662+ yield* ctx.storage.putBindings(
663+ namespace,
664+ manifest.tools.map((e) => ({
665+ toolId: `${namespace}.${e.toolId}`,
666+ binding: toBinding(e),
667+ })),
668+ );
669+ yield* ctx.core.sources.register({
670+ id: namespace,
671+ kind: "mcp",
672+ name: sourceName,
673+ url: sd.transport === "remote" ? sd.endpoint : undefined,
674+ canRemove: true,
675+ canRefresh: true,
676+ canEdit: sd.transport === "remote",
677+ tools: manifest.tools.map((e) => ({
678+ name: e.toolId,
679+ description: e.description ?? `MCP tool: ${e.toolName}`,
680+ inputSchema: e.inputSchema,
681+ outputSchema: e.outputSchema,
682+ })),
683+ });
684+ }),
685+ )
686+ .pipe(
687+ Effect.withSpan("mcp.plugin.persist_source", {
688+ attributes: {
689+ "mcp.source.namespace": namespace,
690+ "mcp.source.tool_count": manifest.tools.length,
691+ },
692+ }),
693+ );
631694
632695 return { toolCount: manifest.tools.length };
633− });
696+ }).pipe(
697+ Effect.withSpan("mcp.plugin.refresh_source", {
698+ attributes: { "mcp.source.namespace": namespace },
699+ }),
700+ );
634701
635702 const startOAuth = (input: McpOAuthStartInput) =>
636703 Effect.gen(function* () {
@@ -659,24 +726,27 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
659726 Effect.mapError((e) =>
660727 mcpOAuthError(`OAuth start failed: ${e.message}`),
661728 ),
729+ Effect.withSpan("mcp.plugin.oauth.start_authorization"),
662730 );
663731
664− yield* ctx.storage.putOAuthSession(sessionId, {
665− endpoint: fullEndpoint,
666− redirectUrl: input.redirectUrl,
667− codeVerifier: started.codeVerifier,
668− resourceMetadataUrl: started.resourceMetadataUrl,
669− authorizationServerUrl: started.authorizationServerUrl,
670− resourceMetadata: started.resourceMetadata,
671− authorizationServerMetadata: started.authorizationServerMetadata,
672− clientInformation: started.clientInformation,
673− });
732+ yield* ctx.storage
733+ .putOAuthSession(sessionId, {
734+ endpoint: fullEndpoint,
735+ redirectUrl: input.redirectUrl,
736+ codeVerifier: started.codeVerifier,
737+ resourceMetadataUrl: started.resourceMetadataUrl,
738+ authorizationServerUrl: started.authorizationServerUrl,
739+ resourceMetadata: started.resourceMetadata,
740+ authorizationServerMetadata: started.authorizationServerMetadata,
741+ clientInformation: started.clientInformation,
742+ })
743+ .pipe(Effect.withSpan("mcp.plugin.oauth.persist_session"));
674744
675745 return {
676746 sessionId,
677747 authorizationUrl: started.authorizationUrl,
678748 };
679− });
749+ }).pipe(Effect.withSpan("mcp.plugin.start_oauth"));
680750
681751 const completeOAuth = (input: McpOAuthCompleteInput) =>
682752 Effect.gen(function* () {
@@ -705,6 +775,7 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
705775 Effect.mapError((e) =>
706776 mcpOAuthError(`OAuth exchange failed: ${e.message}`),
707777 ),
778+ Effect.withSpan("mcp.plugin.oauth.exchange_code"),
708779 );
709780
710781 const accessSecretId = `mcp-oauth-access-${input.state}`;
@@ -757,7 +828,7 @@ export const mcpPlugin = definePlugin( · 46 unmodified lines
757828 expiresAt,
758829 scope: exchanged.tokens.scope ?? null,
759830 };
760− });
831+ }).pipe(Effect.withSpan("mcp.plugin.complete_oauth"));
761832
762833 const updateSource = (
763834 namespace: string,
@@ -783,10 +854,18 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
783854 name: input.name?.trim() || existing.name,
784855 config: updatedConfig,
785856 });
786− });
857+ }).pipe(
858+ Effect.withSpan("mcp.plugin.update_source", {
859+ attributes: { "mcp.source.namespace": namespace },
860+ }),
861+ );
787862
788863 const getSource = (namespace: string) =>
789− ctx.storage.getSource(namespace);
864+ ctx.storage.getSource(namespace).pipe(
865+ Effect.withSpan("mcp.plugin.get_source", {
866+ attributes: { "mcp.source.namespace": namespace },
867+ }),
868+ );
790869
791870 return {
792871 probeEndpoint,
@@ -804,14 +883,22 @@ export const mcpPlugin = definePlugin( · 11 unmodified lines
804883 Effect.gen(function* () {
805884 const runtime = yield* ensureRuntime();
806885
807− const entry = yield* ctx.storage.getBinding(toolRow.id);
886+ const entry = yield* ctx.storage.getBinding(toolRow.id).pipe(
887+ Effect.withSpan("mcp.plugin.load_binding", {
888+ attributes: { "mcp.tool.name": toolRow.id },
889+ }),
890+ );
808891 if (!entry) {
809892 return yield* Effect.fail(
810893 new Error(`No MCP binding found for tool "${toolRow.id}"`),
811894 );
812895 }
813896
814− const sd = yield* ctx.storage.getSourceConfig(entry.namespace);
897+ const sd = yield* ctx.storage.getSourceConfig(entry.namespace).pipe(
898+ Effect.withSpan("mcp.plugin.load_source_config", {
899+ attributes: { "mcp.source.namespace": entry.namespace },
900+ }),
901+ );
815902 if (!sd) {
816903 return yield* Effect.fail(
817904 new Error(
@@ -837,12 +924,25 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
837924 err instanceof Error ? err.message : String(err),
838925 }),
839926 ),
927+ Effect.withSpan("mcp.plugin.resolve_connector", {
928+ attributes: {
929+ "mcp.source.namespace": entry.namespace,
930+ "mcp.source.transport": sd.transport,
931+ },
932+ }),
840933 ),
841934 connectionCache: runtime.connectionCache,
842935 pendingConnectors: runtime.pendingConnectors,
843936 elicit,
844937 });
845− }),
938+ }).pipe(
939+ Effect.withSpan("mcp.plugin.invoke_tool", {
940+ attributes: {
941+ "mcp.tool.name": toolRow.id,
942+ "mcp.tool.source_id": toolRow.source_id,
943+ },
944+ }),
945+ ),
846946
847947 detect: ({ url }) =>
848948 Effect.gen(function* () {
@@ -865,6 +965,7 @@ export const mcpPlugin = definePlugin( · 16 unmodified lines
865965 const connected = yield* discoverTools(connector).pipe(
866966 Effect.map(() => true),
867967 Effect.catchAll(() => Effect.succeed(false)),
968+ Effect.withSpan("mcp.plugin.discover_tools"),
868969 );
869970
870971 if (connected) {
@@ -885,6 +986,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
885986 }).pipe(
886987 Effect.map(() => true),
887988 Effect.catchAll(() => Effect.succeed(false)),
989+ Effect.withSpan("mcp.plugin.probe_oauth"),
888990 );
889991
890992 if (hasOAuth) {
@@ -898,7 +1000,12 @@ export const mcpPlugin = definePlugin( · 7 unmodified lines
8981000 }
8991001
9001002 return null;
901− }).pipe(Effect.catchAll(() => Effect.succeed(null))),
1003+ }).pipe(
1004+ Effect.catchAll(() => Effect.succeed(null)),
1005+ Effect.withSpan("mcp.plugin.detect", {
1006+ attributes: { "mcp.endpoint": url },
1007+ }),
1008+ ),
9021009
9031010 // MCP tools never require approval at the tool level — elicitation is
9041011 // handled mid-invocation by the server via the elicit capability.
@@ -928,7 +1035,7 @@ export const mcpPlugin = definePlugin( · 23 unmodified lines
9281035 yield* Scope.close(runtime.cacheScope, Exit.void);
9291036 runtimeRef.current = null;
9301037 }
931− }),
1038+ }).pipe(Effect.withSpan("mcp.plugin.close")),
9321039 };
9331040 },
9341041 );
packages/plugins/openapi/src/sdk/invoke.ts+37−5
@@ -102,9 +102,13 @@ export const resolveHeaders = ( · 101 unmodified lines
102102 ): Effect.Effect<
103103 Record<string, string>,
104104 OpenApiInvocationError | StorageFailure
105−> =>
106− Effect.gen(function* () {
107− const entries = Object.entries(headers);
105+> => {
106+ const entries = Object.entries(headers);
107+ const secretCount = entries.reduce(
108+ (acc, [, value]) => (typeof value === "string" ? acc : acc + 1),
109+ 0,
110+ );
111+ return Effect.gen(function* () {
108112 // Fan out secret lookups: on every invocation, one or two headers
109113 // typically each hit the secret store. Resolving them in parallel
110114 // is a free wall-clock win — preserved order is only needed for
@@ -134,7 +138,15 @@ export const resolveHeaders = ( · 23 unmodified lines
134138 const resolved: Record<string, string> = {};
135139 for (const { name, value } of values) resolved[name] = value;
136140 return resolved;
137− });
141+ }).pipe(
142+ Effect.withSpan("plugin.openapi.secret.resolve", {
143+ attributes: {
144+ "plugin.openapi.headers.total": entries.length,
145+ "plugin.openapi.headers.secret_count": secretCount,
146+ },
147+ }),
148+ );
149+};
138150
139151 const applyHeaders = (
140152 request: HttpClientRequest.HttpClientRequest,
@@ -170,6 +182,14 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 29 unmodified lines
170182 ) {
171183 const client = yield* HttpClient.HttpClient;
172184
185+ yield* Effect.annotateCurrentSpan({
186+ "http.method": operation.method.toUpperCase(),
187+ "http.route": operation.pathTemplate,
188+ "plugin.openapi.method": operation.method.toUpperCase(),
189+ "plugin.openapi.path_template": operation.pathTemplate,
190+ "plugin.openapi.headers.resolved_count": Object.keys(resolvedHeaders).length,
191+ });
192+
173193 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);
174194
175195 const path = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
@@ -216,6 +236,9 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 40 unmodified lines
216236 );
217237
218238 const status = response.status;
239+ yield* Effect.annotateCurrentSpan({
240+ "http.status_code": status,
241+ });
219242 const responseHeaders: Record<string, string> = { ...response.headers };
220243
221244 const contentType = response.headers["content-type"] ?? null;
@@ -268,7 +291,16 @@ export const invokeWithLayer = ( · 46 unmodified lines
268291 ).pipe(Layer.provide(httpClientLayer))
269292 : httpClientLayer;
270293
271− return invoke(operation, args, resolvedHeaders).pipe(Effect.provide(clientWithBaseUrl));
294+ return invoke(operation, args, resolvedHeaders).pipe(
295+ Effect.provide(clientWithBaseUrl),
296+ Effect.withSpan("plugin.openapi.invoke", {
297+ attributes: {
298+ "plugin.openapi.method": operation.method.toUpperCase(),
299+ "plugin.openapi.path_template": operation.pathTemplate,
300+ "plugin.openapi.base_url": baseUrl,
301+ },
302+ }),
303+ );
272304 };
273305
274306 // ---------------------------------------------------------------------------