Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Monitoring] Kibana Alerting #48464

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const CLOUD_METADATA_SERVICES = {

// GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes)
// To bypass potential DNS changes, the IP was used because it's shared with other cloud services
GCP_URL_PREFIX: 'http://169.254.169.254/computeMetadata/v1/instance'
GCP_URL_PREFIX: 'http://169.254.169.254/computeMetadata/v1/instance',
};

/**
Expand All @@ -122,13 +122,13 @@ export const LOGSTASH = {
MAJOR_VER_REQD_FOR_PIPELINES: 6,

/*
* Names ES keys on for different Logstash pipeline queues.
* @type {string}
*/
* Names ES keys on for different Logstash pipeline queues.
* @type {string}
*/
QUEUE_TYPES: {
MEMORY: 'memory',
PERSISTED: 'persisted'
}
PERSISTED: 'persisted',
},
};

export const DEBOUNCE_SLOW_MS = 17; // roughly how long it takes to render a frame at 60fps
Expand Down Expand Up @@ -162,12 +162,12 @@ export const ELASTICSEARCH_SYSTEM_ID = 'elasticsearch';
export const INFRA_SOURCE_ID = 'internal-stack-monitoring';

/*
* These constants represent code paths within `getClustersFromRequest`
* that an api call wants to invoke. This is meant as an optimization to
* avoid unnecessary ES queries (looking at you logstash) when the data
* is not used. In the long term, it'd be nice to have separate api calls
* instead of this path logic.
*/
* These constants represent code paths within `getClustersFromRequest`
* that an api call wants to invoke. This is meant as an optimization to
* avoid unnecessary ES queries (looking at you logstash) when the data
* is not used. In the long term, it'd be nice to have separate api calls
* instead of this path logic.
*/
export const CODE_PATH_ALL = 'all';
export const CODE_PATH_ALERTS = 'alerts';
export const CODE_PATH_KIBANA = 'kibana';
Expand Down Expand Up @@ -222,3 +222,39 @@ export const REPORTING_SYSTEM_ID = 'reporting';
* @type {Number}
*/
export const TELEMETRY_COLLECTION_INTERVAL = 86400000;

/**
* We want to slowly rollout the migration from watcher-based cluster alerts to
* kibana alerts and we only want to enable the kibana alerts once all
* watcher-based cluster alerts have been migrated so this flag will serve
* as the only way to see the new UI and actually run Kibana alerts. It will
* be false until all alerts have been migrated, then it will be removed
*/
export const KIBANA_ALERTING_ENABLED = true;

/**
* The prefix for all alert types used by monitoring
*/
export const ALERT_TYPE_PREFIX = 'monitoring_';

