[APM] Diagnostics: show both doc count and event count (#160973)

Adds support for showing both doc count and event count for metrics, and
improve number formatting for large numbers

<img width="1482" alt="image"
src="18cec5ac-e65a-49c9-96c1-536843a68502">
This commit is contained in:
Søren Louv-Jansen 2023-07-04 09:04:27 +02:00 committed by GitHub
parent c1eacbabfe
commit be4a4d74d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 217 additions and 34 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { asPercent, asDecimalOrInteger } from './formatters';
import { asPercent, asDecimalOrInteger, asBigNumber } from './formatters';
describe('formatters', () => {
describe('asPercent', () => {
@ -73,3 +73,52 @@ describe('formatters', () => {
});
});
});
describe('asBigNumber', () => {
[
{
input: 0,
output: '0',
},
{
input: 999,
output: '999',
},
{
input: 999.999,
output: '1,000',
},
{
input: 449900,
output: '450k',
},
{
input: 450000,
output: '450k',
},
{
input: 450010,
output: '450k',
},
{
input: 2.4991e7,
output: '25m',
},
{
input: 9e9,
output: '9b',
},
{
input: 1e12,
output: '1t',
},
{
input: 1e15,
output: '1,000t',
},
].forEach(({ input, output }) => {
it(`${input} becomes ${output}`, () => {
expect(asBigNumber(input)).toBe(output);
});
});
});

View file

@ -62,3 +62,23 @@ export function asDecimalOrInteger(value: number, threshold = 10) {
}
return asDecimal(value);
}
export function asBigNumber(value: number): string {
if (value < 1e3) {
return asInteger(value);
}
if (value < 1e6) {
return `${asInteger(value / 1e3)}k`;
}
if (value < 1e9) {
return `${asInteger(value / 1e6)}m`;
}
if (value < 1e12) {
return `${asInteger(value / 1e9)}b`;
}
return `${asInteger(value / 1e12)}t`;
}

View file

