Skip to content

Commit

Permalink
feat(dashmate): doctor analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
shumkov committed Aug 27, 2024
1 parent 0fec811 commit e9727be
Show file tree
Hide file tree
Showing 11 changed files with 655 additions and 301 deletions.
259 changes: 54 additions & 205 deletions packages/dashmate/src/commands/doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,254 +2,93 @@ import process from 'process';
import { Flags } from '@oclif/core';
import { Listr } from 'listr2';
import chalk from 'chalk';
import archiveSamples from '../doctor/archiveSamples.js';
import unarchiveSamples from '../doctor/unarchiveSamples.js';
import ConfigBaseCommand from '../oclif/command/ConfigBaseCommand.js';
import Report from '../doctor/report.js';
import { DASHMATE_VERSION } from '../constants.js';
import obfuscateConfig from '../config/obfuscateConfig.js';
import Samples from '../doctor/Samples.js';
import MuteOneLineError from '../oclif/errors/MuteOneLineError.js';
import hideString from '../util/hideString.js';
import obfuscateObjectRecursive from '../util/obfuscateObjectRecursive.js';

/**
*
* @param {string} url
* @return {Promise<string>}
*/
async function fetchTextOrError(url) {
try {
const response = await fetch(url);

return await response.text();
} catch (e) {
return e.toString();
}
}

