Friday, September 05, 2008

Calling log4j's Logger.error from PeopleCode

A couple of years have passed since I first posted about using log4j as a logging framework for PeopleCode. In my post log4j and PeopleCode Part II, I noted that it is not possible to directly call the Logger.error method because error is a keyword in PeopleCode. I also mentioned that it would be possible to use reflection to call this method. Here is the PeopleCode required to call the error method using reflection:

Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
Local JavaObject &jErrorArgTypes = CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"));
Local JavaObject &jErrorMethod = &logger.getClass().getMethod("error", &jErrorArgTypes);

&jErrorMethod.invoke(&logger, CreateJavaObject("java.lang.Object[]", "This is an error message"));

Want it all on one line?

Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
&logger.getClass().getMethod("error", CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"))).invoke(&logger, CreateJavaObject("java.lang.Object[]", "This is another error message"));

Using reflection from PeopleCode can get ugly. If you are going to use Logger.error, then you may want to hide the Java implementation details in an app class.

29 comments:

kane81 said...

My App Class - Thank you for the reflection help!


/*****************************************************************************
* Class: LogHandler
*
* Description: Debugs using Log4J in PeopleCode
*
*****************************************************************************/
class LogHandler
method LogHandler(&className As string);
method DebugMsg(&msg As string);
method InfoMsg(&msg As string);
method WarnMsg(&msg As string);
method ErrorMsg(&msg As string);
method FatalMsg(&msg As string);
property boolean IsDebugEnabled get;
property boolean IsInfoEnabled get;
property JavaObject Log get;
private
Constant &LOG_CLASS_NAME = "org.apache.log4j.Logger";
instance JavaObject &_logger;
instance JavaObject &_javaErrorMethod;
method InitJavaMethodCall();
method GetCurrentUser() Returns string;
end-class;

/**
* Constructor
*
* @param &className - the class name that is using this instance of the log handler.
**/
method LogHandler
/+ &className as String +/
&_logger = GetJavaClass(&LOG_CLASS_NAME).getLogger(&className);
GetJavaClass("org.apache.log4j.PropertyConfigurator").configureAndWatch("log4j.properties", 60000);
%This.InitJavaMethodCall();
end-method;

/**
* InitJavaMethodCall
*
* Init a reflection java call to Log.error() method. We have to use reflection because
* error is a PeopleCode keyword.
**/
method InitJavaMethodCall
Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
Local JavaObject &jErrorArgTypes = CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"));
&_javaErrorMethod = &logger.getClass().getMethod("error", &jErrorArgTypes);
end-method;

/**
* DebugMsg
*
* @param &msg - out the debug message to log4j.
**/
method DebugMsg
/+ &msg as String +/
&_logger.debug(%This.GetCurrentUser() | " " | &msg);
end-method;

/**
* InfoMsg
*
* @param &msg - out the info message to log4j.
**/
method InfoMsg
/+ &msg as String +/
&_logger.info(%This.GetCurrentUser() | " " | &msg);
end-method;

/**
* WarnMsg
*
* @param &msg - out the warn message to log4j.
**/
method WarnMsg
/+ &msg as String +/
&_logger.warn(%This.GetCurrentUser() | " " | &msg);
end-method;

/**
* ErrorMsg
*
* We need to use the reflection api to call this method beacuse
* error is a peoplesoft reserved word.
*
* @param &msg - out the error message to log4j.
**/
method ErrorMsg
/+ &msg as String +/
&_javaErrorMethod.invoke(&_logger, CreateJavaObject("java.lang.Object[]", %This.GetCurrentUser() | " " | &msg));
end-method;

/**
* FatalMsg
*
* @param &msg - out the fatal message to log4j.
**/
method FatalMsg
/+ &msg as String +/
&_logger.fatal(%This.GetCurrentUser() | " " | &msg);
end-method;

/**
* IsDebugEnabled
*
* @return if debug is enabled.
**/
get IsDebugEnabled
/+ Returns Boolean +/
Return &_logger.isDebugEnabled;
end-get;

/**
* IsInfoEnabled
*
* @return if debug is enabled.
**/
get IsInfoEnabled
/+ Returns Boolean +/
Return &_logger.isInfoEnabled;
end-get;

/**
* Get Log
*
* @return the java log4j instance
**/
get Log
/+ Returns JavaObject +/
Return &_logger;
end-get;

/**
* GetCurrentUser
*
* @return the current user
**/
method GetCurrentUser
/+ Returns String +/
If (%UserId = "") Then
Return "[Unknown User]";
Else
Return "[" | %UserId | "]";
End-If;
end-method;

Jim Marion said...

SWEET! Thanks kane81! I've been thinking about writing the same code!

I noticed that you used configureAndWatch. That is a great way to initialize log4j when you expect to make changes to the configuration and don't want to restart the app server. It would be great if you could stick that code in a static initializer of an app class so it would only run once per app server, but app classes don't have that concept. Instead, each time you create an instance of the LogHandler, it will re-initialize the log4j framework. Something that is interesting is that each app server instance will load the JVM once and keep it in memory until you restart the app server. What this means is that you only need to configure log4j once, not every time you create an instance of a LogHandler. Unfortunately, I haven't figured out a way to work around this... well, I do have a way, and that is to do the initialization in a static Java method and just call that method from the App Class constructor (public void init() {if (m_init) {return} else { do init code}}).

Since the LogHandler app class uses Java, each instance of this App Class has to be declared local, it can't be component or global scoped. What this means is that you will be reconfiguring the log4j framework on every event/PeopleCode routine that uses this app class. That is fine. I don't think the overhead is that great. Honestly, the approach you use is the best approach I've come up with for ensuring that the logging framework gets initialized. Now, if you have to reconfigure the log4j Framework for each event, I wonder if it really makes sense to use configureAndWatch. The reason I say that is because an event usually runs so fast, it won't be possible to change the config file while the event is running. Therefore, I'm wondering if it might be good enough to use the configure method rather than the configureAndWatch method. I say this because you will be reconfiguring the framework prior to each use anyway.

I also see that this class has JavaObject's as instance fields. Since it is possible to use a think time function (WinMessage, etc) in a PeopleCode event, you might want to put code in your app class that checks your JavaObject for null pointers (I think all or none will do it; try/catch will do it also) and reinitialize your JavaObjects if they are null.

The most efficient way to initialize the log4j framework is to use the log4j.xml or log4j.properties file and put this in your $PS_HOME/class directory. Unfortunately, however, most developers don't have access to that directory, and, therefore, need to initialize the system from a different file or use a different configuration method (store config in message catalog, etc).

Since the log4j framework will automatically initialize itself from a log4j.properties or log4j.xml file, and since I have access to that file, I don't need to reinitialize the logging framework each time I access a logger. I do, however, need to change log levels and appenders while the app server is running (I believe that is why you used configureAndWatch). Using the default initialization method, this would require an app server restart. I've been thinking about creating a LogConfigurator app class that allows you to programmatically reconfigure the log4j framework, and then creating a page to make these changes. If you wanted to persist the changes, you could even have the page write to the log4j.xml or log4j.properties file. Mainly, I just want to be able to crank the log setting up from WARN to DEBUG on a live system for debugging purposes.

Thanks again for sharing your app class! I really, really appreciate it!! I would really like to see an open source PeopleCode logging project (code.google.com or sourceforge.net) that contains reusable app classes for making logging easier. Your LogHandler is a great start on that project! It would also be great to see PeopleSoft specific appenders, for example, an appender that writes to the app server log, or an appender that writes to the PeopleSoft database.

Chili Joe said...

Hi Jim,

Have you tried using the ObjectDoMethod function? I think this might work:

ObjectDoMethod(&logger, "error", "This is an error message");

Also, even though PeopleBooks doesn't state it, ObjectDoMethod is also capable of return values from the invoked method.

bollywood girls said...

i like your blog ....

Thomas D'Andrea Jr. said...

I've tried replicating log4j use in PeopleSoft - we use PT849 for Fin, EPM, and HR, and though I can get your code to run in an AppEngine with no error, nothing is sent to the stdout file. There must be something I am missing?

Jim Marion said...

@Thomas, does this forum post reflect your situation? AE with java

Ciphersbak said...

Hi Jim, kane81

Thank you for the class!!!

But, i was trying to use it for an online component and I can't see the stdout being updated ...
Does it generate a file elsewhere?

Thanks!
Prashant

Jim Marion said...

@Ciphersbak, I see you are using it online. I'm not sure about STDOUT on the app server. I recommend you use a file appender.

dmath said...

Jim,

I am calling a java progam from PeopleCode and am writing logs using log4j. I have provided a location on the appserver for the log file via the log4j.properties file. But, I am not seeing the file being created anywhere. This is really a pain since I cannot debug my program which is failing when I call it. Any help qould be GREAT. I loved your blog btw.
- Dave.

Jim Marion said...

@Dave, are you sure your log4j.properties file is in your classpath? PeopleSoft delivers a log4j.properties file, so you will need to add your configuration to that delivered log4j.properties file.

To find out if your logger exists in the logger configuration used by your app server, use the following PeopleCode:

Local JavaObject &logger = GetJavaClass("org.apache.log4j.LogManager").exists("name.of.your.logger");

If(all(&logger)) Then
REM It found my logger;
End-If;

If it is finding your logger in the configuration, then the problem would have to be with your logger configuration rather than file placement.

dmath said...

Thanks a lot for the reply. I tried the peoplecode that you have provided and found that my logger was not registered. I am going to use some code to append loggers via a log4j properites that I am providing and then try it out. I am surprised that the Peoplesoft installation on our appserver does not have a log4j.properites file. I searched the whole server and did not find any. I found a "logging.properties" file though.
Also, I am not able to set the logger from within the java process that I am caling since it has to be defined as a "static" object. There seems to be some issue with the use of static variables/objects in java code being called from PeopleCode. I found a mention of this in PeopleBooks too.
Hence, I will try to set the logger from PeopleCode before calling the java process.

Thanks a lot again and please let me know if I am doing anything wrong here!

Jim Marion said...

@Dave, the hardest part is getting your log4j.properties file into the right folder. Did you put it in your app server's $PS_HOME/classes or %PS_HOME%\class (depends on your OS: Linux/Unix or Windows) directory? I recommend placing the file in the same location as your log4j.jar file.

I use statics with PeopleCode all the time. In fact, when I write Java for use with PeopleCode, I almost always use static methods. I find it much easier to use GetJavaClass (for static methods) than CreateJavaObject. I think this is because my purpose for writing custom Java is just to create a convenience wrapper around existing Java so I don't have to deal with reflection, etc from PeopleCode (because it isn't needed in Java).

