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