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: {