From 1a03fc9cfd079b959c06080f4ed0c62284f425f9 Mon Sep 17 00:00:00 2001 From: Thomas Dullien Date: Thu, 15 Jun 2023 22:37:50 +0200 Subject: [PATCH 1/5] Add support for the ES profiling plugin returning a sampling rate (#159211) ## Summary When visualizing profiling data, the backend ElasticSearch plugin performs statistical sampling to approximate the results, and returns the "sampling rate" to the user. This means that the Kibana profiling UI needs to ... 1) Signal to the user that certain numbers are estimates if the sampling rate is smaller than 1.0 2) Multiply the numbers that it derives from the sample by the inverse of the sample rate. This change does both, for the flamegraph and the TopN view. For the stacked bar chart, a separate PR will be needed. ![Screenshot from 2023-06-07 15-59-03](https://github.com/elastic/kibana/assets/4587964/e6f0b5bd-ec83-45e9-8a1d-a380cde9328e) This screenshot shows that we are displaying a `~` in the flamechart popups to indicate estimated quantities. There are further UI changes in the topN functions window. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/profiling/common/callee.test.ts | 2 +- x-pack/plugins/profiling/common/callee.ts | 10 ++++- .../common/columnar_view_model.test.ts | 4 +- .../profiling/common/flamegraph.test.ts | 4 +- x-pack/plugins/profiling/common/flamegraph.ts | 10 ++++- .../profiling/common/functions.test.ts | 3 +- x-pack/plugins/profiling/common/functions.ts | 16 ++++--- x-pack/plugins/profiling/common/profiling.ts | 3 ++ .../plugins/profiling/common/stack_traces.ts | 1 + .../flamegraph/flamegraph_tooltip.tsx | 9 ++++ .../public/components/flamegraph/index.tsx | 1 + .../components/flamegraph/tooltip_row.tsx | 6 ++- .../get_impact_rows.tsx | 2 + .../frame_information_window/index.tsx | 10 ++++- .../key_value_list.tsx | 9 ++-- .../components/topn_functions/index.tsx | 45 ++++++++++++++----- .../profiling/server/routes/flamechart.ts | 23 ++++++---- .../profiling/server/routes/functions.ts | 10 ++--- .../server/routes/search_stacktraces.test.ts | 4 ++ .../server/routes/search_stacktraces.ts | 1 + 20 files changed, 126 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/profiling/common/callee.test.ts b/x-pack/plugins/profiling/common/callee.test.ts index 4e3ef4b286e31..3cf6fb7484372 100644 --- a/x-pack/plugins/profiling/common/callee.test.ts +++ b/x-pack/plugins/profiling/common/callee.test.ts @@ -12,7 +12,7 @@ import { events, stackTraces, stackFrames, executables } from './__fixtures__/st const totalSamples = sum([...events.values()]); const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length)); -const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames); +const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames, 1.0); describe('Callee operations', () => { test('inclusive count of root equals total sampled stacktraces', () => { diff --git a/x-pack/plugins/profiling/common/callee.ts b/x-pack/plugins/profiling/common/callee.ts index 5f373dc25d25b..04206843463fc 100644 --- a/x-pack/plugins/profiling/common/callee.ts +++ b/x-pack/plugins/profiling/common/callee.ts @@ -43,7 +43,8 @@ export function createCalleeTree( stackTraces: Map, stackFrames: Map, executables: Map, - totalFrames: number + totalFrames: number, + samplingRate: number ): CalleeTree { const tree: CalleeTree = { Size: 1, @@ -62,6 +63,9 @@ export function createCalleeTree( CountExclusive: new Array(totalFrames), }; + // The inverse of the sampling rate is the number with which to multiply the number of + // samples to get an estimate of the actual number of samples the backend received. + const scalingFactor = 1.0 / samplingRate; tree.Edges[0] = new Map(); tree.FileID[0] = ''; @@ -97,10 +101,12 @@ export function createCalleeTree( // e.g. when stopping the host agent or on network errors. const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace; const lenStackTrace = stackTrace.FrameIDs.length; - const samples = events.get(stackTraceID) ?? 0; + const samples = (events.get(stackTraceID) ?? 0) * scalingFactor; let currentNode = 0; + // Increment the count by the number of samples observed, multiplied with the inverse of the + // samplingrate (this essentially means scaling up the total samples). It would incur tree.CountInclusive[currentNode] += samples; tree.CountExclusive[currentNode] = 0; diff --git a/x-pack/plugins/profiling/common/columnar_view_model.test.ts b/x-pack/plugins/profiling/common/columnar_view_model.test.ts index c41e2b0aef4ea..a40f2225b6c19 100644 --- a/x-pack/plugins/profiling/common/columnar_view_model.test.ts +++ b/x-pack/plugins/profiling/common/columnar_view_model.test.ts @@ -14,8 +14,8 @@ import { events, stackTraces, stackFrames, executables } from './__fixtures__/st const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length)); -const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames); -const graph = createFlameGraph(createBaseFlameGraph(tree, 60)); +const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames, 1.0); +const graph = createFlameGraph(createBaseFlameGraph(tree, 1.0, 60)); describe('Columnar view model operations', () => { test('color values are generated by default', () => { diff --git a/x-pack/plugins/profiling/common/flamegraph.test.ts b/x-pack/plugins/profiling/common/flamegraph.test.ts index 5f13d8f9db89b..a5e22e783ce65 100644 --- a/x-pack/plugins/profiling/common/flamegraph.test.ts +++ b/x-pack/plugins/profiling/common/flamegraph.test.ts @@ -12,8 +12,8 @@ import { createBaseFlameGraph, createFlameGraph } from './flamegraph'; import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces'; const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length)); -const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames); -const baseFlamegraph = createBaseFlameGraph(tree, 60); +const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames, 1.0); +const baseFlamegraph = createBaseFlameGraph(tree, 1.0, 60); const flamegraph = createFlameGraph(baseFlamegraph); describe('Flamegraph operations', () => { diff --git a/x-pack/plugins/profiling/common/flamegraph.ts b/x-pack/plugins/profiling/common/flamegraph.ts index ae9adc37679f2..16fb8c1a396c5 100644 --- a/x-pack/plugins/profiling/common/flamegraph.ts +++ b/x-pack/plugins/profiling/common/flamegraph.ts @@ -28,12 +28,18 @@ export interface BaseFlameGraph { CountExclusive: number[]; TotalSeconds: number; + SamplingRate: number; } // createBaseFlameGraph encapsulates the tree representation into a serialized form. -export function createBaseFlameGraph(tree: CalleeTree, totalSeconds: number): BaseFlameGraph { +export function createBaseFlameGraph( + tree: CalleeTree, + samplingRate: number, + totalSeconds: number +): BaseFlameGraph { const graph: BaseFlameGraph = { Size: tree.Size, + SamplingRate: samplingRate, Edges: new Array(tree.Size), FileID: tree.FileID.slice(0, tree.Size), @@ -76,6 +82,7 @@ export interface ElasticFlameGraph extends BaseFlameGraph { export function createFlameGraph(base: BaseFlameGraph): ElasticFlameGraph { const graph: ElasticFlameGraph = { Size: base.Size, + SamplingRate: base.SamplingRate, Edges: base.Edges, FileID: base.FileID, @@ -137,6 +144,7 @@ export function createFlameGraph(base: BaseFlameGraph): ElasticFlameGraph { FunctionOffset: graph.FunctionOffset[i], SourceFilename: graph.SourceFilename[i], SourceLine: graph.SourceLine[i], + SamplingRate: graph.SamplingRate, }); graph.Label[i] = getCalleeLabel(metadata); } diff --git a/x-pack/plugins/profiling/common/functions.test.ts b/x-pack/plugins/profiling/common/functions.test.ts index c17687453c2fd..491fd06ecb2b0 100644 --- a/x-pack/plugins/profiling/common/functions.test.ts +++ b/x-pack/plugins/profiling/common/functions.test.ts @@ -20,7 +20,8 @@ describe('TopN function operations', () => { stackFrames, executables, 0, - maxTopN + maxTopN, + 1.0 ); expect(topNFunctions.TotalCount).toEqual(totalSamples); diff --git a/x-pack/plugins/profiling/common/functions.ts b/x-pack/plugins/profiling/common/functions.ts index 50dd7b3b793d3..6e5f97a6cdbfe 100644 --- a/x-pack/plugins/profiling/common/functions.ts +++ b/x-pack/plugins/profiling/common/functions.ts @@ -35,6 +35,7 @@ type TopNFunction = Pick< export interface TopNFunctions { TotalCount: number; TopN: TopNFunction[]; + SamplingRate: number; } export function createTopNFunctions( @@ -43,7 +44,8 @@ export function createTopNFunctions( stackFrames: Map, executables: Map, startIndex: number, - endIndex: number + endIndex: number, + samplingRate: number ): TopNFunctions { // The `count` associated with a frame provides the total number of // traces in which that node has appeared at least once. However, a @@ -52,12 +54,14 @@ export function createTopNFunctions( // far in each trace. let totalCount = 0; const topNFunctions = new Map(); + // The factor to apply to sampled events to scale the estimated result correctly. + const scalingFactor = 1.0 / samplingRate; // Collect metadata and inclusive + exclusive counts for each distinct frame. for (const [stackTraceID, count] of events) { const uniqueFrameGroupsPerEvent = new Set(); - - totalCount += count; + const scaledCount = count * scalingFactor; + totalCount += scaledCount; // It is possible that we do not have a stacktrace for an event, // e.g. when stopping the host agent or on network errors. @@ -107,12 +111,12 @@ export function createTopNFunctions( if (!uniqueFrameGroupsPerEvent.has(frameGroupID)) { uniqueFrameGroupsPerEvent.add(frameGroupID); - topNFunction.CountInclusive += count; + topNFunction.CountInclusive += scaledCount; } if (i === lenStackTrace - 1) { // Leaf frame: sum up counts for exclusive CPU. - topNFunction.CountExclusive += count; + topNFunction.CountExclusive += scaledCount; } } } @@ -146,10 +150,10 @@ export function createTopNFunctions( CountInclusive: frameAndCount.CountInclusive, Id: frameAndCount.FrameGroupID, })); - return { TotalCount: totalCount, TopN: framesAndCountsAndIds, + SamplingRate: samplingRate, }; } diff --git a/x-pack/plugins/profiling/common/profiling.ts b/x-pack/plugins/profiling/common/profiling.ts index a2e1af7d4ae60..22480a26f8ab2 100644 --- a/x-pack/plugins/profiling/common/profiling.ts +++ b/x-pack/plugins/profiling/common/profiling.ts @@ -159,6 +159,8 @@ export interface StackFrameMetadata { // unused atm due to lack of symbolization metadata SourcePackageURL: string; // unused atm due to lack of symbolization metadata + + SamplingRate: number; } export function createStackFrameMetadata( @@ -181,6 +183,7 @@ export function createStackFrameMetadata( metadata.SourceFilename = options.SourceFilename ?? ''; metadata.SourcePackageHash = options.SourcePackageHash ?? ''; metadata.SourcePackageURL = options.SourcePackageURL ?? ''; + metadata.SamplingRate = options.SamplingRate ?? 1.0; // Unknown/invalid offsets are currently set to 0. // diff --git a/x-pack/plugins/profiling/common/stack_traces.ts b/x-pack/plugins/profiling/common/stack_traces.ts index 5384e60639b9d..2b7ce4f700f29 100644 --- a/x-pack/plugins/profiling/common/stack_traces.ts +++ b/x-pack/plugins/profiling/common/stack_traces.ts @@ -55,6 +55,7 @@ export interface StackTraceResponse { ['stack_frames']?: ProfilingStackFrames; ['executables']?: ProfilingExecutables; ['total_frames']: number; + ['sampling_rate']: number; } export enum StackTracesDisplayOption { diff --git a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx index 9a4f53be00e8c..6f0de5ee500ac 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx @@ -38,6 +38,7 @@ interface Props { comparisonCountExclusive?: number; comparisonTotalSamples?: number; comparisonTotalSeconds?: number; + samplingRate?: number; onShowMoreClick?: () => void; } @@ -54,6 +55,7 @@ export function FlameGraphTooltip({ comparisonCountExclusive, comparisonTotalSamples, comparisonTotalSeconds, + samplingRate, onShowMoreClick, }: Props) { const theme = useEuiTheme(); @@ -78,6 +80,8 @@ export function FlameGraphTooltip({ }) : undefined; + const prependString = samplingRate === 1.0 ? ' ' : '~'; + return ( @@ -100,6 +104,7 @@ export function FlameGraphTooltip({ formatValue={asPercentage} showDifference formatDifferenceAsPercentage + prependValue={prependString} /> )} @@ -132,6 +138,7 @@ export function FlameGraphTooltip({ } showDifference formatDifferenceAsPercentage={false} + prependValue={prependString} /> {onShowMoreClick && ( <> diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx index 1d5ac238ce017..b16da9a6db8e8 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx @@ -176,6 +176,7 @@ export function FlameGraph({ frame={selected} totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0} totalSamples={totalSamples} + samplingRate={primaryFlamegraph?.SamplingRate ?? 1.0} /> )} diff --git a/x-pack/plugins/profiling/public/components/flamegraph/tooltip_row.tsx b/x-pack/plugins/profiling/public/components/flamegraph/tooltip_row.tsx index 5f3511169ecdc..de2054371f5d2 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/tooltip_row.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/tooltip_row.tsx @@ -17,6 +17,7 @@ export function TooltipRow({ formatDifferenceAsPercentage, showDifference, formatValue, + prependValue = '', }: { value: number; label: string | React.ReactElement; @@ -24,8 +25,11 @@ export function TooltipRow({ formatDifferenceAsPercentage: boolean; showDifference: boolean; formatValue?: (value: number) => string; + prependValue?: string; }) { - const valueLabel = formatValue ? formatValue(Math.abs(value)) : value.toString(); + const valueLabel = `${prependValue}${ + formatValue ? formatValue(Math.abs(value)) : value.toString() + }`; const comparisonLabel = formatValue && isNumber(comparison) ? formatValue(comparison) : comparison?.toString(); diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx index fb5478b65aa2e..85a15206166c5 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx @@ -20,11 +20,13 @@ export function getImpactRows({ countExclusive, totalSamples, totalSeconds, + isApproximate = false, }: { countInclusive: number; countExclusive: number; totalSamples: number; totalSeconds: number; + isApproximate: boolean; }) { const { percentage, diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx index 41717078fe3f6..db3fc5d10c9dd 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx @@ -30,9 +30,10 @@ export interface Props { }; totalSamples: number; totalSeconds: number; + samplingRate: number; } -export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Props) { +export function FrameInformationWindow({ frame, totalSamples, totalSeconds, samplingRate }: Props) { const coPilotService = useCoPilot(); const promptParams = useMemo(() => { @@ -84,11 +85,16 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr sourceLine, }); + // Are the results sampled? If yes, prepend a '~'. + const isApproximate = (samplingRate ?? 1.0) === 1.0; + const prependString = isApproximate ? undefined : '~'; + const impactRows = getImpactRows({ countInclusive, countExclusive, totalSamples, totalSeconds, + isApproximate, }); return ( @@ -138,7 +144,7 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr - + diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx index 752be18349be1..4dc6909173e63 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx @@ -10,20 +10,21 @@ import React from 'react'; interface Props { rows: Array<{ label: string | React.ReactNode; value: React.ReactNode }>; + prependString?: string; } -export function KeyValueList({ rows }: Props) { +export function KeyValueList({ rows, prependString = '' }: Props) { return ( {rows.map((row, index) => ( - <> + {row.label}: - {row.value} + {`${prependString}row.value`} @@ -32,7 +33,7 @@ export function KeyValueList({ rows }: Props) { ) : undefined} - + ))} ); diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index d86f5fe62d66f..0b5736c72fe39 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -45,24 +45,36 @@ interface Row { }; } +function getTotalSamplesLabel(samplingRate?: number) { + if (samplingRate === undefined) { + return i18n.translate('xpack.profiling.functionsView.totalSampleCountLabel', { + defaultMessage: 'Total sample estimate:', + }); + } + return i18n.translate('xpack.profiling.functionsView.totalSampleCountLabelWithSamplingRate', { + defaultMessage: 'Total sample (estimate sample rate: {samplingRate}):', + values: { samplingRate }, + }); +} + function TotalSamplesStat({ totalSamples, newSamples, + samplingRateA, + samplingRateB, }: { totalSamples: number; newSamples: number | undefined; + samplingRateA: number; + samplingRateB: number | undefined; }) { const value = totalSamples.toLocaleString(); - const sampleHeader = i18n.translate('xpack.profiling.functionsView.totalSampleCountLabel', { - defaultMessage: ' Total sample estimate: ', - }); - if (newSamples === undefined || newSamples === 0) { return ( {value}} - description={sampleHeader} + description={getTotalSamplesLabel(samplingRateA)} /> ); } @@ -75,10 +87,10 @@ function TotalSamplesStat({ title={ {value} - + } - description={sampleHeader} + description={getTotalSamplesLabel(samplingRateB)} /> ); } @@ -87,12 +99,14 @@ function SampleStat({ samples, diffSamples, totalSamples, + isSampled, }: { samples: number; diffSamples?: number; totalSamples: number; + isSampled: boolean; }) { - const samplesLabel = samples.toLocaleString(); + const samplesLabel = `${isSampled ? '~ ' : ''}${samples.toLocaleString()}`; if (diffSamples === undefined || diffSamples === 0 || totalSamples === 0) { return <>{samplesLabel}; @@ -114,7 +128,7 @@ function SampleStat({ ); } -function CPUStat({ cpu, diffCPU }: { cpu: number; diffCPU?: number }) { +function CPUStat({ cpu, diffCPU }: { cpu: number; diffCPU?: number; isSampled?: boolean }) { const cpuLabel = `${cpu.toFixed(2)}%`; if (diffCPU === undefined || diffCPU === 0) { @@ -162,7 +176,7 @@ export function TopNFunctionsTable({ comparisonScaleFactor, }: Props) { const [selectedRow, setSelectedRow] = useState(); - + const isEstimatedA = (topNFunctions?.SamplingRate ?? 1.0) !== 1.0; const totalCount: number = useMemo(() => { if (!topNFunctions || !topNFunctions.TotalCount) { return 0; @@ -268,7 +282,12 @@ export function TopNFunctionsTable({ }), render: (_, { samples, diff }) => { return ( - + ); }, align: 'right', @@ -394,12 +413,13 @@ export function TopNFunctionsTable({ }, [sortDirection] ).slice(0, 100); - return ( <> @@ -439,6 +459,7 @@ export function TopNFunctionsTable({ }} totalSeconds={totalSeconds ?? 0} totalSamples={selectedRow.samples} + samplingRate={topNFunctions?.SamplingRate ?? 1.0} /> )} diff --git a/x-pack/plugins/profiling/server/routes/flamechart.ts b/x-pack/plugins/profiling/server/routes/flamechart.ts index 3cd0591e63179..3b33267892e0e 100644 --- a/x-pack/plugins/profiling/server/routes/flamechart.ts +++ b/x-pack/plugins/profiling/server/routes/flamechart.ts @@ -49,12 +49,18 @@ export function registerFlameChartSearchRoute({ const totalSeconds = timeTo - timeFrom; const t0 = Date.now(); - const { stackTraceEvents, stackTraces, executables, stackFrames, totalFrames } = - await searchStackTraces({ - client: profilingElasticsearchClient, - filter, - sampleSize: targetSampleSize, - }); + const { + stackTraceEvents, + stackTraces, + executables, + stackFrames, + totalFrames, + samplingRate, + } = await searchStackTraces({ + client: profilingElasticsearchClient, + filter, + sampleSize: targetSampleSize, + }); logger.info(`querying stacktraces took ${Date.now() - t0} ms`); const flamegraph = await withProfilingSpan('create_flamegraph', async () => { @@ -64,12 +70,13 @@ export function registerFlameChartSearchRoute({ stackTraces, stackFrames, executables, - totalFrames + totalFrames, + samplingRate ); logger.info(`creating callee tree took ${Date.now() - t1} ms`); const t2 = Date.now(); - const fg = createBaseFlameGraph(tree, totalSeconds); + const fg = createBaseFlameGraph(tree, samplingRate, totalSeconds); logger.info(`creating flamegraph took ${Date.now() - t2} ms`); return fg; diff --git a/x-pack/plugins/profiling/server/routes/functions.ts b/x-pack/plugins/profiling/server/routes/functions.ts index 59a13a5df4a20..52790c7a9b35b 100644 --- a/x-pack/plugins/profiling/server/routes/functions.ts +++ b/x-pack/plugins/profiling/server/routes/functions.ts @@ -52,13 +52,12 @@ export function registerTopNFunctionsSearchRoute({ }); const t0 = Date.now(); - const { stackTraceEvents, stackTraces, executables, stackFrames } = await searchStackTraces( - { + const { stackTraceEvents, stackTraces, executables, stackFrames, samplingRate } = + await searchStackTraces({ client: profilingElasticsearchClient, filter, sampleSize: targetSampleSize, - } - ); + }); logger.info(`querying stacktraces took ${Date.now() - t0} ms`); const t1 = Date.now(); @@ -69,7 +68,8 @@ export function registerTopNFunctionsSearchRoute({ stackFrames, executables, startIndex, - endIndex + endIndex, + samplingRate ); }); logger.info(`creating topN functions took ${Date.now() - t1} ms`); diff --git a/x-pack/plugins/profiling/server/routes/search_stacktraces.test.ts b/x-pack/plugins/profiling/server/routes/search_stacktraces.test.ts index b8adc472fe9f5..ede7a7a32f10d 100644 --- a/x-pack/plugins/profiling/server/routes/search_stacktraces.test.ts +++ b/x-pack/plugins/profiling/server/routes/search_stacktraces.test.ts @@ -12,6 +12,7 @@ describe('Stack trace response operations', () => { test('empty stack trace response', () => { const original: StackTraceResponse = { total_frames: 0, + sampling_rate: 1.0, }; const expected = { @@ -72,6 +73,7 @@ describe('Stack trace response operations', () => { def: 'def.c', }, total_frames: 1, + sampling_rate: 1.0, }; const expected = { @@ -121,6 +123,7 @@ describe('Stack trace response operations', () => { ['def', { FileName: 'def.c' }], ]), totalFrames: 1, + samplingRate: 1.0, }; const decoded = decodeStackTraceResponse(original); @@ -146,6 +149,7 @@ describe('Stack trace response operations', () => { stack_trace_events: { a: 1, }, + sampling_rate: 1.0, stack_traces: { a: { file_ids: ['abc'], diff --git a/x-pack/plugins/profiling/server/routes/search_stacktraces.ts b/x-pack/plugins/profiling/server/routes/search_stacktraces.ts index c1145ba57a5d1..452b07c526bac 100644 --- a/x-pack/plugins/profiling/server/routes/search_stacktraces.ts +++ b/x-pack/plugins/profiling/server/routes/search_stacktraces.ts @@ -102,6 +102,7 @@ export function decodeStackTraceResponse(response: StackTraceResponse) { stackFrames, executables, totalFrames: response.total_frames, + samplingRate: response.sampling_rate, }; } From 323cd25115531dc7e4fd5c51a5c20ca4572a9072 Mon Sep 17 00:00:00 2001 From: Adam Demjen Date: Thu, 15 Jun 2023 17:32:10 -0400 Subject: [PATCH 2/5] [8.9] Add ESRE landing page sections (#159809) ## Summary This PR continues #159589 and adds 4 sections to the ESRE landing page, with a few caveats - these will be addressed in subsequent PRs: - The accordions only have placeholders for inner content - Multiple accordions can be opened at the same time - Links are missing ![ESRE_landing_page](https://github.com/elastic/kibana/assets/14224983/5237b4ba-3abb-413a-b4be-9a935fe19883) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../esre_guide/esre_docs_section.tsx | 123 ++++++ .../esre/components/esre_guide/esre_guide.tsx | 67 +++- .../esre_guide/esre_guide_accordion.tsx | 68 ++++ .../measure_performance_section.tsx | 85 ++++ .../esre_guide/rank_aggregation_section.tsx | 88 +++++ .../esre_guide/semantic_search_section.tsx | 106 +++++ .../public/assets/images/analytics.svg | 353 +++++++++++++++++ .../public/assets/images/elser.svg | 10 + .../public/assets/images/linear.svg | 9 + .../public/assets/images/nlp.svg | 9 + .../public/assets/images/rrf.svg | 8 + .../public/assets/images/scalable.svg | 367 ++++++++++++++++++ .../public/assets/images/simplify.svg | 289 ++++++++++++++ .../public/assets/images/vector.svg | 11 + 14 files changed, 1592 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_docs_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide_accordion.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/measure_performance_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/rank_aggregation_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/semantic_search_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/analytics.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/elser.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/linear.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/nlp.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/rrf.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/scalable.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/simplify.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/images/vector.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_docs_section.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_docs_section.tsx new file mode 100644 index 0000000000000..b932ed76cd795 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_docs_section.tsx @@ -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 React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +export const EsreDocsSection: React.FC = () => ( + + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+ + + + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+
+ + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+
+ + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide.tsx index 4286c136f2fda..b06759071301a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide.tsx @@ -7,15 +7,37 @@ import React from 'react'; +import { + EuiImage, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiHorizontalRule, + EuiFlexGrid, + useIsWithinBreakpoints, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import analyticsIllustration from '../../../../assets/images/analytics.svg'; +import scalableIllustration from '../../../../assets/images/scalable.svg'; +import simplifyIllustration from '../../../../assets/images/simplify.svg'; import { SetEsreChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { EnterpriseSearchEsrePageTemplate } from '../layout/page_template'; +import { EsreDocsSection } from './esre_docs_section'; +import { MeasurePerformanceSection } from './measure_performance_section'; +import { RankAggregationSection } from './rank_aggregation_section'; +import { SemanticSearchSection } from './semantic_search_section'; + export const EsreGuide: React.FC = () => { + const isMobile = useIsWithinBreakpoints(['xs']); + return ( { }} > -

ESRE placeholder

+ + + + + + + + + + + + + + + + + + + +

+ +

+
+
+ + + + + + + + + + + + + + + +
+
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide_accordion.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide_accordion.tsx new file mode 100644 index 0000000000000..35af242e9e5a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/esre_guide_accordion.tsx @@ -0,0 +1,68 @@ +/* + * 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 { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTitle, + EuiText, + IconType, + EuiPanel, +} from '@elastic/eui'; + +export interface EsreGuideAccordionProps { + id: string; + icon: IconType; + title: string; + description: string; + initialIsOpen?: boolean; +} + +export const EsreGuideAccordion: React.FC = ({ + id, + icon, + title, + description, + initialIsOpen = false, + children, +}) => { + return ( + + + + + + + + + +

{title}

+
+
+ + +

{description}

+
+
+
+
+ + } + > + {children} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/measure_performance_section.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/measure_performance_section.tsx new file mode 100644 index 0000000000000..c3b839b754da8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/measure_performance_section.tsx @@ -0,0 +1,85 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSteps, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const steps: EuiContainedStepProps[] = [ + { + title: i18n.translate('xpack.enterpriseSearch.esre.measurePerformanceSection.step1.title', { + defaultMessage: 'Create a collection', + }), + children: ( + + ), + status: 'incomplete', + }, + { + title: i18n.translate('xpack.enterpriseSearch.esre.measurePerformanceSection.step2.title', { + defaultMessage: 'Integrate the analytics tracker', + }), + children: ( + + ), + status: 'incomplete', + }, + { + title: i18n.translate('xpack.enterpriseSearch.esre.measurePerformanceSection.step3.title', { + defaultMessage: 'Review your dashboard', + }), + children: ( + + ), + status: 'incomplete', + }, +]; + +export const MeasurePerformanceSection: React.FC = () => ( + + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+ + + + + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/rank_aggregation_section.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/rank_aggregation_section.tsx new file mode 100644 index 0000000000000..1e17a1228da20 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/rank_aggregation_section.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import linearCombinationIllustration from '../../../../assets/images/linear.svg'; +import rrfRankingIllustration from '../../../../assets/images/rrf.svg'; + +import { EsreGuideAccordion } from './esre_guide_accordion'; + +export const RankAggregationSection: React.FC = () => ( + + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+ + + + + <> +

Placeholder

+ +
+
+ + + <> +

Placeholder

+ +
+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/semantic_search_section.tsx b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/semantic_search_section.tsx new file mode 100644 index 0000000000000..2aec9fe706a35 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/components/esre_guide/semantic_search_section.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import elserIllustration from '../../../../assets/images/elser.svg'; +import nlpEnrichmentIllustration from '../../../../assets/images/nlp.svg'; +import vectorSearchIllustration from '../../../../assets/images/vector.svg'; + +import { EsreGuideAccordion } from './esre_guide_accordion'; + +export const SemanticSearchSection: React.FC = () => ( + + + + + +

+ +

+
+
+ + +

+ +

+
+
+
+
+ + + + + <> +

Placeholder

+ +
+
+ + + <> +

Placeholder

+ +
+
+ + + <> +

Placeholder

+ +
+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/assets/images/analytics.svg b/x-pack/plugins/enterprise_search/public/assets/images/analytics.svg new file mode 100644 index 0000000000000..11f59ea0f7807 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/analytics.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/elser.svg b/x-pack/plugins/enterprise_search/public/assets/images/elser.svg new file mode 100644 index 0000000000000..fc16f98934e59 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/elser.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/linear.svg b/x-pack/plugins/enterprise_search/public/assets/images/linear.svg new file mode 100644 index 0000000000000..bcedba45d9776 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/linear.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/nlp.svg b/x-pack/plugins/enterprise_search/public/assets/images/nlp.svg new file mode 100644 index 0000000000000..90fa46f16ed8a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/nlp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/rrf.svg b/x-pack/plugins/enterprise_search/public/assets/images/rrf.svg new file mode 100644 index 0000000000000..dbf4b4cea6e88 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/rrf.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/scalable.svg b/x-pack/plugins/enterprise_search/public/assets/images/scalable.svg new file mode 100644 index 0000000000000..343bc813f2ce2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/scalable.svg @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/simplify.svg b/x-pack/plugins/enterprise_search/public/assets/images/simplify.svg new file mode 100644 index 0000000000000..a639ce10dcb0a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/simplify.svg @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/vector.svg b/x-pack/plugins/enterprise_search/public/assets/images/vector.svg new file mode 100644 index 0000000000000..0a25945853b5d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/vector.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + From dd5ac1500906190b67af70fd6af5dc2b706a3f17 Mon Sep 17 00:00:00 2001 From: Luke <11671118+lgestc@users.noreply.github.com> Date: Thu, 15 Jun 2023 23:57:42 +0200 Subject: [PATCH 3/5] Session preview for expandable flyout (#159767) --- ...ert_details_right_panel_overview_tab.cy.ts | 11 +- .../alert_details_right_panel_overview_tab.ts | 4 + .../public/flyout/left/index.tsx | 1 + .../right/components/analyzer_tree.test.tsx | 56 ++++++- .../flyout/right/components/analyzer_tree.tsx | 27 +++- .../components/prevalence_overview.test.tsx | 1 + .../right/components/prevalence_overview.tsx | 3 +- .../right/components/session_preview.test.tsx | 116 ++++++++++++++ .../right/components/session_preview.tsx | 148 ++++++++++++++++++ .../flyout/right/components/test_ids.ts | 5 + .../flyout/right/components/translations.ts | 35 +++++ .../visualizations_section.test.tsx | 13 +- .../components/visualizations_section.tsx | 6 + .../right/hooks/use_process_data.test.tsx | 84 ++++++++++ .../flyout/right/hooks/use_process_data.ts | 58 +++++++ 15 files changed, 556 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/session_preview.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index ba66d5a87068f..faec335d14039 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -41,6 +41,7 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW, } from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; import { clickCorrelationsViewAllButton, @@ -324,11 +325,19 @@ describe( }); describe('visualizations section', () => { - it('should display analyzer preview', () => { + it('should display analyzer and session previews', () => { toggleOverviewTabDescriptionSection(); toggleOverviewTabVisualizationsSection(); + + cy.log('analyzer graph preview'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).scrollIntoView(); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).should('be.visible'); + + cy.log('session view preview'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).should('be.visible'); }); }); } diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts index d3e1fe10d26f2..922d57b9c49fc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -42,6 +42,7 @@ import { MITRE_ATTACK_TITLE_TEST_ID, REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID, + SESSION_PREVIEW_TEST_ID, VISUALIZATIONS_SECTION_HEADER_TEST_ID, } from '../../../public/flyout/right/components/test_ids'; @@ -132,3 +133,6 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE = getDataTestSubjectSelector(ANALYZER_TREE_TEST_ID); + +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW = + getDataTestSubjectSelector(SESSION_PREVIEW_TEST_ID); diff --git a/x-pack/plugins/security_solution/public/flyout/left/index.tsx b/x-pack/plugins/security_solution/public/flyout/left/index.tsx index c92483a0c20aa..8c77babd483df 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/index.tsx @@ -20,6 +20,7 @@ import { useLeftPanelContext } from './context'; export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation'; export const LeftPanelKey: LeftPanelProps['key'] = 'document-details-left'; +export const LeftPanelVisualizeTabPath: LeftPanelProps['path'] = ['visualize']; export const LeftPanelInsightsTabPath: LeftPanelProps['path'] = ['insights']; export const LeftPanelInvestigationTabPath: LeftPanelProps['path'] = ['investigation']; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx index b07fe9b17bc2d..cf05f61ace9eb 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx @@ -11,25 +11,54 @@ import { ANALYZER_TREE_TEST_ID, ANALYZER_TREE_LOADING_TEST_ID, ANALYZER_TREE_ERROR_TEST_ID, + ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID, } from './test_ids'; import { ANALYZER_PREVIEW_TITLE } from './translations'; import * as mock from '../mocks/mock_analyzer_data'; +import type { AnalyzerTreeProps } from './analyzer_tree'; import { AnalyzerTree } from './analyzer_tree'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { TestProviders } from '@kbn/timelines-plugin/public/mock'; +import { RightPanelContext } from '../context'; +import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left'; -const defaultProps = { +const defaultProps: AnalyzerTreeProps = { statsNodes: mock.mockStatsNodes, loading: false, error: false, }; + +const flyoutContextValue = { + openLeftPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + browserFields: {}, + dataFormattedForFieldBrowser: [], +} as unknown as RightPanelContext; + +const renderAnalyzerTree = (children: React.ReactNode) => + render( + + + + {children} + + + + ); + describe('', () => { it('should render the component when data is passed', () => { - const { getByTestId, getByText } = render(); + const { getByTestId, getByText } = renderAnalyzerTree(); expect(getByText(ANALYZER_PREVIEW_TITLE)).toBeInTheDocument(); expect(getByTestId(ANALYZER_TREE_TEST_ID)).toBeInTheDocument(); }); it('should render blank when data is not passed', () => { - const { queryByTestId, queryByText } = render( + const { queryByTestId, queryByText } = renderAnalyzerTree( ); expect(queryByText(ANALYZER_PREVIEW_TITLE)).not.toBeInTheDocument(); @@ -37,13 +66,30 @@ describe('', () => { }); it('should render loading spinner when loading is true', () => { - const { getByTestId } = render(); + const { getByTestId } = renderAnalyzerTree(); expect(getByTestId(ANALYZER_TREE_LOADING_TEST_ID)).toBeInTheDocument(); }); it('should display error message when error is true', () => { - const { getByTestId, getByText } = render(); + const { getByTestId, getByText } = renderAnalyzerTree( + + ); expect(getByText('Unable to display analyzer preview.')).toBeInTheDocument(); expect(getByTestId(ANALYZER_TREE_ERROR_TEST_ID)).toBeInTheDocument(); }); + + it('should navigate to left section Visualize tab when clicking on title', () => { + const { getByTestId } = renderAnalyzerTree(); + + getByTestId(ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelVisualizeTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx index 99d5924083a12..d1c15dc48b492 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiPanel, EuiButtonEmpty, @@ -12,11 +12,15 @@ import { EuiLoadingSpinner, EuiEmptyPrompt, } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useRightPanelContext } from '../context'; +import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left'; import { ANALYZER_PREVIEW_TITLE, ANALYZER_PREVIEW_TEXT } from './translations'; import { ANALYZER_TREE_TEST_ID, ANALYZER_TREE_LOADING_TEST_ID, ANALYZER_TREE_ERROR_TEST_ID, + ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID, } from './test_ids'; import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import { getTreeNodes } from '../utils/analyzer_helpers'; @@ -41,8 +45,22 @@ export interface AnalyzerTreeProps { * Analyzer tree that represent a summary view of analyzer. It shows current process, and its parent and child processes */ export const AnalyzerTree: React.FC = ({ statsNodes, loading, error }) => { + const { eventId, indexName, scopeId } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); const items = useMemo(() => getTreeNodes(statsNodes ?? []), [statsNodes]); + const goToAnalyserTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelVisualizeTabPath, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }, [eventId, openLeftPanel, indexName, scopeId]); + if (loading) { return ; } @@ -63,7 +81,12 @@ export const AnalyzerTree: React.FC = ({ statsNodes, loading, return ( - {}}> + {ANALYZER_PREVIEW_TITLE} diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.test.tsx index fc0e234c52e1b..13f51e47c6a78 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.test.tsx @@ -133,6 +133,7 @@ describe('', () => { params: { id: panelContextValue.eventId, indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, }, }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx index a0480468d328b..77f5065bad450 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx @@ -33,9 +33,10 @@ export const PrevalenceOverview: FC = () => { params: { id: eventId, indexName, + scopeId, }, }); - }, [eventId, openLeftPanel, indexName]); + }, [eventId, openLeftPanel, indexName, scopeId]); const { empty, prevalenceRows } = usePrevalence({ eventId, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.test.tsx new file mode 100644 index 0000000000000..42b8129919782 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { useProcessData } from '../hooks/use_process_data'; +import { SessionPreview } from './session_preview'; +import { TestProviders } from '../../../common/mock'; +import React from 'react'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { RightPanelContext } from '../context'; +import { SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID } from './test_ids'; +import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left'; + +jest.mock('../hooks/use_process_data'); + +const flyoutContextValue = { + openLeftPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + browserFields: {}, + dataFormattedForFieldBrowser: [], +} as unknown as RightPanelContext; + +const renderSessionPreview = () => + render( + + + + + + + + ); + +describe('SessionPreview', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders session preview with all data', () => { + jest.mocked(useProcessData).mockReturnValue({ + processName: 'process1', + userName: 'user1', + startAt: '2022-01-01T00:00:00.000Z', + ruleName: 'rule1', + ruleId: 'id', + workdir: '/path/to/workdir', + command: 'command1', + }); + + renderSessionPreview(); + + expect(screen.getByText('user1')).toBeInTheDocument(); + expect(screen.getByText('started')).toBeInTheDocument(); + expect(screen.getByText('process1')).toBeInTheDocument(); + expect(screen.getByText('at')).toBeInTheDocument(); + expect(screen.getByText('Jan 1, 2022 @ 00:00:00.000')).toBeInTheDocument(); + expect(screen.getByText('with rule')).toBeInTheDocument(); + expect(screen.getByText('rule1')).toBeInTheDocument(); + expect(screen.getByText('by')).toBeInTheDocument(); + expect(screen.getByText('/path/to/workdir command1')).toBeInTheDocument(); + }); + + it('renders session preview without optional data', () => { + jest.mocked(useProcessData).mockReturnValue({ + processName: 'process1', + userName: 'user1', + startAt: null, + ruleName: null, + ruleId: null, + command: null, + workdir: null, + }); + + renderSessionPreview(); + + expect(screen.getByText('user1')).toBeInTheDocument(); + expect(screen.getByText('started')).toBeInTheDocument(); + expect(screen.getByText('process1')).toBeInTheDocument(); + expect(screen.queryByText('at')).not.toBeInTheDocument(); + expect(screen.queryByText('with rule')).not.toBeInTheDocument(); + expect(screen.queryByText('by')).not.toBeInTheDocument(); + }); + + it('should navigate to left section Visualize tab when clicking on title', () => { + jest.mocked(useProcessData).mockReturnValue({ + processName: 'process1', + userName: 'user1', + startAt: '2022-01-01T00:00:00.000Z', + ruleName: 'rule1', + ruleId: 'id', + workdir: '/path/to/workdir', + command: 'command1', + }); + + const { getByTestId } = renderSessionPreview(); + + getByTestId(SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelVisualizeTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx new file mode 100644 index 0000000000000..0d79d2b51f25b --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx @@ -0,0 +1,148 @@ +/* + * 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 { EuiButtonEmpty, EuiCode, EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui'; +import React, { useMemo, type FC, useCallback } from 'react'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; +import { useRightPanelContext } from '../context'; +import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; + +import { useProcessData } from '../hooks/use_process_data'; +import { SESSION_PREVIEW_TEST_ID, SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID } from './test_ids'; +import { + SESSION_PREVIEW_COMMAND_TEXT, + SESSION_PREVIEW_PROCESS_TEXT, + SESSION_PREVIEW_RULE_TEXT, + SESSION_PREVIEW_TIME_TEXT, + SESSION_PREVIEW_TITLE, +} from './translations'; +import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left'; +import { RenderRuleName } from '../../../timelines/components/timeline/body/renderers/formatted_field_helpers'; + +/** + * One-off helper to make sure that inline values are rendered consistently + */ +const ValueContainer: FC<{ text?: string }> = ({ text, children }) => ( + <> + {text && ( + <> +   + {text} +   + + )} + {children} + +); + +/** + * Renders session preview under visualistions section in the flyout right EuiPanel + */ +export const SessionPreview: FC = () => { + const { eventId, indexName, scopeId } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + + const goToSessionViewTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelVisualizeTabPath, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }, [eventId, openLeftPanel, indexName, scopeId]); + + const { processName, userName, startAt, ruleName, ruleId, workdir, command } = useProcessData(); + const { euiTheme } = useEuiTheme(); + + const emphasisStyles = useMemo( + () => ({ fontWeight: euiTheme.font.weight.bold }), + [euiTheme.font.weight.bold] + ); + + const processNameFragment = useMemo(() => { + return ( + processName && ( + + {processName} + + ) + ); + }, [emphasisStyles, processName]); + + const timeFragment = useMemo(() => { + return ( + startAt && ( + + + + ) + ); + }, [startAt]); + + const ruleFragment = useMemo(() => { + return ( + ruleName && + ruleId && ( + + + + ) + ); + }, [ruleName, ruleId, scopeId, eventId]); + + const commandFragment = useMemo(() => { + return ( + command && ( + + + {workdir} {command} + + + ) + ); + }, [command, workdir]); + + return ( + + + + {SESSION_PREVIEW_TITLE} + + +
+ + +   + {userName} + + {processNameFragment} + {timeFragment} + {ruleFragment} + {commandFragment} +
+
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index 002effd64ee30..48d4ba287785b 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -129,5 +129,10 @@ export const VISUALIZATIONS_SECTION_HEADER_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitleHeader'; export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerPreview'; export const ANALYZER_TREE_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTree'; +export const ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID = + 'securitySolutionDocumentDetailsAnalayzerTreeViewDetailsButton'; export const ANALYZER_TREE_LOADING_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeLoading'; export const ANALYZER_TREE_ERROR_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeError'; +export const SESSION_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsSessionPreview'; +export const SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID = + 'securitySolutionDocumentDetailsSessionPreviewViewDetailsButton'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 80b7bf54a1f18..7952c4dbe8847 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -296,3 +296,38 @@ export const INVESTIGATION_GUIDE_TITLE = i18n.translate( defaultMessage: 'Investigation guide', } ); + +export const SESSION_PREVIEW_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.sessionPreview.title', + { + defaultMessage: 'Session viewer preview', + } +); + +export const SESSION_PREVIEW_PROCESS_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.sessionPreview.processText', + { + defaultMessage: 'started', + } +); + +export const SESSION_PREVIEW_TIME_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.sessionPreview.timeText', + { + defaultMessage: 'at', + } +); + +export const SESSION_PREVIEW_RULE_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.sessionPreview.ruleText', + { + defaultMessage: 'with rule', + } +); + +export const SESSION_PREVIEW_COMMAND_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.sessionPreview.commandText', + { + defaultMessage: 'by', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.test.tsx index 2b447967ca80b..d5d3a48340652 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.test.tsx @@ -13,6 +13,7 @@ import { VisualizationsSection } from './visualizations_section'; import { mockContextValue, mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; import { RightPanelContext } from '../context'; import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), @@ -35,10 +36,16 @@ describe('', () => { }); it('should render visualizations component', () => { + const flyoutContextValue = { + openLeftPanel: jest.fn(), + } as unknown as ExpandableFlyoutContext; + const { getByTestId, getAllByRole } = render( - - - + + + + + ); expect(getByTestId(VISUALIZATIONS_SECTION_HEADER_TEST_ID)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.tsx index c6c84a6a6b34d..a8e3e56c0a5cb 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/visualizations_section.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { ExpandableSection } from './expandable_section'; import { VISUALIZATIONS_SECTION_TEST_ID } from './test_ids'; import { VISUALIZATIONS_TITLE } from './translations'; import { AnalyzerPreview } from './analyzer_preview'; +import { SessionPreview } from './session_preview'; export interface VisualizatioinsSectionProps { /** @@ -30,6 +32,10 @@ export const VisualizationsSection: React.FC = ({ title={VISUALIZATIONS_TITLE} data-test-subj={VISUALIZATIONS_SECTION_TEST_ID} > + + + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.test.tsx new file mode 100644 index 0000000000000..ac6c628ed090e --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { getUserDisplayName, useProcessData } from './use_process_data'; +import { renderHook } from '@testing-library/react-hooks'; +import type { FC } from 'react'; +import { RightPanelContext } from '../context'; +import React from 'react'; + +describe('getUserDisplayName', () => { + const getFieldsData = jest.fn(); + + it('should return userName', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'process.entry_leader.user.name') { + return 'userName'; + } + }); + + expect(getUserDisplayName(getFieldsData)).toEqual('userName'); + }); + + it('should return unknown', () => { + getFieldsData.mockImplementation((field: string) => undefined); + + expect(getUserDisplayName(getFieldsData)).toEqual('unknown'); + }); + + it('should return root', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'process.entry_leader.user.name') { + return undefined; + } + if (field === 'process.entry_leader.user.id') { + return '0'; + } + }); + + expect(getUserDisplayName(getFieldsData)).toEqual('root'); + }); + + it('should return uid+userId', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'process.entry_leader.user.name') { + return undefined; + } + if (field === 'process.entry_leader.user.id') { + return 'userId'; + } + }); + + expect(getUserDisplayName(getFieldsData)).toEqual('uid: userId'); + }); +}); + +const panelContextValue = { + getFieldsData: jest.fn().mockReturnValue('test'), +} as unknown as RightPanelContext; + +const ProviderComponent: FC = ({ children }) => ( + {children} +); + +describe('useProcessData', () => { + it('should return values for session preview component', () => { + const hookResult = renderHook(() => useProcessData(), { + wrapper: ProviderComponent, + }); + + expect(hookResult.result.current).toEqual({ + command: 'test', + processName: 'test', + ruleName: 'test', + ruleId: 'test', + startAt: 'test', + userName: 'test', + workdir: 'test', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts new file mode 100644 index 0000000000000..72ca71badaf07 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts @@ -0,0 +1,58 @@ +/* + * 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 { useMemo } from 'react'; +import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; +import { getField } from '../../shared/utils'; +import { useRightPanelContext } from '../context'; + +const FIELD_USER_NAME = 'process.entry_leader.user.name' as const; +const FIELD_USER_ID = 'process.entry_leader.user.id' as const; +const FIELD_PROCESS_NAME = 'process.entry_leader.name' as const; +const FIELD_START_AT = 'process.entry_leader.start' as const; +const FIELD_RULE_NAME = 'kibana.alert.rule.name' as const; +const FIELD_RULE_ID = 'kibana.alert.rule.uuid' as const; +const FIELD_WORKING_DIRECTORY = 'process.group_leader.working_directory' as const; +const FIELD_COMMAND = 'process.command_line' as const; + +/** + * Returns user name with some fallback logic in case it is not available. The idea was borrowed from session viewer + */ +export const getUserDisplayName = (getFieldsData: GetFieldsData): string => { + const userName = getField(getFieldsData(FIELD_USER_NAME)); + const userId = getField(getFieldsData(FIELD_USER_ID)); + + if (userName) { + return userName; + } + + if (!userId) { + return 'unknown'; + } + + return userId === '0' ? 'root' : `uid: ${userId}`; +}; + +/** + * Returns memoized process-related values for the session preview component + */ +export const useProcessData = () => { + const { getFieldsData } = useRightPanelContext(); + + return useMemo( + () => ({ + userName: getUserDisplayName(getFieldsData), + processName: getField(getFieldsData(FIELD_PROCESS_NAME)), + startAt: getField(getFieldsData(FIELD_START_AT)), + ruleName: getField(getFieldsData(FIELD_RULE_NAME)), + ruleId: getField(getFieldsData(FIELD_RULE_ID)), + workdir: getField(getFieldsData(FIELD_WORKING_DIRECTORY)), + command: getField(getFieldsData(FIELD_COMMAND)), + }), + [getFieldsData] + ); +}; From fa98aa4f8c32e2fe110465cd43fe61b7e0e4237a Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 15 Jun 2023 18:35:56 -0400 Subject: [PATCH 4/5] [Security Solution] Enable upload feature flag (#159843) ## Summary Enable upload response action feature flag for the `8.9` release ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../plugins/security_solution/common/experimental_features.ts | 2 +- .../lib/integration_tests/console_commands_definition.test.tsx | 1 + .../response_actions/view/response_actions_list_page.test.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 07d06e4be4e6b..ed9afb7fa9518 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -69,7 +69,7 @@ export const allowedExperimentalValues = Object.freeze({ /** * Enables the `upload` endpoint response action (v8.9) */ - responseActionUploadEnabled: false, + responseActionUploadEnabled: true, /** * Enables top charts on Alerts Page diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx index 4b1d26a6e243f..25211b3ce136c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx @@ -65,6 +65,7 @@ describe('When displaying Endpoint Response Actions', () => { 'suspend-process --pid', 'get-file --path', 'execute --command', + 'upload --file', ]); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx index 5a98765b46d44..ebde74b17be76 100644 --- a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx @@ -423,7 +423,7 @@ describe('Response actions history page', () => { }); expect(history.location.search).toEqual( - '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses%2Cget-file%2Cexecute' + '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses%2Cget-file%2Cexecute%2Cupload' ); }); From f9d16e160be2522825f22b4b312ae8986e77a56c Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Thu, 15 Jun 2023 18:39:21 -0400 Subject: [PATCH 5/5] feat(slo): Show SLI preview chart for custom kql (#159713) --- .../kbn-slo-schema/src/rest_specs/slo.ts | 39 ++++--- .../kbn-slo-schema/src/schema/common.ts | 6 + .../public/data/slo/indicator.ts | 4 +- .../public/hooks/slo/query_key_factory.ts | 3 + .../public/hooks/slo/use_get_preview_data.ts | 62 +++++++++++ .../components/common/data_preview_chart.tsx | 105 ++++++++++++++++++ .../custom_kql_indicator_type_form.tsx | 11 +- .../pages/slo_edit/hooks/use_preview.ts | 29 +++++ .../hooks/use_section_form_validation.ts | 8 +- .../public/pages/slo_edit/slo_edit.test.tsx | 6 + .../observability/server/errors/errors.ts | 1 + .../observability/server/routes/slo/route.ts | 22 ++++ .../server/services/slo/get_preview_data.ts | 68 ++++++++++++ 13 files changed, 335 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts create mode 100644 x-pack/plugins/observability/public/pages/slo_edit/components/common/data_preview_chart.tsx create mode 100644 x-pack/plugins/observability/public/pages/slo_edit/hooks/use_preview.ts create mode 100644 x-pack/plugins/observability/server/services/slo/get_preview_data.ts diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts index f8f1d60edb7ad..9d37c36c94534 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts @@ -6,24 +6,22 @@ */ import * as t from 'io-ts'; - import { budgetingMethodSchema, dateType, historicalSummarySchema, indicatorSchema, indicatorTypesArraySchema, + kqlCustomIndicatorSchema, + metricCustomIndicatorSchema, objectiveSchema, optionalSettingsSchema, + previewDataSchema, settingsSchema, sloIdSchema, summarySchema, tagsSchema, timeWindowSchema, - metricCustomIndicatorSchema, - kqlCustomIndicatorSchema, - apmTransactionErrorRateIndicatorSchema, - apmTransactionDurationIndicatorSchema, } from '../schema'; const createSLOParamsSchema = t.type({ @@ -44,6 +42,14 @@ const createSLOResponseSchema = t.type({ id: sloIdSchema, }); +const getPreviewDataParamsSchema = t.type({ + body: t.type({ + indicator: indicatorSchema, + }), +}); + +const getPreviewDataResponseSchema = t.array(previewDataSchema); + const deleteSLOParamsSchema = t.type({ path: t.type({ id: sloIdSchema, @@ -156,20 +162,22 @@ type FetchHistoricalSummaryParams = t.TypeOf; type HistoricalSummaryResponse = t.OutputOf; +type GetPreviewDataParams = t.TypeOf; +type GetPreviewDataResponse = t.TypeOf; + type BudgetingMethod = t.TypeOf; -type MetricCustomIndicatorSchema = t.TypeOf; -type KQLCustomIndicatorSchema = t.TypeOf; -type APMTransactionErrorRateIndicatorSchema = t.TypeOf< - typeof apmTransactionErrorRateIndicatorSchema ->; -type APMTransactionDurationIndicatorSchema = t.TypeOf; +type Indicator = t.OutputOf; +type MetricCustomIndicator = t.OutputOf; +type KQLCustomIndicator = t.OutputOf; export { createSLOParamsSchema, deleteSLOParamsSchema, findSLOParamsSchema, findSLOResponseSchema, + getPreviewDataParamsSchema, + getPreviewDataResponseSchema, getSLODiagnosisParamsSchema, getSLOParamsSchema, getSLOResponseSchema, @@ -188,6 +196,8 @@ export type { CreateSLOResponse, FindSLOParams, FindSLOResponse, + GetPreviewDataParams, + GetPreviewDataResponse, GetSLOResponse, FetchHistoricalSummaryParams, FetchHistoricalSummaryResponse, @@ -198,8 +208,7 @@ export type { UpdateSLOInput, UpdateSLOParams, UpdateSLOResponse, - MetricCustomIndicatorSchema, - KQLCustomIndicatorSchema, - APMTransactionDurationIndicatorSchema, - APMTransactionErrorRateIndicatorSchema, + Indicator, + MetricCustomIndicator, + KQLCustomIndicator, }; diff --git a/x-pack/packages/kbn-slo-schema/src/schema/common.ts b/x-pack/packages/kbn-slo-schema/src/schema/common.ts index f702540bf20c7..250525ce2192c 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -52,6 +52,11 @@ const historicalSummarySchema = t.intersection([ summarySchema, ]); +const previewDataSchema = t.type({ + date: dateType, + sliValue: t.number, +}); + const dateRangeSchema = t.type({ from: dateType, to: dateType }); export type { SummarySchema }; @@ -63,6 +68,7 @@ export { dateType, errorBudgetSchema, historicalSummarySchema, + previewDataSchema, statusSchema, summarySchema, }; diff --git a/x-pack/plugins/observability/public/data/slo/indicator.ts b/x-pack/plugins/observability/public/data/slo/indicator.ts index 62ff1c0b8287f..227ddf89fc667 100644 --- a/x-pack/plugins/observability/public/data/slo/indicator.ts +++ b/x-pack/plugins/observability/public/data/slo/indicator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KQLCustomIndicatorSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { KQLCustomIndicator, SLOWithSummaryResponse } from '@kbn/slo-schema'; export const buildApmAvailabilityIndicator = ( params: Partial = {} @@ -53,5 +53,5 @@ export const buildCustomKqlIndicator = ( timestampField: '@timestamp', ...params, }, - } as KQLCustomIndicatorSchema; + } as KQLCustomIndicator; }; diff --git a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts index 73bdd9528dd75..f4c557105ce62 100644 --- a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts +++ b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { Indicator } from '@kbn/slo-schema'; + interface SloKeyFilter { name: string; page: number; @@ -31,6 +33,7 @@ export const sloKeys = { historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const, historicalSummary: (sloIds: string[]) => [...sloKeys.historicalSummaries(), sloIds] as const, globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const, + preview: (indicator?: Indicator) => [...sloKeys.all, 'preview', indicator] as const, }; export const compositeSloKeys = { diff --git a/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts b/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts new file mode 100644 index 0000000000000..89d1fe4e5ef8c --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts @@ -0,0 +1,62 @@ +/* + * 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 { GetPreviewDataResponse, Indicator } from '@kbn/slo-schema'; +import { + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useQuery, +} from '@tanstack/react-query'; +import { useKibana } from '../../utils/kibana_react'; +import { sloKeys } from './query_key_factory'; + +export interface UseGetPreviewData { + data: GetPreviewDataResponse | undefined; + isInitialLoading: boolean; + isRefetching: boolean; + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + refetch: ( + options?: (RefetchOptions & RefetchQueryFilters) | undefined + ) => Promise>; +} + +export function useGetPreviewData(indicator?: Indicator): UseGetPreviewData { + const { http } = useKibana().services; + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( + { + queryKey: sloKeys.preview(indicator), + queryFn: async ({ signal }) => { + const response = await http.post( + '/internal/observability/slos/_preview', + { + body: JSON.stringify({ indicator }), + signal, + } + ); + + return response; + }, + retry: false, + refetchOnWindowFocus: false, + enabled: Boolean(indicator), + } + ); + + return { + data, + isLoading, + isRefetching, + isInitialLoading, + isSuccess, + isError, + refetch, + }; +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/data_preview_chart.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/data_preview_chart.tsx new file mode 100644 index 0000000000000..3f99a9f7638b8 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/data_preview_chart.tsx @@ -0,0 +1,105 @@ +/* + * 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 { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexItem, EuiIcon, EuiLoadingChart, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { CreateSLOInput } from '@kbn/slo-schema'; +import moment from 'moment'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useKibana } from '../../../../utils/kibana_react'; +import { useDebouncedGetPreviewData } from '../../hooks/use_preview'; +export function DataPreviewChart() { + const { watch, getFieldState } = useFormContext(); + const { charts, uiSettings } = useKibana().services; + + const { data: previewData, isLoading: isPreviewLoading } = useDebouncedGetPreviewData( + watch('indicator') + ); + + const theme = charts.theme.useChartsTheme(); + const baseTheme = charts.theme.useChartsBaseTheme(); + const dateFormat = uiSettings.get('dateFormat'); + const percentFormat = uiSettings.get('format:percent:defaultPattern'); + + if (getFieldState('indicator').invalid) { + return null; + } + + return ( + + {isPreviewLoading && } + {!isPreviewLoading && !!previewData && ( + + + + } + /> + + numeral(d).format(percentFormat)} + /> + + moment(d).format(dateFormat)} + position={Position.Bottom} + timeAxisLayerCount={2} + gridLine={{ visible: true }} + style={{ + tickLine: { size: 0.0001, padding: 4, visible: true }, + tickLabel: { + alignment: { + horizontal: Position.Left, + vertical: Position.Bottom, + }, + padding: 0, + offset: { x: 0, y: 0 }, + }, + }} + /> + ({ + date: new Date(datum.date).getTime(), + value: datum.sliValue >= 0 ? datum.sliValue : null, + }))} + /> + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index c0b8ee400efb9..4f3ecc301e08a 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -21,6 +21,7 @@ import { Field, useFetchIndexPatternFields, } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; +import { DataPreviewChart } from '../common/data_preview_chart'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; @@ -31,7 +32,6 @@ interface Option { export function CustomKqlIndicatorTypeForm() { const { control, watch, getFieldState } = useFormContext(); - const { isLoading, data: indexFields } = useFetchIndexPatternFields( watch('indicator.params.index') ); @@ -86,12 +86,7 @@ export function CustomKqlIndicatorTypeForm() { !!watch('indicator.params.index') && !!field.value && timestampFields.some((timestampField) => timestampField.name === field.value) - ? [ - { - value: field.value, - label: field.value, - }, - ] + ? [{ value: field.value, label: field.value }] : [] } singleSelection={{ asPlainText: true }} @@ -187,6 +182,8 @@ export function CustomKqlIndicatorTypeForm() { } /> + + ); } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_preview.ts b/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_preview.ts new file mode 100644 index 0000000000000..47d32b8d9ad3f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_preview.ts @@ -0,0 +1,29 @@ +/* + * 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 { Indicator } from '@kbn/slo-schema'; +import { debounce } from 'lodash'; +import { useCallback, useEffect, useState } from 'react'; +import { useGetPreviewData } from '../../../hooks/slo/use_get_preview_data'; + +export function useDebouncedGetPreviewData(indicator: Indicator) { + const serializedIndicator = JSON.stringify(indicator); + const [indicatorState, setIndicatorState] = useState(serializedIndicator); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const store = useCallback( + debounce((value: string) => setIndicatorState(value), 800), + [] + ); + useEffect(() => { + if (indicatorState !== serializedIndicator) { + store(serializedIndicator); + } + }, [indicatorState, serializedIndicator, store]); + + return useGetPreviewData(JSON.parse(indicatorState)); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_section_form_validation.ts b/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_section_form_validation.ts index 1f1eeca035425..4187590b8688b 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_section_form_validation.ts +++ b/x-pack/plugins/observability/public/pages/slo_edit/hooks/use_section_form_validation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateSLOInput, MetricCustomIndicatorSchema } from '@kbn/slo-schema'; +import { CreateSLOInput, MetricCustomIndicator } from '@kbn/slo-schema'; import { FormState, UseFormGetFieldState, UseFormGetValues, UseFormWatch } from 'react-hook-form'; import { isObject } from 'lodash'; @@ -22,9 +22,7 @@ export function useSectionFormValidation({ getFieldState, getValues, formState, switch (watch('indicator.type')) { case 'sli.metric.custom': const isGoodParamsValid = () => { - const data = getValues( - 'indicator.params.good' - ) as MetricCustomIndicatorSchema['params']['good']; + const data = getValues('indicator.params.good') as MetricCustomIndicator['params']['good']; const isEquationValid = !getFieldState('indicator.params.good.equation').invalid; const areMetricsValid = isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field)); @@ -34,7 +32,7 @@ export function useSectionFormValidation({ getFieldState, getValues, formState, const isTotalParamsValid = () => { const data = getValues( 'indicator.params.total' - ) as MetricCustomIndicatorSchema['params']['total']; + ) as MetricCustomIndicator['params']['total']; const isEquationValid = !getFieldState('indicator.params.total.equation').invalid; const areMetricsValid = isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field)); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx index 520f719447db9..4f55f8121579e 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx @@ -66,6 +66,12 @@ const mockKibana = () => { application: { navigateToUrl: mockNavigate, }, + charts: { + theme: { + useChartsTheme: () => {}, + useChartsBaseTheme: () => {}, + }, + }, data: { dataViews: { find: jest.fn().mockReturnValue([]), diff --git a/x-pack/plugins/observability/server/errors/errors.ts b/x-pack/plugins/observability/server/errors/errors.ts index 7ac7290385dde..dbbb873925636 100644 --- a/x-pack/plugins/observability/server/errors/errors.ts +++ b/x-pack/plugins/observability/server/errors/errors.ts @@ -20,6 +20,7 @@ export class SLOIdConflict extends ObservabilityError {} export class CompositeSLONotFound extends ObservabilityError {} export class CompositeSLOIdConflict extends ObservabilityError {} +export class InvalidQueryError extends ObservabilityError {} export class InternalQueryError extends ObservabilityError {} export class NotSupportedError extends ObservabilityError {} export class IllegalArgumentError extends ObservabilityError {} diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index 849630d368aa5..b65216f227407 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -11,6 +11,7 @@ import { deleteSLOParamsSchema, fetchHistoricalSummaryParamsSchema, findSLOParamsSchema, + getPreviewDataParamsSchema, getSLODiagnosisParamsSchema, getSLOParamsSchema, manageSLOParamsSchema, @@ -41,6 +42,7 @@ import type { IndicatorTypes } from '../../domain/models'; import type { ObservabilityRequestHandlerContext } from '../../types'; import { ManageSLO } from '../../services/slo/manage_slo'; import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis'; +import { GetPreviewData } from '../../services/slo/get_preview_data'; const transformGenerators: Record = { 'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(), @@ -303,6 +305,25 @@ const getSloDiagnosisRoute = createObservabilityServerRoute({ }, }); +const getPreviewData = createObservabilityServerRoute({ + endpoint: 'POST /internal/observability/slos/_preview', + options: { + tags: ['access:slo_read'], + }, + params: getPreviewDataParamsSchema, + handler: async ({ context, params }) => { + const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); + + if (!hasCorrectLicense) { + throw badRequest('Platinum license or higher is needed to make use of this feature.'); + } + + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const service = new GetPreviewData(esClient); + return await service.execute(params.body); + }, +}); + export const sloRouteRepository = { ...createSLORoute, ...deleteSLORoute, @@ -314,4 +335,5 @@ export const sloRouteRepository = { ...updateSLORoute, ...getDiagnosisRoute, ...getSloDiagnosisRoute, + ...getPreviewData, }; diff --git a/x-pack/plugins/observability/server/services/slo/get_preview_data.ts b/x-pack/plugins/observability/server/services/slo/get_preview_data.ts new file mode 100644 index 0000000000000..e71d7e6398577 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/get_preview_data.ts @@ -0,0 +1,68 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { GetPreviewDataParams, GetPreviewDataResponse } from '@kbn/slo-schema'; +import { computeSLI } from '../../domain/services'; +import { InvalidQueryError } from '../../errors'; + +export class GetPreviewData { + constructor(private esClient: ElasticsearchClient) {} + + public async execute(params: GetPreviewDataParams): Promise { + switch (params.indicator.type) { + case 'sli.kql.custom': + const filterQuery = getElastichsearchQueryOrThrow(params.indicator.params.filter); + const goodQuery = getElastichsearchQueryOrThrow(params.indicator.params.good); + const totalQuery = getElastichsearchQueryOrThrow(params.indicator.params.total); + const timestampField = params.indicator.params.timestampField; + + try { + const result = await this.esClient.search({ + index: params.indicator.params.index, + query: { + bool: { + filter: [{ range: { [timestampField]: { gte: 'now-60m' } } }, filterQuery], + }, + }, + aggs: { + perMinute: { + date_histogram: { + field: timestampField, + fixed_interval: '1m', + }, + aggs: { + good: { filter: goodQuery }, + total: { filter: totalQuery }, + }, + }, + }, + }); + + // @ts-ignore buckets is not improperly typed + return result.aggregations?.perMinute.buckets.map((bucket) => ({ + date: bucket.key_as_string, + sliValue: computeSLI(bucket.good.doc_count, bucket.total.doc_count), + })); + } catch (err) { + throw new InvalidQueryError(`Invalid ES query`); + } + + default: + return []; + } + } +} + +function getElastichsearchQueryOrThrow(kuery: string) { + try { + return toElasticsearchQuery(fromKueryExpression(kuery)); + } catch (err) { + throw new InvalidQueryError(`Invalid kuery: ${kuery}`); + } +}