mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
c1eacbabfe
commit
be4a4d74d3
5 changed files with 217 additions and 34 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
<EuiText
|
||||
css={{ fontStyle: 'italic', fontSize: '80%', display: 'inline' }}
|
||||
>
|
||||
({asBigNumber(interval.eventDocCount)} events)
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue