[APM] Add elasticsearch queries to api response (#95146)
This commit is contained in:
parent
5732526f34
commit
84adfe551b
|
@ -412,4 +412,8 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:enableInspectEsQueries': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface UsageStats {
|
|||
'apm:enableSignificantTerms': boolean;
|
||||
'apm:enableServiceOverview': boolean;
|
||||
'observability:enableAlertingExperience': boolean;
|
||||
'observability:enableInspectEsQueries': boolean;
|
||||
'visualize:enableLabs': boolean;
|
||||
'visualization:heatmap:maxBuckets': number;
|
||||
'visualization:colorMapping': string;
|
||||
|
|
|
@ -8032,6 +8032,12 @@
|
|||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:enableInspectEsQueries": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
32
x-pack/plugins/apm/common/apm_api/parse_endpoint.ts
Normal file
32
x-pack/plugins/apm/common/apm_api/parse_endpoint.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
type Method = 'get' | 'post' | 'put' | 'delete';
|
||||
|
||||
export function parseEndpoint(
|
||||
endpoint: string,
|
||||
pathParams: Record<string, any> = {}
|
||||
) {
|
||||
const [method, rawPathname] = endpoint.split(' ');
|
||||
|
||||
// replace template variables with path params
|
||||
const pathname = Object.keys(pathParams).reduce((acc, paramName) => {
|
||||
return acc.replace(`{${paramName}}`, pathParams[paramName]);
|
||||
}, rawPathname);
|
||||
|
||||
return { method: parseMethod(method), pathname };
|
||||
}
|
||||
|
||||
export function parseMethod(method: string) {
|
||||
const res = method.trim().toLowerCase() as Method;
|
||||
|
||||
if (!['get', 'post', 'put', 'delete'].includes(res)) {
|
||||
throw new Error('Endpoint was not prefixed with a valid HTTP method');
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
|
@ -45,10 +45,10 @@ describe('strictKeysRt', () => {
|
|||
{
|
||||
type: t.intersection([
|
||||
t.type({ query: t.type({ bar: t.string }) }),
|
||||
t.partial({ query: t.partial({ _debug: t.boolean }) }),
|
||||
t.partial({ query: t.partial({ _inspect: t.boolean }) }),
|
||||
]),
|
||||
passes: [{ query: { bar: '', _debug: true } }],
|
||||
fails: [{ query: { _debug: true } }],
|
||||
passes: [{ query: { bar: '', _inspect: true } }],
|
||||
fails: [{ query: { _inspect: true } }],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -91,12 +91,12 @@ describe('strictKeysRt', () => {
|
|||
} as Record<string, any>);
|
||||
|
||||
const typeB = t.partial({
|
||||
query: t.partial({ _debug: jsonRt.pipe(t.boolean) }),
|
||||
query: t.partial({ _inspect: jsonRt.pipe(t.boolean) }),
|
||||
});
|
||||
|
||||
const value = {
|
||||
query: {
|
||||
_debug: 'true',
|
||||
_inspect: 'true',
|
||||
filterNames: JSON.stringify(['host', 'agentName']),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { act } from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public';
|
||||
import { AppMountParameters, CoreStart } from 'src/core/public';
|
||||
import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
|
||||
import { createCallApmApi } from '../services/rest/createCallApmApi';
|
||||
|
@ -72,7 +72,7 @@ describe('renderApp', () => {
|
|||
embeddable,
|
||||
};
|
||||
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
|
||||
createCallApmApi((core.http as unknown) as HttpSetup);
|
||||
createCallApmApi((core as unknown) as CoreStart);
|
||||
|
||||
jest
|
||||
.spyOn(window.console, 'warn')
|
||||
|
|
|
@ -118,7 +118,7 @@ export const renderApp = (
|
|||
) => {
|
||||
const { element } = appMountParameters;
|
||||
|
||||
createCallApmApi(core.http);
|
||||
createCallApmApi(core);
|
||||
|
||||
// Automatically creates static index pattern and stores as saved object
|
||||
createStaticIndexPattern().catch((e) => {
|
||||
|
|
|
@ -120,7 +120,7 @@ export const renderApp = (
|
|||
// render APM feedback link in global help menu
|
||||
setHelpExtension(core);
|
||||
setReadonlyBadge(core);
|
||||
createCallApmApi(core.http);
|
||||
createCallApmApi(core);
|
||||
|
||||
// Automatically creates static index pattern and stores as saved object
|
||||
createStaticIndexPattern().catch((e) => {
|
||||
|
|
|
@ -107,7 +107,11 @@ export function ErrorCountAlertTrigger(props: Props) {
|
|||
];
|
||||
|
||||
const chartPreview = (
|
||||
<ChartPreview data={data} threshold={threshold} yTickFormat={asInteger} />
|
||||
<ChartPreview
|
||||
data={data?.errorCountChartPreview}
|
||||
threshold={threshold}
|
||||
yTickFormat={asInteger}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -13,7 +13,6 @@ import { useParams } from 'react-router-dom';
|
|||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||
import { TimeSeries } from '../../../../typings/timeseries';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
|
@ -116,9 +115,9 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
]
|
||||
);
|
||||
|
||||
const maxY = getMaxY([
|
||||
{ data: data ?? [] } as TimeSeries<{ x: number; y: number | null }>,
|
||||
]);
|
||||
const latencyChartPreview = data?.latencyChartPreview ?? [];
|
||||
|
||||
const maxY = getMaxY([{ data: latencyChartPreview }]);
|
||||
const formatter = getDurationFormatter(maxY);
|
||||
const yTickFormat = getResponseTimeTickFormatter(formatter);
|
||||
|
||||
|
@ -127,7 +126,7 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
|
||||
const chartPreview = (
|
||||
<ChartPreview
|
||||
data={data}
|
||||
data={latencyChartPreview}
|
||||
threshold={thresholdMs}
|
||||
yTickFormat={yTickFormat}
|
||||
/>
|
||||
|
|
|
@ -132,7 +132,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
|
||||
const chartPreview = (
|
||||
<ChartPreview
|
||||
data={data}
|
||||
data={data?.errorRateChartPreview}
|
||||
yTickFormat={(d: number | null) => asPercent(d, 1)}
|
||||
threshold={thresholdAsPercent}
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ export function BreakdownSeries({
|
|||
? EUI_CHARTS_THEME_DARK
|
||||
: EUI_CHARTS_THEME_LIGHT;
|
||||
|
||||
const { data, status } = useBreakdowns({
|
||||
const { breakdowns, status } = useBreakdowns({
|
||||
field,
|
||||
value,
|
||||
percentileRange,
|
||||
|
@ -49,7 +49,7 @@ export function BreakdownSeries({
|
|||
// so don't user that here
|
||||
return (
|
||||
<>
|
||||
{data?.map(({ data: seriesData, name }, sortIndex) => (
|
||||
{breakdowns.map(({ data: seriesData, name }, sortIndex) => (
|
||||
<LineSeries
|
||||
id={`${field}-${value}-${name}`}
|
||||
key={`${field}-${value}-${name}`}
|
||||
|
|
|
@ -95,13 +95,13 @@ export function PageLoadDistribution() {
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<PageLoadDistChart
|
||||
data={data}
|
||||
data={data?.pageLoadDistribution}
|
||||
onPercentileChange={onPercentileChange}
|
||||
loading={status !== 'success'}
|
||||
breakdown={breakdown}
|
||||
percentileRange={{
|
||||
max: percentileRange.max || data?.maxDuration,
|
||||
min: percentileRange.min || data?.minDuration,
|
||||
max: percentileRange.max || data?.pageLoadDistribution?.maxDuration,
|
||||
min: percentileRange.min || data?.pageLoadDistribution?.minDuration,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -17,12 +17,10 @@ interface Props {
|
|||
|
||||
export const useBreakdowns = ({ percentileRange, field, value }: Props) => {
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
|
||||
const { start, end, searchTerm } = urlParams;
|
||||
|
||||
const { min: minP, max: maxP } = percentileRange ?? {};
|
||||
|
||||
return useFetcher(
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end && field && value) {
|
||||
return callApmApi({
|
||||
|
@ -47,4 +45,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => {
|
|||
},
|
||||
[end, start, uiFilters, field, value, minP, maxP, searchTerm]
|
||||
);
|
||||
|
||||
return { breakdowns: data?.pageLoadDistBreakdown ?? [], status };
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ export function MainFilters() {
|
|||
[start, end]
|
||||
);
|
||||
|
||||
const rumServiceNames = data?.rumServices ?? [];
|
||||
const { isSmall } = useBreakPoints();
|
||||
|
||||
// on mobile we want it to take full width
|
||||
|
@ -48,7 +49,7 @@ export function MainFilters() {
|
|||
<EuiFlexItem grow={false}>
|
||||
<ServiceNameFilter
|
||||
loading={status !== 'success'}
|
||||
serviceNames={data ?? []}
|
||||
serviceNames={rumServiceNames}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={envStyle}>
|
||||
|
|
|
@ -68,7 +68,7 @@ export function useLocalUIFilters({
|
|||
});
|
||||
};
|
||||
|
||||
const { data = getInitialData(filterNames), status } = useFetcher(
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (shouldFetch && urlParams.start && urlParams.end) {
|
||||
return callApmApi({
|
||||
|
@ -96,7 +96,8 @@ export function useLocalUIFilters({
|
|||
]
|
||||
);
|
||||
|
||||
const filters = data.map((filter) => ({
|
||||
const localUiFilters = data?.localUiFilters ?? getInitialData(filterNames);
|
||||
const filters = localUiFilters.map((filter) => ({
|
||||
...filter,
|
||||
value: values[filter.name] || [],
|
||||
}));
|
||||
|
|
|
@ -11,9 +11,9 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug
|
|||
import { FetchOptions } from '../../../../../common/fetch_options';
|
||||
|
||||
export function useCallApi() {
|
||||
const { http } = useApmPluginContext().core;
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
return useMemo(() => {
|
||||
return <T = void>(options: FetchOptions) => callApi<T>(http, options);
|
||||
}, [http]);
|
||||
return <T = void>(options: FetchOptions) => callApi<T>(core, options);
|
||||
}, [core]);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import cytoscape from 'cytoscape';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
|
@ -21,19 +21,21 @@ export default {
|
|||
component: Popover,
|
||||
decorators: [
|
||||
(Story: ComponentType) => {
|
||||
const httpMock = ({
|
||||
get: async () => ({
|
||||
avgCpuUsage: 0.32809666568309237,
|
||||
avgErrorRate: 0.556068173242986,
|
||||
avgMemoryUsage: 0.5504868173242986,
|
||||
transactionStats: {
|
||||
avgRequestsPerMinute: 164.47222031860858,
|
||||
avgTransactionDuration: 61634.38905590272,
|
||||
},
|
||||
}),
|
||||
} as unknown) as HttpSetup;
|
||||
const coreMock = ({
|
||||
http: {
|
||||
get: async () => ({
|
||||
avgCpuUsage: 0.32809666568309237,
|
||||
avgErrorRate: 0.556068173242986,
|
||||
avgMemoryUsage: 0.5504868173242986,
|
||||
transactionStats: {
|
||||
avgRequestsPerMinute: 164.47222031860858,
|
||||
avgTransactionDuration: 61634.38905590272,
|
||||
},
|
||||
}),
|
||||
},
|
||||
} as unknown) as CoreStart;
|
||||
|
||||
createCallApmApi(httpMock);
|
||||
createCallApmApi(coreMock);
|
||||
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
|
|
|
@ -33,7 +33,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
||||
const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher(
|
||||
const { data: serviceNamesData, status: serviceNamesStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/services',
|
||||
|
@ -43,8 +43,9 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
[],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
const serviceNames = serviceNamesData?.serviceNames ?? [];
|
||||
|
||||
const { data: environments = [], status: environmentStatus } = useFetcher(
|
||||
const { data: environmentsData, status: environmentsStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (newConfig.service.name) {
|
||||
return callApmApi({
|
||||
|
@ -59,6 +60,8 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const environments = environmentsData?.environments ?? [];
|
||||
|
||||
const { status: agentNameStatus } = useFetcher(
|
||||
async (callApmApi) => {
|
||||
const serviceName = newConfig.service.name;
|
||||
|
@ -153,11 +156,11 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
'xpack.apm.agentConfig.servicePage.environment.fieldLabel',
|
||||
{ defaultMessage: 'Service environment' }
|
||||
)}
|
||||
isLoading={environmentStatus === FETCH_STATUS.LOADING}
|
||||
isLoading={environmentsStatus === FETCH_STATUS.LOADING}
|
||||
options={environmentOptions}
|
||||
value={newConfig.service.environment}
|
||||
disabled={
|
||||
!newConfig.service.name || environmentStatus === FETCH_STATUS.LOADING
|
||||
!newConfig.service.name || environmentsStatus === FETCH_STATUS.LOADING
|
||||
}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types';
|
||||
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
|
||||
|
@ -23,10 +23,10 @@ storiesOf(
|
|||
module
|
||||
)
|
||||
.addDecorator((storyFn) => {
|
||||
const httpMock = {};
|
||||
const coreMock = ({} as unknown) as CoreStart;
|
||||
|
||||
// mock
|
||||
createCallApmApi((httpMock as unknown) as HttpSetup);
|
||||
createCallApmApi(coreMock);
|
||||
|
||||
const contextMock = {
|
||||
core: {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../../../../../services/rest/createCallApmApi';
|
||||
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0];
|
||||
type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0];
|
||||
|
||||
interface Props {
|
||||
config: Config;
|
||||
|
|
|
@ -32,15 +32,19 @@ import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable';
|
|||
import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
|
||||
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
|
||||
|
||||
type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0];
|
||||
type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0];
|
||||
|
||||
interface Props {
|
||||
status: FETCH_STATUS;
|
||||
data: Config[];
|
||||
configurations: Config[];
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export function AgentConfigurationList({ status, data, refetch }: Props) {
|
||||
export function AgentConfigurationList({
|
||||
status,
|
||||
configurations,
|
||||
refetch,
|
||||
}: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const canSave = core.application.capabilities.apm.save;
|
||||
const { basePath } = core.http;
|
||||
|
@ -113,7 +117,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
|
|||
return failurePrompt;
|
||||
}
|
||||
|
||||
if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) {
|
||||
if (status === FETCH_STATUS.SUCCESS && isEmpty(configurations)) {
|
||||
return emptyStatePrompt;
|
||||
}
|
||||
|
||||
|
@ -231,7 +235,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
|
|||
<ManagedTable
|
||||
noItemsMessage={<LoadingStatePrompt />}
|
||||
columns={columns}
|
||||
items={data}
|
||||
items={configurations}
|
||||
initialSortField="service.name"
|
||||
initialSortDirection="asc"
|
||||
initialPageSize={20}
|
||||
|
|
|
@ -25,8 +25,10 @@ import { useFetcher } from '../../../../hooks/use_fetcher';
|
|||
import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks';
|
||||
import { AgentConfigurationList } from './List';
|
||||
|
||||
const INITIAL_DATA = { configurations: [] };
|
||||
|
||||
export function AgentConfigurations() {
|
||||
const { refetch, data = [], status } = useFetcher(
|
||||
const { refetch, data = INITIAL_DATA, status } = useFetcher(
|
||||
(callApmApi) =>
|
||||
callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration' }),
|
||||
[],
|
||||
|
@ -36,7 +38,7 @@ export function AgentConfigurations() {
|
|||
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
|
||||
useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 });
|
||||
|
||||
const hasConfigurations = !isEmpty(data);
|
||||
const hasConfigurations = !isEmpty(data.configurations);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -72,7 +74,11 @@ export function AgentConfigurations() {
|
|||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<AgentConfigurationList status={status} data={data} refetch={refetch} />
|
||||
<AgentConfigurationList
|
||||
status={status}
|
||||
configurations={data.configurations}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,10 @@ import React, { useEffect, useState } from 'react';
|
|||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { clearCache } from '../../../../services/rest/callApi';
|
||||
import { callApmApi } from '../../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
APIReturnType,
|
||||
callApmApi,
|
||||
} from '../../../../services/rest/createCallApmApi';
|
||||
|
||||
const APM_INDEX_LABELS = [
|
||||
{
|
||||
|
@ -84,8 +87,10 @@ async function saveApmIndices({
|
|||
clearCache();
|
||||
}
|
||||
|
||||
type ApiResponse = APIReturnType<`GET /api/apm/settings/apm-index-settings`>;
|
||||
|
||||
// avoid infinite loop by initializing the state outside the component
|
||||
const INITIAL_STATE = [] as [];
|
||||
const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] };
|
||||
|
||||
export function ApmIndices() {
|
||||
const { core } = useApmPluginContext();
|
||||
|
@ -108,7 +113,7 @@ export function ApmIndices() {
|
|||
|
||||
useEffect(() => {
|
||||
setApmIndices(
|
||||
data.reduce(
|
||||
data.apmIndexSettings.reduce(
|
||||
(acc, { configurationName, savedValue }) => ({
|
||||
...acc,
|
||||
[configurationName]: savedValue,
|
||||
|
@ -190,7 +195,7 @@ export function ApmIndices() {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiForm>
|
||||
{APM_INDEX_LABELS.map(({ configurationName, label }) => {
|
||||
const matchedConfiguration = data.find(
|
||||
const matchedConfiguration = data.apmIndexSettings.find(
|
||||
({ configurationName: configName }) =>
|
||||
configName === configurationName
|
||||
);
|
||||
|
|
|
@ -24,20 +24,12 @@ import {
|
|||
} from '../../../../../utils/testHelpers';
|
||||
import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'label 1',
|
||||
url: 'url 1',
|
||||
'service.name': 'opbeans-java',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'label 2',
|
||||
url: 'url 2',
|
||||
'transaction.type': 'request',
|
||||
},
|
||||
];
|
||||
const data = {
|
||||
customLinks: [
|
||||
{ id: '1', label: 'label 1', url: 'url 1', 'service.name': 'opbeans-java' },
|
||||
{ id: '2', label: 'label 2', url: 'url 2', 'transaction.type': 'request' },
|
||||
],
|
||||
};
|
||||
|
||||
function getMockAPMContext({ canSave }: { canSave: boolean }) {
|
||||
return ({
|
||||
|
@ -69,7 +61,7 @@ describe('CustomLink', () => {
|
|||
describe('empty prompt', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
|
||||
data: [],
|
||||
data: { customLinks: [] },
|
||||
status: hooks.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
@ -290,7 +282,7 @@ describe('CustomLink', () => {
|
|||
describe('invalid license', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
|
||||
data: [],
|
||||
data: { customLinks: [] },
|
||||
status: hooks.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ export function CustomLinkOverview() {
|
|||
CustomLink | undefined
|
||||
>();
|
||||
|
||||
const { data: customLinks = [], status, refetch } = useFetcher(
|
||||
const { data, status, refetch } = useFetcher(
|
||||
async (callApmApi) => {
|
||||
if (hasValidLicense) {
|
||||
return callApmApi({
|
||||
|
@ -46,6 +46,8 @@ export function CustomLinkOverview() {
|
|||
[hasValidLicense]
|
||||
);
|
||||
|
||||
const customLinks = data?.customLinks ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (customLinkSelected) {
|
||||
setIsFlyoutOpen(true);
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
import { ML_ERRORS } from '../../../../../common/anomaly_detection';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
@ -33,6 +34,10 @@ interface Props {
|
|||
onCreateJobSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type ApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/environments'>;
|
||||
const INITIAL_DATA: ApiResponse = { environments: [] };
|
||||
|
||||
export function AddEnvironments({
|
||||
currentEnvironments,
|
||||
onCreateJobSuccess,
|
||||
|
@ -42,7 +47,7 @@ export function AddEnvironments({
|
|||
const { anomalyDetectionJobsRefetch } = useAnomalyDetectionJobsContext();
|
||||
const canCreateJob = !!application.capabilities.ml.canCreateJob;
|
||||
const { toasts } = notifications;
|
||||
const { data = [], status } = useFetcher(
|
||||
const { data = INITIAL_DATA, status } = useFetcher(
|
||||
(callApmApi) =>
|
||||
callApmApi({
|
||||
endpoint: `GET /api/apm/settings/anomaly-detection/environments`,
|
||||
|
@ -51,7 +56,7 @@ export function AddEnvironments({
|
|||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const environmentOptions = data.map((env) => ({
|
||||
const environmentOptions = data.environments.map((env) => ({
|
||||
label: getEnvironmentLabel(env),
|
||||
value: env,
|
||||
disabled: currentEnvironments.includes(env),
|
||||
|
|
|
@ -49,10 +49,10 @@ const Culprit = euiStyled.div`
|
|||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
type ErrorGroupListAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>;
|
||||
type ErrorGroupItem = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>['errorGroups'][0];
|
||||
|
||||
interface Props {
|
||||
items: ErrorGroupListAPIResponse;
|
||||
items: ErrorGroupItem[];
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
|
@ -128,7 +128,7 @@ function ErrorGroupList({ items, serviceName }: Props) {
|
|||
field: 'message',
|
||||
sortable: false,
|
||||
width: '50%',
|
||||
render: (message: string, item: ErrorGroupListAPIResponse[0]) => {
|
||||
render: (message: string, item: ErrorGroupItem) => {
|
||||
return (
|
||||
<MessageAndCulpritCell>
|
||||
<EuiToolTip
|
||||
|
|
|
@ -97,7 +97,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
|
|||
<EuiSpacer size="s" />
|
||||
|
||||
<ErrorGroupList
|
||||
items={errorGroupListData}
|
||||
items={errorGroupListData.errorGroups}
|
||||
serviceName={serviceName}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -39,7 +39,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
|
|||
urlParams: { kuery, start, end },
|
||||
} = useUrlParams();
|
||||
|
||||
const { data: items = [] } = useFetcher(
|
||||
const { data } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!start || !end) {
|
||||
return undefined;
|
||||
|
@ -61,6 +61,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
|
|||
[kuery, serviceName, start, end]
|
||||
);
|
||||
|
||||
const items = data?.serviceNodes ?? [];
|
||||
const columns: Array<ITableColumn<typeof items[0]>> = [
|
||||
{
|
||||
name: (
|
||||
|
|
|
@ -164,7 +164,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
|
|||
},
|
||||
];
|
||||
|
||||
const { data = [], status } = useFetcher(
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!start || !end) {
|
||||
return;
|
||||
|
@ -188,8 +188,10 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
|
|||
[start, end, serviceName, environment]
|
||||
);
|
||||
|
||||
const serviceDependencies = data?.serviceDependencies ?? [];
|
||||
|
||||
// need top-level sortable fields for the managed table
|
||||
const items = data.map((item) => ({
|
||||
const items = serviceDependencies.map((item) => ({
|
||||
...item,
|
||||
errorRateValue: item.errorRate.value,
|
||||
latencyValue: item.latency.value,
|
||||
|
|
|
@ -12,6 +12,7 @@ import uuid from 'uuid';
|
|||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { APIReturnType } from '../../../services/rest/createCallApmApi';
|
||||
import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison';
|
||||
import {
|
||||
ServiceOverviewInstancesTable,
|
||||
|
@ -30,20 +31,24 @@ interface ServiceOverviewInstancesChartAndTableProps {
|
|||
serviceName: string;
|
||||
}
|
||||
|
||||
const INITIAL_STATE = {
|
||||
items: [] as Array<{
|
||||
serviceNodeName: string;
|
||||
errorRate: number;
|
||||
throughput: number;
|
||||
latency: number;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
}>,
|
||||
requestId: undefined,
|
||||
totalItems: 0,
|
||||
export interface PrimaryStatsServiceInstanceItem {
|
||||
serviceNodeName: string;
|
||||
errorRate: number;
|
||||
throughput: number;
|
||||
latency: number;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
}
|
||||
|
||||
const INITIAL_STATE_PRIMARY_STATS = {
|
||||
primaryStatsItems: [] as PrimaryStatsServiceInstanceItem[],
|
||||
primaryStatsRequestId: undefined,
|
||||
primaryStatsItemCount: 0,
|
||||
};
|
||||
|
||||
const INITIAL_STATE_COMPARISON_STATISTICS = {
|
||||
type ApiResponseComparisonStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>;
|
||||
|
||||
const INITIAL_STATE_COMPARISON_STATISTICS: ApiResponseComparisonStats = {
|
||||
currentPeriod: {},
|
||||
previousPeriod: {},
|
||||
};
|
||||
|
@ -93,7 +98,10 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
comparisonType,
|
||||
});
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
const {
|
||||
data: primaryStatsData = INITIAL_STATE_PRIMARY_STATS,
|
||||
status: primaryStatsStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!start || !end || !transactionType || !latencyAggregationType) {
|
||||
return;
|
||||
|
@ -116,9 +124,9 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
},
|
||||
},
|
||||
}).then((response) => {
|
||||
const tableItems = orderBy(
|
||||
const primaryStatsItems = orderBy(
|
||||
// need top-level sortable fields for the managed table
|
||||
response.map((item) => ({
|
||||
response.serviceInstances.map((item) => ({
|
||||
...item,
|
||||
latency: item.latency ?? 0,
|
||||
throughput: item.throughput ?? 0,
|
||||
|
@ -131,9 +139,9 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE);
|
||||
|
||||
return {
|
||||
requestId: uuid(),
|
||||
items: tableItems,
|
||||
totalItems: response.length,
|
||||
primaryStatsRequestId: uuid(),
|
||||
primaryStatsItems,
|
||||
primaryStatsItemCount: response.serviceInstances.length,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -154,10 +162,14 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
]
|
||||
);
|
||||
|
||||
const { items, requestId, totalItems } = data;
|
||||
const {
|
||||
primaryStatsItems,
|
||||
primaryStatsRequestId,
|
||||
primaryStatsItemCount,
|
||||
} = primaryStatsData;
|
||||
|
||||
const {
|
||||
data: comparisonStatistics = INITIAL_STATE_COMPARISON_STATISTICS,
|
||||
data: comparisonStatsData = INITIAL_STATE_COMPARISON_STATISTICS,
|
||||
status: comparisonStatisticsStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
|
@ -166,7 +178,7 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
!end ||
|
||||
!transactionType ||
|
||||
!latencyAggregationType ||
|
||||
!totalItems
|
||||
!primaryStatsItemCount
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -187,7 +199,7 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
numBuckets: 20,
|
||||
transactionType,
|
||||
serviceNodeIds: JSON.stringify(
|
||||
items.map((item) => item.serviceNodeName)
|
||||
primaryStatsItems.map((item) => item.serviceNodeName)
|
||||
),
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
|
@ -197,7 +209,7 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
},
|
||||
// only fetches comparison statistics when requestId is invalidated by primary statistics api call
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[requestId],
|
||||
[primaryStatsRequestId],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
|
@ -213,14 +225,14 @@ export function ServiceOverviewInstancesChartAndTable({
|
|||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel>
|
||||
<ServiceOverviewInstancesTable
|
||||
items={items}
|
||||
primaryStatsItems={primaryStatsItems}
|
||||
primaryStatsStatus={primaryStatsStatus}
|
||||
primaryStatsItemCount={primaryStatsItemCount}
|
||||
comparisonStatsData={comparisonStatsData}
|
||||
serviceName={serviceName}
|
||||
status={status}
|
||||
tableOptions={tableOptions}
|
||||
totalItems={totalItems}
|
||||
serviceInstanceComparisonStatistics={comparisonStatistics}
|
||||
isLoading={
|
||||
status === FETCH_STATUS.LOADING ||
|
||||
primaryStatsStatus === FETCH_STATUS.LOADING ||
|
||||
comparisonStatisticsStatus === FETCH_STATUS.LOADING
|
||||
}
|
||||
onChangeTableOptions={(newTableOptions) => {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
|
||||
import { isJavaAgentName } from '../../../../../common/agent_name';
|
||||
import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
|
||||
|
@ -25,10 +24,7 @@ import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink
|
|||
import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { getLatencyColumnLabel } from '../get_latency_column_label';
|
||||
|
||||
type ServiceInstancePrimaryStatisticItem = ValuesType<
|
||||
APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'>
|
||||
>;
|
||||
import { PrimaryStatsServiceInstanceItem } from '../service_overview_instances_chart_and_table';
|
||||
|
||||
type ServiceInstanceComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>;
|
||||
|
||||
|
@ -36,15 +32,15 @@ export function getColumns({
|
|||
serviceName,
|
||||
agentName,
|
||||
latencyAggregationType,
|
||||
serviceInstanceComparisonStatistics,
|
||||
comparisonStatsData,
|
||||
comparisonEnabled,
|
||||
}: {
|
||||
serviceName: string;
|
||||
agentName?: string;
|
||||
latencyAggregationType?: LatencyAggregationType;
|
||||
serviceInstanceComparisonStatistics?: ServiceInstanceComparisonStatistics;
|
||||
comparisonStatsData?: ServiceInstanceComparisonStatistics;
|
||||
comparisonEnabled?: boolean;
|
||||
}): Array<EuiBasicTableColumn<ServiceInstancePrimaryStatisticItem>> {
|
||||
}): Array<EuiBasicTableColumn<PrimaryStatsServiceInstanceItem>> {
|
||||
return [
|
||||
{
|
||||
field: 'serviceNodeName',
|
||||
|
@ -91,11 +87,9 @@ export function getColumns({
|
|||
width: px(unit * 10),
|
||||
render: (_, { serviceNodeName, latency }) => {
|
||||
const currentPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName]
|
||||
?.latency;
|
||||
comparisonStatsData?.currentPeriod?.[serviceNodeName]?.latency;
|
||||
const previousPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName]
|
||||
?.latency;
|
||||
comparisonStatsData?.previousPeriod?.[serviceNodeName]?.latency;
|
||||
return (
|
||||
<SparkPlot
|
||||
color="euiColorVis1"
|
||||
|
@ -118,11 +112,9 @@ export function getColumns({
|
|||
width: px(unit * 10),
|
||||
render: (_, { serviceNodeName, throughput }) => {
|
||||
const currentPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName]
|
||||
?.throughput;
|
||||
comparisonStatsData?.currentPeriod?.[serviceNodeName]?.throughput;
|
||||
const previousPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName]
|
||||
?.throughput;
|
||||
comparisonStatsData?.previousPeriod?.[serviceNodeName]?.throughput;
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
|
@ -146,11 +138,9 @@ export function getColumns({
|
|||
width: px(unit * 8),
|
||||
render: (_, { serviceNodeName, errorRate }) => {
|
||||
const currentPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName]
|
||||
?.errorRate;
|
||||
comparisonStatsData?.currentPeriod?.[serviceNodeName]?.errorRate;
|
||||
const previousPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName]
|
||||
?.errorRate;
|
||||
comparisonStatsData?.previousPeriod?.[serviceNodeName]?.errorRate;
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
|
@ -174,11 +164,9 @@ export function getColumns({
|
|||
width: px(unit * 8),
|
||||
render: (_, { serviceNodeName, cpuUsage }) => {
|
||||
const currentPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName]
|
||||
?.cpuUsage;
|
||||
comparisonStatsData?.currentPeriod?.[serviceNodeName]?.cpuUsage;
|
||||
const previousPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName]
|
||||
?.cpuUsage;
|
||||
comparisonStatsData?.previousPeriod?.[serviceNodeName]?.cpuUsage;
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
|
@ -202,11 +190,9 @@ export function getColumns({
|
|||
width: px(unit * 9),
|
||||
render: (_, { serviceNodeName, memoryUsage }) => {
|
||||
const currentPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName]
|
||||
?.memoryUsage;
|
||||
comparisonStatsData?.currentPeriod?.[serviceNodeName]?.memoryUsage;
|
||||
const previousPeriodTimestamp =
|
||||
serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName]
|
||||
?.memoryUsage;
|
||||
comparisonStatsData?.previousPeriod?.[serviceNodeName]?.memoryUsage;
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
|
@ -21,16 +20,13 @@ import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
|||
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
|
||||
import {
|
||||
PAGE_SIZE,
|
||||
PrimaryStatsServiceInstanceItem,
|
||||
SortDirection,
|
||||
SortField,
|
||||
} from '../service_overview_instances_chart_and_table';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
|
||||
import { getColumns } from './get_columns';
|
||||
|
||||
type ServiceInstanceItem = ValuesType<
|
||||
APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'>
|
||||
>;
|
||||
|
||||
type ServiceInstanceComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>;
|
||||
|
||||
export interface TableOptions {
|
||||
|
@ -42,26 +38,26 @@ export interface TableOptions {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
items?: ServiceInstanceItem[];
|
||||
primaryStatsItems: PrimaryStatsServiceInstanceItem[];
|
||||
serviceName: string;
|
||||
status: FETCH_STATUS;
|
||||
totalItems: number;
|
||||
primaryStatsStatus: FETCH_STATUS;
|
||||
primaryStatsItemCount: number;
|
||||
tableOptions: TableOptions;
|
||||
onChangeTableOptions: (newTableOptions: {
|
||||
page?: { index: number };
|
||||
sort?: { field: string; direction: SortDirection };
|
||||
}) => void;
|
||||
serviceInstanceComparisonStatistics?: ServiceInstanceComparisonStatistics;
|
||||
comparisonStatsData?: ServiceInstanceComparisonStatistics;
|
||||
isLoading: boolean;
|
||||
}
|
||||
export function ServiceOverviewInstancesTable({
|
||||
items = [],
|
||||
totalItems,
|
||||
primaryStatsItems = [],
|
||||
primaryStatsItemCount,
|
||||
serviceName,
|
||||
status,
|
||||
primaryStatsStatus: status,
|
||||
tableOptions,
|
||||
onChangeTableOptions,
|
||||
serviceInstanceComparisonStatistics,
|
||||
comparisonStatsData: comparisonStatsData,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const { agentName } = useApmServiceContext();
|
||||
|
@ -76,14 +72,14 @@ export function ServiceOverviewInstancesTable({
|
|||
agentName,
|
||||
serviceName,
|
||||
latencyAggregationType,
|
||||
serviceInstanceComparisonStatistics,
|
||||
comparisonStatsData,
|
||||
comparisonEnabled,
|
||||
});
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount: totalItems,
|
||||
totalItemCount: primaryStatsItemCount,
|
||||
hidePerPageOptions: true,
|
||||
};
|
||||
|
||||
|
@ -101,11 +97,11 @@ export function ServiceOverviewInstancesTable({
|
|||
<EuiFlexItem>
|
||||
<TableFetchWrapper status={status}>
|
||||
<ServiceOverviewTableContainer
|
||||
isEmptyAndLoading={totalItems === 0 && isLoading}
|
||||
isEmptyAndLoading={primaryStatsItemCount === 0 && isLoading}
|
||||
>
|
||||
<EuiBasicTable
|
||||
loading={isLoading}
|
||||
items={items}
|
||||
items={primaryStatsItems}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={{ sort: { field, direction } }}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { orderBy } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
|
@ -28,8 +29,9 @@ interface Props {
|
|||
serviceName: string;
|
||||
}
|
||||
|
||||
type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>;
|
||||
const INITIAL_STATE = {
|
||||
transactionGroups: [],
|
||||
transactionGroups: [] as ApiResponse['transactionGroups'],
|
||||
isAggregationAccurate: true,
|
||||
requestId: '',
|
||||
transactionGroupsTotalItems: 0,
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../../../common/profiling';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { APIReturnType } from '../../../services/rest/createCallApmApi';
|
||||
import { SearchBar } from '../../shared/search_bar';
|
||||
import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph';
|
||||
import { ServiceProfilingTimeline } from './service_profiling_timeline';
|
||||
|
@ -28,6 +29,9 @@ interface ServiceProfilingProps {
|
|||
environment?: string;
|
||||
}
|
||||
|
||||
type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/profiling/timeline'>;
|
||||
const DEFAULT_DATA: ApiResponse = { profilingTimeline: [] };
|
||||
|
||||
export function ServiceProfiling({
|
||||
serviceName,
|
||||
environment,
|
||||
|
@ -36,7 +40,7 @@ export function ServiceProfiling({
|
|||
urlParams: { kuery, start, end },
|
||||
} = useUrlParams();
|
||||
|
||||
const { data = [] } = useFetcher(
|
||||
const { data = DEFAULT_DATA } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!start || !end) {
|
||||
return;
|
||||
|
@ -58,14 +62,16 @@ export function ServiceProfiling({
|
|||
[kuery, start, end, serviceName, environment]
|
||||
);
|
||||
|
||||
const { profilingTimeline } = data;
|
||||
|
||||
const [valueType, setValueType] = useState<ProfilingValueType | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.length) {
|
||||
if (!profilingTimeline.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableValueTypes = data.reduce((set, point) => {
|
||||
const availableValueTypes = profilingTimeline.reduce((set, point) => {
|
||||
(Object.keys(point.valueTypes).filter(
|
||||
(type) => type !== 'unknown'
|
||||
) as ProfilingValueType[])
|
||||
|
@ -80,7 +86,7 @@ export function ServiceProfiling({
|
|||
if (!valueType || !availableValueTypes.has(valueType)) {
|
||||
setValueType(Array.from(availableValueTypes)[0]);
|
||||
}
|
||||
}, [data, valueType]);
|
||||
}, [profilingTimeline, valueType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -103,7 +109,7 @@ export function ServiceProfiling({
|
|||
<ServiceProfilingTimeline
|
||||
start={start!}
|
||||
end={end!}
|
||||
series={data}
|
||||
series={profilingTimeline}
|
||||
onValueTypeSelect={(type) => {
|
||||
setValueType(type);
|
||||
}}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiTitle } from '@elastic/eui';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { HttpSetup } from '../../../../../../../src/core/public';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
|
||||
|
@ -20,7 +20,7 @@ export default {
|
|||
component: ApmHeader,
|
||||
decorators: [
|
||||
(Story: ComponentType) => {
|
||||
createCallApmApi(({} as unknown) as HttpSetup);
|
||||
createCallApmApi(({} as unknown) as CoreStart);
|
||||
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { act, fireEvent, render } from '@testing-library/react';
|
|||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { CustomLinkMenuSection } from '.';
|
||||
import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
|
||||
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import * as useFetcher from '../../../../hooks/use_fetcher';
|
||||
|
@ -40,7 +39,7 @@ const transaction = ({
|
|||
describe('Custom links', () => {
|
||||
it('shows empty message when no custom link is available', () => {
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: [],
|
||||
data: { customLinks: [] },
|
||||
status: useFetcher.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
@ -58,7 +57,7 @@ describe('Custom links', () => {
|
|||
|
||||
it('shows loading while custom links are fetched', () => {
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: [],
|
||||
data: { customLinks: [] },
|
||||
status: useFetcher.FETCH_STATUS.LOADING,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
@ -71,12 +70,14 @@ describe('Custom links', () => {
|
|||
});
|
||||
|
||||
it('shows first 3 custom links available', () => {
|
||||
const customLinks = [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
] as CustomLinkType[];
|
||||
const customLinks = {
|
||||
customLinks: [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
],
|
||||
};
|
||||
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: customLinks,
|
||||
|
@ -93,15 +94,17 @@ describe('Custom links', () => {
|
|||
});
|
||||
|
||||
it('clicks "show all" and "show fewer"', () => {
|
||||
const customLinks = [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
] as CustomLinkType[];
|
||||
const data = {
|
||||
customLinks: [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
],
|
||||
};
|
||||
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: customLinks,
|
||||
data,
|
||||
status: useFetcher.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
@ -125,7 +128,7 @@ describe('Custom links', () => {
|
|||
describe('create custom link buttons', () => {
|
||||
it('shows create button below empty message', () => {
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: [],
|
||||
data: { customLinks: [] },
|
||||
status: useFetcher.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
@ -140,15 +143,17 @@ describe('Custom links', () => {
|
|||
});
|
||||
|
||||
it('shows create button besides the title', () => {
|
||||
const customLinks = [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
] as CustomLinkType[];
|
||||
const data = {
|
||||
customLinks: [
|
||||
{ id: '1', label: 'foo', url: 'foo' },
|
||||
{ id: '2', label: 'bar', url: 'bar' },
|
||||
{ id: '3', label: 'baz', url: 'baz' },
|
||||
{ id: '4', label: 'qux', url: 'qux' },
|
||||
],
|
||||
};
|
||||
|
||||
jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({
|
||||
data: customLinks,
|
||||
data,
|
||||
status: useFetcher.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ export function CustomLinkMenuSection({
|
|||
[transaction]
|
||||
);
|
||||
|
||||
const { data: customLinks = [], status, refetch } = useFetcher(
|
||||
const { data, status, refetch } = useFetcher(
|
||||
(callApmApi) =>
|
||||
callApmApi({
|
||||
isCachable: false,
|
||||
|
@ -68,6 +68,8 @@ export function CustomLinkMenuSection({
|
|||
[filters]
|
||||
);
|
||||
|
||||
const customLinks = data?.customLinks ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCreateEditFlyoutOpen && (
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { onBrushEnd, isTimeseriesEmpty } from './helper';
|
||||
import { History } from 'history';
|
||||
import { TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
||||
|
||||
describe('Chart helper', () => {
|
||||
describe('onBrushEnd', () => {
|
||||
|
@ -52,7 +52,7 @@ describe('Chart helper', () => {
|
|||
type: 'line',
|
||||
color: 'red',
|
||||
},
|
||||
] as TimeSeries[];
|
||||
] as Array<TimeSeries<Coordinate>>;
|
||||
expect(isTimeseriesEmpty(timeseries)).toBeTruthy();
|
||||
});
|
||||
it('returns true when y coordinate is null', () => {
|
||||
|
@ -63,7 +63,7 @@ describe('Chart helper', () => {
|
|||
type: 'line',
|
||||
color: 'red',
|
||||
},
|
||||
] as TimeSeries[];
|
||||
] as Array<TimeSeries<Coordinate>>;
|
||||
expect(isTimeseriesEmpty(timeseries)).toBeTruthy();
|
||||
});
|
||||
it('returns true when y coordinate is undefined', () => {
|
||||
|
@ -74,7 +74,7 @@ describe('Chart helper', () => {
|
|||
type: 'line',
|
||||
color: 'red',
|
||||
},
|
||||
] as TimeSeries[];
|
||||
] as Array<TimeSeries<Coordinate>>;
|
||||
expect(isTimeseriesEmpty(timeseries)).toBeTruthy();
|
||||
});
|
||||
it('returns false when at least one coordinate is filled', () => {
|
||||
|
@ -91,7 +91,7 @@ describe('Chart helper', () => {
|
|||
type: 'line',
|
||||
color: 'green',
|
||||
},
|
||||
] as TimeSeries[];
|
||||
] as Array<TimeSeries<Coordinate>>;
|
||||
expect(isTimeseriesEmpty(timeseries)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { XYBrushArea } from '@elastic/charts';
|
||||
import { History } from 'history';
|
||||
import { TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { fromQuery, toQuery } from '../../Links/url_helpers';
|
||||
|
||||
export const onBrushEnd = ({
|
||||
|
@ -36,15 +36,12 @@ export const onBrushEnd = ({
|
|||
}
|
||||
};
|
||||
|
||||
export function isTimeseriesEmpty(timeseries?: TimeSeries[]) {
|
||||
export function isTimeseriesEmpty(timeseries?: Array<TimeSeries<Coordinate>>) {
|
||||
return (
|
||||
!timeseries ||
|
||||
timeseries
|
||||
.map((serie) => serie.data)
|
||||
.flat()
|
||||
.every(
|
||||
({ y }: { x?: number | null; y?: number | null }) =>
|
||||
y === null || y === undefined
|
||||
)
|
||||
.every(({ y }: Coordinate) => y === null || y === undefined)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,13 +23,13 @@ import {
|
|||
} from '../../../../../common/utils/formatters';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table';
|
||||
import { ChartContainer } from '../chart_container';
|
||||
import { getResponseTimeTickFormatter } from '../transaction_charts/helper';
|
||||
|
||||
interface InstancesLatencyDistributionChartProps {
|
||||
height: number;
|
||||
items?: APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'>;
|
||||
items?: PrimaryStatsServiceInstanceItem[];
|
||||
status: FETCH_STATUS;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,11 @@ import React from 'react';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import { useChartTheme } from '../../../../../observability/public';
|
||||
import { asAbsoluteDateTime } from '../../../../common/utils/formatters';
|
||||
import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries';
|
||||
import {
|
||||
Coordinate,
|
||||
RectCoordinate,
|
||||
TimeSeries,
|
||||
} from '../../../../typings/timeseries';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../hooks/use_theme';
|
||||
import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context';
|
||||
|
@ -43,7 +47,7 @@ interface Props {
|
|||
fetchStatus: FETCH_STATUS;
|
||||
height?: number;
|
||||
onToggleLegend?: LegendItemListener;
|
||||
timeseries: TimeSeries[];
|
||||
timeseries: Array<TimeSeries<Coordinate>>;
|
||||
/**
|
||||
* Formatter for y-axis tick values
|
||||
*/
|
||||
|
@ -85,12 +89,10 @@ export function TimeseriesChart({
|
|||
const max = Math.max(...xValues);
|
||||
|
||||
const xFormatter = niceTimeFormatter([min, max]);
|
||||
|
||||
const isEmpty = isTimeseriesEmpty(timeseries);
|
||||
|
||||
const annotationColor = theme.eui.euiColorSecondary;
|
||||
|
||||
const allSeries = [...timeseries, ...(anomalyTimeseries?.boundaries ?? [])];
|
||||
const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max };
|
||||
|
||||
return (
|
||||
<ChartContainer hasData={!isEmpty} height={height} status={fetchStatus}>
|
||||
|
@ -111,7 +113,7 @@ export function TimeseriesChart({
|
|||
showLegend
|
||||
showLegendExtra
|
||||
legendPosition={Position.Bottom}
|
||||
xDomain={{ min, max }}
|
||||
xDomain={xDomain}
|
||||
onLegendItemClick={(legend) => {
|
||||
if (onToggleLegend) {
|
||||
onToggleLegend(legend);
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
asAbsoluteDateTime,
|
||||
asPercent,
|
||||
} from '../../../../../common/utils/formatters';
|
||||
import { TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
|
@ -42,7 +42,7 @@ interface Props {
|
|||
fetchStatus: FETCH_STATUS;
|
||||
height?: number;
|
||||
showAnnotations: boolean;
|
||||
timeseries?: TimeSeries[];
|
||||
timeseries?: Array<TimeSeries<Coordinate>>;
|
||||
}
|
||||
|
||||
export function TransactionBreakdownChartContents({
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import { isFiniteNumber } from '../../../../../common/utils/is_finite_number';
|
||||
import { APMChartSpec, Coordinate } from '../../../../../typings/timeseries';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { TimeFormatter } from '../../../../../common/utils/formatters';
|
||||
|
||||
export function getResponseTimeTickFormatter(formatter: TimeFormatter) {
|
||||
return (t: number) => formatter(t).formatted;
|
||||
}
|
||||
|
||||
export function getMaxY(specs?: Array<APMChartSpec<Coordinate>>) {
|
||||
export function getMaxY(specs?: Array<{ data: Coordinate[] }>) {
|
||||
const values = specs
|
||||
?.flatMap((spec) => spec.data)
|
||||
.map((coord) => coord.y)
|
||||
|
|
|
@ -7,14 +7,21 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { enableInspectEsQueries } from '../../../../observability/public';
|
||||
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
|
||||
import { px, unit } from '../../style/variables';
|
||||
import { DatePicker } from './DatePicker';
|
||||
import { KueryBar } from './KueryBar';
|
||||
import { TimeComparison } from './time_comparison';
|
||||
import { useBreakPoints } from '../../hooks/use_break_points';
|
||||
import { useKibanaUrl } from '../../hooks/useKibanaUrl';
|
||||
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
const SearchBarFlexGroup = euiStyled(EuiFlexGroup)`
|
||||
const EuiFlexGroupSpaced = euiStyled(EuiFlexGroup)`
|
||||
margin: ${({ theme }) =>
|
||||
`${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`};
|
||||
`;
|
||||
|
@ -29,6 +36,52 @@ function getRowDirection(showColumn: boolean) {
|
|||
return showColumn ? 'column' : 'row';
|
||||
}
|
||||
|
||||
function DebugQueryCallout() {
|
||||
const { uiSettings } = useApmPluginContext().core;
|
||||
const advancedSettingsUrl = useKibanaUrl('/app/management/kibana/settings', {
|
||||
query: {
|
||||
query: 'category:(observability)',
|
||||
},
|
||||
});
|
||||
|
||||
if (!uiSettings.get(enableInspectEsQueries)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroupSpaced>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title',
|
||||
{
|
||||
defaultMessage:
|
||||
'Inspectable ES queries (`apm:enableInspectEsQueries`)',
|
||||
}
|
||||
)}
|
||||
iconType="beaker"
|
||||
color="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description"
|
||||
defaultMessage="You can now inspect every Elasticsearch query by opening your browser's Dev Tool and looking at the API responses. The setting can be disabled in Kibana's {advancedSettingsLink}"
|
||||
values={{
|
||||
advancedSettingsLink: (
|
||||
<EuiLink href={advancedSettingsUrl}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings',
|
||||
{ defaultMessage: 'Advanced Setting' }
|
||||
)}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroupSpaced>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
prepend,
|
||||
showTimeComparison = false,
|
||||
|
@ -38,26 +91,29 @@ export function SearchBar({
|
|||
const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 };
|
||||
|
||||
return (
|
||||
<SearchBarFlexGroup gutterSize="m" direction={getRowDirection(isLarge)}>
|
||||
<EuiFlexItem>
|
||||
<KueryBar prepend={prepend} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
direction={getRowDirection(isMedium)}
|
||||
>
|
||||
{showTimeComparison && (
|
||||
<EuiFlexItem style={{ ...itemsStyle, minWidth: px(300) }}>
|
||||
<TimeComparison />
|
||||
<>
|
||||
<DebugQueryCallout />
|
||||
<EuiFlexGroupSpaced gutterSize="m" direction={getRowDirection(isLarge)}>
|
||||
<EuiFlexItem>
|
||||
<KueryBar prepend={prepend} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
direction={getRowDirection(isMedium)}
|
||||
>
|
||||
{showTimeComparison && (
|
||||
<EuiFlexItem style={{ ...itemsStyle, minWidth: px(300) }}>
|
||||
<TimeComparison />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem style={itemsStyle}>
|
||||
<DatePicker />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem style={itemsStyle}>
|
||||
<DatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</SearchBarFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroupSpaced>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -116,8 +116,8 @@ export function MockApmPluginContextWrapper({
|
|||
children?: React.ReactNode;
|
||||
value?: ApmPluginContextValue;
|
||||
}) {
|
||||
if (value.core?.http) {
|
||||
createCallApmApi(value.core?.http);
|
||||
if (value.core) {
|
||||
createCallApmApi(value.core);
|
||||
}
|
||||
return (
|
||||
<ApmPluginContext.Provider
|
||||
|
|
|
@ -8,13 +8,10 @@
|
|||
import url from 'url';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function useKibanaUrl(
|
||||
/** The path to the plugin */ path: string,
|
||||
/** The hash path */ hash?: string
|
||||
) {
|
||||
export function useKibanaUrl(path: string, urlObject?: url.UrlObject) {
|
||||
const { core } = useApmPluginContext();
|
||||
return url.format({
|
||||
...urlObject,
|
||||
pathname: core.http.basePath.prepend(path),
|
||||
hash,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ function getEnvironmentOptions(environments: string[]) {
|
|||
return [ENVIRONMENT_ALL, ...environmentOptions];
|
||||
}
|
||||
|
||||
const INITIAL_DATA = { environments: [] };
|
||||
|
||||
export function useEnvironmentsFetcher({
|
||||
serviceName,
|
||||
start,
|
||||
|
@ -32,7 +34,7 @@ export function useEnvironmentsFetcher({
|
|||
start?: string;
|
||||
end?: string;
|
||||
}) {
|
||||
const { data: environments = [], status = 'loading' } = useFetcher(
|
||||
const { data = INITIAL_DATA, status = 'loading' } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
|
@ -51,9 +53,9 @@ export function useEnvironmentsFetcher({
|
|||
);
|
||||
|
||||
const environmentOptions = useMemo(
|
||||
() => getEnvironmentOptions(environments),
|
||||
[environments]
|
||||
() => getEnvironmentOptions(data.environments),
|
||||
[data?.environments]
|
||||
);
|
||||
|
||||
return { environments, status, environmentOptions };
|
||||
return { environments: data.environments, status, environmentOptions };
|
||||
}
|
||||
|
|
|
@ -85,19 +85,19 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
const getApmDataHelper = async () => {
|
||||
const {
|
||||
fetchObservabilityOverviewPageData,
|
||||
hasData,
|
||||
getHasData,
|
||||
createCallApmApi,
|
||||
} = await import('./services/rest/apm_observability_overview_fetchers');
|
||||
// have to do this here as well in case app isn't mounted yet
|
||||
createCallApmApi(core.http);
|
||||
createCallApmApi(core);
|
||||
|
||||
return { fetchObservabilityOverviewPageData, hasData };
|
||||
return { fetchObservabilityOverviewPageData, getHasData };
|
||||
};
|
||||
plugins.observability.dashboard.register({
|
||||
appName: 'apm',
|
||||
hasData: async () => {
|
||||
const dataHelper = await getApmDataHelper();
|
||||
return await dataHelper.hasData();
|
||||
return await dataHelper.getHasData();
|
||||
},
|
||||
fetchData: async (params: FetchDataParams) => {
|
||||
const dataHelper = await getApmDataHelper();
|
||||
|
@ -112,7 +112,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
createCallApmApi,
|
||||
} = await import('./components/app/RumDashboard/ux_overview_fetchers');
|
||||
// have to do this here as well in case app isn't mounted yet
|
||||
createCallApmApi(core.http);
|
||||
createCallApmApi(core);
|
||||
|
||||
return { fetchUxOverviewDate, hasRumData };
|
||||
};
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
import { difference, zipObject } from 'lodash';
|
||||
import { EuiTheme } from '../../../../../src/plugins/kibana_react/common';
|
||||
import { asTransactionRate } from '../../common/utils/formatters';
|
||||
import { TimeSeries } from '../../typings/timeseries';
|
||||
import { Coordinate, TimeSeries } from '../../typings/timeseries';
|
||||
import { APIReturnType } from '../services/rest/createCallApmApi';
|
||||
import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor';
|
||||
|
||||
export type ThroughputChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/throughput'>;
|
||||
|
||||
export interface ThroughputChart {
|
||||
throughputTimeseries: TimeSeries[];
|
||||
throughputTimeseries: Array<TimeSeries<Coordinate>>;
|
||||
}
|
||||
|
||||
export function getThroughputChartSelector({
|
||||
|
|
|
@ -7,49 +7,51 @@
|
|||
|
||||
import { mockNow } from '../utils/testHelpers';
|
||||
import { clearCache, callApi } from './rest/callApi';
|
||||
import { SessionStorageMock } from './__mocks__/SessionStorageMock';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreStart, HttpSetup } from 'kibana/public';
|
||||
|
||||
type HttpMock = HttpSetup & {
|
||||
get: jest.SpyInstance<HttpSetup['get']>;
|
||||
type CoreMock = CoreStart & {
|
||||
http: {
|
||||
get: jest.SpyInstance<HttpSetup['get']>;
|
||||
};
|
||||
};
|
||||
|
||||
describe('callApi', () => {
|
||||
let http: HttpMock;
|
||||
let core: CoreMock;
|
||||
|
||||
beforeEach(() => {
|
||||
http = ({
|
||||
get: jest.fn().mockReturnValue({
|
||||
my_key: 'hello_world',
|
||||
}),
|
||||
} as unknown) as HttpMock;
|
||||
|
||||
// @ts-expect-error
|
||||
global.sessionStorage = new SessionStorageMock();
|
||||
core = ({
|
||||
http: {
|
||||
get: jest.fn().mockReturnValue({
|
||||
my_key: 'hello_world',
|
||||
}),
|
||||
},
|
||||
uiSettings: { get: () => false }, // disable `observability:enableInspectEsQueries` setting
|
||||
} as unknown) as CoreMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
http.get.mockClear();
|
||||
core.http.get.mockClear();
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe('apm_debug', () => {
|
||||
describe('_inspect', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.setItem('apm_debug', 'true');
|
||||
// @ts-expect-error
|
||||
core.uiSettings.get = () => true; // enable `observability:enableInspectEsQueries` setting
|
||||
});
|
||||
|
||||
it('should add debug param for APM endpoints', async () => {
|
||||
await callApi(http, { pathname: `/api/apm/status/server` });
|
||||
await callApi(core, { pathname: `/api/apm/status/server` });
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/api/apm/status/server', {
|
||||
query: { _debug: true },
|
||||
expect(core.http.get).toHaveBeenCalledWith('/api/apm/status/server', {
|
||||
query: { _inspect: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add debug param for non-APM endpoints', async () => {
|
||||
await callApi(http, { pathname: `/api/kibana` });
|
||||
await callApi(core, { pathname: `/api/kibana` });
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/api/kibana', { query: {} });
|
||||
expect(core.http.get).toHaveBeenCalledWith('/api/kibana', { query: {} });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -65,138 +67,138 @@ describe('callApi', () => {
|
|||
|
||||
describe('when the call does not contain start/end params', () => {
|
||||
it('should not return cached response for identical calls', async () => {
|
||||
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } });
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(3);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the call contains start/end params', () => {
|
||||
it('should return cached response for identical calls', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(1);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not return cached response for subsequent calls if arguments change', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011', foo: 'bar1' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011', foo: 'bar2' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011', foo: 'bar3' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(3);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not return cached response if `end` is a future timestamp', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(3);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should return cached response if calls contain `end` param in the past', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2009', end: '2010' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2009', end: '2010' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2009', end: '2010' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(1);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return cached response even if order of properties change', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2010', start: '2009' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2009', end: '2010' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
query: { start: '2009', end: '2010' },
|
||||
pathname: `/api/kibana`,
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(1);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not return cached response with `isCachable: false` option', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: false,
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: false,
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: false,
|
||||
pathname: `/api/kibana`,
|
||||
query: { start: '2010', end: '2011' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(3);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should return cached response with `isCachable: true` option', async () => {
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: true,
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: true,
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
await callApi(http, {
|
||||
await callApi(core, {
|
||||
isCachable: true,
|
||||
pathname: `/api/kibana`,
|
||||
query: { end: '2030' },
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledTimes(1);
|
||||
expect(core.http.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as callApiExports from './rest/callApi';
|
||||
import { createCallApmApi, callApmApi } from './rest/createCallApmApi';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
|
||||
const callApi = jest
|
||||
.spyOn(callApiExports, 'callApi')
|
||||
|
@ -15,7 +15,7 @@ const callApi = jest
|
|||
|
||||
describe('callApmApi', () => {
|
||||
beforeEach(() => {
|
||||
createCallApmApi({} as HttpSetup);
|
||||
createCallApmApi({} as CoreStart);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -79,7 +79,7 @@ describe('callApmApi', () => {
|
|||
{},
|
||||
expect.objectContaining({
|
||||
pathname: '/api/apm',
|
||||
method: 'POST',
|
||||
method: 'post',
|
||||
body: {
|
||||
foo: 'bar',
|
||||
bar: 'foo',
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import moment from 'moment';
|
||||
import {
|
||||
fetchObservabilityOverviewPageData,
|
||||
hasData,
|
||||
getHasData,
|
||||
} from './apm_observability_overview_fetchers';
|
||||
import * as createCallApmApi from './createCallApmApi';
|
||||
|
||||
|
@ -31,12 +31,12 @@ describe('Observability dashboard data', () => {
|
|||
describe('hasData', () => {
|
||||
it('returns false when no data is available', async () => {
|
||||
callApmApiMock.mockImplementation(() => Promise.resolve(false));
|
||||
const response = await hasData();
|
||||
const response = await getHasData();
|
||||
expect(response).toBeFalsy();
|
||||
});
|
||||
it('returns true when data is available', async () => {
|
||||
callApmApiMock.mockImplementation(() => Promise.resolve(true));
|
||||
const response = await hasData();
|
||||
callApmApiMock.mockResolvedValue({ hasData: true });
|
||||
const response = await getHasData();
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,9 +58,11 @@ export const fetchObservabilityOverviewPageData = async ({
|
|||
};
|
||||
};
|
||||
|
||||
export async function hasData() {
|
||||
return await callApmApi({
|
||||
export async function getHasData() {
|
||||
const res = await callApmApi({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_data',
|
||||
signal: null,
|
||||
});
|
||||
|
||||
return res.hasData;
|
||||
}
|
||||
|
|
|
@ -5,15 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { isString, startsWith } from 'lodash';
|
||||
import LRU from 'lru-cache';
|
||||
import hash from 'object-hash';
|
||||
import { enableInspectEsQueries } from '../../../../observability/public';
|
||||
import { FetchOptions } from '../../../common/fetch_options';
|
||||
|
||||
function fetchOptionsWithDebug(fetchOptions: FetchOptions) {
|
||||
function fetchOptionsWithDebug(
|
||||
fetchOptions: FetchOptions,
|
||||
inspectableEsQueriesEnabled: boolean
|
||||
) {
|
||||
const debugEnabled =
|
||||
sessionStorage.getItem('apm_debug') === 'true' &&
|
||||
inspectableEsQueriesEnabled &&
|
||||
startsWith(fetchOptions.pathname, '/api/apm');
|
||||
|
||||
const { body, ...rest } = fetchOptions;
|
||||
|
@ -23,7 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) {
|
|||
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
||||
query: {
|
||||
...fetchOptions.query,
|
||||
...(debugEnabled ? { _debug: true } : {}),
|
||||
...(debugEnabled ? { _inspect: true } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -37,9 +41,12 @@ export function clearCache() {
|
|||
export type CallApi = typeof callApi;
|
||||
|
||||
export async function callApi<T = void>(
|
||||
http: HttpSetup,
|
||||
{ http, uiSettings }: CoreStart | CoreSetup,
|
||||
fetchOptions: FetchOptions
|
||||
): Promise<T> {
|
||||
const inspectableEsQueriesEnabled: boolean = uiSettings.get(
|
||||
enableInspectEsQueries
|
||||
);
|
||||
const cacheKey = getCacheKey(fetchOptions);
|
||||
const cacheResponse = cache.get(cacheKey);
|
||||
if (cacheResponse) {
|
||||
|
@ -47,7 +54,8 @@ export async function callApi<T = void>(
|
|||
}
|
||||
|
||||
const { pathname, method = 'get', ...options } = fetchOptionsWithDebug(
|
||||
fetchOptions
|
||||
fetchOptions,
|
||||
inspectableEsQueriesEnabled
|
||||
);
|
||||
|
||||
const lowercaseMethod = method.toLowerCase() as
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { parseEndpoint } from '../../../common/apm_api/parse_endpoint';
|
||||
import { FetchOptions } from '../../../common/fetch_options';
|
||||
import { callApi } from './callApi';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { APMAPI } from '../../../server/routes/create_apm_api';
|
||||
import type { APMAPI } from '../../../server/routes/create_apm_api';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { Client } from '../../../server/routes/typings';
|
||||
import type { Client } from '../../../server/routes/typings';
|
||||
|
||||
export type APMClient = Client<APMAPI['_S']>;
|
||||
export type AutoAbortedAPMClient = Client<APMAPI['_S'], { abortable: false }>;
|
||||
|
@ -24,8 +25,8 @@ export type APMClientOptions = Omit<
|
|||
signal: AbortSignal | null;
|
||||
params?: {
|
||||
body?: any;
|
||||
query?: any;
|
||||
path?: any;
|
||||
query?: Record<string, any>;
|
||||
path?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -35,23 +36,17 @@ export let callApmApi: APMClient = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export function createCallApmApi(http: HttpSetup) {
|
||||
export function createCallApmApi(core: CoreStart | CoreSetup) {
|
||||
callApmApi = ((options: APMClientOptions) => {
|
||||
const { endpoint, params = {}, ...opts } = options;
|
||||
const { endpoint, params, ...opts } = options;
|
||||
const { method, pathname } = parseEndpoint(endpoint, params?.path);
|
||||
|
||||
const path = (params.path || {}) as Record<string, any>;
|
||||
const [method, pathname] = endpoint.split(' ');
|
||||
|
||||
const formattedPathname = Object.keys(path).reduce((acc, paramName) => {
|
||||
return acc.replace(`{${paramName}}`, path[paramName]);
|
||||
}, pathname);
|
||||
|
||||
return callApi(http, {
|
||||
return callApi(core, {
|
||||
...opts,
|
||||
method,
|
||||
pathname: formattedPathname,
|
||||
body: params.body,
|
||||
query: params.query,
|
||||
pathname,
|
||||
body: params?.body,
|
||||
query: params?.query,
|
||||
});
|
||||
}) as APMClient;
|
||||
}
|
||||
|
|
|
@ -160,10 +160,10 @@ The users will be created with the password specified in kibana.dev.yml for `ela
|
|||
|
||||
## Debugging Elasticsearch queries
|
||||
|
||||
All APM api endpoints accept `_debug=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process.
|
||||
All APM api endpoints accept `_inspect=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process.
|
||||
|
||||
Example:
|
||||
`/api/apm/services/my_service?_debug=true`
|
||||
`/api/apm/services/my_service?_inspect=true`
|
||||
|
||||
## Storybook
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ export async function getLatencyDistribution({
|
|||
type Agg = NonNullable<typeof response.aggregations>;
|
||||
|
||||
if (!response.aggregations) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
function formatDistribution(distribution: Agg['distribution']) {
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import chalk from 'chalk';
|
||||
import { KibanaRequest } from '../../../../../../../src/core/server';
|
||||
import { inspectableEsQueriesMap } from '../../../routes/create_api';
|
||||
|
||||
function formatObj(obj: Record<string, any>) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
|
@ -18,10 +20,18 @@ export async function callAsyncWithDebug<T>({
|
|||
cb,
|
||||
getDebugMessage,
|
||||
debug,
|
||||
request,
|
||||
requestType,
|
||||
requestParams,
|
||||
isCalledWithInternalUser,
|
||||
}: {
|
||||
cb: () => Promise<T>;
|
||||
getDebugMessage: () => { body: string; title: string };
|
||||
debug: boolean;
|
||||
request: KibanaRequest;
|
||||
requestType: string;
|
||||
requestParams: Record<string, any>;
|
||||
isCalledWithInternalUser: boolean; // only allow inspection of queries that were retrieved with credentials of the end user
|
||||
}) {
|
||||
if (!debug) {
|
||||
return cb();
|
||||
|
@ -41,16 +51,27 @@ export async function callAsyncWithDebug<T>({
|
|||
if (debug) {
|
||||
const highlightColor = esError ? 'bgRed' : 'inverse';
|
||||
const diff = process.hrtime(startTime);
|
||||
const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`;
|
||||
const duration = Math.round(diff[0] * 1000 + diff[1] / 1e6); // duration in ms
|
||||
|
||||
const { title, body } = getDebugMessage();
|
||||
|
||||
console.log(
|
||||
chalk.bold[highlightColor](`=== Debug: ${title} (${duration}) ===`)
|
||||
chalk.bold[highlightColor](`=== Debug: ${title} (${duration}ms) ===`)
|
||||
);
|
||||
|
||||
console.log(body);
|
||||
console.log(`\n`);
|
||||
|
||||
const inspectableEsQueries = inspectableEsQueriesMap.get(request);
|
||||
if (!isCalledWithInternalUser && inspectableEsQueries) {
|
||||
inspectableEsQueries.push({
|
||||
response: res,
|
||||
duration,
|
||||
requestType,
|
||||
requestParams: omit(requestParams, 'headers'),
|
||||
esError: esError?.response ?? esError?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (esError) {
|
||||
|
@ -62,13 +83,13 @@ export async function callAsyncWithDebug<T>({
|
|||
|
||||
export const getDebugBody = (
|
||||
params: Record<string, any>,
|
||||
operationName: string
|
||||
requestType: string
|
||||
) => {
|
||||
if (operationName === 'search') {
|
||||
if (requestType === 'search') {
|
||||
return `GET ${params.index}/_search\n${formatObj(params.body)}`;
|
||||
}
|
||||
|
||||
return `${chalk.bold('ES operation:')} ${operationName}\n${chalk.bold(
|
||||
return `${chalk.bold('ES operation:')} ${requestType}\n${chalk.bold(
|
||||
'ES query:'
|
||||
)}\n${formatObj(params)}`;
|
||||
};
|
||||
|
|
|
@ -93,6 +93,9 @@ export function createApmEventClient({
|
|||
ignore_unavailable: true,
|
||||
};
|
||||
|
||||
// only "search" operation is currently supported
|
||||
const requestType = 'search';
|
||||
|
||||
return callAsyncWithDebug({
|
||||
cb: () => {
|
||||
const searchPromise = cancelEsRequestOnAbort(
|
||||
|
@ -103,10 +106,14 @@ export function createApmEventClient({
|
|||
return unwrapEsResponse(searchPromise);
|
||||
},
|
||||
getDebugMessage: () => ({
|
||||
body: getDebugBody(searchParams, 'search'),
|
||||
body: getDebugBody(searchParams, requestType),
|
||||
title: getDebugTitle(request),
|
||||
}),
|
||||
isCalledWithInternalUser: false,
|
||||
debug,
|
||||
request,
|
||||
requestType,
|
||||
requestParams: searchParams,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,10 +40,10 @@ export function createInternalESClient({
|
|||
|
||||
function callEs<T extends { body: any }>({
|
||||
cb,
|
||||
operationName,
|
||||
requestType,
|
||||
params,
|
||||
}: {
|
||||
operationName: string;
|
||||
requestType: string;
|
||||
cb: () => TransportRequestPromise<T>;
|
||||
params: Record<string, any>;
|
||||
}) {
|
||||
|
@ -51,9 +51,13 @@ export function createInternalESClient({
|
|||
cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)),
|
||||
getDebugMessage: () => ({
|
||||
title: getDebugTitle(request),
|
||||
body: getDebugBody(params, operationName),
|
||||
body: getDebugBody(params, requestType),
|
||||
}),
|
||||
debug: context.params.query._debug,
|
||||
debug: context.params.query._inspect,
|
||||
isCalledWithInternalUser: true,
|
||||
request,
|
||||
requestType,
|
||||
requestParams: params,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,28 +69,28 @@ export function createInternalESClient({
|
|||
params: TSearchRequest
|
||||
): Promise<ESSearchResponse<TDocument, TSearchRequest>> => {
|
||||
return callEs({
|
||||
operationName: 'search',
|
||||
requestType: 'search',
|
||||
cb: () => asInternalUser.search(params),
|
||||
params,
|
||||
});
|
||||
},
|
||||
index: <T>(params: APMIndexDocumentParams<T>) => {
|
||||
return callEs({
|
||||
operationName: 'index',
|
||||
requestType: 'index',
|
||||
cb: () => asInternalUser.index(params),
|
||||
params,
|
||||
});
|
||||
},
|
||||
delete: (params: DeleteRequest): Promise<{ result: string }> => {
|
||||
return callEs({
|
||||
operationName: 'delete',
|
||||
requestType: 'delete',
|
||||
cb: () => asInternalUser.delete(params),
|
||||
params,
|
||||
});
|
||||
},
|
||||
indicesCreate: (params: CreateIndexRequest) => {
|
||||
return callEs({
|
||||
operationName: 'indices.create',
|
||||
requestType: 'indices.create',
|
||||
cb: () => asInternalUser.indices.create(params),
|
||||
params,
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ export const withDefaultValidators = (
|
|||
validators: { [key: string]: Schema } = {}
|
||||
) => {
|
||||
return Joi.object().keys({
|
||||
_debug: Joi.bool(),
|
||||
_inspect: Joi.bool(),
|
||||
start: dateValidation,
|
||||
end: dateValidation,
|
||||
uiFilters: Joi.string(),
|
||||
|
|
|
@ -51,7 +51,7 @@ function getMockRequest() {
|
|||
) as APMConfig,
|
||||
params: {
|
||||
query: {
|
||||
_debug: false,
|
||||
_inspect: false,
|
||||
},
|
||||
},
|
||||
core: {
|
||||
|
|
|
@ -45,7 +45,7 @@ export interface SetupTimeRange {
|
|||
|
||||
interface SetupRequestParams {
|
||||
query?: {
|
||||
_debug?: boolean;
|
||||
_inspect?: boolean;
|
||||
|
||||
/**
|
||||
* Timestamp in ms since epoch
|
||||
|
@ -88,7 +88,7 @@ export async function setupRequest<TParams extends SetupRequestParams>(
|
|||
indices,
|
||||
apmEventClient: createApmEventClient({
|
||||
esClient: context.core.elasticsearch.client.asCurrentUser,
|
||||
debug: context.params.query._debug,
|
||||
debug: context.params.query._inspect,
|
||||
request,
|
||||
indices,
|
||||
options: { includeFrozen },
|
||||
|
|
|
@ -21,20 +21,20 @@ export async function createStaticIndexPattern(
|
|||
setup: Setup,
|
||||
context: APMRequestHandlerContext,
|
||||
savedObjectsClient: InternalSavedObjectsClient
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
return withApmSpan('create_static_index_pattern', async () => {
|
||||
const { config } = context;
|
||||
|
||||
// don't autocreate APM index pattern if it's been disabled via the config
|
||||
if (!config['xpack.apm.autocreateApmIndexPattern']) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Discover and other apps will throw errors if an index pattern exists without having matching indices.
|
||||
// The following ensures the index pattern is only created if APM data is found
|
||||
const hasData = await hasHistoricalAgentData(setup);
|
||||
if (!hasData) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -49,12 +49,12 @@ export async function createStaticIndexPattern(
|
|||
{ id: APM_STATIC_INDEX_PATTERN_ID, overwrite: false }
|
||||
)
|
||||
);
|
||||
return;
|
||||
return true;
|
||||
} catch (e) {
|
||||
// if the index pattern (saved object) already exists a conflict error (code: 409) will be thrown
|
||||
// that error should be silenced
|
||||
if (SavedObjectsErrorHelpers.isConflictError(e)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ProcessorEvent } from '../../../common/processor_event';
|
|||
import { withApmSpan } from '../../utils/with_apm_span';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
export function hasData({ setup }: { setup: Setup }) {
|
||||
export function getHasData({ setup }: { setup: Setup }) {
|
||||
return withApmSpan('observability_overview_has_apm_data', async () => {
|
||||
const { apmEventClient } = setup;
|
||||
try {
|
||||
|
|
|
@ -35,12 +35,14 @@ export const transactionErrorRateChartPreview = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _debug, ...alertParams } = context.params.query;
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
|
||||
return getTransactionErrorRateChartPreview({
|
||||
const errorRateChartPreview = await getTransactionErrorRateChartPreview({
|
||||
setup,
|
||||
alertParams,
|
||||
});
|
||||
|
||||
return { errorRateChartPreview };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -50,11 +52,13 @@ export const transactionErrorCountChartPreview = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _debug, ...alertParams } = context.params.query;
|
||||
return getTransactionErrorCountChartPreview({
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
const errorCountChartPreview = await getTransactionErrorCountChartPreview({
|
||||
setup,
|
||||
alertParams,
|
||||
});
|
||||
|
||||
return { errorCountChartPreview };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -64,11 +68,13 @@ export const transactionDurationChartPreview = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _debug, ...alertParams } = context.params.query;
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
|
||||
return getTransactionDurationChartPreview({
|
||||
const latencyChartPreview = await getTransactionDurationChartPreview({
|
||||
alertParams,
|
||||
setup,
|
||||
});
|
||||
|
||||
return { latencyChartPreview };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -48,6 +48,49 @@ const getCoreMock = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const initApi = (params?: RouteParamsRT) => {
|
||||
const { mock, context, createRouter, get, post } = getCoreMock();
|
||||
const handlerMock = jest.fn();
|
||||
createApi()
|
||||
.add(() => ({
|
||||
endpoint: 'GET /foo',
|
||||
params,
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: handlerMock,
|
||||
}))
|
||||
.init(mock, context);
|
||||
|
||||
const routeHandler = get.mock.calls[0][1];
|
||||
|
||||
const responseMock = {
|
||||
ok: jest.fn(),
|
||||
custom: jest.fn(),
|
||||
};
|
||||
|
||||
const simulateRequest = (requestMock: any) => {
|
||||
return routeHandler(
|
||||
{},
|
||||
{
|
||||
// stub default values
|
||||
params: {},
|
||||
query: {},
|
||||
body: null,
|
||||
...requestMock,
|
||||
},
|
||||
responseMock
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
simulateRequest,
|
||||
handlerMock,
|
||||
createRouter,
|
||||
get,
|
||||
post,
|
||||
responseMock,
|
||||
};
|
||||
};
|
||||
|
||||
describe('createApi', () => {
|
||||
it('registers a route with the server', () => {
|
||||
const { mock, context, createRouter, post, get, put } = getCoreMock();
|
||||
|
@ -56,7 +99,7 @@ describe('createApi', () => {
|
|||
.add(() => ({
|
||||
endpoint: 'GET /foo',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => null,
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add(() => ({
|
||||
endpoint: 'POST /bar',
|
||||
|
@ -64,21 +107,21 @@ describe('createApi', () => {
|
|||
body: t.string,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => null,
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add(() => ({
|
||||
endpoint: 'PUT /baz',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => null,
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add({
|
||||
endpoint: 'GET /qux',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => null,
|
||||
handler: async () => ({}),
|
||||
})
|
||||
.init(mock, context);
|
||||
|
||||
|
@ -122,102 +165,78 @@ describe('createApi', () => {
|
|||
});
|
||||
|
||||
describe('when validating', () => {
|
||||
const initApi = (params?: RouteParamsRT) => {
|
||||
const { mock, context, createRouter, get, post } = getCoreMock();
|
||||
const handlerMock = jest.fn();
|
||||
createApi()
|
||||
.add(() => ({
|
||||
endpoint: 'GET /foo',
|
||||
params,
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: handlerMock,
|
||||
}))
|
||||
.init(mock, context);
|
||||
describe('_inspect', () => {
|
||||
it('allows _inspect=true', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi();
|
||||
await simulateRequest({ query: { _inspect: 'true' } });
|
||||
|
||||
const routeHandler = get.mock.calls[0][1];
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
expect(params).toEqual({ query: { _inspect: true } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const responseMock = {
|
||||
ok: jest.fn(),
|
||||
internalError: jest.fn(),
|
||||
notFound: jest.fn(),
|
||||
forbidden: jest.fn(),
|
||||
badRequest: jest.fn(),
|
||||
};
|
||||
// responds with ok
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(responseMock.ok).toHaveBeenCalledWith({
|
||||
body: { _inspect: [] },
|
||||
});
|
||||
});
|
||||
|
||||
const simulate = (requestMock: any) => {
|
||||
return routeHandler(
|
||||
{},
|
||||
{
|
||||
// stub default values
|
||||
params: {},
|
||||
query: {},
|
||||
body: null,
|
||||
...requestMock,
|
||||
it('rejects _inspect=1', async () => {
|
||||
const { simulateRequest, responseMock } = initApi();
|
||||
await simulateRequest({ query: { _inspect: 1 } });
|
||||
|
||||
// responds with error handler
|
||||
expect(responseMock.ok).not.toHaveBeenCalled();
|
||||
expect(responseMock.custom).toHaveBeenCalledWith({
|
||||
body: {
|
||||
attributes: { _inspect: [] },
|
||||
message:
|
||||
'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
|
||||
},
|
||||
responseMock
|
||||
);
|
||||
};
|
||||
|
||||
return { simulate, handlerMock, createRouter, get, post, responseMock };
|
||||
};
|
||||
|
||||
it('adds a _debug query parameter by default', async () => {
|
||||
const { simulate, handlerMock, responseMock } = initApi();
|
||||
|
||||
await simulate({ query: { _debug: 'true' } });
|
||||
|
||||
expect(responseMock.badRequest).not.toHaveBeenCalled();
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(responseMock.ok).toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
|
||||
expect(params).toEqual({
|
||||
query: {
|
||||
_debug: true,
|
||||
},
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
await simulate({
|
||||
query: {
|
||||
_debug: 1,
|
||||
},
|
||||
});
|
||||
it('allows omitting _inspect', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi();
|
||||
await simulateRequest({ query: {} });
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalled();
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
expect(params).toEqual({ query: { _inspect: false } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// responds with ok
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(responseMock.ok).toHaveBeenCalledWith({ body: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if any parameters are used but no types are defined', async () => {
|
||||
const { simulate, responseMock } = initApi();
|
||||
it('throws if unknown parameters are provided', async () => {
|
||||
const { simulateRequest, responseMock } = initApi();
|
||||
|
||||
await simulate({
|
||||
query: {
|
||||
_debug: true,
|
||||
extra: '',
|
||||
},
|
||||
await simulateRequest({
|
||||
query: { _inspect: true, extra: '' },
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
body: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(2);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(3);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates path parameters', async () => {
|
||||
const { simulate, handlerMock, responseMock } = initApi(
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
path: t.type({
|
||||
foo: t.string,
|
||||
|
@ -225,7 +244,7 @@ describe('createApi', () => {
|
|||
})
|
||||
);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
|
@ -234,7 +253,7 @@ describe('createApi', () => {
|
|||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.badRequest).not.toHaveBeenCalled();
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
|
||||
|
@ -243,48 +262,48 @@ describe('createApi', () => {
|
|||
foo: 'bar',
|
||||
},
|
||||
query: {
|
||||
_debug: false,
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
params: {
|
||||
bar: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 9,
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(2);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
extra: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(3);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates body parameters', async () => {
|
||||
const { simulate, handlerMock, responseMock } = initApi(
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
body: t.string,
|
||||
})
|
||||
);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
body: '',
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).not.toHaveBeenCalled();
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
@ -293,19 +312,19 @@ describe('createApi', () => {
|
|||
expect(params).toEqual({
|
||||
body: '',
|
||||
query: {
|
||||
_debug: false,
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
body: null,
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('validates query parameters', async () => {
|
||||
const { simulate, handlerMock, responseMock } = initApi(
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
query: t.type({
|
||||
bar: t.string,
|
||||
|
@ -314,15 +333,15 @@ describe('createApi', () => {
|
|||
})
|
||||
);
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
query: {
|
||||
bar: '',
|
||||
_debug: 'true',
|
||||
_inspect: 'true',
|
||||
filterNames: JSON.stringify(['hostName', 'agentName']),
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).not.toHaveBeenCalled();
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
@ -331,19 +350,19 @@ describe('createApi', () => {
|
|||
expect(params).toEqual({
|
||||
query: {
|
||||
bar: '',
|
||||
_debug: true,
|
||||
_inspect: true,
|
||||
filterNames: ['hostName', 'agentName'],
|
||||
},
|
||||
});
|
||||
|
||||
await simulate({
|
||||
await simulateRequest({
|
||||
query: {
|
||||
bar: '',
|
||||
foo: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.badRequest).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,19 +11,20 @@ import { schema } from '@kbn/config-schema';
|
|||
import * as t from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server';
|
||||
import { KibanaRequest, RouteRegistrar } from 'src/core/server';
|
||||
import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
|
||||
import agent from 'elastic-apm-node';
|
||||
import { parseMethod } from '../../../common/apm_api/parse_endpoint';
|
||||
import { merge } from '../../../common/runtime_types/merge';
|
||||
import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt';
|
||||
import { APMConfig } from '../..';
|
||||
import { ServerAPI } from '../typings';
|
||||
import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings';
|
||||
import { jsonRt } from '../../../common/runtime_types/json_rt';
|
||||
import type { ApmPluginRequestHandlerContext } from '../typings';
|
||||
|
||||
const debugRt = t.exact(
|
||||
const inspectRt = t.exact(
|
||||
t.partial({
|
||||
query: t.exact(t.partial({ _debug: jsonRt.pipe(t.boolean) })),
|
||||
query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -32,6 +33,11 @@ type RouteOrRouteFactoryFn = Parameters<ServerAPI<{}>['add']>[0];
|
|||
const isNotEmpty = (val: any) =>
|
||||
val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val));
|
||||
|
||||
export const inspectableEsQueriesMap = new WeakMap<
|
||||
KibanaRequest,
|
||||
InspectResponse
|
||||
>();
|
||||
|
||||
export function createApi() {
|
||||
const routes: RouteOrRouteFactoryFn[] = [];
|
||||
const api: ServerAPI<{}> = {
|
||||
|
@ -58,24 +64,10 @@ export function createApi() {
|
|||
const { params, endpoint, options, handler } = route;
|
||||
|
||||
const [method, path] = endpoint.split(' ');
|
||||
|
||||
const typedRouterMethod = method.trim().toLowerCase() as
|
||||
| 'get'
|
||||
| 'post'
|
||||
| 'put'
|
||||
| 'delete';
|
||||
|
||||
if (!['get', 'post', 'put', 'delete'].includes(typedRouterMethod)) {
|
||||
throw new Error(
|
||||
"Couldn't register route, as endpoint was not prefixed with a valid HTTP method"
|
||||
);
|
||||
}
|
||||
const typedRouterMethod = parseMethod(method);
|
||||
|
||||
// For all runtime types with props, we create an exact
|
||||
// version that will strip all keys that are unvalidated.
|
||||
|
||||
const paramsRt = params ? merge([params, debugRt]) : debugRt;
|
||||
|
||||
const anyObject = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
(router[typedRouterMethod] as RouteRegistrar<
|
||||
|
@ -102,56 +94,52 @@ export function createApi() {
|
|||
});
|
||||
}
|
||||
|
||||
// init debug queries
|
||||
inspectableEsQueriesMap.set(request, []);
|
||||
|
||||
try {
|
||||
const paramMap = pickBy(
|
||||
{
|
||||
path: request.params,
|
||||
body: request.body,
|
||||
query: {
|
||||
_debug: 'false',
|
||||
...request.query,
|
||||
},
|
||||
},
|
||||
isNotEmpty
|
||||
);
|
||||
|
||||
const result = strictKeysRt(paramsRt).decode(paramMap);
|
||||
|
||||
if (isLeft(result)) {
|
||||
throw Boom.badRequest(PathReporter.report(result)[0]);
|
||||
}
|
||||
const validParams = validateParams(request, params);
|
||||
const data = await handler({
|
||||
request,
|
||||
context: {
|
||||
...context,
|
||||
plugins,
|
||||
// Only return values for parameters that have runtime types,
|
||||
// but always include query as _debug is always set even if
|
||||
// it's not defined in the route.
|
||||
params: mergeLodash(
|
||||
{ query: { _debug: false } },
|
||||
pickBy(result.right, isNotEmpty)
|
||||
),
|
||||
params: validParams,
|
||||
config,
|
||||
logger,
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({ body: data as any });
|
||||
const body = { ...data };
|
||||
if (validParams.query._inspect) {
|
||||
body._inspect = inspectableEsQueriesMap.get(request);
|
||||
}
|
||||
|
||||
// cleanup
|
||||
inspectableEsQueriesMap.delete(request);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
const opts = {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: error.message,
|
||||
attributes: {
|
||||
_inspect: inspectableEsQueriesMap.get(request),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
return convertBoomToKibanaResponse(error, response);
|
||||
opts.statusCode = error.output.statusCode;
|
||||
}
|
||||
|
||||
if (error instanceof RequestAbortedError) {
|
||||
return response.custom({
|
||||
statusCode: 499,
|
||||
body: {
|
||||
message: 'Client closed request',
|
||||
},
|
||||
});
|
||||
opts.statusCode = 499;
|
||||
opts.body.message = 'Client closed request';
|
||||
}
|
||||
throw error;
|
||||
|
||||
return response.custom(opts);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -162,22 +150,35 @@ export function createApi() {
|
|||
return api;
|
||||
}
|
||||
|
||||
function convertBoomToKibanaResponse(
|
||||
error: Boom.Boom,
|
||||
response: KibanaResponseFactory
|
||||
function validateParams(
|
||||
request: KibanaRequest,
|
||||
params: RouteParamsRT | undefined
|
||||
) {
|
||||
const opts = { body: { message: error.message } };
|
||||
switch (error.output.statusCode) {
|
||||
case 404:
|
||||
return response.notFound(opts);
|
||||
const paramsRt = params ? merge([params, inspectRt]) : inspectRt;
|
||||
const paramMap = pickBy(
|
||||
{
|
||||
path: request.params,
|
||||
body: request.body,
|
||||
query: {
|
||||
_inspect: 'false',
|
||||
// @ts-ignore
|
||||
...request.query,
|
||||
},
|
||||
},
|
||||
isNotEmpty
|
||||
);
|
||||
|
||||
case 400:
|
||||
return response.badRequest(opts);
|
||||
const result = strictKeysRt(paramsRt).decode(paramMap);
|
||||
|
||||
case 403:
|
||||
return response.forbidden(opts);
|
||||
|
||||
default:
|
||||
throw error;
|
||||
if (isLeft(result)) {
|
||||
throw Boom.badRequest(PathReporter.report(result)[0]);
|
||||
}
|
||||
|
||||
// Only return values for parameters that have runtime types,
|
||||
// but always include query as _inspect is always set even if
|
||||
// it's not defined in the route.
|
||||
return mergeLodash(
|
||||
{ query: { _inspect: false } },
|
||||
pickBy(result.right, isNotEmpty)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { Route, RouteParamsRT } from './typings';
|
||||
import { HandlerReturn, Route, RouteParamsRT } from './typings';
|
||||
|
||||
export function createRoute<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined,
|
||||
TReturn = unknown
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
): Route<TEndpoint, TRouteParamsRT, TReturn>;
|
||||
|
||||
export function createRoute<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined,
|
||||
TReturn = unknown
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: (core: CoreSetup) => Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
): (core: CoreSetup) => Route<TEndpoint, TRouteParamsRT, TReturn>;
|
||||
|
|
|
@ -30,10 +30,12 @@ export const environmentsRoute = createRoute({
|
|||
setup
|
||||
);
|
||||
|
||||
return getEnvironments({
|
||||
const environments = await getEnvironments({
|
||||
setup,
|
||||
serviceName,
|
||||
searchAggregatedTransactions,
|
||||
});
|
||||
|
||||
return { environments };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ export const errorsRoute = createRoute({
|
|||
const { serviceName } = params.path;
|
||||
const { environment, kuery, sortField, sortDirection } = params.query;
|
||||
|
||||
return getErrorGroups({
|
||||
const errorGroups = await getErrorGroups({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
|
@ -44,6 +44,8 @@ export const errorsRoute = createRoute({
|
|||
sortDirection,
|
||||
setup,
|
||||
});
|
||||
|
||||
return { errorGroups };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -21,10 +21,13 @@ export const staticIndexPatternRoute = createRoute((core) => ({
|
|||
getInternalSavedObjectsClient(core),
|
||||
]);
|
||||
|
||||
await createStaticIndexPattern(setup, context, savedObjectsClient);
|
||||
const didCreateIndexPattern = await createStaticIndexPattern(
|
||||
setup,
|
||||
context,
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
// send empty response regardless of outcome
|
||||
return undefined;
|
||||
return { created: didCreateIndexPattern };
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -41,6 +44,8 @@ export const apmIndexPatternTitleRoute = createRoute({
|
|||
endpoint: 'GET /api/apm/index_pattern/title',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
return getApmIndexPatternTitle(context);
|
||||
return {
|
||||
indexPatternTitle: getApmIndexPatternTitle(context),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as t from 'io-ts';
|
|||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getServiceCount } from '../lib/observability_overview/get_service_count';
|
||||
import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates';
|
||||
import { hasData } from '../lib/observability_overview/has_data';
|
||||
import { getHasData } from '../lib/observability_overview/has_data';
|
||||
import { createRoute } from './create_route';
|
||||
import { rangeRt } from './default_api_types';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
|
@ -20,7 +20,8 @@ export const observabilityOverviewHasDataRoute = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
return await hasData({ setup });
|
||||
const res = await getHasData({ setup });
|
||||
return { hasData: res };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -79,12 +79,14 @@ export const rumPageLoadDistributionRoute = createRoute({
|
|||
query: { minPercentile, maxPercentile, urlQuery },
|
||||
} = context.params;
|
||||
|
||||
return getPageLoadDistribution({
|
||||
const pageLoadDistribution = await getPageLoadDistribution({
|
||||
setup,
|
||||
minPercentile,
|
||||
maxPercentile,
|
||||
urlQuery,
|
||||
});
|
||||
|
||||
return { pageLoadDistribution };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -105,13 +107,15 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
|
|||
query: { minPercentile, maxPercentile, breakdown, urlQuery },
|
||||
} = context.params;
|
||||
|
||||
return getPageLoadDistBreakdown({
|
||||
const pageLoadDistBreakdown = await getPageLoadDistBreakdown({
|
||||
setup,
|
||||
minPercentile: Number(minPercentile),
|
||||
maxPercentile: Number(maxPercentile),
|
||||
breakdown,
|
||||
urlQuery,
|
||||
});
|
||||
|
||||
return { pageLoadDistBreakdown };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -145,7 +149,8 @@ export const rumServicesRoute = createRoute({
|
|||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
return getRumServices({ setup });
|
||||
const rumServices = await getRumServices({ setup });
|
||||
return { rumServices };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -322,12 +327,14 @@ function createLocalFiltersRoute<
|
|||
setup,
|
||||
});
|
||||
|
||||
return getLocalUIFilters({
|
||||
const localUiFilters = await getLocalUIFilters({
|
||||
projection,
|
||||
setup,
|
||||
uiFilters,
|
||||
localFilterNames: filterNames,
|
||||
});
|
||||
|
||||
return { localUiFilters };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,10 +26,7 @@ export const serviceNodesRoute = createRoute({
|
|||
const { serviceName } = params.path;
|
||||
const { kuery } = params.query;
|
||||
|
||||
return getServiceNodes({
|
||||
kuery,
|
||||
setup,
|
||||
serviceName,
|
||||
});
|
||||
const serviceNodes = await getServiceNodes({ kuery, setup, serviceName });
|
||||
return { serviceNodes };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -56,15 +56,13 @@ export const servicesRoute = createRoute({
|
|||
setup
|
||||
);
|
||||
|
||||
const services = await getServices({
|
||||
return getServices({
|
||||
environment,
|
||||
kuery,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
logger: context.logger,
|
||||
});
|
||||
|
||||
return services;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -465,7 +463,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
|
|||
|
||||
const { start, end } = setup;
|
||||
|
||||
return getServiceInstancesPrimaryStatistics({
|
||||
const serviceInstances = await getServiceInstancesPrimaryStatistics({
|
||||
environment,
|
||||
kuery,
|
||||
latencyAggregationType,
|
||||
|
@ -476,6 +474,8 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
|
|||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return { serviceInstances };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -558,12 +558,14 @@ export const serviceDependenciesRoute = createRoute({
|
|||
const { serviceName } = context.params.path;
|
||||
const { environment, numBuckets } = context.params.query;
|
||||
|
||||
return getServiceDependencies({
|
||||
const serviceDependencies = await getServiceDependencies({
|
||||
serviceName,
|
||||
environment,
|
||||
setup,
|
||||
numBuckets,
|
||||
});
|
||||
|
||||
return { serviceDependencies };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -586,12 +588,14 @@ export const serviceProfilingTimelineRoute = createRoute({
|
|||
query: { environment, kuery },
|
||||
} = context.params;
|
||||
|
||||
return getServiceProfilingTimeline({
|
||||
const profilingTimeline = await getServiceProfilingTimeline({
|
||||
kuery,
|
||||
setup,
|
||||
serviceName,
|
||||
environment,
|
||||
});
|
||||
|
||||
return { profilingTimeline };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ export const agentConfigurationRoute = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
return await listConfigurations({ setup });
|
||||
const configurations = await listConfigurations({ setup });
|
||||
return { configurations };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -204,10 +205,12 @@ export const listAgentConfigurationServicesRoute = createRoute({
|
|||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
return await getServiceNames({
|
||||
const serviceNames = await getServiceNames({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
});
|
||||
|
||||
return { serviceNames };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -225,11 +228,13 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({
|
|||
setup
|
||||
);
|
||||
|
||||
return await getEnvironments({
|
||||
const environments = await getEnvironments({
|
||||
serviceName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
});
|
||||
|
||||
return { environments };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -71,6 +71,8 @@ export const createAnomalyDetectionJobsRoute = createRoute({
|
|||
licensingPlugin: context.licensing,
|
||||
featureName: 'ml',
|
||||
});
|
||||
|
||||
return { jobCreated: true };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -85,10 +87,12 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({
|
|||
setup
|
||||
);
|
||||
|
||||
return await getAllEnvironments({
|
||||
const environments = await getAllEnvironments({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
includeMissing: true,
|
||||
});
|
||||
|
||||
return { environments };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,7 +18,8 @@ export const apmIndexSettingsRoute = createRoute({
|
|||
endpoint: 'GET /api/apm/settings/apm-index-settings',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
return await getApmIndexSettings({ context });
|
||||
const apmIndexSettings = await getApmIndexSettings({ context });
|
||||
return { apmIndexSettings };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,8 @@ export const listCustomLinksRoute = createRoute({
|
|||
const { query } = context.params;
|
||||
// picks only the items listed in FILTER_OPTIONS
|
||||
const filters = pick(query, FILTER_OPTIONS);
|
||||
return await listCustomLinks({ setup, filters });
|
||||
const customLinks = await listCustomLinks({ setup, filters });
|
||||
return { customLinks };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
Logger,
|
||||
} from 'src/core/server';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RequiredKeys } from 'utility-types';
|
||||
import { RequiredKeys, DeepPartial } from 'utility-types';
|
||||
import { ObservabilityPluginSetup } from '../../../observability/server';
|
||||
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
|
@ -21,6 +21,20 @@ import { MlPluginSetup } from '../../../ml/server';
|
|||
import { FetchOptions } from '../../common/fetch_options';
|
||||
import { APMConfig } from '..';
|
||||
|
||||
export type HandlerReturn = Record<string, any>;
|
||||
|
||||
interface InspectQueryParam {
|
||||
query: { _inspect: boolean };
|
||||
}
|
||||
|
||||
export type InspectResponse = Array<{
|
||||
response: any;
|
||||
duration: number;
|
||||
requestType: string;
|
||||
requestParams: Record<string, unknown>;
|
||||
esError: Error;
|
||||
}>;
|
||||
|
||||
export interface RouteParams {
|
||||
path?: Record<string, unknown>;
|
||||
query?: Record<string, unknown>;
|
||||
|
@ -36,15 +50,14 @@ export type RouteParamsRT = WithoutIncompatibleMethods<t.Type<RouteParams>>;
|
|||
|
||||
export type RouteHandler<
|
||||
TParamsRT extends RouteParamsRT | undefined,
|
||||
TReturn
|
||||
TReturn extends HandlerReturn
|
||||
> = (kibanaContext: {
|
||||
context: APMRequestHandlerContext<
|
||||
(TParamsRT extends RouteParamsRT ? t.TypeOf<TParamsRT> : {}) & {
|
||||
query: { _debug: boolean };
|
||||
}
|
||||
(TParamsRT extends RouteParamsRT ? t.TypeOf<TParamsRT> : {}) &
|
||||
InspectQueryParam
|
||||
>;
|
||||
request: KibanaRequest;
|
||||
}) => Promise<TReturn>;
|
||||
}) => Promise<TReturn extends any[] ? never : TReturn>;
|
||||
|
||||
interface RouteOptions {
|
||||
tags: Array<
|
||||
|
@ -58,7 +71,7 @@ interface RouteOptions {
|
|||
export interface Route<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined,
|
||||
TReturn
|
||||
TReturn extends HandlerReturn
|
||||
> {
|
||||
endpoint: TEndpoint;
|
||||
options: RouteOptions;
|
||||
|
@ -76,7 +89,7 @@ export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
|
|||
export type APMRequestHandlerContext<
|
||||
TRouteParams = {}
|
||||
> = ApmPluginRequestHandlerContext & {
|
||||
params: TRouteParams & { query: { _debug: boolean } };
|
||||
params: TRouteParams & InspectQueryParam;
|
||||
config: APMConfig;
|
||||
logger: Logger;
|
||||
plugins: {
|
||||
|
@ -97,8 +110,8 @@ export interface ServerAPI<TRouteState extends RouteState> {
|
|||
_S: TRouteState;
|
||||
add<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined,
|
||||
TReturn = unknown
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route:
|
||||
| Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
|
@ -108,7 +121,7 @@ export interface ServerAPI<TRouteState extends RouteState> {
|
|||
{
|
||||
[key in TEndpoint]: {
|
||||
params: TRouteParamsRT;
|
||||
ret: TReturn;
|
||||
ret: TReturn & { _inspect?: InspectResponse };
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
@ -132,6 +145,16 @@ type MaybeOptional<T extends { params: Record<string, any> }> = RequiredKeys<
|
|||
? { params?: T['params'] }
|
||||
: { params: T['params'] };
|
||||
|
||||
export type MaybeParams<
|
||||
TRouteState,
|
||||
TEndpoint extends keyof TRouteState & string
|
||||
> = TRouteState[TEndpoint] extends { params: t.Any }
|
||||
? MaybeOptional<{
|
||||
params: t.OutputOf<TRouteState[TEndpoint]['params']> &
|
||||
DeepPartial<InspectQueryParam>;
|
||||
}>
|
||||
: {};
|
||||
|
||||
export type Client<
|
||||
TRouteState,
|
||||
TOptions extends { abortable: boolean } = { abortable: true }
|
||||
|
@ -142,9 +165,7 @@ export type Client<
|
|||
> & {
|
||||
forceCache?: boolean;
|
||||
endpoint: TEndpoint;
|
||||
} & (TRouteState[TEndpoint] extends { params: t.Any }
|
||||
? MaybeOptional<{ params: t.OutputOf<TRouteState[TEndpoint]['params']> }>
|
||||
: {}) &
|
||||
} & MaybeParams<TRouteState, TEndpoint> &
|
||||
(TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
|
||||
) => Promise<
|
||||
TRouteState[TEndpoint] extends { ret: any }
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export const enableAlertingExperience = 'observability:enableAlertingExperience';
|
||||
export const enableInspectEsQueries = 'observability:enableInspectEsQueries';
|
||||
|
|
|
@ -19,6 +19,7 @@ export type {
|
|||
ObservabilityPublicPluginsSetup,
|
||||
ObservabilityPublicPluginsStart,
|
||||
};
|
||||
export { enableInspectEsQueries } from '../common/ui_settings_keys';
|
||||
|
||||
export const plugin: PluginInitializer<
|
||||
ObservabilityPublicSetup,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiSettingsParams } from '../../../../src/core/types';
|
||||
import { enableAlertingExperience } from '../common/ui_settings_keys';
|
||||
import { enableAlertingExperience, enableInspectEsQueries } from '../common/ui_settings_keys';
|
||||
|
||||
/**
|
||||
* uiSettings definitions for Observability.
|
||||
|
@ -29,4 +29,15 @@ export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
|
|||
),
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[enableInspectEsQueries]: {
|
||||
category: ['observability'],
|
||||
name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', {
|
||||
defaultMessage: 'inspect ES queries',
|
||||
}),
|
||||
value: false,
|
||||
description: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentDescription', {
|
||||
defaultMessage: 'Inspect Elasticsearch queries in API responses.',
|
||||
}),
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -67,7 +67,7 @@ class ApiService {
|
|||
|
||||
const response = await this._http!.fetch({
|
||||
path: apiUrl,
|
||||
query: { ...params, ...(debugEnabled ? { _debug: true } : {}) },
|
||||
query: { ...params, ...(debugEnabled ? { _inspect: true } : {}) },
|
||||
asResponse,
|
||||
});
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export function createUptimeESClient({
|
|||
request?: KibanaRequest;
|
||||
savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository;
|
||||
}) {
|
||||
const { _debug = false } = (request?.query as { _debug: boolean }) ?? {};
|
||||
const { _inspect = false } = (request?.query as { _inspect: boolean }) ?? {};
|
||||
|
||||
return {
|
||||
baseESClient: esClient,
|
||||
|
@ -72,7 +72,7 @@ export function createUptimeESClient({
|
|||
} catch (e) {
|
||||
esError = e;
|
||||
}
|
||||
if (_debug && request) {
|
||||
if (_inspect && request) {
|
||||
debugESCall({ startTime, request, esError, operationName: 'search', params: esParams });
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ export function createUptimeESClient({
|
|||
esError = e;
|
||||
}
|
||||
|
||||
if (_debug && request) {
|
||||
if (_inspect && request) {
|
||||
debugESCall({ startTime, request, esError, operationName: 'count', params: esParams });
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerL
|
|||
path: API_URLS.INDEX_STATUS,
|
||||
validate: {
|
||||
query: schema.object({
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient }): Promise<any> => {
|
||||
|
|
|
@ -21,7 +21,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
|||
statusFilter: schema.maybe(schema.string()),
|
||||
query: schema.maybe(schema.string()),
|
||||
pageSize: schema.number(),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
|
|
|
@ -18,7 +18,7 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe
|
|||
monitorId: schema.string(),
|
||||
dateStart: schema.string(),
|
||||
dateEnd: schema.string(),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
|
@ -18,7 +18,7 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib
|
|||
monitorId: schema.string(),
|
||||
dateStart: schema.string(),
|
||||
dateEnd: schema.string(),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
|
@ -18,7 +18,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ
|
|||
monitorId: schema.string(),
|
||||
dateStart: schema.maybe(schema.string()),
|
||||
dateEnd: schema.maybe(schema.string()),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, context, request }): Promise<any> => {
|
||||
|
|
|
@ -19,7 +19,7 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer
|
|||
monitorId: schema.string(),
|
||||
dateStart: schema.string(),
|
||||
dateEnd: schema.string(),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
|
@ -27,7 +27,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi
|
|||
schemes: arrayOrStringType,
|
||||
ports: arrayOrStringType,
|
||||
tags: arrayOrStringType,
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
|
||||
|
|
|
@ -21,7 +21,7 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe
|
|||
filters: schema.maybe(schema.string()),
|
||||
bucketSize: schema.maybe(schema.string()),
|
||||
query: schema.maybe(schema.string()),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
|
@ -23,7 +23,7 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =
|
|||
size: schema.maybe(schema.number()),
|
||||
sort: schema.maybe(schema.string()),
|
||||
status: schema.maybe(schema.string()),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
|
||||
|
|
|
@ -16,10 +16,10 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ
|
|||
params: schema.object({
|
||||
checkGroup: schema.string(),
|
||||
stepIndex: schema.number(),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
query: schema.object({
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request, response }) => {
|
||||
|
|
|
@ -22,7 +22,7 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =>
|
|||
syntheticEventTypes: schema.maybe(
|
||||
schema.oneOf([schema.arrayOf(schema.string()), schema.string()])
|
||||
),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
@ -55,7 +55,7 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer
|
|||
validate: {
|
||||
query: schema.object({
|
||||
checkGroups: schema.arrayOf(schema.string()),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
|
@ -19,7 +19,7 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs
|
|||
dateRangeEnd: schema.string(),
|
||||
filters: schema.maybe(schema.string()),
|
||||
query: schema.maybe(schema.string()),
|
||||
_debug: schema.maybe(schema.boolean()),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue