Apex Logging Basics

This articles explains the structure and some of the basic principles behind Pharos Triton.

Now that we've completed all the preliminary work, let's get going! We'll cover the basics of what's included in the logging tools and look at some examples.

Instantiating a Logger

Pharos Triton utilizes a Singleton pattern for accessing its capabilities. This is a handy way of ensuring that the state is preserved throughout the entire transaction. It also cuts down on the verbosity of creating a new class from scratch by referencing a static instance instead. We think that you will like this approach. However, if you don't, you can remove the Singleton implementation and switch over to the standard usage pattern.

Below is an example of how logging methods can be invoked.

try {
....
} catch (Exception e) {
    Triton.instance.error(TritonTypes.Area.Opportunities, e);
}

If you decide to remove the singleton usage, take care to observe the use cases around bulk logging.

Default Logging Methods

Pharos Triton provides a variety of logging methods to give you plenty of options for how you pass data to your log records. All of these reside inside Triton.cls. Below are a few examples:

public void error(Area area, Exception e)
public void addError(Area area, Exception e)

public void debug(Type type, Area area, String summary, String details)
public void addDebug(Type type, Area area, String summary, String details)

public void event(Level level, Type type, Area area, String summary, String details)
public void addEvent(Type type, Area area, String summary, String details)

.....

Many methods are overloaded to accept varying sets of parameters. You can log as much as you want or as little as you want.

Instantaneous vs Buffered

The logging methods are divided into two major categories: instantaneous and buffered.

The first category immediately persists the logs. In other words, the methods in this category will build the log record, buffer it, and publish the platform event.

The second category of methods simply stashes the logs into a buffer until the flush() method is called. All methods in this second category are prefixed with "add".

For example this method will instantly publish an error log event:

public void error(TritonTypes.Area area, Exception e)

This method will buffer an error log record without publishing:

public void addError(TritonTypes.Area area, Exception e)

You'll notice that most methods have a corresponding add* version. In fact, the instantaneous methods will utilize their add counterpart, followed by a flush(). Here's an example of an instantaneous error method:

public void error(TritonTypes.Area area, Exception e) {
    addError(area, e);
    flush();
}

Saving a Log

In order to save a log there are three steps involved:

  1. Build the log, using Pharos builder.

  2. Append the log to the buffer.

  3. flush() when ready.

The last step will publish the platform event. Using the default methods mentioned above will take care of all these steps for you. It's good practice to follow this pattern for your own logging methods as well.

Let's examine more closely what happens inside one of the default methods. The following method will create an error log and add it to the log buffer:

public void addError(TritonTypes.Area area, Exception e) {
    add(
        makeBuilder()
        .category(TritonTypes.Category.Apex.name())
        //use exception type, Backend if blank
        .type(String.isBlank(e.getTypeName()) ? TritonTypes.Type.Backend.name() : e.getTypeName())
        .area(area.name())
        .summary(e.getMessage())
        .stackTrace(e.getStackTraceString())
        .details(String.valueOf(e) + SPACE_SEP + e.getStackTraceString())
        .transactionId(TRANSACTION_ID)
        .createIssue()
        .attribute(LOG_LEVEL, TritonTypes.Level.ERROR.name())
        .build());
}

Few things to note here:

  • The use of the Category, Type and Area enums within the TritonTypes class. These indicate that this log is to be presented as an error with a specific type and functional area. This is covered in more detail in the enums section.

  • The use of the makeBuilder() method and the builder pattern for constructing logs. This is covered in more detail here.

  • The use of the .attribute() method that allows setting of standard and custom fields on Log records. This is addressed further in this section.

  • The use of the add() method to buffer the log.

You'll find all of the default logging methods adhering to the same pattern to various degrees. We encourage you to explore this approach and adapt it for your own needs.

This article details the full list of logging methods.

Bulk Logging

As you know, the name of the game on the Salesforce platform is "bulkification." Wherever possible, all operations are performed in bulk. There are multiple reasons for the platform to require bulk processing. When it comes to logging, though, there's one main reason: platform event (PE) allocations. In other words, there is a limit on how many platform events can be emitted. For more details on platform event limits, please refer to the Salesforce documentation here.

If your org is consuming a large number of PE allocations, the added logging will impact this limit as well. Therefore, it is more important to log in bulk by buffering as many logs as possible before calling the flush() method. In general, the same best practices apply here as for DML operations. There is a finer balance to strike here, however. If all logs are buffered, in the event of an error or exception the stashed logs may never get created. The code between logging statements will need to be carefully examined for exception possibilities. If there is a risk of an exception, it's best to flush() logs before moving on.

Important Enums

There are three default enums that come with Pharos Triton. They can be found inside the TritonTypes.cls. The purpose of these is to provide a consistent list of values for three key fields: Category, Type and Functional Area.

Why Enums? Technically, any string value can be written into the three fields mentioned above. However, for clarity purposes, it's a good idea to keep these values explicit. When new values are introduced, they should be added to the enums accordingly. This will help you with code maintenance and readability.

We highly encourage you to add your own enum values, especially for the Area enum. These values are meant to represent accurate functional areas in the context of your org. There is simply no one-size-fits-all here so feel free to expand your enums value sets.

Category

This value represents a broad category of a log or issue. These values are predefined by Pharos and it's a good idea to stick to the value set that comes out of the box. The full list is as follows:

  • Apex

  • Integration

  • LWC

  • Aura

  • Bulk API

  • Flow

  • Process Builder

  • Debug

  • Error

  • Event

  • Warning

  • Email-to-Case

  • Web-to-Lead

Many methods within Pharos Triton will automatically set a category for you.

The main reason to stick with these predefined values is to ensure that Log and Issue detail pages are rendered with the appropriate lightning page layout and debug views. While Pharos Triton allows for lots of flexibility when it comes to populating your custom logs with data, mislabeling a Category on a log record can result in a different view being displayed. For example, if you label an Apex log with a Flow category, the resulting log record will still contain all the appropriate data, but the detail page will not show the Apex stack trace.

The general guideline is to stick to the specific technology where logging is performed. If you are logging from Apex, set the category as Apex. If you are logging in Flows, leave the default Flow category and so on.

The following values are always set automatically as part of Pharos' automated error capture:

  • Bulk API

  • Flow

  • Process Builder

  • Email-to-Case

  • Web-to-Lead

It's best to stick to these category values for your custom logging needs as well. This will ensure that the correct view is presented and the relevant fields are visible

  • Apex

  • Flow

  • LWC

  • Aura

  • Integration

  • Error

  • Event

  • Warning

  • Debug

Type

Type provides a more specific technical classification. This value will be written to the Type field. For your custom logs, utilize this value as you see fit. This value assumes more freedom when it comes to the possible values and doesn't influence the view of a log record. Types are set by default during Pharos' automated error capture. For example, the default behavior of Pharos apex logs is to set the Type to the corresponding Exception type, such as DmlException. For flows, Type is set according the particular kind of flow that is utilized (e.g, Screen Flow or Autolaunched Flow). The default values included in Triton are:

  • Backend

  • Frontend

Area

Area represents Functional Area at the Log and Issue level. This goal of this value is to convey a business or data centric description of functionality. For example, let's say there's an Account trigger that you would like to add logging to. In this case it would be appropriate to introduce an Area of Accounts. Alternatively, if there's a more specific operation that is performed in the Account trigger, such as data enrichment or an integration that pushes new Accounts into an external system, you could consider utilizing a Functional Area such as AccountEnrichment or AccountSync.

Level

This enum represents log levels. These are typically used by *event() and *debug() logging methods to indicate a custom log level. Use this enum to control the verbosity of your logging output. Values are listed from the least verbose to the most verbose. For example if org logging level is set to INFO, any logs created with ERROR, WARNING or INFO levels will get saved and all others will not.

  • ERROR

  • WARNING

  • INFO

  • DEBUG

  • FINE

  • FINER

  • FINEST

LogBuilder

Pharos Triton makes use of the Builder design pattern. There's lots of information out there about this approach. Here is as an example.

In short, the builder paradigm allows for an easy-to-understand, self-documenting approach to passing data to your log files. Here is an example of what Pharos Triton does to create a log record:

makeBuilder()
    .category(Category.Debug.name())
    .type(type)
    .area(area)
    .summary(summary)
    .details(details)
.build());

Invoking the build function at the end returns a pharos__Log__c object with all the relevant fields populated. At this point the log is ready to either be buffered for later or persisted right away via the platform event.

One thing to note is the usage of the makeBuilder() method. This is simply a shorthand way of creating an instance of the Pharos builder class. It serves no other purpose than to cut down on the verbosity of creating a builder directly by referencing the pharos namespace.

public static pharos.LogBuilder makeBuilder() {
     return pharos.LogBuilder.getInstance();
}

Please refer to this section for the full API reference.

Passing Custom Attributes to Logs

In the event you have a need to track additional custom attributes on the Log record, the builder offers a simple way of passing these values. In the example below, consider a custom field My_Custom_Attribute__c on the Log record.

makeBuilder()
    .attribute('My_Custom_Attribute__c', customValue)

If you've already examined Triton.cls you might have noticed several static final constants declared at the very top, such as:

public static final String APEX_NAME = 'pharos__Apex_Name__c';
public static final String CREATED_TIMESTAMP = 'pharos__Created_Timestamp__c';
public static final String DURATION = 'pharos__Duration__c';
public static final String INTERVIEW_GUID = 'pharos__Interview_GUID_External__c';
......

These are used by various default logging methods to set custom log attributes. It's a good idea to follow this practice for your own custom attributes as well.

First declare a static final constant, and then reference it:

public static final String MY_CUSTOM_ATTRIBUTE = 'My_Custom_Attribute__c';
....
makeBuilder()
    .attribute(MY_CUSTOM_ATTRIBUTE, customValue)
....

It's a good practice to keep things organized and keep a list of Log field dependencies all in one place.

Last updated