SAP CI – Automated Notifications via SAP Alert Notification Service for Critical or Exhausted JMS Resources

This blog describes how to send automated notifications via the SAP Alert Notification service for SAP BTP in case JMS resources are critical or exhausted using an integration flow. It builds on a similar blog and partially reuses content by my colleague Mandy Krimmel. I highly encourage you to go through this in more detail prior to reading this blog.

Description of scenario

Because JMS resources are limited on the JMS broker, it is important to become aware as soon as resource levels gets critical to take necessary actions and to forgo potential runtime issues which could have been easily prevented. Therefore, the goal is to develop an iflow which pulls JMS resource capacity and sends automated notifications using alert notification service whenever JMS resources are critical or exhausted. With this service providers can publish events through a common API which can be consumed via real-time notifications by alert subscribers. The configuration effort is minimal. An interesting background read on alerting in CPI, including the alert notification service, can be found here.

For the basic setup of the iflow refer to this blog which explains how JMS resource information can be collected from CI. I will focus on how to adapt the existing content to leverage the SAP alert notification service. An overview of the iflow can be seen below. Specific adaptations based on alert notification service usage are highlighted in red.

Configuration of SAP alert notification service

CPI and the alert notification service should both available on the tenant and accessible without issue. You can then configure the alert notification service via your instance on the subaccount.

Navigate to SubscriptionsCreate to add a new service subscription that can be used by the iflow. Set the eventType equal to “JMSQueueDepletion” and the resourceType equal to “JMSQueue”. As action you can enter your email information. To verify the service is working as expected you can Send a Test Event. From here you can enable or disable the subscription as required.

You are now ready to configure the iflow.

Configuration of iflow process steps

In this part, I will describe the alert notification service specific process steps.

Get JMS resource information

The iflow is set to run via timer.  First, needed properties for the service are set in a content modifier which will be used later on to send the notification. The properties are configured as externalized parameters so the iflow will not have to be altered, e.g. if credential names change.

Next, the JMS resource capacity data is pulled via OData adapter and JMS resource information is saved as property via Xpath in a content modifier before the JMS resource status is evaluated with a groovy script and checked via router. If the overall JMS status is Ok, the path route 2 → JMS state OK is followed and the iflow is finished. No notification is sent in this case. If the overall JMS status is not Ok route 1 → JMS state C/E is followed. In this case the payload is filtered by queue states and all critical or exhausted queues are returned. The described steps and their configuration can be explored in detail here.

Prepare and send notification

The message is prepared based on the conditions configured via the BTP cockpit and set as body with a groovy script.

