[APM] Remove watcher integration (#71655)

This commit is contained in:
Søren Louv-Jansen 2020-07-15 08:42:17 +02:00 committed by GitHub
parent 51a862988c
commit f760d8513b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 0 additions and 1558 deletions

View file

@ -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>
);
}
}

View file

@ -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",
},
},
},
}
`;

View file

@ -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;
}

View file

@ -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],
},
],
},
},
},
],
},
},
};

View file

@ -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;
}

View file

@ -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}
/>
</>
);
}
}

View file

@ -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

View file

@ -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 },
});
}

View file

@ -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": "システムメモリー使用状況",

View file

@ -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": "系统内存使用",