[APM] Add elasticsearch queries to api response (#95146)

This commit is contained in:
Søren Louv-Jansen 2021-03-29 07:50:27 +02:00 committed by GitHub
parent 5732526f34
commit 84adfe551b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 2080 additions and 1467 deletions

View file

@ -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.' },
},
};

View file

@ -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;

View file

@ -8032,6 +8032,12 @@
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:enableInspectEsQueries": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
}
}
},

View 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;
}

View file

@ -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']),
},
};

View file

@ -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')

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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 (

View file

@ -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}
/>

View file

@ -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}
/>

View file

@ -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}`}

View file

@ -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>

View file

@ -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 };
};

View file

@ -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}>

View file

@ -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] || [],
}));

View file

@ -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]);
}

View file

@ -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>

View file

@ -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();

View file

@ -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: {

View file

@ -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;

View file

@ -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}

View file

@ -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>
</>
);

View file

@ -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
);

View file

@ -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(),
});

View file

@ -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);

View file

@ -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),

View file

@ -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

View file

@ -97,7 +97,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
<EuiSpacer size="s" />
<ErrorGroupList
items={errorGroupListData}
items={errorGroupListData.errorGroups}
serviceName={serviceName}
/>
</EuiPanel>

View file

@ -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: (

View file

@ -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,

View file

@ -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) => {

View file

@ -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

View file

@ -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 } }}

View file

@ -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,

View file

@ -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);
}}

View file

@ -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>

View file

@ -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(),
});

View file

@ -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 && (

View file

@ -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();
});
});

View file

@ -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)
);
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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({

View file

@ -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)

View file

@ -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>
</>
);
}

View file

@ -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

View file

@ -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,
});
}

View file

@ -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 };
}

View file

@ -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 };
};

View file

@ -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({

View file

@ -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);
});
});
});

View file

@ -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',

View file

@ -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();
});
});

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -106,7 +106,7 @@ export async function getLatencyDistribution({
type Agg = NonNullable<typeof response.aggregations>;
if (!response.aggregations) {
return;
return {};
}
function formatDistribution(distribution: Agg['distribution']) {

View file

@ -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)}`;
};

View file

@ -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,
});
},
};

View file

@ -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,
});

View file

@ -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(),

View file

@ -51,7 +51,7 @@ function getMockRequest() {
) as APMConfig,
params: {
query: {
_debug: false,
_inspect: false,
},
},
core: {

View file

@ -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 },

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 };
},
});

View file

@ -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);
});
});
});

View file

@ -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)
);
}

View file

@ -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>;

View file

@ -30,10 +30,12 @@ export const environmentsRoute = createRoute({
setup
);
return getEnvironments({
const environments = await getEnvironments({
setup,
serviceName,
searchAggregatedTransactions,
});
return { environments };
},
});

View file

@ -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 };
},
});

View file

@ -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),
};
},
});

View file

@ -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 };
},
});

View file

@ -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 };
},
});
}

View file

@ -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 };
},
});

View file

@ -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 };
},
});

View file

@ -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 };
},
});

View file

@ -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 };
},
});

View file

@ -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 };
},
});

View file

@ -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 };
},
});

View file

@ -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 }

View file

@ -6,3 +6,4 @@
*/
export const enableAlertingExperience = 'observability:enableAlertingExperience';
export const enableInspectEsQueries = 'observability:enableInspectEsQueries';

View file

@ -19,6 +19,7 @@ export type {
ObservabilityPublicPluginsSetup,
ObservabilityPublicPluginsStart,
};
export { enableInspectEsQueries } from '../common/ui_settings_keys';
export const plugin: PluginInitializer<
ObservabilityPublicSetup,

View file

@ -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(),
},
};

View file

@ -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,
});

View file

@ -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 });
}

View file

@ -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> => {

View file

@ -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: {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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> => {

View file

@ -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 }) => {

View file

@ -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> => {

View file

@ -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