[Connectors][PagerDuty] Add support for links and custom_details in the API (#170459)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2023-11-09 12:00:31 +02:00 committed by GitHub
parent c29ce0d27b
commit 0b40e3e7c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 372 additions and 11 deletions

View file

@ -301,6 +301,7 @@ describe('Execution Handler', () => {
foo: true,
stateVal: 'My goes here',
},
ruleName: rule.name,
});
expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE);
@ -1988,6 +1989,7 @@ describe('Execution Handler', () => {
"val": "rule url: http://localhost:12345/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": Object {
"absoluteUrl": "http://localhost:12345/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1",
"basePathname": "",
@ -2060,6 +2062,7 @@ describe('Execution Handler', () => {
"val": "rule url: http://localhost:12345/basePath/s/test1/app/test/rule/1?start=30000&end=90000",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": Object {
"absoluteUrl": "http://localhost:12345/basePath/s/test1/app/test/rule/1?start=30000&end=90000",
"basePathname": "/basePath",
@ -2095,6 +2098,7 @@ describe('Execution Handler', () => {
"val": "rule url: http://localhost:12345/app/management/insightsAndAlerting/triggersActions/rule/1",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": Object {
"absoluteUrl": "http://localhost:12345/app/management/insightsAndAlerting/triggersActions/rule/1",
"basePathname": "",
@ -2127,6 +2131,7 @@ describe('Execution Handler', () => {
"val": "rule url: http://localhost:12345/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": Object {
"absoluteUrl": "http://localhost:12345/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1",
"basePathname": "",
@ -2159,6 +2164,7 @@ describe('Execution Handler', () => {
"val": "rule url: ",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": undefined,
},
]
@ -2188,6 +2194,7 @@ describe('Execution Handler', () => {
"val": "rule url: ",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": undefined,
},
]
@ -2217,6 +2224,7 @@ describe('Execution Handler', () => {
"val": "rule url: ",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": undefined,
},
]
@ -2249,6 +2257,7 @@ describe('Execution Handler', () => {
"val": "rule url: http://localhost:12345/s/test1/app/management/some/other/place",
},
"actionTypeId": "test",
"ruleName": "name-of-alert",
"ruleUrl": Object {
"absoluteUrl": "http://localhost:12345/s/test1/app/management/some/other/place",
"basePathname": "",

View file

@ -256,6 +256,7 @@ export class ExecutionHandler<
params: injectActionParams({
actionTypeId,
ruleUrl,
ruleName: this.rule.name,
actionParams: transformSummaryActionParams({
alerts: summarizedAlerts,
rule: this.rule,
@ -296,6 +297,7 @@ export class ExecutionHandler<
params: injectActionParams({
actionTypeId,
ruleUrl,
ruleName: this.rule.name,
actionParams: transformActionParams({
actionsPlugin,
alertId: ruleId,

View file

@ -8,14 +8,16 @@
import { injectActionParams } from './inject_action_params';
describe('injectActionParams', () => {
test(`passes through when actionTypeId isn't .email`, () => {
test(`passes through when actionTypeId isn't .email or .pagerduty`, () => {
const actionParams = {
message: 'State: "{{state.value}}", Context: "{{context.value}}"',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.server-log',
});
expect(result).toMatchInlineSnapshot(`
Object {
"message": "State: \\"{{state.value}}\\", Context: \\"{{context.value}}\\"",
@ -55,6 +57,146 @@ describe('injectActionParams', () => {
`);
});
test('injects the absoluteUrl to the links when actionTypeId is .pagerduty and there are no links', () => {
const actionParams = {
summary: 'My summary',
};
const ruleUrl = {
absoluteUrl:
'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
kibanaBaseUrl: 'http://localhost:5601',
basePathname: '',
spaceIdSegment: '',
relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.pagerduty',
ruleUrl,
});
expect(result).toMatchInlineSnapshot(`
Object {
"links": Array [
Object {
"href": "http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1",
"text": "Elastic Rule \\"Unknown\\"",
},
],
"summary": "My summary",
}
`);
});
test('adds the rule name if the rule is defined when actionTypeId is .pagerduty', () => {
const actionParams = {
summary: 'My summary',
};
const ruleUrl = {
absoluteUrl:
'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
kibanaBaseUrl: 'http://localhost:5601',
basePathname: '',
spaceIdSegment: '',
relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.pagerduty',
ruleUrl,
ruleName: 'My rule',
});
expect(result).toMatchInlineSnapshot(`
Object {
"links": Array [
Object {
"href": "http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1",
"text": "Elastic Rule \\"My rule\\"",
},
],
"summary": "My summary",
}
`);
});
test('does not produce a runtime error when the actionTypeId is .pagerduty and the links are not an array', () => {
const actionParams = {
summary: 'My summary',
links: 'error',
};
const ruleUrl = {
absoluteUrl:
'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
kibanaBaseUrl: 'http://localhost:5601',
basePathname: '',
spaceIdSegment: '',
relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.pagerduty',
ruleUrl,
ruleName: 'My rule',
});
expect(result).toMatchInlineSnapshot(`
Object {
"links": Array [
Object {
"href": "http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1",
"text": "Elastic Rule \\"My rule\\"",
},
],
"summary": "My summary",
}
`);
});
test('injects the absoluteUrl to the links when actionTypeId is .pagerduty with links', () => {
const actionParams = {
summary: 'My summary',
links: [{ href: 'https://example.com', text: 'My link' }],
};
const ruleUrl = {
absoluteUrl:
'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
kibanaBaseUrl: 'http://localhost:5601',
basePathname: '',
spaceIdSegment: '',
relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.pagerduty',
ruleUrl,
});
expect(result).toMatchInlineSnapshot(`
Object {
"links": Array [
Object {
"href": "http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1",
"text": "Elastic Rule \\"Unknown\\"",
},
Object {
"href": "https://example.com",
"text": "My link",
},
],
"summary": "My summary",
}
`);
});
test('injects viewInKibanaPath and viewInKibanaText when actionTypeId is .email with basePathname and spaceId', () => {
const actionParams = {
body: {
@ -88,16 +230,18 @@ describe('injectActionParams', () => {
`);
});
test('injects viewInKibanaPath as empty string when the ruleUrl is undefined', () => {
test('injects viewInKibanaPath as empty string when the ruleUrl is undefined and the actionTypeId is .email', () => {
const actionParams = {
body: {
message: 'State: "{{state.value}}", Context: "{{context.value}}"',
},
};
const result = injectActionParams({
actionParams,
actionTypeId: '.email',
});
expect(result).toMatchInlineSnapshot(`
Object {
"body": Object {
@ -110,4 +254,21 @@ describe('injectActionParams', () => {
}
`);
});
test('does not add the rule URL when the absoluteUrl is undefined and the actionTypeId is .pagerduty', () => {
const actionParams = {
summary: 'My summary',
};
const result = injectActionParams({
actionParams,
actionTypeId: '.pagerduty',
});
expect(result).toMatchInlineSnapshot(`
Object {
"summary": "My summary",
}
`);
});
});

View file

@ -13,11 +13,13 @@ export interface InjectActionParamsOpts {
actionTypeId: string;
actionParams: RuleActionParams;
ruleUrl?: RuleUrl;
ruleName?: string;
}
export function injectActionParams({
actionTypeId,
actionParams,
ruleName,
ruleUrl = {},
}: InjectActionParamsOpts) {
// Inject kibanaFooterLink if action type is email. This is used by the email action type
@ -36,6 +38,33 @@ export function injectActionParams({
};
}
if (actionTypeId === '.pagerduty') {
/**
* TODO: Remove and use connector adapters
*/
const path = ruleUrl?.absoluteUrl ?? '';
if (path.length === 0) {
return actionParams;
}
const links = Array.isArray(actionParams.links) ? actionParams.links : [];
return {
...actionParams,
links: [
{
href: path,
text: i18n.translate('xpack.alerting.injectActionParams.pagerduty.kibanaLinkText', {
defaultMessage: 'Elastic Rule "{ruleName}"',
values: { ruleName: ruleName ?? 'Unknown' },
}),
},
...links,
],
};
}
// Fallback, return action params unchanged
return actionParams;
}

View file

@ -6,6 +6,7 @@ The `stack_connectors` plugin provides connector types shipped with Kibana, buil
Table of Contents
- [Stack Connectors](#stack-connectors)
- [Connector Types](#connector-types)
- [ServiceNow ITSM](#servicenow-itsm)
- [`params`](#params)
@ -41,13 +42,17 @@ Table of Contents
- [Swimlane](#swimlane)
- [`params`](#params-5)
- [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-)
- [Ospgenie](#ospgenie)
- [`params`](#params-6)
- [PagerDuty](#pagerduty)
- [`params`](#params-7)
- [Developing New Connector Types](#developing-new-connector-types)
- [licensing](#licensing)
- [plugin location](#plugin-location)
- [documentation](#documentation)
- [tests](#tests)
- [connector type config and secrets](#connector-type-config-and-secrets)
- [user interface](#user-interface)
- [Licensing](#licensing)
- [Plugin location](#plugin-location)
- [Documentation](#documentation)
- [Tests](#tests)
- [Connector type config and secrets](#connector-type-config-and-secrets)
- [User interface](#user-interface)
# Connector Types
@ -346,9 +351,9 @@ for the full list of properties.
`subActionParams (createAlert)`
| Property | Description | Type |
| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
| message | The alert message. | string |
| Property | Description | Type |
| -------- | ------------------ | ------ |
| message | The alert message. | string |
The optional parameters `alias`, `description`, `responders`, `visibleTo`, `actions`, `tags`, `details`, `entity`, `source`, `priority`, `user`, and `note` are supported. See the [Opsgenie API documentation](https://docs.opsgenie.com/docs/alert-api#create-alert) for more information on their types.
@ -358,6 +363,28 @@ No parameters are required. For the definition of the optional parameters see th
---
## PagerDuty
The [PagerDuty user documentation `params`](https://www.elastic.co/guide/en/kibana/master/pagerduty-action-type.html) lists configuration properties for the `params`. For more details on these properties, see [PagerDuty v2 event parameters](https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgx-send-an-alert-event) .
### `params`
| Property | Description | Type |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| eventAction | The type of event. | `trigger` \| `resolve` \| `acknowledge` |
| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. The maximum length is 255 characters. | string |
| summary | An optional text summary of the event. The maximum length is 1024 characters. | string _(optional)_ |
| source | An optional value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the Kibana saved object id of the action. | string _(optional)_ |
| severity | The perceived severity of on the affected system. Default: `info`. | `critical` \| `error` \| `warning` \| `info` |
| timestamp | An optional ISO-8601 format date-time, indicating the time the event was detected or generated. | date _(optional)_ |
| component | An optional value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ |
| group | An optional value indicating the logical grouping of components of a service, for example `app-stack`. | string _(optional)_ |
| class | An optional value indicating the class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ |
| links | List of links to add to the event | Array<{ href: string; text: string }> _(optional)_ |
| customDetails | Additional details to add to the event. | object |
---
# Developing New Connector Types
When creating a new connector type, your plugin will eventually call `server.plugins.actions.setup.registerType()` to register the type with the `actions` plugin, but there are some additional things to think about about and implement.

View file

@ -316,6 +316,25 @@ describe('execute()', () => {
component: 'the-component',
group: 'the-group',
class: 'the-class',
customDetails: {
myString: 'foo',
myNumber: 10,
myArray: ['foo', 'baz'],
myBoolean: true,
myObject: {
myNestedObject: 'foo',
},
},
links: [
{
href: 'http://example.com',
text: 'a link',
},
{
href: 'http://example.com',
text: 'a second link',
},
],
};
postPagerdutyMock.mockImplementation(() => {
@ -340,9 +359,31 @@ describe('execute()', () => {
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "trigger",
"links": Array [
Object {
"href": "http://example.com",
"text": "a link",
},
Object {
"href": "http://example.com",
"text": "a second link",
},
],
"payload": Object {
"class": "the-class",
"component": "the-component",
"custom_details": Object {
"myArray": Array [
"foo",
"baz",
],
"myBoolean": true,
"myNumber": 10,
"myObject": Object {
"myNestedObject": "foo",
},
"myString": "foo",
},
"group": "the-group",
"severity": "critical",
"source": "the-source",
@ -383,6 +424,25 @@ describe('execute()', () => {
component: 'the-component',
group: 'the-group',
class: 'the-class',
customDetails: {
myString: 'foo',
myNumber: 10,
myArray: ['foo', 'baz'],
myBoolean: true,
myObject: {
myNestedObject: 'foo',
},
},
links: [
{
href: 'http://example.com',
text: 'a link',
},
{
href: 'http://example.com',
text: 'a second link',
},
],
};
postPagerdutyMock.mockImplementation(() => {
@ -441,6 +501,25 @@ describe('execute()', () => {
component: 'the-component',
group: 'the-group',
class: 'the-class',
customDetails: {
myString: 'foo',
myNumber: 10,
myArray: ['foo', 'baz'],
myBoolean: true,
myObject: {
myNestedObject: 'foo',
},
},
links: [
{
href: 'http://example.com',
text: 'a link',
},
{
href: 'http://example.com',
text: 'a second link',
},
],
};
postPagerdutyMock.mockImplementation(() => {

View file

@ -79,6 +79,9 @@ const PayloadSeveritySchema = schema.oneOf([
schema.literal('info'),
]);
const LinksSchema = schema.arrayOf(schema.object({ href: schema.string(), text: schema.string() }));
const customDetailsSchema = schema.recordOf(schema.string(), schema.any());
const ParamsSchema = schema.object(
{
eventAction: schema.maybe(EventActionSchema),
@ -90,6 +93,8 @@ const ParamsSchema = schema.object(
component: schema.maybe(schema.string()),
group: schema.maybe(schema.string()),
class: schema.maybe(schema.string()),
links: schema.maybe(LinksSchema),
customDetails: schema.maybe(customDetailsSchema),
},
{ validate: validateParams }
);
@ -292,7 +297,9 @@ interface PagerDutyPayload {
component?: string;
group?: string;
class?: string;
custom_details?: Record<string, unknown>;
};
links?: Array<{ href: string; text: string }>;
}
function getBodyForEventAction(actionId: string, params: ActionParamsType): PagerDutyPayload {
@ -301,6 +308,7 @@ function getBodyForEventAction(actionId: string, params: ActionParamsType): Page
const data: PagerDutyPayload = {
event_action: eventAction,
};
if (params.dedupKey) {
data.dedup_key = params.dedupKey;
}
@ -318,7 +326,12 @@ function getBodyForEventAction(actionId: string, params: ActionParamsType): Page
severity: params.severity || 'info',
...(validatedTimestamp ? { timestamp: moment(validatedTimestamp).toISOString() } : {}),
...omitBy(pick(params, ['component', 'group', 'class']), isUndefined),
...(params.customDetails ? { custom_details: params.customDetails } : {}),
};
if (params.links) {
data.links = params.links;
}
return data;
}

View file

@ -175,6 +175,47 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
});
});
it('should execute successfully with links and customDetails', async () => {
const { body: result } = await supertest
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
summary: 'just a test',
customDetails: {
myString: 'foo',
myNumber: 10,
myArray: ['foo', 'baz'],
myBoolean: true,
myObject: {
myNestedObject: 'foo',
},
},
links: [
{
href: 'http://example.com',
text: 'a link',
},
{
href: 'http://example.com',
text: 'a second link',
},
],
},
})
.expect(200);
expect(proxyHaveBeenCalled).to.equal(true);
expect(result).to.eql({
status: 'ok',
connector_id: simulatedActionId,
data: {
message: 'Event processed',
status: 'success',
},
});
});
it('should handle a 40x pagerduty error', async () => {
const { body: result } = await supertest
.post(`/api/actions/connector/${simulatedActionId}/_execute`)