mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ML] Explain log rate spikes: Plugin setup (#131317)
Sets up the boilerplate code for the aiops plugin and adds a demo page within the ML app to demonstrate single API request data streaming from Kibana server to UI client.
This commit is contained in:
parent
d6805f9a8a
commit
6df1b28a82
50 changed files with 1286 additions and 5 deletions
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
|
@ -187,10 +187,11 @@
|
|||
/x-pack/test/screenshot_creation/apps/ml_docs @elastic/ml-ui
|
||||
/x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui
|
||||
|
||||
# ML team owns and maintains the transform plugin despite it living in the Data management section.
|
||||
/x-pack/plugins/transform/ @elastic/ml-ui
|
||||
# Additional plugins maintained by the ML team.
|
||||
/x-pack/plugins/aiops/ @elastic/ml-ui
|
||||
/x-pack/plugins/data_visualizer/ @elastic/ml-ui
|
||||
/x-pack/plugins/file_upload/ @elastic/ml-ui
|
||||
/x-pack/plugins/transform/ @elastic/ml-ui
|
||||
/x-pack/test/accessibility/apps/transform.ts @elastic/ml-ui
|
||||
/x-pack/test/api_integration/apis/transform/ @elastic/ml-ui
|
||||
/x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui
|
||||
|
|
|
@ -376,6 +376,10 @@ The plugin exposes the static DefaultEditorController class to consume.
|
|||
|The Kibana actions plugin provides a framework to create executable actions. You can:
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/aiops/README.md[aiops]
|
||||
|The plugin provides APIs and components for AIOps features, including the “Explain log rate spikes” UI, maintained by the ML team.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/alerting/README.md[alerting]
|
||||
|The Kibana Alerting plugin provides a common place to set up rules. You can:
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pageLoadAssetSize:
|
||||
advancedSettings: 27596
|
||||
actions: 20000
|
||||
aiops: 10000
|
||||
alerting: 106936
|
||||
apm: 64385
|
||||
canvas: 1066647
|
||||
|
|
|
@ -277,6 +277,8 @@
|
|||
"@kbn/ui-actions-enhanced-examples-plugin/*": ["x-pack/examples/ui_actions_enhanced_examples/*"],
|
||||
"@kbn/actions-plugin": ["x-pack/plugins/actions"],
|
||||
"@kbn/actions-plugin/*": ["x-pack/plugins/actions/*"],
|
||||
"@kbn/aiops-plugin": ["x-pack/plugins/aiops"],
|
||||
"@kbn/aiops-plugin/*": ["x-pack/plugins/aiops/*"],
|
||||
"@kbn/alerting-plugin": ["x-pack/plugins/alerting"],
|
||||
"@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"],
|
||||
"@kbn/apm-plugin": ["x-pack/plugins/apm"],
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"xpack.logstash": ["plugins/logstash"],
|
||||
"xpack.main": "legacy/plugins/xpack_main",
|
||||
"xpack.maps": ["plugins/maps"],
|
||||
"xpack.aiops": ["plugins/aiops"],
|
||||
"xpack.ml": ["plugins/ml"],
|
||||
"xpack.monitoring": ["plugins/monitoring"],
|
||||
"xpack.osquery": ["plugins/osquery"],
|
||||
|
|
9
x-pack/plugins/aiops/README.md
Executable file
9
x-pack/plugins/aiops/README.md
Executable file
|
@ -0,0 +1,9 @@
|
|||
# aiops
|
||||
|
||||
The plugin provides APIs and components for AIOps features, including the “Explain log rate spikes” UI, maintained by the ML team.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.
|
68
x-pack/plugins/aiops/common/api/example_stream.ts
Normal file
68
x-pack/plugins/aiops/common/api/example_stream.ts
Normal file
|
@ -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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const aiopsExampleStreamSchema = schema.object({
|
||||
/** Boolean flag to enable/disabling simulation of response errors. */
|
||||
simulateErrors: schema.maybe(schema.boolean()),
|
||||
/** Maximum timeout between streaming messages. */
|
||||
timeout: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
export type AiopsExampleStreamSchema = TypeOf<typeof aiopsExampleStreamSchema>;
|
||||
|
||||
export const API_ACTION_NAME = {
|
||||
UPDATE_PROGRESS: 'update_progress',
|
||||
ADD_TO_ENTITY: 'add_to_entity',
|
||||
DELETE_ENTITY: 'delete_entity',
|
||||
} as const;
|
||||
export type ApiActionName = typeof API_ACTION_NAME[keyof typeof API_ACTION_NAME];
|
||||
|
||||
interface ApiActionUpdateProgress {
|
||||
type: typeof API_ACTION_NAME.UPDATE_PROGRESS;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export function updateProgressAction(payload: number): ApiActionUpdateProgress {
|
||||
return {
|
||||
type: API_ACTION_NAME.UPDATE_PROGRESS,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiActionAddToEntity {
|
||||
type: typeof API_ACTION_NAME.ADD_TO_ENTITY;
|
||||
payload: {
|
||||
entity: string;
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function addToEntityAction(entity: string, value: number): ApiActionAddToEntity {
|
||||
return {
|
||||
type: API_ACTION_NAME.ADD_TO_ENTITY,
|
||||
payload: {
|
||||
entity,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiActionDeleteEntity {
|
||||
type: typeof API_ACTION_NAME.DELETE_ENTITY;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export function deleteEntityAction(payload: string): ApiActionDeleteEntity {
|
||||
return {
|
||||
type: API_ACTION_NAME.DELETE_ENTITY,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export type ApiAction = ApiActionUpdateProgress | ApiActionAddToEntity | ApiActionDeleteEntity;
|
19
x-pack/plugins/aiops/common/api/index.ts
Normal file
19
x-pack/plugins/aiops/common/api/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 type { AiopsExampleStreamSchema } from './example_stream';
|
||||
|
||||
export const API_ENDPOINT = {
|
||||
EXAMPLE_STREAM: '/internal/aiops/example_stream',
|
||||
ANOTHER: '/internal/aiops/another',
|
||||
} as const;
|
||||
export type ApiEndpoint = typeof API_ENDPOINT[keyof typeof API_ENDPOINT];
|
||||
|
||||
export interface ApiEndpointOptions {
|
||||
[API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamSchema;
|
||||
[API_ENDPOINT.ANOTHER]: { anotherOption: string };
|
||||
}
|
22
x-pack/plugins/aiops/common/index.ts
Executable file
22
x-pack/plugins/aiops/common/index.ts
Executable file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* PLUGIN_ID is used as a unique identifier for the aiops plugin
|
||||
*/
|
||||
export const PLUGIN_ID = 'aiops';
|
||||
|
||||
/**
|
||||
* PLUGIN_NAME is used as the display name for the aiops plugin
|
||||
*/
|
||||
export const PLUGIN_NAME = 'AIOps';
|
||||
|
||||
/**
|
||||
* This is an internal hard coded feature flag so we can easily turn on/off the
|
||||
* "Explain log rate spikes UI" during development until the first release.
|
||||
*/
|
||||
export const AIOPS_ENABLED = true;
|
15
x-pack/plugins/aiops/jest.config.js
Normal file
15
x-pack/plugins/aiops/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/aiops'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/aiops',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/plugins/aiops/{common,public,server}/**/*.{js,ts,tsx}'],
|
||||
};
|
16
x-pack/plugins/aiops/kibana.json
Executable file
16
x-pack/plugins/aiops/kibana.json
Executable file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"id": "aiops",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Machine Learning UI",
|
||||
"githubTeam": "ml-ui"
|
||||
},
|
||||
"description": "AIOps plugin maintained by ML team.",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": [],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaReact"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
15
x-pack/plugins/aiops/public/api/index.ts
Normal file
15
x-pack/plugins/aiops/public/api/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazyLoadModules } from '../lazy_load_bundle';
|
||||
|
||||
import type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes';
|
||||
|
||||
export async function getExplainLogRateSpikesComponent(): Promise<() => ExplainLogRateSpikesSpec> {
|
||||
const modules = await lazyLoadModules();
|
||||
return () => modules.ExplainLogRateSpikes;
|
||||
}
|
167
x-pack/plugins/aiops/public/components/app.tsx
Executable file
167
x-pack/plugins/aiops/public/components/app.tsx
Executable file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
|
||||
import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { getStatusMessage } from './get_status_message';
|
||||
import { initialState, resetStream, streamReducer } from './stream_reducer';
|
||||
import { useStreamFetchReducer } from './use_stream_fetch_reducer';
|
||||
|
||||
export const AiopsApp = () => {
|
||||
const { notifications } = useKibana();
|
||||
|
||||
const [simulateErrors, setSimulateErrors] = useState(false);
|
||||
|
||||
const { dispatch, start, cancel, data, isCancelled, isRunning } = useStreamFetchReducer(
|
||||
'/internal/aiops/example_stream',
|
||||
streamReducer,
|
||||
initialState,
|
||||
{ simulateErrors }
|
||||
);
|
||||
|
||||
const { errors, progress, entities } = data;
|
||||
|
||||
const onClickHandler = async () => {
|
||||
if (isRunning) {
|
||||
cancel();
|
||||
} else {
|
||||
dispatch(resetStream());
|
||||
start();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (errors.length > 0) {
|
||||
notifications.toasts.danger({ body: errors[errors.length - 1] });
|
||||
}
|
||||
}, [errors, notifications.toasts]);
|
||||
|
||||
const buttonLabel = isRunning
|
||||
? i18n.translate('xpack.aiops.stopbuttonText', {
|
||||
defaultMessage: 'Stop development',
|
||||
})
|
||||
: i18n.translate('xpack.aiops.startbuttonText', {
|
||||
defaultMessage: 'Start development',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPage restrictWidth="1000px">
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.congratulationsTitle"
|
||||
defaultMessage="Single endpoint streaming demo"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiText>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
type="primary"
|
||||
size="s"
|
||||
onClick={onClickHandler}
|
||||
aria-label={buttonLabel}
|
||||
>
|
||||
{buttonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiBadge>{progress}%</EuiBadge>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiProgress value={progress} max={100} size="xs" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<div style={{ height: '300px' }}>
|
||||
<Chart>
|
||||
<Settings rotation={90} />
|
||||
<Axis
|
||||
id="entities"
|
||||
position={Position.Bottom}
|
||||
title={i18n.translate('xpack.aiops.barChart.commitsTitle', {
|
||||
defaultMessage: 'Commits',
|
||||
})}
|
||||
showOverlappingTicks
|
||||
/>
|
||||
<Axis
|
||||
id="left2"
|
||||
title={i18n.translate('xpack.aiops.barChart.developersTitle', {
|
||||
defaultMessage: 'Developers',
|
||||
})}
|
||||
position={Position.Left}
|
||||
/>
|
||||
|
||||
<BarSeries
|
||||
id="commits"
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
data={Object.entries(entities)
|
||||
.map(([x, y]) => {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.y - a.y)}
|
||||
/>
|
||||
</Chart>
|
||||
</div>
|
||||
<p>{getStatusMessage(isRunning, isCancelled, data.progress)}</p>
|
||||
<EuiCheckbox
|
||||
id="aiopSimulateErrorsCheckbox"
|
||||
label={i18n.translate(
|
||||
'xpack.aiops.explainLogRateSpikes.simulateErrorsCheckboxLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Simulate errors (gets applied to new streams only, not currently running ones).',
|
||||
}
|
||||
)}
|
||||
checked={simulateErrors}
|
||||
onChange={(e) => setSimulateErrors(!simulateErrors)}
|
||||
compressed
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
import { getCoreStart } from '../kibana_services';
|
||||
|
||||
import { AiopsApp } from './app';
|
||||
|
||||
/**
|
||||
* Spec used for lazy loading in the ML plugin
|
||||
*/
|
||||
export type ExplainLogRateSpikesSpec = typeof ExplainLogRateSpikes;
|
||||
|
||||
export const ExplainLogRateSpikes: FC = () => {
|
||||
const coreStart = getCoreStart();
|
||||
|
||||
return (
|
||||
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<I18nProvider>
|
||||
<AiopsApp />
|
||||
</I18nProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export function getStatusMessage(isRunning: boolean, isCancelled: boolean, progress: number) {
|
||||
if (!isRunning && !isCancelled && progress === 0) {
|
||||
return 'Development did not start yet.';
|
||||
} else if (isRunning && !isCancelled) {
|
||||
return 'Development is ongoing, the hype is real!';
|
||||
} else if (!isRunning && isCancelled) {
|
||||
return 'Oh no, development got cancelled!';
|
||||
} else if (!isRunning && progress === 100) {
|
||||
return 'Development clompeted, the release got out the door!';
|
||||
}
|
||||
|
||||
// When the process stops but wasn't cancelled by the user and progress is not yet at 100%,
|
||||
// this indicates there must have been a problem with the stream.
|
||||
return 'Oh no, looks like there was a bug?!';
|
||||
}
|
80
x-pack/plugins/aiops/public/components/stream_fetch.ts
Normal file
80
x-pack/plugins/aiops/public/components/stream_fetch.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 type React from 'react';
|
||||
|
||||
import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api';
|
||||
|
||||
export async function* streamFetch<A = unknown, E = ApiEndpoint>(
|
||||
endpoint: E,
|
||||
abortCtrl: React.MutableRefObject<AbortController>,
|
||||
options: ApiEndpointOptions[ApiEndpoint],
|
||||
basePath = ''
|
||||
) {
|
||||
const stream = await fetch(`${basePath}${endpoint}`, {
|
||||
signal: abortCtrl.current.signal,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
// This refers to the format of the request body,
|
||||
// not the response, which will be a uint8array Buffer.
|
||||
'Content-Type': 'application/json',
|
||||
'kbn-xsrf': 'stream',
|
||||
},
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
|
||||
if (stream.body !== null) {
|
||||
// Note that Firefox 99 doesn't support `TextDecoderStream` yet.
|
||||
// That's why we skip it here and use `TextDecoder` later to decode each chunk.
|
||||
// Once Firefox supports it, we can use the following alternative:
|
||||
// const reader = stream.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
const reader = stream.body.getReader();
|
||||
|
||||
const bufferBounce = 100;
|
||||
let partial = '';
|
||||
let actionBuffer: A[] = [];
|
||||
let lastCall = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { value: uint8array, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const value = new TextDecoder().decode(uint8array);
|
||||
|
||||
const full = `${partial}${value}`;
|
||||
const parts = full.split('\n');
|
||||
const last = parts.pop();
|
||||
|
||||
partial = last ?? '';
|
||||
|
||||
const actions = parts.map((p) => JSON.parse(p));
|
||||
actionBuffer.push(...actions);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastCall >= bufferBounce && actionBuffer.length > 0) {
|
||||
yield actionBuffer;
|
||||
actionBuffer = [];
|
||||
lastCall = now;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
yield { type: 'error', payload: error.toString() };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The reader might finish with a partially filled actionBuffer so
|
||||
// we need to clear it once more after the request is done.
|
||||
if (actionBuffer.length > 0) {
|
||||
yield actionBuffer;
|
||||
actionBuffer.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
86
x-pack/plugins/aiops/public/components/stream_reducer.ts
Normal file
86
x-pack/plugins/aiops/public/components/stream_reducer.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { ApiAction, API_ACTION_NAME } from '../../common/api/example_stream';
|
||||
|
||||
export const UI_ACTION_NAME = {
|
||||
ERROR: 'error',
|
||||
RESET: 'reset',
|
||||
} as const;
|
||||
export type UiActionName = typeof UI_ACTION_NAME[keyof typeof UI_ACTION_NAME];
|
||||
|
||||
export interface StreamState {
|
||||
errors: string[];
|
||||
progress: number;
|
||||
entities: Record<string, number>;
|
||||
}
|
||||
export const initialState: StreamState = {
|
||||
errors: [],
|
||||
progress: 0,
|
||||
entities: {},
|
||||
};
|
||||
|
||||
interface UiActionError {
|
||||
type: typeof UI_ACTION_NAME.ERROR;
|
||||
payload: string;
|
||||
}
|
||||
interface UiActionResetStream {
|
||||
type: typeof UI_ACTION_NAME.RESET;
|
||||
}
|
||||
|
||||
export function resetStream(): UiActionResetStream {
|
||||
return { type: UI_ACTION_NAME.RESET };
|
||||
}
|
||||
|
||||
type UiAction = UiActionResetStream | UiActionError;
|
||||
export type ReducerAction = ApiAction | UiAction;
|
||||
export function streamReducer(
|
||||
state: StreamState,
|
||||
action: ReducerAction | ReducerAction[]
|
||||
): StreamState {
|
||||
if (Array.isArray(action)) {
|
||||
return action.reduce(streamReducer, state);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case API_ACTION_NAME.UPDATE_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
progress: action.payload,
|
||||
};
|
||||
case API_ACTION_NAME.DELETE_ENTITY:
|
||||
const deleteFromEntities = { ...state.entities };
|
||||
delete deleteFromEntities[action.payload];
|
||||
return {
|
||||
...state,
|
||||
entities: deleteFromEntities,
|
||||
};
|
||||
case API_ACTION_NAME.ADD_TO_ENTITY:
|
||||
const addToEntities = { ...state.entities };
|
||||
if (addToEntities[action.payload.entity] === undefined) {
|
||||
addToEntities[action.payload.entity] = action.payload.value;
|
||||
} else {
|
||||
addToEntities[action.payload.entity] += action.payload.value;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
entities: addToEntities,
|
||||
};
|
||||
case UI_ACTION_NAME.RESET:
|
||||
return initialState;
|
||||
case UI_ACTION_NAME.ERROR:
|
||||
return {
|
||||
...state,
|
||||
errors: [...state.errors, action.payload],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
errors: [...state.errors, 'UNKNOWN_ACTION_ERROR'],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { useReducer, useRef, useState, Reducer, ReducerAction, ReducerState } from 'react';
|
||||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api';
|
||||
|
||||
import { streamFetch } from './stream_fetch';
|
||||
|
||||
export const useStreamFetchReducer = <R extends Reducer<any, any>, E = ApiEndpoint>(
|
||||
endpoint: E,
|
||||
reducer: R,
|
||||
initialState: ReducerState<R>,
|
||||
options: ApiEndpointOptions[ApiEndpoint]
|
||||
) => {
|
||||
const kibana = useKibana();
|
||||
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
const [data, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
const start = async () => {
|
||||
if (isRunning) {
|
||||
throw new Error('Restart not supported yet');
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
setIsCancelled(false);
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
|
||||
for await (const actions of streamFetch(
|
||||
endpoint,
|
||||
abortCtrl,
|
||||
options,
|
||||
kibana.services.http?.basePath.get()
|
||||
)) {
|
||||
dispatch(actions as ReducerAction<R>);
|
||||
}
|
||||
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
abortCtrl.current.abort();
|
||||
setIsCancelled(true);
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
return {
|
||||
cancel,
|
||||
data,
|
||||
dispatch,
|
||||
isCancelled,
|
||||
isRunning,
|
||||
start,
|
||||
};
|
||||
};
|
18
x-pack/plugins/aiops/public/index.ts
Executable file
18
x-pack/plugins/aiops/public/index.ts
Executable file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { AiopsPlugin } from './plugin';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
// as well as, Kibana Platform `plugin()` initializer.
|
||||
export function plugin() {
|
||||
return new AiopsPlugin();
|
||||
}
|
||||
|
||||
export type { AiopsPluginSetup, AiopsPluginStart } from './types';
|
||||
|
||||
export type { ExplainLogRateSpikesSpec } from './components/explain_log_rate_spikes';
|
19
x-pack/plugins/aiops/public/kibana_services.ts
Normal file
19
x-pack/plugins/aiops/public/kibana_services.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '@kbn/core/public';
|
||||
import { AppPluginStartDependencies } from './types';
|
||||
|
||||
let coreStart: CoreStart;
|
||||
let pluginsStart: AppPluginStartDependencies;
|
||||
export function setStartServices(core: CoreStart, plugins: AppPluginStartDependencies) {
|
||||
coreStart = core;
|
||||
pluginsStart = plugins;
|
||||
}
|
||||
|
||||
export const getCoreStart = () => coreStart;
|
||||
export const getPluginsStart = () => pluginsStart;
|
30
x-pack/plugins/aiops/public/lazy_load_bundle/index.ts
Normal file
30
x-pack/plugins/aiops/public/lazy_load_bundle/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes';
|
||||
|
||||
let loadModulesPromise: Promise<LazyLoadedModules>;
|
||||
|
||||
interface LazyLoadedModules {
|
||||
ExplainLogRateSpikes: ExplainLogRateSpikesSpec;
|
||||
}
|
||||
|
||||
export async function lazyLoadModules(): Promise<LazyLoadedModules> {
|
||||
if (typeof loadModulesPromise !== 'undefined') {
|
||||
return loadModulesPromise;
|
||||
}
|
||||
|
||||
loadModulesPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const lazyImports = await import('./lazy');
|
||||
resolve({ ...lazyImports });
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadModulesPromise;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type { ExplainLogRateSpikesSpec } from '../../components/explain_log_rate_spikes';
|
||||
export { ExplainLogRateSpikes } from '../../components/explain_log_rate_spikes';
|
25
x-pack/plugins/aiops/public/plugin.ts
Executable file
25
x-pack/plugins/aiops/public/plugin.ts
Executable file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
|
||||
import { getExplainLogRateSpikesComponent } from './api';
|
||||
import { setStartServices } from './kibana_services';
|
||||
import { AiopsPluginSetup, AiopsPluginStart } from './types';
|
||||
|
||||
export class AiopsPlugin implements Plugin<AiopsPluginSetup, AiopsPluginStart> {
|
||||
public setup(core: CoreSetup) {}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
setStartServices(core, {});
|
||||
return {
|
||||
getExplainLogRateSpikesComponent,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
21
x-pack/plugins/aiops/public/types.ts
Executable file
21
x-pack/plugins/aiops/public/types.ts
Executable file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 type { AiopsPlugin } from './plugin';
|
||||
|
||||
/**
|
||||
* aiops plugin public setup contract
|
||||
*/
|
||||
export type AiopsPluginSetup = ReturnType<AiopsPlugin['setup']>;
|
||||
|
||||
/**
|
||||
* aiops plugin public start contract
|
||||
*/
|
||||
export type AiopsPluginStart = ReturnType<AiopsPlugin['start']>;
|
||||
|
||||
// eslint-disable-next-line
|
||||
export type AppPluginStartDependencies = {};
|
15
x-pack/plugins/aiops/server/index.ts
Executable file
15
x-pack/plugins/aiops/server/index.ts
Executable file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { AiopsPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new AiopsPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export type { AiopsPluginSetup, AiopsPluginStart } from './types';
|
36
x-pack/plugins/aiops/server/plugin.ts
Executable file
36
x-pack/plugins/aiops/server/plugin.ts
Executable file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
||||
|
||||
import { AiopsPluginSetup, AiopsPluginStart } from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
|
||||
export class AiopsPlugin implements Plugin<AiopsPluginSetup, AiopsPluginStart> {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
this.logger.debug('aiops: Setup');
|
||||
const router = core.http.createRouter();
|
||||
|
||||
// Register server side APIs
|
||||
defineRoutes(router, this.logger);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
this.logger.debug('aiops: Started');
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
129
x-pack/plugins/aiops/server/routes/index.ts
Executable file
129
x-pack/plugins/aiops/server/routes/index.ts
Executable file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 { Readable } from 'stream';
|
||||
|
||||
import type { IRouter, Logger } from '@kbn/core/server';
|
||||
|
||||
import { AIOPS_ENABLED } from '../../common';
|
||||
import type { ApiAction } from '../../common/api/example_stream';
|
||||
import {
|
||||
aiopsExampleStreamSchema,
|
||||
updateProgressAction,
|
||||
addToEntityAction,
|
||||
deleteEntityAction,
|
||||
} from '../../common/api/example_stream';
|
||||
|
||||
// We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error.
|
||||
class ResponseStream extends Readable {
|
||||
_read(): void {}
|
||||
}
|
||||
|
||||
const delimiter = '\n';
|
||||
|
||||
export function defineRoutes(router: IRouter, logger: Logger) {
|
||||
if (AIOPS_ENABLED) {
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/aiops/example_stream',
|
||||
validate: {
|
||||
body: aiopsExampleStreamSchema,
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const maxTimeoutMs = request.body.timeout ?? 250;
|
||||
const simulateError = request.body.simulateErrors ?? false;
|
||||
|
||||
let shouldStop = false;
|
||||
request.events.aborted$.subscribe(() => {
|
||||
shouldStop = true;
|
||||
});
|
||||
request.events.completed$.subscribe(() => {
|
||||
shouldStop = true;
|
||||
});
|
||||
|
||||
const stream = new ResponseStream();
|
||||
|
||||
function streamPush(d: ApiAction) {
|
||||
try {
|
||||
const line = JSON.stringify(d);
|
||||
stream.push(`${line}${delimiter}`);
|
||||
} catch (error) {
|
||||
logger.error('Could not serialize or stream a message.');
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const entities = [
|
||||
'kimchy',
|
||||
's1monw',
|
||||
'martijnvg',
|
||||
'jasontedor',
|
||||
'nik9000',
|
||||
'javanna',
|
||||
'rjernst',
|
||||
'jrodewig',
|
||||
];
|
||||
|
||||
const actions = [...Array(19).fill('add'), 'delete'];
|
||||
|
||||
if (simulateError) {
|
||||
actions.push('server-only-error');
|
||||
actions.push('server-to-client-error');
|
||||
actions.push('client-error');
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
|
||||
async function pushStreamUpdate() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
progress++;
|
||||
|
||||
if (progress > 100 || shouldStop) {
|
||||
stream.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
streamPush(updateProgressAction(progress));
|
||||
|
||||
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
|
||||
const randomAction = actions[Math.floor(Math.random() * actions.length)];
|
||||
|
||||
if (randomAction === 'add') {
|
||||
const randomCommits = Math.floor(Math.random() * 100);
|
||||
streamPush(addToEntityAction(randomEntity, randomCommits));
|
||||
} else if (randomAction === 'delete') {
|
||||
streamPush(deleteEntityAction(randomEntity));
|
||||
} else if (randomAction === 'server-to-client-error') {
|
||||
// Throw an error. It should not crash Kibana!
|
||||
throw new Error('There was a (simulated) server side error!');
|
||||
} else if (randomAction === 'client-error') {
|
||||
// Return not properly encoded JSON to the client.
|
||||
stream.push(`{body:'Not valid JSON${delimiter}`);
|
||||
}
|
||||
|
||||
pushStreamUpdate();
|
||||
} catch (error) {
|
||||
stream.push(
|
||||
`${JSON.stringify({ type: 'error', payload: error.toString() })}${delimiter}`
|
||||
);
|
||||
stream.push(null);
|
||||
}
|
||||
}, Math.floor(Math.random() * maxTimeoutMs));
|
||||
}
|
||||
|
||||
// do not call this using `await` so it will run asynchronously while we return the stream already.
|
||||
pushStreamUpdate();
|
||||
|
||||
return response.ok({
|
||||
body: stream,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
18
x-pack/plugins/aiops/server/types.ts
Executable file
18
x-pack/plugins/aiops/server/types.ts
Executable file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* aiops plugin server setup contract
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AiopsPluginSetup {}
|
||||
|
||||
/**
|
||||
* aiops plugin server start contract
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AiopsPluginStart {}
|
28
x-pack/plugins/aiops/tsconfig.json
Normal file
28
x-pack/plugins/aiops/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"scripts/**/*",
|
||||
"server/**/*",
|
||||
"types/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/custom_integrations/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/navigation/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/unified_search/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
]
|
||||
}
|
|
@ -51,6 +51,8 @@ export const ML_PAGES = {
|
|||
FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list',
|
||||
ACCESS_DENIED: 'access-denied',
|
||||
OVERVIEW: 'overview',
|
||||
AIOPS: 'aiops',
|
||||
AIOPS_EXPLAIN_LOG_RATE_SPIKES: 'aiops/explain_log_rate_spikes',
|
||||
} as const;
|
||||
|
||||
export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES];
|
||||
|
|
|
@ -60,7 +60,9 @@ export type MlGenericUrlState = MLPageState<
|
|||
| typeof ML_PAGES.ACCESS_DENIED
|
||||
| typeof ML_PAGES.DATA_VISUALIZER
|
||||
| typeof ML_PAGES.DATA_VISUALIZER_FILE
|
||||
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT,
|
||||
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS
|
||||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES,
|
||||
MlGenericUrlPageState | undefined
|
||||
>;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"ml"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"aiops",
|
||||
"cloud",
|
||||
"data",
|
||||
"dataViews",
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { ExplainLogRateSpikesSpec } from '@kbn/aiops-plugin/public';
|
||||
import { useMlKibana, useTimefilter } from '../contexts/kibana';
|
||||
import { HelpMenu } from '../components/help_menu';
|
||||
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
|
||||
export const ExplainLogRateSpikesPage: FC = () => {
|
||||
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
|
||||
const {
|
||||
services: { docLinks, aiops },
|
||||
} = useMlKibana();
|
||||
|
||||
const [ExplainLogRateSpikes, setExplainLogRateSpikes] = useState<ExplainLogRateSpikesSpec | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (aiops !== undefined) {
|
||||
const { getExplainLogRateSpikesComponent } = aiops;
|
||||
getExplainLogRateSpikesComponent().then(setExplainLogRateSpikes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ExplainLogRateSpikes !== null ? (
|
||||
<>
|
||||
<MlPageHeader>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explainLogRateSpikes.pageHeader"
|
||||
defaultMessage="Explain log rate spikes"
|
||||
/>
|
||||
</MlPageHeader>
|
||||
<ExplainLogRateSpikes />
|
||||
</>
|
||||
) : null}
|
||||
<HelpMenu docLink={docLinks.links.ml.guide} />
|
||||
</>
|
||||
);
|
||||
};
|
8
x-pack/plugins/ml/public/application/aiops/index.ts
Normal file
8
x-pack/plugins/ml/public/application/aiops/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ExplainLogRateSpikesPage } from './explain_log_rate_spikes';
|
|
@ -82,6 +82,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
maps: deps.maps,
|
||||
triggersActionsUi: deps.triggersActionsUi,
|
||||
dataVisualizer: deps.dataVisualizer,
|
||||
aiops: deps.aiops,
|
||||
usageCollection: deps.usageCollection,
|
||||
fieldFormats: deps.fieldFormats,
|
||||
dashboard: deps.dashboard,
|
||||
|
@ -135,6 +136,7 @@ export const renderApp = (
|
|||
dashboard: deps.dashboard,
|
||||
maps: deps.maps,
|
||||
dataVisualizer: deps.dataVisualizer,
|
||||
aiops: deps.aiops,
|
||||
dataViews: deps.data.dataViews,
|
||||
});
|
||||
|
||||
|
|
|
@ -71,7 +71,10 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
|
|||
);
|
||||
|
||||
const routeList = useMemo(
|
||||
() => Object.values(routes).map((routeFactory) => routeFactory(navigateToPath, basePath.get())),
|
||||
() =>
|
||||
Object.values(routes)
|
||||
.map((routeFactory) => routeFactory(navigateToPath, basePath.get()))
|
||||
.filter((d) => !d.disabled),
|
||||
[]
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiSideNavItemType } from '@elastic/eui';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common';
|
||||
import type { MlLocatorParams } from '../../../../common/types/locator';
|
||||
import { useUrlState } from '../../util/url_state';
|
||||
import { useMlLocator, useNavigateToPath } from '../../contexts/kibana';
|
||||
|
@ -64,7 +65,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
const tabsDefinition: Tab[] = useMemo((): Tab[] => {
|
||||
const disableLinks = mlFeaturesDisabled;
|
||||
|
||||
return [
|
||||
const mlTabs: Tab[] = [
|
||||
{
|
||||
id: 'main_section',
|
||||
name: '',
|
||||
|
@ -218,6 +219,28 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (AIOPS_ENABLED) {
|
||||
mlTabs.push({
|
||||
id: 'aiops_section',
|
||||
name: i18n.translate('xpack.ml.navMenu.aiopsTabLinkText', {
|
||||
defaultMessage: 'AIOps',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'explainlogratespikes',
|
||||
pathId: ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES,
|
||||
name: i18n.translate('xpack.ml.navMenu.explainLogRateSpikesLinkText', {
|
||||
defaultMessage: 'Explain log rate spikes',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab explainLogRateSpikes',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return mlTabs;
|
||||
}, [mlFeaturesDisabled, canViewMlNodes]);
|
||||
|
||||
const getTabItem: (tab: Tab) => EuiSideNavItemType<unknown> = useCallback(
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
|||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { MapsStartApi } from '@kbn/maps-plugin/public';
|
||||
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
|
||||
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
|
||||
import type { DashboardSetup } from '@kbn/dashboard-plugin/public';
|
||||
|
@ -32,6 +33,7 @@ interface StartPlugins {
|
|||
maps?: MapsStartApi;
|
||||
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
|
||||
dataVisualizer?: DataVisualizerPluginStart;
|
||||
aiops?: AiopsPluginStart;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
fieldFormats: FieldFormatsRegistry;
|
||||
dashboard: DashboardSetup;
|
||||
|
|
|
@ -55,6 +55,13 @@ export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
|||
href: '/datavisualizer',
|
||||
});
|
||||
|
||||
export const AIOPS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', {
|
||||
defaultMessage: 'AIOps',
|
||||
}),
|
||||
href: '/aiops',
|
||||
});
|
||||
|
||||
export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', {
|
||||
defaultMessage: 'Create job',
|
||||
|
@ -83,6 +90,7 @@ const breadcrumbs = {
|
|||
DATA_FRAME_ANALYTICS_BREADCRUMB,
|
||||
TRAINED_MODELS,
|
||||
DATA_VISUALIZER_BREADCRUMB,
|
||||
AIOPS_BREADCRUMB,
|
||||
CREATE_JOB_BREADCRUMB,
|
||||
CALENDAR_MANAGEMENT_BREADCRUMB,
|
||||
FILTER_LISTS_BREADCRUMB,
|
||||
|
|
|
@ -48,6 +48,7 @@ export interface MlRoute {
|
|||
enableDatePicker?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
actionMenu?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PageProps {
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { parse } from 'query-string';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common';
|
||||
|
||||
import { NavigateToPath } from '../../../contexts/kibana';
|
||||
|
||||
import { MlRoute, PageLoader, PageProps } from '../../router';
|
||||
import { useResolver } from '../../use_resolver';
|
||||
import { ExplainLogRateSpikesPage as Page } from '../../../aiops/explain_log_rate_spikes';
|
||||
|
||||
import { checkBasicLicense } from '../../../license';
|
||||
import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
|
||||
import { cacheDataViewsContract } from '../../../util/index_utils';
|
||||
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
|
||||
|
||||
export const explainLogRateSpikesRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
basePath: string
|
||||
): MlRoute => ({
|
||||
id: 'explain_log_rate_spikes',
|
||||
path: '/aiops/explain_log_rate_spikes',
|
||||
title: i18n.translate('xpack.ml.aiops.explainLogRateSpikes.docTitle', {
|
||||
defaultMessage: 'Explain log rate spikes',
|
||||
}),
|
||||
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
|
||||
breadcrumbs: [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.AiopsBreadcrumbs.explainLogRateSpikesLabel', {
|
||||
defaultMessage: 'Explain log rate spikes',
|
||||
}),
|
||||
},
|
||||
],
|
||||
disabled: !AIOPS_ENABLED,
|
||||
});
|
||||
|
||||
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
|
||||
const { redirectToMlAccessDeniedPage } = deps;
|
||||
|
||||
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
|
||||
const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
|
||||
checkBasicLicense,
|
||||
cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
|
||||
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLoader context={context}>
|
||||
<Page />
|
||||
</PageLoader>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './explain_log_rate_spikes';
|
|
@ -11,6 +11,7 @@ export * from './new_job';
|
|||
export * from './datavisualizer';
|
||||
export * from './settings';
|
||||
export * from './data_frame_analytics';
|
||||
export * from './aiops';
|
||||
export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer';
|
||||
export * from './explorer';
|
||||
export * from './access_denied';
|
||||
|
|
|
@ -27,6 +27,7 @@ import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
|||
import type { SecurityPluginSetup } from '@kbn/security-plugin/public';
|
||||
import type { MapsStartApi } from '@kbn/maps-plugin/public';
|
||||
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
|
||||
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
|
||||
|
||||
export interface DependencyCache {
|
||||
timefilter: DataPublicPluginSetup['query']['timefilter'] | null;
|
||||
|
@ -48,6 +49,7 @@ export interface DependencyCache {
|
|||
dashboard: DashboardStart | null;
|
||||
maps: MapsStartApi | null;
|
||||
dataVisualizer: DataVisualizerPluginStart | null;
|
||||
aiops: AiopsPluginStart | null;
|
||||
dataViews: DataViewsContract | null;
|
||||
}
|
||||
|
||||
|
@ -71,6 +73,7 @@ const cache: DependencyCache = {
|
|||
dashboard: null,
|
||||
maps: null,
|
||||
dataVisualizer: null,
|
||||
aiops: null,
|
||||
dataViews: null,
|
||||
};
|
||||
|
||||
|
@ -93,6 +96,7 @@ export function setDependencyCache(deps: Partial<DependencyCache>) {
|
|||
cache.i18n = deps.i18n || null;
|
||||
cache.dashboard = deps.dashboard || null;
|
||||
cache.dataVisualizer = deps.dataVisualizer || null;
|
||||
cache.aiops = deps.aiops || null;
|
||||
cache.dataViews = deps.dataViews || null;
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,8 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
|
|||
case ML_PAGES.DATA_VISUALIZER_FILE:
|
||||
case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER:
|
||||
case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT:
|
||||
case ML_PAGES.AIOPS:
|
||||
case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES:
|
||||
case ML_PAGES.OVERVIEW:
|
||||
case ML_PAGES.SETTINGS:
|
||||
case ML_PAGES.FILTER_LISTS_MANAGE:
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
|
||||
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
|
||||
import type { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/public';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import type { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
@ -59,6 +60,7 @@ export interface MlStartDependencies {
|
|||
maps?: MapsStartApi;
|
||||
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
|
||||
dataVisualizer: DataVisualizerPluginStart;
|
||||
aiops: AiopsPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
dashboard: DashboardStart;
|
||||
charts: ChartsPluginStart;
|
||||
|
@ -125,6 +127,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
kibanaVersion,
|
||||
triggersActionsUi: pluginsStart.triggersActionsUi,
|
||||
dataVisualizer: pluginsStart.dataVisualizer,
|
||||
aiops: pluginsStart.aiops,
|
||||
usageCollection: pluginsSetup.usageCollection,
|
||||
fieldFormats: pluginsStart.fieldFormats,
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
{ "path": "../cloud/tsconfig.json" },
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
{ "path": "../data_visualizer/tsconfig.json"},
|
||||
{ "path": "../aiops/tsconfig.json"},
|
||||
{ "path": "../license_management/tsconfig.json" },
|
||||
{ "path": "../licensing/tsconfig.json" },
|
||||
{ "path": "../maps/tsconfig.json" },
|
||||
|
|
104
x-pack/test/api_integration/apis/aiops/example_stream.ts
Normal file
104
x-pack/test/api_integration/apis/aiops/example_stream.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 fetch from 'node-fetch';
|
||||
import { format as formatUrl } from 'url';
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const config = getService('config');
|
||||
const kibanaServerUrl = formatUrl(config.get('servers.kibana'));
|
||||
|
||||
describe('POST /internal/aiops/example_stream', () => {
|
||||
it('should return full data without streaming', async () => {
|
||||
const resp = await supertest
|
||||
.post(`/internal/aiops/example_stream`)
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.send({
|
||||
timeout: 1,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(Buffer.isBuffer(resp.body)).to.be(true);
|
||||
|
||||
const chunks: string[] = resp.body.toString().split('\n');
|
||||
|
||||
expect(chunks.length).to.be(201);
|
||||
|
||||
const lastChunk = chunks.pop();
|
||||
expect(lastChunk).to.be('');
|
||||
|
||||
let data: any[] = [];
|
||||
|
||||
expect(() => {
|
||||
data = chunks.map((c) => JSON.parse(c));
|
||||
}).not.to.throwError();
|
||||
|
||||
data.forEach((d) => {
|
||||
expect(typeof d.type).to.be('string');
|
||||
});
|
||||
|
||||
const progressData = data.filter((d) => d.type === 'update_progress');
|
||||
expect(progressData.length).to.be(100);
|
||||
expect(progressData[0].payload).to.be(1);
|
||||
expect(progressData[progressData.length - 1].payload).to.be(100);
|
||||
});
|
||||
|
||||
it('should return data in chunks with streaming', async () => {
|
||||
const response = await fetch(`${kibanaServerUrl}/internal/aiops/example_stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'kbn-xsrf': 'stream',
|
||||
},
|
||||
body: JSON.stringify({ timeout: 1 }),
|
||||
});
|
||||
|
||||
const stream = response.body;
|
||||
|
||||
expect(stream).not.to.be(null);
|
||||
|
||||
if (stream !== null) {
|
||||
let partial = '';
|
||||
let threw = false;
|
||||
const progressData: any[] = [];
|
||||
|
||||
try {
|
||||
for await (const value of stream) {
|
||||
const full = `${partial}${value}`;
|
||||
const parts = full.split('\n');
|
||||
const last = parts.pop();
|
||||
|
||||
partial = last ?? '';
|
||||
|
||||
const actions = parts.map((p) => JSON.parse(p));
|
||||
|
||||
actions.forEach((action) => {
|
||||
expect(typeof action.type).to.be('string');
|
||||
|
||||
if (action.type === 'update_progress') {
|
||||
progressData.push(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
|
||||
expect(threw).to.be(false);
|
||||
|
||||
expect(progressData.length).to.be(100);
|
||||
expect(progressData[0].payload).to.be(1);
|
||||
expect(progressData[progressData.length - 1].payload).to.be(100);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
16
x-pack/test/api_integration/apis/aiops/index.ts
Normal file
16
x-pack/test/api_integration/apis/aiops/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('AIOps', function () {
|
||||
this.tags(['ml']);
|
||||
|
||||
loadTestFile(require.resolve('./example_stream'));
|
||||
});
|
||||
}
|
|
@ -31,6 +31,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./searchprofiler'));
|
||||
loadTestFile(require.resolve('./painless_lab'));
|
||||
loadTestFile(require.resolve('./file_upload'));
|
||||
loadTestFile(require.resolve('./aiops'));
|
||||
loadTestFile(require.resolve('./ml'));
|
||||
loadTestFile(require.resolve('./watcher'));
|
||||
loadTestFile(require.resolve('./logs_ui'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue