8

I wish to include the call stack (e.g. the methods that called me) in a log4net message. Is there a standard way of doing this?

(I know this will be slow, but I only need to do it on some errors)

Peter Lillevold
  • 33,668
  • 7
  • 97
  • 131
Ian Ringrose
  • 51,220
  • 55
  • 213
  • 317

4 Answers4

12

Yes - you can get this stack information using the following patterns in a pattern layout:

%type %file %line %method %location %class

See the this documentation on the PatternLayout for more information.

Edit in response to Ian's comment below: I don't think log4net can be configured to write out the whole stack.

You can always fall back on writing it out for yourself, using something like new StackTrace().ToString(), but I'm guessing the reason you ask is you want this to be configurable in the logging configuration.

I'll have a deeper look, but my gut feeling is there is no way to configure this, and that you'd end up having to implement your own Layout class.

Edit++ OK - here is a custom pattern layout class that derives from PatternLayout but adds in a layout %stack.

This code is a bit rough - illustrative only - not production ready! (for example, you may not have security permission to access the stack you are trying to print)

public class CustomPatternLayout : PatternLayout
{
    public CustomPatternLayout()
    {
        this.AddConverter("stack", typeof(StackTraceConverter));
    }
}

public class StackTraceConverter : PatternLayoutConverter
{
    protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
    {
        var stack = new StackTrace();

        var frames = stack.GetFrames();
        for (var i = 0; i < frames.Length; i++ )
        {
            var frame = frames[i];

            // if the stack frame corresponds to still being inside the log4net assembly, skip it.
            if (frame.GetMethod().DeclaringType.Assembly != typeof(LogManager).Assembly)
            {
                writer.WriteLine("{0}.{1} line {2}",
                    frame.GetMethod().DeclaringType.FullName,
                    frame.GetMethod().Name, 
                    frame.GetFileLineNumber());
            }
        }
    }
}

You can then configure this with the following pattern configuration (note %stack at the end of the layout):

  <layout type="ScratchPad.CustomPatternLayout,ScratchPad">
    <conversionPattern value="%date %-5level %message%newline %type %file %line %method %location %class %stack" />
  </layout>
David Gardiner
  • 16,892
  • 20
  • 80
  • 117
Rob Levine
  • 40,328
  • 13
  • 85
  • 111
  • when I raed that page, I took %location to only meen the fully qualified name of the CALLING method, not the full call stack – Ian Ringrose Dec 15 '09 at 09:48
  • I wish to include the callstack in some messages but not others by default. (E.g. a log to Ilog.Error() to include the callstack would be spot on.) – Ian Ringrose Dec 15 '09 at 09:50
  • You don't need to use a loop to write out the call stack. Take a look at the documentation on StackFrame.ToString: http://msdn.microsoft.com/en-us/library/system.diagnostics.stackframe.tostring.aspx – CodeMonkeyKing Jan 01 '10 at 23:52
  • @CodeMonkeyKing - not sure I follow your suggestion. StackFrame.ToString() only outputs the current frame, not the the whole trace up to the current frame. In any case, as mentioned in the code comment, I still need to loop through all frames in the trace in order to "push past" those frames which are internal to log4net. Have I misunderstood your suggestion? – Rob Levine Jan 02 '10 at 11:52
  • Rob - I like your example. did you ever get some version of it to work? What I would like to see would be a patternlayout like Method, but which can take an argument, an interger specifying how far up the stack to walk. It would ouput the calling method and its nth caller, as in: Main.SomeMethod.SomeMethodWhichCalledTheLogger – Daniel Williams Jul 12 '11 at 20:16
  • @Daniel - yes - the above example is working code - I tested it before I posted it. With regard to having an argument to specify how far up the stack you want to go: I think it probably *could* be done, but I suspect you'd have to write some custom code to parse the layout pattern, so you could have something like: meaning "get me 10 lines of the stack". This is beyond the scope of what I was answering above, but feel free to ask it as another question referencing this one. – Rob Levine Jul 13 '11 at 09:46
  • Is this the namespace or something? ScratchPad.CustomPatternLayout,ScratchPad – James Braz Feb 06 '20 at 21:44
  • @JamesBraz - yes - that's right. I should have clarified that. When I created this as a sample to test it, I created it in a project (assembly) named "Scratchpad". – Rob Levine Feb 10 '20 at 07:53
4

Robs answer was the best i found, i decided to extend it a little to print the full stack trace only for exceptions if it helps anyone else.

