Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ResponseOps][Stack Connectors] Opsgenie backend #142164

Merged
merged 21 commits into from
Oct 11, 2022

Conversation

jonathan-buttner
Copy link
Contributor

@jonathan-buttner jonathan-buttner commented Sep 28, 2022

This PR implements the backend of the opsgenie connector.

Issue: #142776

Opsgenie API Docs: https://docs.opsgenie.com/docs/alert-api#create-alert

Examples

Creating alerts

Postman creating connector create_connector
Postman creating an alert creating_alert
Created Alerts within Opsgenie image

Closing alerts

Postman closing an alert image
Closed alert within Opsgenie image

Testing

  1. Create an Opsgenie account here: https://www.atlassian.com/software/opsgenie
  2. Create a team
  3. On the team page there should be Integrations on the left, click that, then click Add Integration
  4. Search for and add API Integration
  5. After the integration is added, it will create an API key for use in the following APIs, save this key for later
  6. Click Save Integration

Creating a connector

curl
curl --location --request POST 'http://elastic:changeme@localhost:5601/api/actions/connector' \
--header 'kbn-xsrf: foo' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Opsgenie",
    "connector_type_id": ".opsgenie",
    "config": {
        "apiUrl": "https://api.opsgenie.com"
    },
    "secrets": {
        "apiKey": "<apiKey>"
    }
}'
Raw
POST http://elastic:changeme@localhost:5601/api/actions/connector

{
    "name": "Opsgenie",
    "connector_type_id": ".opsgenie",
    "config": {
        "apiUrl": "https://api.opsgenie.com"
    },
    "secrets": {
        "apiKey": "<apiKey>"
    }
}

Closing an alert

curl
curl --location --request POST 'http://elastic:changeme@localhost:5601/api/actions/connector/<connector_id>/_execute' \
--header 'kbn-xsrf: foo' \
--header 'Content-Type: application/json' \
--data-raw '{
    "params": {
        "subAction": "createAlert",
        "subActionParams": {
            "message": "It worked"
        }
    }
}'
Raw
POST http://elastic:changeme@localhost:5601/api/actions/connector/<connector_id>/_execute

{
    "params": {
        "subAction": "closeAlert",
        "subActionParams": {
            "alias": "123"
        }
    }
}

@jonathan-buttner jonathan-buttner added Feature:Alerting release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v8.6.0 labels Sep 28, 2022
@jonathan-buttner jonathan-buttner changed the title [ResponseOps][Cases] Opsgenie backend [ResponseOps][Stack Connectors] Opsgenie backend Sep 30, 2022
To make this connector usable in the Kibana UI, you will need to provide all the UI editing aspects of the connector. The existing connector type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was a whitespace change 🤔

@jonathan-buttner jonathan-buttner marked this pull request as ready for review October 6, 2022 14:49
@jonathan-buttner jonathan-buttner requested review from a team as code owners October 6, 2022 14:49
@elasticmachine
Copy link
Contributor

Pinging @elastic/response-ops (Team:ResponseOps)

@@ -49,6 +49,8 @@ export {
} from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook';

export { getOpsgenieConnectorType, OpsgenieConnectorTypeId } from './opsgenie';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the OpsgenieActionParams in the next UI PR when it's actually needed externally