/**
* This is the alert type id for the license expiration alert
*/
export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`;

/**
* Matches the id for the built-in in email action type
* See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts
*/
export const ALERT_ACTION_TYPE_EMAIL = '.email';

/**
* The number of alerts that have been migrated
*/
export const NUMBER_OF_MIGRATED_ALERTS = 1;

/**
* We store config data in a single saved object of this id
*/
export const MONITORING_CONFIG_SAVED_OBJECT_ID = 'monitoring';
export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'alertingEmailAddress';
5 changes: 3 additions & 2 deletions x-pack/legacy/plugins/monitoring/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { initInfraSource } from './server/lib/logs/init_infra_source';
* @return {Object} Monitoring UI Kibana plugin object
*/
export const monitoring = (kibana) => new kibana.Plugin({
require: ['kibana', 'elasticsearch', 'xpack_main'],
require: ['kibana', 'elasticsearch', 'xpack_main', 'alerting'],
id: 'monitoring',
configPrefix: 'xpack.monitoring',
publicDir: resolve(__dirname, 'public'),
Expand Down Expand Up @@ -48,6 +48,7 @@ export const monitoring = (kibana) => new kibana.Plugin({

const serverConfig = server.config();
const serverFacade = {
newPlatform: server.newPlatform,
config: () => ({
get: key => {
if (configs.includes(key)) {
Expand All @@ -74,7 +75,7 @@ export const monitoring = (kibana) => new kibana.Plugin({
const plugins = {
xpack_main: server.plugins.xpack_main,
elasticsearch: server.plugins.elasticsearch,
infra: server.plugins.infra,
alerting: server.plugins.alerting,
};

new Plugin().setup(serverFacade, plugins);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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 {
EuiForm,
EuiFormRow,
EuiFieldText,
EuiLink,
EuiSpacer,
EuiFieldNumber,
EuiFieldPassword,
EuiSwitch,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ActionResult } from '../../../../actions/server/types';
import { getMissingFieldErrors, hasErrors } from '../../lib/form_validation';

export interface EmailActionData {
service: string;
host: string;
port: number | string; // support a string to ensure the user can backspace to an empty field
secure: boolean;
from: string;
user: string;
password: string;
}

interface ManageActionModalProps {
createEmailAction: (handler: EmailActionData) => void;
deleteEmailAction: () => void;
cancel?: () => void;
isNew: boolean;
action?: ActionResult | null;
}

const DEFAULT_DATA: EmailActionData = {
service: '',
host: '',
port: 0,
secure: false,
from: '',
user: '',
password: '',
};

const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', {
defaultMessage: 'Create email action',
});
const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', {
defaultMessage: 'Save email action',
});
const DELETE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.deleteLabel', {
defaultMessage: 'Delete',
});
const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', {
defaultMessage: 'Cancel',
});

export const ManageEmailAction: React.FC<ManageActionModalProps> = (
props: ManageActionModalProps
) => {
const { createEmailAction, deleteEmailAction, cancel, isNew, action } = props;

const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {});
const [isSaving, setIsSaving] = React.useState(false);
const [showErrors, setShowErrors] = React.useState(false);
const [errors, setErrors] = React.useState<EmailActionData | any>(
getMissingFieldErrors(defaultData, DEFAULT_DATA)
);
const [data, setData] = React.useState(defaultData);

React.useEffect(() => {
setErrors(getMissingFieldErrors(data, DEFAULT_DATA));
}, [data]);

function saveEmailAction() {
setShowErrors(true);
if (!hasErrors(errors)) {
setShowErrors(false);
setIsSaving(true);
createEmailAction(data);
}
}

return (
<EuiForm isInvalid={showErrors} error={Object.values(errors)}>
<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceText', {
defaultMessage: 'Service',
})}
helpText={
<EuiLink target="_blank" href="https://nodemailer.com/smtp/well-known/">
{i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', {
defaultMessage: 'Find out more',
})}
</EuiLink>
}
error={errors.service}
isInvalid={showErrors && !!errors.service}
>
<EuiFieldText
value={data.service}
onChange={e => setData({ ...data, service: e.target.value })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostText', {
defaultMessage: 'Host',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostHelpText', {
defaultMessage: 'Host name of the service provider',
})}
error={errors.host}
isInvalid={showErrors && !!errors.host}
>
<EuiFieldText
value={data.host}
onChange={e => setData({ ...data, host: e.target.value })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portText', {
defaultMessage: 'Port',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portHelpText', {
defaultMessage: 'Port number of the service provider',
})}
error={errors.port}
isInvalid={showErrors && !!errors.port}
>
<EuiFieldNumber
value={data.port}
onChange={e => setData({ ...data, port: parseInt(e.target.value, 10) || '' })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureText', {
defaultMessage: 'Secure',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureHelpText', {
defaultMessage: 'Whether to use TLS with the service provider',
})}
>
<EuiSwitch
checked={data.secure}
onChange={e => setData({ ...data, secure: e.target.checked })}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromText', {
defaultMessage: 'From',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromHelpText', {
defaultMessage: 'The from email address for alerts',
})}
error={errors.from}
isInvalid={showErrors && !!errors.from}
>
<EuiFieldText
value={data.from}
onChange={e => setData({ ...data, from: e.target.value })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userText', {
defaultMessage: 'User',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userHelpText', {
defaultMessage: 'The user to use with the service provider',
})}
error={errors.user}
isInvalid={showErrors && !!errors.user}
>
<EuiFieldText
value={data.user}
onChange={e => setData({ ...data, user: e.target.value })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordText', {
defaultMessage: 'Password',
})}
helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordHelpText', {
defaultMessage: 'The password to use with the service provider',
})}
error={errors.password}
isInvalid={showErrors && !!errors.password}
>
<EuiFieldPassword
value={data.password}
onChange={e => setData({ ...data, password: e.target.value })}
isInvalid={showErrors}
/>
</EuiFormRow>

<EuiSpacer />

<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton type="submit" fill onClick={saveEmailAction} isLoading={isSaving}>
{isNew ? CREATE_LABEL : SAVE_LABEL}
</EuiButton>
</EuiFlexItem>
{!action || isNew ? null : (
<EuiFlexItem grow={false}>
<EuiButton onClick={cancel}>{CANCEL_LABEL}</EuiButton>
</EuiFlexItem>
)}
{isNew ? null : (
<EuiFlexItem grow={false}>
<EuiButton onClick={deleteEmailAction} color="danger" isLoading={isSaving}>
{DELETE_LABEL}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiForm>
);
};
Loading