One characteristic of professional applications is their manageability. How easy is it for run-time Database and Application Server Administrators to work with the application, especially when there are issues with it, such as performance degradation. An important element to support administration of applications is the built-in instrumentation of those applications: which switches can be thrown to have the application expose information about what it is doing, which resources it is using, which tasks it is performing as well as the values of key data elements. To trace performance bottlenecks, we need to know which tasks in the application are taking up more time than anticipated. To be able to solve those problems, we need to analyze which steps performed where (which tier) are causing the problems.
One example of a superbly instrumented infrastructure is the Oracle RDBMS. Using dynamic Data Dictionary Views (V$..) and the many trace-facilities, an Oracle DBA can analyze database operations in great detail. If the issues in an ADF Application can be traced back to the database, they can be traced to individual queries or PL/SQL statements. And subsequently solved!
However, to trace an issue experienced by the end-user in the ADF Web Application to a specific operation in the database, we have to associate whatever is going on in the database with the end-user operation in the Web Application. That is where the supplied package dbms_application_info enters our story: It can be used to set information about the client application currently using a certain database connection or session. One of the advantages of using this package is that the values set can be seen in the v$session view – across sessions – as well as in trace files. For a more detailed description of using dbms_application_info, see also a previous article: Good Citizenship – Have Client Applications register themselves with the database (https://technology.amis.nl/blog/?p=814 ).
begin
DBMS_APPLICATION_INFO.SET_MODULE
( module_name => 'MODULE_NAME'
, action_name => 'The Current Mode or State of the Module, something like ''Browsing'' '
);
-- sets columns Module and Action in the V$SESSION view
DBMS_APPLICATION_INFO.SET_CLIENT_INFO
( client_info => 'extra information about the client module, such as the current Task Context'
);
-- sets column Client Info in the V$SESSION view
dbms_session.set_identifier('An identification of the current client and user');
-- corresponds to value in the v$session view column CLIENT_IDENTIFIER
end;
(Also see: http://blogs.ittoolbox.com/database/solutions/archives/oracle-session-tracing-part-ii-16500 .)
In PL/SQL terms, this is what we must do to enable instrumentation of the database operations of our ADF application. Calling these procedures from our ADF application is easy of course. The main questions are: which information should we pass to dbms_application_info and when should we make the calls (what are the hooks in ADF to call dbms_application_info).
The first question is: which information to set? Well, considering that our web application is likely to use a single database user to serve many web users and given the fact that our database potentially at least supports several applications, it seems obvious we should inform the database about the name of our application as well as the name of the current end-user. The task being performed in a web application can frequently be seen as analogous to the Web page being processed (in JSF terms the FacesContext.getCurrentInstance().getApplication().getView()).
Question #2: When and how to set these values
The name of the Web User and the Application typically do not change during the session of a user. So telling the database about them needs to be done only when the session starts using a new database connection. In ADF BC terms, that event is covered by the prepareSession() method on the ApplicationModule.
The name of the Web page being processed can change per request – and even within a request. The ADF BindingFilter calls beginRequest and endRequest on all Data Controls. The more granular ADF and JSF life cycle events are:
(see: http://download.oracle.com/docs/html/B25947_01/bcampool002.htm )
Typical LifeCycle events to hook into: prepareModel (after initBinding Context , just before we start accessing the Model (and therefore in case of ADF BC the Database) for preparations to eventually use Model services for processing the page and possibly prepareRender at the end of which we are more or less done with the Database.
Also see: http://download-uk.oracle.com/docs/html/B25947_01/web_form002.htm#CFHDGFEF (JDeveloper 10.1.3 on ADF/JSF LifeCycle Events)
We have basically two ways to hook into the LifeCycle events:
- extend the PageLifeCycle class
- register a custom PhaseListener that responds to prepareModel and renderResponse events
Alternatively, we can add page level calls into the Application Module to set the database application info, by adding invokeAction elements and method bindings to individual Page Definitions.
Note: using one of Steve Muench’s undocumented samples (Understanding How Pooling Parameters Affect Runtime) we can learn how to implement a DataControlFactory that wraps the standard oracle.adf.model.bc4j.DataControlFactoryImpl and returns a custom DataControl MyDCJboDataControl that extends JUApplication – the default for ADF BC DataControls – and overrides beginRequest() and endRequest(). In these overridden methods, we could call the ApplicationModule to set some Database Context information. BeginRequest() is executed exactly once per request as is EndRequest(). PrepareSession() – as we have seen – by contrast is only called when the ApplicationModule instance is used with a new HttpSession, which may be only once in many requests for a specific session. Configuring the custom DataControl Factory is done by the way in the DataBinding.ctx file, for individual DataControls, such as this one:
...
<dataControlUsages>
<BC4JDataControl id="AppModuleDataControl" Package="devguide.model"
FactoryClass="devguide.view.MyDataControlFactoryImpl"
SupportsTransactions="true" SupportsFindMode="true"
SupportsRangesize="true" SupportsResetState="true"
SupportsSortCollection="true"
Configuration="AppModuleLocal" syncMode="Immediate"
xmlns="http://xmlns.oracle.com/adfm/datacontrol"/>
</dataControlUsages>
...
For now, we will stick with the custom PageLifeCycle Class, using the (start) prepareModel and (complete) prepareRender events.
Implementing Instrumentation in ADF
The st
eps we have to go through to
get it all up and running:
- override prepareSession() in our ApplicationModuleImpl (have it call dbms_application_info and dbms_session using information it learns from ADFContext.getCurrent().getSessionScope().get(KEY))
- create a method setApplicationTask() in the ApplicationModuleImpl; publish this method in the Client Interface of the ApplicationModule; the method calls dbms_application_info.set_task for fine grained activity tracing
- (probably) set up some ServletFilter to load some information into the Session: name of web-user, additional client info (such as remote IP Address), name of the module
- create a PageDefinition with a MethodBinding for the setModuleTask() method on the ApplicationModuleImpl
- create a custom extended PageLifeCycle class and override prepareModel() and prepareRender(); configure the custom PageLifeCycle class through a class that extends ADFPhaseListener (overriding createPageLifecycle()) and is configured in faces-config.xml
<lifecycle>
<phase-listener>my.personal.extensions.ExtendedADFPhaseListener</phase-listener>
</lifecycle> - optionally: add calls to setModuleTask() in custom methods in ApplicationModuleImpl and ViewObject(Row)Impl that start potentially long-running PL/SQL operations
Specifics:
prepareSession() is implemented like this:
public void prepareSession(SessionData sessionData) {
final String MODULE_ID = "MODULE_ID";
final String CLIENT_IDENTIFIER = "CLIENT_IDENTIFIER";
final String CLIENT_INFO = "CLIENT_INFO";
super.prepareSession(sessionData);
String sqlStr =
"begin \n" +
" DBMS_APPLICATION_INFO.SET_MODULE \n" +
" ( module_name => '"+ ADFContext.getCurrent().getSessionScope().get(MODULE_ID) +"' \n" +
" , action_name => 'Initialization, AM Check Out'\n" +
" );\n" +
" DBMS_APPLICATION_INFO.SET_CLIENT_INFO \n" +
" ( client_info => '"+ ADFContext.getCurrent().getSessionScope().get(CLIENT_INFO) +"' \n" +
" );\n" +
" dbms_session.set_identifier" +
" ('"+ ADFContext.getCurrent().getSessionScope().get(CLIENT_IDENTIFIER) +"'); \n" +
"end; "
;
int n = getTransaction().executeCommand(sqlStr);
}
Also in the ApplicationModuleImpl:
public void setApplicationTask(String task) {
String sqlStr =
"begin " +
" DBMS_APPLICATION_INFO.SET_action \n" +
" ( action_name =>'"+task+"' \n" +
" );\n" +
"end; "
;
int n = getTransaction().executeCommand(sqlStr);
}
Then we published this method in the Client Interface. The following class:
package nl.amis.adf.instrumentation;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class ContextLoaderFilter implements Filter {
private static final String MODULE_ID = "MODULE_ID";
private static final String CLIENT_IDENTIFIER = "CLIENT_IDENTIFIER";
private static final String CLIENT_INFO = "CLIENT_INFO";
public ContextLoaderFilter() {
}
public void init(FilterConfig filterConfig) {
}
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpSession session = request.getSession();
if (session.getAttribute(MODULE_ID)==null) // it is the first request for this session
{
session.setAttribute(MODULE_ID,"Customer Relationship Management, release 2.1");
session.setAttribute(CLIENT_IDENTIFIER,"Johnny.Doe@s.it.again"); // username possibly retrieved from Principal object set by JAAS Authentication
session.setAttribute(CLIENT_INFO,"IP:"+request.getRemoteAddr()+" Remote Host:"+request.getRemoteHost());
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}
was configured as ServletFilter in web.xml – to ensure that prepareSession() would find in the ADFContext what it is looking for.
<filter>
<filter-name>InstrumentationContextLoaderFilter</filter-name>
<filter-class>nl.amis.adf.instrumentation.ContextLoaderFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>InstrumentationContextLoaderFilter</filter-name>
<url-pattern>*.jspx</url-pattern>
</filter-mapping>
To be able to set fine grained application task information, we can set up a custom PageLifeCycle class:
package nl.amis.adf.instrumentation;
import oracle.adf.controller.faces.lifecycle.FacesPageLifecycle;
import oracle.adf.controller.v2.context.LifecycleContext;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.binding.OperationBinding;
public class ExtendedPageLifeCycle extends FacesPageLifecycle{
private static final String setApplicationTaskBC = "pages_SetApplicationActionPageDef";
public ExtendedPageLifeCycle() {
}
public void prepareModel(LifecycleContext lfContext) {
// update state
String task = lfContext.getBindingContainer().getName();
setCurrentTask(lfContext, "Start "+task);
super.prepareModel(lfContext);
}
private void setCurrentTask(LifecycleContext lfContext, String task) {
DCBindingContainer bc= lfContext.getBindingContext().findBindingContainer(setApplicationTaskBC);
OperationBinding ob= bc.getOperationBinding("setApplicationTask");
ob.getParamsMap().put("task", task);
ob.execute();
}
public void prepareRender(LifecycleContext lfContext) {
String task = lfContext.getBindingContainer().getName();
setCurrentTask(lfContext, task);
super.prepareRender(lfContext);
// update state
setCurrentTask(lfContext, "Done "+task);
}
}
Configure it:
add this to faces-config.xml:
<lifecycle>
<phase-listener>nl.amis.adf.instrumentation.InstrumentedADFPhaseListener</phase-listener>
</lifecycle>
<managed-bean>
<managed-bean-name>myPageLifecycle</managed-bean-name>
<managed-bean-class>nl.amis.adf.instrumentation.ExtendedPageLifeCycle</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>
And implement the InstrumentedADFPhaseListener class:
public class InstrumentedADFPhaseListener extends ADFPhaseListener {
protected PageLifecycle createPageLifecycle()
{
String expression = "#{myPageLifecycle}";
return (PageLifecycle)FacesContext.getCurrentInstance().
getApplication().createValueBinding(jsfExpression).getValue(FacesContext.getCurrentInstance());;
}
}
And create a page definition for the PageLifeCycle class to refer to. Simply create a new JSF page, drag and drop the setApplicationTask() operation from the Data Control palette to the page and save.
Now we can track in the database what our ADF Application is up to – or rather what each of the sessions opened from the ApplicationModule Pool is up to:
Note: it appears that the Application Task (ACTION in V$SESSION) is constantly trailing the page shown in the browser. The prepareRender() seems to be called only for restoring the view – while a new view does not seem to trigger prepareRender(). So while the overall mechanism is probably ok, I have to work a little on the details. (Steve, if you happen to read this – any comments?).
Resources
Documentation on DBMS_APPLICATION_INFO: http://download-west.oracle.com/docs/cd/B19306_01/appdev.102/b14258/d_appinf.htm
Life Cycle of a Web Page Request with ADF and JSF: http://download.oracle.com/docs/html/B25947_01/bcampool002.htm
Any idea on how to implement performance instrumentation for the ADF application that does not use BC.
Well done, Lucas.