diff --git a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap
index 2219e0d7609b8..cc4e27a6d6388 100644
--- a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap
+++ b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap
@@ -4,17 +4,24 @@ exports[`MetricTile correct displays a byte metric 1`] = `
`;
exports[`MetricTile correct displays a float metric 1`] = `
-
`;
@@ -22,7 +29,7 @@ exports[`MetricTile correct displays a time metric 1`] = `
`;
@@ -31,7 +38,29 @@ exports[`MetricTile correct displays an untyped metric 1`] = `
`;
+
+exports[`MetricTile correctly displays a metric with metadata 1`] = `
+
+`;
diff --git a/src/core/public/core_app/status/components/metric_tiles.test.tsx b/src/core/public/core_app/status/components/metric_tiles.test.tsx
index 76608718e8cd3..8e6d1cf38cd01 100644
--- a/src/core/public/core_app/status/components/metric_tiles.test.tsx
+++ b/src/core/public/core_app/status/components/metric_tiles.test.tsx
@@ -35,6 +35,18 @@ const timeMetric: Metric = {
value: 1234,
};
+const metricWithMeta: Metric = {
+ name: 'Delay',
+ type: 'time',
+ value: 1,
+ meta: {
+ description: 'Percentiles',
+ title: '',
+ value: [1, 5, 10],
+ type: 'time',
+ },
+};
+
describe('MetricTile', () => {
it('correct displays an untyped metric', () => {
const component = shallow();
@@ -55,4 +67,9 @@ describe('MetricTile', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
+
+ it('correctly displays a metric with metadata', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/core/public/core_app/status/components/metric_tiles.tsx b/src/core/public/core_app/status/components/metric_tiles.tsx
index 1eb5ee4c95dd8..18fa9ae738227 100644
--- a/src/core/public/core_app/status/components/metric_tiles.tsx
+++ b/src/core/public/core_app/status/components/metric_tiles.tsx
@@ -7,24 +7,105 @@
*/
import React, { FunctionComponent } from 'react';
-import { EuiFlexGrid, EuiFlexItem, EuiCard } from '@elastic/eui';
-import { formatNumber, Metric } from '../lib';
+import { EuiFlexGrid, EuiFlexItem, EuiCard, EuiStat } from '@elastic/eui';
+import { DataType, formatNumber, Metric } from '../lib';
/*
- * Displays a metric with the correct format.
+ * Displays metadata for a metric.
*/
-export const MetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => {
- const { name } = metric;
+const MetricCardFooter: FunctionComponent<{
+ title: string;
+ description: string;
+}> = ({ title, description }) => {
+ return (
+
+ );
+};
+
+const DelayMetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => {
+ const { name, meta } = metric;
return (
+ )
+ }
+ />
+ );
+};
+
+const LoadMetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => {
+ const { name, meta } = metric;
+ return (
+ }
/>
);
};
+const ResponseTimeMetric: FunctionComponent<{ metric: Metric }> = ({ metric }) => {
+ const { name, meta } = metric;
+ return (
+
+ )
+ }
+ />
+ );
+};
+
+/*
+ * Displays a metric with the correct format.
+ */
+export const MetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => {
+ const { name } = metric;
+ switch (name) {
+ case 'Delay':
+ return ;
+ case 'Load':
+ return ;
+ case 'Response time avg':
+ return ;
+ default:
+ return (
+
+ );
+ }
+};
+
/*
* Wrapper component that simply maps each metric to MetricTile inside a FlexGroup
*/
@@ -38,11 +119,20 @@ export const MetricTiles: FunctionComponent<{ metrics: Metric[] }> = ({ metrics
);
+// formatting helper functions
+
const formatMetric = ({ value, type }: Metric) => {
const metrics = Array.isArray(value) ? value : [value];
return metrics.map((metric) => formatNumber(metric, type)).join(', ');
};
-const formatMetricId = ({ name }: Metric) => {
+const formatMetricId = (name: Metric['name']) => {
return name.toLowerCase().replace(/[ ]+/g, '-');
};
+
+const formatDelayFooterTitle = (values: number[], type?: DataType) => {
+ return `
+ 50: ${formatNumber(values[0], type)};
+ 95: ${formatNumber(values[1], type)};
+ 99: ${formatNumber(values[2], type)}`;
+};
diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts
index 73c697c3d55aa..5b5a2d0af99bc 100644
--- a/src/core/public/core_app/status/lib/load_status.test.ts
+++ b/src/core/public/core_app/status/lib/load_status.test.ts
@@ -218,13 +218,23 @@ describe('response processing', () => {
expect(names).toEqual([
'Heap total',
'Heap used',
+ 'Requests per second',
'Load',
+ 'Delay',
'Response time avg',
- 'Response time max',
- 'Requests per second',
]);
-
const values = data.metrics.map((m) => m.value);
- expect(values).toEqual([1000000, 100, [4.1, 2.1, 0.1], 4000, 8000, 400]);
+ expect(values).toEqual([1000000, 100, 400, [4.1, 2.1, 0.1], 1, 4000]);
+ });
+
+ test('adds meta details to Load, Delay and Response time', async () => {
+ const data = await loadStatus({ http, notifications });
+ const metricNames = data.metrics.filter((met) => met.meta);
+ expect(metricNames.map((item) => item.name)).toEqual(['Load', 'Delay', 'Response time avg']);
+ expect(metricNames.map((item) => item.meta!.description)).toEqual([
+ 'Load interval',
+ 'Percentiles',
+ 'Response time max',
+ ]);
});
});
diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts
index a5cc18ffd6c16..f33ad70c63f53 100644
--- a/src/core/public/core_app/status/lib/load_status.ts
+++ b/src/core/public/core_app/status/lib/load_status.ts
@@ -13,10 +13,17 @@ import type { HttpSetup } from '../../../http';
import type { NotificationsSetup } from '../../../notifications';
import type { DataType } from '../lib';
+interface MetricMeta {
+ title: string;
+ description: string;
+ value?: number[];
+ type?: DataType;
+}
export interface Metric {
name: string;
value: number | number[];
type?: DataType;
+ meta?: MetricMeta;
}
export interface FormattedStatus {
@@ -57,33 +64,62 @@ function formatMetrics({ metrics }: StatusResponse): Metric[] {
value: metrics.process.memory.heap.used_in_bytes,
type: 'byte',
},
+ {
+ name: i18n.translate('core.statusPage.metricsTiles.columns.requestsPerSecHeader', {
+ defaultMessage: 'Requests per second',
+ }),
+ value: (metrics.requests.total * 1000) / metrics.collection_interval_in_millis,
+ type: 'float',
+ },
{
name: i18n.translate('core.statusPage.metricsTiles.columns.loadHeader', {
defaultMessage: 'Load',
}),
value: [metrics.os.load['1m'], metrics.os.load['5m'], metrics.os.load['15m']],
type: 'float',
+ meta: {
+ description: i18n.translate('core.statusPage.metricsTiles.columns.load.metaHeader', {
+ defaultMessage: 'Load interval',
+ }),
+ title: Object.keys(metrics.os.load).join('; '),
+ },
},
{
- name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeAvgHeader', {
- defaultMessage: 'Response time avg',
+ name: i18n.translate('core.statusPage.metricsTiles.columns.processDelayHeader', {
+ defaultMessage: 'Delay',
}),
- value: metrics.response_times.avg_in_millis,
+ value: metrics.process.event_loop_delay,
type: 'time',
+ meta: {
+ description: i18n.translate(
+ 'core.statusPage.metricsTiles.columns.processDelayDetailsHeader',
+ {
+ defaultMessage: 'Percentiles',
+ }
+ ),
+ title: '',
+ value: [
+ metrics.process.event_loop_delay_histogram?.percentiles['50'],
+ metrics.process.event_loop_delay_histogram?.percentiles['95'],
+ metrics.process.event_loop_delay_histogram?.percentiles['99'],
+ ],
+ type: 'time',
+ },
},
{
- name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeMaxHeader', {
- defaultMessage: 'Response time max',
+ name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeAvgHeader', {
+ defaultMessage: 'Response time avg',
}),
- value: metrics.response_times.max_in_millis,
+ value: metrics.response_times.avg_in_millis,
type: 'time',
- },
- {
- name: i18n.translate('core.statusPage.metricsTiles.columns.requestsPerSecHeader', {
- defaultMessage: 'Requests per second',
- }),
- value: (metrics.requests.total * 1000) / metrics.collection_interval_in_millis,
- type: 'float',
+ meta: {
+ description: i18n.translate('core.statusPage.metricsTiles.columns.resTimeMaxHeader', {
+ defaultMessage: 'Response time max',
+ }),
+ title: '',
+ value: [metrics.response_times.max_in_millis],
+ type: 'time',
+ },
},
];
}
diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts
index cba188c94c74e..3fd3c4a7a24d6 100644
--- a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts
+++ b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts
@@ -42,6 +42,7 @@ const testMetrics = {
memory: { heap: { used_in_bytes: 100 } },
uptime_in_millis: 1500,
event_loop_delay: 50,
+ event_loop_delay_histogram: { percentiles: { '50': 50, '75': 75, '95': 95, '99': 99 } },
},
os: {
load: {
@@ -56,7 +57,7 @@ describe('getEcsOpsMetricsLog', () => {
it('provides correctly formatted message', () => {
const result = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics));
expect(result.message).toMatchInlineSnapshot(
- `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] delay: 50.000"`
+ `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] mean delay: 50.000 delay histogram: { 50: 50.000; 95: 95.000; 99: 99.000 }"`
);
});
@@ -70,6 +71,7 @@ describe('getEcsOpsMetricsLog', () => {
const missingMetrics = {
...baseMetrics,
process: {},
+ processes: [],
os: {},
} as unknown as OpsMetrics;
const logMeta = getEcsOpsMetricsLog(missingMetrics);
@@ -77,39 +79,41 @@ describe('getEcsOpsMetricsLog', () => {
});
it('provides an ECS-compatible response', () => {
- const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics());
- expect(logMeta).toMatchInlineSnapshot(`
+ const logMeta = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics));
+ expect(logMeta.meta).toMatchInlineSnapshot(`
Object {
- "message": "memory: 1.0B load: [1.00,1.00,1.00] delay: 1.000",
- "meta": Object {
- "event": Object {
- "category": Array [
- "process",
- "host",
- ],
- "kind": "metric",
- "type": Array [
- "info",
- ],
- },
- "host": Object {
- "os": Object {
- "load": Object {
- "15m": 1,
- "1m": 1,
- "5m": 1,
- },
+ "event": Object {
+ "category": Array [
+ "process",
+ "host",
+ ],
+ "kind": "metric",
+ "type": Array [
+ "info",
+ ],
+ },
+ "host": Object {
+ "os": Object {
+ "load": Object {
+ "15m": 30,
+ "1m": 10,
+ "5m": 20,
},
},
- "process": Object {
- "eventLoopDelay": 1,
- "memory": Object {
- "heap": Object {
- "usedInBytes": 1,
- },
+ },
+ "process": Object {
+ "eventLoopDelay": 50,
+ "eventLoopDelayHistogram": Object {
+ "50": 50,
+ "95": 95,
+ "99": 99,
+ },
+ "memory": Object {
+ "heap": Object {
+ "usedInBytes": 100,
},
- "uptime": 0,
},
+ "uptime": 1,
},
}
`);
diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.ts b/src/core/server/metrics/logging/get_ops_metrics_log.ts
index 7e13f35889ec7..6211407ae86f0 100644
--- a/src/core/server/metrics/logging/get_ops_metrics_log.ts
+++ b/src/core/server/metrics/logging/get_ops_metrics_log.ts
@@ -30,10 +30,29 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) {
// HH:mm:ss message format for backward compatibility
const uptimeValMsg = uptimeVal ? `uptime: ${numeral(uptimeVal).format('00:00:00')} ` : '';
- // Event loop delay is in ms
+ // Event loop delay metrics are in ms
const eventLoopDelayVal = process?.event_loop_delay;
const eventLoopDelayValMsg = eventLoopDelayVal
- ? `delay: ${numeral(process?.event_loop_delay).format('0.000')}`
+ ? `mean delay: ${numeral(process?.event_loop_delay).format('0.000')}`
+ : '';
+
+ const eventLoopDelayPercentiles = process?.event_loop_delay_histogram?.percentiles;
+
+ // Extract 50th, 95th and 99th percentiles for log meta
+ const eventLoopDelayHistVals = eventLoopDelayPercentiles
+ ? {
+ 50: eventLoopDelayPercentiles[50],
+ 95: eventLoopDelayPercentiles[95],
+ 99: eventLoopDelayPercentiles[99],
+ }
+ : undefined;
+ // Format message from 50th, 95th and 99th percentiles
+ const eventLoopDelayHistMsg = eventLoopDelayPercentiles
+ ? ` delay histogram: { 50: ${numeral(eventLoopDelayPercentiles['50']).format(
+ '0.000'
+ )}; 95: ${numeral(eventLoopDelayPercentiles['95']).format('0.000')}; 99: ${numeral(
+ eventLoopDelayPercentiles['99']
+ ).format('0.000')} }`
: '';
const loadEntries = {
@@ -65,6 +84,7 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) {
},
},
eventLoopDelay: eventLoopDelayVal,
+ eventLoopDelayHistogram: eventLoopDelayHistVals,
},
host: {
os: {
@@ -75,7 +95,13 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) {
};
return {
- message: `${processMemoryUsedInBytesMsg}${uptimeValMsg}${loadValsMsg}${eventLoopDelayValMsg}`,
+ message: [
+ processMemoryUsedInBytesMsg,
+ uptimeValMsg,
+ loadValsMsg,
+ eventLoopDelayValMsg,
+ eventLoopDelayHistMsg,
+ ].join(''),
meta,
};
}
diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts
index d7de41fd7ccf7..27043b8fa2c8a 100644
--- a/src/core/server/metrics/metrics_service.test.ts
+++ b/src/core/server/metrics/metrics_service.test.ts
@@ -203,6 +203,7 @@ describe('MetricsService', () => {
},
"process": Object {
"eventLoopDelay": undefined,
+ "eventLoopDelayHistogram": undefined,
"memory": Object {
"heap": Object {
"usedInBytes": undefined,
diff --git a/test/functional/apps/status_page/index.ts b/test/functional/apps/status_page/index.ts
index 08693372cc6eb..21a3b382f7aed 100644
--- a/test/functional/apps/status_page/index.ts
+++ b/test/functional/apps/status_page/index.ts
@@ -33,6 +33,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(metrics).to.have.length(6);
});
+ it('should display the server metrics meta', async () => {
+ const metricsMetas = await testSubjects.findAll('serverMetricMeta');
+ expect(metricsMetas).to.have.length(3);
+ });
+
it('should display the server status', async () => {
const titleText = await testSubjects.getVisibleText('serverStatusTitle');
expect(titleText).to.contain('Kibana status is');
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index 67b57dea6e310..93e5d68c74a97 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
@@ -265,7 +265,6 @@ async function installDataStreamComponentTemplates(params: {
});
const templateNames = Object.keys(templates);
const templateEntries = Object.entries(templates);
-
// TODO: Check return values for errors
await Promise.all(
templateEntries.map(async ([name, body]) => {
@@ -275,7 +274,10 @@ async function installDataStreamComponentTemplates(params: {
const hasUserSettingsTemplate = result.body.component_templates?.length === 1;
if (!hasUserSettingsTemplate) {
// only add if one isn't already present
- const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true });
+ const { clusterPromise } = putComponentTemplate(esClient, {
+ body,
+ name,
+ });
return clusterPromise;
}
} else {
@@ -303,7 +305,6 @@ export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClie
await putComponentTemplate(esClient, {
name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
- create: true,
});
}
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx
index 84864902f75d3..5f89f261e4246 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx
@@ -30,6 +30,8 @@ const columns: Array> = [
align: 'right',
field: 'count',
name: 'Risk Score',
+ render: (riskScore) =>
+ Number.isNaN(riskScore) ? riskScore : Number.parseFloat(riskScore).toFixed(2),
sortable: true,
truncateText: true,
width: '15%',
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
index a483a33ea4c8d..494505b92a8d1 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
@@ -214,9 +214,10 @@ describe('Host Isolation', () => {
Promise.resolve({ body: legacyMetadataSearchResponse(searchResponse) })
);
if (indexExists) {
- ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse;
+ ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse;
}
- ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse;
+
+ ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse;
ctx.core.elasticsearch.client.asCurrentUser.search = mockSearchResponse;
const withLicense = license ? license : Platinum;
licenseEmitter.next(withLicense);
@@ -268,7 +269,7 @@ describe('Host Isolation', () => {
searchResponse: metadataResponse,
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.agents).toContain(AgentID);
});
@@ -279,7 +280,7 @@ describe('Host Isolation', () => {
mockUser: testU,
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.user_id).toEqual(testU.username);
});
@@ -289,7 +290,7 @@ describe('Host Isolation', () => {
body: { endpoint_ids: ['XYZ'], comment: CommentText },
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.data.comment).toEqual(CommentText);
});
@@ -298,7 +299,7 @@ describe('Host Isolation', () => {
body: { endpoint_ids: ['XYZ'], comment: 'XYZ' },
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
const actionID = actionDoc.action_id;
expect(mockResponse.ok).toBeCalled();
@@ -311,7 +312,7 @@ describe('Host Isolation', () => {
body: { endpoint_ids: ['XYZ'] },
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.timeout).toEqual(300);
});
@@ -324,7 +325,7 @@ describe('Host Isolation', () => {
searchResponse: doc,
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.agents).toContain(AgentID);
});
@@ -334,7 +335,7 @@ describe('Host Isolation', () => {
body: { endpoint_ids: ['XYZ'] },
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.data.command).toEqual('isolate');
});
@@ -343,7 +344,7 @@ describe('Host Isolation', () => {
body: { endpoint_ids: ['XYZ'] },
});
const actionDoc: EndpointAction = (
- ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock
+ ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock
).mock.calls[0][0].body;
expect(actionDoc.data.command).toEqual('unisolate');
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
index 02f0cb4867646..fc84b1b1c91cb 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
@@ -209,13 +209,9 @@ export const isolationRequestHandler = function (
}
try {
- let esClient = context.core.elasticsearch.client.asCurrentUser;
- if (doesLogsEndpointActionsDsExist) {
- // create action request record as system user with user in .fleet-actions
- esClient = context.core.elasticsearch.client.asInternalUser;
- }
- // write as the current user if the new indices do not exist
- // ({
index: AGENT_ACTIONS_INDEX,
body: {