Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,105 @@
|
||||
// Generated, do not edit. sha256:3c9603902fe01cc9593bf69315c4734fdbba824688512bed37af01059c22d688
|
||||
import * as db from '../db.js';
|
||||
import { recordApiCall } from './api-metrics.js';
|
||||
import { CONFIG, ENUMS } from '../../shared/config.js';
|
||||
|
||||
const LOG = CONFIG.INTEGRATION_LOG;
|
||||
|
||||
const MIN_SECRET_LENGTH = 4;
|
||||
|
||||
const AUTH_VALUE = /\b(Bearer|Basic)\s+\S+/gi;
|
||||
const SECRET_PARAM = /\b((?:api[_-]?key|apikey|access[_-]?token|token|key|secret|signature|password)=)[^&\s"']+/gi;
|
||||
const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/\S+/gi;
|
||||
|
||||
function cleanDetail(text, secrets) {
|
||||
if (text == null) return null;
|
||||
let s = String(text);
|
||||
for (const v of secrets) {
|
||||
if (typeof v !== 'string' || v.length < MIN_SECRET_LENGTH) continue;
|
||||
for (const form of new Set([v, encodeURIComponent(v)])) s = s.split(form).join('***');
|
||||
}
|
||||
s = s.replace(URL_TEXT, '<url>').replace(AUTH_VALUE, '$1 ***').replace(SECRET_PARAM, '$1***');
|
||||
s = s.replace(/\s+/g, ' ').trim();
|
||||
if (!s) return null;
|
||||
return s.length > LOG.DETAIL_MAX_CHARS ? `${s.slice(0, LOG.DETAIL_MAX_CHARS - 1)}…` : s;
|
||||
}
|
||||
|
||||
const KEY_PART = /^[A-Za-z][\w.-]{0,79}$/;
|
||||
const cleanKey = (key) => {
|
||||
const parts = String(key ?? '').split(/\s+/).filter(p => KEY_PART.test(p));
|
||||
return parts.length ? parts.join(' ') : null;
|
||||
};
|
||||
|
||||
const REQUEST_ID = /^[A-Za-z0-9-]{8,64}$/;
|
||||
|
||||
function timeOf(at) {
|
||||
if (at == null) return new Date().toISOString();
|
||||
const d = new Date(at);
|
||||
if (Number.isNaN(d.getTime())) throw new Error(`Invalid occurredAt: '${at}'.`);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
function prune() {
|
||||
const grens = new Date(Date.now() - LOG.RETENTION_DAYS * 86_400_000).toISOString();
|
||||
db.run(`DELETE FROM IntegrationCall WHERE occurredAt < ?`, [grens]);
|
||||
db.run(`DELETE FROM IntegrationCall
|
||||
WHERE id <= (SELECT id FROM IntegrationCall ORDER BY id DESC LIMIT 1 OFFSET ?)`, [LOG.MAX_ROWS]);
|
||||
}
|
||||
|
||||
export async function recordIntegrationCall(entry, { secrets = [] } = {}) {
|
||||
const e = entry || {};
|
||||
if (!ENUMS.INTEGRATION_TARGET.includes(e.target)) throw new Error(`Unknown integration target: '${e.target}'.`);
|
||||
if (!ENUMS.INTEGRATION_OUTCOME.includes(e.outcome)) throw new Error(`Unknown integration outcome: '${e.outcome}'.`);
|
||||
const providerKey = e.target === 'provider' ? String(e.providerKey || '') : null;
|
||||
if (e.target === 'provider' && !providerKey) throw new Error('A provider row needs a providerKey.');
|
||||
const operation = String(e.operation || '');
|
||||
if (!operation || /[?#]|:\/\//.test(operation)) throw new Error(`An operation is a path or a capability: '${operation}'.`);
|
||||
const subject = e.subject == null ? null : String(e.subject).slice(0, 100);
|
||||
const status = Number.isInteger(e.httpStatus) && e.httpStatus >= 100 && e.httpStatus <= 599 ? e.httpStatus : null;
|
||||
const latency = Number.isFinite(e.latencyMs) && e.latencyMs >= 0 ? Math.round(e.latencyMs) : null;
|
||||
const requestId = e.target === 'service' && REQUEST_ID.test(String(e.requestId ?? '')) ? e.requestId : null;
|
||||
const clean = cleanDetail(e.detail, Array.isArray(secrets) ? secrets : []);
|
||||
|
||||
db.run(`INSERT INTO IntegrationCall
|
||||
(target, providerKey, operation, subject, outcome, httpStatus, errorKey, detail, latencyMs, requestId, occurredAt)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[e.target, providerKey, operation, subject, e.outcome, status, cleanKey(e.errorKey), clean, latency, requestId,
|
||||
timeOf(e.occurredAt)]);
|
||||
prune();
|
||||
await db.persist();
|
||||
}
|
||||
|
||||
export async function countServiceCall({ capability, ok, latencyMs }) {
|
||||
const p = db.one(`SELECT id FROM Provider WHERE key = ?`, [CONFIG.MARKET_SERVICE.PROVIDER_KEY]);
|
||||
if (!p) return;
|
||||
await recordApiCall({ providerId: p.id, capability, ok: !!ok, latency_ms: latencyMs });
|
||||
}
|
||||
|
||||
export function listIntegrationCalls({ days = 7, target = null, providerKey = null, limit = 200 } = {}) {
|
||||
const sinds = new Date(Date.now() - Math.max(0, Number(days) || 0) * 86_400_000).toISOString();
|
||||
const n = Math.min(Math.max(1, Math.trunc(Number(limit)) || 200), LOG.MAX_ROWS);
|
||||
const t = target || null;
|
||||
const p = providerKey || null;
|
||||
return db.all(`SELECT * FROM IntegrationCall
|
||||
WHERE occurredAt >= ? AND (? IS NULL OR target = ?) AND (? IS NULL OR providerKey = ?)
|
||||
ORDER BY id DESC LIMIT ?`, [sinds, t, t, p, p, n]);
|
||||
}
|
||||
|
||||
export function serviceState() {
|
||||
const lastFail = db.one(`SELECT * FROM IntegrationCall WHERE target = 'service' AND outcome = 'fail'
|
||||
ORDER BY id DESC LIMIT 1`) || null;
|
||||
const row = db.one(`SELECT MAX(bucketMinute) AS at FROM ApiCallMetric
|
||||
JOIN Provider ON Provider.id = ApiCallMetric.providerId
|
||||
WHERE Provider.key = ? AND okCount > 0`, [CONFIG.MARKET_SERVICE.PROVIDER_KEY]);
|
||||
const lastOkAt = row?.at ?? null;
|
||||
if (lastFail && (!lastOkAt || lastFail.occurredAt.slice(0, 16) >= lastOkAt)) {
|
||||
return { ok: false, at: lastFail.occurredAt, lastFail, lastOkAt };
|
||||
}
|
||||
return { ok: lastOkAt ? true : null, at: lastOkAt ? `${lastOkAt}:00.000Z` : null, lastFail, lastOkAt };
|
||||
}
|
||||
|
||||
export function lastIntegrationFailures() {
|
||||
return db.all(`SELECT * FROM IntegrationCall
|
||||
WHERE id IN (SELECT MAX(id) FROM IntegrationCall GROUP BY target, providerKey)
|
||||
ORDER BY id DESC`);
|
||||
}
|
||||
Reference in new issue
Block a user