7

There is a logger class in my org that accepts class name and method name as parameters. So often, you will see code of roughly the following form:

public class MyClass
{
    public static void myMethod()
    {
        try
        {
            // do stuff
        }
        catch (SomeException e)
        {
            new Logger(e, 'MyClass', 'myMethod').handle();
        }
    }
}

Is it possible to get these values programmatically instead of forcing the developer to pass them in? It would be a bonus to get the line and column numbers as well.

public class Logger
{
    public final String className, methodName;
    public final Integer line, column
    public Logger()
    {
        // determine class and method names of the calling context
        // optionally determine line and column numbers
    }
}
8

Here's a fun fact to get the ball rolling: the constructor for any Exception class will generate the current stack trace (no need to throw). So you can, for instance, get the stack trace in your logger constructor:

final String firstLine;
public Logger()
{
    String stackTrace = new DmlException()
        .getStackTraceString().substringAfter('\n');

    // being in the constructor adds one line at the top of the stack
    // ignore the first line to get the stack trace of the calling context

    firstLine = stackTrace.substringBefore('\n');
}

That provides a good basis to start working the problem, but we still need to understand what sort of input/output combos we might need to support. To that end, I spun up a quick class with some basic types of calling context (first line of stack trace in comments):

public class Demo
{
    public Logger l;
    public Demo()
    { // top-level constructor
        l = new Logger(); //Class.Demo.<init>: line 6, column 1
    }
    public static Logger foo()
    { // top-level method
        return new Logger(); //Class.Demo.foo: line 10, column 1
    }
    public class Sub
    {
        Logger l;
        public Sub()
        { // inner-class constructor
            l = new Logger(); //Class.Demo.Sub.<init>: line 17, column 1
        }
        public Logger bar()
        { // inner-class method
            return new Logger(); //Class.Demo.Sub.bar: line 21, column 1
        }
    }
    public static String someProperty
    {
        get
        { // property getter
            system.debug(new Logger()); //Class.Demo.__sfdc_someProperty: line 28, column 1
            return someProperty;
        }
        set
        { // property setter
            system.debug(new Logger()); //Class.Demo.__sfdc_someProperty: line 33, column 1
            someProperty = value;
        }
    }
}

It's also a bit different from anonymous scripts:

Logger l = new Logger(); //AnonymousBlock: line 1, column 1
Logger foo()
{ // anonymous method
    return new Logger(); //AnonymousBlock: line 4, column 1
}
class Anon
{
    Logger l;
    Anon()
    { // anonymous class constructor
        l = new Logger(); //Class.Anon.<init>: line 10, column 1
    }
    Logger bar()
    { // anonymous class method
        return new Logger(); //Class.Anon.bar: line 15, column 1
    }
}

Okay, so a Regular Expression seems like the way to go. Let's try one out:

Explanation
/ ^(?:class\.)?([^.]+)\.?([^\.\:]+)?[\.\:]?([^\.\:]*): line (\d+), column (\d+)$ / gim

  • ^ asserts position at start of a line
  • Non-capturing group (?:class\.)?
    • ? Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
    • class matches the characters class literally (case insensitive)
    • \. matches the character . literally (case insensitive)
  • 1st Capturing Group ([^.]+)
    • Match a single character not present in the list below [^.]+
      • + Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
      • . matches the character . literally (case insensitive)
  • \.? matches the character . literally (case insensitive)
    • ? Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
  • 2nd Capturing Group ([^\.\:]+)?
    • ? Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
    • Match a single character not present in the list below [^\.\:]+
      • + Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
      • \. matches the character . literally (case insensitive)
      • \: matches the character : literally (case insensitive)
  • Match a single character present in the list below [.:]?
    • ? Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
      • \. matches the character . literally (case insensitive)
      • \: matches the character : literally (case insensitive)
  • 3rd Capturing Group ([^\.\:]*)
    • Match a single character not present in the list below [^\.\:]*
      • * Quantifier — Matches between zero and unlimited times, as many times as possible, giving back as needed (greedy)
      • \. matches the character . literally (case insensitive)
      • \: matches the character : literally (case insensitive)
  • : line matches the characters : line literally (case insensitive)
  • 4th Capturing Group (\d+)
    • \d+ matches a digit (equal to [0-9])
      • + Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
  • , column matches the characters , column literally (case insensitive)
  • 5th Capturing Group (\d+)
  • $ asserts position at the end of a line

