« Volver a FrontPage

Adding Auditing Functionality to Portlets

Introduction

One major requirement for enterprise applications is to audit user changes. Through the audit hook and audit portlet plugins, it is easy to add auditing capabilities to your portlets. This page will help you set up the auditing plugins and spell out what you need to do to add auditing to your portlet.

Installation And Configuration

Downloading

Download the audit hook and audit portlet for the version of Liferay you're using, and drop them into the deploy folder so Liferay will auto-deploy them.

Configuration

The one configuration step we need to take is to enable the audit filter for the portal. To do that, perform the following steps:

  1. Stop the application server.
  2. Enable the audit filter.
  3. Start the application server.

Enable The Audit Filter

The audit filter is a servlet filter available in the main Liferay servlet, but it is disabled by default. The audit filter sole responsibility is to populate a ThreadLocal variable (com.liferay.portal.kernel.audit.AuditRequestThreadLocal) with the details of the incoming request (user id, client IP address and hostname, etc.) so it is available within portlets that are adding auditing information.

Note: Since this is a ThreadLocal instance, this information will not be available to other threads. If you are creating threads to do some work and want the thread to be able to add auditing information, you will need to find a way to transfer the info to the thread manually.

Enabling the audit filter is done by adding the following to your portal-ext.properties file:

# Enable the audit filter
com.liferay.portal.servlet.filters.audit.AuditFilter=true

If you do not enable the audit filter, the audit information added by your code will not contain the user and other information that you probably really need to have.

Current Auditing State

After starting your application server, log in as an administrator and go to the control panel.

You'll see a new option under the Portal section labeled Audit Reports; this is provided by the audit-portlet plugin. Clicking on Audit Reports will show you just a couple of records indicating that you've logged into the portal.

These records were added via the audit-hook plugin. The hook will add auditing info for the following events:

  • Portal Login/Logout
  • User Impersonation
  • User Profile Changes (address and contact information)
  • Role changes for users, groups, and organizations
  • User Group Changes (group changes, membership changes)
  • User changes (add, delete, update)

Basically all the kinds of administrative tasks that systems auditing is concerned about.

Adding Auditing to Portlets

Now we are getting to the fun stuff...

Adding auditing information is done through two basic steps:

  1. Create and populate a com.liferay.portal.kernel.audit.AuditMessage instance.
  2. Call the com.liferay.portal.kernel.audit.AuditRouterUtil.route() method with your AuditMessage instance.

Creating the AuditMessage

The com.liferay.portal.kernel.audit.AuditMessage class is a container for the information used to capture the audit information. As a container, you can really put a lot of information in it relevant to what you are trying to audit.

The AuditMessage class has numerous constructors, the most interesting one is:

public AuditMessage(String eventType, long companyId, long userId, String userName, String className, String classPK, String message, Date timestamp, JSONObject additionalInfo);

Parameters for this constructor are:

Parameter Description
eventType The event type being captured, relates to a message bundle key.
companyId The companyId of the user.
userId The userId the audit message is for.
userName The name of the user.
className The class name the audit message is for, typically the class of the object that is being modified.
classPK The primary key value for the object being modified.
message The message for the audit record.
timestamp The time to store with the audit message, if null the current time is used.
additionalInfo A JSONObject for storing optional, arbitrary info with the audit message.

This is a lot of stuff to try to collect just to build an AuditMessage instance. Since you've got the audit hook, I recommend looking at the classes in the audit-hook/WEB-INF/src/com/liferay/portal/audit/hook/listeners/util directory. You'll find a very handy class, AuditMessageBuilder, which you can either copy and use as-is or as a foundation for developing your own AuditMessage factory class.

Remember the audit filter we enabled in portal-ext.properties file earlier? Well this AuditMessageBuilder class leverages that AuditRequestThreadLocal object to provide the method:

