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
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
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:
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
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:
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:
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!
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