Skip to main content
Use the logger through the high-level proxy API first: enable autoLog, add request tags with ctx.log, and pass a custom logger only when logs need to leave stdout.

Quick Start

Enable automatic proxy logging where you declare the proxy.
import { fentaris, mcp, stdio } from "@fentaris/core";

const proxy = fentaris({
  servers: [
    mcp("filesystem", {
      transport: stdio({
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/srv/shared"],
      }),
    }),
  ],
  autoLog: true,
});
autoLog: true records proxied tool call start, success, and failure entries with safe context such as operation, subject, upstream server, tool, downstream transport, session id, credential source metadata, and policy outcome when available.

JSON Stdout

Use jsonConsoleLogger() when your process manager or log collector expects newline-delimited JSON on stdout.
import { fentaris, jsonConsoleLogger } from "@fentaris/core";

const proxy = fentaris({
  logger: jsonConsoleLogger(),
  autoLog: true,
});
Each log line includes level, message, timestamp, context, and metadata. Redaction stays enabled by default.

Add Request Tags

ctx.log is available in middleware, server routes, tool routes, operation routes, and lifecycle events. Use tags for stable fields that should be attached to every later log entry in the request.
proxy.use(async (ctx, next) => {
  ctx.log.setTag("traceId", ctx.transport.requestId ?? "no-trace");
  ctx.log.setTag("tenantId", String(ctx.subject?.tenant?.id ?? "unknown"));
  ctx.log.setTag("subject", ctx.subject?.id ?? "anonymous");

  ctx.log.info("request.received", {
    operation: ctx.operation,
    transport: ctx.transport.type,
  });

  return next();
});
Keep tag names short and consistent. Prefer traceId, tenantId, subject, server, and tool over free-form labels.

Use Metadata For Event Detail

Metadata should describe one log entry. Keep it structured and bounded so log queries remain predictable.
proxy.mcp("github").tool("create_issue", async (ctx, next) => {
  const result = await next();

  ctx.log.info("github.create_issue.completed", {
    allowed: ctx.policy.allowed,
    reason: ctx.policy.reason,
    server: ctx.server?.name,
    tool: ctx.tool?.proxyName,
  });

  return result;
});
Do not put raw prompts, raw tool arguments, request bodies, or secret values in metadata. Log identifiers, outcomes, durations, and bounded status fields instead.

Logging Levels

Use levels consistently so alerts and dashboards stay predictable.
ctx.log.debug("tool.input.validated", { tool: ctx.tool?.proxyName });
ctx.log.info("tool.call.completed", { tool: ctx.tool?.proxyName });
ctx.log.warn("tool.call.denied", { reason: ctx.policy.reason });
ctx.log.error("tool.call.failed", { message: "upstream failed" });
  • debug: noisy diagnostics for local development.
  • info: normal operations such as successful calls and routing decisions.
  • warn: recoverable problems, degraded behavior, and policy denies.
  • error: failed operations that need attention.
  • fatal: unrecoverable runtime failures.

Customize Auto Logs

Use the object form when you want to keep automatic logging but tune levels.
const proxy = fentaris({
  servers: [
    mcp("github", {
      transport: stdio({ command: "github-mcp-server" }),
    }),
  ],
  autoLog: {
    startLevel: "debug",
    successLevel: "info",
    failureLevel: "error",
  },
});
Set autoLog: false or omit it when another layer already records the same lifecycle and duplicate logs would make operations harder to read.

Use A Custom Logger

Pass a logger to fentaris(...) when stdout is not the right destination.
import { Logger, fentaris, mcp, stdio } from "@fentaris/core";

const logger = new Logger({
  level: "info",
  driver: {
    write(entry) {
      sendToLogPipeline({
        level: entry.level,
        message: entry.message,
        timestamp: entry.timestamp.toISOString(),
        context: entry.context,
        metadata: entry.metadata,
      });
    },
  },
});

const proxy = fentaris({
  logger,
  autoLog: true,
  servers: [
    mcp("github", {
      transport: stdio({ command: "github-mcp-server" }),
    }),
  ],
});
The driver receives structured entries. Forward the full entry when possible, then map context and metadata to the indexed fields your telemetry system expects.

Entry Shape

Custom drivers receive this shape:
type LogEntry = {
  level: "debug" | "info" | "warn" | "error" | "fatal";
  message: string;
  timestamp: Date;
  context: Record<string, unknown>;
  metadata: Record<string, unknown>;
};
context comes from the logger and child loggers created by Fentaris. metadata comes from tags, annotations, and the metadata passed to each log call. Tags are stored as metadata keys prefixed with tag..

Redaction

Redaction is enabled by default. Fentaris redacts common sensitive keys such as tokens, secrets, passwords, authorization values, API keys, and credentials. It also redacts common token-shaped string values.
const logger = new Logger({
  redact: {
    keys: [/token/i, /secret/i, /customerSsn/i],
    paths: ["payment.cardNumber"],
    replacement: "[redacted]",
  },
});
Redaction is a safety net, not a logging strategy. Avoid writing secrets and raw payloads in the first place.

Redis Driver

Use RedisLoggerDriver when you want Fentaris to append structured log entries to a Redis list.
import { Logger, RedisLoggerDriver, fentaris, mcp, stdio } from "@fentaris/core";

const logger = new Logger({
  driver: new RedisLoggerDriver({
    client: redis,
    key: "fentaris:logs",
  }),
});

const proxy = fentaris({
  logger,
  autoLog: true,
  servers: [
    mcp("github", {
      transport: stdio({ command: "github-mcp-server" }),
    }),
  ],
});
Redis is useful for local aggregation, short retention windows, and workers that transform logs before forwarding them elsewhere. For long-term production storage, forward entries to your logging platform.

Lifecycle Logs

Lifecycle events are the best place for outcome-specific logs because Fentaris has already measured the operation and knows whether it succeeded.
proxy.on("tool:success", async ({ ctx, durationMs }) => {
  ctx.log.info("tool.success", {
    server: ctx.server?.name,
    tool: ctx.tool?.proxyName,
    durationMs,
  });
});

proxy.on("resource:success", async ({ ctx, durationMs }) => {
  ctx.log.info("resource.success", {
    server: ctx.server?.name,
    uri: ctx.resource?.uri,
    durationMs,
  });
});
Use lifecycle logs for audit trails, latency reporting, and operation-level metrics. Use middleware logs for request intake and validation.

Error Handling

When middleware catches an error, log a stable message and return a safe denial or failure response.
proxy.use(async (ctx, next) => {
  try {
    return await next();
  } catch (error) {
    ctx.log.error("tool.error", {
      message: error instanceof Error ? error.message : String(error),
      operation: ctx.operation,
      server: ctx.server?.name,
      tool: ctx.tool?.proxyName,
    });

    return ctx.deny("Tool failed. Try again later.");
  }
});
Do not return internal error details to clients unless the client is trusted and the detail is safe.

Best Practices

  • Enable autoLog before adding custom entries.
  • Add traceId, tenantId, and subject tags in middleware.
  • Use stable event names such as tool.success or github.create_issue.completed.
  • Put dimensions in metadata instead of interpolating them into messages.
  • Keep metric labels and log tags bounded.
  • Use warn for policy denies and degraded behavior.
  • Use error for failed upstream calls and runtime failures.
  • Forward the complete structured entry to external logging systems.