Skip to content

Commit

Permalink
[APM] Add sparklines to the multi-signal view table (elastic#187782)
Browse files Browse the repository at this point in the history
Closes elastic#187567

## Summary

This PR adds sparklines to the multi-signal view table


![image](https://github.com/elastic/kibana/assets/14139027/d29aa76f-1ec1-4720-bf85-84e818971bc0)

## Testing 

1. Enable `observability:apmEnableMultiSignal` in advanced settings
 
<details>


<summary>2. Run the entities definition in the dev tools</summary>


```
POST kbn:/internal/api/entities/definition
{
  "id": "apm-services-with-metadata",
  "name": "Services from logs and metrics",
  "displayNameTemplate": "test",
  "history": {
    "timestampField": "@timestamp",
    "interval": "5m"
  },
  "type": "service",
  "indexPatterns": [
    "logs-*",
    "metrics-*"
  ],
  "timestampField": "@timestamp",
  "lookback": "5m",
  "identityFields": [
    {
      "field": "service.name",
      "optional": false
    },
    {
      "field": "service.environment",
      "optional": true
    }
  ],
  "identityTemplate": "{{service.name}}:{{service.environment}}",
  "metadata": [
    "tags",
    "host.name",
    "data_stream.type",
    "service.name", 
    "service.instance.id",
    "service.namespace",
    "service.environment",
    "service.version",
    "service.runtime.name",
    "service.runtime.version",
    "service.node.name",
    "service.language.name",
    "agent.name",
    "cloud.provider",
    "cloud.instance.id",
    "cloud.availability_zone",
    "cloud.instance.name",
    "cloud.machine.type",
    "container.id"
  ],
  "metrics": [
    {
      "name": "latency",
      "equation": "A",
      "metrics": [
        {
          "name": "A",
          "aggregation": "avg",
          "field": "transaction.duration.histogram"
           
          
        }
      ]
    },
    {
      "name": "throughput",
      "equation": "A / 5",
      "metrics": [
        {
          "name": "A",
          "aggregation": "doc_count",
          "filter": "transaction.duration.histogram:*"
        }
      ]
    },
    {
      "name": "failedTransactionRate",
      "equation": "A / B",
      "metrics": [
        {
          "name": "A",
          "aggregation": "doc_count",
          "filter": "event.outcome: \"failure\""
        },
        {
          "name": "B",
          "aggregation": "doc_count",
          "filter": "event.outcome: *"
        }
      ]
    },
    {
      "name": "logErrorRate",
      "equation": "A / B",
      "metrics": [
        {
          "name": "A",
          "aggregation": "doc_count",
          "filter": "log.level: \"error\""
        },
        {
          "name": "B",
          "aggregation": "doc_count",
          "filter": "log.level: *"
        }
      ]
    },
     {
      "name": "logRatePerMinute",
      "equation": "A / 5",
      "metrics": [
        {
          "name": "A",
          "aggregation": "doc_count",
          "filter": "log.level: \"error\""
        }
      ]
    }
  ]
}
```

</details>

3. Generate data with synthrace

    1. logs only: `node scripts/synthtrace simple_logs.ts`
    2. APM only: `node scripts/synthtrace simple_trace.ts` 

4. Open services inventory
- the sparklines should be visible next to the values in the table (big
screen only like in the services table)
   
<img width="1920" alt="image"
src="https://github.com/elastic/kibana/assets/14139027/698de74c-0d54-4f70-9802-5b80bfc74511">

   - on small screens, the sparklines should not be visible
      
<img width="989" alt="image"
src="https://github.com/elastic/kibana/assets/14139027/4bef372d-7b1c-4e50-a3e2-d11ec0df5bc1">
  • Loading branch information
jennypavlova authored Jul 11, 2024
1 parent 57be31c commit 35b5fcc
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ApmDocumentType } from '../../../../../common/document_type';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { EmptyMessage } from '../../../shared/empty_message';
import { SearchBar } from '../../../shared/search_bar/search_bar';
Expand All @@ -22,6 +23,9 @@ import {
MultiSignalServicesTable,
ServiceInventoryFieldName,
} from './table/multi_signal_services_table';
import { ServiceListItem } from '../../../../../common/service_inventory';
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';

type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;

Expand Down Expand Up @@ -74,9 +78,72 @@ function useServicesEntitiesMainStatisticsFetcher() {
return { mainStatisticsData: data, mainStatisticsStatus: status };
}

export const MultiSignalInventory = () => {
function useServicesEntitiesDetailedStatisticsFetcher({
mainStatisticsFetch,
services,
}: {
mainStatisticsFetch: ReturnType<typeof useServicesEntitiesMainStatisticsFetcher>;
services: ServiceListItem[];
}) {
const {
query: { rangeFrom, rangeTo, environment, kuery },
} = useApmParams('/services');

const { start, end } = useTimeRange({ rangeFrom, rangeTo });

const dataSourceOptions = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.ServiceTransactionMetric,
numBuckets: 20,
});

const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;

const timeseriesDataFetch = useProgressiveFetcher(
(callApmApi) => {
const serviceNames = services.map(({ serviceName }) => serviceName);

if (
start &&
end &&
serviceNames.length > 0 &&
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
dataSourceOptions
) {
return callApmApi('POST /internal/apm/entities/services/detailed_statistics', {
params: {
query: {
environment,
kuery,
start,
end,
documentType: dataSourceOptions.source.documentType,
rollupInterval: dataSourceOptions.source.rollupInterval,
bucketSizeInSeconds: dataSourceOptions.bucketSizeInSeconds,
},
body: {
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
serviceNames: JSON.stringify(serviceNames.sort()),
},
},
});
}
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatisticsData.requestId, services],
{ preservePreviousData: false }
);

return { timeseriesDataFetch };
}

