CodeLive: Log You Some Events for Great Good

#BugsHappen

Bugs Happen. Finding, fixing, and releasing those fixes often starts by looking not at code, but at the logs the code generates. As Apex developers I’m sure your familiar with System.debug(...) but this isn’t optimal for a number of reasons. In November, Chris Peterson and I added a Logging framework to Apex Recipes, let’s look at how it works, and why. Missed the live sessions? That’s ok – you can view them on demand here: CodeLive: Log you Some Events for Great Good Part 1 and Part 2.

Why Platform Events?

System.debug is fine, but it can be hard to wade through the system logs to identify errors, not to mention you need to have debug logs turned on for the user ahead of time. Instead of just dropping debug information to the logs, developers often create a Log object, and write Log Records. Unfortunately, if your code encounters an uncaught exception, all DML is rolled back — including your Log record. This is, of course, maddening for logging where you want to escape from the transactional boundary.

Recently, PlatformEvents got a quiet upgrade. Now you can publish a platform event immediatley without it counting as DML (at least for the sake of making subsequent callouts). (And there was much rejoicing) This means your event publication will not be rolled back, should your code encounter an uncaught exception. This change prompted Chris and I to ponder… could we build a logging framework using this? tl;dr; Yes! Yes we can.

Hold ‘up, let’s overthink this?

Every framework should be backed by a solid architecture. Chris and I wanted this to be a solid, extensible framework for logging. What we ended up with is a collection of three classes, and a Platform Event Trigger.

Developers interact with the Log class, which has only a handful of methods. It’s a great example of the Singleton pattern in action (and since Apex doesn’t share statics across requests, many of the normal evils of singletons don’t apply). Developers call Log.get() which returns the singleton Log object. After that, you can call one of the add() or publish() methods. These methods have several overloads allowing developers to add log items (Strings, Exceptions) to the buffer, or publish the buffer.

Quiddity is a new System level Enum that identifies the specific nature of this Apex transactions’ request. For instance, Quiddity will tell you that this Apex was run in a Synchronous Test environment, or at the request of an LWC component. Learn more here

Each item in the buffer is a LogMessage instance. This class allows us to enrich the logs with diagnostics like Quiddity, and RequestId to every log message without additional developer interaction. It also features a curious method that returns an sObject, but constructs a Log__e object! Our Log class’ publish methods all publish a generic list of sObjects. This is an extensibility trick. In the future, you might extend your org to not only have Log__e objects but also Communities_log__e objects. Because Events are down-castable to sObjects, the publish method can publish events of both types in one call.

Our platform event trigger uses the standard Apex Recipes trigger handler, which means our logic actually lives in the LogTriggerHandler. For the purposes of demonstration, the trigger simply stores the logs in a custom object. Ideally, you’d store these in a bigObject or otherwise collate and/or visualize the data.

Enums without classes?

Eagle-eyed observers of the dependency diagram above may be wondering about that LogSeverity class. Turns out, it’s not actually a class, but an independent Enum. When Chris first proposed this, I was unaware that it was possible! But here’s the entirety of the code:

public enum LogSeverity {
    DEBUG,
    INFO,
    WARN,
    ERROR
}

I wanted to draw your attention to this, because it addresses the number one pain point I have with Enums – having to preface their use with their enclosing Class’ name. By placing the Enum outside of a class, you no longer need to fully qualify their use. Let’s compare the use of LogSeverity to another Enum from Apex Recipes – CrudType. If you look at the CanTheUser_Tests class, you’ll see that when we utilize the CrudType Enum, we have to preface it with the class name that encloses the Enum. For instance, you have to call CanTheUser.CrudType.CREATE In contrast, we can access and use LogSeverity Enum values just by calling LogSeverity.DEBUG.

Example logging flow

All this is wonderful in theory, but what does using this logging framework actually look like? Let’s look at a couple of use-cases to illustrate its flexibility. First up, Hello World:

Log.get().publish('Hello World');

This is as simple as it gets. Because we use the singleton pattern for the Log class, we can chain our .get() call with a .publish() call. The result is a single line of code resulting in an immediate event publication. That’s great, especially for a try/catch situation – but if you want to log a number of things throughout the course of your code, then this example makes more sense:

public inherited sharing class ExampleClass {
    private static Log logger = Log.get();
    
    public ExampleClass(){
        logger.add('ExampleClass Constructed');
    }
    
    public void exampleMethod(){
        // Do some work
        logger.add('Another Log statment');
        try {
            insert new Account(name='demo');
        } catch (DMLException dmle){
            // this will add the current exception to the list and publish immediately
            logger.publish(dmle);
        }
        // if we dont catch a DML Exception, ensure we publish the logs
        logger.publish();
    }
}

In this example there are several things we want to log. Rather than immediately publishing the log buffer like we did in the first example, here we call the .add() method. At the end of our example method we call publish() to emit all the LogMessages in the buffer. Additionally, we call the publish() override method with an exception object if we catch a DMLException. This ensures that our exception is added to the buffer and the entire buffer is published before we move on!

What’s next?

Logging is an important debugging tool. Ensuring you always end up with logs —even when there’s an uncaught exception— is key to identifying and fixing bugs quickly. There’s more to the design and build of this logging framework, and you can watch the two sessions online here: CodeLive: Log You Some events for Great Good Part 1 and Part 2. Check out Apex Recipes here, to see and adopt this framework for your own org!

About the author

public inherited sharing Kevin {
    public static String pronouns = 'he/him';
    public static Double startedWithSalesforceAtApiLevel = 11.0;
    public static String[] interests = ['Apex', 'Testing', 'iOS SDK', 'Generics', 'Metaprogramming'];
    public static String funFact = 'Has two daughters he\'s training to take over the world.';
    public static String twitterHandle = '@Codefriar'; 
}