public static AuditMessage buildAuditMessage( String eventType, String className, long classPK, 
   List<com.liferay.portal.audit.hook.listeners.util.Attribute> attributes) {
 long companyId = CompanyThreadLocal.getCompanyId();
 long userId = 0;
 if (PrincipalThreadLocal.getName() != null) { 
   userId = GetterUtil.getLong(PrincipalThreadLocal.getName()); 
 }
 AuditRequestThreadLocal auditRequestThreadLocal = AuditRequestThreadLocal.getAuditThreadLocal();
 long realUserId = auditRequestThreadLocal.getRealUserId(); 
 String realUserName = PortalUtil.getUserName( realUserId, StringPool.BLANK);
 JSONObject additionalInfo = JSONFactoryUtil.createJSONObject();
 if ((realUserId > 0) && (userId != realUserId)) { 
   additionalInfo.put("doAsUserId", String.valueOf(userId)); 
   additionalInfo.put( "doAsUserName", PortalUtil.getUserName(userId, StringPool.BLANK)); 
 }
 if (attributes != null) { 
   additionalInfo.put("attributes", _getAttributesJSON(attributes)); 
 }
 return new AuditMessage( eventType, companyId, realUserId, realUserName, className, 
   String.valueOf(classPK), null, additionalInfo); 
}

Notice how you can use the AuditRequestThreadLocal class to get not just access to the current user information, but it also knows about the user being impersonated!

The flexibility of the AuditMessage to contain information is really the additionalInfo member of type JSONObject. Through this member you can add name/value pairs, triplets, individual values, etc. The Attribute class referenced above is a name/new value/old value container (for auditing changes) and a handy AttributesBuilder class (also in the com.liferay.portal.audit.hook.listeners.util package) can be used to track new value/old value member changes for two POJOs (the new and old POJO).

Calling AuditRouterUtil.route()

After creating and populating your AuditMessage object, the final step is to actually post it. That is done using the following code:

try {
 AuditRouterUtil.route(myAuditMessage);
} catch (AuditException e) {
       handle exception here 
}

Auditing Example

So here's an example snippet demonstrating the auditing.

We'll assume we have a class MyBean that we want to audit changes on. The MyBean class is defined as follows:

package com.example;
public class MyBean {
 ...
 private Long id;
 private String value;
        appropriate getters/setters 
}

For the auditing, we'll assume that we want to capture the before and after picture for the MyBean class, and we'll also assume that we've collected the various changes and we're ready to save them. Our save method basically would be something along the lines of:

package com.example.myservice;
import com.example.MyBean;
import com.liferay.portal.audit.hook.listeners.util.*;
import com.liferay.portal.kernel.audit.*;
public class MyService {
 ...
 public void updateMyBean(MyBean oldBean, MyBean newBean) {
   ...
          collect the attributes we want to audit    
   AttributesBuilder attributesBuilder = new AttributesBuilder(newBean, oldBean);
   attributesBuilder.add("id");
   attributesBuilder.add("value");
          create the audit message
   AuditMessage auditMessage = AuditMessageBuilder.buildAuditMessage(
     "my.bean.updated", newBean.getClass().getName(), newBean.getId(), attributesBuilder.getAttributes());
         add the audit message
   try {
     AuditRouterUtil.route(auditMessage);
   } catch (AuditException e) {
     ...
   }
 }
 ...
}

Pretty simple, huh?

Viewing The Results

Build and deploy your portlet that has the code above. Add the portlet to a page and do something in the portlet that invokes the auditing code.

Go to the control panel to the Audit Reports page, and in your list you'll see an entries like the following:

User Name Resource ID Resource Name Resource Action
Dave Nebinger 15 model.resource.com.example.MyBean action.my.bean.updated

audit01.png

Note: The funny client IP addresses result from testing on localhost.

We can see that our audit event is in the list, but it's not very pretty compared to the other auditing events. Our next step will address this.

Providing an Audit Language Bundle

The audit events and types are made pretty by providing a language bundle with our portlet. To add a language bundle to the portlet, you have two options:

  1. Add the <resource-bundle /> tag to the portlet.xml <portlet /> block.
  2. Add a liferay-hook.xml file in the WEB-INF directory.

For the <resource-bundle /> tag option, add <resource-bundle>content/Language</resource-bundle> to the portlet.xml <portlet /> tag.

For the liferay-hook.xml option, create the liferay-hook.xml file in the WEB-INF directory w/ the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<hook>
	<language-properties>content/Language.properties</language-properties>
</hook>

In the src directory, we'll then have to create the content folder and then our Language.properties file in that folder.

