Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import { PrometheusSerializer } from "@opentelemetry/exporter-prometheus";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { auth } from "../auth";
import {
ContextFactory,
Expand Down Expand Up @@ -94,6 +94,7 @@ import {
import { MiddlewareHandler } from "hono/types";
import { getToolsByCategory, MANAGEMENT_TOOLS } from "../tools/registry";
import { Env } from "./env";
import { devLogger } from "./utils/dev-logger";
const getHandleOAuthProtectedResourceMetadata = () =>
oAuthProtectedResourceMetadata(auth);
const getHandleOAuthDiscoveryMetadata = () => oAuthDiscoveryMetadata(auth);
Expand Down Expand Up @@ -176,8 +177,11 @@ export function createApp(options: CreateAppOptions = {}) {
}),
);

// Request logging
app.use("*", logger());
if (process.env.NODE_ENV === "production") {
app.use("*", logger());
} else {
app.use("*", devLogger());
}

// Log response body for 5xx errors
app.use("*", async (c, next) => {
Expand Down
153 changes: 153 additions & 0 deletions apps/mesh/src/api/utils/dev-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { Context, Next } from "hono";

// ANSI color codes for elegant logging
const colors = {
reset: "\x1b[0m",
dim: "\x1b[2m",
bold: "\x1b[1m",
// Request methods
GET: "\x1b[36m", // cyan
POST: "\x1b[33m", // yellow
PUT: "\x1b[35m", // magenta
DELETE: "\x1b[31m", // red
// Status codes
ok: "\x1b[32m", // green
redirect: "\x1b[36m", // cyan
clientError: "\x1b[33m", // yellow
serverError: "\x1b[31m", // red
// Special
mcp: "\x1b[35m", // magenta for MCP
tool: "\x1b[96m", // bright cyan for tool names
duration: "\x1b[90m", // gray
};

const getStatusColor = (status: number) => {
if (status >= 500) return colors.serverError;
if (status >= 400) return colors.clientError;
if (status >= 300) return colors.redirect;
return colors.ok;
};

const getMethodColor = (method: string) => {
return colors[method as keyof typeof colors] || colors.reset;
};

/**
* Sanitize strings for safe logging by removing control characters
* Prevents log forging and terminal escape injection attacks
*/
const sanitizeForLog = (str: string): string => {
return (
str
.replace(/\r/g, "") // Remove carriage returns
.replace(/\n/g, "") // Remove newlines
// eslint-disable-next-line no-control-regex
.replace(/\x1b\[[0-9;]*m/g, "") // Remove ANSI escape sequences
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
); // Remove other control characters
};

export function devLogger() {
return async (c: Context, next: Next) => {
const start = Date.now();
const method = c.req.method;
const path = c.req.path;

// Skip noisy paths
if (path === "/api/auth/get-session" || path.includes("favicon")) {
await next();
return;
}

// For MCP calls, extract tool/method info
// Note: In production, we skip body cloning for performance. Detailed parsing
// happens deeper in the call stack where the body is already parsed (e.g., in proxy routes).
let mcpInfo = "";
let isMcpCall = false;
if (path.startsWith("/mcp") && method === "POST") {
isMcpCall = true;
// Skip expensive body cloning in production
try {
// Only attempt to parse if Content-Type suggests JSON and body exists
const contentType = c.req.header("Content-Type");
if (contentType?.includes("application/json")) {
const cloned = c.req.raw.clone();
const body = (await cloned.json()) as {
method?: string;
params?: {
name?: string;
arguments?: Record<string, unknown>;
};
};
if (body.method === "tools/call" && body.params?.name) {
// Sanitize all user-provided fields before logging
const toolName = sanitizeForLog(body.params.name);
const args = body.params.arguments || {};

// For event bus calls, show the event type prominently
if (toolName === "EVENT_PUBLISH" && args.type) {
const eventType = sanitizeForLog(String(args.type));
mcpInfo = `${colors.tool}EVENT_PUBLISH${colors.reset} ${colors.bold}→ ${eventType}${colors.reset}`;
} else if (toolName === "EVENT_SUBSCRIBE" && args.eventType) {
const eventType = sanitizeForLog(String(args.eventType));
mcpInfo = `${colors.tool}EVENT_SUBSCRIBE${colors.reset} ${colors.bold}← ${eventType}${colors.reset}`;
} else if (toolName === "EVENT_UNSUBSCRIBE" && args.eventType) {
const eventType = sanitizeForLog(String(args.eventType));
mcpInfo = `${colors.tool}EVENT_UNSUBSCRIBE${colors.reset} ${colors.dim}✕ ${eventType}${colors.reset}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EVENT_UNSUBSCRIBE special log formatting never triggers

Low Severity

The condition args.eventType for EVENT_UNSUBSCRIBE will never be truthy because this tool's input schema uses subscriptionId, not eventType. The spec and implementation confirm that EVENT_UNSUBSCRIBE takes { subscriptionId: string }. As a result, the special "✕ eventType" formatting will never appear for unsubscribe operations - they will fall through to the default formatting instead.

Fix in Cursor Fix in Web

} else {
// Default: show tool name with arg keys (sanitized)
const argKeys = Object.keys(args).map((k) => sanitizeForLog(k));
const argsStr =
argKeys.length > 0
? argKeys.slice(0, 3).join(",") +
(argKeys.length > 3 ? "…" : "")
: "";
mcpInfo = `${colors.tool}${toolName}${colors.dim}(${argsStr})${colors.reset}`;
}
} else if (body.method) {
mcpInfo = `${colors.dim}${sanitizeForLog(body.method)}${colors.reset}`;
}
}
} catch {
// Ignore parse errors - body parsing failures shouldn't break the request
// Detailed error logging happens deeper in the stack
}
}

// Format path - shorten connection IDs (sanitize path for safety)
let displayPath = sanitizeForLog(path);
if (path.startsWith("/mcp/conn_")) {
const connId = path.split("/")[2] ?? "";
displayPath = `/mcp/${colors.mcp}${sanitizeForLog(connId.slice(0, 12))}…${colors.reset}`;
} else if (path === "/mcp") {
displayPath = `${colors.mcp}/mcp${colors.reset}`;
} else if (path === "/mcp/registry") {
displayPath = `${colors.mcp}/mcp/registry${colors.reset}`;
}

// Log incoming request
const methodColor = getMethodColor(method);
const arrow = isMcpCall ? "◀" : "←";
console.log(
`${colors.dim}${arrow}${colors.reset} ${methodColor}${method}${colors.reset} ${displayPath}${mcpInfo ? ` ${mcpInfo}` : ""}`,
);

// Wrap next() in try/finally to ensure completion logs always run
// even if downstream throws an error
try {
await next();
} finally {
const duration = Date.now() - start;
const status = c.res.status;
const statusColor = getStatusColor(status);
const durationStr =
duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s`;
const outArrow = isMcpCall ? "▶" : "→";

console.log(
`${colors.dim}${outArrow}${colors.reset} ${methodColor}${method}${colors.reset} ${displayPath}${mcpInfo ? ` ${mcpInfo}` : ""} ${statusColor}${status}${colors.reset} ${colors.duration}${durationStr}${colors.reset}`,
);
}
};
}