import groovy.json.JsonOutput Message processData(Message message) { String JMSStatus = message.getProperty("JMSStatus"); String CapacityStatus = message.getProperty("CapacityStatus"); String PercentCapacity = message.getProperty("PercentCapacity"); String QueueCapacityError = message.getProperty("QueueCapacityError"); String QueueCapacityWarning = message.getProperty("QueueCapacityWarning"); String QueueCapacityOK = message.getProperty("QueueCapacityOK"); String QueuesCriticalNames = message.getProperty("QueuesCriticalNames"); String QueuesExhaustedNames = message.getProperty("QueuesExhaustedNames"); String QueueStatus = message.getProperty("QueueStatus"); String Queues = message.getProperty("Queues"); String MaxQueues = message.getProperty("MaxQueues"); String TransactionStatus = message.getProperty("TransactionStatus"); String ConsumerStatus = message.getProperty("ConsumerStatus"); String ProviderStatus = message.getProperty("ProviderStatus"); String JMSQueueUrl = ""; Event event = new Event( eventType: "JMSQueueDepletion", resource: new Resource( resourceName: "CPIJMSQueue", resourceType: "JMSQueue" ), severity: "WARNING", category: "ALERT", subject: "JMS resource depletion: " + JMSStatus, body: "The JMS Resources on the Cloud Integration tenant are ${JMSStatus}.\nOverall Queue Capacity Status: ${CapacityStatus} (${PercentCapacity} percent)\n\nCapacity Status for Queues:\nOK: ${QueueCapacityOK} Queues\nWarning: ${QueueCapacityWarning} Queues\nNames of queues with warnings:${QueuesCriticalNames}\nError: ${QueueCapacityError} Queues \nNames of queues with errors: ${QueuesExhaustedNames}\n\nQueue Status: ${QueueStatus} ${Queues} / ${MaxQueues}\nTransactions Status: ${TransactionStatus}\nConsumer Connections Status: ${ConsumerStatus}\nProvider Connections Status: ${ProviderStatus}", tags: [ 'ans:detailsLink' : JMSQueueUrl ] ) def eventMsg = JsonOutput.toJson(event); message.setBody(eventMsg); return message
} class Event { String eventType Resource resource String severity String category String subject String body Map<String, String> tags
} class Resource { String resourceName String resourceType

With another groovy script the message is sent to the alert notification service and the iflow terminates.

import groovy.transform.Field
import groovy.json.JsonSlurper @Field final String AUTHORIZATION_HEADER_NAME = 'Authorization'
@Field final String CONTENT_TYPE_HEADER_NAME = 'Content-Type'
@Field final String APPLICATION_JSON_UTF8_CONTENT_TYPE = 'application/json;charset=utf-8'
@Field final String TEXT_PLAIN_CONTENT_TYPE = 'text/plain'
@Field final String HTTP_METHOD_POST = 'POST'
@Field final String UTF_8 = 'UTF-8'
@Field final String LOG_PROPERTY_KEY = 'Log'
@Field final String CREDENTIALS_TYPE_PROPERTY_KEY = 'sec:credential.kind'
@Field final String OAUTH2_URL_PROPERTY_KEY = 'sec:server.url'
@Field final String BASIC_CREDENTIALS_TYPE = 'default'
@Field final String OAUTH2_CREDENTIALS_TYPE = 'oauth2:default'
@Field final String INVALID_CREDENTIALS_TYPE_ERROR_MESSAGE = 'Invalid credentials type. Required OAuth2 Client Credentials or Basic Authentication'
@Field final int SERVICE_SUCCESS_STATUS_CODE = 202
@Field final boolean DEFAULT_ENABLE_LOG = false
@Field final int DEFAULT_SERVICE_REQUEST_TIMEOUT_MINUTES = 1 Message processData(Message message) { UserCredential credentials = retrieveCredentials(getStringProperty(message, SERVICE_CREDENTIALS_NAME_PROPERTY_KEY)) int requestTimeoutMinutes = getIntProperty(message, SERVICE_REQUEST_TIMEOUT_MINUTES_PROPERTY_KEY, DEFAULT_SERVICE_REQUEST_TIMEOUT_MINUTES) String authorizationHeaderValue = retrieveAuthorizationHeaderValue(credentials, requestTimeoutMinutes) sendNotification(new URL(getStringProperty(message, SERVICE_RESOURCE_EVENTS_URL_PROPERTY_KEY)), requestTimeoutMinutes, authorizationHeaderValue, message.getBody()); return message;
} static UserCredential retrieveCredentials(String credentialsName) { return ITApiFactory.getService(SecureStoreService.class, null).getUserCredential(credentialsName)
} String retrieveAuthorizationHeaderValue(UserCredential credentials, int requestTimeoutMinutes) { String credentialsType = credentials.getCredentialProperties().get(CREDENTIALS_TYPE_PROPERTY_KEY) if (credentialsType == BASIC_CREDENTIALS_TYPE) { String base64EncodedCredentials = toBase64EncodedCredentials(credentials) return "Basic ${base64EncodedCredentials}" } else if (credentialsType == OAUTH2_CREDENTIALS_TYPE) { String accessToken = obtainOAuth2Token(credentials, requestTimeoutMinutes) return "Bearer ${accessToken}" } else { throw new Exception(INVALID_CREDENTIALS_TYPE_ERROR_MESSAGE) }
} String obtainOAuth2Token(UserCredential credentials, int requestTimeoutMinutes) { String base64EncodedCredentials = toBase64EncodedCredentials(credentials) URLConnection postRequest = new URL(credentials.getCredentialProperties().get(OAUTH2_URL_PROPERTY_KEY)).openConnection() postRequest.setConnectTimeout(minutesToMilliseconds(requestTimeoutMinutes)) postRequest.setReadTimeout(minutesToMilliseconds(requestTimeoutMinutes)) postRequest.setRequestMethod(HTTP_METHOD_POST) postRequest.setDoOutput(true) postRequest.setRequestProperty(AUTHORIZATION_HEADER_NAME, "Basic ${base64EncodedCredentials}") assert postRequest.getResponseCode() == OAUTH2_PROVIDER_SUCCESS_STATUS_CODE return new JsonSlurper().parseText(postRequest.getInputStream().getText()).access_token.toString()
} void sendNotifications(String urlString, int requestTimeoutMinutes, String authorizationHeaderValue, String eventsAsJsonString, StringBuilder logMessage) { URL url = new URL(urlString) def events = new JsonSlurper().parseText(eventsAsJsonString) logMessage.append("Sending notifications to the service\n") events.each { event -> sendNotification(url, requestTimeoutMinutes, authorizationHeaderValue, JsonOutput.toJson(event)) }
} void sendNotification(URL url, int requestTimeoutMinutes, String authorizationHeaderValue, String body) { URLConnection postRequest = url.openConnection() postRequest.setConnectTimeout(minutesToMilliseconds(requestTimeoutMinutes)) postRequest.setReadTimeout(minutesToMilliseconds(requestTimeoutMinutes)) postRequest.setRequestMethod(HTTP_METHOD_POST) postRequest.setDoOutput(true) postRequest.setRequestProperty(AUTHORIZATION_HEADER_NAME, authorizationHeaderValue) postRequest.setRequestProperty(CONTENT_TYPE_HEADER_NAME, APPLICATION_JSON_UTF8_CONTENT_TYPE) postRequest.getOutputStream().write(body.getBytes(UTF_8)) assert postRequest.getResponseCode() == SERVICE_SUCCESS_STATUS_CODE
} void attachLogs(Message message, StringBuilder logMessage) { if (getBooleanProperty(message, ENABLE_LOG_PROPERTY_KEY, DEFAULT_ENABLE_LOG)) { messageLogFactory.getMessageLog(message).addAttachmentAsString(LOG_PROPERTY_KEY, logMessage.toString(), TEXT_PLAIN_CONTENT_TYPE) }
} static int getIntProperty(Message message, String propertyName, int defaultValue) { String propertyValue = getStringProperty(message, propertyName) return propertyValue != null && propertyValue.isInteger() ? propertyValue.toInteger() : defaultValue
} static boolean getBooleanProperty(Message message, String propertyName, boolean defaultValue) { String propertyValue = getStringProperty(message, propertyName) return propertyValue != null ? propertyValue.toBoolean() : defaultValue
} static String getStringProperty(Message message, String propertyName) { def propertyValue = message.getProperty(propertyName) return propertyValue != null ? propertyValue.toString() : null
} static String toBase64EncodedCredentials(UserCredential credentials) { return (credentials.getUsername() + ":" + credentials.getPassword()).getBytes().encodeBase64().toString()
} static int minutesToMilliseconds(int minutes) { return minutes * 60 * 1000

The created email notification can be viewed below.

Naturally, the iflow can be adapted to specific needs. However, please remember that if you make changes, it can also be needed to change the underlying alert notification service configuration and vice versa. For example, if you wish to change the event name this will have to be changed in the script of the iflow as well as the service to match.

And that’s all it takes to set up an iFlow that sends automated notification with the alert notification service if the JMS resources become critical or exhausted.  You can be up and running with minimal effort and in a short timeframe. Please feel free to share your thoughts and questions below and check out the linked resources throughout the blog.