@frontmcp/observability 1.10.16 → 1.11.0-rc.1

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.
@@ -1,6 +1,6 @@
1
- import { LogTransportInterface, LogRecord } from '@frontmcp/sdk';
1
+ import { LogTransportInterface, type LogRecord } from '@frontmcp/sdk';
2
2
  import type { LogSink } from './log-sink.interface';
3
- import type { StructuredLogEntry, StructuredLogTransportOptions } from './structured-log.types';
3
+ import { type StructuredLogEntry, type StructuredLogTransportOptions } from './structured-log.types';
4
4
  /**
5
5
  * Context accessor function type.
6
6
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/observability",
3
- "version": "1.10.16",
3
+ "version": "1.11.0-rc.1",
4
4
  "description": "OpenTelemetry tracing, structured JSON logging, request logs and Prometheus-format metrics for FrontMCP servers",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -53,8 +53,8 @@
53
53
  "@opentelemetry/api": "^1.9.0"
54
54
  },
55
55
  "peerDependencies": {
56
- "@frontmcp/sdk": "1.10.16",
57
- "@frontmcp/utils": "1.10.16",
56
+ "@frontmcp/sdk": "1.11.0-rc.1",
57
+ "@frontmcp/utils": "1.11.0-rc.1",
58
58
  "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
59
59
  "@opentelemetry/sdk-node": "^0.219.0",
60
60
  "@opentelemetry/sdk-trace-base": "^2.8.0",
package/plugin/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { default as ObservabilityPlugin } from './observability.plugin';
2
2
  export type { ObservabilityPluginOptions, ObservabilityPluginOptionsInput, ObservabilityLoggingOptions, } from './observability.plugin.types';
3
+ export { ObservabilityConfigurationError } from './observability.errors';
4
+ export type { DevEventsOptions, StallDetectionOptions } from './observability.options';
3
5
  export { sessionTracingId, SPAN_KEY, SPAN_CTX_KEY, EXEC_SPAN_KEY, ACTIVE_SPAN_KEY, ACTIVE_OTEL_CTX_KEY, reportStartup, } from './observability.hooks';
@@ -0,0 +1,6 @@
1
+ import { InternalMcpError } from '@frontmcp/sdk';
2
+ /** Raised when `ObservabilityPlugin.init()` or `@FrontMcp({ observability })` is given options it cannot honour. */
3
+ export declare class ObservabilityConfigurationError extends InternalMcpError {
4
+ readonly name = "ObservabilityConfigurationError";
5
+ constructor(message: string);
6
+ }
@@ -18,6 +18,7 @@
18
18
  * - Every flow stage recorded as a timed span event
19
19
  * - Error status propagated to parent spans
20
20
  */
21
+ import { type HookObserver } from '@frontmcp/sdk';
21
22
  import { type TracingOptions } from '../otel/otel.types';
22
23
  import { type StartupTelemetryData } from '../otel/spans/startup.span';
23
24
  /** Root span for the current flow (HTTP server span or RPC span) */
@@ -104,14 +105,8 @@ export declare function onAuthStage(stageName: string, flowCtx: any): void;
104
105
  export declare function onAuthDidFinalize(flowCtx: any): void;
105
106
  export declare function wrapContextFetch(options: TracingOptions, flowCtx: any): void;
106
107
  export declare function onAgentDidExecuteEnrich(flowCtx: any): void;
107
- /** Wraps each hook a plugin, app or entry injected into a flow of the request: `run` runs the hook. */
108
- export type HookObserver = (hook: {
109
- flowName: string;
110
- stage: string;
111
- target: unknown;
112
- flowContext: unknown;
113
- }, run: () => Promise<void>) => Promise<void>;
114
- /** Installs `observer` for the request the flow `flowCtx` serves, unless one watches it already. */
108
+ export type { HookObserver };
109
+ /** Adds `observer` to the hook observers of the request the flow `flowCtx` serves, unless this plugin added one. */
115
110
  export declare function installHookObserver(flowCtx: FlowContextLike, observer: HookObserver): void;
116
111
  /** Runs one hook inside a `hook <stage>` span, a child of its flow's span. `fail` and `abort` end it as an error; other control-flow signals end it OK. */
