@inso_web/els-mcp
Version:
MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.
184 lines • 8.1 kB
JavaScript
import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics, } from 'prom-client';
/**
* Prometheus metrics для MCP-сервера.
*
* Источник истины по именам — `todo/error-logs-service/mcp/07-observability.md`
* § 1. Категории:
* - RED (rate / errors / duration) — `mcp_requests_total`, `mcp_request_duration_seconds`, `mcp_errors_total`
* - Cache — `mcp_cache_hits_total`, `mcp_cache_misses_total`, `mcp_cache_hit_ratio`
* - Upstream (ELS) — `mcp_els_upstream_errors_total`
* - SSE / sessions — `mcp_sse_connections_active`
* - Security — `mcp_redaction_applied_total`
* - Billing — `mcp_billing_events_total`
*
* Регистр — singleton (один на процесс). Endpoint `/els/metrics` сериализует
* текущее состояние в Prometheus text format.
*/
export const registry = new Registry();
// Default метрики (process_cpu, nodejs_eventloop_lag, etc) — собираются автоматически.
collectDefaultMetrics({ register: registry, prefix: 'mcp_' });
// ─── RED (rate / errors / duration) ───────────────────────────────────────
export const mcpRequestsTotal = new Counter({
name: 'mcp_requests_total',
help: 'Total tool-call requests',
labelNames: ['tool', 'status', 'cached'],
registers: [registry],
});
export const mcpRequestDuration = new Histogram({
name: 'mcp_request_duration_seconds',
help: 'Tool-call duration histogram',
labelNames: ['tool'],
// 12 buckets per spec § 1.1 (07-observability.md)
buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30],
registers: [registry],
});
export const mcpErrorsTotal = new Counter({
name: 'mcp_errors_total',
help: 'Total tool-call errors by code',
labelNames: ['tool', 'code'],
registers: [registry],
});
// ─── Cache ───────────────────────────────────────────────────────────────
export const mcpCacheHitsTotal = new Counter({
name: 'mcp_cache_hits_total',
help: 'Cache hits per tool-class',
labelNames: ['tool_class'],
registers: [registry],
});
export const mcpCacheMissesTotal = new Counter({
name: 'mcp_cache_misses_total',
help: 'Cache misses per tool-class',
labelNames: ['tool_class'],
registers: [registry],
});
export const mcpCacheHitRatio = new Gauge({
name: 'mcp_cache_hit_ratio',
help: 'Cache hit ratio per tool-class (manually updated every 10s)',
labelNames: ['tool_class'],
registers: [registry],
});
// ─── Upstream (ELS) ──────────────────────────────────────────────────────
export const mcpElsUpstreamErrorsTotal = new Counter({
name: 'mcp_els_upstream_errors_total',
help: 'Total upstream ELS errors (HTTP status >= 400)',
labelNames: ['endpoint', 'status'],
registers: [registry],
});
// ─── SSE / sessions ──────────────────────────────────────────────────────
export const mcpSseConnectionsActive = new Gauge({
name: 'mcp_sse_connections_active',
help: 'Active SSE connections (for HPA)',
registers: [registry],
});
// ─── Security ────────────────────────────────────────────────────────────
export const mcpRedactionAppliedTotal = new Counter({
name: 'mcp_redaction_applied_total',
help: 'Number of times redaction was applied per field',
labelNames: ['field'],
registers: [registry],
});
export const mcpAuthRejectionsTotal = new Counter({
name: 'mcp_auth_rejections_total',
help: 'Total auth rejections by reason',
labelNames: ['reason'],
registers: [registry],
});
export const mcpPromptInjectionBlockedTotal = new Counter({
name: 'mcp_prompt_injection_blocked_total',
help: 'Times suspicious content was detected and (potentially) blocked, by rule',
labelNames: ['rule'],
registers: [registry],
});
export const mcpSseConnectionsActiveByApp = new Gauge({
name: 'mcp_sse_connections_active_by_app',
help: 'Active SSE/MCP sessions per appSlug (per-tenant cap enforcement)',
labelNames: ['appSlug'],
registers: [registry],
});
export const mcpSseRejectionsTotal = new Counter({
name: 'mcp_sse_rejections_total',
help: 'SSE/MCP session-create rejections by reason (concurrency, tier...)',
labelNames: ['reason', 'appSlug'],
registers: [registry],
});
// ─── Billing ─────────────────────────────────────────────────────────────
export const mcpBillingEventsTotal = new Counter({
name: 'mcp_billing_events_total',
help: 'Per-tool counter for billing (appSlug, tier)',
labelNames: ['appSlug', 'tier'],
registers: [registry],
});
// ─── Helpers (используются из cache wrapper и tools) ─────────────────────
/**
* Внутренние счётчики per tool-class — для расчёта `mcp_cache_hit_ratio`
* без блокировки на recording rules Prometheus.
*/
const cacheStats = {};
export function recordCacheHit(toolClass) {
mcpCacheHitsTotal.inc({ tool_class: toolClass });
const s = cacheStats[toolClass] ?? { hits: 0, misses: 0 };
s.hits += 1;
cacheStats[toolClass] = s;
updateHitRatio(toolClass, s);
}
export function recordCacheMiss(toolClass) {
mcpCacheMissesTotal.inc({ tool_class: toolClass });
const s = cacheStats[toolClass] ?? { hits: 0, misses: 0 };
s.misses += 1;
cacheStats[toolClass] = s;
updateHitRatio(toolClass, s);
}
function updateHitRatio(toolClass, s) {
const total = s.hits + s.misses;
if (total === 0)
return;
mcpCacheHitRatio.set({ tool_class: toolClass }, s.hits / total);
}
export function recordToolRequest(tool, status, cached, durationSec) {
mcpRequestsTotal.inc({ tool, status, cached: String(cached) });
mcpRequestDuration.observe({ tool }, durationSec);
}
export function recordToolError(tool, code) {
mcpErrorsTotal.inc({ tool, code });
}
export function recordUpstreamError(endpoint, status) {
mcpElsUpstreamErrorsTotal.inc({ endpoint, status: String(status) });
}
export function recordRedaction(field) {
mcpRedactionAppliedTotal.inc({ field });
}
export function recordBillingEvent(appSlug, tier) {
mcpBillingEventsTotal.inc({ appSlug, tier });
}
export function recordAuthRejection(reason) {
mcpAuthRejectionsTotal.inc({ reason });
}
export function recordPromptInjectionBlocked(rule) {
mcpPromptInjectionBlockedTotal.inc({ rule });
}
export function incSseActive(appSlug) {
mcpSseConnectionsActiveByApp.inc({ appSlug });
mcpSseConnectionsActive.inc();
}
export function decSseActive(appSlug) {
mcpSseConnectionsActiveByApp.dec({ appSlug });
mcpSseConnectionsActive.dec();
}
export function recordSseRejection(reason, appSlug) {
mcpSseRejectionsTotal.inc({ reason, appSlug });
}
/** Сброс per-process состояния (для тестов). */
export function resetMetricsForTests() {
registry.resetMetrics();
for (const k of Object.keys(cacheStats)) {
delete cacheStats[k];
}
}
/** Сериализация в Prometheus text format. */
export async function getMetricsText() {
return registry.metrics();
}
export function getMetricsContentType() {
return registry.contentType;
}
//# sourceMappingURL=metrics.js.map