Monday, August 19, 2019

save all infoLog messages to log

When you run a process, all errors are usually shown in the Infolog, but when the user closes the infolog window you don’t have any trace of what happened. So, happens that when a user reports an issue, unless he took a print screen, you are sometimes a bit in the dark.

Luckily in Ax there are already a built-in feature to log errors.

Let’s start with something simple and introduce the AifInfoLog class.

I suggest you look inside the method \Classes\AifInfoLog\getInfoLogData to get some understanding how it works, before you continue reading.

To demo, we can start with a simple example. Create a new class that will log some lines in the Infolog and see that AifInfoLog leverages the info caching to remember all info lines until it is reset (when you create a new AifInfoLog objects it resets):

class EF_AifInfoLogTrials

{

}

public void runSomethingInvalid()

{

    throw error(funcName() + ' did not work!!');

}

public static void main(Args _args)

{

    AifInfoLog aifInfoLog = new AifInfoLog();

   SysInfologEnumerator infologEnumerator;

    Exception ex;

    SysInfoLogStr msg;

   SysInfologMessageStruct infoMessageStruct;

    warning("This is a warning");

    error("This is an error");

    error("This is an error2");

    info("This is an info");

 

    try

    {

        new EF_AifInfoLogTrials().runSomethingInvalid();

    }

    catch

    {

        Global::exceptionTextFallThrough();

    }

 

    info('--------------');

 

    infologEnumerator = SysInfologEnumerator::newData(aifInfoLog.getInfoLogData());

    while (infologEnumerator.moveNext())

    {

        ex = infologEnumerator.currentException();

        infoMessageStruct = SysInfologMessageStruct::construct(infologEnumerator.currentMessage());

        msg = infoMessageStruct.message();

        info(strFmt('%1: %2', ex, msg));

    }

}

 

 

This is what you will get, so you see that it has cached all the lines that are then shown again when I iterate the infologEnumerator:

clip_image001

 

To log the exceptions we can introduce the SysExceptionTable where we can store our exceptions.

Before we get there, two points:

·        We need a transaction independent connection because otherwise if the main transaction is aborted because an error is thrown it would roll back the insert to SysExceptionTable too.

·        In the SysExceptionTable we set the property CreatedTransactionId = Yes so that when we insert a group of errors we have the transactionId to group them together like this:

clip_image002

 

To make the code clean and reusable is good practice to create an abstract class that all classes that need logging can inherit from

abstract class EF_AifInfoLogToSysExceptionTable

{

    AifInfoLog aifInfoLog;

}

protected void new()

{

    aifInfoLog = new AifInfoLog();//must be declared at the very beginning so to reset it.

}

protected void checkIfAnyError()

{

    /*

    we loop through the list of errors and unless it is just an informative or warning message

    we throw an exception.

    we can use this method at the end of a transaction and if any class used within the

   transaction shows an error message we will abort the transaction.

    */

   SysInfologEnumerator infologEnumerator;

    Exception ex;

 

    infologEnumerator = SysInfologEnumerator::newData(aifInfoLog.getInfoLogData());

 

    while (infologEnumerator.moveNext())

    {

        ex = infologEnumerator.currentException();

        if (ex == Exception::Info)

            continue;

        if (ex == Exception::Warning)

            continue;

 

        throw Exception::Error;

    }

}

protected void saveInfoLog(IdentifierName module = funcName())

{

   SysInfologEnumerator infologEnumerator;

    Exception ex;

    SysInfoLogStr exceptionMessage;

   SysInfologMessageStruct infoMessageStruct;

   SysExceptionTable exceptionTable;

    UserConnection ucForTransIndependent, ucForTransactionId;

 

    infologEnumerator = SysInfologEnumerator::newData(aifInfoLog.getInfoLogData());

 

    /*

    we need to use a database independet transaction

   otherwise any exception could cause a rollback of the whole transaction,

   including the log entry.

    */

   ucForTransIndependent = new UserConnection();

   ucForTransactionId = new UserConnection();

    //set the createdTransactionId to be able to group all exception together.

   exceptionTable.setConnection(ucForTransactionId);

 

    while (infologEnumerator.moveNext())

    {

        ex = infologEnumerator.currentException();

        infoMessageStruct = SysInfologMessageStruct::construct(infologEnumerator.currentMessage());

        exceptionMessage = infoMessageStruct.message();

        //info(strFmt('%1: %2', ex, exceptionMessage));

        ucForTransIndependent.ttsbegin();

        exceptionTable.Exception = ex;

        exceptionTable.Description = exceptionMessage;

        exceptionTable.Module = module;

        exceptionTable.insert();

        ucForTransIndependent.ttscommit();

    }

}

 

To complete the example, I will try to add to create a new customer with an invalid address.

In order to do that we must remind SOLID principles: single responsibility and dependency injection in particular.

So first I need to create two classes that each have the single responsibility to

·        create a new customer

·        to add the address

Next, I will create a build class that puts everything together.

Here how the project will look like:

clip_image003

Service classes Interfaces:

interface EF_ICreateCustomer

{

}

public CustAccount getNewCustAccount()

{

}

public void run()

{

}

 

interface EF_IAddAddressToCustomer

{

}

public void setNewCustAccount(CustAccount _custAccount)

{

}

public void run()

{

}

 

Service classes implementations

class EF_CreateCustomer implements EF_ICreateCustomer

{

    CustAccount newCustAccount;

}

public CustAccount getNewCustAccount()

{

    return newCustAccount;

}

public void run()