public getResponseErrorMessage(error: AxiosError<ErrorSchema>) {
return `Message: ${
error.response?.data.errors?.message ?? error.response?.data.message ?? i18n.UNKNOWN_ERROR
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add another fallback to include the error message? For example, error.response?.data.errors?.message ?? error.response?.data.message ?? error.message ?? i18n.UNKNOWN_ERROR. I am also thinking if we should improve the message thrown by the SubActionConnector class in the request method. At the moment we do

const errorMessage = this.getResponseErrorMessage(error);
throw new Error(errorMessage);

Maybe we can include more details like the status code. For example,

const errorMessage = `Status code: ${error.status}. Message: ${this.getResponseErrorMessage(error)}`;
throw new Error(errorMessage);

What do you think? @pmuellr Any information you think we should include in the error message to help us with SDHs?

}

private createHeaders() {
return { Authorization: `GenieKey ${this.secrets.apiKey}`, 'Content-Type': 'application/json' };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

public async closeAlert(params: CloseAlertParams) {
const fullURL = new URL(`v2/alerts/${params.alias}/close`, this.config.apiUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the concatPathToURL helper function? The concatPathToURL can return the URL object so we can set the searchParams.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good idea.

Comment on lines 26 to 38
const validateDetails = (details: Record<string, string>): string | void => {
let totalChars = 0;

for (const [key, value] of Object.entries(details)) {
totalChars += key.length + value.length;

if (totalChars > 8000) {
return i18n.translate('xpack.stackConnectors.opsgenie.invalidDetails', {
defaultMessage: 'details field character count exceeds the 8000 limit',
});
}
}
};
Copy link
Member

@cnasikas cnasikas Oct 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs here we should only validate the values and not the whole JSON string (keys included). The Extra Properties is the details. You can test it if you open the Network Tab in your dev tools. The API docs are misleading. I thought the same as you at first 🙂 .

Also, if we had to validate the whole JSON string I think it is better to do

const validateDetails = (details: Record<string, string>): string | void => {
  let totalChars = 0;
  try {
    JSON.stringify(details).length
  } catch (e) { // throw error about JSON stringify }
  
  if (totalChars > 8000) {
      return i18n.translate('xpack.stackConnectors.opsgenie.invalidDetails', {
        defaultMessage: 'details field character count exceeds the 8000 limit',
      });
    }
};

because we need to validate {, ,, and }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch!

.send({
params: {},
})
.then((resp: any) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: For readability maybe is better to do:

const res = await supertest.post(...).set(...).send(...).expect(200)
// the expects
expect(resp.body.connector_id).to.eql(opsgenieActionId);
....

});
});
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add more tests to validate some of the optional fields.

});

it('calls request with the correct arguments for creating an alert', async () => {
const connectorSpy = jest
Copy link
Member

@cnasikas cnasikas Oct 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is better to mock axios instead of the method of the class. This way if the sub-action framework introduces an unintended breaking change or a feature change your tests can catch it. You can follow the examples here x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.test.ts

public getResponseErrorMessage(error: AxiosError<ErrorSchema>) {
return `Message: ${
error.response?.data.errors?.message ?? error.response?.data.message ?? i18n.UNKNOWN_ERROR
}.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should omit the dot. I got an error while testing and I noticed that there are two dots in the message. For example: "Message: To perform this action, use an API key from an API integration.."

@ymao1
Copy link
Contributor

ymao1 commented Oct 10, 2022

If I am reading "Connector collaboration" doc correctly, Opsgenie should go under the cases subfolder, not the stack subfolder. Is that not the case? Happy to update the doc if so!

@jonathan-buttner
Copy link
Contributor Author

If I am reading "Connector collaboration" doc correctly, Opsgenie should go under the cases subfolder, not the stack subfolder. Is that not the case? Happy to update the doc if so!

@cnasikas what do you think? We might not support cases initially. From that doc it seems like any connector that has support for cases should be moved to the cases directory.

schema.literal('schedule'),
]);

const validateDetails = (details: Record<string, string>): string | void => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I confused you before, but I meant that each value has to be less than 8K characters. Not collectively. You can have two values with 8K characters for each.

Screenshot 2022-10-11 at 3 42 31 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooooh got it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to remove the validation offline because opsgenie will just truncate the strings and not throw an error.

@jonathan-buttner
Copy link
Contributor Author

@elasticmachine merge upstream

Copy link
Member

@cnasikas cnasikas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LTGM! Tested without any issues. I could create and close an alert in OpsGenie. 🚀

@jonathan-buttner
Copy link
Contributor Author

@elasticmachine merge upstream

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@jonathan-buttner jonathan-buttner merged commit b86bb63 into elastic:main Oct 11, 2022
@jonathan-buttner jonathan-buttner deleted the opsgenie-connector branch October 11, 2022 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Alerting release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v8.6.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants