OVO Tech Blog

ForceLog: A structured, extensible logger for Salesforce Apex

Introduction

David Bond

David Bond

(CRM) Software Engineer @ OVO Energy


Salesforce Apex Logging

ForceLog: A structured, extensible logger for Salesforce Apex

Posted by David Bond on .
Featured

Salesforce Apex Logging

ForceLog: A structured, extensible logger for Salesforce Apex

Posted by David Bond on .

In my opinion, one of the greatest caveats of Salesforce's proprietary language Apex is having to reinvent the wheel. The standard library offers very little when it comes to treating Salesforce like the production service you'd want it to be. This is especially true when it comes to logging and monitoring.

Most modern programming languages offer a decent logging implementation in their respective standard libraries. They're usually structured, using syslog levels or something similar. They also tend to offer some form of hook/middleware for logging, so you can ship your logs to platforms like CloudWatch, Graylog or Stackdriver. Worst case, you can write to stdout or stderr and let your cloud platform of choice handle the rest.

Often is the case where Salesforce developers have to write their own logging implementation to actually get some valuable information on the running state of the platform. In many cases, this functionality is outright disregarded.

If you're curious as to what it can do out of the box, you have one option, calling the System.debug method. This will write whatever parameter you pass it to the debug log. While you may think this suits most purposes, here's what the logging output amounts to:

log

That's the logging output of a single line of anonymous apex, where we pass a string to the debugging method. Imagine scenarios where the stack spans several classes, triggers and queries. This output can become incredibly overcrowded with mostly irrelevant information to the problem you're trying to solve, or process you're trying to monitor.

On top of this, there's no way to really do anything with these logs, such as alerting or generating metrics. Arguably, you can query the debug log like you would any other object in Salesforce using SOQL. But is output like this really of any additional value?

log-1

The CRM team at OVO manage two seperate Salesforce instances, so uniform logging and metrics are very important on systems operating a such large scales. We decided to create a one-size fits all solution to logging that provides everything you'd expect, plus the ability to tailor it to your specific needs. This is ForceLog!

I'm going to rattle off a few reasons you should consider downloading the source and adding it to your Salesforce implementation:

  • We've thought of basically everything you'd want to log and provided suitable overloads. This includes:
    • Exceptions (stack traces, messages, even includes nested exceptions)
    • Database operation result classes like SaveResult, DeleteResult etc.
    • String formatting for log messages
    • HTTP requests/responses
    • Arbitrary extensions of the SObject class, with field exclusion for all you GDPR junkies.
  • We've followed syslog levels, which is supported by most mainstream monitoring platforms. Featuring levels such as notice, critical and everyone's favourite, emergency. Plus many more!
  • Support for handling logs individually, or in bulk. You may not want to make HTTP callouts for every log line, and you probably shouldn't.
  • It has a fluent API. We chose method chaining so you don't waste valuable lines of Apex, which are a (somewhat) limited resource in Salesforce instances.
  • Add as much or as little context to your logs as you like using the withField and withFields methods.

It's pretty straightforward. Once you've got the source code mixed in with your own, you can extend the ForceLog.Logger and ForceLog.BulkLogger classes, override their respective flush and bulkFlush methods, then start calling your implementation from within your own Apex classes.

This is actually how the tests for the library work. By implementing an extension of those classes that expect certain values to be passed, we can use the library as its own test harness. This results in super clean tests that don't have System.assertEquals and System.Test calls everywhere. For full implementation details and available methods, see the repository's README

So what does a standard implementation look like? Below is a super simple implementation that sends a batch of logs in JSON format via HTTP callouts

public class CalloutLogger extends ForceLog.BulkLogger {
    /**
     * @description The HTTP endpoint to send requests to
     * @type {String}
     */
    private String endpoint;

    /**
     * @description The HTTP client to use for making requests
     * @type {Http}
     */
    private Http client;

    /**
     * @description Initializes a new instance of the CalloutLogger class
     * @param {String} name The log name, should be a class or method name.
     * @param {String} endpoint The endpoint to send HTTP requests to.
     * @constructor
     */
    public CalloutLogger(String name, String endpoint) {
        // Initialize the parent class using the name.
        super(name);

        this.endpoint = endpoint;
        this.client = new Http();
    }

    /**
     * @description Creates an HTTP POST request containing
     * the JSON-encoded logs as the body.
     * @param {List<Map<String, Object>>} logs The log data
     * @return {void}
     */
    protected override void bulkFlush(List<Map<String, Object>> logs) {
        HttpRequest req = new HttpRequest();

        req.setEndpoint(this.endpoint);
        req.setMethod('POST');
        req.setBody(JSON.serialize(logs));

        this.client.send(req);
    }
}

Once implemented, you can instantiate your CalloutLogger class wherever you intend to use it and start writing logs like so:

public with sharing class AClass {
    private CalloutLogger log;

    public AClass() {
        this.log = new CalloutLogger(AClass.type.getName(), 'callout:Logs');
    }
    
    public void doIt() {
        this.log.debug('I did it!');
        
        // When you're all done, don't forget to call `dispose` when using the bulk logger
        this.log.dispose();
    }
}

This will result in a HTTP request being made with the following JSON body:

[{
    "name": "AClass",
    "message": "I did it!",
    "level": "debug",
    "timestamp": "2012-04-23T18:25:43.511Z"
}]

Using this library will allow you to ship operational data about your Salesforce instance to an off-platform provider. The library is also open source, so feel free to raise issues and contribute.

David Bond

David Bond

https://github.com/davidsbond

(CRM) Software Engineer @ OVO Energy

View Comments...