May 19, 2010

Log4j Gmail SmtpAppender, send log emails via Gmail on Errors !

Log4j gives SMTPAppender for sending emails on logging. This SMTP Appender works good in  most of the cases, except TLS/SSL SMTP setups. Gmail works on the same SSL/TLS setup, so any one who wants to configure the default log4j SMTPAppender with Gmail will be troubled with strange Java Mail errors.

Though this problem can be easily solved by writing a Log4j Custom SMTP Appender.

“GmailSMTPAppender“ - Custom SMTPAppender for GMAIL

“GmailSMTPAppender” is the solution to the default log4j SMTPAppender SSL/TLS issue. GmailSMTPAppender is basically an extension to existing log4j SMTPAppender. It just overrides two methods from SMTPAppender i.e.

  1. protected Session createSession()
  2. protected void sendBuffer()

So by this inheritance GmailSMTPAppender utilizes the log4j’s SMTPAppender for most of the logic and specializes few methods for Gmail specific handling.

How to Install GmailSmtpAppender in my project ?

GmailSMTPAppender is open source, here is the project landing page [link]. Following are the steps to install this appender to your project.

  • Checkout the project source from here. This source is checking of a full Eclipse “Java project”.
  • Copy GmailSMTPAppender.java to your source folder.
  • Only dependencies are log4j-1.2.16.jar and java mail.jar. You can replace them with latest version if required or use your existing ones.
  • Copy and replace log4j.properties available here as required. You have to change the usual SMTPAppender settings for "log4j.appender.EMAIL". This is explained in detail below.
  • Thats it, all done !!. Use Logger.error() to send email notifications.

What about Google APPS Account ?

GmailSmtpAppender works fine with both normal Gmail accounts and Google apps account. The same SMTP Host will go for Google apps accounts. So just update the SMTPUser/Pass with From/To addresses and you are done.

Source Code & DEPENDENCIES ?

The complete code + dependencies is hosted on GoogleCode. You can checkout the code from here.

Setting up log4j.properties

Here is the sample log4j.properties. You can change or copy stuff like appenders from it as required. GmailSMTPAppender has not exposed any new appender settings. So just use your usual SMTPAppender settings and they will work fine with GmailSMTPAppender too.

#@author abhinav@tgerm.com
#Google Code Project  http://code.google.com/p/log4j-gmail-smtp-appender/
#My Blog : http://www.tgerm.com

log4j.rootLogger=ERROR, Console,R
log4j.logger.com.tgerm=DEBUG, Console, R, EMAIL
log4j.additivity.com.tgerm=false


log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=appender.log
log4j.appender.R.Append=false
log4j.appender.R.MaxFileSize=100KB
log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n

log4j.appender.EMAIL=com.tgerm.log4j.appender.GmailSMTPAppender
log4j.appender.EMAIL.SMTPHost=smtp.gmail.com
# Turn off debugging if not required
log4j.appender.EMAIL.SMTPDebug=true
log4j.appender.EMAIL.From=from@gmail.com
log4j.appender.EMAIL.To=to@tgerm.com
log4j.appender.EMAIL.SMTPUsername=smtpuser@gmail.com
log4j.appender.EMAIL.SMTPPassword=somepass
log4j.appender.EMAIL.Subject=Email Notification from Gmail SMTP Appender
log4j.appender.EMAIL.cc=cc@gmail.com
log4j.appender.EMAIL.layout=org.apache.log4j.PatternLayout
log4j.appender.EMAIL.layout.ConversionPattern=%p %t %c - %m%n
log4j.appender.EMAIL.BufferSize=1

Sample ERROR Emails !

log4j error email in gmail inbox

Log4j error email body

Feedback

