Skip to main content
The this.telemetry API is available on all execution contexts (ToolContext, ResourceContext, PromptContext, AgentContext) when observability is enabled. It provides a simple interface for creating custom spans, recording events, and setting attributes — with automatic trace context propagation.
Requires @frontmcp/observability installed and observability config enabled. See the Observability Guide for setup.

TelemetryAccessor

Available as this.telemetry in all execution contexts. One instance per request (context-scoped). Automatically inherits the current request’s trace ID, session ID, and scope.

startSpan(name, attributes?)

Create a child span under the current execution span.
startSpan(name: string, attributes?: Record<string, string | number | boolean>): TelemetrySpan
You must call span.end() or span.endWithError() when done.
const span = this.telemetry.startSpan('fetch-user', { 'user.id': userId });
try {
  const user = await this.fetch(`/api/users/${userId}`);
  span.setAttribute('user.name', user.name);
  span.end();
} catch (err) {
  span.endWithError(err);
  throw err;
}

withSpan(name, fn, attributes?)

Run a function within a span. The span is automatically ended on success or error.
withSpan<T>(
  name: string,
  fn: (span: TelemetrySpan) => Promise<T>,
  attributes?: Record<string, string | number | boolean>,
): Promise<T>
const data = await this.telemetry.withSpan('query-database', async (span) => {
  span.addEvent('query-sent');
  const results = await db.query(sql);
  span.setAttribute('rows', results.length);
  return results;
});

addEvent(name, attributes?)

Add an event to the active flow execution span (e.g., the tool span during execute()).
addEvent(name: string, attributes?: Record<string, string | number | boolean>): void
Events are lightweight markers that appear on the parent span’s timeline. Use them for milestones rather than creating child spans.
this.telemetry.addEvent('cache-checked', { hit: false });
this.telemetry.addEvent('validation-complete', { fields: 5 });
Events go on the active execution span, not a new span. If called during tool.execute(), they appear on the "tool my_tool" span. If called outside an execution context, a short-lived child span is created as a fallback.

setAttributes(attrs)

Set attributes on the active flow execution span.
setAttributes(attrs: Record<string, string | number | boolean>): void
this.telemetry.setAttributes({
  'user.tier': 'premium',
  'request.size': body.length,
  'cache.hit': true,
});

traceId

Get the current request’s trace ID. Useful for including in external API calls or logs.
get traceId(): string
const response = await this.fetch('/api/data', {
  headers: { 'X-Trace-Id': this.telemetry.traceId },
});

sessionId

Get the privacy-safe session tracing ID (16-char SHA-256 hash).
get sessionId(): string

TelemetrySpan

Returned by startSpan() and passed to withSpan() callbacks. All setter methods return this for chaining.

Methods

MethodSignatureDescription
setAttribute(key: string, value: string | number | boolean) => thisSet a single attribute
setAttributes(attrs: Record<string, string | number | boolean>) => thisSet multiple attributes
addEvent(name: string, attributes?: Record<...>) => thisAdd a named event
recordError(error: Error) => thisRecord an exception on the span
end() => voidEnd the span with OK status
endWithError(error: Error | string) => voidEnd with ERROR status
rawSpan (getter)Access the underlying OTel Span
const span = this.telemetry.startSpan('process');
span
  .setAttribute('input.size', 1024)
  .addEvent('step-1-done')
  .addEvent('step-2-done', { items: 42 });

if (failed) {
  span.recordError(new Error('processing failed'));
}

span.end(); // Preserves ERROR status if recordError was called

Counters (Metrics)

Counters are cumulative, monotonically-increasing metrics. Unlike spans (which describe one request) or events (which mark a point on a span), counters aggregate across many requests and are scraped by your monitoring backend at a steady cadence.

createCounter(name, description?)

createCounter(name: string, description?: string): Counter

interface Counter {
  inc(by?: number, attrs?: Record<string, string | number | boolean>): void;
}
const counter = this.telemetry.createCounter('my_app_widgets_total', 'Widgets processed');
counter.inc(1, { status: 'ok' });

Built-in skill counters