In plain english:

  • The entire search is case-insensitive.
  • The line may or may not start with the substring 'class.'. Ignore this substring.
  • First Capturing Group (group(1))
    • If there are one or more '.' characters, match the remaining characters before the first '.' character.
    • If there is no '.' character, match the remaining characters before the ':' character.
  • Match the first '.' character if one is present. Ignore this substring.
  • Second Capturing Group (group(2))
    • If there is no '.' character, match nothing.
    • If there is one '.' character, match everything between it and the ':' character.
    • If there are two '.' characters, match everything between the first and second '.' character.
  • Match the second '.' character if one is present. Ignore this substring.
  • Third Capturing Group (group(3))
    • If there are fewer than two '.' characters, match nothing.
    • If there are two '.' characters, match everything between the second '.' character and the ':' character.
  • Match ': column '. This substring must be present. Ignore it.
  • Fourth Capturing Group (group(4))
    • Match all digits. There must be at least one.
  • Match ', line '. This substring must be present. Ignore it.
  • Fifth Capturing Group (group(5))
    • Match all digits. There must be at least one.

With that expression figured out, here's a class implementation that works for me in the scenarios I have tested:

public class Logger
{
    public static Matcher generateMatcher(String firstLine)
    {
        return Pattern.compile(
            '(?i)^(?:class\\.)?([^.]+)\\.?([^\\.\\:]+)?[\\.\\:]?([^\\.\\:]*): line (\\d+), column (\\d+)$'
        ).matcher(firstLine);
    }

    public final String className, methodName;
    public final Integer line, column;
    public Logger()
    {
        Matcher m = generateMatcher(
            new DmlException()
                .getStackTraceString()
                .substringAfter('\n')
                .substringBefore('\n')
        );
        if (m.find())
        {
            if (String.isBlank(m.group(3)))
            {
                className = m.group(1);
                methodName = prettifyMethodName(m.group(2));
            }
            else
            {
                className = m.group(1) + '.' + m.group(2);
                methodName = prettifyMethodName(m.group(3));
            }
            line = Integer.valueOf(m.group(4));
            column = Integer.valueOf(m.group(5));
        }
    }
    String prettifyMethodName(String name)
    {
        return (name == null) ? null :
            name.replace('<init>', '(constructor) ')
                .replace('__sfdc_', '(getter/setter) ');
    }
}

The prettification is obviously an unnecessary step. For instance, with it removed, I get the following yields:

public class Demo
{
    public Logger l;
    public Demo()
    { // top-level class constructor
        l = new Logger(); // Logger:[className=Demo, column=1, line=6, methodName=<init>]
    }
    public static Logger foo()
    { // top-level class method
        return new Logger(); // Logger:[className=Demo, column=1, line=10, methodName=foo]
    }
    public class Sub
    {
        public Logger l;
        public Sub()
        { // inner class constructor
            l = new Logger(); // Logger:[className=Demo.Sub, column=1, line=17, methodName=<init>]
        }
        public Logger bar()
        { // inner class method
            return new Logger(); // Logger:[className=Demo.Sub, column=1, line=21, methodName=bar]
        }
    }
    public static String someProperty
    {
        get
        { // property getter
            system.debug(new Logger()); // Logger:[className=Demo, column=1, line=33, methodName=__sfdc_someProperty]
            return someProperty;
        }
        set
        { // property setter
            system.debug(new Logger()); // Logger:[className=Demo, column=1, line=28, methodName=__sfdc_someProperty]
            someProperty = value;
        }
    }
}
  • This is similar to the design I came up with a few years ago. The only caveat is that exceptions are "expensive" (~50ms per throw/catch), so this method has to be used relatively sparingly. – sfdcfox Jun 23 '17 at 19:47
  • @sfdcfox Reread the first sentence. :) They grab the stack trace at construction time, not throw time. – Adrian Larson Jun 23 '17 at 19:48
  • Hmm. Kind of surprised that works. Looks like I need to update my library :D – sfdcfox Jun 23 '17 at 19:51
  • 2
    Epic stuff. I will be bookmarking this. – Caspar Harmer Jun 24 '17 at 1:33
  • Very nice, Adrian. I wanted to do exactly the same today. I owe you one. Could I suggest to add a reference to this answer over here: salesforce.stackexchange.com/questions/153835/… – Christian Szandor Knapp Feb 17 '18 at 16:54

Not the answer you're looking for? Browse other questions tagged or ask your own question.