Let me know in case of issues.

    May 6, 2010

    Apex No Operation Available Request Web Service Errors !

    Sometimes while working with Salesforce Web Service APIs, we get “No Operation Available for Request” error. This error comes because the “Web service ENDPOINT URL” is not correct. In my case this error came when I was trying to use “apex.wsdl” for executing Apex test cases via my java code.

    Here is the code sample that was failing for “No Operation Available For Request…/runTests”.

    SoapBindingStub binding = (SoapBindingStub) new SforceServiceLocator()
    		.getSoap();
    LoginResult lr = binding.login(username, password);
    binding._setProperty(SoapBindingStub.ENDPOINT_ADDRESS_PROPERTY, lr
    		.getServerUrl());
    SessionHeader sh = new SessionHeader();
    sh.setSessionId(lr.getSessionId());
    binding.setHeader(new SforceServiceLocator().getServiceName()
    		.getNamespaceURI(), "SessionHeader", sh);
    ApexBindingStub apexBinding = (ApexBindingStub) new ApexServiceLocator()
    		.getApex();
    /*
    Line below was cause of problem, I was setting incorrect url for binding end point.
    */
    apexBinding._setProperty(ApexBindingStub.ENDPOINT_ADDRESS_PROPERTY,
    		lr.getServerUrl());
    
    // Apex Session Header
    com.sforce.soap._2006._08.apex.SessionHeader ash = new com.sforce.soap._2006._08.apex.SessionHeader();
    ash.setSessionId(lr.getSessionId());
    apexBinding.setHeader(new ApexServiceLocator().getServiceName()
    		.getNamespaceURI(), "SessionHeader", ash);
    
    RunTestsRequest runTestsRequest = new RunTestsRequest(false,
    		new String[] { "TestFooBar" }, "myns", null);
    RunTestsResult runTests = apexBinding.runTests(runTestsRequest);
    System.out.println("Failures " + runTests.getNumFailures());

    To make this working I need to point the Apex Binding Stub end point to the correct URL. Here is the fixed line

    /*
    Metadata WSDL/ENDPOINT URL : https://[api node].salesforce.com/services/Soap/m/18.0/[org-id]
    Partner WSDL/ENDPOINT URL : https://[api node].salesforce.com/services/Soap/u/18.0/[org-id]
    
    Both these URLs can be fetched via LoginResult.getServerUrl() and LoginResult.getMetadataServerUrl(). But there is no direct method  for Apex WSDL URL. 
    Though Apex WSDL/ENDPOINT URL is simply one char change
    https://[api node].salesforce.com/services/Soap/s/18.0/[org-id]
    So here is the trick, and the above code snippet will work fine then.
    */
    apexBinding._setProperty(ApexBindingStub.ENDPOINT_ADDRESS_PROPERTY,
    	lr.getServerUrl().replaceAll("/u/", "/s/"));

    Let me know any other such issues, specially with Enterprise WSDL, I haven’t tried it.

    May 3, 2010

    Using System.runAs() for Too Many Query Rows 501 errors !

    System.runAs(<User>) is often ignored by Apex developers for their Apex Test Classes, though its a wonderful unique feature available for Apex test cases. If we use this correctly in our test code, it will for sure reduce the chances of getting “Too many query rows 501 error” on test case execution in any org.

    System.Exception: Too many query rows: 501 – Why I GOT THIS ?

    On this error, common reactions are

    • What is this too many rows 501 error ?
    • Why I am getting this error now, I did no code changes and my test code was running fine before ?
    • I am not accessing too many rows in my test code, my test data set is limited to 10-20 records at max only.
    • I am just using COUNT() in SOQL, how can I get more than 500 records in that.

    501 error comes when we cross the governor limit “Total number of records retrieved by SOQL queries”. As per this limit a single Apex testMethod can only retrieve 500 rows in all SOQL calls. We all should agree that 500 is decently big number for any good test data set. I have never seen any developer creating this big test data in Apex code, we are lazy in this regard and this is boring too sometimes :)

    Prime reasons for “TOO MANY QUERY ROWS 501” error

    1. Test code relies on ORG data, so its quite possible that in sandbox or developer env you have limited data. But the same code will fail for this error, if executed in production or any other org having massive user/customer data.
    2. Developer believes that he is only using Aggregate functions like COUNT() that return a single row, hence should be counting for 1 SOQL statement for limits. But the truth is, all records being counted on firing a SOQL having COUNT() function are deducted from your governor limits quota. Try executing this SOQL “[select count() from contact]” in Anonymous block, what number are you expecting for “Number of query rows” LIMIT ? Is that ONE, Nooo its is the total number of records counted. On my org its “Number of query rows: 1392 out of 10000” for this single SOQL query.
    3. Developer creates good test data apart from ORG data. But the code written to be executed in DEFAULT/SYSTEM MODE(The permissions and record sharing of the current user are not taken into account), thus its accessing both the ORG DATA + TEST DATA. Again, You might not see the error if
      • One is running tests in development org, where developers create limited data for Manual testing and Apex Test Class.
      • The application you built, is getting installed for first time. Assuming its not depending on many of Standard Objects and there are no records for newly created custom objects.
      • Developer is using System.runAs() in all test cases, Trust me it will be safe if used correctly :)

    How should I get rid of “Too many query rows 501 error” ?

    The only key to get rid of this error is is “ISOLATION of ORG DATA from TEST DATA !”. Following are the key points to take care while writing apex test code

    • Always create good test data for each test method, never rely on ORG data. It might not be available on deployments to other orgs.
    • Never drive test in SYSTEM MODE, that is apex default if you don’t use System.runAs(). System mode by passes most of the sharing rules and will make your test code see huge org + test data.

    I will not explain creation of good test data, its out of scope for this post. Rest of the post below will explain what is System.runAs() and how to best use it.

    What is System.runAs() ?

    The system method runAs enables you to write test methods that change either the user contexts to an existing user or a new user, or to run using the code from a specific version of a managed package.When running as a user, all of that user's record sharing is then enforced. You can only use runAs in a test method. The original system context is started again after all runAs test methods complete.

    How System.runas() will kick OFF “Too many query rows: 501” ?

    The key reason behind this 501 error is SYSTEM MODE, which is default for apex code execution. This mode opens all org data to your test code.So so so your test case is vulnerable to this error in any execution scenario, where the org has too much user data.

    System.runAs() is a life saver because, it drives the test code for a User/Profile mode rather SYSTEM MODE, so your test code only sees the data visible to the given user/profile as per records sharing rules. 

    Best practice for using System.runAs()

    You can still get “TOO MANY QUERY ROWS: 501” error after using System.runAs(). It will for sure happen, if you are depending on any existing user in System, and that user has access to huge org data that is related to your test context. So, the best way to use System.runAs() is to create a brand new User with appropriate Role/Profile, this User object should be used as argument in System.runAs(<Newly Created User>).

    Together with System.runAs(), try using Test.startTest() and Test.stopTest() to get some extra bandwidth on governor limits. As shown in code sample below.

    Here is a sample of using System.runAs()

    public class TestRunAs2 {
        public static testMethod void test2() {
            Profile p = [SELECT Id FROM profile WHERE name='Standard User'];
            User u2 = new User(alias = 'newUser', email='newuser@testorg.com',
            emailencodingkey='UTF-8', lastname='Testing', 
            languagelocalekey='en_US', localesidkey='en_US', profileid = p.Id,
            timezonesidkey='America/Los_Angeles', username='newuser@testorg.com');
    
            System.runAs(u2) {    
              // The following code runs as user u2.
              // all your test data, created here in
              // this block will not run in system mode. 
              // So access to org level data 
              // will be pretty limited
    
              // Create all your Test Data + Structures till here
              // use startTest() to give some extra buffer
              // to your real test code
              Test.startTest();
                // Do any extra DML and other stuff  
                // you have more bandwidth now.
              Test.stopTest();
            }
            
            // Any other validation and DML statements
            // if required to validate the results.
            
        }
    }

    Note: In the code sample above “We didn’t inserted the user object, the in-memory user instance is working in System.runAs() perfectly. So we can safely avoid doing such insertions !”