When skillsConfig.enabled: true is set on @FrontMcp, the framework emits the following counters automatically:
CounterAttributesDescription
frontmcp_skills_bundle_pulls_totalstatus: 'ok' | 'error', source: 'static' | 'npm' | 'saas-pull' | 'filesystem' | 'unknown', reason (errors only)Skill bundle pulls / hot-swaps
frontmcp_skills_signature_verifications_totalstatus: 'ok' | 'error'Bundle signature verifications attempted
frontmcp_skills_signature_failures_totalreason (bounded vocabulary)Signature failures, classified by reason
frontmcp_skills_replay_checks_totalstatus: 'ok' | 'error'Replay-protection checks
frontmcp_skills_replay_rejects_totalreason (bounded vocabulary)Replay rejections, classified by reason
frontmcp_skills_audit_dropped_totalreasonAudit log records dropped (queue overflow / write failure)
The framework also emits a skill.bundle.swap span (with source, bundle_id, version, skill_count, from_version attributes), and adds skill_search.query, skill_search.results, and skill_action.phase events to the active flow span when the skill HTTP catalog is exercised.

Wiring a MeterProvider

Counters become observable once you register a global OTel MeterProvider. Without one, counters still increment in an in-memory snapshot (getMetricSnapshot() from @frontmcp/observability) intended for tests and local debugging only.
import { metrics } from '@opentelemetry/api';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';

const meterProvider = new MeterProvider({
  resource: new Resource({ 'service.name': 'my-mcp-server' }),
  readers: [
    new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({
        url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/metrics',
      }),
      exportIntervalMillis: 10_000,
    }),
  ],
});

metrics.setGlobalMeterProvider(meterProvider);

Testing Utilities

Import from @frontmcp/observability:
import {
  createTestTracer,
  getFinishedSpans,
  assertSpanExists,
  assertSpanAttribute,
  findSpan,
  findSpansByAttribute,
} from '@frontmcp/observability';

createTestTracer(name?)

Create an isolated test tracer with in-memory span exporter. Does not register globally — safe for parallel tests.
function createTestTracer(name?: string): {
  tracer: Tracer;
  exporter: InMemorySpanExporter;
  provider: BasicTracerProvider;
  cleanup: () => Promise<void>;
}
const { tracer, exporter, cleanup } = createTestTracer();

afterEach(() => exporter.reset());
afterAll(() => cleanup());

assertSpanExists(spans, name)

Assert that a span with the given name exists. Returns the span or throws.
function assertSpanExists(spans: ReadableSpan[], name: string): ReadableSpan
const spans = getFinishedSpans(exporter);
const toolSpan = assertSpanExists(spans, 'tool get_weather');

assertSpanAttribute(span, key, value)

Assert a span has a specific attribute value.
function assertSpanAttribute(span: ReadableSpan, key: string, value: string | number | boolean): void

findSpan(spans, name) / findSpansByAttribute(spans, key, value)

Query helpers for finding spans in test assertions.
const dbSpan = findSpan(spans, 'database-query');
const toolSpans = findSpansByAttribute(spans, 'mcp.component.type', 'tool');

Configuration Types

ObservabilityOptionsInterface

The config object for @FrontMcp({ observability: { ... } }):
PropertyTypeDefaultDescription
tracingboolean | TracingOptionstrueEnable/configure OTel tracing
loggingboolean | LoggingOptionsfalseEnable/configure structured logging
requestLogsboolean | RequestLogOptionsfalseEnable/configure per-request log collection

TracingOptions

PropertyTypeDefaultDescription
httpSpansbooleantrueHTTP request spans
executionSpansbooleantrueTool/resource/prompt/agent spans
fetchSpansbooleantrueOutbound ctx.fetch() spans
flowStageEventsbooleantrueFlow stage events on execution spans
hookSpansbooleanfalseIndividual hook spans (verbose)
transportSpansbooleantrueSSE/HTTP transport spans
authSpansbooleantrueAuth/session verify spans
oauthSpansbooleantrueOAuth flow spans
elicitationSpansbooleantrueElicitation request/result spans
startupReportbooleantrueEmit startup telemetry on first request

SinkConfig

TypeDescriptionPlatform
{ type: 'stdout' }NDJSON to stdout (12-factor)Node.js
{ type: 'console' }console.log/warn/errorBrowser, Node.js
{ type: 'otlp', endpoint, headers? }OTLP HTTP to any backendNode.js
{ type: 'winston', logger }Forward to winston instanceNode.js
{ type: 'pino', logger }Forward to pino instanceNode.js
{ type: 'callback', fn }Custom callback functionAny

Observability Guide

Step-by-step setup with vendor integrations

Observability Feature

Overview of what FrontMCP observability provides