In the Language.properties file, we need to provide keys to map both the object class and the action.

For our simple example, we'd use the following:

# Map the class types
model.resource.com.example.MyBean=My Bean
# Map the actions
action.my.bean.updated=My Bean Updated

Build and deploy your modified portlet. When you go back to the Audit Reports control panel, your previously unfriendly resource name and action are now friendly, as the table shows below.

User Name Resource ID Resource Name Resource Action
Dave Nebinger 15 My Bean My Bean Updated

audit02.png

Note: The funny client IP addresses result from testing on localhost.

Clicking into one of the audit message (to view the details), results in all the details for the audit event. You get the event information, the user information, the resource information, client/server host details, plus the additional information which is a string representation of the JSONObject that was added with the audit message.

audit03.png

Note: The funny client IP addresses result from testing on localhost.

The Additional Information that we added is visible, but is not friendly for users who don't know the intimate details of your coding environment.

Conclusion

You now have the basic framework to support auditing in your portlets. It's completely functionally, but could use a few improvements:

  1. The com.liferay.portal.audit.hook.listeners.util.* classes are useful and hide some of the details, but they still feel clumsy. AuditMessageBuilder needs some overloaded methods so you don't have to pass nulls for things you don't know or care about. AttributesBuilder should support chaining in the add methods and should allow for cases when you don't have an old value to play with.
  2. The Audit Reports portlet has room for improvement. First it's only available in the control panel (easy fix), but second it has no export functionality (bosses always want the reports sent to them via email attachments). Also even though we're using friendly resource names and actions, there's still stuff in here we wouldn't want to expose to non-Liferay admins. If you're going to create your own audit-portlet implementation, be sure to move the audit-portlet-service.jar file from the audit-portlet/WEB-INF/lib directory to the global lib directory (i.e. tomcat/lib/ext on Tomcat).
  3. The Additional Info is a JSONObject, so it can really hold a lot of different stuff. For capturing audit information, this works out well. But for reporting in a user-friendly manner, not so much. Planning for how you'd handle a user-friendly report will need to be considered when you are creating auditing messages.
3 archivos adjuntos
31100 Accesos
Promedio (5 Votos)
La valoración media es de 4.6 estrellas de 5.
Comentarios
Respuestas anidadas Autor Fecha
Awesome job Sandeep Nair 7 de febrero de 2012 22:57
Good job of documenting the process of adding... Vivek Agarwal 11 de marzo de 2012 19:17
Thanks! How can I get files: the audit hook... Carlos Fritz 12 de julio de 2012 16:45
They are available only to EE folks via the... David H Nebinger 20 de julio de 2012 17:00
I can't install it on trial version of Liferay... jean reget 2 de agosto de 2012 7:55
Is there an easy way to find what resource the... Alex Pline 5 de octubre de 2012 6:20
Resource ID is usually the low-level ID of the... David H Nebinger 15 de noviembre de 2012 11:17
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:04
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:05
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:05
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:05
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:05
Thanks for documenting it. good job Murali Krishna 10 de abril de 2014 6:05

Publicado el día 7/02/12 22:57.
Good job of documenting the process of adding auditing to your custom portlets, David!
Publicado el día 11/03/12 19:17.
Thanks!
How can I get files: the audit hook and audit portlet plugins?

Carlos.
Publicado el día 12/07/12 16:45.
They are available only to EE folks via the customer portal.
Publicado el día 20/07/12 17:00 en respuesta a Carlos Freitas.
I can't install it on trial version of Liferay Portal Enterprise Edition 6.1.20 EE (Paton / Build 6120 / July 31, 2012)
I have the following message "This app is only available to EE subscribers. Find out how to get a subscription. " in the store
Is it normal ?
Publicado el día 2/08/12 7:55.
Is there an easy way to find what resource the ID is referring to?
Publicado el día 5/10/12 6:20.
Resource ID is usually the low-level ID of the resource name, the actual entity. So for a Login resource action, the resource name is User (a User entity) and the resource ID is the user ID of the user that logged in.
Publicado el día 15/11/12 11:17 en respuesta a Alex Pline.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:04.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:05.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:05.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:05.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:05.
Thanks for documenting it. good job
Publicado el día 10/04/14 6:05.