export default class DoctorCommand extends ConfigBaseCommand {
static description = 'Dashmate node diagnostic. Bring your node to a doctor';

static flags = {
...ConfigBaseCommand.flags,
verbose: Flags.boolean({ char: 'v', description: 'use verbose mode for output', default: false }),
samples: Flags.string({ char: 's', description: 'path to the samples archive', default: '' }),
};

/**
* @param {Object} args
* @param {Object} flags
* @param createRpcClient
* @param {DockerCompose} dockerCompose
* @param {getConnectionHost} getConnectionHost
* @param {Config} config
* @param createTenderdashRpcClient
* @param getServiceList
* @param getOperatingSystemInfo
* @param {analyseSamplesTask} analyseSamplesTask
* @param {collectSamplesTask} collectSamplesTask
* @param {prescriptionTask} prescriptionTask
* @return {Promise<void>}
*/
async runWithDependencies(
args,
{ verbose: isVerbose },
createRpcClient,
dockerCompose,
getConnectionHost,
{
verbose: isVerbose,
samples: samplesFile,
},
config,
createTenderdashRpcClient,
getServiceList,
getOperatingSystemInfo,
analyseSamplesTask,
collectSamplesTask,
prescriptionTask,
) {
const tasks = new Listr(
[
{
title: 'Collecting samples',
enabled: () => !samplesFile,
task: async () => collectSamplesTask(config),
},
{
title: 'Analyzing samples',
task: async () => analyseSamplesTask(config),
},
{
title: 'Prescription',
task: prescriptionTask,
options: {
persistentOutput: true,
},
},
{
title: 'Archive samples',
enabled: () => !samplesFile,
task: async (ctx, task) => {
const agreement = await task.prompt({
type: 'toggle',
name: 'confirm',
header: chalk` Dashmate is going to collect all necessary debug data from the node to create a report, including:
header: chalk` Do you want to create an archive of already collected data for further investigation?
The archive will include:
- System information
- The node configuration
- Service logs, metrics and status
Collected data will contain only anonymous information. All sensitive data like private keys or passwords is obfuscated.
Collected data will not contain only private information which is already not available publicly.
All sensitive data like private keys or passwords is obfuscated.
The report will be created as an TAR archive in {bold.cyanBright ${process.cwd()}}
You can use the report to analyze your node condition yourself or send it to the Dash Core Group ({underline.cyanBright [email protected]}) in case you need help.\n`,
message: 'Create a report?',
The archive will compressed with TAR/GZIP and placed to {bold.cyanBright ${process.cwd()}}
You can use it to analyze your node condition yourself or send it to the Dash Core Group ({underline.cyanBright [email protected]}) in case you need help.\n`,
message: 'Archive samples?',
enabled: 'Yes',
disabled: 'No',
});

if (!agreement) {
throw new Error('Operation is cancelled');
}

ctx.report = new Report();
},
},
{
title: 'System information',
task: async (ctx) => {
const osInfo = await getOperatingSystemInfo();

ctx.report.setSystemInfo(osInfo);
},
},
{
title: 'The node configuration',
task: async (ctx) => {
ctx.report.setDashmateVersion(DASHMATE_VERSION);
ctx.report.setDashmateConfig(obfuscateConfig(config));
},
},
{
title: 'Core status',
task: async (ctx) => {
const rpcClient = createRpcClient({
port: config.get('core.rpc.port'),
user: 'dashmate',
pass: config.get('core.rpc.users.dashmate.password'),
host: await getConnectionHost(config, 'core', 'core.rpc.host'),
});

const coreCalls = [
rpcClient.getBestChainLock(),
rpcClient.quorum('listextended'),
rpcClient.getBlockchainInfo(),
rpcClient.getPeerInfo(),
];

if (config.get('core.masternode.enable')) {
coreCalls.push(rpcClient.masternode('status'));
}

const [
getBestChainLock,
quorums,
getBlockchainInfo,
getPeerInfo,
masternodeStatus,
] = (await Promise.allSettled(coreCalls)).map((e) => e.value?.result || e.reason);

ctx.report.setServiceInfo('core', 'bestChainLock', getBestChainLock);
ctx.report.setServiceInfo('core', 'quorums', quorums);
ctx.report.setServiceInfo('core', 'blockchainInfo', getBlockchainInfo);
ctx.report.setServiceInfo('core', 'peerInfo', getPeerInfo);
ctx.report.setServiceInfo('core', 'masternodeStatus', masternodeStatus);
},
},
{
title: 'Tenderdash status',
enabled: () => config.get('platform.enable'),
task: async (ctx) => {
const tenderdashRPCClient = createTenderdashRpcClient({
host: config.get('platform.drive.tenderdash.rpc.host'),
port: config.get('platform.drive.tenderdash.rpc.port'),
});
task.skip();

// Tenderdash requires to pass all params, so we use basic fetch
async function fetchValidators() {
const url = `http://${config.get('platform.drive.tenderdash.rpc.host')}:${config.get('platform.drive.tenderdash.rpc.port')}/validators?request_quorum_info=true`;
const response = await fetch(url, 'GET');
return response.json();
return;
}

const [
status,
genesis,
peers,
abciInfo,
consensusState,
validators,
] = await Promise.allSettled([
tenderdashRPCClient.request('status', []),
tenderdashRPCClient.request('genesis', []),
tenderdashRPCClient.request('net_info', []),
tenderdashRPCClient.request('abci_info', []),
tenderdashRPCClient.request('dump_consensus_state', []),
fetchValidators(),
]);

ctx.report.setServiceInfo('drive_tenderdash', 'status', status);
ctx.report.setServiceInfo('drive_tenderdash', 'validators', validators);
ctx.report.setServiceInfo('drive_tenderdash', 'genesis', genesis);
ctx.report.setServiceInfo('drive_tenderdash', 'peers', peers);
ctx.report.setServiceInfo('drive_tenderdash', 'abciInfo', abciInfo);
ctx.report.setServiceInfo('drive_tenderdash', 'consensusState', consensusState);
},
},
{
title: 'Metrics',
enabled: () => config.get('platform.enable'),
task: async (ctx, task) => {
if (config.get('platform.drive.tenderdash.metrics.enabled')) {
// eslint-disable-next-line no-param-reassign
task.output = 'Reading Tenderdash metrics';

const url = `http://${config.get('platform.drive.tenderdash.rpc.host')}:${config.get('platform.drive.tenderdash.rpc.port')}/metrics`;

const result = fetchTextOrError(url);

ctx.report.setServiceInfo('drive_tenderdash', 'metrics', result);
}

if (config.get('platform.drive.abci.metrics.enabled')) {
// eslint-disable-next-line no-param-reassign
task.output = 'Reading Drive metrics';

const url = `http://${config.get('platform.drive.abci.rpc.host')}:${config.get('platform.drive.abci.rpc.port')}/metrics`;

const result = fetchTextOrError(url);

ctx.report.setServiceInfo('drive_abci', 'metrics', result);
}

if (config.get('platform.gateway.metrics.enabled')) {
// eslint-disable-next-line no-param-reassign
task.output = 'Reading Gateway metrics';

const url = `http://${config.get('platform.gateway.metrics.host')}:${config.get('platform.gateway.metrics.port')}/metrics`;

const result = fetchTextOrError(url);

ctx.report.setServiceInfo('gateway', 'metrics', result);
}
},
},
{
title: 'Logs',
task: async (ctx, task) => {
const services = await getServiceList(config);

// eslint-disable-next-line no-param-reassign
task.output = `Pulling logs from ${services.map((e) => e.name)}`;

await Promise.all(
services.map(async (service) => {
const [inspect, logs] = (await Promise.allSettled([
dockerCompose.inspectService(config, service.name),
dockerCompose.logs(config, [service.name]),
])).map((e) => e.value || e.reason);

// Hide username & external ip from logs
logs.out = logs.out.replaceAll(process.env.USER, hideString(process.env.USER));
logs.err = logs.err.replaceAll(process.env.USER, hideString(process.env.USER));

// Hide username & external ip from inspect
obfuscateObjectRecursive(inspect, (_field, value) => (typeof value === 'string'
? value.replaceAll(process.env.USER, hideString(process.env.USER)) : value));

ctx.report.setServiceInfo(service.name, 'stdOut', logs.out);
ctx.report.setServiceInfo(service.name, 'stdErr', logs.err);
ctx.report.setServiceInfo(service.name, 'dockerInspect', inspect);
}),
);
},
},
{
title: 'Create an archive',
task: async (ctx, task) => {
const archivePath = process.cwd();

await ctx.report.archive(archivePath);
await archiveSamples(ctx.samples, archivePath);

// eslint-disable-next-line no-param-reassign
task.output = chalk`Saved to {bold.cyanBright ${archivePath}/dashmate-report-${ctx.report.date.toISOString()}.tar.gz}`;
Expand All @@ -264,15 +103,25 @@ export default class DoctorCommand extends ConfigBaseCommand {
rendererOptions: {
clearOutput: false,
showTimer: isVerbose,
bottomBar: true,
// bottomBar: true,
removeEmptyLines: false,
collapse: false,
},
},
);

let samples;
if (samplesFile) {
samples = unarchiveSamples(samplesFile);
} else {
samples = new Samples();
}

try {
await tasks.run({
isVerbose,
samples,
problems: [],
});
} catch (e) {
throw new MuteOneLineError(e);
Expand Down
6 changes: 6 additions & 0 deletions packages/dashmate/src/createDIContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ import writeConfigTemplatesFactory from './templates/writeConfigTemplatesFactory
import importCoreDataTaskFactory from './listr/tasks/setup/regular/importCoreDataTaskFactory.js';
import verifySystemRequirementsTaskFactory
from './listr/tasks/setup/regular/verifySystemRequirementsTaskFactory.js';
import analyseSamplesTaskFactory from './listr/tasks/doctor/analyseSamplesTaskFactory.js';
import collectSamplesTaskFactory from './listr/tasks/doctor/collectSamplesTaskFactory.js';
import prescriptionTaskFactory from './listr/tasks/doctor/prescriptionTaskFactory.js';

/**
* @param {Object} [options]
Expand Down Expand Up @@ -306,6 +309,9 @@ export default async function createDIContainer(options = {}) {
importCoreDataTask: asFunction(importCoreDataTaskFactory).singleton(),
verifySystemRequirementsTask: asFunction(verifySystemRequirementsTaskFactory)
.singleton(),
analyseSamplesTask: asFunction(analyseSamplesTaskFactory).singleton(),
collectSamplesTask: asFunction(collectSamplesTaskFactory).singleton(),
prescriptionTask: asFunction(prescriptionTaskFactory).singleton(),
});

/**
Expand Down
23 changes: 19 additions & 4 deletions packages/dashmate/src/docker/DockerCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export default class DockerCompose {
*/
#isDockerSetupVerified = false;

/**
* @type {Error}
*/
#dockerVerifiicationError;

/**
* @type {HomeDir}
*/
Expand Down Expand Up @@ -499,14 +504,24 @@ export default class DockerCompose {
*/
async throwErrorIfNotInstalled() {
if (this.#isDockerSetupVerified) {
return;
if (this.#dockerVerifiicationError) {
throw this.#dockerVerifiicationError;
} else {
return;
}
}

this.#isDockerSetupVerified = true;
try {
await this.throwErrorIfDockerIsNotInstalled();

await this.throwErrorIfDockerIsNotInstalled();
await this.throwErrorIfDockerComposeIsNotInstalled();
} catch (e) {
this.#dockerVerifiicationError = e;

await this.throwErrorIfDockerComposeIsNotInstalled();
throw e;
} finally {
this.#isDockerSetupVerified = true;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/dashmate/src/docker/getServiceListFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function getServiceListFactory(generateEnvs, getConfigProfiles) {
/**
* Returns list of services and corresponding docker images from the config
*
* @typedef {getServiceList}
* @typedef {function} getServiceList
* @param {Config} config
* @return {Object[]}
*/
Expand Down
Loading

0 comments on commit e9727be

Please sign in to comment.