117
112
  export declare function runInHookSpan(flowCtx: any, hook: {
@@ -0,0 +1,34 @@
1
+ import type { DevEventSink } from '../dev-events/dev-event-stream';
2
+ /** Event-loop stall detection: a warning, a metric and a dev event for each stall, naming the calls in flight. */
3
+ export interface StallDetectionOptions {
4
+ /** The shortest loop delay reported as a stall, in milliseconds (200 by default, from 20 to 60000). */
5
+ thresholdMs?: number;
6
+ }
7
+ /** The `frontmcp dev` event stream: call timings, failures and stalls. Only on in development. */
8
+ export interface DevEventsOptions {
9
+ /** A JSON Lines file to append the events to; `frontmcp dev` names one (`--events-file`) when this is unset. */
10
+ file?: string;
11
+ /** More places to send the events to. */
12
+ sinks?: DevEventSink[];
13
+ /** How many events wait for the sinks at most before new ones are dropped (1000 by default). */
14
+ maxQueue?: number;
15
+ }
16
+ export interface ResolvedStallOptions {
17
+ thresholdMs: number;
18
+ }
19
+ export interface ResolvedDevEventsOptions {
20
+ file?: string;
21
+ /** Whether to send the events to the dev parent over IPC (`frontmcp dev --stdio --serve`). */
22
+ ipc: boolean;
23
+ sinks: DevEventSink[];
24
+ maxQueue: number;
25
+ }
26
+ /** Set by `frontmcp dev` on the server it starts: the events file, or `1` to send the events over IPC. */
27
+ export declare const DEV_EVENTS_FILE_ENV = "FRONTMCP_DEV_EVENTS_FILE";
28
+ export declare const DEV_EVENTS_IPC_ENV = "FRONTMCP_DEV_EVENTS_IPC";
29
+ export declare function resolveStallOptions(input: boolean | StallDetectionOptions | undefined): ResolvedStallOptions | false;
30
+ /**
31
+ * The dev event stream's options, or `false` when it is off: outside development (no `frontmcp dev` and
32
+ * `NODE_ENV` not `development`), when turned off, or with nowhere to write.
33
+ */
34
+ export declare function resolveDevEventsOptions(input: boolean | DevEventsOptions | undefined, env?: NodeJS.ProcessEnv): ResolvedDevEventsOptions | false;
@@ -1,6 +1,7 @@
1
- import type { TracingOptions } from '../otel/otel.types';
2
1
  import type { SinkConfig } from '../logging/log-sink.interface';
2
+ import type { TracingOptions } from '../otel/otel.types';
3
3
  import type { RequestLogCollectorOptions } from '../request-log/request-log.types';
4
+ import type { DevEventsOptions, ResolvedDevEventsOptions, ResolvedStallOptions, StallDetectionOptions } from './observability.options';
4
5
  /**
5
6
  * Full resolved options for ObservabilityPlugin.
6
7
  */
@@ -11,6 +12,10 @@ export interface ObservabilityPluginOptions {
11
12
  logging: ObservabilityLoggingOptions | false;
12
13
  /** Request log collection configuration */
13
14
  requestLogs: RequestLogCollectorOptions | false;
15
+ /** Event-loop stall detection */
16
+ stalls: ResolvedStallOptions | false;
17
+ /** The `frontmcp dev` event stream; off outside development */
18
+ devEvents: ResolvedDevEventsOptions | false;
14
19
  }
15
20
  /**
16
21
  * Logging options within the plugin.
@@ -35,4 +40,8 @@ export interface ObservabilityPluginOptionsInput {
35
40
  logging?: boolean | ObservabilityLoggingOptions;
36
41
  /** Enable/configure request log collection */
37
42
  requestLogs?: boolean | RequestLogCollectorOptions;
43
+ /** Report event-loop stalls with the calls in flight: a warning, a metric and a dev event (default: off) */
44
+ stalls?: boolean | StallDetectionOptions;
45
+ /** Write call timings, failures and stalls to the `frontmcp dev` event stream (default: on, in development only) */
46
+ devEvents?: boolean | DevEventsOptions;
38
47
  }
@@ -0,0 +1,39 @@
1
+ import { DevEventStream } from '../dev-events/dev-event-stream';
2
+ import type { ResolvedDevEventsOptions, ResolvedStallOptions } from '../plugin/observability.options';
3
+ import { StallDetector } from './stall-detector';
4
+ interface MonitorLogger {
5
+ warn(message: string): void;
6
+ }
7
+ /**
8
+ * A server's dev event stream, and its share of the process's stall detector. Its hooks keep each entry's execute stage
9
+ * in the calls in flight (an `Around` hook, so a failed call ends too) and report each call when its flow ends (a
10
+ * `Finally` hook).
11
+ */
12
+ export declare class CallMonitor {
13
+ private readonly logger;
14
+ /** One stall detector per threshold in the process, shared by the monitor of every server, so a stall is reported once. */
15
+ private static readonly sharedDetectors;
16
+ readonly stalls?: StallDetector;
17
+ readonly devEvents?: DevEventStream;
18
+ private readonly sharedStalls?;
19
+ private readonly stallCounter?;
20
+ private readonly callStarts;
21
+ constructor(stallOptions: ResolvedStallOptions | false, devEventsOptions: ResolvedDevEventsOptions | false, logger: MonitorLogger);
22
+ private static sharedDetectorFor;
23
+ onReady(): void;
24
+ onShutdown(): Promise<void>;
25
+ markToolCallStart(ctx: unknown): void;
26
+ trackToolExecution(ctx: unknown, next: () => Promise<void>): Promise<void>;
27
+ reportToolCall(ctx: unknown): void;
28
+ markResourceReadStart(ctx: unknown): void;
29
+ trackResourceExecution(ctx: unknown, next: () => Promise<void>): Promise<void>;
30
+ reportResourceRead(ctx: unknown): void;
31
+ markPromptGetStart(ctx: unknown): void;
32
+ trackPromptExecution(ctx: unknown, next: () => Promise<void>): Promise<void>;
33
+ reportPromptGet(ctx: unknown): void;
34
+ private markStart;
35
+ private track;
36
+ private reportStall;
37
+ private report;
38
+ }
39
+ export {};
@@ -0,0 +1,29 @@
1
+ /** The requests whose entries a stall can be blamed on. */
2
+ export type CallMethod = 'tools/call' | 'resources/read' | 'prompts/get';
3
+ export interface CallIdentity {
4
+ method: CallMethod;
5
+ /** The entry's full name, its app's id first (`reports:export_rows`). */
6
+ entry: string;
7
+ /** The first 12 hex characters of the session's tracing id, never the session id itself. */
8
+ session?: string;
9
+ }
10
+ export interface CallRecord extends CallIdentity {
11
+ startedAt: number;
12
+ endedAt?: number;
13
+ }
14
+ /** The calls running now, and the ones that ended recently enough to have run during a stall still being measured. */
15
+ export declare class InFlightCalls {
16
+ private readonly now;
17
+ private readonly records;
18
+ constructor(now: () => number);
19
+ /**
20
+ * Runs `body` as a call in flight. The call ends when `body` settles, whatever its outcome, or, when
21
+ * `stillRunning()` then returns a promise (an execution a timeout left running), when that promise settles.
22
+ */
23
+ track(call: CallIdentity, body: () => Promise<void>, stillRunning?: () => unknown): Promise<void>;
24
+ running(): CallRecord[];
25
+ /** The calls that ran at some point between `from` and `to`. */
26
+ during(from: number, to: number): CallRecord[];
27
+ /** Forgets the calls that ended before `time`. */
28
+ prune(time: number): void;
29
+ }
@@ -0,0 +1,6 @@
1
+ export { CallMonitor } from './call-monitor';
2
+ export { InFlightCalls } from './in-flight-calls';
3
+ export type { CallIdentity, CallMethod, CallRecord } from './in-flight-calls';
4
+ export { StallDetector } from './stall-detector';
5
+ export type { GcPause, StallDetectorOptions, StallReport, StalledCall } from './stall-detector';
6
+ export { countStall, describeStall, STALL_SECONDS_COUNTER } from './stall-report';
@@ -0,0 +1,52 @@
1
+ import { InFlightCalls, type CallIdentity } from './in-flight-calls';
2
+ /** A garbage-collection pause, on the `performance.now()` clock. */
3
+ export interface GcPause {
4
+ startTime: number;
5
+ duration: number;
6
+ }
7
+ export interface StalledCall extends CallIdentity {
8
+ /** How long the call had been running when the stall ended. */
9
+ runningMs: number;
10
+ }
11
+ export interface StallReport {
12
+ stallMs: number;
13
+ /** How much of the stall garbage collection took. */
14
+ gcMs: number;
15
+ /** `gc` when the stall less its GC time is under the threshold; else `calls`, or `unknown` with no call in flight. */
16
+ cause: 'calls' | 'gc' | 'unknown';
17
+ /** The calls in flight during the stall. */
18
+ calls: StalledCall[];
19
+ }
20
+ export interface StallDetectorOptions {
21
+ thresholdMs: number;
22
+ onStall: (report: StallReport) => void;
23
+ now?: () => number;
24
+ /** Feeds GC pauses to `record`, and returns how to stop; none where the runtime has no `perf_hooks`, so no sampling. */
25
+ observeGc?: (record: (pause: GcPause) => void) => (() => void) | undefined;
26
+ }
27
+ /**
28
+ * Samples the event loop every 10 ms and reports each gap of `thresholdMs` or more between two samples as a stall,
29
+ * with the calls in flight during it. A stall is reported on the sample after the one that measured it, once its
30
+ * GC pauses have been delivered.
31
+ */
32
+ export declare class StallDetector {
33
+ readonly calls: InFlightCalls;
34
+ readonly onStall: (report: StallReport) => void;
35
+ private readonly thresholdMs;
36
+ private readonly now;
37
+ private readonly observeGc;
38
+ private stopObservingGc?;
39
+ private timer?;
40
+ private pending;
41
+ private gcPauses;
42
+ private previousSampleAt;
43
+ constructor(options: StallDetectorOptions);
44
+ /** Whether it samples: started, on a runtime with `perf_hooks`, and not stopped. */
45
+ get running(): boolean;
46
+ start(): void;
47
+ /** Stops sampling, and reports the stall measured last if there is one. */
48
+ stop(): void;
49
+ sample(): void;
50
+ private reportPending;
51
+ private reportOf;
52
+ }
@@ -0,0 +1,9 @@
1
+ import type { TelemetryCounter } from '../telemetry/telemetry.counters';
2
+ import type { StallReport } from './stall-detector';
3
+ /** Seconds the event loop was stalled, labelled `cause` (a call's method, `gc` or `unknown`) and `entry`. */
4
+ export declare const STALL_SECONDS_COUNTER = "frontmcp_event_loop_stall_seconds_total";
5
+ export declare const STALL_SECONDS_DESCRIPTION = "Seconds the event loop was stalled, by the method and entry of each call in flight, or gc, or unknown";
6
+ /** One log line: how long the loop stalled, how much of it was GC, and the calls in flight; never their inputs. */
7
+ export declare function describeStall(report: StallReport): string;
8
+ /** Adds GC time under `gc`, and the rest of a stall to each entry in flight once, or to `unknown`. */
9
+ export declare function countStall(report: StallReport, counter: TelemetryCounter): void;
@@ -1,17 +0,0 @@
1
- /**
2
- * Field redaction utilities for structured log entries.
3
- *
4
- * Traverses objects and replaces values of sensitive fields with '[REDACTED]'.
5
- */
6
- /**
7
- * Redact sensitive fields from an object.
8
- *
9
- * Performs a shallow clone with redacted values — does not mutate the input.
10
- * Field matching is case-insensitive.
11
- *
12
- * @param obj - Object to redact fields from
13
- * @param fields - Field names to redact (case-insensitive)
14
- * @param maxDepth - Maximum recursion depth (default: 5)
15
- * @returns New object with sensitive fields replaced by '[REDACTED]'
16
- */
17
- export declare function redactFields(obj: Record<string, unknown>, fields: string[], maxDepth?: number): Record<string, unknown>;