export function MultiSignalInventory() {
const [searchQuery, setSearchQuery] = React.useState('');
const { mainStatisticsData, mainStatisticsStatus } = useServicesEntitiesMainStatisticsFetcher();
const mainStatisticsFetch = useServicesEntitiesMainStatisticsFetcher();

const initialSortField = ServiceInventoryFieldName.Throughput;

Expand All @@ -86,6 +153,11 @@ export const MultiSignalInventory = () => {
fieldsToSearch: [ServiceInventoryFieldName.ServiceName],
});

const { timeseriesDataFetch } = useServicesEntitiesDetailedStatisticsFetcher({
mainStatisticsFetch,
services: mainStatisticsData.services,
});

return (
<>
<EuiFlexGroup gutterSize="m">
Expand All @@ -110,6 +182,8 @@ export const MultiSignalInventory = () => {
initialSortField={initialSortField}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortDirection={INITIAL_SORT_DIRECTION}
timeseriesData={timeseriesDataFetch?.data}
timeseriesDataLoading={timeseriesDataFetch.status === FETCH_STATUS.LOADING}
noItemsMessage={
<EmptyMessage
heading={i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
Expand All @@ -122,4 +196,4 @@ export const MultiSignalInventory = () => {
</EuiFlexGroup>
</>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,26 @@ import { TruncateWithTooltip } from '../../../../shared/truncate_with_tooltip';
import { ServiceInventoryFieldName } from './multi_signal_services_table';
import { EntityServiceListItem, SignalTypes } from '../../../../../../common/entities/types';
import { isApmSignal } from '../../../../../utils/get_signal_type';
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';

type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;

export function getServiceColumns({
query,
breakpoints,
link,
timeseriesDataLoading,
timeseriesData,
}: {
query: TypeOf<ApmRoutes, '/services'>['query'];
breakpoints: Breakpoints;
link: any;
timeseriesDataLoading: boolean;
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
}): Array<ITableColumn<EntityServiceListItem>> {
const { isSmall, isLarge } = breakpoints;
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
return [
{
field: ServiceInventoryFieldName.ServiceName,
Expand Down Expand Up @@ -90,17 +101,18 @@ export function getServiceColumns({
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, signalTypes }) => {
render: (_, { metrics, serviceName, signalTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LATENCY_AVG);

return !isApmSignal(signalTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
isLoading={false}
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.apm[serviceName]?.latency}
color={currentPeriodColor}
hideSeries
valueLabel={asMillisecondDuration(metrics.latency)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
Expand All @@ -113,17 +125,18 @@ export function getServiceColumns({
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, signalTypes }) => {
render: (_, { metrics, serviceName, signalTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.THROUGHPUT);

return !isApmSignal(signalTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
isLoading={false}
color={currentPeriodColor}
hideSeries
valueLabel={asTransactionRate(metrics.throughput)}
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.apm[serviceName]?.throughput}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
Expand All @@ -136,17 +149,18 @@ export function getServiceColumns({
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, signalTypes }) => {
render: (_, { metrics, serviceName, signalTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);

return !isApmSignal(signalTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
isLoading={false}
color={currentPeriodColor}
hideSeries
valueLabel={asPercent(metrics.failedTransactionRate, 1)}
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.apm[serviceName]?.transactionErrorRate}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
Expand All @@ -159,15 +173,16 @@ export function getServiceColumns({
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics }) => {
render: (_, { metrics, serviceName }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_RATE);

return (
<ListMetric
isLoading={false}
color={currentPeriodColor}
hideSeries
series={timeseriesData?.currentPeriod?.logRate[serviceName] ?? []}
valueLabel={asDecimalOrInteger(metrics.logRatePerMinute)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
Expand All @@ -180,15 +195,16 @@ export function getServiceColumns({
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics }) => {
render: (_, { metrics, serviceName }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_ERROR_RATE);

return (
<ListMetric
isLoading={false}
color={currentPeriodColor}
hideSeries
series={timeseriesData?.currentPeriod?.logErrorRate[serviceName] ?? []}
valueLabel={asPercent(metrics.logErrorRate, 1)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { ManagedTable } from '../../../../shared/managed_table';
import { getServiceColumns } from './get_service_columns';

type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;
type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;

export enum ServiceInventoryFieldName {
ServiceName = 'serviceName',
Expand All @@ -35,6 +37,8 @@ interface Props {
initialSortDirection: 'asc' | 'desc';
noItemsMessage: React.ReactNode;
data: MainStatisticsApiResponse['services'];
timeseriesDataLoading: boolean;
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
}

export function MultiSignalServicesTable({
Expand All @@ -44,6 +48,8 @@ export function MultiSignalServicesTable({
initialPageSize,
initialSortDirection,
noItemsMessage,
timeseriesDataLoading,
timeseriesData,
}: Props) {
const breakpoints = useBreakpoints();
const { query } = useApmParams('/services');
Expand All @@ -55,8 +61,10 @@ export function MultiSignalServicesTable({
query: omit(query, 'page', 'pageSize', 'sortDirection', 'sortField'),
breakpoints,
link,
timeseriesDataLoading,
timeseriesData,
});
}, [query, link, breakpoints]);
}, [query, breakpoints, link, timeseriesDataLoading, timeseriesData]);

return (
<EuiFlexGroup gutterSize="xs" direction="column" responsive={false}>
Expand Down
Loading

0 comments on commit 35b5fcc

Please sign in to comment.