mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Logs only service details view (#187221)
closes https://github.com/elastic/kibana/issues/183013
- Introduce`logs-services` route for the logs only entities with the
tabs
- overview page
- Logs
- Dashboard
- Log error rate and Log rate charts
- Add 2 services in the logs access plugin to fetch the timeseries for
the above charts
6969b373
-6710-44ab-8a2c-3e6c0e365004
### How to test
2. Enable `observability:apmEnableMultiSignal` in advansted settings
<details>
<summary>3. 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>
4. Generate data with synthrace
1. logs only: `node scripts/synthtrace simple_logs.ts`
2. APM only: `node scripts/synthtrace simple_trace.ts`
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: jennypavlova <jennypavlova94@gmail.com>
Co-authored-by: jennypavlova <dzheni.pavlova@elastic.co>
This commit is contained in:
parent
c4837014c5
commit
372f99b213
35 changed files with 1622 additions and 36 deletions
|
@ -14,6 +14,7 @@ export type LogDocument = Fields &
|
|||
'input.type': string;
|
||||
'log.file.path'?: string;
|
||||
'service.name'?: string;
|
||||
'service.environment'?: string;
|
||||
'data_stream.namespace': string;
|
||||
'data_stream.type': string;
|
||||
'data_stream.dataset': string;
|
||||
|
|
|
@ -10,5 +10,12 @@ import noResultsIllustrationDark from './src/assets/no_results_dark.svg';
|
|||
import noResultsIllustrationLight from './src/assets/no_results_light.svg';
|
||||
import dashboardsLight from './src/assets/dashboards_light.svg';
|
||||
import dashboardsDark from './src/assets/dashboards_dark.svg';
|
||||
import apmLight from './src/assets/oblt_apm_light.svg';
|
||||
|
||||
export { noResultsIllustrationDark, noResultsIllustrationLight, dashboardsLight, dashboardsDark };
|
||||
export {
|
||||
noResultsIllustrationDark,
|
||||
noResultsIllustrationLight,
|
||||
dashboardsLight,
|
||||
dashboardsDark,
|
||||
apmLight,
|
||||
};
|
||||
|
|
14
packages/kbn-shared-svg/src/assets/oblt_apm_light.svg
Normal file
14
packages/kbn-shared-svg/src/assets/oblt_apm_light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 60 KiB |
|
@ -9,6 +9,7 @@ import { AgentName } from '../../typings/es_schemas/ui/fields/agent';
|
|||
|
||||
export enum SignalTypes {
|
||||
METRICS = 'metrics',
|
||||
TRACES = 'traces',
|
||||
LOGS = 'logs',
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { asPercent as obltAsPercent } from '@kbn/observability-plugin/common';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { Maybe } from '../../../typings/common';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../i18n';
|
||||
|
@ -82,3 +83,7 @@ export function asBigNumber(value: number): string {
|
|||
|
||||
return `${asInteger(value / 1e12)}t`;
|
||||
}
|
||||
|
||||
export const yLabelAsPercent = (y?: number | null) => {
|
||||
return obltAsPercent(y || 0, 1);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel, EuiTitle, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { getTimeSeriesColor, ChartType } from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context';
|
||||
import { yLabelAsPercent } from '../../../../../common/utils/formatters';
|
||||
|
||||
type LogErrorRateReturnType =
|
||||
APIReturnType<'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries'>;
|
||||
|
||||
const INITIAL_STATE: LogErrorRateReturnType = {
|
||||
currentPeriod: {},
|
||||
};
|
||||
|
||||
export function LogErrorRateChart({ height }: { height: number }) {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
path: { serviceName },
|
||||
} = useApmParams('/logs-services/{serviceName}');
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[environment, kuery, serviceName, start, end]
|
||||
);
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_ERROR_RATE);
|
||||
|
||||
const timeseries = [
|
||||
{
|
||||
data: data?.currentPeriod?.[serviceName] ?? [],
|
||||
type: 'linemark',
|
||||
color: currentPeriodColor,
|
||||
title: i18n.translate('xpack.apm.logs.chart.logsErrorRate', {
|
||||
defaultMessage: 'Log Error Rate',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.logErrorRate', {
|
||||
defaultMessage: 'Log error rate',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<TimeseriesChartWithContext
|
||||
id="logErrorRate"
|
||||
height={height}
|
||||
showAnnotations={false}
|
||||
fetchStatus={status}
|
||||
timeseries={timeseries}
|
||||
yLabelFormat={yLabelAsPercent}
|
||||
yDomain={{ min: 0, max: 1 }}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel, EuiTitle, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { getTimeSeriesColor, ChartType } from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context';
|
||||
import { yLabelAsPercent } from '../../../../../common/utils/formatters';
|
||||
|
||||
type LogRateReturnType =
|
||||
APIReturnType<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>;
|
||||
|
||||
const INITIAL_STATE: LogRateReturnType = {
|
||||
currentPeriod: {},
|
||||
};
|
||||
|
||||
export function LogRateChart({ height }: { height: number }) {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
path: { serviceName },
|
||||
} = useApmParams('/logs-services/{serviceName}');
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[environment, kuery, serviceName, start, end]
|
||||
);
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_RATE);
|
||||
|
||||
const timeseries = [
|
||||
{
|
||||
data: data?.currentPeriod?.[serviceName] ?? [],
|
||||
type: 'linemark',
|
||||
color: currentPeriodColor,
|
||||
title: i18n.translate('xpack.apm.logs.chart.logRate', {
|
||||
defaultMessage: 'Log Rate',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.logRate', {
|
||||
defaultMessage: 'Log rate',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<TimeseriesChartWithContext
|
||||
id="logRate"
|
||||
height={height}
|
||||
showAnnotations={false}
|
||||
fetchStatus={status}
|
||||
timeseries={timeseries}
|
||||
yLabelFormat={yLabelAsPercent}
|
||||
yDomain={{ min: 0, max: 1 }}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { apmLight } from '@kbn/shared-svg';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function AddAPMCallOut() {
|
||||
const { core } = useApmPluginContext();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" hasShadow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiImage
|
||||
css={{
|
||||
background: euiTheme.colors.emptyShade,
|
||||
}}
|
||||
width="160"
|
||||
height="100"
|
||||
size="m"
|
||||
src={apmLight}
|
||||
alt="apm-logo"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiTitle size="xs">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.addAPMCallOut.title"
|
||||
defaultMessage="Detect and resolve issues faster with deep visibility into your application"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.addAPMCallOut.description"
|
||||
defaultMessage="Understanding your application performance, relationships and dependencies by
|
||||
instrumenting with APM."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<EuiButton
|
||||
data-test-subj="apmAddApmCallOutButton"
|
||||
href={core.http.basePath.prepend('/app/apm/tutorial')}
|
||||
>
|
||||
{i18n.translate('xpack.apm.logsServiceOverview.callout.addApm', {
|
||||
defaultMessage: 'Add APM',
|
||||
})}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmAddApmCallOutLearnMoreButton"
|
||||
iconType="popout"
|
||||
iconSide="right"
|
||||
href="https://www.elastic.co/observability/application-performance-monitoring"
|
||||
>
|
||||
{i18n.translate('xpack.apm.addAPMCallOut.linkToElasticcoButtonEmptyLabel', {
|
||||
defaultMessage: 'Learn more',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroupProps, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { useBreakpoints } from '../../../../hooks/use_breakpoints';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { AddAPMCallOut } from './add_apm_callout';
|
||||
import { LogRateChart } from '../charts/log_rate_chart';
|
||||
import { LogErrorRateChart } from '../charts/log_error_rate_chart';
|
||||
/**
|
||||
* The height a chart should be if it's next to a table with 5 rows and a title.
|
||||
* Add the height of the pagination row.
|
||||
*/
|
||||
|
||||
const chartHeight = 400;
|
||||
|
||||
export function LogsServiceOverview() {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
const {
|
||||
query: { environment, rangeFrom, rangeTo },
|
||||
} = useApmParams('/logs-services/{serviceName}/overview');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { isLarge } = useBreakpoints();
|
||||
const isSingleColumn = isLarge;
|
||||
|
||||
const rowDirection: EuiFlexGroupProps['direction'] = isSingleColumn ? 'column' : 'row';
|
||||
|
||||
return (
|
||||
<AnnotationsContextProvider
|
||||
serviceName={serviceName}
|
||||
environment={environment}
|
||||
start={start}
|
||||
end={end}
|
||||
>
|
||||
<ChartPointerEventContextProvider>
|
||||
<AddAPMCallOut />
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction={rowDirection} gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={4}>
|
||||
<LogRateChart height={chartHeight} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<LogErrorRateChart height={chartHeight} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</ChartPointerEventContextProvider>
|
||||
</AnnotationsContextProvider>
|
||||
);
|
||||
}
|
|
@ -45,13 +45,14 @@ export interface MergedServiceDashboard extends SavedApmCustomDashboard {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export function ServiceDashboards() {
|
||||
export function ServiceDashboards({ checkForEntities = false }: { checkForEntities?: boolean }) {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery, rangeFrom, rangeTo, dashboardId },
|
||||
} = useAnyOfApmParams(
|
||||
'/services/{serviceName}/dashboards',
|
||||
'/mobile-services/{serviceName}/dashboards'
|
||||
'/mobile-services/{serviceName}/dashboards',
|
||||
'/logs-services/{serviceName}/dashboards'
|
||||
);
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const [serviceDashboards, setServiceDashboards] = useState<MergedServiceDashboard[]>([]);
|
||||
|
@ -68,12 +69,12 @@ export function ServiceDashboards() {
|
|||
isCachable: false,
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: { start, end },
|
||||
query: { start, end, checkFor: checkForEntities ? 'entities' : 'services' },
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end]
|
||||
[serviceName, start, end, checkForEntities]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -30,6 +30,7 @@ import { NotAvailableApmMetrics } from '../../../../shared/not_available_apm_met
|
|||
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';
|
||||
export function getServiceColumns({
|
||||
query,
|
||||
breakpoints,
|
||||
|
@ -46,14 +47,19 @@ export function getServiceColumns({
|
|||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (_, { serviceName, agentName }) => (
|
||||
render: (_, { serviceName, agentName, signalTypes }) => (
|
||||
<TruncateWithTooltip
|
||||
data-test-subj="apmServiceListAppLink"
|
||||
text={serviceName}
|
||||
content={
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceLink serviceName={serviceName} agentName={agentName} query={query} />
|
||||
<ServiceLink
|
||||
signalTypes={signalTypes}
|
||||
serviceName={serviceName}
|
||||
agentName={agentName}
|
||||
query={query}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
|
@ -87,7 +93,7 @@ export function getServiceColumns({
|
|||
render: (_, { metrics, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LATENCY_AVG);
|
||||
|
||||
return !signalTypes.includes(SignalTypes.METRICS) ? (
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
|
@ -110,7 +116,7 @@ export function getServiceColumns({
|
|||
render: (_, { metrics, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.THROUGHPUT);
|
||||
|
||||
return !signalTypes.includes(SignalTypes.METRICS) ? (
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
|
@ -133,7 +139,7 @@ export function getServiceColumns({
|
|||
render: (_, { metrics, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);
|
||||
|
||||
return !signalTypes.includes(SignalTypes.METRICS) ? (
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
|
|
|
@ -14,7 +14,7 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi
|
|||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
|
||||
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
|
||||
export function ServiceLogs() {
|
||||
|
@ -22,7 +22,7 @@ export function ServiceLogs() {
|
|||
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}/logs');
|
||||
} = useAnyOfApmParams('/services/{serviceName}/logs', '/logs-services/{serviceName}/logs');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { TransactionLink } from '../app/transaction_link';
|
|||
import { homeRoute } from './home';
|
||||
import { serviceDetailRoute } from './service_detail';
|
||||
import { mobileServiceDetailRoute } from './mobile_service_detail';
|
||||
import { logsServiceDetailsRoute } from './entities/logs_service_details';
|
||||
import { settingsRoute } from './settings';
|
||||
import { onboarding } from './onboarding';
|
||||
import { tutorialRedirectRoute } from './onboarding/redirect';
|
||||
|
@ -130,6 +131,7 @@ const apmRoutes = {
|
|||
...settingsRoute,
|
||||
...serviceDetailRoute,
|
||||
...mobileServiceDetailRoute,
|
||||
...logsServiceDetailsRoute,
|
||||
...homeRoute,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toBooleanRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { LogsServiceTemplate } from '../../templates/entities/logs_service_template';
|
||||
import { offsetRt } from '../../../../../common/comparison_rt';
|
||||
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
|
||||
import { environmentRt } from '../../../../../common/environment_rt';
|
||||
import { ApmTimeRangeMetadataContextProvider } from '../../../../context/time_range_metadata/time_range_metadata_context';
|
||||
import { ServiceDashboards } from '../../../app/service_dashboards';
|
||||
import { ServiceLogs } from '../../../app/service_logs';
|
||||
import { LogsServiceOverview } from '../../../app/entities/logs/logs_service_overview';
|
||||
import { RedirectToDefaultLogsServiceRouteView } from '../../service_detail/redirect_to_default_service_route_view';
|
||||
|
||||
export function page({
|
||||
title,
|
||||
tabKey,
|
||||
element,
|
||||
searchBarOptions,
|
||||
}: {
|
||||
title: string;
|
||||
tabKey: React.ComponentProps<typeof LogsServiceTemplate>['selectedTabKey'];
|
||||
element: React.ReactElement<any, any>;
|
||||
searchBarOptions?: {
|
||||
showUnifiedSearchBar?: boolean;
|
||||
showTransactionTypeSelector?: boolean;
|
||||
showTimeComparison?: boolean;
|
||||
showMobileFilters?: boolean;
|
||||
hidden?: boolean;
|
||||
};
|
||||
}): {
|
||||
element: React.ReactElement<any, any>;
|
||||
} {
|
||||
return {
|
||||
element: (
|
||||
<LogsServiceTemplate
|
||||
title={title}
|
||||
selectedTabKey={tabKey}
|
||||
searchBarOptions={searchBarOptions}
|
||||
>
|
||||
{element}
|
||||
</LogsServiceTemplate>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const logsServiceDetailsRoute = {
|
||||
'/logs-services/{serviceName}': {
|
||||
element: (
|
||||
<ApmTimeRangeMetadataContextProvider>
|
||||
<Outlet />
|
||||
</ApmTimeRangeMetadataContextProvider>
|
||||
),
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
t.type({
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
t.type({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
kuery: t.string,
|
||||
serviceGroup: t.string,
|
||||
comparisonEnabled: toBooleanRt,
|
||||
}),
|
||||
t.partial({
|
||||
transactionType: t.string,
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
}),
|
||||
offsetRt,
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
defaults: {
|
||||
query: {
|
||||
kuery: '',
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
serviceGroup: '',
|
||||
},
|
||||
},
|
||||
children: {
|
||||
'/logs-services/{serviceName}/overview': {
|
||||
...page({
|
||||
element: <LogsServiceOverview />,
|
||||
tabKey: 'overview',
|
||||
title: i18n.translate('xpack.apm.views.overview.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
searchBarOptions: {
|
||||
showUnifiedSearchBar: true,
|
||||
},
|
||||
}),
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
page: toNumberRt,
|
||||
pageSize: toNumberRt,
|
||||
sortField: t.string,
|
||||
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/logs-services/{serviceName}/logs': {
|
||||
...page({
|
||||
tabKey: 'logs',
|
||||
title: i18n.translate('xpack.apm.views.logs.title', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
element: <ServiceLogs />,
|
||||
searchBarOptions: {
|
||||
showUnifiedSearchBar: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
'/logs-services/{serviceName}/dashboards': {
|
||||
...page({
|
||||
tabKey: 'dashboards',
|
||||
title: i18n.translate('xpack.apm.views.dashboard.title', {
|
||||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
element: <ServiceDashboards checkForEntities />,
|
||||
searchBarOptions: {
|
||||
showUnifiedSearchBar: false,
|
||||
},
|
||||
}),
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
dashboardId: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/logs-services/{serviceName}/': {
|
||||
element: <RedirectToDefaultLogsServiceRouteView />,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -19,3 +19,14 @@ export function RedirectToDefaultServiceRouteView() {
|
|||
|
||||
return <Redirect to={{ pathname: `/services/${serviceName}/overview`, search }} />;
|
||||
}
|
||||
|
||||
export function RedirectToDefaultLogsServiceRouteView() {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
} = useApmParams('/logs-services/{serviceName}/*');
|
||||
|
||||
const search = qs.stringify(query);
|
||||
|
||||
return <Redirect to={{ pathname: `/logs-services/${serviceName}/overview`, search }} />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderProps, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import { ApmServiceContextProvider } from '../../../../../context/apm_service/apm_service_context';
|
||||
import { useBreadcrumb } from '../../../../../context/breadcrumbs/use_breadcrumb';
|
||||
import { ServiceAnomalyTimeseriesContextProvider } from '../../../../../context/service_anomaly_timeseries/service_anomaly_timeseries_context';
|
||||
import { useApmParams } from '../../../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../../../hooks/use_apm_router';
|
||||
import { useTimeRange } from '../../../../../hooks/use_time_range';
|
||||
import { MobileSearchBar } from '../../../../app/mobile/search_bar';
|
||||
import { SearchBar } from '../../../../shared/search_bar/search_bar';
|
||||
import { ServiceIcons } from '../../../../shared/service_icons';
|
||||
import { TechnicalPreviewBadge } from '../../../../shared/technical_preview_badge';
|
||||
import { ApmMainTemplate } from '../../apm_main_template';
|
||||
|
||||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||
key: 'overview' | 'logs' | 'dashboards';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
children: React.ReactChild;
|
||||
selectedTabKey: Tab['key'];
|
||||
searchBarOptions?: React.ComponentProps<typeof MobileSearchBar>;
|
||||
}
|
||||
|
||||
export function LogsServiceTemplate(props: Props) {
|
||||
return (
|
||||
<ApmServiceContextProvider>
|
||||
<TemplateWithContext {...props} />
|
||||
</ApmServiceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplateWithContext({ title, children, selectedTabKey, searchBarOptions }: Props) {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
query: { rangeFrom, rangeTo, environment },
|
||||
} = useApmParams('/logs-services/{serviceName}/*');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const router = useApmRouter();
|
||||
|
||||
const tabs = useTabs({ selectedTabKey });
|
||||
const selectedTab = tabs?.find(({ isSelected }) => isSelected);
|
||||
|
||||
const servicesLink = router.link('/services', {
|
||||
query: { ...query },
|
||||
});
|
||||
|
||||
useBreadcrumb(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate('xpack.apm.logServices.breadcrumb.title', {
|
||||
defaultMessage: 'Services',
|
||||
}),
|
||||
href: servicesLink,
|
||||
},
|
||||
...(selectedTab
|
||||
? [
|
||||
{
|
||||
title: serviceName,
|
||||
href: router.link('/logs-services/{serviceName}', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: selectedTab.label,
|
||||
href: selectedTab.href,
|
||||
} as { title: string; href: string },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[query, router, selectedTab, serviceName, servicesLink]
|
||||
);
|
||||
|
||||
return (
|
||||
<ApmMainTemplate
|
||||
pageHeader={{
|
||||
tabs,
|
||||
pageTitle: (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="apmMainTemplateHeaderServiceName">
|
||||
{serviceName} <TechnicalPreviewBadge icon="beaker" />
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIcons
|
||||
serviceName={serviceName}
|
||||
environment={environment}
|
||||
start={start}
|
||||
end={end}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<SearchBar {...searchBarOptions} />
|
||||
<ServiceAnomalyTimeseriesContextProvider>{children}</ServiceAnomalyTimeseriesContextProvider>
|
||||
</ApmMainTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function useTabs({ selectedTabKey }: { selectedTabKey: Tab['key'] }) {
|
||||
const router = useApmRouter();
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: queryFromUrl,
|
||||
} = useApmParams(`/logs-services/{serviceName}/${selectedTabKey}` as const);
|
||||
|
||||
const query = omit(queryFromUrl, 'page', 'pageSize', 'sortField', 'sortDirection');
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
key: 'overview',
|
||||
href: router.link('/logs-services/{serviceName}/overview', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.logsServiceDetails.overviewTabLabel', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
href: router.link('/logs-services/{serviceName}/logs', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.logsServiceDetails.logsTabLabel', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'dashboards',
|
||||
href: router.link('/logs-services/{serviceName}/dashboards', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
append: <TechnicalPreviewBadge icon="beaker" />,
|
||||
label: i18n.translate('xpack.apm.logsServiceDetails.dashboardsTabLabel', {
|
||||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return tabs
|
||||
.filter((t) => !t.hidden)
|
||||
.map(({ href, key, label, append }) => ({
|
||||
href,
|
||||
label,
|
||||
append,
|
||||
isSelected: key === selectedTabKey,
|
||||
'data-test-subj': `${key}Tab`,
|
||||
}));
|
||||
}
|
|
@ -25,6 +25,7 @@ export function isRouteWithTimeRange({
|
|||
route.path === '/dependencies/inventory' ||
|
||||
route.path === '/services/{serviceName}' ||
|
||||
route.path === '/mobile-services/{serviceName}' ||
|
||||
route.path === '/logs-services/{serviceName}' ||
|
||||
route.path === '/service-groups' ||
|
||||
route.path === '/storage-explorer' ||
|
||||
location.pathname === '/' ||
|
||||
|
|
|
@ -12,9 +12,11 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
|||
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
import { isMobileAgentName } from '../../../../../../common/agent_name';
|
||||
import { SignalTypes } from '../../../../../../common/entities/types';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n';
|
||||
import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
import { useApmRouter } from '../../../../../hooks/use_apm_router';
|
||||
import { isLogsSignal } from '../../../../../utils/get_signal_type';
|
||||
import { truncate, unit } from '../../../../../utils/style';
|
||||
import { ApmRoutes } from '../../../../routing/apm_route_config';
|
||||
import { PopoverTooltip } from '../../../popover_tooltip';
|
||||
|
@ -32,12 +34,20 @@ interface ServiceLinkProps {
|
|||
query: TypeOf<ApmRoutes, '/services/{serviceName}/overview'>['query'];
|
||||
serviceName: string;
|
||||
serviceOverflowCount?: number;
|
||||
signalTypes?: SignalTypes[];
|
||||
}
|
||||
export function ServiceLink({ agentName, query, serviceName }: ServiceLinkProps) {
|
||||
const { link } = useApmRouter();
|
||||
export function ServiceLink({
|
||||
agentName,
|
||||
query,
|
||||
serviceName,
|
||||
signalTypes = [SignalTypes.METRICS],
|
||||
}: ServiceLinkProps) {
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const serviceLink = isMobileAgentName(agentName)
|
||||
? '/mobile-services/{serviceName}/overview'
|
||||
: isLogsSignal(signalTypes)
|
||||
? '/logs-services/{serviceName}/overview'
|
||||
: '/services/{serviceName}/overview';
|
||||
|
||||
if (serviceName === OTHER_SERVICE_NAME) {
|
||||
|
@ -74,7 +84,7 @@ export function ServiceLink({ agentName, query, serviceName }: ServiceLinkProps)
|
|||
content={
|
||||
<StyledLink
|
||||
data-test-subj={`serviceLink_${agentName}`}
|
||||
href={link(serviceLink, {
|
||||
href={apmRouter.link(serviceLink, {
|
||||
path: { serviceName },
|
||||
query,
|
||||
})}
|
||||
|
|
|
@ -47,7 +47,11 @@ export function ApmServiceContextProvider({ children }: { children: ReactNode })
|
|||
path: { serviceName },
|
||||
query,
|
||||
query: { kuery, rangeFrom, rangeTo },
|
||||
} = useAnyOfApmParams('/services/{serviceName}', '/mobile-services/{serviceName}');
|
||||
} = useAnyOfApmParams(
|
||||
'/services/{serviceName}',
|
||||
'/mobile-services/{serviceName}',
|
||||
'/logs-services/{serviceName}'
|
||||
);
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
|
|
|
@ -41,7 +41,11 @@ export function ServiceAnomalyTimeseriesContextProvider({
|
|||
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useAnyOfApmParams('/services/{serviceName}', '/mobile-services/{serviceName}');
|
||||
} = useAnyOfApmParams(
|
||||
'/services/{serviceName}',
|
||||
'/mobile-services/{serviceName}',
|
||||
'/logs-services/{serviceName}'
|
||||
);
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const { preferredEnvironment } = useEnvironmentsContext();
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { SignalTypes } from '../../common/entities/types';
|
||||
|
||||
export function isApmSignal(signalTypes: SignalTypes[]) {
|
||||
return signalTypes.includes(SignalTypes.METRICS) || signalTypes.includes(SignalTypes.TRACES);
|
||||
}
|
||||
export function isLogsSignal(signalTypes: SignalTypes[]) {
|
||||
return signalTypes.includes(SignalTypes.LOGS) && !isApmSignal(signalTypes);
|
||||
}
|
|
@ -8,6 +8,10 @@ import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
|
|||
import type { KibanaRequest } from '@kbn/core/server';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { unwrapEsResponse } from '@kbn/observability-plugin/common/utils/unwrap_es_response';
|
||||
import {
|
||||
MsearchMultisearchBody,
|
||||
MsearchMultisearchHeader,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { withApmSpan } from '../../../../utils/with_apm_span';
|
||||
|
||||
const ENTITIES_INDEX_NAME = '.entities-observability.latest-*';
|
||||
|
@ -29,6 +33,9 @@ export interface EntitiesESClient {
|
|||
operationName: string,
|
||||
searchRequest: TSearchRequest
|
||||
): Promise<InferSearchResponseOf<TDocument, TSearchRequest>>;
|
||||
msearch<TDocument = unknown, TSearchRequest extends ESSearchRequest = ESSearchRequest>(
|
||||
allSearches: TSearchRequest[]
|
||||
): Promise<{ responses: Array<InferSearchResponseOf<TDocument, TSearchRequest>> }>;
|
||||
}
|
||||
|
||||
export async function createEntitiesESClient({
|
||||
|
@ -63,5 +70,36 @@ export async function createEntitiesESClient({
|
|||
|
||||
return unwrapEsResponse(promise);
|
||||
},
|
||||
|
||||
async msearch<TDocument = unknown, TSearchRequest extends ESSearchRequest = ESSearchRequest>(
|
||||
allSearches: TSearchRequest[]
|
||||
): Promise<{ responses: Array<InferSearchResponseOf<TDocument, TSearchRequest>> }> {
|
||||
const searches = allSearches
|
||||
.map((params) => {
|
||||
const searchParams: [MsearchMultisearchHeader, MsearchMultisearchBody] = [
|
||||
{
|
||||
index: [ENTITIES_INDEX_NAME],
|
||||
},
|
||||
{
|
||||
...params.body,
|
||||
},
|
||||
];
|
||||
|
||||
return searchParams;
|
||||
})
|
||||
.flat();
|
||||
|
||||
const promise = esClient.msearch(
|
||||
{ searches },
|
||||
{
|
||||
meta: true,
|
||||
}
|
||||
) as unknown as Promise<{
|
||||
body: { responses: Array<InferSearchResponseOf<TDocument, TSearchRequest>> };
|
||||
}>;
|
||||
|
||||
const { body } = await promise;
|
||||
return { responses: body.responses };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { SERVICE_NAME } from '../../../common/es_fields/apm';
|
||||
import { SavedApmCustomDashboard } from '../../../common/custom_dashboards';
|
||||
import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients';
|
||||
|
||||
function getSearchRequest(filters: estypes.QueryDslQueryContainer[]) {
|
||||
return {
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
terminate_after: 1,
|
||||
size: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter: filters,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
export async function getEntitiesWithDashboards({
|
||||
entitiesESClient,
|
||||
allLinkedCustomDashboards,
|
||||
serviceName,
|
||||
}: {
|
||||
entitiesESClient: EntitiesESClient;
|
||||
allLinkedCustomDashboards: SavedApmCustomDashboard[];
|
||||
serviceName: string;
|
||||
}): Promise<SavedApmCustomDashboard[]> {
|
||||
const allKueryPerDashboard = allLinkedCustomDashboards.map(({ kuery }) => ({
|
||||
kuery,
|
||||
}));
|
||||
|
||||
const allSearches = allKueryPerDashboard.map((dashboard) =>
|
||||
getSearchRequest([...kqlQuery(dashboard.kuery), ...termQuery(SERVICE_NAME, serviceName)])
|
||||
);
|
||||
|
||||
const filteredDashboards = [];
|
||||
|
||||
if (allSearches.length > 0) {
|
||||
const allResponses = (await entitiesESClient.msearch(allSearches)).responses;
|
||||
|
||||
for (let index = 0; index < allLinkedCustomDashboards.length; index++) {
|
||||
const responsePerDashboard = allResponses[index];
|
||||
const dashboard = allLinkedCustomDashboards[index];
|
||||
|
||||
if (responsePerDashboard.hits.hits.length > 0) {
|
||||
filteredDashboards.push(dashboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDashboards;
|
||||
}
|
|
@ -14,6 +14,8 @@ import { getCustomDashboards } from './get_custom_dashboards';
|
|||
import { getServicesWithDashboards } from './get_services_with_dashboards';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { rangeRt } from '../default_api_types';
|
||||
import { createEntitiesESClient } from '../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients';
|
||||
import { getEntitiesWithDashboards } from './get_entities_with_dashboards';
|
||||
|
||||
const serviceDashboardSaveRoute = createApmServerRoute({
|
||||
endpoint: 'POST /internal/apm/custom-dashboard',
|
||||
|
@ -53,14 +55,20 @@ const serviceDashboardsRoute = createApmServerRoute({
|
|||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: rangeRt,
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
t.partial({
|
||||
checkFor: t.union([t.literal('entities'), t.literal('services')]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async (resources): Promise<{ serviceDashboards: SavedApmCustomDashboard[] }> => {
|
||||
const { context, params } = resources;
|
||||
const { start, end } = params.query;
|
||||
const { context, params, request } = resources;
|
||||
const coreContext = await context.core;
|
||||
const { start, end, checkFor } = params.query;
|
||||
|
||||
const { serviceName } = params.path;
|
||||
|
||||
|
@ -74,6 +82,21 @@ const serviceDashboardsRoute = createApmServerRoute({
|
|||
savedObjectsClient,
|
||||
});
|
||||
|
||||
if (checkFor === 'entities') {
|
||||
const entitiesESClient = await createEntitiesESClient({
|
||||
request,
|
||||
esClient: coreContext.elasticsearch.client.asCurrentUser,
|
||||
});
|
||||
|
||||
const entitiesWithDashboards = await getEntitiesWithDashboards({
|
||||
entitiesESClient,
|
||||
allLinkedCustomDashboards,
|
||||
serviceName,
|
||||
});
|
||||
|
||||
return { serviceDashboards: entitiesWithDashboards };
|
||||
}
|
||||
|
||||
const servicesWithDashboards = await getServicesWithDashboards({
|
||||
apmEventClient,
|
||||
allLinkedCustomDashboards,
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { EntityServiceListItem } from '../../../../common/entities/types';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
import { createEntitiesESClient } from '../../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients';
|
||||
import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client';
|
||||
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from '../../default_api_types';
|
||||
import { getServiceEntities } from './get_service_entities';
|
||||
|
@ -23,12 +23,8 @@ const servicesEntitiesRoute = createApmServerRoute({
|
|||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
async handler(resources): Promise<EntityServicesResponse> {
|
||||
const { context, params, request, plugins } = resources;
|
||||
const [coreContext] = await Promise.all([
|
||||
context.core,
|
||||
getApmEventClient(resources),
|
||||
plugins.logsDataAccess.start(),
|
||||
]);
|
||||
const { context, params, request } = resources;
|
||||
const coreContext = await context.core;
|
||||
|
||||
const entitiesESClient = await createEntitiesESClient({
|
||||
request,
|
||||
|
@ -50,6 +46,76 @@ const servicesEntitiesRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const serviceLogRateTimeseriesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([environmentRt, kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
async handler(resources) {
|
||||
const { context, params, plugins } = resources;
|
||||
const [coreContext, logsDataAccessStart] = await Promise.all([
|
||||
context.core,
|
||||
plugins.logsDataAccess.start(),
|
||||
]);
|
||||
|
||||
const { serviceName } = params.path;
|
||||
const { start, end, kuery, environment } = params.query;
|
||||
|
||||
const curentPeriodlogsRateTimeseries = await logsDataAccessStart.services.getLogsRateTimeseries(
|
||||
{
|
||||
esClient: coreContext.elasticsearch.client.asCurrentUser,
|
||||
identifyingMetadata: 'service.name',
|
||||
timeFrom: start,
|
||||
timeTo: end,
|
||||
kuery,
|
||||
serviceEnvironmentQuery: environmentQuery(environment),
|
||||
serviceNames: [serviceName],
|
||||
}
|
||||
);
|
||||
|
||||
return { currentPeriod: curentPeriodlogsRateTimeseries };
|
||||
},
|
||||
});
|
||||
|
||||
const serviceLogErrorRateTimeseriesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([environmentRt, kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
async handler(resources) {
|
||||
const { context, params, plugins } = resources;
|
||||
const [coreContext, logsDataAccessStart] = await Promise.all([
|
||||
context.core,
|
||||
plugins.logsDataAccess.start(),
|
||||
]);
|
||||
|
||||
const { serviceName } = params.path;
|
||||
const { start, end, kuery, environment } = params.query;
|
||||
|
||||
const logsErrorRateTimeseries = await logsDataAccessStart.services.getLogsErrorRateTimeseries({
|
||||
esClient: coreContext.elasticsearch.client.asCurrentUser,
|
||||
identifyingMetadata: 'service.name',
|
||||
timeFrom: start,
|
||||
timeTo: end,
|
||||
kuery,
|
||||
serviceEnvironmentQuery: environmentQuery(environment),
|
||||
serviceNames: [serviceName],
|
||||
});
|
||||
|
||||
return { currentPeriod: logsErrorRateTimeseries };
|
||||
},
|
||||
});
|
||||
|
||||
export const servicesEntitiesRoutesRepository = {
|
||||
...servicesEntitiesRoute,
|
||||
...serviceLogRateTimeseriesRoute,
|
||||
...serviceLogErrorRateTimeseriesRoute,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const LOG_LEVEL = 'log.level';
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { AggregationOptionsByType, AggregationResultOf } from '@kbn/es-types';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { existsQuery, kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { getBucketSizeFromTimeRangeAndBucketCount, getLogErrorRate } from '../../utils';
|
||||
import { LOG_LEVEL } from '../../es_fields';
|
||||
|
||||
export interface LogsErrorRateTimeseries {
|
||||
esClient: ElasticsearchClient;
|
||||
serviceEnvironmentQuery?: QueryDslQueryContainer[];
|
||||
serviceNames: string[];
|
||||
identifyingMetadata: string;
|
||||
timeFrom: number;
|
||||
timeTo: number;
|
||||
kuery?: string;
|
||||
}
|
||||
|
||||
export const getLogErrorsAggegation = () => ({
|
||||
terms: {
|
||||
field: LOG_LEVEL,
|
||||
include: ['error', 'ERROR'],
|
||||
},
|
||||
});
|
||||
|
||||
type LogErrorsAggregation = ReturnType<typeof getLogErrorsAggegation>;
|
||||
interface LogsErrorRateTimeseriesHistogram {
|
||||
timeseries: AggregationResultOf<
|
||||
{
|
||||
date_histogram: AggregationOptionsByType['date_histogram'];
|
||||
aggs: { logErrors: LogErrorsAggregation };
|
||||
},
|
||||
{}
|
||||
>;
|
||||
doc_count: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface LogRateQueryAggregation {
|
||||
services: estypes.AggregationsTermsAggregateBase<LogsErrorRateTimeseriesHistogram>;
|
||||
}
|
||||
export interface LogsErrorRateTimeseriesReturnType {
|
||||
[serviceName: string]: Array<{ x: number; y: number | null }>;
|
||||
}
|
||||
export function createGetLogErrorRateTimeseries() {
|
||||
return async ({
|
||||
esClient,
|
||||
identifyingMetadata,
|
||||
serviceNames,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
kuery,
|
||||
serviceEnvironmentQuery = [],
|
||||
}: LogsErrorRateTimeseries): Promise<LogsErrorRateTimeseriesReturnType> => {
|
||||
const intervalString = getBucketSizeFromTimeRangeAndBucketCount(timeFrom, timeTo, 50);
|
||||
|
||||
const esResponse = await esClient.search({
|
||||
index: 'logs-*-*',
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...existsQuery(LOG_LEVEL),
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
terms: {
|
||||
[identifyingMetadata]: serviceNames,
|
||||
},
|
||||
},
|
||||
...serviceEnvironmentQuery,
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: timeFrom,
|
||||
lte: timeTo,
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: identifyingMetadata,
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: `${intervalString}s`,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: timeFrom,
|
||||
max: timeTo,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
logErrors: getLogErrorsAggegation(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const aggregations = esResponse.aggregations as LogRateQueryAggregation | undefined;
|
||||
const buckets = aggregations?.services.buckets as LogsErrorRateTimeseriesHistogram[];
|
||||
|
||||
return buckets
|
||||
? buckets.reduce<LogsErrorRateTimeseriesReturnType>((acc, bucket) => {
|
||||
const timeseries = bucket.timeseries.buckets.map((timeseriesBucket) => {
|
||||
const totalCount = timeseriesBucket.doc_count;
|
||||
const logErrorCount = timeseriesBucket.logErrors.buckets[0]?.doc_count;
|
||||
|
||||
return {
|
||||
x: timeseriesBucket.key,
|
||||
y: logErrorCount ? getLogErrorRate({ logCount: totalCount, logErrorCount }) : null,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[bucket.key]: timeseries,
|
||||
};
|
||||
}, {})
|
||||
: {};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { AggregationOptionsByType, AggregationResultOf } from '@kbn/es-types';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { existsQuery, kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { getBucketSizeFromTimeRangeAndBucketCount } from '../../utils';
|
||||
import { LOG_LEVEL } from '../../es_fields';
|
||||
|
||||
export interface LogsRateTimeseries {
|
||||
esClient: ElasticsearchClient;
|
||||
serviceEnvironmentQuery?: QueryDslQueryContainer[];
|
||||
serviceNames: string[];
|
||||
identifyingMetadata: string;
|
||||
timeFrom: number;
|
||||
timeTo: number;
|
||||
kuery?: string;
|
||||
}
|
||||
|
||||
interface LogsRateTimeseriesHistogram {
|
||||
timeseries: AggregationResultOf<
|
||||
{
|
||||
date_histogram: AggregationOptionsByType['date_histogram'];
|
||||
},
|
||||
{}
|
||||
>;
|
||||
doc_count: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface LogRateQueryAggregation {
|
||||
service: estypes.AggregationsTermsAggregateBase<LogsRateTimeseriesHistogram>;
|
||||
}
|
||||
export interface LogsRateTimeseriesReturnType {
|
||||
[serviceName: string]: Array<{ x: number; y: number | null }>;
|
||||
}
|
||||
export function createGetLogsRateTimeseries() {
|
||||
return async ({
|
||||
esClient,
|
||||
identifyingMetadata,
|
||||
serviceNames,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
kuery,
|
||||
serviceEnvironmentQuery = [],
|
||||
}: LogsRateTimeseries): Promise<LogsRateTimeseriesReturnType> => {
|
||||
const intervalString = getBucketSizeFromTimeRangeAndBucketCount(timeFrom, timeTo, 50);
|
||||
|
||||
const esResponse = await esClient.search({
|
||||
index: 'logs-*-*',
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...existsQuery(LOG_LEVEL),
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
terms: {
|
||||
[identifyingMetadata]: serviceNames,
|
||||
},
|
||||
},
|
||||
...serviceEnvironmentQuery,
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: timeFrom,
|
||||
lte: timeTo,
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
service: {
|
||||
terms: {
|
||||
field: identifyingMetadata,
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: `${intervalString}s`,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: timeFrom,
|
||||
max: timeTo,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const aggregations = esResponse.aggregations as LogRateQueryAggregation | undefined;
|
||||
const buckets = aggregations?.service.buckets as LogsRateTimeseriesHistogram[];
|
||||
|
||||
return buckets
|
||||
? buckets.reduce<LogsRateTimeseriesReturnType>((acc, bucket) => {
|
||||
const totalCount = bucket.doc_count;
|
||||
|
||||
const timeseries = bucket.timeseries.buckets.map((timeseriesBucket) => {
|
||||
return {
|
||||
x: timeseriesBucket.key,
|
||||
y: timeseriesBucket.doc_count / totalCount,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[bucket.key]: timeseries,
|
||||
};
|
||||
}, {})
|
||||
: {};
|
||||
};
|
||||
}
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { RegisterServicesParams } from '../register_services';
|
||||
import { getLogErrorRate, getLogRatePerMinute } from './utils';
|
||||
import { getLogErrorRate, getLogRatePerMinute } from '../../utils';
|
||||
import { LOG_LEVEL } from '../../es_fields';
|
||||
|
||||
export interface LogsRatesServiceParams {
|
||||
esClient: ElasticsearchClient;
|
||||
|
@ -35,7 +35,7 @@ export interface LogsRatesServiceReturnType {
|
|||
[serviceName: string]: LogsRatesMetrics;
|
||||
}
|
||||
|
||||
export function createGetLogsRatesService(params: RegisterServicesParams) {
|
||||
export function createGetLogsRatesService() {
|
||||
return async ({
|
||||
esClient,
|
||||
identifyingMetadata,
|
||||
|
@ -52,7 +52,7 @@ export function createGetLogsRatesService(params: RegisterServicesParams) {
|
|||
{
|
||||
exists: {
|
||||
// For now, we don't want to count APM server logs or any other logs that don't have the log.level field.
|
||||
field: 'log.level',
|
||||
field: LOG_LEVEL,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -80,7 +80,7 @@ export function createGetLogsRatesService(params: RegisterServicesParams) {
|
|||
aggs: {
|
||||
logErrors: {
|
||||
terms: {
|
||||
field: 'log.level',
|
||||
field: LOG_LEVEL,
|
||||
include: ['error', 'ERROR'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server';
|
||||
import { UiSettingsServiceStart } from '@kbn/core-ui-settings-server';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { createGetLogsRateTimeseries } from './get_logs_rate_timeseries/get_logs_rate_timeseries';
|
||||
import { createGetLogErrorRateTimeseries } from './get_logs_error_rate_timeseries/get_logs_error_rate_timeseries';
|
||||
import { createGetLogsRatesService } from './get_logs_rates_service';
|
||||
import { createGetLogSourcesService } from './log_sources_service';
|
||||
|
||||
|
@ -21,7 +23,9 @@ export interface RegisterServicesParams {
|
|||
|
||||
export function registerServices(params: RegisterServicesParams) {
|
||||
return {
|
||||
getLogsRatesService: createGetLogsRatesService(params),
|
||||
getLogsRatesService: createGetLogsRatesService(),
|
||||
getLogsRateTimeseries: createGetLogsRateTimeseries(),
|
||||
getLogsErrorRateTimeseries: createGetLogErrorRateTimeseries(),
|
||||
getLogSourcesService: createGetLogSourcesService(params),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment/moment';
|
||||
import { calculateAuto } from '@kbn/calculate-auto';
|
||||
|
||||
export function getBucketSizeFromTimeRangeAndBucketCount(
|
||||
timeFrom: number,
|
||||
timeTo: number,
|
||||
numBuckets: number
|
||||
): number {
|
||||
const duration = moment.duration(timeTo - timeFrom, 'ms');
|
||||
|
||||
return Math.max(calculateAuto.near(numBuckets, duration)?.asSeconds() ?? 0, 60);
|
||||
}
|
||||
|
||||
export function getLogRatePerMinute({
|
||||
logCount,
|
||||
timeFrom,
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { getLogRatePerMinute, getLogErrorRate } from './utils';
|
||||
import { getLogRatePerMinute, getLogErrorRate } from '.';
|
||||
|
||||
describe('getLogRatePerMinute', () => {
|
||||
it('should log rate per minute for one minute period', () => {
|
|
@ -3,13 +3,15 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["common/**/*", "server/**/*", "public/**/*", "jest.config.js"],
|
||||
"include": ["common/**/*", "server/**/*", "public/**/*", "jest.config.js"],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/logging",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/es-types",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/config-schema",
|
||||
|
@ -18,5 +20,6 @@
|
|||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/core-ui-settings-server",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/logging"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { log, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { first, last } from 'lodash';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const logSynthtrace = getService('logSynthtraceEsClient');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
const start = new Date('2024-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
const hostName = 'synth-host';
|
||||
|
||||
async function getLogsErrorRateTimeseries(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries'>['params']
|
||||
>
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'synth-go',
|
||||
...overrides?.path,
|
||||
},
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Logs error rate timeseries when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Logs error rate api', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await getLogsErrorRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.empty();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Logs error rate timeseries when data loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Logs without log level field', () => {
|
||||
before(async () => {
|
||||
return logSynthtrace.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log.create().message('This is a log message').timestamp(timestamp).defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
after(async () => {
|
||||
await logSynthtrace.clean();
|
||||
});
|
||||
|
||||
it('returns {} if log level is not available ', async () => {
|
||||
const response = await getLogsErrorRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logs with log.level=error', () => {
|
||||
before(async () => {
|
||||
return logSynthtrace.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is a log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
'service.environment': 'test',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('2m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an error log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an info message')
|
||||
.logLevel('info')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
after(async () => {
|
||||
await logSynthtrace.clean();
|
||||
});
|
||||
|
||||
it('returns log error rate timeseries', async () => {
|
||||
const response = await getLogsErrorRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod[serviceName].every(({ y }) => y === 1)).to.be(true);
|
||||
});
|
||||
|
||||
it('handles environment filter', async () => {
|
||||
const response = await getLogsErrorRateTimeseries({ query: { environment: 'foo' } });
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.empty();
|
||||
});
|
||||
|
||||
describe('when my-service is selected', () => {
|
||||
it('returns some data', async () => {
|
||||
const response = await getLogsErrorRateTimeseries({
|
||||
path: { serviceName: 'my-service' },
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be(0.5);
|
||||
expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { log, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { first, last } from 'lodash';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const logSynthtrace = getService('logSynthtraceEsClient');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
const start = new Date('2024-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
const hostName = 'synth-host';
|
||||
|
||||
async function getLogsRateTimeseries(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>['params']
|
||||
>
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'synth-go',
|
||||
...overrides?.path,
|
||||
},
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Logs rate timeseries when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Logs rate api', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await getLogsRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.empty();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when('Logs rate timeseries when data loaded', { config: 'basic', archives: [] }, () => {
|
||||
describe('Logs without log level field', () => {
|
||||
before(async () => {
|
||||
return logSynthtrace.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log.create().message('This is a log message').timestamp(timestamp).defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
after(async () => {
|
||||
await logSynthtrace.clean();
|
||||
});
|
||||
|
||||
it('returns {} if log level is not available ', async () => {
|
||||
const response = await getLogsRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logs with log.level=error', () => {
|
||||
before(async () => {
|
||||
return logSynthtrace.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is a log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
'service.environment': 'test',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('2m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an error log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an info message')
|
||||
.logLevel('info')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
after(async () => {
|
||||
await logSynthtrace.clean();
|
||||
});
|
||||
|
||||
it('returns log rate timeseries', async () => {
|
||||
const response = await getLogsRateTimeseries();
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667)
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
it('handles environment filter', async () => {
|
||||
const response = await getLogsRateTimeseries({ query: { environment: 'foo' } });
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.empty();
|
||||
});
|
||||
|
||||
describe('when my-service is selected', () => {
|
||||
it('returns some data', async () => {
|
||||
const response = await getLogsRateTimeseries({
|
||||
path: { serviceName: 'my-service' },
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be(0.18181818181818182);
|
||||
expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue