@frontmcp/observability 1.9.1 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.mjs CHANGED
@@ -332,7 +332,7 @@ import {
332
332
  AgentCallHook,
333
333
  DynamicPlugin,
334
334
  FlowHooksOf,
335
- FRONTMCP_CONTEXT,
335
+ FRONTMCP_CONTEXT as FRONTMCP_CONTEXT2,
336
336
  HttpHook,
337
337
  ListResourcesHook,
338
338
  ListResourceTemplatesHook,
@@ -704,6 +704,13 @@ var RequestLogCollector = class {
704
704
  this.status = status;
705
705
  this.statusCode = statusCode;
706
706
  }
707
+ /**
708
+ * Set the HTTP status the request was answered with; one of 400 or more marks it failed.
709
+ */
710
+ setStatusCode(statusCode) {
711
+ this.statusCode = statusCode;
712
+ if (statusCode >= 400) this.status = "error";
713
+ }
707
714
  /**
708
715
  * Set error details.
709
716
  */
@@ -772,15 +779,18 @@ var RequestLogCollector = class {
772
779
  }
773
780
  };
774
781
 
775
- // libs/observability/src/request-log/request-log.tokens.ts
776
- var REQUEST_LOG_COLLECTOR = /* @__PURE__ */ Symbol.for("frontmcp:observability:request-log-collector");
782
+ // libs/observability/src/request-log/request-log.hooks.ts
783
+ import { isAnonymousSubject } from "@frontmcp/sdk";
777
784
 
778
- // libs/observability/src/telemetry/telemetry.accessor.ts
785
+ // libs/observability/src/plugin/observability.hooks.ts
779
786
  import {
787
+ context as otelContext3,
780
788
  SpanKind as SpanKind12,
781
789
  SpanStatusCode as SpanStatusCode2,
782
- trace as trace4
790
+ trace as trace3
783
791
  } from "@opentelemetry/api";
792
+ import { FlowControl, InternalMcpError, toMcpError } from "@frontmcp/sdk";
793
+ import { sha256Hex } from "@frontmcp/utils";
784
794
 
785
795
  // libs/observability/src/otel/otel.types.ts
