diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts index 6d46fe8fd230..68fec2d0c674 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts @@ -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; +} diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts index 83671f6b97fc..d7217ec0fd4e 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts @@ -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( - '/internal/apm/settings/apm-index-settings' + const res = await axios.get( + '/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; }