Introduction

It’s 3 AM. A critical alert wakes you up: a service in production is failing. You open the logs, desperately searching for a clue, only to find a sea of generic messages like "An error occurred during processing". Frustration mounts. Where in the thousands of lines of code did this happen? We’ve all been there. This moment is where the true value of well-structured logging becomes painfully clear.

Writing good logs is an act of empathy—empathy for your future self and for the colleagues who will have to debug your code under pressure. Fortunately, the .NET platform provides powerful, yet simple, tools to turn our logs from ambiguous notes into precise, actionable information. In this article, we’ll explore how to use the [CallerFilePath] and [CallerLineNumber] attributes to enrich our logs, making debugging faster, more efficient, and much less stressful.

A comic strip illustrating the typical developer experience when debugging with vague logs versus the relief of having well-structured logs with caller information.

The Problem with Vague Logs

In complex systems, especially those based on microservices, identifying the exact source of an error is the first and most crucial step. A log message without context is like a signpost with no direction.

A Common (and Inefficient) Log Example:

public class PaymentService
{
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(ILogger<PaymentService> logger)
    {
        _logger = logger;
    }

    public void ProcessPayment(decimal amount)
    {
        try
        {
            // Complex logic here...
            if (amount <= 0)
            {
                throw new ArgumentException("Invalid payment amount.");
            }
            // More logic...
        }
        catch (Exception ex)
        {
            // This log is not very helpful. Where did it come from?
            _logger.LogError(ex, "An error occurred while processing payment.");
        }
    }
}

When this log appears in a centralized tool like Seq, Datadog, or Kibana, you know what happened (an error), but you don’t know the exact line of code that triggered it. You have to rely on the stack trace, which might be complex or even missing in some asynchronous scenarios.

Introducing Caller Information Attributes

C# provides a set of attributes, known as Caller Information attributes, that instruct the compiler to inject information about the calling method’s source code. The best part? This is done at compile-time, meaning there’s no performance penalty from reflection at runtime.

The three main attributes are:

  • [CallerFilePath]: Provides the full path of the source file that contains the caller.
  • [CallerLineNumber]: Provides the line number in the source file at which the method is called.
  • [CallerMemberName]: Provides the method or property name of the caller.

These attributes allow us to automatically capture the exact origin of a log message without any manual effort.

Practical Implementation: Creating an ILogger Wrapper

To avoid manually adding these parameters to every log call, the best practice is to create a simple extension method for the ILogger interface. This centralizes our logging logic and keeps our application code clean.

Let’s create a static class for our logger extensions:

// LoggerExtensions.cs
using System.Runtime.CompilerServices;

public static class LoggerExtensions
{
    public static void LogErrorWithCallerInfo<T>(
        this ILogger<T> logger,
        Exception exception,
        string message,
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
    {
        // We can format the message to include the file path and line number
        // Or, even better, use structured logging to add them as properties
        logger.LogError(exception, "{Message} [at {SourceFilePath}:{SourceLineNumber}]", 
                       message, sourceFilePath, sourceLineNumber);
    }

    // You can create similar extensions for LogWarning, LogInformation, etc.
    public static void LogInformationWithCallerInfo<T>(
        this ILogger<T> logger,
        string message,
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
    {
        logger.LogInformation("{Message} [at {SourceFilePath}:{SourceLineNumber}]",
                              message, sourceFilePath, sourceLineNumber);
    }
}

Now, we can refactor our PaymentService to use this new extension method.

The “After” Example: Clear and Actionable Logging

public class PaymentService
{
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(ILogger<PaymentService> logger)
    {
        _logger = logger;
    }

    public void ProcessPayment(decimal amount)
    {
        try
        {
            // Complex logic here...
            if (amount <= 0)
            {
                throw new ArgumentException("Invalid payment amount.");
            }
            // More logic...
        }
        catch (Exception ex)
        {
            // Now, the call is just as simple, but the result is much more powerful.
            _logger.LogErrorWithCallerInfo(ex, "An error occurred while processing payment.");
        }
    }
}

The Resulting Log Output

When the LogErrorWithCallerInfo method is called, the log output will be dramatically enriched. If you are using a structured logging provider like Serilog, the output might look like this in JSON format:

{
  "Timestamp": "2025-10-07T09:15:00.123Z",
  "Level": "Error",
  "MessageTemplate": "{Message} [at {SourceFilePath}:{SourceLineNumber}]",
  "RenderedMessage": "An error occurred while processing payment. [at C:\\Users\\Ivaldo\\Projects\\Veetz\\PaymentService.cs:25]",
  "Properties": {
    "Message": "An error occurred while processing payment.",
    "SourceFilePath": "C:\\Users\\Ivaldo\\Projects\\Veetz\\PaymentService.cs",
    "SourceLineNumber": 25,
    "Exception": "System.ArgumentException: Invalid payment amount."
  }
}

Instantly, you can see the exact file and line number (PaymentService.cs:25) where the log was triggered. The 3 AM debugging session just got 90% easier.

Benefits Beyond the Code

Adopting this practice offers benefits that extend from the technical to the strategic level:

  1. Pinpoint Accuracy: It eliminates guesswork. Developers can navigate directly to the source of the problem, dramatically reducing the mean time to resolution (MTTR). For directors, this means less downtime and a more reliable product.

  2. Stress Reduction and Empowerment: Providing developers with precise tools to solve critical issues is empowering. It reduces the stress associated with production failures and allows them to focus on solutions rather than on hunting for clues.

  3. A Culture of Quality and Empathy: When you write logs this way, you are thinking about the person who will read them. This fosters a culture where developers build tools and code that help each other succeed, which is the foundation of a high-performing team.

  4. No Performance Cost: Since this is a compile-time feature, you gain immense diagnostic power without sacrificing runtime performance, making it a safe choice even for high-performance applications.

Conclusion

Technology is at its best when it helps us be better, more efficient, and more empathetic professionals. C#’s Caller Information attributes are a perfect example of this. They are not just a clever language feature; they are a tool for building more maintainable, resilient, and developer-friendly systems.

By taking a few moments to set up a simple logging extension, you are leaving a trail of clear, actionable information. You are showing empathy for your team and your future self. The next time an alert fires in the middle of the night, that small act of empathy will make all the difference.