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-запуска.

167 lines 8.33 kB
import { CACHE_POLICIES, paramsHash, tenantKey } from './policies.js'; /** * Кэширующая обёртка над `ElsClient`. * * Дизайн — **composition over inheritance**: * - сохраняет 100% совместимую сигнатуру с ElsClient; * - не модифицирует ElsClient (additive only); * - `ctx` принимается опционально (по умолчанию — fallback context от config). * * Если cache недоступен (Redis down или disabled) — все методы прозрачно * прокидываются в underlying `ElsClient`. * * Все cache keys построены через `tenantKey()` (см. `policies.ts`), что * гарантирует tenant-isolation (cross-tenant leak guard). * * Какие методы кэшируем (см. `05-high-load.md` § 2.1): * * | Method | Class | TTL | * |----------------------|------------------|--------| * | getLogDetails | log_details | 1h | * | searchLogs | search_recent | 15s | * | topErrorMessages | top_messages | 2m | * | errorHistogram | histogram | 1m | * | trafficStats | traffic_long | 5m | * | listApps | list_apps | 30s | * | heatmap | heatmap | 5m | * | errorStats | stats_breakdown | 2m | * | groupedErrors | grouped_errors | 2m | * | baseline | baseline | 5m | * | versionRegression | version_timeline | 5m | * * Что НЕ кэшируем (см. § 2.4): * - `queryLogsJql` — высокая кардинальность ключей, hit ratio < 5%. * - `findCorrelatedErrors` — корреляция меняется при ingest. * - `findSimilarErrors` — агрегаты "за последний час" — слишком волатильны. */ export class CachedElsClient { inner; cache; policies; _log; constructor(inner, cache, policies = { ...CACHE_POLICIES }, _log) { this.inner = inner; this.cache = cache; this.policies = policies; this._log = _log; } /** Доступ к raw client (нужен для методов без кэша). */ get raw() { return this.inner; } async close() { await this.inner.close(); } // ─── Cached methods ───────────────────────────────────────────────────── async getLogDetails(traceId, ctx) { const key = this.buildKey('log_details', ctx, [traceId]); const ttl = this.policies.log_details; if (!key) return this.inner.getLogDetails(traceId); const { value } = await this.cache.cached(key, ttl, () => this.inner.getLogDetails(traceId), { toolClass: 'log_details' }); return value; } async searchLogs(params, ctx) { const key = this.buildKey('search_recent', ctx, [paramsHash(params)]); const ttl = this.policies.search_recent; if (!key) return this.inner.searchLogs(params); const { value } = await this.cache.cached(key, ttl, () => this.inner.searchLogs(params), { toolClass: 'search_recent' }); return value; } async topErrorMessages(params, ctx) { const key = this.buildKey('top_messages', ctx, [paramsHash(params)]); if (!key) return this.inner.topErrorMessages(params); const { value } = await this.cache.cached(key, this.policies.top_messages, () => this.inner.topErrorMessages(params), { toolClass: 'top_messages' }); return value; } async errorHistogram(params, ctx) { const key = this.buildKey('histogram', ctx, [paramsHash(params)]); if (!key) return this.inner.errorHistogram(params); const { value } = await this.cache.cached(key, this.policies.histogram, () => this.inner.errorHistogram(params), { toolClass: 'histogram' }); return value; } async trafficStats(params, ctx) { const key = this.buildKey('traffic_long', ctx, [paramsHash(params)]); if (!key) return this.inner.trafficStats(params); const { value } = await this.cache.cached(key, this.policies.traffic_long, () => this.inner.trafficStats(params), { toolClass: 'traffic_long' }); return value; } async listApps(ctx) { // listApps — единственный case, где key может быть только по keyPrefix // (master-key вообще без appSlug). Передаём пустой scope-tenant fallback. const key = this.buildKey('list_apps', ctx, ['v1']); if (!key) return this.inner.listApps(); const { value } = await this.cache.cached(key, this.policies.list_apps, () => this.inner.listApps(), { toolClass: 'list_apps' }); return value; } async heatmap(params, ctx) { const key = this.buildKey('heatmap', ctx, [paramsHash(params)]); if (!key) return this.inner.heatmap(params); const { value } = await this.cache.cached(key, this.policies.heatmap, () => this.inner.heatmap(params), { toolClass: 'heatmap' }); return value; } async errorStats(params, ctx) { const key = this.buildKey('stats_breakdown', ctx, [paramsHash(params)]); if (!key) return this.inner.errorStats(params); const { value } = await this.cache.cached(key, this.policies.stats_breakdown, () => this.inner.errorStats(params), { toolClass: 'stats_breakdown' }); return value; } async groupedErrors(params, ctx) { const key = this.buildKey('grouped_errors', ctx, [paramsHash(params)]); if (!key) return this.inner.groupedErrors(params); const { value } = await this.cache.cached(key, this.policies.grouped_errors, () => this.inner.groupedErrors(params), { toolClass: 'grouped_errors' }); return value; } async baseline(params, ctx) { const key = this.buildKey('baseline', ctx, [paramsHash(params)]); if (!key) return this.inner.baseline(params); const { value } = await this.cache.cached(key, this.policies.baseline, () => this.inner.baseline(params), { toolClass: 'baseline' }); return value; } async versionRegression(params, ctx) { const key = this.buildKey('version_timeline', ctx, [paramsHash(params)]); if (!key) return this.inner.versionRegression(params); const { value } = await this.cache.cached(key, this.policies.version_timeline, () => this.inner.versionRegression(params), { toolClass: 'version_timeline' }); return value; } // ─── Pass-through (не кэшируем) ──────────────────────────────────────── async findSimilarErrors(...args) { return this.inner.findSimilarErrors(...args); } async findCorrelatedErrors(...args) { return this.inner.findCorrelatedErrors(...args); } async queryLogsJql(...args) { return this.inner.queryLogsJql(...args); } async impact(...args) { return this.inner.impact(...args); } // ─── helpers ──────────────────────────────────────────────────────────── /** * Строит cache key с tenant-isolation. Возвращает null если контекст не даёт * tenant идентификатор — caller обязан в этом случае идти в bypass-path. */ buildKey(scope, ctx, parts) { const appSlug = ctx?.appSlug ?? null; const keyPrefix = ctx?.keyPrefix; try { return tenantKey(scope, appSlug, parts, keyPrefix); } catch { // Без tenant guard'а — отключаем кэш для этого вызова. return null; } } } //# sourceMappingURL=cachedElsClient.js.map