[APM] Add support for versioned APIs in diagnostics tool (#167050)

This fixes a problem where versioned APIs were not supported. It also
adds a `--local` flag for easily running the diagnostics tool against a
local cluster running with default credentials (elastic/changeme)
This commit is contained in:
Søren Louv-Jansen 2023-09-30 00:56:50 +02:00 committed by GitHub
parent bfafd369a0
commit 7e32fc8432
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 185 additions and 98 deletions

View file

@ -7,103 +7,149 @@
/* eslint-disable no-console */
import { URL } from 'url';
import datemath from '@elastic/datemath';
import { errors } from '@elastic/elasticsearch';
import { AxiosError } from 'axios';
import axios, { AxiosError } from 'axios';
import yargs from 'yargs';
import { initDiagnosticsBundle } from './diagnostics_bundle';
const { argv } = yargs(process.argv.slice(2))
.option('esHost', {
type: 'string',
description: 'Elasticsearch host name',
})
.option('kbHost', {
type: 'string',
description: 'Kibana host name',
})
.option('username', {
type: 'string',
description: 'Kibana host name',
})
.option('password', {
type: 'string',
description: 'Kibana host name',
})
.option('cloudId', {
type: 'string',
})
.option('apiKey', {
type: 'string',
})
.option('rangeFrom', {
type: 'string',
description: 'Time-range start',
coerce: convertDate,
})
.option('rangeTo', {
type: 'string',
description: 'Time range end',
coerce: convertDate,
})
.option('kuery', {
type: 'string',
description: 'KQL query to filter documents by',
})
.help();
async function init() {
const { argv } = yargs(process.argv.slice(2))
.option('esHost', {
type: 'string',
description: 'Elasticsearch host name',
})
.option('kbHost', {
type: 'string',
description: 'Kibana host name',
})
.option('username', {
type: 'string',
description: 'Kibana host name',
})
.option('password', {
type: 'string',
description: 'Kibana host name',
})
.option('local', {
type: 'boolean',
description: 'Connect to local cluster',
default: false,
})
.option('cloudId', {
type: 'string',
})
.option('apiKey', {
type: 'string',
})
.option('rangeFrom', {
type: 'string',
description: 'Time-range start',
coerce: convertDate,
})
.option('rangeTo', {
type: 'string',
description: 'Time range end',
coerce: convertDate,
})
.option('kuery', {
type: 'string',
description: 'KQL query to filter documents by',
})
.help();
const { esHost, kbHost, password, username, kuery, apiKey, cloudId } = argv;
const rangeFrom = argv.rangeFrom as unknown as number;
const rangeTo = argv.rangeTo as unknown as number;
const { kuery, apiKey, cloudId } = argv;
let esHost = argv.esHost;
let kbHost = argv.kbHost;
let password = argv.password;
let username = argv.username;
if ((!esHost || !kbHost) && !cloudId) {
console.error('Either esHost and kbHost or cloudId must be provided');
process.exit(1);
}
const rangeFrom = argv.rangeFrom as unknown as number;
const rangeTo = argv.rangeTo as unknown as number;
if ((!username || !password) && !apiKey) {
console.error('Either username and password or apiKey must be provided');
process.exit(1);
}
if (argv.local) {
esHost = 'http://localhost:9200';
kbHost = 'http://127.0.0.1:5601';
password = 'changeme';
username = 'elastic';
}
if (rangeFrom) {
console.log(`rangeFrom = ${new Date(rangeFrom).toISOString()}`);
}
if ((!esHost || !kbHost) && !cloudId) {
console.error(
'Please provide either: --esHost and --kbHost or --cloudId\n'
);
if (rangeTo) {
console.log(`rangeTo = ${new Date(rangeTo).toISOString()}`);
}
console.log('Example 1:');
console.log(
'--kbHost https://foo.kb.us-west2.gcp.elastic-cloud.com --esHost https://foo.es.us-west2.gcp.elastic-cloud.com\n'
);
initDiagnosticsBundle({
esHost,
kbHost,
password,
apiKey,
cloudId,
username,
start: rangeFrom,
end: rangeTo,
kuery,
})
.then((res) => {
console.log(res);
console.log('Example 2:');
console.log('--cloudId foo:very_secret\n');
console.log('Example 3:');
console.log('--local');
process.exit(1);
}
if ((!username || !password) && !apiKey) {
console.error(
'Please provide either: --username and --password or --apiKey \n'
);
console.log('Example 1:');
console.log('--username elastic --password changeme\n');
console.log('Example 2:');
console.log('--apiKey very_secret\n');
console.log('Example 3:');
console.log('--local');
process.exit(1);
}
if (rangeFrom) {
console.log(`rangeFrom = ${new Date(rangeFrom).toISOString()}`);
}
if (rangeTo) {
console.log(`rangeTo = ${new Date(rangeTo).toISOString()}`);
}
initDiagnosticsBundle({
esHost,
kbHost: await getHostnameWithBasePath(kbHost),
password,
apiKey,
cloudId,
username,
start: rangeFrom,
end: rangeTo,
kuery,
})
.catch((err) => {
process.exitCode = 1;
if (err instanceof AxiosError && err.response?.data) {
console.error(err.response.data);
return;
}
.then((res) => {
console.log(res);
})
.catch((err) => {
process.exitCode = 1;
if (err instanceof AxiosError && err.response?.data) {
console.error(err.response.data);
return;
}
// @ts-expect-error
if (err instanceof errors.ResponseError && err.meta.body.error.reason) {
// @ts-expect-error
console.error(err.meta.body.error.reason);
return;
}
if (err instanceof errors.ResponseError && err.meta.body.error.reason) {
// @ts-expect-error
console.error(err.meta.body.error.reason);
return;
}
console.error(err);
});
console.error(err);
});
}
init();
function convertDate(dateString: string): number {
const parsed = datemath.parse(dateString);
@ -113,3 +159,38 @@ function convertDate(dateString: string): number {
throw new Error(`Incorrect argument: ${dateString}`);
}
async function getHostnameWithBasePath(kibanaHostname?: string) {
if (!kibanaHostname) {
return;
}
const parsedHostName = parseHostName(kibanaHostname);
try {
await axios.get(parsedHostName, { maxRedirects: 0 });
} catch (e) {
if (isAxiosError(e) && e.response?.status === 302) {
const location = e.response?.headers?.location ?? '';
return `${parsedHostName}${location}`;
}
throw e;
}
return parsedHostName;
}
function parseHostName(hostname: string) {
// replace localhost with 127.0.0.1
// https://github.com/node-fetch/node-fetch/issues/1624#issuecomment-1235826631
hostname = hostname.replace('localhost', '127.0.0.1');
// extract just the hostname in case user provided a full URL
const parsedUrl = new URL(hostname);
return parsedUrl.origin;
}
export function isAxiosError(e: AxiosError | Error): e is AxiosError {
return 'isAxiosError' in e;
}

View file

@ -9,7 +9,7 @@
import { Client } from '@elastic/elasticsearch';
import fs from 'fs/promises';
import axios, { AxiosInstance } from 'axios';
import axios, { AxiosRequestConfig } from 'axios';
import type { APMIndices } from '@kbn/apm-data-access-plugin/server';
import { APIReturnType } from '../../public/services/rest/create_call_apm_api';
import { getDiagnosticsBundle } from '../../server/routes/diagnostics/get_diagnostics_bundle';
@ -39,7 +39,7 @@ export async function initDiagnosticsBundle({
}) {
const auth = username && password ? { username, password } : undefined;
const apiKeyHeader = apiKey ? { Authorization: `ApiKey ${apiKey}` } : {};
const { kibanaHost } = parseCloudId(cloudId);
const parsedCloudId = parseCloudId(cloudId);
const esClient = new Client({
...(esHost ? { node: esHost } : {}),
@ -48,12 +48,17 @@ export async function initDiagnosticsBundle({
headers: { ...apiKeyHeader },
});
const kibanaClient = axios.create({
baseURL: kbHost ?? kibanaHost,
const kibanaClientOpts = {
baseURL: kbHost ?? parsedCloudId.kibanaHost,
auth,
headers: { 'kbn-xsrf': 'true', ...apiKeyHeader },
});
const apmIndices = await getApmIndices(kibanaClient);
headers: {
'kbn-xsrf': 'true',
'elastic-api-version': '2023-10-31',
...apiKeyHeader,
},
};
const apmIndices = await getApmIndices(kibanaClientOpts);
const bundle = await getDiagnosticsBundle({
esClient,
@ -62,8 +67,8 @@ export async function initDiagnosticsBundle({
end,
kuery,
});
const fleetPackageInfo = await getFleetPackageInfo(kibanaClient);
const kibanaVersion = await getKibanaVersion(kibanaClient);
const fleetPackageInfo = await getFleetPackageInfo(kibanaClientOpts);
const kibanaVersion = await getKibanaVersion(kibanaClientOpts);
await saveReportToFile({ ...bundle, fleetPackageInfo, kibanaVersion });
}
@ -79,7 +84,7 @@ async function saveReportToFile(combinedReport: DiagnosticsBundle) {
console.log(`Diagnostics report written to "${filename}"`);
}
async function getApmIndices(kibanaClient: AxiosInstance) {
async function getApmIndices(kbnClientOpts: AxiosRequestConfig) {
interface Response {
apmIndexSettings: Array<{
configurationName: string;
@ -88,8 +93,9 @@ async function getApmIndices(kibanaClient: AxiosInstance) {
}>;
}
const res = await kibanaClient.get<Response>(
'/internal/apm/settings/apm-index-settings'
const res = await axios.get<Response>(
'/internal/apm/settings/apm-index-settings',
kbnClientOpts
);
return Object.fromEntries(
@ -102,16 +108,16 @@ async function getApmIndices(kibanaClient: AxiosInstance) {
) as APMIndices;
}
async function getFleetPackageInfo(kibanaClient: AxiosInstance) {
const res = await kibanaClient.get('/api/fleet/epm/packages/apm');
async function getFleetPackageInfo(kbnClientOpts: AxiosRequestConfig) {
const res = await axios.get('/api/fleet/epm/packages/apm', kbnClientOpts);
return {
version: res.data.response.version,
isInstalled: res.data.response.status,
};
}
async function getKibanaVersion(kibanaClient: AxiosInstance) {
const res = await kibanaClient.get('/api/status');
async function getKibanaVersion(kbnClientOpts: AxiosRequestConfig) {
const res = await axios.get('/api/status', kbnClientOpts);
return res.data.version.number;
}