dmath said...

Jim,

Thanks a lot for all your suggestions. Your suggesttions led me to read up more on using log4j from PS and now I know it a lot better than what I knew before:) I tried what you suggested and it works for me now. My java application is being properly called from PeopleCode and it is running smooth. Now, that all of that is fixed, not I can go ahead and focus on the business logic and will be able to install my project on time. Thanks a lot again.
- Dave.

Jim Marion said...

@Dave, very good. Thank you very much for the feedback!

Unknown said...

Hi Jim,
Please help in calling a java from peoplecode,I have java program

import java.awt.Color;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Comparator;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.html.HtmlParser;
import com.lowagie.text.html.HtmlTags;
import com.lowagie.text.pdf.PdfWriter;
import com.lowagie.*;
public class Hello {
/**
* @param args
* @throws DocumentException
* @throws FileNotFoundException
*/
public static void main(String[] args) throws

FileNotFoundException,
DocumentException {
// TODO Auto-generated method stub
Document document = new Document(PageSize.A4,

50, 50, 50, 50);
PdfWriter writer =

PdfWriter.getInstance(document,
new

FileOutputStream("/psoft/pshcm/pshrapp/pt847/apps
erv/test.PDF"));
document.open();
document.add(
new Paragraph(" First page of
document."));
document .add(new Paragraph(
"Some more text on the first page with different color and font type.",
FontFactory.getFont(FontFactory.COURIER,

14, Font.BOLD,
new Color(255, 150, 200))));
document.close();
writer.close();
}
}
/*----------------*/

I need to call this java program thru peoplecode

whether i need to use CreateJavaObject(Hello)/GetJavaClass(Hello)

If so whether i need to give path of class file from appserv
i.e(CreateJavaObject(/psoft/pshcm.../Hello) or directly we can give the class file


Thanks
Vignesh

Jim Marion said...

@Vignesh, whether or not you use CreateJavaObject or GetJavaClass depends on whether you are calling a static method or an instance method. Static methods are declared to be static, as you see in the public static void main declaration in your code. For static methods, you use GetJavaClass.

The value you pass to GetJavaClass or CreateJavaObject is the fully qualified class name (package.class). In your case, I don't see a package identifier, so the parameter would just be "Hello". The app server uses the standard Java search mechanism for finding classes. You don't specify the path to the class. Instead, you place the class in the app server's (or process scheduler server's) class path. This is usually $PS_HOME/classes or %PS_HOME%\class (or something like that).

From the code, it looks like you are calling the "main" method. This is acceptable, but unusual. The "main" method is the entry point used to launch a program. If you want to interact with Java objects, etc, then you don't usually call main, but you can. A more appropriate way would be to refactor your class so that it has a static method you can call to get the class to do its work then have main call that method. That way you can call the method without calling main. The reason behind this is that main only accepts String arguments. By moving the logic out of main, you can call it and pass in strongly typed parameters, like File or OuptuStream, etc.

Dan said...

I finally got the incentive and time to implement this. Based on Jim's comments and my own preferences, I made some minor mods to kane81's code. Here's my version:


/*****************************************************************************
* Class: LogHandler
*
* Description: Debugs using Log4J in PeopleCode
*
*****************************************************************************/
class Logger
method Logger(&className As string, &appendUserId As boolean);
method debug(&msg As string);
method info(&msg As string);
method warn(&msg As string);
method err(&msg As string);
method fatal(&msg As string);
property boolean isDebugEnabled get;
property boolean isInfoEnabled get;
property JavaObject log get;
private
Constant &LOG_CLASS_NAME = "org.apache.log4j.Logger";
instance JavaObject &_logger;
instance JavaObject &_javaErrorMethod;
instance boolean &_appendUserId;
method getCurrentUser() Returns string;
method formatMessage(&msg As string) Returns string;
end-class;

/**
* Constructor
*
* @param &className - the class name that is using this instance of the log handler.
* @param &appendUserId - include optional Oprid in log message.
**/
method Logger
/+ &className as String, +/
/+ &appendUserId as Boolean +/
&_logger = GetJavaClass(&LOG_CLASS_NAME).getLogger(&className);
&_appendUserId = &appendUserId;
end-method;


/**
* debug
*
* @param &msg - out the debug message to log4j.
**/
method debug
/+ &msg as String +/
&_logger.debug(%This.getCurrentUser() | " " | &msg);
end-method;

/**
* Info
*
* @param &msg - out the info message to log4j.
**/
method info
/+ &msg as String +/
&_logger.info(%This.getCurrentUser() | " " | &msg);
end-method;

/**
* Warn
*
* @param &msg - out the warn message to log4j.
**/
method warn
/+ &msg as String +/
&_logger.warn(%This.getCurrentUser() | " " | &msg);
end-method;

/**
* err
*
* We need to use the reflection api to call this method beacuse
* error is a peoplesoft reserved word.
*
* @param &msg - out the error message to log4j.
**/
method err
/+ &msg as String +/

If &_javaErrorMethod = Null Then
Local JavaObject &jErrorArgTypes = CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"));
&_javaErrorMethod = &_logger.getClass().getMethod("error", &jErrorArgTypes);
End-If;
&_javaErrorMethod.invoke(&_logger, CreateJavaObject("java.lang.Object[]", %This.formatMessage(&msg)));
end-method;

/**
* fatal
*
* @param &msg - out the fatal message to log4j.
**/
method fatal
/+ &msg as String +/
&_logger.fatal(%This.getCurrentUser() | " " | &msg);
end-method;

/**
* isDebugEnabled
*
* @return if debug is enabled.
**/
get isDebugEnabled
/+ Returns Boolean +/
Return &_logger.isDebugEnabled;
end-get;

/**
* isInfoEnabled
*
* @return if debug is enabled.
**/
get isInfoEnabled
/+ Returns Boolean +/
Return &_logger.isInfoEnabled;
end-get;

/**
* Get log
*
* @return the java log4j instance
**/
get log
/+ Returns JavaObject +/
Return &_logger;
end-get;

/**
* getCurrentUser
*
* @return the current user
**/
method getCurrentUser
/+ Returns String +/
If (%UserId = "") Then
Return "[Unknown User]";
Else
Return "[" | %UserId | "]";
End-If;
end-method;

/**
* formatMessage
*
* Format message to include optional Oprid
*
* @return the message
**/
method formatMessage
/+ &msg as String +/
/+ Returns String +/
If &_appendUserId Then
&msg = %This.getCurrentUser() | " " | &msg;
End-If;
Return &msg;
end-method;

Jim Marion said...

@Dan, thank you for sharing.

Dan said...

Oops - forgot to carry tho on my last change. Here it is again.



/*****************************************************************************
* Class: LogHandler
*
* Description: Debugs using Log4J in PeopleCode
*
*****************************************************************************/
class Logger
method Logger(&className As string, &appendUserId As boolean);
method debug(&msg As string);
method info(&msg As string);
method warn(&msg As string);
method err(&msg As string);
method fatal(&msg As string);
property boolean isDebugEnabled get;
property boolean isInfoEnabled get;
property JavaObject log get;
private
Constant &LOG_CLASS_NAME = "org.apache.log4j.Logger";
instance JavaObject &_logger;
instance JavaObject &_javaErrorMethod;
instance boolean &_appendUserId;
method getCurrentUser() Returns string;
method formatMessage(&msg As string) Returns string;
end-class;

/**
* Constructor
*
* @param &className - the class name that is using this instance of the log handler.
* @param &appendUserId - include optional Oprid in log message.
**/
method Logger
/+ &className as String, +/
/+ &appendUserId as Boolean +/
&_logger = GetJavaClass(&LOG_CLASS_NAME).getLogger(&className);
&_appendUserId = &appendUserId;
end-method;


/**
* debug
*
* @param &msg - out the debug message to log4j.
**/
method debug
/+ &msg as String +/
&_logger.debug(%This.formatMessage(&msg));
end-method;

/**
* Info
*
* @param &msg - out the info message to log4j.
**/
method info
/+ &msg as String +/
&_logger.info(%This.formatMessage(&msg));
end-method;

/**
* Warn
*
* @param &msg - out the warn message to log4j.
**/
method warn
/+ &msg as String +/
&_logger.warn(%This.formatMessage(&msg));
end-method;

/**
* err
*
* We need to use the reflection api to call this method beacuse
* error is a peoplesoft reserved word.
*
* @param &msg - out the error message to log4j.
**/
method err
/+ &msg as String +/

If &_javaErrorMethod = Null Then
Local JavaObject &jErrorArgTypes = CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"));
&_javaErrorMethod = &_logger.getClass().getMethod("error", &jErrorArgTypes);
End-If;
&_javaErrorMethod.invoke(&_logger, CreateJavaObject("java.lang.Object[]", %This.formatMessage(&msg)));
end-method;

/**
* fatal
*
* @param &msg - out the fatal message to log4j.
**/
method fatal
/+ &msg as String +/
&_logger.fatal(%This.formatMessage(&msg));
end-method;

/**
* isDebugEnabled
*
* @return if debug is enabled.
**/
get isDebugEnabled
/+ Returns Boolean +/
Return &_logger.isDebugEnabled;
end-get;

/**
* isInfoEnabled
*
* @return if debug is enabled.
**/
get isInfoEnabled
/+ Returns Boolean +/
Return &_logger.isInfoEnabled;
end-get;

/**
* Get log
*
* @return the java log4j instance
**/
get log
/+ Returns JavaObject +/
Return &_logger;
end-get;

/**
* getCurrentUser
*
* @return the current user
**/
method getCurrentUser
/+ Returns String +/
If (%UserId = "") Then
Return "[Unknown User]";
Else
Return "[" | %UserId | "]";
End-If;
end-method;

/**
* formatMessage
*
* Format message to include optional Oprid
*
* @return the message
**/
method formatMessage
/+ &msg as String +/
/+ Returns String +/
If &_appendUserId Then
&msg = %This.getCurrentUser() | " " | &msg;
End-If;
Return &msg;
end-method;

Srini said...

Hi Jim,

I have been reading a blog post that explains creating Excel documents in PeopleSoft using JExcel.

When I tried the reflection example, I'm getting an internal error.(java.lang.InternalError)..Does this normally happen?

Your thoughts please..Java version 1.4

Jim Marion said...

@Srini, I have not seen that before. When I worked with Excel spreadsheets from PeopleCode, I used the Apache POI Java classes.

It sounds like what is thrown is a java.lang.InternalError. What you want to do is catch the error and log the values from some of its methods: toString(), printStackTrace(PrintWriter), etc. The thing is though... I'm not sure how you would get access to the thrown Java object within PeopleCode. If you use a PeopleCode try/catch, you will get a PeopleCode error object, not a Java error object. If I were debugging this, I might create a Java wrapper class with a try/catch and in my catch create a PrintWriter(new FileWriter("path to log file")) so I could log what is going on.

Srini said...

Thanks Jim. I will give a try..

CSV said...

Please give a workaround on the below problem.

Getting an error
"java.lang.reflect.InvocationTargetException: during call of java.lang.reflect.Method.invoke"

At this code

"&configureMethod.invoke(&configurator,
CreateJavaObject("java.lang.Object[]", &props));"

when trying to configure the properties file, it is throwing the error.




Code given below

method initialize
/* Create test logger used to verify system initialized.
* Note: This is just the default config. If you want to see
* debug statements, then create a configuration for a logger
* named APT_LOG4J.
*/
&thisLogger_ = GetJavaClass(
"org.apache.log4j.Logger").getLogger(&THIS_LOGGER_NAME);
Local JavaObject &appender =
CreateJavaObject("org.apache.log4j.varia.NullAppender");

&thisLogger_.addAppender(&appender);
&thisLogger_.setLevel(GetJavaClass("org.apache.log4j.Level").OFF);

/* Initialize loggers from a Message Catalog Entry.
* See http://bit.ly/439d4T
*/
Local string &config = MsgGetExplainText(26000, 1, "");
Local JavaObject &configString =
CreateJavaObject("java.lang.String", &config);
Local JavaObject &in = CreateJavaObject(
"java.io.ByteArrayInputStream", &configString.getBytes());
Local JavaObject &props = CreateJavaObject("java.util.Properties");
Local JavaObject &configurator =
CreateJavaObject("org.apache.log4j.PropertyConfigurator");

REM ** Use reflection to avoid "More than one overload matches";
Local JavaObject &configureMethod =
&configurator.getClass().getMethod("configure",
CreateJavaObject("java.lang.Class[]", &props.getClass()));

&props.load(&in);
&configureMethod.invoke(&configurator,
CreateJavaObject("java.lang.Object[]", &props));

%This.getThisLogger().info("Log4j subsystem initialized from " |
"Message Catalog Entry");
end-method;

Jim Marion said...

@CSV, InvocationTargetException is a wrapper around the exception thrown by the invoked method: org.apache.log4j.PropertyConfigurator.configure(). Check your app server logs (or AE/Process Scheduler log if running a process) for the full Java stack trace.

Unknown said...

Local JavaObject &workbook1 = GetJavaClass("org.apache.poi.hssf.usermodel.HSSFWorkbook");
Local JavaObject &sheet1 = GetJavaClass("org.apache.poi.hssf.usermodel.HSSFSheet");
&sheet1 = &workbook1.createSheet(" sheet1");

Trying to create a blank sheet using above code but getting the error.

Calling Java org.apache.poi.hssf.usermodel.HSSFWorkbook.createSheet: more than one overload matches.

I now we can resolve using reflection ,but can anybody can help me as i am new to java.

Jim Marion said...

@Rishabh you are headed down the right track. You may want to repost your question on the PeopleSoft OTN General Discussion forum.

Sasank Vemana said...

Jim - I am trying to implement log4j using the examples provided in your book.

I like your approach where we can configure the log4j properties on the fly using a message catalog.

But as you know in a production environment, we would have several app servers with each server consisting of several domains.

So using the IScript Reload option (as you had implemented in the example) would only reload the configuration on the app server domain where the IScript request was served.

It will not be possible to reload the configuration on each app server domain using this approach manually using the IScript. I think this might be necessary because we cannot predict which app server domain the user(s) (we are targeting).

I am trying to implement something that would detect a change to the message catalog and efficiently reload the configuration.

Any ideas?

Thanks,
Sasank

Sasank Vemana said...

Cross posting this issue (already posted on OTN) as it is a progression from my previous question. Hoping I can get some input from Jim or other visitors to the group!

I am trying to implement log4j in PeopleCode using Jim Marion's book (PeopleTools Tips and Tricks) as a guide.

It works great. I have found a problem with it which I am hoping I would find some guidance from anyone who has implemented anything similar.

During my unit testing, I am finding that the log file rolls (as expected) based on the configuration in my properties only if I reload the configuration every time I invoke the logging.

If I load the config one time (initially), and then continue without reloading the configuration every subsequent call (until it is necessary... i.e.: the logger cannot be found by logManager), it still works fine but the rolling does not work.

It is almost as if the custom appender for maxFileSize is not taking effect.

#File appender configuration

log4j.appender.cust_file=org.apache.log4j.RollingFileAppender
log4j.appender.cust_file.maxFileSize=100000
log4j.appender.cust_file.maxBackupIndex=5
log4j.appender.cust_file.File=./LOGS/cust_log4j.log
log4j.appender.cust_file.layout=org.apache.log4j.PatternLayout
log4j.appender.cust_file.layout.ConversionPattern=%d %-5p [%c] - %m%n

Has anyone experienced something similar?

Also, I would like to share something that might be useful for others. I found a way to check the current log4j properties in memory using the following code. And I can actually see my custom log4j loggers and appenders in memory. Therefore, I am not sure what is going wrong with my issue (some sort of override)?

PeopleCode to identify current in-memory log4j properties:

/* Get current log4j properties */
Local JavaObject &bos1 = CreateJavaObject("java.io.ByteArrayOutputStream");
Local JavaObject &pw = CreateJavaObject("java.io.PrintWriter", &bos1);
Local JavaObject &pp = CreateJavaObject("org.apache.log4j.config.PropertyPrinter", &pw);
&pp.print(&pw);

Local string &currentProperties = &bos1.toString();

Here are some details on my environment:

PeopleTools: 8.52.22
Application: Campus Solutions 9.0 Bundle 34
Database: Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - 64bit Production
App Server: Linux - oel-5-x86_64

Note: I am currently only in our development environment and we don't have multiple app server domains (only one). So I doubt that it could be anything to do with the properties NOT being loaded on the app server domain. Also, I have used the code above to verify that in the app server domain where the files does NOT roll, I do see the custom properties in memory.

Thanks in advance!

Jim Marion said...

@Sasank, log4j configurations are static within the JVM. As long as the JVM is resident, the configurations will stick. I tested this extensively in PeopleTools 8.49. I became aware of the impact on PeopleTools while using the Telnet log target. It starts a Telnet server, which is great, but you can only do this ONCE. Successive calls will fail because there is already a listener.

I have not tested on later releases. Perhaps PeopleTools treats Java differently than it used to?