If you are wondering about sending Liferay Audit Messages to a Slack channel using the Liferay Audit Service, you are at the right place. These event notifications, aka Audit Messages, can be sent out to an external chat client such as Slack or Stride. This can be useful for many common business scenarios, but especially useful for updating people who are already on Slack, but may not necessarily log into Liferay every day (if at all!). You do not need to know much about the Audit functionality to understand the concept described here.

Before we jump to the main topic let’s talk about the Audit Service that the Liferay provides. Liferay’s Audit Service can be used to produce and store an audit trail of user actions. This is useful for identifying some things that may not always be easy to track down without doing a lot of digging. By default, Liferay provides the Audit Hook for Login / Logout, User Impersonation, User Profile changes (address & contact update), Role changes (create, remove, update, grant, revoke), User Group changes (create, remove, update) and User changes (create, remove, update). You can view the audit trail in “Control Panel > Configuration > Audit”. If you would like to add the audit trail of the user actions on other resources, then you can use the a model listener to generate the audit message.

Using Model Listeners to add Audit Messages

Model Listeners are used for listening to persistence events such as create, remove or update. Liferay has built in ways that enable you to perform actions immediately before or after the event occurs. To create a Model Listener you have to extend BaseModelListener. And, to register the Model Listener service you should use Liferay’s OSGi runtime; setting ‘service= ModelListener.class’ and ‘immediate=true’ in the Component. For example:

@Component(service = ModelListener.class, immediate = true)
public class CustomEntityListener  extends BaseModelListener<CustomEntity> {
	/* Override one or more methods as needed from the ModelListener interface. */
}

After overriding the methods from the ModelListener interface (normally onBefore* or onAfter* methods are overridden to create the audit trail), we can construct our AuditMessage like this:

AuditMessage auditMessage = new AuditMessage(eventType, companyId, realUserId, realUserName, className, classPK, message, additionalInfo);

After the Audit Message is created we can post it to audit trail using route() method of AuditRouterUtil class from com.liferay.portal.kernel.audit package. For example:

AuditRouterUtil.route(auditMessage);

Here is a sample implementation of the onBeforeCreate() method for the Model Listener of the Organization class:

package …;

import …;

@Component(service = ModelListener.class, immediate = true)
public class OrganizationListener  extends BaseModelListener<Organization> {
    @Override
    public void onBeforeCreate(Organization organization) throws ModelListenerException {
       try {
	  String eventType = "ADD";
  long companyId = CompanyThreadLocal.getCompanyId().longValue();
  long userId = 0L;
  if (PrincipalThreadLocal.getName() != null) {
     userId = GetterUtil.getLong(PrincipalThreadLocal.getName());
  }
  AuditRequestThreadLocal auditRequestThreadLocal = AuditRequestThreadLocal.getAuditThreadLocal();

  long realUserId = auditRequestThreadLocal.getRealUserId();
  String realUserName = PortalUtil.getUserName(realUserId, "");

  JSONObject additionalInfo = JSONFactoryUtil.createJSONObject();
  if ((realUserId > 0L) && (userId != realUserId)) {
     additionalInfo.put("doAsUserId", String.valueOf(userId));
     additionalInfo.put("doAsUserName", PortalUtil.getUserName(userId, ""));
  }
   additionalInfo.put("name", organization.getName());
   additionalInfo.put("type",organization.getType());
   additionalInfo.put("countryId", organization.getCountryId());
   additionalInfo.put("regionId", organization.getRegionId());
   if(organization.getParentOrganization() != null) {
       additionalInfo.put("parentName", organization.getParentOrganization().getName());
   }

   AuditMessage auditMessage = new AuditMessage(eventType, companyId, realUserId, realUserName, Organization.class.getName(), String.valueOf(organization.getOrganizationId()), null, additionalInfo);
   
AuditRouterUtil.route(auditMessage);
} catch (Exception e) {
   throw new ModelListenerException(e);
}
      }
}

Sending Notification about Audit Message to Slack

Now that we have our Model Listener and Audit Messages created, we need to create an AuditMessageProcessor class that sends the Audit Message to Slack. The sample implementation below shows you how to override the process method. To make Liferay’s OSGi runtime aware of the service, you will need to set ‘service= AuditMessageProcessor.class’, ‘immediate=true’ and ‘property = “eventTypes=*”‘ in the Component to register the service. For example:

@Component(immediate = true, property = "eventTypes=" + StringPool.STAR, service = AuditMessageProcessor.class)
public class AuditMessageToSlack implements AuditMessageProcessor {
	@Override
public void process(AuditMessage auditMessage) throws AuditException {
	// send notification to slack
	}
}

Before you can send Audit Messages to Slack, you need to do some setup in Slack itself. For example, you will want to setup a new Slack channel that you can tell Liferay to deliver the Audit Messages to. You can call the channel whatever you want, but for this example, we called it LiferayAuditMessages. When you go into Slack, click the + icon next to your channel list. In the channel, setting click on “Add an app”, search for “Incoming WebHooks” and set up the hook. You will need the Webhook URL to use in your code.

Now format the message as String so that it can be sent to Slack channel. This is another thing you will want to tailor to your scenario. For this example, we used the following format:

 StringBuffer message = new StringBuffer();
       message.append("Create Date: ");
       message.append(auditMessage.getTimestamp());
       message.append("\nResource ID: ");
       message.append(auditMessage.getClassPK());
       message.append("\nResource Name: ");
       message.append(auditMessage.getClassName());
       message.append("\nResource Action: ");
       message.append(auditMessage.getEventType());
       ......

Now, using the Apache HttpClient (or another client of your choosing), we can send the post request to the Webhook URL as shown in the example below:

CloseableHttpClient httpClient = HttpClients.createDefault();
         	HttpPost httpPost = new HttpPost(WebHookUrl);
         
JSONObject jsonObject = JSONFactoryUtil.createJSONObject();
jsonObject.put("text", message.toString());
jsonObject.put("channel", slackChannelName);
jsonObject.put("username", slackHookName);
jsonObject.put("icon_emoji", slackHookIcon);
httpPost.setEntity(new StringEntity(jsonObject.toString(), "UTF8"));
httpPost.setHeader("Content-type", "application/json");
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
	// log the response
} catch (IOException e) {
throw new AuditException(e);
}
  • WebHookUrl – the URL where your Slack implementation will receive JSON payloads
  • slackChannelName – name of the channel you setup to receive the Audit Messages
  • slackHookName – the name you will see on the Audit Message posts
  • slackHookIcon – the icon that will be shown for the message.

Summary

Liferay Audit Service is useful to save the audit trail. Implementing the AuditMessageProcessor allows us to store our Audit Trail outside of Liferay, provide visibility to important events that may otherwise go unnoticed, or take a lot of time to find. If you have questions or need help with this topic, please engage with us via comments on this blog post, or reach out to us at https://www.xtivia.com/contact/ or [email protected].

Additional Reading

If you like to learn more about Liferay Audit Service, you can visit Liferay Wiki. And to know more about Model Listeners, you can check Liferay Developer Tutorial. You can continue to explore Liferay DXP by checking out other blogs posted on https://www.xtivia.com/blog/ like The Top 10 New Features in Liferay DXP 7 from a functional perspective, or Top 5 New Features in Liferay DXP UI Development and Creating JAX-RS REST Services in Liferay DXP from a development perspective, or Top 5 DevOps Features in Liferay DXP from a devops perspective.