{

    CustTable custTable;

    NumberSeq numberSeq;

    Name name;

 

    numberSeq = NumberSeq::newGetNum(CustParameters::numRefCustAccount());

    newCustAccount = numberSeq.num();

 

   custTable.initValue();

 

    name                    = 'Ferrari auto1';

   custTable.AccountNum    = newCustAccount;

   custTable.CustGroup     = '020';

   custTable.Currency      = 'EUR';

   custTable.PaymTermId    = '10DD';

    custTable.PaymMode      = 'CHEQUE-01';

 

   custTable.insert(DirPartyType::Organization, name);

}

 

class EF_AddAddressToCustomer implements EF_IAddAddressToCustomer

{

    CustAccount custAccount;

}

public void setNewCustAccount(CustAccount _custAccount)

{

    custAccount = _custAccount;

}

public void run()

{

    DirParty dirParty;

   DirPartyPostalAddressView dirPartyPostalAddressView, newDirPartyPostalAddressView;

    container roles;

 

    roles = [LogisticsLocationRole::findBytype(LogisticsLocationRoleType::Invoice).RecId];

 

    dirParty = DirParty::constructFromCommon(CustTable::find(custAccount));

 

   dirPartyPostalAddressView.LocationName      = 'Headquater';

   dirPartyPostalAddressView.City              = 'Rome';

    dirPartyPostalAddressView.Street            = 'via Senato';

   dirPartyPostalAddressView.StreetNumber      = '18';

   dirPartyPostalAddressView.CountryRegionId   = 'ITA';

   dirPartyPostalAddressView.State             = 'SP';//if state does not exist you get an error.

 

    // Fill address

   newDirPartyPostalAddressView = dirParty.createOrUpdatePostalAddress(dirPartyPostalAddressView, roles);

}

 

Finally we can wrap up and make the class the builds the customer

class EF_BuildCustomer extends EF_AifInfoLogToSysExceptionTable

{

    CustAccount newCustAccount;

   EF_ICreateCustomer createCustomer;

   EF_IAddAddressToCustomer addAddressToCustomer;

}

protected void new(

                ClassId createCustomerClassId = classNum(EF_CreateCustomer),

                ClassId addAddressToCustomerClassId = classNum(EF_AddAddressToCustomer)

                )

{

    /*

   According the SOLID dependency injection principle we must never new a class

    and create a hard coded dependency.

    the best way is using an inversion of control container, but as we don't have it in AX

    we must still provide an alternate way to inject our dependent class.

   here we provide a default class but we leave to the constructor the option to inject

    a different implementation.

    */

    createCustomer = classFactory.createClass(createCustomerClassId);

   addAddressToCustomer = classFactory.createClass(addAddressToCustomerClassId);

    super();

}

public static EF_BuildCustomer construct()

{

    return new EF_BuildCustomer();

}

public void run()

{

    this.build();

}

public static void main(Args _args)

{

   EF_BuildCustomer cl = EF_BuildCustomer::construct();

    cl.run();

}

private void createCustomer()

{

   createCustomer.run();

    newCustAccount = createCustomer.getNewCustAccount();

}

private void addAddress()

{

   addAddressToCustomer.setNewCustAccount(newCustAccount);

   addAddressToCustomer.run();

}

private void build()

{

    #OCCRetryCount

 

    try

    {

        ttsBegin;

        this.createCustomer();

        this.addAddress();

        this.checkIfAnyError();

        ttsCommit;

    }

    catch (Exception::Deadlock)

    {

        // retry on deadlock

        retry;

    }

    catch (Exception::UpdateConflict)

    {

        // try to resolve update conflict

        if (appl.ttsLevel() == 0)

        {

            if (xSession::currentRetryCount() >= #RetryNum)

            {

                error(strFmt('%1', Exception::UpdateConflictNotRecovered));

                this.saveInfoLog(funcName());

                throw Exception::UpdateConflictNotRecovered;

            }

            else

            {

                retry;

            }

        }

        else

        {

            error(strFmt('%1', Exception::UpdateConflict));

            this.saveInfoLog(funcName());

            throw Exception::UpdateConflict;

        }

    }

    catch(Exception::DuplicateKeyException)

    {

        // retry in case of an duplicate key conflict

        if (appl.ttsLevel() == 0)

        {

            if (xSession::currentRetryCount() >= #RetryNum)

            {

                error(strFmt('%1', Exception::DuplicateKeyExceptionNotRecovered));

                this.saveInfoLog(funcName());

                throw Exception::DuplicateKeyExceptionNotRecovered;

            }

            else

            {

                retry;

            }

        }

        else

        {

            error(strFmt('%1', Exception::DuplicateKeyException));

            this.saveInfoLog(funcName());

            throw Exception::DuplicateKeyException;

        }

    }

    catch (Exception::Error)

    {

        error(strFmt('%1', Exception::Error));

        this.saveInfoLog(funcName());

        //throwing an error will roll back the transaction.

        throw error('sorry the customer could not be created');

    }

    catch

    {

        //always create a catch all: https://docs.microsoft.com/en-us/dynamicsax-2012/developer/exception-handling-with-try-and-catch-keywords#aa893385collapse_allen-usax60gifthe-try-and-catch-statements

        this.saveInfoLog(funcName());

        throw error('sorry the customer could not be created');

    }

}

 

Now if we try to run this class and will fail you will get the Infolog that shows the error

clip_image004

But you will also see it saved into the SysException table

clip_image006

Note also that we called this.checkIfAnyError();

At the end of the transaction in the build method.

You can see that the validate method in the LogisticsPostalAddressEntity does only logs an error, but if we would not have used this.checkIfAnyError();

The transaction would not have been aborted and we would have ended up with a customer created without the address.

So we accomplished two things, one we log the error, second we are able to abort transaction where dependent classes return an error log.

 

No comments:

Post a Comment