UNPKG

@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
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