Alerts and cases page components for observability plugin (#93365)

Create components for the alerts and cases pages. This only contains basic empty components and stories for them.
This commit is contained in:
Nathan L Smith 2021-03-23 07:57:56 -05:00 committed by GitHub
parent 1e6a024c4d
commit 5e31f91614
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1220 additions and 27 deletions

View file

@ -408,4 +408,8 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:enableAlertingExperience': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
};

View file

@ -30,6 +30,7 @@ export interface UsageStats {
'securitySolution:rulesTableRefresh': string;
'apm:enableSignificantTerms': boolean;
'apm:enableServiceOverview': boolean;
'observability:enableAlertingExperience': boolean;
'visualize:enableLabs': boolean;
'visualization:heatmap:maxBuckets': number;
'visualization:colorMapping': string;

View file

@ -8026,6 +8026,12 @@
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:enableAlertingExperience": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
}
}
},

View 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 const enableAlertingExperience = 'observability:enableAlertingExperience';

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import React, { MouseEvent, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
@ -21,12 +21,7 @@ import { useRouteParams } from '../hooks/use_route_params';
import { ObservabilityPluginSetupDeps } from '../plugin';
import { HasDataContextProvider } from '../context/has_data_context';
import { Breadcrumbs, routes } from '../routes';
const observabilityLabelBreadcrumb = {
text: i18n.translate('xpack.observability.observability.breadcrumb.', {
defaultMessage: 'Observability',
}),
};
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) {
return breadcrumbs.map(({ text }) => text).reverse();
@ -42,12 +37,24 @@ function App() {
const Wrapper = () => {
const { core } = usePluginContext();
// eslint-disable-next-line react-hooks/exhaustive-deps
const breadcrumb = [observabilityLabelBreadcrumb, ...route.breadcrumb];
useEffect(() => {
core.chrome.setBreadcrumbs(breadcrumb);
core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb));
}, [core, breadcrumb]);
const href = core.http.basePath.prepend('/app/observability');
const breadcrumbs = [
{
href,
text: i18n.translate('xpack.observability.observability.breadcrumb.', {
defaultMessage: 'Observability',
}),
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
core.application.navigateToUrl(href);
},
},
...route.breadcrumb,
];
core.chrome.setBreadcrumbs(breadcrumbs);
core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumbs));
}, [core]);
const params = useRouteParams(path);
return route.handler(params);
@ -76,7 +83,7 @@ export const renderApp = (
});
ReactDOM.render(
<KibanaContextProvider services={{ ...core, ...plugins }}>
<KibanaContextProvider services={{ ...core, ...plugins, storage: new Storage(localStorage) }}>
<PluginContext.Provider value={{ appMountParameters, core, plugins }}>
<Router history={history}>
<EuiThemeProvider darkMode={isDarkMode}>

View file

@ -21,6 +21,7 @@ import React, { useState } from 'react';
import { EuiSelect } from '@elastic/eui';
import { uniqBy } from 'lodash';
import { Alert } from '../../../../../../alerting/common';
import { enableAlertingExperience } from '../../../../../common/ui_settings_keys';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { SectionContainer } from '..';
@ -40,6 +41,10 @@ export function AlertsSection({ alerts }: Props) {
const { core } = usePluginContext();
const [filter, setFilter] = useState(ALL_TYPES);
const href = core.uiSettings.get(enableAlertingExperience)
? '/app/observability/alerts'
: '/app/management/insightsAndAlerting/triggersActions/alerts';
const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({
value: consumer,
text: consumer,
@ -51,7 +56,7 @@ export function AlertsSection({ alerts }: Props) {
defaultMessage: 'Alerts',
})}
appLink={{
href: '/app/management/insightsAndAlerting/triggersActions/alerts',
href,
label: i18n.translate('xpack.observability.overview.alert.appLink', {
defaultMessage: 'Manage alerts',
}),

View file

@ -0,0 +1,24 @@
/*
* 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 { EuiBetaBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
export function ExperimentalBadge() {
return (
<EuiBetaBadge
label={i18n.translate('xpack.observability.experimentalBadgeLabel', {
defaultMessage: 'Experimental',
})}
tooltipContent={i18n.translate('xpack.observability.experimentalBadgeDescription', {
defaultMessage:
'This functionality is experimental and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but experimental features are not subject to the support SLA of official GA features.',
})}
/>
);
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ComponentType } from 'react';
import { IntlProvider } from 'react-intl';
import { AlertsPage } from '.';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { PluginContext, PluginContextValue } from '../../context/plugin_context';
import { AlertsFlyout } from './alerts_flyout';
import { AlertItem } from './alerts_table';
import { eventLogPocData, wireframeData } from './example_data';
export default {
title: 'app/Alerts',
component: AlertsPage,
decorators: [
(Story: ComponentType) => {
return (
<IntlProvider locale="en">
<KibanaContextProvider
services={{
data: { query: {} },
docLinks: { links: { query: {} } },
storage: { get: () => {} },
uiSettings: {
get: (setting: string) => {
if (setting === 'dateFormat') {
return '';
} else {
return [];
}
},
},
}}
>
<PluginContext.Provider
value={
({
core: {
http: { basePath: { prepend: (_: string) => '' } },
},
} as unknown) as PluginContextValue
}
>
<Story />
</PluginContext.Provider>
</KibanaContextProvider>
</IntlProvider>
);
},
],
};
export function Example() {
return <AlertsPage items={wireframeData} routeParams={{ query: {} }} />;
}
export function EventLog() {
return <AlertsPage items={eventLogPocData as AlertItem[]} routeParams={{ query: {} }} />;
}
export function EmptyState() {
return <AlertsPage items={[]} routeParams={{ query: {} }} />;
}
export function Flyout() {
return <AlertsFlyout {...wireframeData[0]} onClose={() => {}} />;
}

View file

@ -0,0 +1,132 @@
/*
* 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 {
EuiBadge,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutProps,
EuiInMemoryTable,
EuiSpacer,
EuiTabbedContent,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { AlertItem } from './alerts_table';
type AlertsFlyoutProps = AlertItem & EuiFlyoutProps;
export function AlertsFlyout(props: AlertsFlyoutProps) {
const {
actualValue,
affectedEntity,
expectedValue,
onClose,
reason,
severity,
severityLog,
status,
duration,
type,
} = props;
const timestamp = props['@timestamp'];
const overviewListItems = [
{
title: 'Status',
description: status || '-',
},
{
title: 'Severity',
description: severity || '-', // TODO: badge and "(changed 2 min ago)"
},
{
title: 'Affected entity',
description: affectedEntity || '-', // TODO: link to entity
},
{
title: 'Triggered',
description: timestamp, // TODO: format date
},
{
title: 'Duration',
description: duration || '-', // TODO: format duration
},
{
title: 'Expected value',
description: expectedValue || '-',
},
{
title: 'Actual value',
description: actualValue || '-',
},
{
title: 'Type',
description: type || '-',
},
];
const tabs = [
{
id: 'overview',
name: i18n.translate('xpack.observability.alerts.flyoutOverviewTabTitle', {
defaultMessage: 'Overview',
}),
content: (
<>
<EuiSpacer />
<EuiInMemoryTable
columns={[
{ field: 'title', name: '' },
{ field: 'description', name: '' },
]}
items={overviewListItems}
/>
<EuiSpacer />
<EuiTitle size="xs">
<h4>Severity log</h4>
</EuiTitle>
<EuiInMemoryTable
columns={[
{ field: '@timestamp', name: 'Timestamp', dataType: 'date' },
{
field: 'severity',
name: 'Severity',
render: (_, item) => (
<>
<EuiBadge>{item.severity}</EuiBadge> {item.message}
</>
),
},
]}
items={severityLog ?? []}
/>
</>
),
},
{
id: 'metadata',
name: i18n.translate('xpack.observability.alerts.flyoutMetadataTabTitle', {
defaultMessage: 'Metadata',
}),
disabled: true,
content: <></>,
},
];
return (
<EuiFlyout onClose={onClose} size="s">
<EuiFlyoutHeader>
<EuiTitle size="xs">
<h2>{reason}</h2>
</EuiTitle>
<EuiTabbedContent size="s" tabs={tabs} />
</EuiFlyoutHeader>
</EuiFlyout>
);
}

View file

@ -0,0 +1,24 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { SearchBar, TimeHistory } from '../../../../../../src/plugins/data/public';
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
export function AlertsSearchBar() {
return (
<SearchBar
indexPatterns={[]}
placeholder={i18n.translate('xpack.observability.alerts.searchBarPlaceholder', {
defaultMessage: '"domain": "ecommerce" AND ("service.name": "ProductCatalogService" …)',
})}
query={{ query: '', language: 'kuery' }}
timeHistory={new TimeHistory(new Storage(localStorage))}
/>
);
}

View file

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiBasicTableProps,
DefaultItemAction,
EuiTableSelectionType,
EuiLink,
} from '@elastic/eui';
import React, { useState } from 'react';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { AlertsFlyout } from './alerts_flyout';
/**
* The type of an item in the alert list.
*
* The fields here are the minimum to make this work at this time, but
* eventually this type should be derived from the schema of what is returned in
* the API response.
*/
export interface AlertItem {
'@timestamp': number;
reason: string;
severity: string;
// These are just made up so we can make example links
service?: { name?: string };
pod?: string;
log?: boolean;
// Other fields used in the flyout
actualValue?: string;
affectedEntity?: string;
expectedValue?: string;
severityLog?: Array<{ '@timestamp': number; severity: string; message: string }>;
status?: string;
duration?: string;
type?: string;
}
type AlertsTableProps = Omit<
EuiBasicTableProps<AlertItem>,
'columns' | 'isSelectable' | 'pagination' | 'selection'
>;
export function AlertsTable(props: AlertsTableProps) {
const [flyoutAlert, setFlyoutAlert] = useState<AlertItem | undefined>(undefined);
const handleFlyoutClose = () => setFlyoutAlert(undefined);
const { prepend } = usePluginContext().core.http.basePath;
// This is a contrived implementation of the reason field that shows how
// you could link to certain types of resources based on what's contained
// in their alert data.
function reasonRenderer(text: string, item: AlertItem) {
const serviceName = item.service?.name;
const pod = item.pod;
const log = item.log;
if (serviceName) {
return <EuiLink href={prepend(`/app/apm/services/${serviceName}`)}>{text}</EuiLink>;
} else if (pod) {
return <EuiLink href={prepend(`/app/metrics/link-to/host-detail/${pod}`)}>{text}</EuiLink>;
} else if (log) {
return <EuiLink href={prepend(`/app/logs/stream`)}>{text}</EuiLink>;
} else {
return <>{text}</>;
}
}
const actions: Array<DefaultItemAction<AlertItem>> = [
{
name: 'Alert details',
description: 'Alert details',
onClick: (item) => {
setFlyoutAlert(item);
},
isPrimary: true,
},
];
const columns: Array<EuiBasicTableColumn<AlertItem>> = [
{
field: '@timestamp',
name: 'Triggered',
dataType: 'date',
},
{
field: 'duration',
name: 'Duration',
},
{
field: 'severity',
name: 'Severity',
},
{
field: 'reason',
name: 'Reason',
dataType: 'string',
render: reasonRenderer,
},
{
actions,
name: 'Actions',
},
];
return (
<>
{flyoutAlert && <AlertsFlyout {...flyoutAlert} onClose={handleFlyoutClose} />}
<EuiBasicTable<AlertItem>
{...props}
isSelectable={true}
selection={{} as EuiTableSelectionType<AlertItem>}
columns={columns}
pagination={{ pageIndex: 0, pageSize: 0, totalItemCount: 0 }}
/>
</>
);
}

View file

@ -0,0 +1,509 @@
/*
* 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.
*/
/**
* Example data from Whimsical wireframes: https://whimsical.com/observability-alerting-user-journeys-8TFDcHRPMQDJgtLpJ7XuBj
*/
export const wireframeData = [
{
'@timestamp': 1615392661000,
duration: '10 min 2 s',
severity: '-',
reason: 'Error count is greater than 100 (current value is 135) on shippingService',
service: { name: 'opbeans-go' },
affectedEntity: 'opbeans-go service',
status: 'Active',
expectedValue: '< 100',
actualValue: '135',
severityLog: [
{ '@timestamp': 1615392661000, severity: 'critical', message: 'Load is 3.5' },
{ '@timestamp': 1615392600000, severity: 'warning', message: 'Load is 2.5' },
{ '@timestamp': 1615392552000, severity: 'critical', message: 'Load is 3.5' },
],
type: 'APM Error count',
},
{
'@timestamp': 1615392600000,
duration: '11 min 1 s',
severity: '-',
reason: 'Latency is greater than 1500ms (current value is 1700ms) on frontend',
service: { name: 'opbeans-go' },
severityLog: [],
},
{
'@timestamp': 1615392552000,
duration: '10 min 2 s',
severity: 'critical',
reason: 'Latency anomaly score is 84 on checkoutService',
service: { name: 'opbeans-go' },
severityLog: [],
},
{
'@timestamp': 1615392391000,
duration: '10 min 2 s',
severity: '-',
reason:
'CPU is greater than a threshold of 75% (current value is 83%) on gke-eden-3-prod-pool-2-395ef018-06xg',
pod: 'gke-dev-oblt-dev-oblt-pool-30f1ba48-skw',
severityLog: [],
},
{
'@timestamp': 1615392363000,
duration: '10 min 2 s',
severity: '-',
reason:
"Log count with 'Log.level.error' and 'service.name; frontend' is greater than 75 (current value 122)",
log: true,
severityLog: [],
},
{
'@timestamp': 1615392361000,
duration: '10 min 2 s',
severity: 'critical',
reason: 'Load is greater than 2 (current value is 3.5) on gke-eden-3-prod-pool-2-395ef018-06xg',
pod: 'gke-dev-oblt-dev-oblt-pool-30f1ba48-skw',
severityLog: [],
},
];
/**
* Example data from this proof of concept: https://github.com/dgieselaar/kibana/tree/alerting-event-log-poc
*/
export const eventLogPocData = [
{
'@timestamp': 1615395754597,
first_seen: 1615362488702,
severity: 'warning',
severity_value: 1241.4546,
reason:
'Transaction duration for opbeans-java/request in production was above the threshold of 1.0 ms (1.2 ms)',
rule_id: 'cb1fc3e0-7fef-11eb-827d-d94e80a23d8d',
rule_name: 'Latency threshold | opbeans-java',
rule_type_id: 'apm.transaction_duration',
rule_type_name: 'Latency threshold',
alert_instance_title: ['opbeans-java/request:production'],
alert_instance_name: 'apm.transaction_duration_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: '1b354805-4bf3-4626-b6be-5801d7d1e256',
influencers: [
'service.name:opbeans-java',
'service.environment:production',
'transaction.type:request',
],
fields: {
'processor.event': 'transaction',
'service.name': 'opbeans-java',
'service.environment': 'production',
'transaction.type': 'request',
},
timeseries: [
{
x: 1615359600000,
y: 48805,
threshold: 1000,
},
{
x: 1615370400000,
y: 3992.5,
threshold: 1000,
},
{
x: 1615381200000,
y: 4296.7998046875,
threshold: 1000,
},
{
x: 1615392000000,
y: 1633.8182373046875,
threshold: 1000,
},
],
recovered: false,
},
{
'@timestamp': 1615326143423,
first_seen: 1615323802378,
severity: 'warning',
severity_value: 27,
reason: 'Error count for opbeans-node in production was above the threshold of 2 (27)',
rule_id: '335b38d0-80be-11eb-9fd1-d3725789930d',
rule_name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
rule_type_name: 'Error count threshold',
alert_instance_title: ['opbeans-node:production'],
alert_instance_name: 'opbeans-node_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: '19165a4f-296a-4045-9448-40c793d97d02',
influencers: ['service.name:opbeans-node', 'service.environment:production'],
fields: {
'processor.event': 'error',
'service.name': 'opbeans-node',
'service.environment': 'production',
},
timeseries: [
{
x: 1615323780000,
y: 32,
threshold: 2,
},
{
x: 1615324080000,
y: 34,
threshold: 2,
},
{
x: 1615324380000,
y: 32,
threshold: 2,
},
{
x: 1615324680000,
y: 34,
threshold: 2,
},
{
x: 1615324980000,
y: 35,
threshold: 2,
},
{
x: 1615325280000,
y: 31,
threshold: 2,
},
{
x: 1615325580000,
y: 36,
threshold: 2,
},
{
x: 1615325880000,
y: 35,
threshold: 2,
},
],
recovered: true,
},
{
'@timestamp': 1615326143423,
first_seen: 1615325783256,
severity: 'warning',
severity_value: 27,
reason: 'Error count for opbeans-java in production was above the threshold of 2 (27)',
rule_id: '335b38d0-80be-11eb-9fd1-d3725789930d',
rule_name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
rule_type_name: 'Error count threshold',
alert_instance_title: ['opbeans-java:production'],
alert_instance_name: 'opbeans-java_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: '73075d90-e27a-4e20-9ba0-3512a16c2829',
influencers: ['service.name:opbeans-java', 'service.environment:production'],
fields: {
'processor.event': 'error',
'service.name': 'opbeans-java',
'service.environment': 'production',
},
timeseries: [
{
x: 1615325760000,
y: 36,
threshold: 2,
},
{
x: 1615325820000,
y: 26,
threshold: 2,
},
{
x: 1615325880000,
y: 28,
threshold: 2,
},
{
x: 1615325940000,
y: 35,
threshold: 2,
},
{
x: 1615326000000,
y: 32,
threshold: 2,
},
{
x: 1615326060000,
y: 23,
threshold: 2,
},
{
x: 1615326120000,
y: 27,
threshold: 2,
},
],
recovered: true,
},
{
'@timestamp': 1615326143423,
first_seen: 1615323802378,
severity: 'warning',
severity_value: 4759.9116,
reason:
'Transaction duration for opbeans-java/request in production was above the threshold of 1.0 ms (4.8 ms)',
rule_id: 'cb1fc3e0-7fef-11eb-827d-d94e80a23d8d',
rule_name: 'Latency threshold | opbeans-java',
rule_type_id: 'apm.transaction_duration',
rule_type_name: 'Latency threshold',
alert_instance_title: ['opbeans-java/request:production'],
alert_instance_name: 'apm.transaction_duration_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: 'ffa0437d-6656-4553-a1cd-c170fc6e2f81',
influencers: [
'service.name:opbeans-java',
'service.environment:production',
'transaction.type:request',
],
fields: {
'processor.event': 'transaction',
'service.name': 'opbeans-java',
'service.environment': 'production',
'transaction.type': 'request',
},
timeseries: [
{
x: 1615323780000,
y: 13145.51171875,
threshold: 1000,
},
{
x: 1615324080000,
y: 15995.15625,
threshold: 1000,
},
{
x: 1615324380000,
y: 18974.59375,
threshold: 1000,
},
{
x: 1615324680000,
y: 11604.87890625,
threshold: 1000,
},
{
x: 1615324980000,
y: 17945.9609375,
threshold: 1000,
},
{
x: 1615325280000,
y: 9933.22265625,
threshold: 1000,
},
{
x: 1615325580000,
y: 10011.58984375,
threshold: 1000,
},
{
x: 1615325880000,
y: 10953.1845703125,
threshold: 1000,
},
],
recovered: true,
},
{
'@timestamp': 1615325663207,
first_seen: 1615324762861,
severity: 'warning',
severity_value: 27,
reason: 'Error count for opbeans-java in production was above the threshold of 2 (27)',
rule_id: '335b38d0-80be-11eb-9fd1-d3725789930d',
rule_name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
rule_type_name: 'Error count threshold',
alert_instance_title: ['opbeans-java:production'],
alert_instance_name: 'opbeans-java_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: 'bf5f9574-57c8-44ed-9a3c-512b446695cf',
influencers: ['service.name:opbeans-java', 'service.environment:production'],
fields: {
'processor.event': 'error',
'service.name': 'opbeans-java',
'service.environment': 'production',
},
timeseries: [
{
x: 1615324740000,
y: 34,
threshold: 2,
},
{
x: 1615325040000,
y: 35,
threshold: 2,
},
{
x: 1615325340000,
y: 31,
threshold: 2,
},
{
x: 1615325640000,
y: 27,
threshold: 2,
},
],
recovered: true,
},
{
'@timestamp': 1615324642764,
first_seen: 1615324402620,
severity: 'warning',
severity_value: 32,
reason: 'Error count for opbeans-java in production was above the threshold of 2 (32)',
rule_id: '335b38d0-80be-11eb-9fd1-d3725789930d',
rule_name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
rule_type_name: 'Error count threshold',
alert_instance_title: ['opbeans-java:production'],
alert_instance_name: 'opbeans-java_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: '87768bef-67a3-4ddd-b95d-7ab8830b30ef',
influencers: ['service.name:opbeans-java', 'service.environment:production'],
fields: {
'processor.event': 'error',
'service.name': 'opbeans-java',
'service.environment': 'production',
},
timeseries: [
{
x: 1615324402000,
y: 30,
threshold: 2,
},
{
x: 1615324432000,
y: null,
threshold: null,
},
{
x: 1615324462000,
y: 28,
threshold: 2,
},
{
x: 1615324492000,
y: null,
threshold: null,
},
{
x: 1615324522000,
y: 30,
threshold: 2,
},
{
x: 1615324552000,
y: null,
threshold: null,
},
{
x: 1615324582000,
y: 18,
threshold: 2,
},
{
x: 1615324612000,
y: null,
threshold: null,
},
{
x: 1615324642000,
y: 32,
threshold: 2,
},
],
recovered: true,
},
{
'@timestamp': 1615324282583,
first_seen: 1615323802378,
severity: 'warning',
severity_value: 30,
reason: 'Error count for opbeans-java in production was above the threshold of 2 (30)',
rule_id: '335b38d0-80be-11eb-9fd1-d3725789930d',
rule_name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
rule_type_name: 'Error count threshold',
alert_instance_title: ['opbeans-java:production'],
alert_instance_name: 'opbeans-java_production',
unique: 1,
group_by_field: 'alert_instance.uuid',
group_by_value: '31d087bd-51ae-419d-81c0-d0671eb97392',
influencers: ['service.name:opbeans-java', 'service.environment:production'],
fields: {
'processor.event': 'error',
'service.name': 'opbeans-java',
'service.environment': 'production',
},
timeseries: [
{
x: 1615323780000,
y: 31,
threshold: 2,
},
{
x: 1615323840000,
y: 30,
threshold: 2,
},
{
x: 1615323900000,
y: 24,
threshold: 2,
},
{
x: 1615323960000,
y: 32,
threshold: 2,
},
{
x: 1615324020000,
y: 32,
threshold: 2,
},
{
x: 1615324080000,
y: 30,
threshold: 2,
},
{
x: 1615324140000,
y: 25,
threshold: 2,
},
{
x: 1615324200000,
y: 34,
threshold: 2,
},
{
x: 1615324260000,
y: 30,
threshold: 2,
},
],
recovered: true,
},
];

View file

@ -0,0 +1,99 @@
/*
* 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 {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPage,
EuiPageHeader,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { RouteParams } from '../../routes';
import { AlertsSearchBar } from './alerts_search_bar';
import { AlertItem, AlertsTable } from './alerts_table';
import { wireframeData } from './example_data';
interface AlertsPageProps {
items?: AlertItem[];
routeParams: RouteParams<'/alerts'>;
}
export function AlertsPage({ items }: AlertsPageProps) {
// For now, if we're not passed any items load the example wireframe data.
if (!items) {
items = wireframeData;
}
const { core } = usePluginContext();
const { prepend } = core.http.basePath;
// In a future milestone we'll have a page dedicated to rule management in
// observability. For now link to the settings page.
const manageDetectionRulesHref = prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
);
return (
<EuiPage>
<EuiPageHeader
pageTitle={
<>
{i18n.translate('xpack.observability.alertsTitle', { defaultMessage: 'Alerts' })}{' '}
<ExperimentalBadge />
</>
}
rightSideItems={[
<EuiButton fill href={manageDetectionRulesHref} iconType="gear">
{i18n.translate('xpack.observability.alerts.manageDetectionRulesButtonLabel', {
defaultMessage: 'Manage detection rules',
})}
</EuiButton>,
]}
>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiCallOut
title={i18n.translate('xpack.observability.alertsDisclaimerTitle', {
defaultMessage: 'Experimental',
})}
color="warning"
iconType="beaker"
>
<p>
{i18n.translate('xpack.observability.alertsDisclaimerText', {
defaultMessage:
'This page shows an experimental alerting view. The data shown here will probably not be an accurate representation of alerts. A non-experimental list of alerts is available in the Alerts and Actions settings in Stack Management.',
})}
</p>
<p>
<EuiLink
href={prepend('/app/management/insightsAndAlerting/triggersActions/alerts')}
>
{i18n.translate('xpack.observability.alertsDisclaimerLinkText', {
defaultMessage: 'Alerts and Actions',
})}
</EuiLink>
</p>
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<AlertsSearchBar />
</EuiFlexItem>
<EuiFlexItem>
<AlertsTable items={items} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeader>
</EuiPage>
);
}

View file

@ -0,0 +1,24 @@
/*
* 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, { ComponentType } from 'react';
import { CasesPage } from '.';
import { RouteParams } from '../../routes';
export default {
title: 'app/Cases',
component: CasesPage,
decorators: [
(Story: ComponentType) => {
return <Story />;
},
],
};
export function EmptyState() {
return <CasesPage routeParams={{} as RouteParams<'/cases'>} />;
}

View file

@ -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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPage, EuiPageHeader } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
import { RouteParams } from '../../routes';
interface CasesProps {
routeParams: RouteParams<'/cases'>;
}
export function CasesPage(props: CasesProps) {
return (
<EuiPage>
<EuiPageHeader
pageTitle={
<>
{i18n.translate('xpack.observability.casesTitle', { defaultMessage: 'Cases' })}{' '}
<ExperimentalBadge />
</>
}
>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiCallOut
title={i18n.translate('xpack.observability.casesDisclaimerTitle', {
defaultMessage: 'Coming soon',
})}
color="warning"
iconType="beaker"
>
<p>
{i18n.translate('xpack.observability.casesDisclaimerText', {
defaultMessage: 'This is the future home of cases.',
})}
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeader>
</EuiPage>
);
}

View file

@ -38,25 +38,53 @@ export class Plugin implements PluginClass<ObservabilityPluginSetup, Observabili
constructor(context: PluginInitializerContext) {}
public setup(core: CoreSetup, plugins: ObservabilityPluginSetupDeps) {
const category = DEFAULT_APP_CATEGORIES.observability;
const euiIconType = 'logo-observability';
const mount = async (params: AppMountParameters<unknown>) => {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services
const [coreStart] = await core.getStartServices();
return renderApp(coreStart, plugins, params);
};
const updater$ = this.appUpdater$;
core.application.register({
id: 'observability-overview',
title: 'Overview',
order: 8000,
euiIconType: 'logoObservability',
appRoute: '/app/observability',
updater$: this.appUpdater$,
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters<unknown>) => {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services
const [coreStart] = await core.getStartServices();
return renderApp(coreStart, plugins, params);
},
order: 8000,
category,
euiIconType,
mount,
updater$,
});
if (core.uiSettings.get('observability:enableAlertingExperience')) {
core.application.register({
id: 'observability-alerts',
title: 'Alerts',
appRoute: '/app/observability/alerts',
order: 8025,
category,
euiIconType,
mount,
updater$,
});
core.application.register({
id: 'observability-cases',
title: 'Cases',
appRoute: '/app/observability/cases',
order: 8050,
category,
euiIconType,
mount,
updater$,
});
}
if (plugins.home) {
plugins.home.featureCatalogue.registerSolution({
id: 'observability',

View file

@ -12,6 +12,8 @@ import { HomePage } from '../pages/home';
import { LandingPage } from '../pages/landing';
import { OverviewPage } from '../pages/overview';
import { jsonRt } from './json_rt';
import { AlertsPage } from '../pages/alerts';
import { CasesPage } from '../pages/cases';
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
@ -25,6 +27,7 @@ export interface Params {
query?: t.HasProps;
path?: t.HasProps;
}
export const routes = {
'/': {
handler: () => {
@ -72,4 +75,44 @@ export const routes = {
},
],
},
'/cases': {
handler: (routeParams: any) => {
return <CasesPage routeParams={routeParams} />;
},
params: {
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
refreshPaused: jsonRt.pipe(t.boolean),
refreshInterval: jsonRt.pipe(t.number),
}),
},
breadcrumb: [
{
text: i18n.translate('xpack.observability.cases.breadcrumb', {
defaultMessage: 'Cases',
}),
},
],
},
'/alerts': {
handler: (routeParams: any) => {
return <AlertsPage routeParams={routeParams} />;
},
params: {
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
refreshPaused: jsonRt.pipe(t.boolean),
refreshInterval: jsonRt.pipe(t.number),
}),
},
breadcrumb: [
{
text: i18n.translate('xpack.observability.alerts.breadcrumb', {
defaultMessage: 'Alerts',
}),
},
],
},
};

View file

@ -13,6 +13,7 @@ import {
ScopedAnnotationsClientFactory,
AnnotationsAPI,
} from './lib/annotations/bootstrap_annotations';
import { uiSettings } from './ui_settings';
type LazyScopedAnnotationsClientFactory = (
...args: Parameters<ScopedAnnotationsClientFactory>
@ -32,6 +33,8 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
let annotationsApiPromise: Promise<AnnotationsAPI> | undefined;
core.uiSettings.register(uiSettings);
if (config.annotations.enabled) {
annotationsApiPromise = bootstrapAnnotations({
core,

View file

@ -0,0 +1,32 @@
/*
* 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 } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from '../../../../src/core/types';
import { enableAlertingExperience } from '../common/ui_settings_keys';
/**
* uiSettings definitions for Observability.
*/
export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
[enableAlertingExperience]: {
category: ['observability'],
name: i18n.translate('xpack.observability.enableAlertingExperienceExperimentName', {
defaultMessage: 'Observability alerting experience',
}),
value: false,
description: i18n.translate(
'xpack.observability.enableAlertingExperienceExperimentDescription',
{
defaultMessage:
'Enable the experimental alerting experience for Observability. Adds the Alerts and Cases pages.',
}
),
schema: schema.boolean(),
},
};