[Logs UI] Reorganise log rate anomaly table (#69516)
* Remove top level chart
Remove top level anomalies chart
* Refactor table columns to accomodate new formatting
* Tyical vs actual stats in expanded row
* Format message based on actual vs typical
* Start fleshing out log rate examples endpoint and lib methods
* Use the real document ID for expanded rows so React doesn't re-render content
* Add all data fetching resources for log entry rate examples
* Move log entry example and severity indicator components to a shared location
* Render examples for log rate
* Add severity indicator
* Styling tweaks
* Move horizontal button popover menu to a shared components so log rate table can use it
* Revert "Move horizontal button popover menu to a shared components so log rate table can use it"
This reverts commit f80db5984d
.
* Add "view in stream" and "view in anomaly explorer" links
* Hook links into the new context menu component
* Add log column headers and add styling tweaks etc
* Fix translations
* Tweak comments
* Chart tweaks
* Update x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* PR amendments
- Pass href to context menu items
- Fix start and end times used for example logs
- Use "fewer" rather than "less"
* Update x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* Update x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
* PR amendments
- Fix typechecking
- Add an empty log example column header to account for the context menu
- Add anomaly start time to rows
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
This commit is contained in:
parent
97ca7bfc2e
commit
7ec48fd966
|
@ -8,3 +8,4 @@ export * from './log_entry_categories';
|
|||
export * from './log_entry_category_datasets';
|
||||
export * from './log_entry_category_examples';
|
||||
export * from './log_entry_rate';
|
||||
export * from './log_entry_rate_examples';
|
||||
|
|
|
@ -30,6 +30,7 @@ export type GetLogEntryRateRequestPayload = rt.TypeOf<typeof getLogEntryRateRequ
|
|||
*/
|
||||
|
||||
export const logEntryRateAnomalyRT = rt.type({
|
||||
id: rt.string,
|
||||
actualLogEntryRate: rt.number,
|
||||
anomalyScore: rt.number,
|
||||
duration: rt.number,
|
||||
|
@ -37,6 +38,8 @@ export const logEntryRateAnomalyRT = rt.type({
|
|||
typicalLogEntryRate: rt.number,
|
||||
});
|
||||
|
||||
export type LogEntryRateAnomaly = rt.TypeOf<typeof logEntryRateAnomalyRT>;
|
||||
|
||||
export const logEntryRatePartitionRT = rt.type({
|
||||
analysisBucketCount: rt.number,
|
||||
anomalies: rt.array(logEntryRateAnomalyRT),
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
import {
|
||||
badRequestErrorRT,
|
||||
forbiddenErrorRT,
|
||||
timeRangeRT,
|
||||
routeTimingMetadataRT,
|
||||
} from '../../shared';
|
||||
|
||||
export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH =
|
||||
'/api/infra/log_analysis/results/log_entry_rate_examples';
|
||||
|
||||
/**
|
||||
* request
|
||||
*/
|
||||
|
||||
export const getLogEntryRateExamplesRequestPayloadRT = rt.type({
|
||||
data: rt.type({
|
||||
// the dataset to fetch the log rate examples from
|
||||
dataset: rt.string,
|
||||
// the number of examples to fetch
|
||||
exampleCount: rt.number,
|
||||
// the id of the source configuration
|
||||
sourceId: rt.string,
|
||||
// the time range to fetch the log rate examples from
|
||||
timeRange: timeRangeRT,
|
||||
}),
|
||||
});
|
||||
|
||||
export type GetLogEntryRateExamplesRequestPayload = rt.TypeOf<
|
||||
typeof getLogEntryRateExamplesRequestPayloadRT
|
||||
>;
|
||||
|
||||
/**
|
||||
* response
|
||||
*/
|
||||
|
||||
const logEntryRateExampleRT = rt.type({
|
||||
id: rt.string,
|
||||
dataset: rt.string,
|
||||
message: rt.string,
|
||||
timestamp: rt.number,
|
||||
tiebreaker: rt.number,
|
||||
});
|
||||
|
||||
export type LogEntryRateExample = rt.TypeOf<typeof logEntryRateExampleRT>;
|
||||
|
||||
export const getLogEntryRateExamplesSuccessReponsePayloadRT = rt.intersection([
|
||||
rt.type({
|
||||
data: rt.type({
|
||||
examples: rt.array(logEntryRateExampleRT),
|
||||
}),
|
||||
}),
|
||||
rt.partial({
|
||||
timing: routeTimingMetadataRT,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GetLogEntryRateExamplesSuccessReponsePayload = rt.TypeOf<
|
||||
typeof getLogEntryRateExamplesSuccessReponsePayloadRT
|
||||
>;
|
||||
|
||||
export const getLogEntryRateExamplesResponsePayloadRT = rt.union([
|
||||
getLogEntryRateExamplesSuccessReponsePayloadRT,
|
||||
badRequestErrorRT,
|
||||
forbiddenErrorRT,
|
||||
]);
|
||||
|
||||
export type GetLogEntryRateExamplesResponsePayload = rt.TypeOf<
|
||||
typeof getLogEntryRateExamplesResponsePayloadRT
|
||||
>;
|
|
@ -10,7 +10,7 @@ import {
|
|||
formatAnomalyScore,
|
||||
getSeverityCategoryForScore,
|
||||
ML_SEVERITY_COLORS,
|
||||
} from '../../../../../../common/log_analysis';
|
||||
} from '../../../../common/log_analysis';
|
||||
|
||||
export const AnomalySeverityIndicator: React.FunctionComponent<{
|
||||
anomalyScore: number;
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import { LogEntryExampleMessagesEmptyIndicator } from './log_entry_examples_empty_indicator';
|
||||
import { LogEntryExampleMessagesFailureIndicator } from './log_entry_examples_failure_indicator';
|
||||
import { LogEntryExampleMessagesLoadingIndicator } from './log_entry_examples_loading_indicator';
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
hasFailedLoading: boolean;
|
||||
hasResults: boolean;
|
||||
exampleCount: number;
|
||||
onReload: () => void;
|
||||
}
|
||||
export const LogEntryExampleMessages: React.FunctionComponent<Props> = ({
|
||||
isLoading,
|
||||
hasFailedLoading,
|
||||
exampleCount,
|
||||
hasResults,
|
||||
onReload,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
{isLoading ? (
|
||||
<LogEntryExampleMessagesLoadingIndicator exampleCount={exampleCount} />
|
||||
) : hasFailedLoading ? (
|
||||
<LogEntryExampleMessagesFailureIndicator onRetry={onReload} />
|
||||
) : !hasResults ? (
|
||||
<LogEntryExampleMessagesEmptyIndicator onReload={onReload} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = euiStyled.div`
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
flex: 1 0 0%;
|
||||
overflow: hidden;
|
||||
`;
|
|
@ -7,20 +7,20 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
export const CategoryExampleMessagesEmptyIndicator: React.FunctionComponent<{
|
||||
export const LogEntryExampleMessagesEmptyIndicator: React.FunctionComponent<{
|
||||
onReload: () => void;
|
||||
}> = ({ onReload }) => (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false} className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.logEntryCategories.exampleEmptyDescription"
|
||||
id="xpack.infra.logs.logEntryExamples.exampleEmptyDescription"
|
||||
defaultMessage="No examples found within the selected time range. Increase the log entry retention period to improve message sample availability."
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={onReload} size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.logEntryCategories.exampleEmptyReloadButtonLabel"
|
||||
id="xpack.infra.logs.logEntryExamples.exampleEmptyReloadButtonLabel"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
|
@ -7,22 +7,22 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
export const CategoryExampleMessagesFailureIndicator: React.FunctionComponent<{
|
||||
export const LogEntryExampleMessagesFailureIndicator: React.FunctionComponent<{
|
||||
onRetry: () => void;
|
||||
}> = ({ onRetry }) => (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false} className="eui-textNoWrap">
|
||||
<EuiTextColor color="danger">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.logEntryCategories.exampleLoadingFailureDescription"
|
||||
defaultMessage="Failed to load category examples."
|
||||
id="xpack.infra.logs.logEntryExamples.exampleLoadingFailureDescription"
|
||||
defaultMessage="Failed to load examples."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={onRetry} size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.logEntryCategories.exampleLoadingFailureRetryButtonLabel"
|
||||
id="xpack.infra.logs.logEntryExamples.exampleLoadingFailureRetryButtonLabel"
|
||||
defaultMessage="Retry"
|
||||
/>
|
||||
</EuiButton>
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiLoadingContent } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const CategoryExampleMessagesLoadingIndicator: React.FunctionComponent<{
|
||||
export const LogEntryExampleMessagesLoadingIndicator: React.FunctionComponent<{
|
||||
exampleCount: number;
|
||||
}> = ({ exampleCount }) => (
|
||||
<>
|
|
@ -68,7 +68,7 @@ export const LogColumnHeaders: React.FunctionComponent<{
|
|||
);
|
||||
};
|
||||
|
||||
const LogColumnHeader: React.FunctionComponent<{
|
||||
export const LogColumnHeader: React.FunctionComponent<{
|
||||
columnWidth: LogEntryColumnWidth;
|
||||
'data-test-subj'?: string;
|
||||
}> = ({ children, columnWidth, 'data-test-subj': dataTestSubj }) => (
|
||||
|
@ -77,7 +77,7 @@ const LogColumnHeader: React.FunctionComponent<{
|
|||
</LogColumnHeaderWrapper>
|
||||
);
|
||||
|
||||
const LogColumnHeadersWrapper = euiStyled.div.attrs((props) => ({
|
||||
export const LogColumnHeadersWrapper = euiStyled.div.attrs((props) => ({
|
||||
role: props.role ?? 'row',
|
||||
}))`
|
||||
align-items: stretch;
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LogEntryColumn, LogEntryColumnWidths, useColumnWidths } from './log_entry_column';
|
||||
export {
|
||||
LogEntryColumn,
|
||||
LogEntryColumnWidths,
|
||||
useColumnWidths,
|
||||
iconColumnId,
|
||||
} from './log_entry_column';
|
||||
export { LogEntryFieldColumn } from './log_entry_field_column';
|
||||
export { LogEntryMessageColumn } from './log_entry_message_column';
|
||||
export { LogEntryRowWrapper } from './log_entry_row';
|
||||
export { LogEntryTimestampColumn } from './log_entry_timestamp_column';
|
||||
export { ScrollableLogTextStreamView } from './scrollable_log_text_stream_view';
|
||||
export { LogEntryContextMenu } from './log_entry_context_menu';
|
||||
|
|
|
@ -13,7 +13,8 @@ import { LogEntryColumnContent } from './log_entry_column';
|
|||
|
||||
interface LogEntryContextMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface LogEntryContextMenuProps {
|
||||
|
@ -40,9 +41,9 @@ export const LogEntryContextMenu: React.FC<LogEntryContextMenuProps> = ({
|
|||
}) => {
|
||||
const closeMenuAndCall = useMemo(() => {
|
||||
return (callback: LogEntryContextMenuItem['onClick']) => {
|
||||
return () => {
|
||||
return (e: React.MouseEvent) => {
|
||||
onClose();
|
||||
callback();
|
||||
callback(e);
|
||||
};
|
||||
};
|
||||
}, [onClose]);
|
||||
|
@ -60,7 +61,7 @@ export const LogEntryContextMenu: React.FC<LogEntryContextMenuProps> = ({
|
|||
|
||||
const wrappedItems = useMemo(() => {
|
||||
return items.map((item, i) => (
|
||||
<EuiContextMenuItem key={i} onClick={closeMenuAndCall(item.onClick)}>
|
||||
<EuiContextMenuItem key={i} onClick={closeMenuAndCall(item.onClick)} href={item.href}>
|
||||
{item.label}
|
||||
</EuiContextMenuItem>
|
||||
));
|
||||
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
|
||||
import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis';
|
||||
import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis';
|
||||
import { AnomalySeverityIndicator } from './anomaly_severity_indicator';
|
||||
import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator';
|
||||
|
||||
export const AnomalySeverityIndicatorList: React.FunctionComponent<{
|
||||
datasets: LogEntryCategoryDataset[];
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared';
|
||||
import { useLogEntryCategoryExamples } from '../../use_log_entry_category_examples';
|
||||
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared';
|
||||
import { CategoryExampleMessage } from './category_example_message';
|
||||
import { CategoryExampleMessagesEmptyIndicator } from './category_example_messages_empty_indicator';
|
||||
import { CategoryExampleMessagesFailureIndicator } from './category_example_messages_failure_indicator';
|
||||
import { CategoryExampleMessagesLoadingIndicator } from './category_example_messages_loading_indicator';
|
||||
|
||||
const exampleCount = 5;
|
||||
|
||||
|
@ -39,30 +35,21 @@ export const CategoryDetailsRow: React.FunctionComponent<{
|
|||
}, [getLogEntryCategoryExamples]);
|
||||
|
||||
return (
|
||||
<CategoryExampleMessages>
|
||||
{isLoadingLogEntryCategoryExamples ? (
|
||||
<CategoryExampleMessagesLoadingIndicator exampleCount={exampleCount} />
|
||||
) : hasFailedLoadingLogEntryCategoryExamples ? (
|
||||
<CategoryExampleMessagesFailureIndicator onRetry={getLogEntryCategoryExamples} />
|
||||
) : logEntryCategoryExamples.length === 0 ? (
|
||||
<CategoryExampleMessagesEmptyIndicator onReload={getLogEntryCategoryExamples} />
|
||||
) : (
|
||||
logEntryCategoryExamples.map((categoryExample, categoryExampleIndex) => (
|
||||
<CategoryExampleMessage
|
||||
dataset={categoryExample.dataset}
|
||||
key={categoryExampleIndex}
|
||||
message={categoryExample.message}
|
||||
timestamp={categoryExample.timestamp}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CategoryExampleMessages>
|
||||
<LogEntryExampleMessages
|
||||
isLoading={isLoadingLogEntryCategoryExamples}
|
||||
hasFailedLoading={hasFailedLoadingLogEntryCategoryExamples}
|
||||
hasResults={logEntryCategoryExamples.length > 0}
|
||||
exampleCount={exampleCount}
|
||||
onReload={getLogEntryCategoryExamples}
|
||||
>
|
||||
{logEntryCategoryExamples.map((example, exampleIndex) => (
|
||||
<CategoryExampleMessage
|
||||
key={exampleIndex}
|
||||
dataset={example.dataset}
|
||||
message={example.message}
|
||||
timestamp={example.timestamp}
|
||||
/>
|
||||
))}
|
||||
</LogEntryExampleMessages>
|
||||
);
|
||||
};
|
||||
|
||||
const CategoryExampleMessages = euiStyled.div`
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
flex: 1 0 0%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
|
|
@ -4,86 +4,129 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiStat } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import React from 'react';
|
||||
import { useMount } from 'react-use';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
|
||||
import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
|
||||
import { LogEntryRateResults } from '../../use_log_entry_rate_results';
|
||||
import {
|
||||
getAnnotationsForPartition,
|
||||
getLogEntryRateSeriesForPartition,
|
||||
getTotalNumberOfLogEntriesForPartition,
|
||||
} from '../helpers/data_formatters';
|
||||
import { AnomaliesChart } from './chart';
|
||||
import { AnomalyRecord } from '../../use_log_entry_rate_results';
|
||||
import { useLogEntryRateModuleContext } from '../../use_log_entry_rate_module';
|
||||
import { useLogEntryRateExamples } from '../../use_log_entry_rate_examples';
|
||||
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
|
||||
import { LogEntryRateExampleMessage, LogEntryRateExampleMessageHeaders } from './log_entry_example';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
|
||||
const EXAMPLE_COUNT = 5;
|
||||
|
||||
const examplesTitle = i18n.translate('xpack.infra.logs.analysis.anomaliesTableExamplesTitle', {
|
||||
defaultMessage: 'Example log entries',
|
||||
});
|
||||
|
||||
export const AnomaliesTableExpandedRow: React.FunctionComponent<{
|
||||
partitionId: string;
|
||||
results: LogEntryRateResults;
|
||||
setTimeRange: (timeRange: TimeRange) => void;
|
||||
anomaly: AnomalyRecord;
|
||||
timeRange: TimeRange;
|
||||
jobId: string;
|
||||
}> = ({ results, timeRange, setTimeRange, partitionId, jobId }) => {
|
||||
const logEntryRateSeries = useMemo(
|
||||
() =>
|
||||
results?.histogramBuckets ? getLogEntryRateSeriesForPartition(results, partitionId) : [],
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[results, partitionId]
|
||||
);
|
||||
const anomalyAnnotations = useMemo(
|
||||
() =>
|
||||
results?.histogramBuckets
|
||||
? getAnnotationsForPartition(results, partitionId)
|
||||
: {
|
||||
warning: [],
|
||||
minor: [],
|
||||
major: [],
|
||||
critical: [],
|
||||
},
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[results, partitionId]
|
||||
);
|
||||
const totalNumberOfLogEntries = useMemo(
|
||||
() =>
|
||||
results?.histogramBuckets
|
||||
? getTotalNumberOfLogEntriesForPartition(results, partitionId)
|
||||
: undefined,
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[results, partitionId]
|
||||
);
|
||||
}> = ({ anomaly, timeRange, jobId }) => {
|
||||
const {
|
||||
sourceConfiguration: { sourceId },
|
||||
} = useLogEntryRateModuleContext();
|
||||
|
||||
const {
|
||||
getLogEntryRateExamples,
|
||||
hasFailedLoadingLogEntryRateExamples,
|
||||
isLoadingLogEntryRateExamples,
|
||||
logEntryRateExamples,
|
||||
} = useLogEntryRateExamples({
|
||||
dataset: anomaly.partitionId,
|
||||
endTime: anomaly.startTime + anomaly.duration,
|
||||
exampleCount: EXAMPLE_COUNT,
|
||||
sourceId,
|
||||
startTime: anomaly.startTime,
|
||||
});
|
||||
|
||||
useMount(() => {
|
||||
getLogEntryRateExamples();
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={8}>
|
||||
<AnomaliesChart
|
||||
chartId={`${partitionId}-anomalies`}
|
||||
timeRange={timeRange}
|
||||
setTimeRange={setTimeRange}
|
||||
series={logEntryRateSeries}
|
||||
annotations={anomalyAnnotations}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiStat
|
||||
title={numeral(totalNumberOfLogEntries).format('0.00a')}
|
||||
titleSize="m"
|
||||
description={i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesExpandedRowNumberOfLogEntriesDescription',
|
||||
{
|
||||
defaultMessage: 'Number of log entries',
|
||||
}
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnalyzeInMlButton jobId={jobId} timeRange={timeRange} partition={partitionId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
<ExpandedContentWrapper direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>{examplesTitle}</h3>
|
||||
</EuiTitle>
|
||||
<LogEntryExampleMessages
|
||||
isLoading={isLoadingLogEntryRateExamples}
|
||||
hasFailedLoading={hasFailedLoadingLogEntryRateExamples}
|
||||
hasResults={logEntryRateExamples.length > 0}
|
||||
exampleCount={EXAMPLE_COUNT}
|
||||
onReload={getLogEntryRateExamples}
|
||||
>
|
||||
{logEntryRateExamples.length > 0 ? (
|
||||
<>
|
||||
<LogEntryRateExampleMessageHeaders dateTime={logEntryRateExamples[0].timestamp} />
|
||||
{logEntryRateExamples.map((example, exampleIndex) => (
|
||||
<LogEntryRateExampleMessage
|
||||
key={exampleIndex}
|
||||
id={example.id}
|
||||
dataset={example.dataset}
|
||||
message={example.message}
|
||||
timestamp={example.timestamp}
|
||||
tiebreaker={example.tiebreaker}
|
||||
timeRange={timeRange}
|
||||
jobId={jobId}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
</LogEntryExampleMessages>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={`${numeral(anomaly.typicalLogEntryRate).format('0.00a')} ${i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle',
|
||||
{
|
||||
defaultMessage: '{typicalCount, plural, one {message} other {messages}}',
|
||||
values: { typicalCount: anomaly.typicalLogEntryRate },
|
||||
}
|
||||
)}`}
|
||||
description={i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription',
|
||||
{
|
||||
defaultMessage: 'Typical',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={`${numeral(anomaly.actualLogEntryRate).format('0.00a')} ${i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle',
|
||||
{
|
||||
defaultMessage: '{actualCount, plural, one {message} other {messages}}',
|
||||
values: { actualCount: anomaly.actualLogEntryRate },
|
||||
}
|
||||
)}`}
|
||||
description={i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesExpandedRowActualRateDescription',
|
||||
{
|
||||
defaultMessage: 'Actual',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</ExpandedContentWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ExpandedContentWrapper = euiStyled(EuiFlexGroup)`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
|
|
@ -9,23 +9,15 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiStat,
|
||||
EuiTitle,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { LogEntryRateResults } from '../../use_log_entry_rate_results';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
|
||||
import { formatAnomalyScore } from '../../../../../../common/log_analysis';
|
||||
import {
|
||||
getAnnotationsForAll,
|
||||
getLogEntryRateCombinedSeries,
|
||||
getTopAnomalyScoreAcrossAllPartitions,
|
||||
} from '../helpers/data_formatters';
|
||||
import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters';
|
||||
import { AnomaliesChart } from './chart';
|
||||
import { AnomaliesTable } from './table';
|
||||
import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status';
|
||||
|
@ -67,14 +59,6 @@ export const AnomaliesResults: React.FunctionComponent<{
|
|||
[results]
|
||||
);
|
||||
|
||||
const topAnomalyScore = useMemo(
|
||||
() =>
|
||||
results && results.histogramBuckets
|
||||
? getTopAnomalyScoreAcrossAllPartitions(results)
|
||||
: undefined,
|
||||
[results]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
|
@ -124,7 +108,7 @@ export const AnomaliesResults: React.FunctionComponent<{
|
|||
) : (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={8}>
|
||||
<EuiFlexItem>
|
||||
<AnomaliesChart
|
||||
chartId="overall"
|
||||
setTimeRange={setTimeRange}
|
||||
|
@ -134,30 +118,6 @@ export const AnomaliesResults: React.FunctionComponent<{
|
|||
renderAnnotationTooltip={renderAnnotationTooltip}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiStat
|
||||
title={numeral(results.totalNumberOfLogEntries).format('0.00a')}
|
||||
titleSize="m"
|
||||
description={i18n.translate(
|
||||
'xpack.infra.logs.analysis.overallAnomaliesNumberOfLogEntriesDescription',
|
||||
{
|
||||
defaultMessage: 'Number of log entries',
|
||||
}
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
<EuiStat
|
||||
title={topAnomalyScore ? formatAnomalyScore(topAnomalyScore) : null}
|
||||
titleSize="m"
|
||||
description={i18n.translate(
|
||||
'xpack.infra.logs.analysis.overallAnomaliesTopAnomalyScoreDescription',
|
||||
{
|
||||
defaultMessage: 'Max anomaly score',
|
||||
}
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<AnomaliesTable
|
||||
|
@ -177,6 +137,13 @@ const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle',
|
|||
defaultMessage: 'Anomalies',
|
||||
});
|
||||
|
||||
const loadingAriaLabel = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
|
||||
{ defaultMessage: 'Loading anomalies' }
|
||||
);
|
||||
|
||||
const LoadingOverlayContent = () => <EuiLoadingSpinner size="xl" aria-label={loadingAriaLabel} />;
|
||||
|
||||
interface ParsedAnnotationDetails {
|
||||
anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>;
|
||||
}
|
||||
|
@ -222,10 +189,3 @@ const renderAnnotationTooltip = (details?: string) => {
|
|||
const TooltipWrapper = euiStyled('div')`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const loadingAriaLabel = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
|
||||
{ defaultMessage: 'Loading anomalies' }
|
||||
);
|
||||
|
||||
const LoadingOverlayContent = () => <EuiLoadingSpinner size="xl" aria-label={loadingAriaLabel} />;
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { encode } from 'rison-node';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis';
|
||||
import {
|
||||
LogEntryColumn,
|
||||
LogEntryFieldColumn,
|
||||
LogEntryMessageColumn,
|
||||
LogEntryRowWrapper,
|
||||
LogEntryTimestampColumn,
|
||||
LogEntryContextMenu,
|
||||
LogEntryColumnWidths,
|
||||
iconColumnId,
|
||||
} from '../../../../../components/logging/log_text_stream';
|
||||
import {
|
||||
LogColumnHeadersWrapper,
|
||||
LogColumnHeader,
|
||||
} from '../../../../../components/logging/log_text_stream/column_headers';
|
||||
import { useLinkProps } from '../../../../../hooks/use_link_props';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
|
||||
import { partitionField } from '../../../../../../common/log_analysis/job_parameters';
|
||||
import { getEntitySpecificSingleMetricViewerLink } from '../../../../../components/logging/log_analysis_results/analyze_in_ml_button';
|
||||
import { LogEntryRateExample } from '../../../../../../common/http_api/log_analysis/results';
|
||||
import {
|
||||
LogColumnConfiguration,
|
||||
isTimestampLogColumnConfiguration,
|
||||
isFieldLogColumnConfiguration,
|
||||
isMessageLogColumnConfiguration,
|
||||
} from '../../../../../utils/source_configuration';
|
||||
import { localizedDate } from '../../../../../../common/formatters/datetime';
|
||||
|
||||
export const exampleMessageScale = 'medium' as const;
|
||||
export const exampleTimestampFormat = 'time' as const;
|
||||
|
||||
const MENU_LABEL = i18n.translate('xpack.infra.logAnomalies.logEntryExamplesMenuLabel', {
|
||||
defaultMessage: 'View actions for log entry',
|
||||
});
|
||||
|
||||
const VIEW_IN_STREAM_LABEL = i18n.translate(
|
||||
'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel',
|
||||
{
|
||||
defaultMessage: 'View in stream',
|
||||
}
|
||||
);
|
||||
|
||||
const VIEW_ANOMALY_IN_ML_LABEL = i18n.translate(
|
||||
'xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel',
|
||||
{
|
||||
defaultMessage: 'View anomaly in machine learning',
|
||||
}
|
||||
);
|
||||
|
||||
type Props = LogEntryRateExample & {
|
||||
timeRange: TimeRange;
|
||||
jobId: string;
|
||||
};
|
||||
|
||||
export const LogEntryRateExampleMessage: React.FunctionComponent<Props> = ({
|
||||
id,
|
||||
dataset,
|
||||
message,
|
||||
timestamp,
|
||||
tiebreaker,
|
||||
timeRange,
|
||||
jobId,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const openMenu = useCallback(() => setIsMenuOpen(true), []);
|
||||
const closeMenu = useCallback(() => setIsMenuOpen(false), []);
|
||||
const setItemIsHovered = useCallback(() => setIsHovered(true), []);
|
||||
const setItemIsNotHovered = useCallback(() => setIsHovered(false), []);
|
||||
|
||||
// the dataset must be encoded for the field column and the empty value must
|
||||
// be turned into a user-friendly value
|
||||
const encodedDatasetFieldValue = useMemo(
|
||||
() => JSON.stringify(getFriendlyNameForPartitionId(dataset)),
|
||||
[dataset]
|
||||
);
|
||||
|
||||
const viewInStreamLinkProps = useLinkProps({
|
||||
app: 'logs',
|
||||
pathname: 'stream',
|
||||
search: {
|
||||
logPosition: encode({
|
||||
end: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
|
||||
position: { tiebreaker, time: timestamp },
|
||||
start: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
|
||||
streamLive: false,
|
||||
}),
|
||||
flyoutOptions: encode({
|
||||
surroundingLogsId: id,
|
||||
}),
|
||||
logFilter: encode({
|
||||
expression: `${partitionField}: ${dataset}`,
|
||||
kind: 'kuery',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const viewAnomalyInMachineLearningLinkProps = useLinkProps(
|
||||
getEntitySpecificSingleMetricViewerLink(jobId, timeRange, {
|
||||
[partitionField]: dataset,
|
||||
})
|
||||
);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
if (!viewInStreamLinkProps.onClick || !viewAnomalyInMachineLearningLinkProps.onClick) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: VIEW_IN_STREAM_LABEL,
|
||||
onClick: viewInStreamLinkProps.onClick,
|
||||
href: viewInStreamLinkProps.href,
|
||||
},
|
||||
{
|
||||
label: VIEW_ANOMALY_IN_ML_LABEL,
|
||||
onClick: viewAnomalyInMachineLearningLinkProps.onClick,
|
||||
href: viewAnomalyInMachineLearningLinkProps.href,
|
||||
},
|
||||
];
|
||||
}, [viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]);
|
||||
|
||||
return (
|
||||
<LogEntryRowWrapper
|
||||
scale={exampleMessageScale}
|
||||
onMouseEnter={setItemIsHovered}
|
||||
onMouseLeave={setItemIsNotHovered}
|
||||
>
|
||||
<LogEntryColumn {...columnWidths[timestampColumnId]}>
|
||||
<LogEntryTimestampColumn format={exampleTimestampFormat} time={timestamp} />
|
||||
</LogEntryColumn>
|
||||
<LogEntryColumn {...columnWidths[messageColumnId]}>
|
||||
<LogEntryMessageColumn
|
||||
columnValue={{
|
||||
columnId: messageColumnId,
|
||||
message: [{ field: 'message', value: message, highlights: [] }],
|
||||
}}
|
||||
highlights={noHighlights}
|
||||
isActiveHighlight={false}
|
||||
wrapMode="none"
|
||||
/>
|
||||
</LogEntryColumn>
|
||||
<LogEntryColumn {...columnWidths[datasetColumnId]}>
|
||||
<LogEntryFieldColumn
|
||||
columnValue={{
|
||||
columnId: datasetColumnId,
|
||||
field: 'event.dataset',
|
||||
value: encodedDatasetFieldValue,
|
||||
highlights: [],
|
||||
}}
|
||||
highlights={noHighlights}
|
||||
isActiveHighlight={false}
|
||||
wrapMode="none"
|
||||
/>
|
||||
</LogEntryColumn>
|
||||
<LogEntryColumn
|
||||
key="logColumn iconLogColumn iconLogColumn:details"
|
||||
{...columnWidths[iconColumnId]}
|
||||
>
|
||||
{(isHovered || isMenuOpen) && menuItems ? (
|
||||
<LogEntryContextMenu
|
||||
aria-label={MENU_LABEL}
|
||||
isOpen={isMenuOpen}
|
||||
onOpen={openMenu}
|
||||
onClose={closeMenu}
|
||||
items={menuItems}
|
||||
/>
|
||||
) : null}
|
||||
</LogEntryColumn>
|
||||
</LogEntryRowWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const noHighlights: never[] = [];
|
||||
const timestampColumnId = 'log-entry-example-timestamp-column' as const;
|
||||
const messageColumnId = 'log-entry-examples-message-column' as const;
|
||||
const datasetColumnId = 'log-entry-examples-dataset-column' as const;
|
||||
|
||||
const DETAIL_FLYOUT_ICON_MIN_WIDTH = 32;
|
||||
const COLUMN_PADDING = 8;
|
||||
|
||||
export const columnWidths: LogEntryColumnWidths = {
|
||||
[timestampColumnId]: {
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
// w_score - w_padding = 130 px - 8 px
|
||||
baseWidth: '122px',
|
||||
},
|
||||
[messageColumnId]: {
|
||||
growWeight: 1,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: '0%',
|
||||
},
|
||||
[datasetColumnId]: {
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: '250px',
|
||||
},
|
||||
[iconColumnId]: {
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${DETAIL_FLYOUT_ICON_MIN_WIDTH + 2 * COLUMN_PADDING}px`,
|
||||
},
|
||||
};
|
||||
|
||||
export const exampleMessageColumnConfigurations: LogColumnConfiguration[] = [
|
||||
{
|
||||
timestampColumn: {
|
||||
id: timestampColumnId,
|
||||
},
|
||||
},
|
||||
{
|
||||
messageColumn: {
|
||||
id: messageColumnId,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldColumn: {
|
||||
field: 'event.dataset',
|
||||
id: datasetColumnId,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const LogEntryRateExampleMessageHeaders: React.FunctionComponent<{
|
||||
dateTime: number;
|
||||
}> = ({ dateTime }) => {
|
||||
return (
|
||||
<LogEntryRateExampleMessageHeadersWrapper>
|
||||
<>
|
||||
{exampleMessageColumnConfigurations.map((columnConfiguration) => {
|
||||
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
key={columnConfiguration.timestampColumn.id}
|
||||
columnWidth={columnWidths[columnConfiguration.timestampColumn.id]}
|
||||
data-test-subj="logColumnHeader timestampLogColumnHeader"
|
||||
>
|
||||
{localizedDate(dateTime)}
|
||||
</LogColumnHeader>
|
||||
);
|
||||
} else if (isMessageLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidths[columnConfiguration.messageColumn.id]}
|
||||
data-test-subj="logColumnHeader messageLogColumnHeader"
|
||||
key={columnConfiguration.messageColumn.id}
|
||||
>
|
||||
Message
|
||||
</LogColumnHeader>
|
||||
);
|
||||
} else if (isFieldLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidths[columnConfiguration.fieldColumn.id]}
|
||||
data-test-subj="logColumnHeader fieldLogColumnHeader"
|
||||
key={columnConfiguration.fieldColumn.id}
|
||||
>
|
||||
{columnConfiguration.fieldColumn.field}
|
||||
</LogColumnHeader>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidths[iconColumnId]}
|
||||
data-test-subj="logColumnHeader contextMenuLogColumnHeader"
|
||||
key={'icon-column-header'}
|
||||
>
|
||||
{null}
|
||||
</LogColumnHeader>
|
||||
</>
|
||||
</LogEntryRateExampleMessageHeadersWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const LogEntryRateExampleMessageHeadersWrapper = euiStyled(LogColumnHeadersWrapper)`
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
padding-right: 0;
|
||||
`;
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSet } from 'react-use';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
|
||||
import {
|
||||
formatAnomalyScore,
|
||||
|
@ -18,11 +18,16 @@ import {
|
|||
import { RowExpansionButton } from '../../../../../components/basic_table';
|
||||
import { LogEntryRateResults } from '../../use_log_entry_rate_results';
|
||||
import { AnomaliesTableExpandedRow } from './expanded_row';
|
||||
import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator';
|
||||
import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';
|
||||
|
||||
interface TableItem {
|
||||
partitionName: string;
|
||||
partitionId: string;
|
||||
topAnomalyScore: number;
|
||||
id: string;
|
||||
dataset: string;
|
||||
datasetName: string;
|
||||
anomalyScore: number;
|
||||
anomalyMessage: string;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
interface SortingOptions {
|
||||
|
@ -32,73 +37,132 @@ interface SortingOptions {
|
|||
};
|
||||
}
|
||||
|
||||
const partitionColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTablePartitionColumnName',
|
||||
interface PaginationOptions {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
totalItemCount: number;
|
||||
pageSizeOptions: number[];
|
||||
hidePerPageOptions: boolean;
|
||||
}
|
||||
|
||||
const anomalyScoreColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableAnomalyScoreColumnName',
|
||||
{
|
||||
defaultMessage: 'Partition',
|
||||
defaultMessage: 'Anomaly score',
|
||||
}
|
||||
);
|
||||
|
||||
const maxAnomalyScoreColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName',
|
||||
const anomalyMessageColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableAnomalyMessageName',
|
||||
{
|
||||
defaultMessage: 'Max anomaly score',
|
||||
defaultMessage: 'Anomaly',
|
||||
}
|
||||
);
|
||||
|
||||
const anomalyStartTimeColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableAnomalyStartTime',
|
||||
{
|
||||
defaultMessage: 'Start time',
|
||||
}
|
||||
);
|
||||
|
||||
const datasetColumnName = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName',
|
||||
{
|
||||
defaultMessage: 'Dataset',
|
||||
}
|
||||
);
|
||||
|
||||
const moreThanExpectedAnomalyMessage = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage',
|
||||
{
|
||||
defaultMessage: 'More log messages in this dataset than expected',
|
||||
}
|
||||
);
|
||||
|
||||
const fewerThanExpectedAnomalyMessage = i18n.translate(
|
||||
'xpack.infra.logs.analysis.anomaliesTableFewerThanExpectedAnomalyMessage',
|
||||
{
|
||||
defaultMessage: 'Fewer log messages in this dataset than expected',
|
||||
}
|
||||
);
|
||||
|
||||
const getAnomalyMessage = (actualRate: number, typicalRate: number): string => {
|
||||
return actualRate < typicalRate
|
||||
? fewerThanExpectedAnomalyMessage
|
||||
: moreThanExpectedAnomalyMessage;
|
||||
};
|
||||
|
||||
export const AnomaliesTable: React.FunctionComponent<{
|
||||
results: LogEntryRateResults;
|
||||
setTimeRange: (timeRange: TimeRange) => void;
|
||||
timeRange: TimeRange;
|
||||
jobId: string;
|
||||
}> = ({ results, timeRange, setTimeRange, jobId }) => {
|
||||
const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss');
|
||||
|
||||
const tableItems: TableItem[] = useMemo(() => {
|
||||
return Object.entries(results.partitionBuckets).map(([key, value]) => {
|
||||
return results.anomalies.map((anomaly) => {
|
||||
return {
|
||||
// The real ID
|
||||
partitionId: key,
|
||||
// Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here
|
||||
partitionName: getFriendlyNameForPartitionId(key),
|
||||
topAnomalyScore: formatAnomalyScore(value.topAnomalyScore),
|
||||
id: anomaly.id,
|
||||
dataset: anomaly.partitionId,
|
||||
datasetName: getFriendlyNameForPartitionId(anomaly.partitionId),
|
||||
anomalyScore: formatAnomalyScore(anomaly.anomalyScore),
|
||||
anomalyMessage: getAnomalyMessage(anomaly.actualLogEntryRate, anomaly.typicalLogEntryRate),
|
||||
startTime: anomaly.startTime,
|
||||
};
|
||||
});
|
||||
}, [results]);
|
||||
|
||||
const [expandedDatasetIds, { add: expandDataset, remove: collapseDataset }] = useSet<string>(
|
||||
new Set()
|
||||
);
|
||||
const [expandedIds, { add: expandId, remove: collapseId }] = useSet<string>(new Set());
|
||||
|
||||
const expandedDatasetRowContents = useMemo(
|
||||
() =>
|
||||
[...expandedDatasetIds].reduce<Record<string, React.ReactNode>>(
|
||||
(aggregatedDatasetRows, datasetId) => {
|
||||
return {
|
||||
...aggregatedDatasetRows,
|
||||
[getFriendlyNameForPartitionId(datasetId)]: (
|
||||
<AnomaliesTableExpandedRow
|
||||
partitionId={datasetId}
|
||||
results={results}
|
||||
setTimeRange={setTimeRange}
|
||||
timeRange={timeRange}
|
||||
jobId={jobId}
|
||||
/>
|
||||
),
|
||||
};
|
||||
},
|
||||
{}
|
||||
),
|
||||
[expandedDatasetIds, jobId, results, setTimeRange, timeRange]
|
||||
[...expandedIds].reduce<Record<string, React.ReactNode>>((aggregatedDatasetRows, id) => {
|
||||
const anomaly = results.anomalies.find((_anomaly) => _anomaly.id === id);
|
||||
|
||||
return {
|
||||
...aggregatedDatasetRows,
|
||||
[id]: anomaly ? (
|
||||
<AnomaliesTableExpandedRow anomaly={anomaly} timeRange={timeRange} jobId={jobId} />
|
||||
) : null,
|
||||
};
|
||||
}, {}),
|
||||
[expandedIds, results, timeRange, jobId]
|
||||
);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingOptions>({
|
||||
sort: {
|
||||
field: 'topAnomalyScore',
|
||||
field: 'anomalyScore',
|
||||
direction: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
const [_pagination, setPagination] = useState<PaginationOptions>({
|
||||
pageIndex: 0,
|
||||
pageSize: 20,
|
||||
totalItemCount: results.anomalies.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
});
|
||||
|
||||
const paginationOptions = useMemo(() => {
|
||||
return {
|
||||
..._pagination,
|
||||
totalItemCount: results.anomalies.length,
|
||||
};
|
||||
}, [_pagination, results]);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
({ sort = {} }) => {
|
||||
({ page = {}, sort = {} }) => {
|
||||
const { index, size } = page;
|
||||
setPagination((currentPagination) => {
|
||||
return {
|
||||
...currentPagination,
|
||||
pageIndex: index,
|
||||
pageSize: size,
|
||||
};
|
||||
});
|
||||
const { field, direction } = sort;
|
||||
setSorting({
|
||||
sort: {
|
||||
|
@ -107,33 +171,58 @@ export const AnomaliesTable: React.FunctionComponent<{
|
|||
},
|
||||
});
|
||||
},
|
||||
[setSorting]
|
||||
[setSorting, setPagination]
|
||||
);
|
||||
|
||||
const sortedTableItems = useMemo(() => {
|
||||
let sortedItems: TableItem[] = [];
|
||||
if (sorting.sort.field === 'partitionName') {
|
||||
sortedItems = tableItems.sort((a, b) => (a.partitionId > b.partitionId ? 1 : -1));
|
||||
} else if (sorting.sort.field === 'topAnomalyScore') {
|
||||
sortedItems = tableItems.sort((a, b) => a.topAnomalyScore - b.topAnomalyScore);
|
||||
if (sorting.sort.field === 'datasetName') {
|
||||
sortedItems = tableItems.sort((a, b) => (a.datasetName > b.datasetName ? 1 : -1));
|
||||
} else if (sorting.sort.field === 'anomalyScore') {
|
||||
sortedItems = tableItems.sort((a, b) => a.anomalyScore - b.anomalyScore);
|
||||
} else if (sorting.sort.field === 'startTime') {
|
||||
sortedItems = tableItems.sort((a, b) => a.startTime - b.startTime);
|
||||
}
|
||||
|
||||
return sorting.sort.direction === 'asc' ? sortedItems : sortedItems.reverse();
|
||||
}, [tableItems, sorting]);
|
||||
|
||||
const pageOfItems: TableItem[] = useMemo(() => {
|
||||
const { pageIndex, pageSize } = paginationOptions;
|
||||
return sortedTableItems.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
|
||||
}, [paginationOptions, sortedTableItems]);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<TableItem>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'partitionName',
|
||||
name: partitionColumnName,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'topAnomalyScore',
|
||||
name: maxAnomalyScoreColumnName,
|
||||
field: 'anomalyScore',
|
||||
name: anomalyScoreColumnName,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
dataType: 'number' as const,
|
||||
width: '130px',
|
||||
render: (anomalyScore: number) => <AnomalySeverityIndicator anomalyScore={anomalyScore} />,
|
||||
},
|
||||
{
|
||||
field: 'anomalyMessage',
|
||||
name: anomalyMessageColumnName,
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
name: anomalyStartTimeColumnName,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '230px',
|
||||
render: (startTime: number) => moment(startTime).format(dateFormat),
|
||||
},
|
||||
{
|
||||
field: 'datasetName',
|
||||
name: datasetColumnName,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '200px',
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
|
@ -141,33 +230,28 @@ export const AnomaliesTable: React.FunctionComponent<{
|
|||
isExpander: true,
|
||||
render: (item: TableItem) => (
|
||||
<RowExpansionButton
|
||||
isExpanded={expandedDatasetIds.has(item.partitionId)}
|
||||
item={item.partitionId}
|
||||
onExpand={expandDataset}
|
||||
onCollapse={collapseDataset}
|
||||
isExpanded={expandedIds.has(item.id)}
|
||||
item={item.id}
|
||||
onExpand={expandId}
|
||||
onCollapse={collapseId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[collapseDataset, expandDataset, expandedDatasetIds]
|
||||
[collapseId, expandId, expandedIds, dateFormat]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledEuiBasicTable
|
||||
items={sortedTableItems}
|
||||
itemId="partitionName"
|
||||
<EuiBasicTable
|
||||
items={pageOfItems}
|
||||
itemId="id"
|
||||
itemIdToExpandedRowMap={expandedDatasetRowContents}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
columns={columns}
|
||||
pagination={paginationOptions}
|
||||
sorting={sorting}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledEuiBasicTable: typeof EuiBasicTable = euiStyled(EuiBasicTable as any)`
|
||||
& .euiTable {
|
||||
table-layout: auto;
|
||||
}
|
||||
` as any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { npStart } from '../../../../legacy_singletons';
|
||||
|
||||
import {
|
||||
getLogEntryRateExamplesRequestPayloadRT,
|
||||
getLogEntryRateExamplesSuccessReponsePayloadRT,
|
||||
LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
|
||||
} from '../../../../../common/http_api/log_analysis';
|
||||
import { createPlainError, throwErrors } from '../../../../../common/runtime_types';
|
||||
|
||||
export const callGetLogEntryRateExamplesAPI = async (
|
||||
sourceId: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
dataset: string,
|
||||
exampleCount: number
|
||||
) => {
|
||||
const response = await npStart.http.fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
getLogEntryRateExamplesRequestPayloadRT.encode({
|
||||
data: {
|
||||
dataset,
|
||||
exampleCount,
|
||||
sourceId,
|
||||
timeRange: {
|
||||
startTime,
|
||||
endTime,
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
return pipe(
|
||||
getLogEntryRateExamplesSuccessReponsePayloadRT.decode(response),
|
||||
fold(throwErrors(createPlainError), identity)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { LogEntryRateExample } from '../../../../common/http_api';
|
||||
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
|
||||
import { callGetLogEntryRateExamplesAPI } from './service_calls/get_log_entry_rate_examples';
|
||||
|
||||
export const useLogEntryRateExamples = ({
|
||||
dataset,
|
||||
endTime,
|
||||
exampleCount,
|
||||
sourceId,
|
||||
startTime,
|
||||
}: {
|
||||
dataset: string;
|
||||
endTime: number;
|
||||
exampleCount: number;
|
||||
sourceId: string;
|
||||
startTime: number;
|
||||
}) => {
|
||||
const [logEntryRateExamples, setLogEntryRateExamples] = useState<LogEntryRateExample[]>([]);
|
||||
|
||||
const [getLogEntryRateExamplesRequest, getLogEntryRateExamples] = useTrackedPromise(
|
||||
{
|
||||
cancelPreviousOn: 'creation',
|
||||
createPromise: async () => {
|
||||
return await callGetLogEntryRateExamplesAPI(
|
||||
sourceId,
|
||||
startTime,
|
||||
endTime,
|
||||
dataset,
|
||||
exampleCount
|
||||
);
|
||||
},
|
||||
onResolve: ({ data: { examples } }) => {
|
||||
setLogEntryRateExamples(examples);
|
||||
},
|
||||
},
|
||||
[dataset, endTime, exampleCount, sourceId, startTime]
|
||||
);
|
||||
|
||||
const isLoadingLogEntryRateExamples = useMemo(
|
||||
() => getLogEntryRateExamplesRequest.state === 'pending',
|
||||
[getLogEntryRateExamplesRequest.state]
|
||||
);
|
||||
|
||||
const hasFailedLoadingLogEntryRateExamples = useMemo(
|
||||
() => getLogEntryRateExamplesRequest.state === 'rejected',
|
||||
[getLogEntryRateExamplesRequest.state]
|
||||
);
|
||||
|
||||
return {
|
||||
getLogEntryRateExamples,
|
||||
hasFailedLoadingLogEntryRateExamples,
|
||||
isLoadingLogEntryRateExamples,
|
||||
logEntryRateExamples,
|
||||
};
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
GetLogEntryRateSuccessResponsePayload,
|
||||
LogEntryRateHistogramBucket,
|
||||
LogEntryRatePartition,
|
||||
LogEntryRateAnomaly,
|
||||
} from '../../../../common/http_api/log_analysis';
|
||||
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
|
||||
import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate';
|
||||
|
@ -23,11 +24,16 @@ type PartitionRecord = Record<
|
|||
{ buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number }
|
||||
>;
|
||||
|
||||
export type AnomalyRecord = LogEntryRateAnomaly & {
|
||||
partitionId: string;
|
||||
};
|
||||
|
||||
export interface LogEntryRateResults {
|
||||
bucketDuration: number;
|
||||
totalNumberOfLogEntries: number;
|
||||
histogramBuckets: LogEntryRateHistogramBucket[];
|
||||
partitionBuckets: PartitionRecord;
|
||||
anomalies: AnomalyRecord[];
|
||||
}
|
||||
|
||||
export const useLogEntryRateResults = ({
|
||||
|
@ -55,6 +61,7 @@ export const useLogEntryRateResults = ({
|
|||
totalNumberOfLogEntries: data.totalNumberOfLogEntries,
|
||||
histogramBuckets: data.histogramBuckets,
|
||||
partitionBuckets: formatLogEntryRateResultsByPartition(data),
|
||||
anomalies: formatLogEntryRateResultsByAllAnomalies(data),
|
||||
});
|
||||
},
|
||||
onReject: () => {
|
||||
|
@ -117,3 +124,23 @@ const formatLogEntryRateResultsByPartition = (
|
|||
|
||||
return resultsByPartition;
|
||||
};
|
||||
|
||||
const formatLogEntryRateResultsByAllAnomalies = (
|
||||
results: GetLogEntryRateSuccessResponsePayload['data']
|
||||
): AnomalyRecord[] => {
|
||||
return results.histogramBuckets.reduce<AnomalyRecord[]>((anomalies, bucket) => {
|
||||
return bucket.partitions.reduce<AnomalyRecord[]>((_anomalies, partition) => {
|
||||
if (partition.anomalies.length > 0) {
|
||||
partition.anomalies.forEach((anomaly) => {
|
||||
_anomalies.push({
|
||||
partitionId: partition.partitionId,
|
||||
...anomaly,
|
||||
});
|
||||
});
|
||||
return _anomalies;
|
||||
} else {
|
||||
return _anomalies;
|
||||
}
|
||||
}, anomalies);
|
||||
}, []);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
initGetLogEntryCategoryDatasetsRoute,
|
||||
initGetLogEntryCategoryExamplesRoute,
|
||||
initGetLogEntryRateRoute,
|
||||
initGetLogEntryRateExamplesRoute,
|
||||
initValidateLogAnalysisDatasetsRoute,
|
||||
initValidateLogAnalysisIndicesRoute,
|
||||
} from './routes/log_analysis';
|
||||
|
@ -56,6 +57,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
|
|||
initValidateLogAnalysisDatasetsRoute(libs);
|
||||
initValidateLogAnalysisIndicesRoute(libs);
|
||||
initLogEntriesRoute(libs);
|
||||
initGetLogEntryRateExamplesRoute(libs);
|
||||
initLogEntriesHighlightsRoute(libs);
|
||||
initLogEntriesSummaryRoute(libs);
|
||||
initLogEntriesSummaryHighlightsRoute(libs);
|
||||
|
|
|
@ -7,16 +7,30 @@
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { getJobId } from '../../../common/log_analysis';
|
||||
import { RequestHandlerContext } from 'src/core/server';
|
||||
import { throwErrors, createPlainError } from '../../../common/runtime_types';
|
||||
import { NoLogAnalysisResultsIndexError } from './errors';
|
||||
import {
|
||||
logRateModelPlotResponseRT,
|
||||
createLogEntryRateQuery,
|
||||
LogRateModelPlotBucket,
|
||||
CompositeTimestampPartitionKey,
|
||||
} from './queries';
|
||||
import { MlSystem } from '../../types';
|
||||
import { startTracingSpan } from '../../../common/performance_tracing';
|
||||
import { decodeOrThrow } from '../../../common/runtime_types';
|
||||
import { getJobId, jobCustomSettingsRT } from '../../../common/log_analysis';
|
||||
import {
|
||||
createLogEntryRateExamplesQuery,
|
||||
logEntryRateExamplesResponseRT,
|
||||
} from './queries/log_entry_rate_examples';
|
||||
import {
|
||||
InsufficientLogAnalysisMlJobConfigurationError,
|
||||
NoLogAnalysisMlJobError,
|
||||
NoLogAnalysisResultsIndexError,
|
||||
} from './errors';
|
||||
import { InfraSource } from '../sources';
|
||||
import type { MlSystem } from '../../types';
|
||||
import { InfraRequestHandlerContext } from '../../types';
|
||||
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
|
||||
|
||||
const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
|
||||
|
||||
|
@ -73,6 +87,7 @@ export async function getLogEntryRateBuckets(
|
|||
partitions: Array<{
|
||||
analysisBucketCount: number;
|
||||
anomalies: Array<{
|
||||
id: string;
|
||||
actualLogEntryRate: number;
|
||||
anomalyScore: number;
|
||||
duration: number;
|
||||
|
@ -91,7 +106,8 @@ export async function getLogEntryRateBuckets(
|
|||
const partition = {
|
||||
analysisBucketCount: timestampPartitionBucket.filter_model_plot.doc_count,
|
||||
anomalies: timestampPartitionBucket.filter_records.top_hits_record.hits.hits.map(
|
||||
({ _source: record }) => ({
|
||||
({ _id, _source: record }) => ({
|
||||
id: _id,
|
||||
actualLogEntryRate: record.actual[0],
|
||||
anomalyScore: record.record_score,
|
||||
duration: record.bucket_span * 1000,
|
||||
|
@ -127,3 +143,130 @@ export async function getLogEntryRateBuckets(
|
|||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export async function getLogEntryRateExamples(
|
||||
context: RequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||
sourceId: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
dataset: string,
|
||||
exampleCount: number,
|
||||
sourceConfiguration: InfraSource,
|
||||
callWithRequest: KibanaFramework['callWithRequest']
|
||||
) {
|
||||
const finalizeLogEntryRateExamplesSpan = startTracingSpan(
|
||||
'get log entry rate example log entries'
|
||||
);
|
||||
|
||||
const jobId = getJobId(context.infra.spaceId, sourceId, 'log-entry-rate');
|
||||
|
||||
const {
|
||||
mlJob,
|
||||
timing: { spans: fetchMlJobSpans },
|
||||
} = await fetchMlJob(context, jobId);
|
||||
|
||||
const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings);
|
||||
const indices = customSettings?.logs_source_config?.indexPattern;
|
||||
const timestampField = customSettings?.logs_source_config?.timestampField;
|
||||
const tiebreakerField = sourceConfiguration.configuration.fields.tiebreaker;
|
||||
|
||||
if (indices == null || timestampField == null) {
|
||||
throw new InsufficientLogAnalysisMlJobConfigurationError(
|
||||
`Failed to find index configuration for ml job ${jobId}`
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
examples,
|
||||
timing: { spans: fetchLogEntryRateExamplesSpans },
|
||||
} = await fetchLogEntryRateExamples(
|
||||
context,
|
||||
indices,
|
||||
timestampField,
|
||||
tiebreakerField,
|
||||
startTime,
|
||||
endTime,
|
||||
dataset,
|
||||
exampleCount,
|
||||
callWithRequest
|
||||
);
|
||||
|
||||
const logEntryRateExamplesSpan = finalizeLogEntryRateExamplesSpan();
|
||||
|
||||
return {
|
||||
data: examples,
|
||||
timing: {
|
||||
spans: [logEntryRateExamplesSpan, ...fetchMlJobSpans, ...fetchLogEntryRateExamplesSpans],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchLogEntryRateExamples(
|
||||
context: RequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||
indices: string,
|
||||
timestampField: string,
|
||||
tiebreakerField: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
dataset: string,
|
||||
exampleCount: number,
|
||||
callWithRequest: KibanaFramework['callWithRequest']
|
||||
) {
|
||||
const finalizeEsSearchSpan = startTracingSpan('Fetch log rate examples from ES');
|
||||
|
||||
const {
|
||||
hits: { hits },
|
||||
} = decodeOrThrow(logEntryRateExamplesResponseRT)(
|
||||
await callWithRequest(
|
||||
context,
|
||||
'search',
|
||||
createLogEntryRateExamplesQuery(
|
||||
indices,
|
||||
timestampField,
|
||||
tiebreakerField,
|
||||
startTime,
|
||||
endTime,
|
||||
dataset,
|
||||
exampleCount
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const esSearchSpan = finalizeEsSearchSpan();
|
||||
|
||||
return {
|
||||
examples: hits.map((hit) => ({
|
||||
id: hit._id,
|
||||
dataset,
|
||||
message: hit._source.message ?? '',
|
||||
timestamp: hit.sort[0],
|
||||
tiebreaker: hit.sort[1],
|
||||
})),
|
||||
timing: {
|
||||
spans: [esSearchSpan],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchMlJob(
|
||||
context: RequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||
logEntryRateJobId: string
|
||||
) {
|
||||
const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
|
||||
const {
|
||||
jobs: [mlJob],
|
||||
} = await context.infra.mlAnomalyDetectors.jobs(logEntryRateJobId);
|
||||
|
||||
const mlGetJobSpan = finalizeMlGetJobSpan();
|
||||
|
||||
if (mlJob == null) {
|
||||
throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryRateJobId}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
mlJob,
|
||||
timing: {
|
||||
spans: [mlGetJobSpan],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@ export const logRateModelPlotBucketRT = rt.type({
|
|||
hits: rt.type({
|
||||
hits: rt.array(
|
||||
rt.type({
|
||||
_id: rt.string,
|
||||
_source: logRateMlRecordRT,
|
||||
})
|
||||
),
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
||||
import { defaultRequestParameters } from './common';
|
||||
import { partitionField } from '../../../../common/log_analysis';
|
||||
|
||||
export const createLogEntryRateExamplesQuery = (
|
||||
indices: string,
|
||||
timestampField: string,
|
||||
tiebreakerField: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
dataset: string,
|
||||
exampleCount: number
|
||||
) => ({
|
||||
...defaultRequestParameters,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
[timestampField]: {
|
||||
gte: startTime,
|
||||
lte: endTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
[partitionField]: dataset,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [{ [timestampField]: 'asc' }, { [tiebreakerField]: 'asc' }],
|
||||
},
|
||||
_source: ['event.dataset', 'message'],
|
||||
index: indices,
|
||||
size: exampleCount,
|
||||
});
|
||||
|
||||
export const logEntryRateExampleHitRT = rt.type({
|
||||
_id: rt.string,
|
||||
_source: rt.partial({
|
||||
event: rt.partial({
|
||||
dataset: rt.string,
|
||||
}),
|
||||
message: rt.string,
|
||||
}),
|
||||
sort: rt.tuple([rt.number, rt.number]),
|
||||
});
|
||||
|
||||
export type LogEntryRateExampleHit = rt.TypeOf<typeof logEntryRateExampleHitRT>;
|
||||
|
||||
export const logEntryRateExamplesResponseRT = rt.intersection([
|
||||
commonSearchSuccessResponseFieldsRT,
|
||||
rt.type({
|
||||
hits: rt.type({
|
||||
hits: rt.array(logEntryRateExampleHitRT),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type LogEntryRateExamplesResponse = rt.TypeOf<typeof logEntryRateExamplesResponseRT>;
|
|
@ -8,3 +8,4 @@ export * from './log_entry_categories';
|
|||
export * from './log_entry_category_datasets';
|
||||
export * from './log_entry_category_examples';
|
||||
export * from './log_entry_rate';
|
||||
export * from './log_entry_rate_examples';
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { createValidationFunction } from '../../../../common/runtime_types';
|
||||
import { InfraBackendLibs } from '../../../lib/infra_types';
|
||||
import { NoLogAnalysisResultsIndexError, getLogEntryRateExamples } from '../../../lib/log_analysis';
|
||||
import { assertHasInfraMlPlugins } from '../../../utils/request_context';
|
||||
import {
|
||||
getLogEntryRateExamplesRequestPayloadRT,
|
||||
getLogEntryRateExamplesSuccessReponsePayloadRT,
|
||||
LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
|
||||
} from '../../../../common/http_api/log_analysis';
|
||||
|
||||
export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBackendLibs) => {
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'post',
|
||||
path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
|
||||
validate: {
|
||||
body: createValidationFunction(getLogEntryRateExamplesRequestPayloadRT),
|
||||
},
|
||||
},
|
||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||
const {
|
||||
data: {
|
||||
dataset,
|
||||
exampleCount,
|
||||
sourceId,
|
||||
timeRange: { startTime, endTime },
|
||||
},
|
||||
} = request.body;
|
||||
|
||||
const sourceConfiguration = await sources.getSourceConfiguration(
|
||||
requestContext.core.savedObjects.client,
|
||||
sourceId
|
||||
);
|
||||
|
||||
try {
|
||||
assertHasInfraMlPlugins(requestContext);
|
||||
|
||||
const { data: logEntryRateExamples, timing } = await getLogEntryRateExamples(
|
||||
requestContext,
|
||||
sourceId,
|
||||
startTime,
|
||||
endTime,
|
||||
dataset,
|
||||
exampleCount,
|
||||
sourceConfiguration,
|
||||
framework.callWithRequest
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: getLogEntryRateExamplesSuccessReponsePayloadRT.encode({
|
||||
data: {
|
||||
examples: logEntryRateExamples,
|
||||
},
|
||||
timing,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (Boom.isBoom(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof NoLogAnalysisResultsIndexError) {
|
||||
return response.notFound({ body: { message: error.message } });
|
||||
}
|
||||
|
||||
return response.customError({
|
||||
statusCode: error.statusCode ?? 500,
|
||||
body: {
|
||||
message: error.message ?? 'An unexpected error occurred',
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -7473,12 +7473,9 @@
|
|||
"xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "指定された条件と一致したログエントリ数",
|
||||
"xpack.infra.logs.alerting.threshold.fired": "実行",
|
||||
"xpack.infra.logs.analysis.analyzeInMlButtonLabel": "ML で分析",
|
||||
"xpack.infra.logs.analysis.anomaliesExpandedRowNumberOfLogEntriesDescription": "ログエントリーの数です",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionTitle": "異常",
|
||||
"xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName": "最高異常スコア",
|
||||
"xpack.infra.logs.analysis.anomaliesTablePartitionColumnName": "パーティション",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "異常が検出されませんでした。",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。",
|
||||
|
@ -7505,9 +7502,6 @@
|
|||
"xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です",
|
||||
"xpack.infra.logs.analysis.onboardingSuccessContent": "機械学習ロボットがデータの収集を開始するまでしばらくお待ちください。",
|
||||
"xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!",
|
||||
"xpack.infra.logs.analysis.overallAnomaliesNumberOfLogEntriesDescription": "ログエントリーの数です",
|
||||
"xpack.infra.logs.analysis.overallAnomaliesTopAnomalyScoreDescription": "最高異常スコア",
|
||||
"xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最高異常スコア",
|
||||
"xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最高異常スコア: {maxAnomalyScore}",
|
||||
"xpack.infra.logs.analysis.recreateJobButtonLabel": "ML ジョブを再作成",
|
||||
"xpack.infra.logs.analysis.setupStatusTryAgainButton": "再試行",
|
||||
|
@ -7552,10 +7546,6 @@
|
|||
"xpack.infra.logs.logEntryCategories.countColumnTitle": "メッセージ数",
|
||||
"xpack.infra.logs.logEntryCategories.datasetColumnTitle": "データセット",
|
||||
"xpack.infra.logs.logEntryCategories.datasetFilterPlaceholder": "データセットでフィルター",
|
||||
"xpack.infra.logs.logEntryCategories.exampleEmptyDescription": "選択した時間範囲内に例は見つかりませんでした。ログエントリー保持期間を長くするとメッセージサンプルの可用性が向上します。",
|
||||
"xpack.infra.logs.logEntryCategories.exampleEmptyReloadButtonLabel": "再読み込み",
|
||||
"xpack.infra.logs.logEntryCategories.exampleLoadingFailureDescription": "カテゴリーの例を読み込めませんでした。",
|
||||
"xpack.infra.logs.logEntryCategories.exampleLoadingFailureRetryButtonLabel": "再試行",
|
||||
"xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "分類ジョブのステータスを確認中...",
|
||||
"xpack.infra.logs.logEntryCategories.loadDataErrorTitle": "カテゴリーデータを読み込めませんでした",
|
||||
"xpack.infra.logs.logEntryCategories.manyCategoriesWarningReasonDescription": "分析されたドキュメントごとのカテゴリ比率が{categoriesDocumentRatio, number }で、非常に高い値です。",
|
||||
|
|
|
@ -7477,12 +7477,9 @@
|
|||
"xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "匹配所提供条件的日志条目数",
|
||||
"xpack.infra.logs.alerting.threshold.fired": "已触发",
|
||||
"xpack.infra.logs.analysis.analyzeInMlButtonLabel": "在 ML 中分析",
|
||||
"xpack.infra.logs.analysis.anomaliesExpandedRowNumberOfLogEntriesDescription": "日志条目数",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常",
|
||||
"xpack.infra.logs.analysis.anomaliesSectionTitle": "异常",
|
||||
"xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName": "最大异常分数",
|
||||
"xpack.infra.logs.analysis.anomaliesTablePartitionColumnName": "分区",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "未检测到任何异常。",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。",
|
||||
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。",
|
||||
|
@ -7509,9 +7506,6 @@
|
|||
"xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning",
|
||||
"xpack.infra.logs.analysis.onboardingSuccessContent": "请注意,我们的 Machine Learning 机器人若干分钟后才会开始收集数据。",
|
||||
"xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!",
|
||||
"xpack.infra.logs.analysis.overallAnomaliesNumberOfLogEntriesDescription": "日志条目数",
|
||||
"xpack.infra.logs.analysis.overallAnomaliesTopAnomalyScoreDescription": "最大异常分数",
|
||||
"xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最大异常分数:",
|
||||
"xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大异常分数:{maxAnomalyScore}",
|
||||
"xpack.infra.logs.analysis.recreateJobButtonLabel": "重新创建 ML 作业",
|
||||
"xpack.infra.logs.analysis.setupStatusTryAgainButton": "重试",
|
||||
|
@ -7556,10 +7550,6 @@
|
|||
"xpack.infra.logs.logEntryCategories.countColumnTitle": "消息计数",
|
||||
"xpack.infra.logs.logEntryCategories.datasetColumnTitle": "数据集",
|
||||
"xpack.infra.logs.logEntryCategories.datasetFilterPlaceholder": "按数据集筛选",
|
||||
"xpack.infra.logs.logEntryCategories.exampleEmptyDescription": "选定时间范围内未找到任何示例。增大日志条目保留期限以改善消息样例可用性。",
|
||||
"xpack.infra.logs.logEntryCategories.exampleEmptyReloadButtonLabel": "重新加载",
|
||||
"xpack.infra.logs.logEntryCategories.exampleLoadingFailureDescription": "无法加载类别示例。",
|
||||
"xpack.infra.logs.logEntryCategories.exampleLoadingFailureRetryButtonLabel": "重试",
|
||||
"xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "正在检查归类作业的状态......",
|
||||
"xpack.infra.logs.logEntryCategories.loadDataErrorTitle": "无法加载类别数据",
|
||||
"xpack.infra.logs.logEntryCategories.manyCategoriesWarningReasonDescription": "每个分析文档的类别比率非常高,达到 {categoriesDocumentRatio, number }。",
|
||||
|
|
Loading…
Reference in a new issue