[APM] Remove watcher integration (#71655)
This commit is contained in:
parent
51a862988c
commit
f760d8513b
|
@ -1,635 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiRadio,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { padStart, range } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
|
||||
import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
|
||||
import { ApmPluginContext } from '../../../../context/ApmPluginContext';
|
||||
import { getApmIndexPatternTitle } from '../../../../services/rest/index_pattern';
|
||||
|
||||
type ScheduleKey = keyof Schedule;
|
||||
|
||||
const SmallInput = styled.div`
|
||||
.euiFormRow {
|
||||
max-width: 85px;
|
||||
}
|
||||
.euiFormHelpText {
|
||||
width: 200px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface WatcherFlyoutProps {
|
||||
urlParams: IUrlParams;
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
type IntervalUnit = 'm' | 'h';
|
||||
|
||||
interface WatcherFlyoutState {
|
||||
schedule: ScheduleKey;
|
||||
threshold: number;
|
||||
actions: {
|
||||
slack: boolean;
|
||||
email: boolean;
|
||||
};
|
||||
interval: {
|
||||
value: number;
|
||||
unit: IntervalUnit;
|
||||
};
|
||||
daily: string;
|
||||
emails: string;
|
||||
slackUrl: string;
|
||||
}
|
||||
|
||||
export class WatcherFlyout extends Component<
|
||||
WatcherFlyoutProps,
|
||||
WatcherFlyoutState
|
||||
> {
|
||||
static contextType = ApmPluginContext;
|
||||
context!: React.ContextType<typeof ApmPluginContext>;
|
||||
public state: WatcherFlyoutState = {
|
||||
schedule: 'daily',
|
||||
threshold: 10,
|
||||
actions: {
|
||||
slack: false,
|
||||
email: false,
|
||||
},
|
||||
interval: {
|
||||
value: 10,
|
||||
unit: 'm',
|
||||
},
|
||||
daily: '08:00',
|
||||
emails: '',
|
||||
slackUrl: '',
|
||||
};
|
||||
|
||||
public onChangeSchedule = (schedule: ScheduleKey) => {
|
||||
this.setState({ schedule });
|
||||
};
|
||||
|
||||
public onChangeThreshold = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
threshold: parseInt(event.target.value, 10),
|
||||
});
|
||||
};
|
||||
|
||||
public onChangeDailyUnit = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
this.setState({
|
||||
daily: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
public onChangeIntervalValue = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
interval: {
|
||||
value: parseInt(event.target.value, 10),
|
||||
unit: this.state.interval.unit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public onChangeIntervalUnit = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
this.setState({
|
||||
interval: {
|
||||
value: this.state.interval.value,
|
||||
unit: event.target.value as IntervalUnit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public onChangeAction = (actionName: 'slack' | 'email') => {
|
||||
this.setState({
|
||||
actions: {
|
||||
...this.state.actions,
|
||||
[actionName]: !this.state.actions[actionName],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public onChangeEmails = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ emails: event.target.value });
|
||||
};
|
||||
|
||||
public onChangeSlackUrl = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ slackUrl: event.target.value });
|
||||
};
|
||||
|
||||
public createWatch = () => {
|
||||
const { serviceName } = this.props.urlParams;
|
||||
const { core } = this.context;
|
||||
|
||||
if (!serviceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emails = this.state.actions.email
|
||||
? this.state.emails
|
||||
.split(',')
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => !!email)
|
||||
: [];
|
||||
|
||||
const slackUrl = this.state.actions.slack ? this.state.slackUrl : '';
|
||||
|
||||
const schedule =
|
||||
this.state.schedule === 'interval'
|
||||
? {
|
||||
interval: `${this.state.interval.value}${this.state.interval.unit}`,
|
||||
}
|
||||
: {
|
||||
daily: { at: `${this.state.daily}` },
|
||||
};
|
||||
|
||||
const timeRange =
|
||||
this.state.schedule === 'interval'
|
||||
? {
|
||||
value: this.state.interval.value,
|
||||
unit: this.state.interval.unit,
|
||||
}
|
||||
: {
|
||||
value: 24,
|
||||
unit: 'h',
|
||||
};
|
||||
|
||||
return getApmIndexPatternTitle()
|
||||
.then((indexPatternTitle) => {
|
||||
return createErrorGroupWatch({
|
||||
http: core.http,
|
||||
emails,
|
||||
schedule,
|
||||
serviceName,
|
||||
slackUrl,
|
||||
threshold: this.state.threshold,
|
||||
timeRange,
|
||||
apmIndexPatternTitle: indexPatternTitle,
|
||||
}).then((id: string) => {
|
||||
this.props.onClose();
|
||||
this.addSuccessToast(id);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
this.addErrorToast();
|
||||
});
|
||||
};
|
||||
|
||||
public addErrorToast = () => {
|
||||
const { core } = this.context;
|
||||
|
||||
core.notifications.toasts.addWarning({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Watch creation failed',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Make sure your user has permission to create watches.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
public addSuccessToast = (id: string) => {
|
||||
const { core } = this.context;
|
||||
|
||||
core.notifications.toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'New watch created!',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The watch is now ready and will send error reports for {serviceName}.',
|
||||
values: {
|
||||
serviceName: this.props.urlParams.serviceName,
|
||||
},
|
||||
}
|
||||
)}{' '}
|
||||
<ApmPluginContext.Provider value={this.context}>
|
||||
<KibanaLink
|
||||
path={`/management/insightsAndAlerting/watcher/watches/watch/${id}`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
|
||||
{
|
||||
defaultMessage: 'View watch',
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
</ApmPluginContext.Provider>
|
||||
</p>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dailyTime = this.state.daily;
|
||||
const inputTime = `${dailyTime}Z`; // Add tz to make into UTC
|
||||
const inputFormat = 'HH:mmZ'; // Parse as 24 hour w. tz
|
||||
const dailyTimeFormatted = moment(inputTime, inputFormat).format('HH:mm'); // Format as 24h
|
||||
const dailyTime12HourFormatted = moment(inputTime, inputFormat).format(
|
||||
'hh:mm A (z)'
|
||||
); // Format as 12h w. tz
|
||||
|
||||
// Generate UTC hours for Daily Report select field
|
||||
const intervalHours = range(24).map((i) => {
|
||||
const hour = padStart(i.toString(), 2, '0');
|
||||
return { value: `${hour}:00`, text: `${hour}:00 UTC` };
|
||||
});
|
||||
|
||||
const flyoutBody = (
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription"
|
||||
defaultMessage="This form will assist in creating a Watch that can notify you of error occurrences from this service.
|
||||
To learn more about Watcher, please read our {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<ElasticDocsLink
|
||||
target="_blank"
|
||||
section="/x-pack"
|
||||
path="/watcher-getting-started.html"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation',
|
||||
}
|
||||
)}
|
||||
</ElasticDocsLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<EuiForm>
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle',
|
||||
{
|
||||
defaultMessage: 'Condition',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdLabel',
|
||||
{
|
||||
defaultMessage: 'Occurrences threshold per error group',
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Threshold to be met for error group to be included in report.',
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldNumber
|
||||
icon="number"
|
||||
min={1}
|
||||
value={this.state.threshold}
|
||||
onChange={this.onChangeThreshold}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle',
|
||||
{
|
||||
defaultMessage: 'Trigger schedule',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose the time interval for the report, when the threshold is exceeded.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiRadio
|
||||
id="daily"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportRadioButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Daily report',
|
||||
}
|
||||
)}
|
||||
onChange={() => this.onChangeSchedule('daily')}
|
||||
checked={this.state.schedule === 'daily'}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The daily report will be sent at {dailyTimeFormatted} / {dailyTime12HourFormatted}.',
|
||||
values: { dailyTimeFormatted, dailyTime12HourFormatted },
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiSelect
|
||||
value={dailyTime}
|
||||
onChange={this.onChangeDailyUnit}
|
||||
options={intervalHours}
|
||||
disabled={this.state.schedule !== 'daily'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiRadio
|
||||
id="interval"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalRadioButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Interval',
|
||||
}
|
||||
)}
|
||||
onChange={() => this.onChangeSchedule('interval')}
|
||||
checked={this.state.schedule === 'interval'}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SmallInput>
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalHelpText',
|
||||
{
|
||||
defaultMessage: 'Time interval between reports.',
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
icon="clock"
|
||||
min={1}
|
||||
value={this.state.interval.value}
|
||||
onChange={this.onChangeIntervalValue}
|
||||
disabled={this.state.schedule !== 'interval'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</SmallInput>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow compressed>
|
||||
<EuiSelect
|
||||
value={this.state.interval.unit}
|
||||
onChange={this.onChangeIntervalUnit}
|
||||
compressed
|
||||
options={[
|
||||
{
|
||||
value: 'm',
|
||||
text: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.minsLabel',
|
||||
{
|
||||
defaultMessage: 'mins',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'h',
|
||||
text: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.hrsLabel',
|
||||
{
|
||||
defaultMessage: 'hrs',
|
||||
}
|
||||
),
|
||||
},
|
||||
]}
|
||||
disabled={this.state.schedule !== 'interval'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle',
|
||||
{
|
||||
defaultMessage: 'Actions',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Reports can be sent by email or posted to a Slack channel. Each report will include the top 10 errors sorted by occurrence.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.sendEmailLabel',
|
||||
{
|
||||
defaultMessage: 'Send email',
|
||||
}
|
||||
)}
|
||||
checked={this.state.actions.email}
|
||||
onChange={() => this.onChangeAction('email')}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{this.state.actions.email && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsLabel',
|
||||
{
|
||||
defaultMessage: 'Recipients (separated with comma)',
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
helpText={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText"
|
||||
defaultMessage="If you have not configured email, please see the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<ElasticDocsLink
|
||||
target="_blank"
|
||||
section="/x-pack"
|
||||
path="/actions-email.html#configuring-email"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation',
|
||||
}
|
||||
)}
|
||||
</ElasticDocsLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
icon="user"
|
||||
value={this.state.emails}
|
||||
onChange={this.onChangeEmails}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.sendSlackNotificationLabel',
|
||||
{
|
||||
defaultMessage: 'Send Slack notification',
|
||||
}
|
||||
)}
|
||||
checked={this.state.actions.slack}
|
||||
onChange={() => this.onChangeAction('slack')}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{this.state.actions.slack && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLLabel',
|
||||
{
|
||||
defaultMessage: 'Slack Webhook URL',
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
helpText={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText"
|
||||
defaultMessage="To get a Slack webhook, please see the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href="https://get.slack.help/hc/en-us/articles/115005265063-Incoming-WebHooks-for-Slack"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
icon="link"
|
||||
value={this.state.slackUrl}
|
||||
onChange={this.onChangeSlackUrl}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiForm>
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={this.props.onClose} size="s">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle',
|
||||
{
|
||||
defaultMessage: 'Enable error reports',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{flyoutBody}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => this.createWatch()}
|
||||
fill
|
||||
disabled={
|
||||
!this.state.actions.email && !this.state.actions.slack
|
||||
}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create watch',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`createErrorGroupWatch should format email correctly 1`] = `
|
||||
"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
|
||||
|
||||
|
||||
<strong>this is a string</strong>
|
||||
N/A
|
||||
7761 occurrences
|
||||
|
||||
<strong>foo</strong>
|
||||
<anonymous> (server/coffee.js)
|
||||
7752 occurrences
|
||||
|
||||
<strong>socket hang up</strong>
|
||||
createHangUpError (_http_client.js)
|
||||
3887 occurrences
|
||||
|
||||
<strong>this will not get captured by express</strong>
|
||||
<anonymous> (server/coffee.js)
|
||||
3886 occurrences
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`createErrorGroupWatch should format slack message correctly 1`] = `
|
||||
"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
|
||||
|
||||
>*this is a string*
|
||||
>N/A
|
||||
>7761 occurrences
|
||||
|
||||
>*foo*
|
||||
>\`<anonymous> (server/coffee.js)\`
|
||||
>7752 occurrences
|
||||
|
||||
>*socket hang up*
|
||||
>\`createHangUpError (_http_client.js)\`
|
||||
>3887 occurrences
|
||||
|
||||
>*this will not get captured by express*
|
||||
>\`<anonymous> (server/coffee.js)\`
|
||||
>3886 occurrences
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`createErrorGroupWatch should format template correctly 1`] = `
|
||||
Object {
|
||||
"actions": Object {
|
||||
"email": Object {
|
||||
"email": Object {
|
||||
"body": Object {
|
||||
"html": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"<br/><br/><br/><strong>this is a string</strong><br/>N/A<br/>7761 occurrences<br/><br/><strong>foo</strong><br/><anonymous> (server/coffee.js)<br/>7752 occurrences<br/><br/><strong>socket hang up</strong><br/>createHangUpError (_http_client.js)<br/>3887 occurrences<br/><br/><strong>this will not get captured by express</strong><br/><anonymous> (server/coffee.js)<br/>3886 occurrences<br/>",
|
||||
},
|
||||
"subject": "\\"opbeans-node\\" has error groups which exceeds the threshold",
|
||||
"to": "my@email.dk,mySecond@email.dk",
|
||||
},
|
||||
},
|
||||
"log_error": Object {
|
||||
"logging": Object {
|
||||
"text": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"<br/><br/><br/><strong>this is a string</strong><br/>N/A<br/>7761 occurrences<br/><br/><strong>foo</strong><br/><anonymous> (server/coffee.js)<br/>7752 occurrences<br/><br/><strong>socket hang up</strong><br/>createHangUpError (_http_client.js)<br/>3887 occurrences<br/><br/><strong>this will not get captured by express</strong><br/><anonymous> (server/coffee.js)<br/>3886 occurrences<br/>",
|
||||
},
|
||||
},
|
||||
"slack_webhook": Object {
|
||||
"webhook": Object {
|
||||
"body": "__json__::{\\"text\\":\\"Your service \\\\\\"opbeans-node\\\\\\" has error groups which exceeds 10 occurrences within \\\\\\"24h\\\\\\"\\\\n\\\\n>*this is a string*\\\\n>N/A\\\\n>7761 occurrences\\\\n\\\\n>*foo*\\\\n>\`<anonymous> (server/coffee.js)\`\\\\n>7752 occurrences\\\\n\\\\n>*socket hang up*\\\\n>\`createHangUpError (_http_client.js)\`\\\\n>3887 occurrences\\\\n\\\\n>*this will not get captured by express*\\\\n>\`<anonymous> (server/coffee.js)\`\\\\n>3886 occurrences\\\\n\\"}",
|
||||
"headers": Object {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"host": "hooks.slack.com",
|
||||
"method": "POST",
|
||||
"path": "/services/slackid1/slackid2/slackid3",
|
||||
"port": 443,
|
||||
"scheme": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": Object {
|
||||
"script": Object {
|
||||
"source": "return ctx.payload.aggregations.error_groups.buckets.length > 0",
|
||||
},
|
||||
},
|
||||
"input": Object {
|
||||
"search": Object {
|
||||
"request": Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"error_groups": Object {
|
||||
"aggs": Object {
|
||||
"sample": Object {
|
||||
"top_hits": Object {
|
||||
"_source": Array [
|
||||
"error.log.message",
|
||||
"error.exception.message",
|
||||
"error.exception.handled",
|
||||
"error.culprit",
|
||||
"error.grouping_key",
|
||||
"@timestamp",
|
||||
],
|
||||
"size": 1,
|
||||
"sort": Array [
|
||||
Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "error.grouping_key",
|
||||
"min_doc_count": "10",
|
||||
"order": Object {
|
||||
"_count": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "opbeans-node",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"processor.event": "error",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-24h",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"indices": Array [
|
||||
"myIndexPattern",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"metadata": Object {
|
||||
"emails": Array [
|
||||
"my@email.dk",
|
||||
"mySecond@email.dk",
|
||||
],
|
||||
"serviceName": "opbeans-node",
|
||||
"slackUrlPath": "/services/slackid1/slackid2/slackid3",
|
||||
"threshold": 10,
|
||||
"timeRangeUnit": "h",
|
||||
"timeRangeValue": 24,
|
||||
"trigger": "This value must be changed in trigger section",
|
||||
},
|
||||
"trigger": Object {
|
||||
"schedule": Object {
|
||||
"daily": Object {
|
||||
"at": "08:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* 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 { isArray, isObject, isString } from 'lodash';
|
||||
import mustache from 'mustache';
|
||||
import uuid from 'uuid';
|
||||
import * as rest from '../../../../../services/rest/watcher';
|
||||
import { createErrorGroupWatch } from '../createErrorGroupWatch';
|
||||
import { esResponse } from './esResponse';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
|
||||
// disable html escaping since this is also disabled in watcher\s mustache implementation
|
||||
mustache.escape = (value) => value;
|
||||
|
||||
jest.mock('../../../../../services/rest/callApi', () => ({
|
||||
callApi: () => Promise.resolve(null),
|
||||
}));
|
||||
|
||||
describe('createErrorGroupWatch', () => {
|
||||
let createWatchResponse: string;
|
||||
let tmpl: any;
|
||||
const createWatchSpy = jest
|
||||
.spyOn(rest, 'createWatch')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid'));
|
||||
|
||||
createWatchResponse = await createErrorGroupWatch({
|
||||
http: {} as HttpSetup,
|
||||
emails: ['my@email.dk', 'mySecond@email.dk'],
|
||||
schedule: {
|
||||
daily: {
|
||||
at: '08:00',
|
||||
},
|
||||
},
|
||||
serviceName: 'opbeans-node',
|
||||
slackUrl: 'https://hooks.slack.com/services/slackid1/slackid2/slackid3',
|
||||
threshold: 10,
|
||||
timeRange: { value: 24, unit: 'h' },
|
||||
apmIndexPatternTitle: 'myIndexPattern',
|
||||
});
|
||||
|
||||
const watchBody = createWatchSpy.mock.calls[0][0].watch;
|
||||
const templateCtx = {
|
||||
payload: esResponse,
|
||||
metadata: watchBody.metadata,
|
||||
};
|
||||
|
||||
tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx);
|
||||
});
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
it('should call createWatch with correct args', () => {
|
||||
expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid');
|
||||
});
|
||||
|
||||
it('should format slack message correctly', () => {
|
||||
expect(tmpl.actions.slack_webhook.webhook.path).toBe(
|
||||
'/services/slackid1/slackid2/slackid3'
|
||||
);
|
||||
|
||||
expect(
|
||||
JSON.parse(tmpl.actions.slack_webhook.webhook.body.slice(10)).text
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format email correctly', () => {
|
||||
expect(tmpl.actions.email.email.to).toEqual(
|
||||
'my@email.dk,mySecond@email.dk'
|
||||
);
|
||||
expect(tmpl.actions.email.email.subject).toBe(
|
||||
'"opbeans-node" has error groups which exceeds the threshold'
|
||||
);
|
||||
expect(
|
||||
tmpl.actions.email.email.body.html.replace(/<br\/>/g, '\n')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format template correctly', () => {
|
||||
expect(tmpl).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return watch id', async () => {
|
||||
const id = createWatchSpy.mock.calls[0][0].id;
|
||||
expect(createWatchResponse).toEqual(id);
|
||||
});
|
||||
});
|
||||
|
||||
// Recursively iterate a nested structure and render strings as mustache templates
|
||||
type InputOutput = string | string[] | Record<string, any>;
|
||||
function renderMustache(
|
||||
input: InputOutput,
|
||||
ctx: Record<string, unknown>
|
||||
): InputOutput {
|
||||
if (isString(input)) {
|
||||
return mustache.render(input, {
|
||||
ctx,
|
||||
join: () => (text: string, render: any) => render(`{{${text}}}`, { ctx }),
|
||||
});
|
||||
}
|
||||
|
||||
if (isArray(input)) {
|
||||
return input.map((itemValue) => renderMustache(itemValue, ctx));
|
||||
}
|
||||
|
||||
if (isObject(input)) {
|
||||
return Object.keys(input).reduce((acc, key) => {
|
||||
const value = (input as any)[key];
|
||||
|
||||
return { ...acc, [key]: renderMustache(value, ctx) };
|
||||
}, {});
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const esResponse = {
|
||||
took: 454,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 10,
|
||||
successful: 10,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 23287,
|
||||
max_score: 0,
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
error_groups: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: '63925d00b445cdf4b532dd09d185f5c6',
|
||||
doc_count: 7761,
|
||||
sample: {
|
||||
hits: {
|
||||
total: 7761,
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'apm-7.0.0-alpha1-error-2018.04.25',
|
||||
_id: 'qH7C_WIBcmGuKeCHJvvT',
|
||||
_score: null,
|
||||
_source: {
|
||||
'@timestamp': '2018-04-25T17:03:02.296Z',
|
||||
error: {
|
||||
log: {
|
||||
message: 'this is a string',
|
||||
},
|
||||
grouping_key: '63925d00b445cdf4b532dd09d185f5c6',
|
||||
},
|
||||
},
|
||||
sort: [1524675782296],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '89bb1a1f644c7f4bbe8d1781b5cb5fd5',
|
||||
doc_count: 7752,
|
||||
sample: {
|
||||
hits: {
|
||||
total: 7752,
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'apm-7.0.0-alpha1-error-2018.04.25',
|
||||
_id: '_3_D_WIBcmGuKeCHFwOW',
|
||||
_score: null,
|
||||
_source: {
|
||||
'@timestamp': '2018-04-25T17:04:03.504Z',
|
||||
error: {
|
||||
exception: [
|
||||
{
|
||||
handled: true,
|
||||
message: 'foo',
|
||||
},
|
||||
],
|
||||
culprit: '<anonymous> (server/coffee.js)',
|
||||
grouping_key: '89bb1a1f644c7f4bbe8d1781b5cb5fd5',
|
||||
},
|
||||
},
|
||||
sort: [1524675843504],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '7a17ea60604e3531bd8de58645b8631f',
|
||||
doc_count: 3887,
|
||||
sample: {
|
||||
hits: {
|
||||
total: 3887,
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'apm-7.0.0-alpha1-error-2018.04.25',
|
||||
_id: 'dn_D_WIBcmGuKeCHQgXJ',
|
||||
_score: null,
|
||||
_source: {
|
||||
'@timestamp': '2018-04-25T17:04:14.575Z',
|
||||
error: {
|
||||
exception: [
|
||||
{
|
||||
handled: false,
|
||||
message: 'socket hang up',
|
||||
},
|
||||
],
|
||||
culprit: 'createHangUpError (_http_client.js)',
|
||||
grouping_key: '7a17ea60604e3531bd8de58645b8631f',
|
||||
},
|
||||
},
|
||||
sort: [1524675854575],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'b9e1027f29c221763f864f6fa2ad9f5e',
|
||||
doc_count: 3886,
|
||||
sample: {
|
||||
hits: {
|
||||
total: 3886,
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'apm-7.0.0-alpha1-error-2018.04.25',
|
||||
_id: 'dX_D_WIBcmGuKeCHQgXJ',
|
||||
_score: null,
|
||||
_source: {
|
||||
'@timestamp': '2018-04-25T17:04:14.533Z',
|
||||
error: {
|
||||
exception: [
|
||||
{
|
||||
handled: false,
|
||||
message: 'this will not get captured by express',
|
||||
},
|
||||
],
|
||||
culprit: '<anonymous> (server/coffee.js)',
|
||||
grouping_key: 'b9e1027f29c221763f864f6fa2ad9f5e',
|
||||
},
|
||||
},
|
||||
sort: [1524675854533],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,261 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import url from 'url';
|
||||
import uuid from 'uuid';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import {
|
||||
ERROR_CULPRIT,
|
||||
ERROR_EXC_HANDLED,
|
||||
ERROR_EXC_MESSAGE,
|
||||
ERROR_GROUP_ID,
|
||||
ERROR_LOG_MESSAGE,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { createWatch } from '../../../../services/rest/watcher';
|
||||
|
||||
function getSlackPathUrl(slackUrl?: string) {
|
||||
if (slackUrl) {
|
||||
const { path } = url.parse(slackUrl);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
interval?: string;
|
||||
daily?: {
|
||||
at: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Arguments {
|
||||
http: HttpSetup;
|
||||
emails: string[];
|
||||
schedule: Schedule;
|
||||
serviceName: string;
|
||||
slackUrl?: string;
|
||||
threshold: number;
|
||||
timeRange: {
|
||||
value: number;
|
||||
unit: string;
|
||||
};
|
||||
apmIndexPatternTitle: string;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
log_error: { logging: { text: string } };
|
||||
slack_webhook?: Record<string, unknown>;
|
||||
email?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function createErrorGroupWatch({
|
||||
http,
|
||||
emails = [],
|
||||
schedule,
|
||||
serviceName,
|
||||
slackUrl,
|
||||
threshold,
|
||||
timeRange,
|
||||
apmIndexPatternTitle,
|
||||
}: Arguments) {
|
||||
const id = `apm-${uuid.v4()}`;
|
||||
|
||||
const slackUrlPath = getSlackPathUrl(slackUrl);
|
||||
const emailTemplate = i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.emailTemplateText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange}{br}' +
|
||||
'{br}' +
|
||||
'{errorGroupsBuckets}{br}' +
|
||||
'{errorLogMessage}{br}' +
|
||||
'{errorCulprit}N/A{slashErrorCulprit}{br}' +
|
||||
'{docCountParam} occurrences{br}' +
|
||||
'{slashErrorGroupsBucket}',
|
||||
values: {
|
||||
serviceName: '"{{ctx.metadata.serviceName}}"',
|
||||
threshold: '{{ctx.metadata.threshold}}',
|
||||
timeRange:
|
||||
'"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"',
|
||||
errorGroupsBuckets:
|
||||
'{{#ctx.payload.aggregations.error_groups.buckets}}',
|
||||
errorLogMessage:
|
||||
'<strong>{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}</strong>',
|
||||
errorCulprit:
|
||||
'{{sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}',
|
||||
slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}',
|
||||
docCountParam: '{{doc_count}}',
|
||||
slashErrorGroupsBucket:
|
||||
'{{/ctx.payload.aggregations.error_groups.buckets}}',
|
||||
br: '<br/>',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const slackTemplate = i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.slackTemplateText',
|
||||
{
|
||||
defaultMessage: `Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange}
|
||||
{errorGroupsBuckets}
|
||||
{errorLogMessage}
|
||||
{errorCulprit}N/A{slashErrorCulprit}
|
||||
{docCountParam} occurrences
|
||||
{slashErrorGroupsBucket}`,
|
||||
values: {
|
||||
serviceName: '"{{ctx.metadata.serviceName}}"',
|
||||
threshold: '{{ctx.metadata.threshold}}',
|
||||
timeRange:
|
||||
'"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"',
|
||||
errorGroupsBuckets:
|
||||
'{{#ctx.payload.aggregations.error_groups.buckets}}',
|
||||
errorLogMessage:
|
||||
'>*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}*',
|
||||
errorCulprit:
|
||||
'>{{#sample.hits.hits.0._source.error.culprit}}`{{sample.hits.hits.0._source.error.culprit}}`{{/sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}',
|
||||
slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}',
|
||||
docCountParam: '>{{doc_count}}',
|
||||
slashErrorGroupsBucket:
|
||||
'{{/ctx.payload.aggregations.error_groups.buckets}}',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const actions: Actions = {
|
||||
log_error: { logging: { text: emailTemplate } },
|
||||
};
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
emails,
|
||||
trigger: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerText',
|
||||
{
|
||||
defaultMessage: 'This value must be changed in trigger section',
|
||||
}
|
||||
),
|
||||
serviceName,
|
||||
threshold,
|
||||
timeRangeValue: timeRange.value,
|
||||
timeRangeUnit: timeRange.unit,
|
||||
slackUrlPath,
|
||||
},
|
||||
trigger: {
|
||||
schedule,
|
||||
},
|
||||
input: {
|
||||
search: {
|
||||
request: {
|
||||
indices: [apmIndexPatternTitle],
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: '{{ctx.metadata.serviceName}}' } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'error' } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte:
|
||||
'now-{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
error_groups: {
|
||||
terms: {
|
||||
min_doc_count: '{{ctx.metadata.threshold}}',
|
||||
field: ERROR_GROUP_ID,
|
||||
size: 10,
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
top_hits: {
|
||||
_source: [
|
||||
ERROR_LOG_MESSAGE,
|
||||
ERROR_EXC_MESSAGE,
|
||||
ERROR_EXC_HANDLED,
|
||||
ERROR_CULPRIT,
|
||||
ERROR_GROUP_ID,
|
||||
'@timestamp',
|
||||
],
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
script: {
|
||||
source:
|
||||
'return ctx.payload.aggregations.error_groups.buckets.length > 0',
|
||||
},
|
||||
},
|
||||
actions,
|
||||
};
|
||||
|
||||
if (slackUrlPath) {
|
||||
body.actions.slack_webhook = {
|
||||
webhook: {
|
||||
scheme: 'https',
|
||||
host: 'hooks.slack.com',
|
||||
port: 443,
|
||||
method: 'POST',
|
||||
path: '{{ctx.metadata.slackUrlPath}}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: `__json__::${JSON.stringify({
|
||||
text: slackTemplate,
|
||||
})}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!isEmpty(emails)) {
|
||||
body.actions.email = {
|
||||
email: {
|
||||
to: '{{#join}}ctx.metadata.emails{{/join}}',
|
||||
subject: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.emailSubjectText',
|
||||
{
|
||||
defaultMessage:
|
||||
'{serviceName} has error groups which exceeds the threshold',
|
||||
values: { serviceName: '"{{ctx.metadata.serviceName}}"' },
|
||||
}
|
||||
),
|
||||
body: {
|
||||
html: emailTemplate,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await createWatch({
|
||||
http,
|
||||
id,
|
||||
watch: body,
|
||||
});
|
||||
return id;
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { WatcherFlyout } from './WatcherFlyout';
|
||||
import { ApmPluginContext } from '../../../../context/ApmPluginContext';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
activeFlyout: FlyoutName;
|
||||
}
|
||||
type FlyoutName = null | 'Watcher';
|
||||
|
||||
export class ServiceIntegrations extends React.Component<Props, State> {
|
||||
static contextType = ApmPluginContext;
|
||||
context!: React.ContextType<typeof ApmPluginContext>;
|
||||
|
||||
public state: State = { isPopoverOpen: false, activeFlyout: null };
|
||||
|
||||
public getWatcherPanelItems = () => {
|
||||
const { core } = this.context;
|
||||
|
||||
return [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Enable watcher error reports',
|
||||
}
|
||||
),
|
||||
icon: 'watchesApp',
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.openFlyout('Watcher');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View existing watches',
|
||||
}
|
||||
),
|
||||
icon: 'watchesApp',
|
||||
href: core.http.basePath.prepend(
|
||||
'/app/management/insightsAndAlerting/watcher'
|
||||
),
|
||||
target: '_blank',
|
||||
onClick: () => this.closePopover(),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
public openPopover = () =>
|
||||
this.setState({
|
||||
isPopoverOpen: true,
|
||||
});
|
||||
|
||||
public closePopover = () =>
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
|
||||
public openFlyout = (name: FlyoutName) =>
|
||||
this.setState({ activeFlyout: name });
|
||||
|
||||
public closeFlyouts = () => this.setState({ activeFlyout: null });
|
||||
|
||||
public render() {
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={this.openPopover}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Integrations',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
id="integrations-menu"
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
items: this.getWatcherPanelItems(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
<WatcherFlyout
|
||||
isOpen={this.state.activeFlyout === 'Watcher'}
|
||||
onClose={this.closeFlyouts}
|
||||
urlParams={this.props.urlParams}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ApmHeader } from '../../shared/ApmHeader';
|
||||
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
||||
import { ServiceIntegrations } from './ServiceIntegrations';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { AlertIntegrations } from './AlertIntegrations';
|
||||
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
|
||||
|
@ -54,9 +53,6 @@ export function ServiceDetails({ tab }: Props) {
|
|||
<h1>{serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations urlParams={urlParams} />
|
||||
</EuiFlexItem>
|
||||
{isAlertingAvailable && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertIntegrations
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from 'kibana/public';
|
||||
import { callApi } from './callApi';
|
||||
|
||||
export async function createWatch({
|
||||
id,
|
||||
watch,
|
||||
http,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
id: string;
|
||||
watch: any;
|
||||
}) {
|
||||
return callApi(http, {
|
||||
method: 'PUT',
|
||||
pathname: `/api/watcher/watch/${id}`,
|
||||
body: { type: 'json', id, watch, isNew: true, isActive: true },
|
||||
});
|
||||
}
|
|
@ -4279,44 +4279,7 @@
|
|||
"xpack.apm.serviceDetails.alertsMenu.errorRate": "エラー率",
|
||||
"xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間",
|
||||
"xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "レポートはメールで送信するか Slack チャンネルに投稿できます。各レポートにはオカランス別のトップ 10 のエラーが含まれます。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "アクション",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "コンディション",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel": "ウォッチを作成",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportHelpText": "デイリーレポートは {dailyTimeFormatted} / {dailyTime12HourFormatted} に送信されます。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportRadioButtonLabel": "デイリーレポート",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.emailSubjectText": "{serviceName} にしきい値を超えたエラーグループがあります",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.emailTemplateText": "{serviceName} サービスに {timeRange}{br}{br}{errorGroupsBuckets}{br}{errorLogMessage}{br}{errorCulprit}N/A{slashErrorCulprit}{br}{docCountParam} オカレンス {br}{slashErrorGroupsBucket} 内で {threshold} 件のオカレンスを超えるエラーグループがあります",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle": "エラーレポートを有効にする",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription": "このフォームは、このサービスでのエラーのオカレンスを通知するウォッチの作成をアシストします。Watcher の書斎は、{documentationLink} をご覧ください。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText": "ドキュメンテーション",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalHelpText": "レポートの間隔。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalRadioButtonLabel": "間隔",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.hrsLabel": "時間",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.minsLabel": "分",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdHelpText": "エラーグループがレポートに含まれるしきい値です。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdLabel": "エラーグループごとのオカレンスのしきい値",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText": "メールを構成していない場合は、{documentationLink} をご覧ください。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText": "ドキュメンテーション",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsLabel": "受信者 (コンマ区切り)",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.sendEmailLabel": "メールを送信",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.sendSlackNotificationLabel": "Slack 通知を送信",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackTemplateText": "{serviceName} サービスに {timeRange} 以内に {threshold} 件のオカレンスを超えるエラーグループがあります。\n{errorGroupsBuckets}\n{errorLogMessage}\n{errorCulprit}N/A{slashErrorCulprit}\n{docCountParam} 件のオカレンス\n{slashErrorGroupsBucket}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText": "Slack webhook の取得方法は、{documentationLink} をご覧ください。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText": "ドキュメンテーション",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLLabel": "Slack Webhook URL",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription": "しきい値を超えた際のレポートの間隔を選択してください。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle": "トリガースケジュール",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerText": "この値はトリガーセクションで変更する必要があります。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText": "ウォッチの準備が完了し、{serviceName} のエラーレポートが送信されます。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText": "ウォッチを表示",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle": "新規ウォッチが作成されました!",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "ユーザーにウォッチ作成のパーミッションがあることを確認してください。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "ウォッチの作成に失敗",
|
||||
"xpack.apm.serviceDetails.errorsTabLabel": "エラー",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "ウォッチエラーレポートを有効にする",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "統合",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "既存のウォッチを表示",
|
||||
"xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用状況",
|
||||
"xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle": "エラーのオカレンス",
|
||||
"xpack.apm.serviceDetails.metrics.memoryUsageChartTitle": "システムメモリー使用状況",
|
||||
|
|
|
@ -4283,44 +4283,7 @@
|
|||
"xpack.apm.serviceDetails.alertsMenu.errorRate": "错误率",
|
||||
"xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间",
|
||||
"xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "可以通过电子邮件发送报告或将报告发布到 Slack 频道。每个报告将包括按发生次数排序的前 10 个错误。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "操作",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "条件",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel": "创建监视",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportHelpText": "每日报告将在 {dailyTimeFormatted} / {dailyTime12HourFormatted} 发送。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportRadioButtonLabel": "每日报告",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.emailSubjectText": "{serviceName} 具有超过阈值的错误组",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.emailTemplateText": "您的服务 {serviceName} 具有在 {timeRange}内发生次数超过 {threshold} 次的错误组{br}{br}{errorGroupsBuckets}{br}{errorLogMessage}{br}{errorCulprit}不适用{slashErrorCulprit}{br}{docCountParam} 次{br}{slashErrorGroupsBucket}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle": "启用错误报告",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription": "此表单将帮助创建从此服务向您通知错误发生次数的监视。要详细了解 Watcher,请阅读我们的恶{documentationLink}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText": "文档",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalHelpText": "报告时间间隔。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalRadioButtonLabel": "时间间隔",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.hrsLabel": "小时",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.minsLabel": "分钟",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdHelpText": "要将错误组包括在报告中所要达到的阈值。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdLabel": "每错误组的发生次数阈值",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText": "如果未配置电子邮件,请参阅{documentationLink}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText": "文档",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsLabel": "接收人(逗号分隔)",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.sendEmailLabel": "发送电子邮件",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.sendSlackNotificationLabel": "发送 Slack 通知",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackTemplateText": "您的服务 {serviceName} 具有在 {timeRange}内发生次数超过 {threshold} 次的错误组\n{errorGroupsBuckets}\n{errorLogMessage}\n{errorCulprit}不适用{slashErrorCulprit}\n{docCountParam} 次发生\n{slashErrorGroupsBucket}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText": "要获取 Slack Webhook,请参阅{documentationLink}",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText": "文档",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLLabel": "Slack Webhook URL",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription": "选择阈值达到时报告的时间间隔。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle": "触发排定",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.triggerText": "必须在触发器部分更改此值",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText": "监视已就绪,将发送 {serviceName} 的错误报告。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText": "查看监视",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle": "新监视已创建!",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "确保您的用户有权创建监视。",
|
||||
"xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "监视创建失败",
|
||||
"xpack.apm.serviceDetails.errorsTabLabel": "错误",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "启用 Watcher 错误报告",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "集成",
|
||||
"xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "查看现有监视",
|
||||
"xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用",
|
||||
"xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle": "错误发生次数",
|
||||
"xpack.apm.serviceDetails.metrics.memoryUsageChartTitle": "系统内存使用",
|
||||
|
|
Loading…
Reference in a new issue