@ledgerhq/coin-modules-monitoring
Version:
Push monitoring metrics to Datadog
185 lines • 8.66 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const impl_1 = require("@ledgerhq/live-common/bridge/impl");
const test_helpers_1 = require("@ledgerhq/cryptoassets/cal-client/test-helpers");
const cryptoassets_1 = require("@ledgerhq/cryptoassets");
const rxjs_1 = require("rxjs");
const accountId_1 = require("@ledgerhq/ledger-wallet-framework/account/accountId");
const LiveConfig_1 = require("@ledgerhq/live-config/LiveConfig");
const sharedConfig_1 = require("@ledgerhq/live-common/config/sharedConfig");
const load_all_coins_1 = require("@ledgerhq/live-common/coin-modules/load-all-coins");
const currencies_1 = __importDefault(require("./currencies"));
const datadog_1 = require("./datadog");
const measure_1 = require("./measure");
function formatDist(dist, unit = "", decimals = 3) {
const round = (v) => Math.round(v * Math.pow(10, decimals)) / Math.pow(10, decimals);
return `min=${round(dist.min)}${unit}, average=${round(dist.average)}${unit}, max=${round(dist.max)}${unit}, p90=${round(dist.p90)}${unit}, p99=${round(dist.p99)}${unit}`;
}
function formatCalls(total, calls) {
return (`Total: ${total} ` +
Object.entries(calls)
.map(([domain, nbOfCalls]) => `- ${domain}: ${nbOfCalls}`)
.join(" "));
}
function formatDuration(ms) {
if (ms < 1000)
return `${ms.toFixed(0)} ms`;
const totalSeconds = Math.floor(ms / 1000);
const remainingMs = ms % 1000;
const seconds = totalSeconds % 60;
const minutes = Math.floor(totalSeconds / 60) % 60;
const hours = Math.floor(totalSeconds / 3600);
const parts = [];
if (hours)
parts.push(`${hours}h`);
if (minutes)
parts.push(`${minutes}m`);
if (seconds || (!hours && !minutes)) {
if (remainingMs > 0 && ms < 60_000) {
parts.push(`${seconds}.${remainingMs.toString().padStart(3, "0")}s`);
}
else {
parts.push(`${seconds}s`);
}
}
return parts.join(" ");
}
function hasCauseProperty(object) {
return (typeof object === "object" &&
object !== null &&
"cause" in object &&
typeof object.cause === "object" &&
object.cause !== null);
}
function objectWithoutKeys(obj, excludeKeys) {
const set = new Set(excludeKeys);
return Object.fromEntries(Object.entries(obj).filter(([key]) => !set.has(key)));
}
function logErrorProperties(error) {
const errorProperties = objectWithoutKeys(error, ["message", "stack", "cause"]);
console.error(` └─ Error properties:`, errorProperties);
if (hasCauseProperty(error)) {
const causeProperties = objectWithoutKeys(error.cause, ["stack", "cause"]);
console.error(` └─ Error cause properties:`, causeProperties);
}
}
function prettyLog(i, nbOfAccounts, scanDuration, syncDuration, scanCalls, scanRoutes, scanCpu, scanMem, syncCalls, syncRoutes, syncCpu, syncMem) {
const totalDuration = scanDuration + syncDuration;
console.log(`\n[${i} / ${nbOfAccounts}] ✅ Completed in ${formatDuration(totalDuration)}`);
console.log(` ┌─ 🔎 Scan`);
console.log(` │ • Calls: ${formatCalls(scanCalls, scanRoutes)}`);
console.log(` │ • CPU : ${formatDist(scanCpu, "%")}`);
console.log(` │ • MEM : ${formatDist(scanMem, " MB")}`);
console.log(` └─ 🔄 Sync`);
console.log(` • Calls: ${formatCalls(syncCalls, syncRoutes)}`);
console.log(` • CPU : ${formatDist(syncCpu, "%")}`);
console.log(` • MEM : ${formatDist(syncMem, " MB")}\n`);
}
function toEmptyAccount(currency, info) {
const id = (0, accountId_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: info.xpub ?? info.address,
derivationMode: info.derivationMode ?? "",
});
return {
id,
currency,
freshAddress: info.address,
xpub: info.xpub,
derivationMode: info.derivationMode ?? "",
operations: [],
pendingOperations: [],
balanceHistoryCache: {},
blockHeight: 0,
};
}
function getSync(currency) {
const bridge = (0, impl_1.getAccountBridgeByFamily)(currency.family);
return async (account) => (0, rxjs_1.firstValueFrom)((await bridge)
.sync(account, { paginationConfig: {} })
.pipe((0, rxjs_1.reduce)((a, f) => f(a), account)));
}
async function default_1(currencyIds, accountTypes) {
(0, load_all_coins_1.registerAllCoins)();
LiveConfig_1.LiveConfig.setConfig(sharedConfig_1.liveConfig);
// Setup CAL client store for monitoring (automatically set as global store)
(0, test_helpers_1.setupCalClientStore)();
const result = {
entries: [],
failed: false,
};
const nbOfAccounts = currencyIds
.flatMap(currencyId => Object.keys(currencies_1.default[currencyId].accounts))
.filter(currencyAccountType => accountTypes.some(type => type === currencyAccountType)).length;
let i = 0;
console.log(`Monitoring ${nbOfAccounts} account(s) within ${currencyIds.join(", ")}`);
for (const currencyId of currencyIds) {
const monitored = currencies_1.default[currencyId];
const currency = (0, cryptoassets_1.getCryptoCurrencyById)(currencyId);
const sync = getSync(currency);
for (const accountType of accountTypes) {
const info = monitored.accounts[accountType];
if (!info) {
console.log(`\nSkipping currency = "${currencyId}", no account = "${accountType}"`);
continue;
}
console.log(`\n[${++i} / ${nbOfAccounts}] Start (currency = "${currencyId}" account = "${accountType}")`);
try {
const startScan = Date.now();
const { result: initialAccount, totalCalls: scanCalls, callsByDomain: scanRoutes, cpu: scanCpu, memory: scanMem, } = await (0, measure_1.measureCalls)(() => sync(toEmptyAccount(currency, info)));
const endScan = Date.now();
const startSync = Date.now();
const { totalCalls: syncCalls, callsByDomain: syncRoutes, cpu: syncCpu, memory: syncMem, } = await (0, measure_1.measureCalls)(() => sync(initialAccount));
const endSync = Date.now();
const scanDuration = endScan - startScan;
const syncDuration = endSync - startSync;
const { xpubOrAddress } = (0, accountId_1.decodeAccountId)(initialAccount.id);
prettyLog(i, nbOfAccounts, scanDuration, syncDuration, scanCalls, scanRoutes, scanCpu, scanMem, syncCalls, syncRoutes, syncCpu, syncMem);
result.entries.push({
duration: scanDuration,
currencyName: currency.id,
coinModuleName: currency.family,
operationType: "scan",
accountType: accountType,
transactions: initialAccount.operationsCount,
accountAddressOrXpub: xpubOrAddress,
totalNetworkCalls: scanCalls,
networkCallsByDomain: scanRoutes,
cpu: scanCpu,
memory: scanMem,
}, {
duration: syncDuration,
currencyName: currency.id,
coinModuleName: currency.family,
operationType: "sync",
accountType: accountType,
transactions: initialAccount.operationsCount,
accountAddressOrXpub: xpubOrAddress,
totalNetworkCalls: syncCalls,
networkCallsByDomain: syncRoutes,
cpu: syncCpu,
memory: syncMem,
});
}
catch (err) {
console.error(`Skipping failing run. Error: ${err instanceof Error ? err.stack ?? err.message : err}`);
// We may miss some parameters added to the error on runtime
// We display only the message on the top for convenience and better readability
// So we added this log to have the full object
if (err instanceof Error) {
logErrorProperties(err);
}
result.failed = true;
}
}
}
await (0, datadog_1.submitLogs)(result.entries);
return result;
}
//# sourceMappingURL=run.js.map