786
796
  var McpAttributes = {
@@ -895,43 +905,6 @@ var EnduserAttributes = {
895
905
  SCOPE: "enduser.scope"
896
906
  };
897
907
 
898
- // libs/observability/src/otel/trace-context-bridge.ts
899
- import {
900
- TraceFlags,
901
- trace,
902
- context as otelContext
903
- } from "@opentelemetry/api";
904
- function frontmcpToOTelSpanContext(tc) {
905
- return {
906
- traceId: tc.traceId,
907
- spanId: tc.parentId,
908
- traceFlags: tc.traceFlags & TraceFlags.SAMPLED ? TraceFlags.SAMPLED : TraceFlags.NONE,
909
- isRemote: true
910
- };
911
- }
912
- function otelToFrontmcpContext(sc) {
913
- const flags = sc.traceFlags.toString(16).padStart(2, "0");
914
- return {
915
- traceId: sc.traceId,
916
- parentId: sc.spanId,
917
- traceFlags: sc.traceFlags,
918
- raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
919
- };
920
- }
921
- function createOTelContextFromTrace(tc) {
922
- const spanContext = frontmcpToOTelSpanContext(tc);
923
- const span = trace.wrapSpanContext(spanContext);
924
- return trace.setSpan(otelContext.active(), span);
925
- }
926
-
927
- // libs/observability/src/plugin/observability.hooks.ts
928
- import {
929
- context as otelContext3,
930
- SpanKind as SpanKind11,
931
- trace as trace3
932
- } from "@opentelemetry/api";
933
- import { sha256Hex } from "@frontmcp/utils";
934
-
935
908
  // libs/observability/src/otel/spans/auth.span.ts
936
909
  import { SpanKind as SpanKind2 } from "@opentelemetry/api";
937
910
 
@@ -939,11 +912,11 @@ import { SpanKind as SpanKind2 } from "@opentelemetry/api";
939
912
  import {
940
913
  SpanKind,
941
914
  SpanStatusCode,
942
- context as otelContext2,
943
- trace as trace2
915
+ context as otelContext,
916
+ trace
944
917
  } from "@opentelemetry/api";
945
918
  function startSpan(tracer, options) {
946
- const parentCtx = options.parentContext ?? otelContext2.active();
919
+ const parentCtx = options.parentContext ?? otelContext.active();
947
920
  const span = tracer.startSpan(
948
921
  options.name,
949
922
  {
@@ -952,7 +925,7 @@ function startSpan(tracer, options) {
952
925
  },
953
926
  parentCtx
954
927
  );
955
- const spanContext = trace2.setSpan(parentCtx, span);
928
+ const spanContext = trace.setSpan(parentCtx, span);
956
929
  return { span, context: spanContext };
957
930
  }
958
931
  function endSpanOk(span) {
@@ -969,7 +942,7 @@ function endSpanError(span, error) {
969
942
  }
970
943
  async function withSpan(tracer, options, fn) {
971
944
  const { span, context: spanContext } = startSpan(tracer, options);
972
- return otelContext2.with(spanContext, async () => {
945
+ return otelContext.with(spanContext, async () => {
973
946
  try {
974
947
  const result = await fn(span);
975
948
  endSpanOk(span);
@@ -1016,8 +989,35 @@ function setFetchResponseStatus(span, statusCode) {
1016
989
  span.setAttribute(HttpAttributes.STATUS_CODE, statusCode);
1017
990
  }
1018
991
 
1019
- // libs/observability/src/otel/spans/http-server.span.ts
992
+ // libs/observability/src/otel/spans/hook.span.ts
1020
993
  import { SpanKind as SpanKind4 } from "@opentelemetry/api";
994
+ function recordHookEvent(parentSpan, stage, owner) {
995
+ const attributes = {};
996
+ if (owner) {
997
+ attributes[FrontMcpAttributes.HOOK_OWNER] = owner;
998
+ }
999
+ parentSpan.addEvent(`hook.${stage}`, attributes);
1000
+ }
1001
+ function startHookSpan(tracer, options) {
1002
+ const attributes = {
1003
+ [FrontMcpAttributes.HOOK_STAGE]: options.stage
1004
+ };
1005
+ if (options.owner) {
1006
+ attributes[FrontMcpAttributes.HOOK_OWNER] = options.owner;
1007
+ }
1008
+ if (options.flowName) {
1009
+ attributes[FrontMcpAttributes.FLOW_NAME] = options.flowName;
1010
+ }
1011
+ return startSpan(tracer, {
1012
+ name: `hook ${options.stage}`,
1013
+ kind: SpanKind4.INTERNAL,
1014
+ attributes,
1015
+ parentContext: options.parentContext
1016
+ });
1017
+ }
1018
+
1019
+ // libs/observability/src/otel/spans/http-server.span.ts
1020
+ import { SpanKind as SpanKind5 } from "@opentelemetry/api";
1021
1021
  function startHttpServerSpan(tracer, options) {
1022
1022
  const attributes = {
1023
1023
  [HttpAttributes.METHOD]: options.method.toUpperCase(),
@@ -1040,7 +1040,7 @@ function startHttpServerSpan(tracer, options) {
1040
1040
  }
1041
1041
  return startSpan(tracer, {
1042
1042
  name: `${options.method.toUpperCase()} ${options.path}`,
1043
- kind: SpanKind4.SERVER,
1043
+ kind: SpanKind5.SERVER,
1044
1044
  attributes,
1045
1045
  parentContext: options.parentContext
1046
1046
  });
@@ -1050,11 +1050,11 @@ function setHttpResponseStatus(span, statusCode) {
1050
1050
  }
1051
1051
 
1052
1052
  // libs/observability/src/otel/spans/prompt.span.ts
1053
- import { SpanKind as SpanKind5 } from "@opentelemetry/api";
1053
+ import { SpanKind as SpanKind6 } from "@opentelemetry/api";
1054
1054
  function startPromptSpan(tracer, options) {
1055
1055
  return startSpan(tracer, {
1056
1056
  name: `prompt ${options.name}`,
1057
- kind: SpanKind5.INTERNAL,
1057
+ kind: SpanKind6.INTERNAL,
1058
1058
  attributes: {
1059
1059
  [FrontMcpAttributes.PROMPT_NAME]: options.name,
1060
1060
  [McpAttributes.COMPONENT_TYPE]: "prompt",
@@ -1065,7 +1065,7 @@ function startPromptSpan(tracer, options) {
1065
1065
  }
1066
1066
 
1067
1067
  // libs/observability/src/otel/spans/resource.span.ts
1068
- import { SpanKind as SpanKind6 } from "@opentelemetry/api";
1068
+ import { SpanKind as SpanKind7 } from "@opentelemetry/api";
1069
1069
  function startResourceSpan(tracer, options) {
1070
1070
  const attributes = {
1071
1071
  [FrontMcpAttributes.RESOURCE_URI]: options.uri,
@@ -1078,14 +1078,14 @@ function startResourceSpan(tracer, options) {
1078
1078
  }
1079
1079
  return startSpan(tracer, {
1080
1080
  name: `resource ${options.uri}`,
1081
- kind: SpanKind6.INTERNAL,
1081
+ kind: SpanKind7.INTERNAL,
1082
1082
  attributes,
1083
1083
  parentContext: options.parentContext
1084
1084
  });
1085
1085
  }
1086
1086
 
1087
1087
  // libs/observability/src/otel/spans/rpc.span.ts
1088
- import { SpanKind as SpanKind7 } from "@opentelemetry/api";
1088
+ import { SpanKind as SpanKind8 } from "@opentelemetry/api";
1089
1089
  function startRpcSpan(tracer, options) {
1090
1090
  const attributes = {
1091
1091
  [RpcAttributes.SYSTEM]: "mcp",
@@ -1109,14 +1109,14 @@ function startRpcSpan(tracer, options) {
1109
1109
  }
1110
1110
  return startSpan(tracer, {
1111
1111
  name: options.method,
1112
- kind: SpanKind7.SERVER,
1112
+ kind: SpanKind8.SERVER,
1113
1113
  attributes,
1114
1114
  parentContext: options.parentContext
1115
1115
  });
1116
1116
  }
1117
1117
 
1118
1118
  // libs/observability/src/otel/spans/startup.span.ts
1119
- import { SpanKind as SpanKind8 } from "@opentelemetry/api";
1119
+ import { SpanKind as SpanKind9 } from "@opentelemetry/api";
1120
1120
  function emitStartupReport(tracer, data) {
1121
1121
  const attributes = {
1122
1122
  [FrontMcpAttributes.STARTUP_TOOLS_COUNT]: data.toolsCount,
@@ -1130,14 +1130,14 @@ function emitStartupReport(tracer, data) {
1130
1130
  }
1131
1131
  const { span } = startSpan(tracer, {
1132
1132
  name: "frontmcp.startup",
1133
- kind: SpanKind8.INTERNAL,
1133
+ kind: SpanKind9.INTERNAL,
1134
1134
  attributes
1135
1135
  });
1136
1136
  endSpanOk(span);
1137
1137
  }
1138
1138
 
1139
1139
  // libs/observability/src/otel/spans/tool.span.ts
1140
- import { SpanKind as SpanKind9 } from "@opentelemetry/api";
1140
+ import { SpanKind as SpanKind10 } from "@opentelemetry/api";
1141
1141
  function startToolSpan(tracer, options) {
1142
1142
  const attributes = {
1143
1143
  [FrontMcpAttributes.TOOL_NAME]: options.name,
@@ -1155,14 +1155,14 @@ function startToolSpan(tracer, options) {
1155
1155
  }
1156
1156
  return startSpan(tracer, {
1157
1157
  name: `tool ${options.name}`,
1158
- kind: SpanKind9.INTERNAL,
1158
+ kind: SpanKind10.INTERNAL,
1159
1159
  attributes,
1160
1160
  parentContext: options.parentContext
1161
1161
  });
1162
1162
  }
1163
1163
 
1164
1164
  // libs/observability/src/otel/spans/transport.span.ts
1165
- import { SpanKind as SpanKind10 } from "@opentelemetry/api";
1165
+ import { SpanKind as SpanKind11 } from "@opentelemetry/api";
1166
1166
  function startTransportSpan(tracer, options) {
1167
1167
  const attributes = {
1168
1168
  [FrontMcpAttributes.TRANSPORT_TYPE]: options.type
@@ -1175,7 +1175,7 @@ function startTransportSpan(tracer, options) {
1175
1175
  }
1176
1176
  return startSpan(tracer, {
1177
1177
  name: `transport ${options.type}`,
1178
- kind: SpanKind10.INTERNAL,
1178
+ kind: SpanKind11.INTERNAL,
1179
1179
  attributes,
1180
1180
  parentContext: options.parentContext
1181
1181
  });
@@ -1184,6 +1184,35 @@ function setTransportRequestType(span, requestType) {
1184
1184
  span.setAttribute(FrontMcpAttributes.TRANSPORT_REQUEST_TYPE, requestType);
1185
1185
  }
1186
1186
 
1187
+ // libs/observability/src/otel/trace-context-bridge.ts
1188
+ import {
1189
+ TraceFlags,
1190
+ trace as trace2,
1191
+ context as otelContext2
1192
+ } from "@opentelemetry/api";
1193
+ function frontmcpToOTelSpanContext(tc) {
1194
+ return {
1195
+ traceId: tc.traceId,
1196
+ spanId: tc.parentId,
1197
+ traceFlags: tc.traceFlags & TraceFlags.SAMPLED ? TraceFlags.SAMPLED : TraceFlags.NONE,
1198
+ isRemote: true
1199
+ };
1200
+ }
1201
+ function otelToFrontmcpContext(sc) {
1202
+ const flags = sc.traceFlags.toString(16).padStart(2, "0");
1203
+ return {
1204
+ traceId: sc.traceId,
1205
+ parentId: sc.spanId,
1206
+ traceFlags: sc.traceFlags,
1207
+ raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
1208
+ };
1209
+ }
1210
+ function createOTelContextFromTrace(tc) {
1211
+ const spanContext = frontmcpToOTelSpanContext(tc);
1212
+ const span = trace2.wrapSpanContext(spanContext);
1213
+ return trace2.setSpan(otelContext2.active(), span);
1214
+ }
1215
+
1187
1216
  // libs/observability/src/plugin/observability.hooks.ts
1188
1217
  var SPAN_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:span");
1189
1218
  var SPAN_CTX_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:span-ctx");
@@ -1191,6 +1220,8 @@ var EXEC_SPAN_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:exec-span");
1191
1220
  var ACTIVE_SPAN_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:active-span");
1192
1221
  var ACTIVE_OTEL_CTX_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:active-otel-ctx");
1193
1222
  var STAGE_TS_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:stage-ts");
1223
+ var STAGE_EVENTS_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:stage-events");
1224
+ var HTTP_SPAN_CTX_KEY = /* @__PURE__ */ Symbol.for("frontmcp:otel:http-span-ctx");
1194
1225
  function sessionTracingId(sessionId) {
1195
1226
  return sha256Hex(sessionId).slice(0, 16);
1196
1227
  }
@@ -1225,7 +1256,44 @@ function clearActiveSpan(flowCtx) {
1225
1256
  } catch {
1226
1257
  }
1227
1258
  }
1259
+ function beginFlowSpan(flowCtx, options, span, spanCtx) {
1260
+ flowCtx.state[SPAN_KEY] = span;
1261
+ flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1262
+ flowCtx.state[STAGE_TS_KEY] = Date.now();
1263
+ flowCtx.state[STAGE_EVENTS_KEY] = options.flowStageEvents !== false;
1264
+ }
1265
+ function parentContextOf(ctx) {
1266
+ return ctx.get?.(HTTP_SPAN_CTX_KEY) ?? createOTelContextFromTrace(ctx.traceContext);
1267
+ }
1268
+ function flowFailureOf(flowError) {
1269
+ const failed = flowError?.originalError;
1270
+ const error = flowError instanceof FlowControl ? failed instanceof Error ? failed : new InternalMcpError(`Flow ended with: ${flowError.type}`) : flowError instanceof Error ? flowError : new Error(String(flowError));
1271
+ const reported = toMcpError(error);
1272
+ return {
1273
+ error,
1274
+ type: reported.name,
1275
+ message: reported.getPublicMessage(),
1276
+ code: reported.code,
1277
+ errorId: reported.errorId
1278
+ };
1279
+ }
1280
+ function endFailedSpan(span, flowError) {
1281
+ const failure = flowFailureOf(flowError);
1282
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: failure.message });
1283
+ span.recordException({
1284
+ name: failure.type,
1285
+ message: failure.message,
1286
+ code: failure.code,
1287
+ stack: failure.error.stack
1288
+ });
1289
+ span.end();
1290
+ }
1291
+ function traceparentOf(span) {
1292
+ const { traceId, spanId, traceFlags } = span.spanContext();
1293
+ return `00-${traceId}-${spanId}-${traceFlags.toString(16).padStart(2, "0")}`;
1294
+ }
1228
1295
  function recordStageEvent(span, stageName, state) {
1296
+ if (state[STAGE_EVENTS_KEY] === false) return;
1229
1297
  const now = Date.now();
1230
1298
  const prev = state[STAGE_TS_KEY];
1231
1299
  const attributes = {};
@@ -1251,9 +1319,8 @@ function onHttpWillTrace(options, flowCtx) {
1251
1319
  });
1252
1320
  span.setAttribute(McpAttributes.SESSION_ID, sessionHash);
1253
1321
  span.setAttribute(FrontMcpAttributes.SESSION_ID_HASH, sessionHash);
1254
- flowCtx.state[SPAN_KEY] = span;
1255
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1256
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1322
+ ctx.set?.(HTTP_SPAN_CTX_KEY, spanCtx);
1323
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1257
1324
  recordStageEvent(span, "traceRequest", flowCtx.state);
1258
1325
  }
1259
1326
  function onHttpWillAcquireQuota(flowCtx) {
@@ -1297,7 +1364,7 @@ function onToolWillParse(options, flowCtx) {
1297
1364
  const ctx = extractContext(flowCtx);
1298
1365
  if (!ctx) return;
1299
1366
  const tracer = getTracer();
1300
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1367
+ const parentOTelCtx = parentContextOf(ctx);
1301
1368
  const sessionHash = sessionTracingId(ctx.sessionId);
1302
1369
  const { span, context: spanCtx } = startRpcSpan(tracer, {
1303
1370
  method: "tools/call",
@@ -1305,9 +1372,7 @@ function onToolWillParse(options, flowCtx) {
1305
1372
  sessionIdHash: sessionHash,
1306
1373
  parentContext: parentOTelCtx
1307
1374
  });
1308
- flowCtx.state[SPAN_KEY] = span;
1309
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1310
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1375
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1311
1376
  recordStageEvent(span, "parseInput", flowCtx.state);
1312
1377
  }
1313
1378
  function onToolWillFindTool(flowCtx) {
@@ -1375,10 +1440,10 @@ function onToolWillApplyUI(flowCtx) {
1375
1440
  function onToolDidFinalize(flowCtx) {
1376
1441
  const rpcSpan = flowCtx.state?.[SPAN_KEY];
1377
1442
  const toolSpan = flowCtx.state?.[EXEC_SPAN_KEY];
1378
- const error = flowCtx.state?.error;
1443
+ const error = flowCtx.state?.flowError;
1379
1444
  if (error) {
1380
- if (toolSpan) endSpanError(toolSpan, error instanceof Error ? error : String(error));
1381
- if (rpcSpan) endSpanError(rpcSpan, error instanceof Error ? error : String(error));
1445
+ if (toolSpan) endFailedSpan(toolSpan, error);
1446
+ if (rpcSpan) endFailedSpan(rpcSpan, error);
1382
1447
  } else if (rpcSpan) {
1383
1448
  recordStageEvent(rpcSpan, "finalize", flowCtx.state);
1384
1449
  endSpanOk(rpcSpan);
@@ -1389,7 +1454,7 @@ function onResourceWillParse(options, flowCtx) {
1389
1454
  const ctx = extractContext(flowCtx);
1390
1455
  if (!ctx) return;
1391
1456
  const tracer = getTracer();
1392
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1457
+ const parentOTelCtx = parentContextOf(ctx);
1393
1458
  const sessionHash = sessionTracingId(ctx.sessionId);
1394
1459
  const { span, context: spanCtx } = startRpcSpan(tracer, {
1395
1460
  method: "resources/read",
@@ -1397,9 +1462,7 @@ function onResourceWillParse(options, flowCtx) {
1397
1462
  sessionIdHash: sessionHash,
1398
1463
  parentContext: parentOTelCtx
1399
1464
  });
1400
- flowCtx.state[SPAN_KEY] = span;
1401
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1402
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1465
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1403
1466
  recordStageEvent(span, "parseInput", flowCtx.state);
1404
1467
  }
1405
1468
  function onResourceWillFind(flowCtx) {
@@ -1428,10 +1491,10 @@ function onResourceDidExecute(flowCtx) {
1428
1491
  function onResourceDidFinalize(flowCtx) {
1429
1492
  const rpcSpan = flowCtx.state?.[SPAN_KEY];
1430
1493
  const resSpan = flowCtx.state?.[EXEC_SPAN_KEY];
1431
- const error = flowCtx.state?.error;
1494
+ const error = flowCtx.state?.flowError;
1432
1495
  if (error) {
1433
- if (resSpan) endSpanError(resSpan, error instanceof Error ? error : String(error));
1434
- if (rpcSpan) endSpanError(rpcSpan, error instanceof Error ? error : String(error));
1496
+ if (resSpan) endFailedSpan(resSpan, error);
1497
+ if (rpcSpan) endFailedSpan(rpcSpan, error);
1435
1498
  } else if (rpcSpan) {
1436
1499
  recordStageEvent(rpcSpan, "finalize", flowCtx.state);
1437
1500
  endSpanOk(rpcSpan);
@@ -1442,7 +1505,7 @@ function onPromptWillParse(options, flowCtx) {
1442
1505
  const ctx = extractContext(flowCtx);
1443
1506
  if (!ctx) return;
1444
1507
  const tracer = getTracer();
1445
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1508
+ const parentOTelCtx = parentContextOf(ctx);
1446
1509
  const sessionHash = sessionTracingId(ctx.sessionId);
1447
1510
  const { span, context: spanCtx } = startRpcSpan(tracer, {
1448
1511
  method: "prompts/get",
@@ -1450,9 +1513,7 @@ function onPromptWillParse(options, flowCtx) {
1450
1513
  sessionIdHash: sessionHash,
1451
1514
  parentContext: parentOTelCtx
1452
1515
  });
1453
- flowCtx.state[SPAN_KEY] = span;
1454
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1455
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1516
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1456
1517
  recordStageEvent(span, "parseInput", flowCtx.state);
1457
1518
  }
1458
1519
  function onPromptWillFind(flowCtx) {
@@ -1484,10 +1545,10 @@ function onPromptDidExecute(flowCtx) {
1484
1545
  function onPromptDidFinalize(flowCtx) {
1485
1546
  const rpcSpan = flowCtx.state?.[SPAN_KEY];
1486
1547
  const promptSpan = flowCtx.state?.[EXEC_SPAN_KEY];
1487
- const error = flowCtx.state?.error;
1548
+ const error = flowCtx.state?.flowError;
1488
1549
  if (error) {
1489
- if (promptSpan) endSpanError(promptSpan, error instanceof Error ? error : String(error));
1490
- if (rpcSpan) endSpanError(rpcSpan, error instanceof Error ? error : String(error));
1550
+ if (promptSpan) endFailedSpan(promptSpan, error);
1551
+ if (rpcSpan) endFailedSpan(rpcSpan, error);
1491
1552
  } else if (rpcSpan) {
1492
1553
  recordStageEvent(rpcSpan, "finalize", flowCtx.state);
1493
1554
  endSpanOk(rpcSpan);
@@ -1498,7 +1559,7 @@ function onAgentWillParse(options, flowCtx) {
1498
1559
  const ctx = extractContext(flowCtx);
1499
1560
  if (!ctx) return;
1500
1561
  const tracer = getTracer();
1501
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1562
+ const parentOTelCtx = parentContextOf(ctx);
1502
1563
  const sessionHash = sessionTracingId(ctx.sessionId);
1503
1564
  const { span, context: spanCtx } = startRpcSpan(tracer, {
1504
1565
  method: "agents/call",
@@ -1506,9 +1567,7 @@ function onAgentWillParse(options, flowCtx) {
1506
1567
  sessionIdHash: sessionHash,
1507
1568
  parentContext: parentOTelCtx
1508
1569
  });
1509
- flowCtx.state[SPAN_KEY] = span;
1510
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1511
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1570
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1512
1571
  recordStageEvent(span, "parseInput", flowCtx.state);
1513
1572
  }
1514
1573
  function onAgentWillFind(flowCtx) {
@@ -1522,7 +1581,7 @@ function onAgentWillExecute(options, flowCtx) {
1522
1581
  const agentName = flowCtx.state?.input?.name ?? "unknown";
1523
1582
  const { span: agentSpan } = startSpan(tracer, {
1524
1583
  name: `agent ${agentName}`,
1525
- kind: SpanKind11.INTERNAL,
1584
+ kind: SpanKind12.INTERNAL,
1526
1585
  attributes: {
1527
1586
  [FrontMcpAttributes.FLOW_NAME]: "agents:call-agent",
1528
1587
  [McpAttributes.COMPONENT_TYPE]: "agent",
@@ -1547,10 +1606,10 @@ function onAgentDidExecute(flowCtx) {
1547
1606
  function onAgentDidFinalize(flowCtx) {
1548
1607
  const rpcSpan = flowCtx.state?.[SPAN_KEY];
1549
1608
  const agentSpan = flowCtx.state?.[EXEC_SPAN_KEY];
1550
- const error = flowCtx.state?.error;
1609
+ const error = flowCtx.state?.flowError;
1551
1610
  if (error) {
1552
- if (agentSpan) endSpanError(agentSpan, error instanceof Error ? error : String(error));
1553
- if (rpcSpan) endSpanError(rpcSpan, error instanceof Error ? error : String(error));
1611
+ if (agentSpan) endFailedSpan(agentSpan, error);
1612
+ if (rpcSpan) endFailedSpan(rpcSpan, error);
1554
1613
  } else if (rpcSpan) {
1555
1614
  recordStageEvent(rpcSpan, "finalize", flowCtx.state);
1556
1615
  endSpanOk(rpcSpan);
@@ -1561,11 +1620,11 @@ function onGenericFlowWillStart(flowName, options, flowCtx) {
1561
1620
  const ctx = extractContext(flowCtx);
1562
1621
  if (!ctx) return;
1563
1622
  const tracer = getTracer();
1564
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1623
+ const parentOTelCtx = parentContextOf(ctx);
1565
1624
  const sessionHash = sessionTracingId(ctx.sessionId);
1566
1625
  const { span, context: spanCtx } = startSpan(tracer, {
1567
1626
  name: flowName,
1568
- kind: SpanKind11.INTERNAL,
1627
+ kind: SpanKind12.INTERNAL,
1569
1628
  attributes: {
1570
1629
  [FrontMcpAttributes.FLOW_NAME]: flowName,
1571
1630
  [McpAttributes.SESSION_ID]: sessionHash,
@@ -1575,9 +1634,7 @@ function onGenericFlowWillStart(flowName, options, flowCtx) {
1575
1634
  },
1576
1635
  parentContext: parentOTelCtx
1577
1636
  });
1578
- flowCtx.state[SPAN_KEY] = span;
1579
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1580
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1637
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1581
1638
  }
1582
1639
  function onGenericFlowStage(stageName, flowCtx) {
1583
1640
  const span = flowCtx.state?.[SPAN_KEY];
@@ -1586,9 +1643,9 @@ function onGenericFlowStage(stageName, flowCtx) {
1586
1643
  function onGenericFlowDidFinalize(flowCtx) {
1587
1644
  const span = flowCtx.state?.[SPAN_KEY];
1588
1645
  if (!span) return;
1589
- const error = flowCtx.state?.error;
1646
+ const error = flowCtx.state?.flowError;
1590
1647
  if (error) {
1591
- endSpanError(span, error instanceof Error ? error : String(error));
1648
+ endFailedSpan(span, error);
1592
1649
  } else {
1593
1650
  endSpanOk(span);
1594
1651
  }
@@ -1598,16 +1655,14 @@ function onTransportWillStart(transportType, options, flowCtx) {
1598
1655
  const ctx = extractContext(flowCtx);
1599
1656
  if (!ctx) return;
1600
1657
  const tracer = getTracer();
1601
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1658
+ const parentOTelCtx = parentContextOf(ctx);
1602
1659
  const sessionHash = sessionTracingId(ctx.sessionId);
1603
1660
  const { span, context: spanCtx } = startTransportSpan(tracer, {
1604
1661
  type: transportType,
1605
1662
  sessionIdHash: sessionHash,
1606
1663
  parentContext: parentOTelCtx
1607
1664
  });
1608
- flowCtx.state[SPAN_KEY] = span;
1609
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1610
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1665
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1611
1666
  recordStageEvent(span, "parseInput", flowCtx.state);
1612
1667
  }
1613
1668
  function onTransportDidRoute(flowCtx) {
@@ -1625,9 +1680,9 @@ function onTransportDidFinalize(flowCtx) {
1625
1680
  const span = flowCtx.state?.[SPAN_KEY];
1626
1681
  if (!span) return;
1627
1682
  recordStageEvent(span, "cleanup", flowCtx.state);
1628
- const error = flowCtx.state?.error;
1683
+ const error = flowCtx.state?.flowError;
1629
1684
  if (error) {
1630
- endSpanError(span, error instanceof Error ? error : String(error));
1685
+ endFailedSpan(span, error);
1631
1686
  } else {
1632
1687
  endSpanOk(span);
1633
1688
  }
@@ -1637,14 +1692,12 @@ function onAuthWillStart(flowName, options, flowCtx) {
1637
1692
  const ctx = extractContext(flowCtx);
1638
1693
  if (!ctx) return;
1639
1694
  const tracer = getTracer();
1640
- const parentOTelCtx = createOTelContextFromTrace(ctx.traceContext);
1695
+ const parentOTelCtx = parentContextOf(ctx);
1641
1696
  const { span, context: spanCtx } = startAuthSpan(tracer, {
1642
1697
  flowName,
1643
1698
  parentContext: parentOTelCtx
1644
1699
  });
1645
- flowCtx.state[SPAN_KEY] = span;
1646
- flowCtx.state[SPAN_CTX_KEY] = spanCtx;
1647
- flowCtx.state[STAGE_TS_KEY] = Date.now();
1700
+ beginFlowSpan(flowCtx, options, span, spanCtx);
1648
1701
  recordStageEvent(span, "parseInput", flowCtx.state);
1649
1702
  }
1650
1703
  function onAuthDidDetermineMode(flowCtx) {
@@ -1661,33 +1714,38 @@ function onAuthStage(stageName, flowCtx) {
1661
1714
  function onAuthDidFinalize(flowCtx) {
1662
1715
  const span = flowCtx.state?.[SPAN_KEY];
1663
1716
  if (!span) return;
1664
- const error = flowCtx.state?.error;
1717
+ const error = flowCtx.state?.flowError;
1665
1718
  if (error) {
1666
1719
  setAuthResult(span, "unauthorized");
1667
- endSpanError(span, error instanceof Error ? error : String(error));
1720
+ endFailedSpan(span, error);
1668
1721
  } else {
1669
1722
  setAuthResult(span, "authorized");
1670
1723
  endSpanOk(span);
1671
1724
  }
1672
1725
  }
1673
1726
  function wrapContextFetch(options, flowCtx) {
1674
- if (options.fetchSpans === false) return;
1675
1727
  const toolCtx = flowCtx.state?.toolContext ?? flowCtx.state?.toolCallContext;
1676
1728
  if (!toolCtx || typeof toolCtx.fetch !== "function") return;
1677
1729
  const originalFetch = toolCtx.fetch.bind(toolCtx);
1678
1730
  const parentOTelCtx = flowCtx.state?.[SPAN_CTX_KEY];
1679
1731
  toolCtx.fetch = async (input, init) => {
1680
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1681
- const method = init?.method ?? "GET";
1682
- const tracer = getTracer();
1683
- const { span } = startFetchSpan(tracer, { method, url, parentContext: parentOTelCtx });
1732
+ const fetchSpan = options.fetchSpans === false ? void 0 : startFetchSpan(getTracer(), {
1733
+ method: init?.method ?? (input instanceof Request ? input.method : "GET"),
1734
+ url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url,
1735
+ parentContext: parentOTelCtx
1736
+ }).span;
1737
+ const callingSpan = fetchSpan ?? flowCtx.state?.[EXEC_SPAN_KEY] ?? flowCtx.state?.[SPAN_KEY];
1738
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
1739
+ if (callingSpan && !headers.has("traceparent")) headers.set("traceparent", traceparentOf(callingSpan));
1684
1740
  try {
1685
- const response = await originalFetch(input, init);
1686
- setFetchResponseStatus(span, response.status);
1687
- endSpanOk(span);
1741
+ const response = await originalFetch(input, { ...init, headers });
1742
+ if (fetchSpan) {
1743
+ setFetchResponseStatus(fetchSpan, response.status);
1744
+ endSpanOk(fetchSpan);
1745
+ }
1688
1746
  return response;
1689
1747
  } catch (err) {
1690
- endSpanError(span, err instanceof Error ? err : String(err));
1748
+ if (fetchSpan) endSpanError(fetchSpan, err instanceof Error ? err : String(err));
1691
1749
  throw err;
1692
1750
  }
1693
1751
  };
@@ -1705,11 +1763,96 @@ function onAgentDidExecuteEnrich(flowCtx) {
1705
1763
  }
1706
1764
  }
1707
1765
  }
1766
+ var HOOK_OBSERVER_KEY = /* @__PURE__ */ Symbol.for("frontmcp:hook-observer");
1767
+ function installHookObserver(flowCtx, observer) {
1768
+ const ctx = extractContext(flowCtx);
1769
+ if (ctx && !ctx.get?.(HOOK_OBSERVER_KEY)) ctx.set?.(HOOK_OBSERVER_KEY, observer);
1770
+ }
1771
+ async function runInHookSpan(flowCtx, hook, run) {
1772
+ const ctx = extractContext(flowCtx);
1773
+ const { span } = startHookSpan(getTracer(), {
1774
+ ...hook,
1775
+ parentContext: flowCtx.state?.[SPAN_CTX_KEY] ?? (ctx && parentContextOf(ctx))
1776
+ });
1777
+ try {
1778
+ await run();
1779
+ endSpanOk(span);
1780
+ } catch (error) {
1781
+ if (error instanceof FlowControl && error.type !== "fail" && error.type !== "abort") endSpanOk(span);
1782
+ else endFailedSpan(span, error);
1783
+ throw error;
1784
+ }
1785
+ }
1708
1786
  function reportStartup(data) {
1709
1787
  const tracer = getTracer();
1710
1788
  emitStartupReport(tracer, data);
1711
1789
  }
1712
1790
 
1791
+ // libs/observability/src/request-log/request-log.tokens.ts
1792
+ var REQUEST_LOG_COLLECTOR = /* @__PURE__ */ Symbol.for("frontmcp:observability:request-log-collector");
1793
+
1794
+ // libs/observability/src/request-log/request-log.hooks.ts
1795
+ var FRONTMCP_CONTEXT = /* @__PURE__ */ Symbol.for("frontmcp:CONTEXT");
1796
+ function resolve(flowCtx, token) {
1797
+ try {
1798
+ return flowCtx.get?.(token);
1799
+ } catch {
1800
+ return void 0;
1801
+ }
1802
+ }
1803
+ function currentRequestLog(flowCtx) {
1804
+ return resolve(flowCtx, FRONTMCP_CONTEXT)?.get(REQUEST_LOG_COLLECTOR);
1805
+ }
1806
+ function startRequestLog(flowCtx, options) {
1807
+ const context = resolve(flowCtx, FRONTMCP_CONTEXT);
1808
+ if (!context) return;
1809
+ const collector = new RequestLogCollector(
1810
+ {
1811
+ requestId: context.requestId,
1812
+ traceId: context.traceContext.traceId,
1813
+ sessionIdHash: sessionTracingId(context.sessionId).slice(0, 12),
1814
+ scopeId: context.scopeId
1815
+ },
1816
+ options
1817
+ );
1818
+ context.set(REQUEST_LOG_COLLECTOR, collector);
1819
+ const request = flowCtx.rawInput?.request;
1820
+ if (request?.method && request.path) collector.setHttpInfo(request.method, request.path);
1821
+ const rpcMethod = request?.body?.method;
1822
+ if (typeof rpcMethod === "string") collector.setRpcMethod(rpcMethod);
1823
+ }
1824
+ function recordRequestLogFailure(flowCtx) {
1825
+ const flowError = flowCtx.state?.["flowError"];
1826
+ if (!flowError) return;
1827
+ const { type, message, code, errorId } = flowFailureOf(flowError);
1828
+ currentRequestLog(flowCtx)?.setError({ type, message, code, error_id: errorId });
1829
+ }
1830
+ function recordRequestLogAuth(flowCtx) {
1831
+ const verifyResult = flowCtx.state?.["verifyResult"];
1832
+ const authorization = flowCtx.rawInput?.request?.headers?.["authorization"];
1833
+ const scheme = typeof authorization === "string" ? authorization.trim().split(/\s+/)[0].toLowerCase() : "";
1834
+ const sub = verifyResult?.authorization?.user?.sub;
1835
+ const anonymous = typeof sub !== "string" || isAnonymousSubject(sub);
1836
+ currentRequestLog(flowCtx)?.setAuthInfo(
1837
+ anonymous || !scheme ? "anonymous" : scheme,
1838
+ verifyResult?.kind === "authorized" && !anonymous
1839
+ );
1840
+ }
1841
+ async function completeRequestLog(flowCtx) {
1842
+ const collector = currentRequestLog(flowCtx);
1843
+ if (!collector || collector.isFinalized()) return;
1844
+ const statusCode = flowCtx.state?.["statusCode"] ?? flowCtx.rawInput?.response?.statusCode;
1845
+ if (typeof statusCode === "number") collector.setStatusCode(statusCode);
1846
+ await collector.finalize();
1847
+ }
1848
+
1849
+ // libs/observability/src/telemetry/telemetry.accessor.ts
1850
+ import {
1851
+ SpanKind as SpanKind13,
1852
+ SpanStatusCode as SpanStatusCode3,
1853
+ trace as trace4
1854
+ } from "@opentelemetry/api";
1855
+
1713
1856
  // libs/observability/src/telemetry/telemetry.counters.ts
1714
1857
  import { metrics } from "@opentelemetry/api";
1715
1858
  function snapshotKey(name, attributes) {
@@ -1825,14 +1968,14 @@ var TelemetrySpan = class {
1825
1968
  /** Record an error on this span */
1826
1969
  recordError(error) {
1827
1970
  this.span.recordException(error);
1828
- this.span.setStatus({ code: SpanStatusCode2.ERROR, message: error.message });
1971
+ this.span.setStatus({ code: SpanStatusCode3.ERROR, message: error.message });
1829
1972
  this.hasError = true;
1830
1973
  return this;
1831
1974
  }
1832
1975
  /** End the span (preserves ERROR status if already set) */
1833
1976
  end() {
1834
1977
  if (!this.hasError) {
1835
- this.span.setStatus({ code: SpanStatusCode2.OK });
1978
+ this.span.setStatus({ code: SpanStatusCode3.OK });
1836
1979
  }
1837
1980
  this.span.end();
1838
1981
  }
@@ -1840,7 +1983,7 @@ var TelemetrySpan = class {
1840
1983
  endWithError(error) {
1841
1984
  const msg = typeof error === "string" ? error : error.message;
1842
1985
  if (error instanceof Error) this.span.recordException(error);
1843
- this.span.setStatus({ code: SpanStatusCode2.ERROR, message: msg });
1986
+ this.span.setStatus({ code: SpanStatusCode3.ERROR, message: msg });
1844
1987
  this.span.end();
1845
1988
  }
1846
1989
  /** Get the underlying OTel span (escape hatch) */
@@ -1887,7 +2030,7 @@ var TelemetryAccessor = class {
1887
2030
  const span = this.tracer.startSpan(
1888
2031
  name,
1889
2032
  {
1890
- kind: SpanKind12.INTERNAL,
2033
+ kind: SpanKind13.INTERNAL,
1891
2034
  attributes: { ...this.baseAttributes, ...attributes }
1892
2035
  },
1893
2036
  this.getActiveContext()
@@ -1936,11 +2079,11 @@ var TelemetryAccessor = class {
1936
2079
  } else {
1937
2080
  const span = this.tracer.startSpan(
1938
2081
  name,
1939
- { kind: SpanKind12.INTERNAL, attributes: this.baseAttributes },
2082
+ { kind: SpanKind13.INTERNAL, attributes: this.baseAttributes },
1940
2083
  this.fallbackContext
1941
2084
  );
1942
2085
  if (attributes) span.addEvent(name, attributes);
1943
- span.setStatus({ code: SpanStatusCode2.OK });
2086
+ span.setStatus({ code: SpanStatusCode3.OK });
1944
2087
  span.end();
1945
2088
  }
1946
2089
  }
@@ -1988,7 +2131,7 @@ var TelemetryAccessor = class {
1988
2131
  };
1989
2132
 
1990
2133
  // libs/observability/src/telemetry/telemetry.factory.ts
1991
- import { SpanStatusCode as SpanStatusCode3, trace as trace5 } from "@opentelemetry/api";
2134
+ import { SpanStatusCode as SpanStatusCode4, trace as trace5 } from "@opentelemetry/api";
1992
2135
  var TelemetryFactory = class {
1993
2136
  constructor(tracerName = "@frontmcp/observability") {
1994
2137
  this.tracer = trace5.getTracer(tracerName);
@@ -2079,14 +2222,43 @@ var DEFAULT_TRACING = {
2079
2222
  var ObservabilityPlugin = class extends DynamicPlugin {
2080
2223
  constructor(options = {}) {
2081
2224
  super();
2225
+ /** Wraps each other hook the request runs: names it on the request's log, and gives it a span with `hookSpans`. */
2226
+ this.observeHook = async ({ flowName, stage, target, flowContext }, run) => {
2227
+ if (target === this) return run();
2228
+ if (this.requestLogsEnabled) currentRequestLog(flowContext)?.addHook(`${flowName}:${stage}`);
2229
+ if (!this.traces("hookSpans")) return run();
2230
+ const owner = target?.constructor?.name;
2231
+ return runInHookSpan(flowContext, { flowName, stage, owner }, run);
2232
+ };
2082
2233
  this.options = resolveOptions(options);
2083
2234
  this.tracingOpts = this.options.tracing !== false ? this.options.tracing : DEFAULT_TRACING;
2084
2235
  }
2085
2236
  get tracingEnabled() {
2086
2237
  return this.options.tracing !== false;
2087
2238
  }
2239
+ get requestLogsEnabled() {
2240
+ return this.options.requestLogs !== false;
2241
+ }
2242
+ /** Whether tracing is on and its `kind` option isn't off. */
2243
+ traces(kind) {
2244
+ return this.tracingEnabled && this.tracingOpts[kind] !== false;
2245
+ }
2246
+ /** Watches the hooks of the request `ctx` serves, from the first flow of it that this plugin sees. */
2247
+ watchHooks(ctx) {
2248
+ if (this.requestLogsEnabled || this.traces("hookSpans"))
2249
+ installHookObserver(ctx, this.observeHook);
2250
+ }
2251
+ /** Names the entry a request serves on its request log, and records the entry flow's failure. */
2252
+ annotateRequestLog(ctx, set) {
2253
+ if (!this.requestLogsEnabled) return;
2254
+ const flowCtx = ctx;
2255
+ const log = currentRequestLog(ctx);
2256
+ if (log && flowCtx.state?.input) set(log, flowCtx.state.input);
2257
+ }
2088
2258
  _httpWillTrace(ctx) {
2089
2259
  if (this.tracingEnabled) onHttpWillTrace(this.tracingOpts, ctx);
2260
+ if (this.options.requestLogs !== false) startRequestLog(ctx, this.options.requestLogs);
2261
+ this.watchHooks(ctx);
2090
2262
  }
2091
2263
  _httpWillQuota(ctx) {
2092
2264
  if (this.tracingEnabled) onHttpWillAcquireQuota(ctx);
@@ -2099,6 +2271,7 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2099
2271
  }
2100
2272
  _httpDidAuth(ctx) {
2101
2273
  if (this.tracingEnabled) onHttpDidCheckAuth(ctx);
2274
+ if (this.requestLogsEnabled) recordRequestLogAuth(ctx);
2102
2275
  }
2103
2276
  _httpWillRoute(ctx) {
2104
2277
  if (this.tracingEnabled) onHttpWillRoute(ctx);
@@ -2108,12 +2281,15 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2108
2281
  }
2109
2282
  _httpDidFinalize(ctx) {
2110
2283
  if (this.tracingEnabled) onHttpDidFinalize(ctx);
2284
+ if (this.requestLogsEnabled) void completeRequestLog(ctx);
2111
2285
  }
2112
2286
  _toolWillParse(ctx) {
2287
+ this.watchHooks(ctx);
2113
2288
  if (this.tracingEnabled) onToolWillParse(this.tracingOpts, ctx);
2114
2289
  }
2115
2290
  _toolWillFind(ctx) {
2116
2291
  if (this.tracingEnabled) onToolWillFindTool(ctx);
2292
+ this.annotateRequestLog(ctx, (log, input) => log.setToolName(String(input["name"])));
2117
2293
  }
2118
2294
  _toolWillCheckAuth(ctx) {
2119
2295
  if (this.tracingEnabled) onToolWillCheckAuth(ctx);
@@ -2144,12 +2320,15 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2144
2320
  }
2145
2321
  _toolDidFinalize(ctx) {
2146
2322
  if (this.tracingEnabled) onToolDidFinalize(ctx);
2323
+ if (this.requestLogsEnabled) recordRequestLogFailure(ctx);
2147
2324
  }
2148
2325
  _resourceWillParse(ctx) {
2326
+ this.watchHooks(ctx);
2149
2327
  if (this.tracingEnabled) onResourceWillParse(this.tracingOpts, ctx);
2150
2328
  }
2151
2329
  _resourceWillFind(ctx) {
2152
2330
  if (this.tracingEnabled) onResourceWillFind(ctx);
2331
+ this.annotateRequestLog(ctx, (log, input) => log.setResourceUri(String(input["uri"])));
2153
2332
  }
2154
2333
  _resourceWillCheckAuthorities(ctx) {
2155
2334
  if (this.tracingEnabled) onEntryWillCheckAuthorities(ctx);
@@ -2165,12 +2344,15 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2165
2344
  }
2166
2345
  _resourceDidFinalize(ctx) {
2167
2346
  if (this.tracingEnabled) onResourceDidFinalize(ctx);
2347
+ if (this.requestLogsEnabled) recordRequestLogFailure(ctx);
2168
2348
  }
2169
2349
  _promptWillParse(ctx) {
2350
+ this.watchHooks(ctx);
2170
2351
  if (this.tracingEnabled) onPromptWillParse(this.tracingOpts, ctx);
2171
2352
  }
2172
2353
  _promptWillFind(ctx) {
2173
2354
  if (this.tracingEnabled) onPromptWillFind(ctx);
2355
+ this.annotateRequestLog(ctx, (log, input) => log.setPromptName(String(input["name"])));
2174
2356
  }
2175
2357
  _promptWillCheckAuthorities(ctx) {
2176
2358
  if (this.tracingEnabled) onEntryWillCheckAuthorities(ctx);
@@ -2186,8 +2368,10 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2186
2368
  }
2187
2369
  _promptDidFinalize(ctx) {
2188
2370
  if (this.tracingEnabled) onPromptDidFinalize(ctx);
2371
+ if (this.requestLogsEnabled) recordRequestLogFailure(ctx);
2189
2372
  }
2190
2373
  _agentWillParse(ctx) {
2374
+ this.watchHooks(ctx);
2191
2375
  if (this.tracingEnabled) onAgentWillParse(this.tracingOpts, ctx);
2192
2376
  }
2193
2377
  _agentWillFind(ctx) {
@@ -2341,46 +2525,46 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2341
2525
  if (this.tracingEnabled) onAuthDidFinalize(ctx);
2342
2526
  }
2343
2527
  _oauthTokenWill(ctx) {
2344
- if (this.tracingEnabled) onGenericFlowWillStart("oauth/token", this.tracingOpts, ctx);
2528
+ if (this.traces("oauthSpans")) onGenericFlowWillStart("oauth/token", this.tracingOpts, ctx);
2345
2529
  }
2346
2530
  _oauthTokenDone(ctx) {
2347
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2531
+ if (this.traces("oauthSpans")) onGenericFlowDidFinalize(ctx);
2348
2532
  }
2349
2533
  _oauthAuthzWill(ctx) {
2350
- if (this.tracingEnabled) onGenericFlowWillStart("oauth/authorize", this.tracingOpts, ctx);
2534
+ if (this.traces("oauthSpans")) onGenericFlowWillStart("oauth/authorize", this.tracingOpts, ctx);
2351
2535
  }
2352
2536
  _oauthAuthzDone(ctx) {
2353
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2537
+ if (this.traces("oauthSpans")) onGenericFlowDidFinalize(ctx);
2354
2538
  }
2355
2539
  _oauthCallbackWill(ctx) {
2356
- if (this.tracingEnabled) onGenericFlowWillStart("oauth/callback", this.tracingOpts, ctx);
2540
+ if (this.traces("oauthSpans")) onGenericFlowWillStart("oauth/callback", this.tracingOpts, ctx);
2357
2541
  }
2358
2542
  _oauthCallbackDone(ctx) {
2359
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2543
+ if (this.traces("oauthSpans")) onGenericFlowDidFinalize(ctx);
2360
2544
  }
2361
2545
  _oauthProviderWill(ctx) {
2362
- if (this.tracingEnabled) onGenericFlowWillStart("oauth/provider-callback", this.tracingOpts, ctx);
2546
+ if (this.traces("oauthSpans")) onGenericFlowWillStart("oauth/provider-callback", this.tracingOpts, ctx);
2363
2547
  }
2364
2548
  _oauthProviderDone(ctx) {
2365
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2549
+ if (this.traces("oauthSpans")) onGenericFlowDidFinalize(ctx);
2366
2550
  }
2367
2551
  _oauthRegisterWill(ctx) {
2368
- if (this.tracingEnabled) onGenericFlowWillStart("oauth/register", this.tracingOpts, ctx);
2552
+ if (this.traces("oauthSpans")) onGenericFlowWillStart("oauth/register", this.tracingOpts, ctx);
2369
2553
  }
2370
2554
  _oauthRegisterDone(ctx) {
2371
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2555
+ if (this.traces("oauthSpans")) onGenericFlowDidFinalize(ctx);
2372
2556
  }
2373
2557
  _elicitReqWill(ctx) {
2374
- if (this.tracingEnabled) onGenericFlowWillStart("elicitation/request", this.tracingOpts, ctx);
2558
+ if (this.traces("elicitationSpans")) onGenericFlowWillStart("elicitation/request", this.tracingOpts, ctx);
2375
2559
  }
2376
2560
  _elicitReqDone(ctx) {
2377
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2561
+ if (this.traces("elicitationSpans")) onGenericFlowDidFinalize(ctx);
2378
2562
  }
2379
2563
  _elicitResWill(ctx) {
2380
- if (this.tracingEnabled) onGenericFlowWillStart("elicitation/result", this.tracingOpts, ctx);
2564
+ if (this.traces("elicitationSpans")) onGenericFlowWillStart("elicitation/result", this.tracingOpts, ctx);
2381
2565
  }
2382
2566
  _elicitResDone(ctx) {
2383
- if (this.tracingEnabled) onGenericFlowDidFinalize(ctx);
2567
+ if (this.traces("elicitationSpans")) onGenericFlowDidFinalize(ctx);
2384
2568
  }
2385
2569
  _subscribeWill(ctx) {
2386
2570
  if (this.tracingEnabled) onGenericFlowWillStart("resources/subscribe", this.tracingOpts, ctx);
@@ -2446,12 +2630,10 @@ var ObservabilityPlugin = class extends DynamicPlugin {
2446
2630
  // Startup Report (public method)
2447
2631
  // ═══════════════════════════════════════════════════════════════════════════
2448
2632
  /**
2449
- * Emit startup telemetry report.
2450
- * Call after scope.initialize() completes with component counts.
2633
+ * Emit the `frontmcp.startup` span (`tracing.startupReport`). The scope calls it once it is ready.
2451
2634
  */
2452
2635
  reportStartupTelemetry(data) {
2453
- if (this.options.tracing === false) return;
2454
- reportStartup(data);
2636
+ if (this.traces("startupReport")) reportStartup(data);
2455
2637
  }
2456
2638
  };
2457
2639
  ObservabilityPlugin.defaultOptions = {
@@ -2480,7 +2662,7 @@ ObservabilityPlugin.dynamicProviders = (input) => {
2480
2662
  name: "observability:telemetry-accessor",
2481
2663
  provide: TELEMETRY_ACCESSOR,
2482
2664
  scope: ProviderScope.CONTEXT,
2483
- inject: () => [FRONTMCP_CONTEXT],
2665
+ inject: () => [FRONTMCP_CONTEXT2],
2484
2666
  useFactory: (ctx) => new TelemetryAccessor(ctx)
2485
2667
  });
2486
2668
  providers.push({
@@ -2513,7 +2695,7 @@ ObservabilityPlugin.dynamicProviders = (input) => {
2513
2695
  name: "observability:request-log-collector",
2514
2696
  provide: REQUEST_LOG_COLLECTOR,
2515
2697
  scope: ProviderScope.CONTEXT,
2516
- inject: () => [FRONTMCP_CONTEXT],
2698
+ inject: () => [FRONTMCP_CONTEXT2],
2517
2699
  useFactory: (ctx) => {
2518
2700
  return new RequestLogCollector(
2519
2701
  {
@@ -3018,35 +3200,8 @@ function setupOTel(options) {
3018
3200
  }
3019
3201
  }
3020
3202
 
3021
- // libs/observability/src/otel/spans/hook.span.ts
3022
- import { SpanKind as SpanKind13 } from "@opentelemetry/api";
3023
- function recordHookEvent(parentSpan, stage, owner) {
3024
- const attributes = {};
3025
- if (owner) {
3026
- attributes[FrontMcpAttributes.HOOK_OWNER] = owner;
3027
- }
3028
- parentSpan.addEvent(`hook.${stage}`, attributes);
3029
- }
3030
- function startHookSpan(tracer, options) {
3031
- const attributes = {
3032
- [FrontMcpAttributes.HOOK_STAGE]: options.stage
3033
- };
3034
- if (options.owner) {
3035
- attributes[FrontMcpAttributes.HOOK_OWNER] = options.owner;
3036
- }
3037
- if (options.flowName) {
3038
- attributes[FrontMcpAttributes.FLOW_NAME] = options.flowName;
3039
- }
3040
- return startSpan(tracer, {
3041
- name: `hook ${options.stage}`,
3042
- kind: SpanKind13.INTERNAL,
3043
- attributes,
3044
- parentContext: options.parentContext
3045
- });
3046
- }
3047
-
3048
3203
  // libs/observability/src/otel/spans/pretty-span-exporter.ts
3049
- import { SpanKind as SpanKind14, SpanStatusCode as SpanStatusCode4 } from "@opentelemetry/api";
3204
+ import { SpanKind as SpanKind14, SpanStatusCode as SpanStatusCode5 } from "@opentelemetry/api";
3050
3205
  var RESET = "\x1B[0m";
3051
3206
  var DIM = "\x1B[2m";
3052
3207
  var BOLD = "\x1B[1m";
@@ -3098,7 +3253,7 @@ var PrettySpanExporter = class {
3098
3253
  const traceShort = span.spanContext().traceId.slice(0, 8);
3099
3254
  const durationMs = (span.duration[0] * 1e3 + span.duration[1] / 1e6).toFixed(1);
3100
3255
  const kind = KIND_LABELS[span.kind] ?? "UNKNOWN";
3101
- const statusOk = span.status.code !== SpanStatusCode4.ERROR;
3256
+ const statusOk = span.status.code !== SpanStatusCode5.ERROR;
3102
3257
  const attrs = Object.entries(span.attributes).filter(([k]) => SHOW_ATTRS.has(k)).map(([k, v]) => {
3103
3258
  const short = k.replace("frontmcp.", "").replace("mcp.", "");
3104
3259
  return `${short}=${v}`;
@@ -3212,6 +3367,13 @@ function renderJsonExposition(counters, gauges = []) {
3212
3367
  import { readdirSync } from "@frontmcp/utils";
3213
3368
  var NS_PER_SECOND = 1e9;
3214
3369
  var MICROS_PER_SECOND = 1e6;
3370
+ function probeSafely(probe) {
3371
+ try {
3372
+ return probe();
3373
+ } catch {
3374
+ return void 0;
3375
+ }
3376
+ }
3215
3377
  function defaultMonitorEventLoopDelay() {
3216
3378
  try {
3217
3379
  const { monitorEventLoopDelay } = __require("node:perf_hooks");
@@ -3257,54 +3419,70 @@ var ProcessStatsCollector = class {
3257
3419
  this.getActiveHandles = init.getActiveHandles ?? defaultGetActiveHandles;
3258
3420
  this.getActiveRequests = init.getActiveRequests ?? defaultGetActiveRequests;
3259
3421
  this.readFdCount = init.readFdCount ?? defaultReadFdCount;
3260
- this.cpuStart = this.cpuUsage();
3422
+ this.cpuStart = probeSafely(() => this.cpuUsage());
3261
3423
  if (this.options.eventLoopLag !== false) {
3262
3424
  this.histogram = init.monitorEventLoopDelay ? init.monitorEventLoopDelay() : defaultMonitorEventLoopDelay();
3263
3425
  }
3264
3426
  }
3265
3427
  collect() {
3266
3428
  const entries = [];
3267
- const cpu = this.cpuUsage(this.cpuStart);
3268
- entries.push({
3269
- name: "frontmcp_process_cpu_seconds_total",
3270
- value: cpu.user / MICROS_PER_SECOND,
3271
- attributes: { mode: "user" },
3272
- help: "CPU time consumed since collector start, by mode (seconds)"
3273
- });
3274
- entries.push({
3275
- name: "frontmcp_process_cpu_seconds_total",
3276
- value: cpu.system / MICROS_PER_SECOND,
3277
- attributes: { mode: "system" }
3278
- });
3279
- const mem = this.memoryUsage();
3280
- entries.push({
3281
- name: "frontmcp_process_resident_memory_bytes",
3282
- value: mem.rss,
3283
- help: "Resident memory size in bytes"
3284
- });
3285
- entries.push({
3286
- name: "frontmcp_process_heap_bytes",
3287
- value: mem.heapTotal,
3288
- help: "Total V8 heap size in bytes"
3289
- });
3290
- entries.push({
3291
- name: "frontmcp_process_heap_used_bytes",
3292
- value: mem.heapUsed,
3293
- help: "Used V8 heap size in bytes"
3294
- });
3295
- entries.push({
3296
- name: "frontmcp_process_external_bytes",
3297
- value: mem.external,
3298
- help: "Memory used by C++ objects bound to JS in bytes"
3299
- });
3300
- entries.push({
3301
- name: "frontmcp_process_uptime_seconds",
3302
- value: this.uptime(),
3303
- help: "Time since process start in seconds"
3429
+ this.cpuStart ??= probeSafely(() => this.cpuUsage());
3430
+ const cpu = this.cpuStart && probeSafely(() => this.cpuUsage(this.cpuStart));
3431
+ if (cpu) {
3432
+ entries.push({
3433
+ name: "frontmcp_process_cpu_seconds_total",
3434
+ value: cpu.user / MICROS_PER_SECOND,
3435
+ attributes: { mode: "user" },
3436
+ help: "CPU time consumed since collector start, by mode (seconds)"
3437
+ });
3438
+ entries.push({
3439
+ name: "frontmcp_process_cpu_seconds_total",
3440
+ value: cpu.system / MICROS_PER_SECOND,
3441
+ attributes: { mode: "system" }
3442
+ });
3443
+ }
3444
+ const mem = probeSafely(() => this.memoryUsage());
3445
+ if (mem && mem.rss > 0) {
3446
+ entries.push({
3447
+ name: "frontmcp_process_resident_memory_bytes",
3448
+ value: mem.rss,
3449
+ help: "Resident memory size in bytes"
3450
+ });
3451
+ entries.push({
3452
+ name: "frontmcp_process_heap_bytes",
3453
+ value: mem.heapTotal,
3454
+ help: "Total V8 heap size in bytes"
3455
+ });
3456
+ entries.push({
3457
+ name: "frontmcp_process_heap_used_bytes",
3458
+ value: mem.heapUsed,
3459
+ help: "Used V8 heap size in bytes"
3460
+ });
3461
+ entries.push({
3462
+ name: "frontmcp_process_external_bytes",
3463
+ value: mem.external,
3464
+ help: "Memory used by C++ objects bound to JS in bytes"
3465
+ });
3466
+ }
3467
+ const uptime = probeSafely(() => this.uptime());
3468
+ if (uptime !== void 0 && uptime > 0) {
3469
+ entries.push({
3470
+ name: "frontmcp_process_uptime_seconds",
3471
+ value: uptime,
3472
+ help: "Time since process start in seconds"
3473
+ });
3474
+ }
3475
+ const histogram = this.options.eventLoopLag !== false ? this.histogram : void 0;
3476
+ const lag = histogram && probeSafely(() => {
3477
+ const sample = {
3478
+ meanSeconds: histogram.mean / NS_PER_SECOND,
3479
+ p99Seconds: histogram.percentile(99) / NS_PER_SECOND
3480
+ };
3481
+ histogram.reset();
3482
+ return sample;
3304
3483
  });
3305
- if (this.options.eventLoopLag !== false && this.histogram) {
3306
- const meanSeconds = this.histogram.mean / NS_PER_SECOND;
3307
- const p99Seconds = this.histogram.percentile(99) / NS_PER_SECOND;
3484
+ if (lag) {
3485
+ const { meanSeconds, p99Seconds } = lag;
3308
3486
  if (Number.isFinite(meanSeconds)) {
3309
3487
  entries.push({
3310
3488
  name: "frontmcp_nodejs_eventloop_lag_seconds",
@@ -3320,7 +3498,6 @@ var ProcessStatsCollector = class {
3320
3498
  attributes: { quantile: "p99" }
3321
3499
  });
3322
3500
  }
3323
- this.histogram.reset();
3324
3501
  }
3325
3502
  if (this.options.activeHandles !== false) {
3326
3503
  const handles = this.getActiveHandles?.();