@ -10,26 +10,20 @@ import {
EuiBasicTable,
EuiBasicTableColumn,
EuiSpacer,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import React, { useState, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { orderBy } from 'lodash';
import { useApmParams } from '../../../hooks/use_apm_params';
import { asInteger } from '../../../../common/utils/formatters';
import { asBigNumber, asInteger } from '../../../../common/utils/formatters';
import { APM_STATIC_DATA_VIEW_ID } from '../../../../common/data_view_constants';
import type { ApmEvent } from '../../../../server/routes/diagnostics/bundle/get_apm_events';
import { useDiagnosticsContext } from './context/use_diagnostics';
import { ApmPluginStartDeps } from '../../../plugin';
import { SearchBar } from '../../shared/search_bar/search_bar';
function formatDocCount(count?: number) {
if (count === undefined) {
return '-';
}
return asInteger(count);
}
export function DiagnosticsApmDocuments() {
const { diagnosticsBundle, isImported } = useDiagnosticsContext();
const { discover } = useKibana<ApmPluginStartDeps>().services;
@ -46,7 +40,9 @@ export function DiagnosticsApmDocuments() {
legacy === true &&
docCount === 0 &&
intervals &&
Object.values(intervals).every((interval) => interval === 0);
Object.values(intervals).every(
(interval) => interval.eventDocCount === 0
);
return !isLegacyAndUnused;
}) ?? []
@ -57,36 +53,40 @@ export function DiagnosticsApmDocuments() {
{
name: 'Name',
field: 'name',
width: '40%',
width: '30%',
},
{
name: 'Doc count',
field: 'docCount',
render: (_, { docCount }) => asInteger(docCount),
render: (_, { docCount }) => (
<EuiToolTip content={`${asInteger(docCount)} docs`}>
<div style={{ cursor: 'pointer' }}>{asBigNumber(docCount)}</div>
</EuiToolTip>
),
sortable: true,
},
{
name: '1m',
field: 'intervals.1m',
render: (_, { intervals }) => {
const docCount = intervals?.['1m'];
return formatDocCount(docCount);
const interval = intervals?.['1m'];
return <IntervalDocCount interval={interval} />;
},
},
{
name: '10m',
field: 'intervals.10m',
render: (_, { intervals }) => {
const docCount = intervals?.['10m'];
return formatDocCount(docCount);
const interval = intervals?.['10m'];
return <IntervalDocCount interval={interval} />;
},
},
{
name: '60m',
field: 'intervals.60m',
render: (_, { intervals }) => {
const docCount = intervals?.['60m'];
return formatDocCount(docCount);
const interval = intervals?.['60m'];
return <IntervalDocCount interval={interval} />;
},
},
{
@ -159,3 +159,33 @@ export function DiagnosticsApmDocuments() {
</>
);
}
function IntervalDocCount({
interval,
}: {
interval?: {
metricDocCount: number;
eventDocCount: number;
};
}) {
if (interval === undefined) {
return <>-</>;
}
return (
<EuiToolTip
content={`${asInteger(interval.metricDocCount)} docs / ${asInteger(
interval.eventDocCount
)} events`}
>
<div style={{ cursor: 'pointer' }}>
{asBigNumber(interval.metricDocCount)}&nbsp;
<EuiText
css={{ fontStyle: 'italic', fontSize: '80%', display: 'inline' }}
>
({asBigNumber(interval.eventDocCount)} events)
</EuiText>
</div>
</EuiToolTip>
);
}

View file

@ -13,6 +13,7 @@ import {
METRICSET_NAME,
METRICSET_INTERVAL,
TRANSACTION_DURATION_SUMMARY,
INDEX,
} from '../../../../common/es_fields/apm';
import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
import { getTypedSearch, TypedSearch } from '../create_typed_es_client';
@ -24,7 +25,7 @@ export interface ApmEvent {
kuery: string;
index: string[];
docCount: number;
intervals?: Record<string, number>;
intervals?: Record<string, { metricDocCount: number; eventDocCount: number }>;
}
export async function getApmEvents({
@ -91,15 +92,6 @@ export async function getApmEvents({
kuery
),
}),
getEventWithMetricsetInterval({
...commonProps,
name: 'Metric: Span breakdown',
index: getApmIndexPatterns([apmIndices.metric]),
kuery: mergeKueries(
`${PROCESSOR_EVENT}: "metric" AND ${METRICSET_NAME}: "span_breakdown"`,
kuery
),
}),
getEventWithMetricsetInterval({
...commonProps,
name: 'Metric: Service summary',
@ -109,6 +101,15 @@ export async function getApmEvents({
kuery
),
}),
getEvent({
...commonProps,
name: 'Metric: Span breakdown',
index: getApmIndexPatterns([apmIndices.metric]),
kuery: mergeKueries(
`${PROCESSOR_EVENT}: "metric" AND ${METRICSET_NAME}: "span_breakdown"`,
kuery
),
}),
getEvent({
...commonProps,
name: 'Event: Transaction',
@ -165,20 +166,33 @@ async function getEventWithMetricsetInterval({
size: 1000,
field: METRICSET_INTERVAL,
},
aggs: {
metric_doc_count: {
value_count: {
field: INDEX,
},
},
},
},
},
});
const defaultIntervals = { '1m': 0, '10m': 0, '60m': 0 };
const defaultIntervals = {
'1m': { metricDocCount: 0, eventDocCount: 0 },
'10m': { metricDocCount: 0, eventDocCount: 0 },
'60m': { metricDocCount: 0, eventDocCount: 0 },
};
const foundIntervals = res.aggregations?.metricset_intervals.buckets.reduce<
Record<string, number>
Record<string, { metricDocCount: number; eventDocCount: number }>
>((acc, item) => {
acc[item.key] = item.doc_count;
acc[item.key] = {
metricDocCount: item.metric_doc_count.value,
eventDocCount: item.doc_count,
};
return acc;
}, {});
const intervals = merge(defaultIntervals, foundIntervals);
return {
legacy,
name,

View file

@ -7,6 +7,8 @@
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { sumBy } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
@ -88,15 +90,83 @@ export default function ApiTest({ getService }: FtrProviderContext) {
'processor.event: "metric" AND metricset.name: "transaction" AND transaction.duration.summary :* ',
docCount: 21,
},
{ kuery: 'processor.event: "metric" AND metricset.name: "span_breakdown"', docCount: 15 },
{
kuery: 'processor.event: "metric" AND metricset.name: "service_summary"',
docCount: 21,
},
{ kuery: 'processor.event: "metric" AND metricset.name: "span_breakdown"', docCount: 15 },
{ kuery: 'processor.event: "transaction"', docCount: 450 },
]);
});
describe('transactions', async () => {
let body: APIReturnType<'GET /internal/apm/diagnostics'>;
const expectedDocCount = 450;
beforeEach(async () => {
const res = await apmApiClient.adminUser({
endpoint: 'GET /internal/apm/diagnostics',
params: {
query: { start: new Date(start).toISOString(), end: new Date(end).toISOString() },
},
});
body = res.body;
});
it('raw transaction events', () => {
const rawTransactions = body.apmEvents.find(({ kuery }) =>
kuery.includes('processor.event: "transaction"')
);
expect(rawTransactions?.docCount).to.be(expectedDocCount);
});
it('transaction metrics', () => {
const transactionMetrics = body.apmEvents.find(({ kuery }) =>
kuery.includes('metricset.name: "transaction"')
);
const intervalDocCount = sumBy(
Object.values(transactionMetrics?.intervals ?? {}),
({ metricDocCount }) => metricDocCount
);
expect(transactionMetrics?.docCount).to.be(intervalDocCount);
expect(transactionMetrics?.docCount).to.be(21);
expect(transactionMetrics?.intervals).to.eql({
'1m': { metricDocCount: 15, eventDocCount: expectedDocCount },
'10m': { metricDocCount: 4, eventDocCount: expectedDocCount },
'60m': { metricDocCount: 2, eventDocCount: expectedDocCount },
});
});
it('service transactions', () => {
const serviceTransactionMetrics = body.apmEvents.find(({ kuery }) =>
kuery.includes('metricset.name: "service_transaction"')
);
const intervalDocCount = sumBy(
Object.values(serviceTransactionMetrics?.intervals ?? {}),
({ metricDocCount }) => metricDocCount
);
expect(serviceTransactionMetrics?.docCount).to.be(intervalDocCount);
expect(serviceTransactionMetrics?.docCount).to.be(21);
expect(serviceTransactionMetrics?.kuery).to.be(
'processor.event: "metric" AND metricset.name: "service_transaction" AND transaction.duration.summary :* '
);
expect(serviceTransactionMetrics?.intervals).to.eql({
'1m': { metricDocCount: 15, eventDocCount: expectedDocCount },
'10m': { metricDocCount: 4, eventDocCount: expectedDocCount },
'60m': { metricDocCount: 2, eventDocCount: expectedDocCount },
});
});
});
it('returns zero doc_counts when filtering by a non-existing service', async () => {
const { body } = await apmApiClient.adminUser({
endpoint: 'GET /internal/apm/diagnostics',