public class StackTraceConverter : PatternLayoutConverter
{
    private static readonly Assembly _assembly = typeof (PatternLayoutConverter).Assembly;

    public StackTraceConverter()
    {
        base.IgnoresException = false;
    }

    protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
    {
        var ex = loggingEvent.ExceptionObject;
        if (ex == null)
            return;
        writer.WriteLine(ex.ToString());

        bool displayFilenames = true;   // we'll try, but demand may fail
        var stack = new StackTrace(displayFilenames);
        int skip = 0;
        for (var i = 0; i < stack.FrameCount; i++)
        {
            var sf = stack.GetFrame(i);
            var mb = sf.GetMethod();
            if (mb != null)
            {
                var t = mb.DeclaringType;
                if (t.Assembly != _assembly)
                {
                    //this skips the current method and the method catching the exception
                    if (skip < 2)
                    {
                        skip++;
                        continue;
                    }
                    writer.Write("   at ");

                    // if there is a type (non global method) print it
                    if (t != null)
                    {
                        writer.Write(t.FullName.Replace('+', '.'));
                        writer.Write(".");
                    }
                    writer.Write(mb.Name);

                    // deal with the generic portion of the method
                    if (mb is MethodInfo && mb.IsGenericMethod)
                    {
                        Type[] typars = ((MethodInfo) mb).GetGenericArguments();
                        writer.Write("[");
                        int k = 0;
                        bool fFirstTyParam = true;
                        while (k < typars.Length)
                        {
                            if (fFirstTyParam == false)
                                writer.Write(",");
                            else
                                fFirstTyParam = false;

                            writer.Write(typars[k].Name);
                            k++;
                        }
                        writer.Write("]");
                    }

                    // arguments printing
                    writer.Write("(");
                    ParameterInfo[] pi = mb.GetParameters();
                    bool fFirstParam = true;
                    for (int j = 0; j < pi.Length; j++)
                    {
                        if (fFirstParam == false)
                            writer.Write(", ");
                        else
                            fFirstParam = false;

                        String typeName = "<UnknownType>";
                        if (pi[j].ParameterType != null)
                            typeName = pi[j].ParameterType.Name;
                        writer.Write(typeName + " " + pi[j].Name);
                    }
                    writer.Write(")");

                    // source location printing
                    if (displayFilenames && (sf.GetILOffset() != -1))
                    {
                        // If we don't have a PDB or PDB-reading is disabled for the module,
                        // then the file name will be null.
                        String fileName = null;

                        // Getting the filename from a StackFrame is a privileged operation - we won't want
                        // to disclose full path names to arbitrarily untrusted code.  Rather than just omit
                        // this we could probably trim to just the filename so it's still mostly usefull.
                        try
                        {
                            fileName = sf.GetFileName();
                        }
                        catch (SecurityException)
                        {
                            // If the demand for displaying filenames fails, then it won't
                            // succeed later in the loop.  Avoid repeated exceptions by not trying again.
                            displayFilenames = false;
                        }

                        if (fileName != null)
                        {
                            // tack on " in c:\tmp\MyFile.cs:line 5"
                            writer.Write(" in {0}:line {1}", fileName, sf.GetFileLineNumber());
                        }
                    }
                    writer.WriteLine();
                }
            }
        }
    }
}
park896
  • 455
  • 2
  • 6
  • 9
  • This worked well for me - thanks! Note that (perhaps obviously) you have to include the exception in the log call, e.g. `log.Fatal(message, ex)` – Geoff Aug 15 '14 at 15:20
3

If you are catching exceptions, then just log Exception.ToString(), as that will contain the full stack trace, and will be available as the normal log message.

si618
  • 16,580
  • 12
  • 67
  • 84
1

It's not the cleanest approach, but a quick way to achieve this is to create an artificial exception that contains the desired stack trace:

public class ArtificialStackTraceException : Exception
{
    public ArtificialStackTraceException(
        bool shouldIncludeFileInfo = true,
        string message = "Artificial exception used to generate a stack trace."
    ) : base(message)
    {
        var stackTrace = new System.Diagnostics.StackTrace(shouldIncludeFileInfo);
        StackTrace = stackTrace.ToString();
    }

    public override string StackTrace { get; }

    public override string ToString()
    {
        return $"{nameof(ArtificialStackTraceException)}:{Environment.NewLine}{StackTrace}";
    }
}

(full source)

Then you could use it like so:

Log.Error("It went kaboom", new ArtificialStackTraceException());
Michael Kropat
  • 14,557
  • 12
  • 70
  • 91