Logging and Tracing in C# - Making Your Code Observable

Vaibhav • September 10, 2025

In the previous article, we explored Visual Studio’s powerful debugging tools - from breakpoints and data tips to performance profiling and async call stacks. But debugging is reactive: it helps you fix problems after they happen. What if you could observe your application’s behavior as it runs, even in production? That’s where logging and tracing come in. These techniques let you record what your code is doing, when, and why - without stopping execution.

In this article, we’ll explore how to use logging and tracing in C# to monitor your application, diagnose issues, and build systems that are easier to support and maintain. We’ll focus on the built-in tools available in .NET, and show how to use them effectively in real-world scenarios.

What Is Logging?

Logging is the process of writing messages from your application to a persistent output - such as a file, console, or log server. These messages can describe errors, warnings, informational events, or detailed debug data. Logging helps you understand what your application is doing, especially when something goes wrong.

In .NET, logging is typically done using the ILogger interface, which is part of the Microsoft.Extensions.Logging namespace. This abstraction lets you plug in different logging providers - such as console, file, or cloud-based systems - without changing your code.

Basic Logging with ILogger

Here’s a simple example of logging in a console application using the built-in console logger:

using Microsoft.Extensions.Logging;

class Program
{
    static void Main()
    {
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddConsole();
        });

        ILogger logger = loggerFactory.CreateLogger<Program>();

        logger.LogInformation("Application started.");
        logger.LogWarning("Low disk space.");
        logger.LogError("Unhandled exception occurred.");
    }
}

Let’s break this down:

  • LoggerFactory.Create sets up the logging system.
  • AddConsole tells it to write logs to the console.
  • CreateLogger<Program> creates a logger scoped to the Program class.
  • LogInformation, LogWarning, and LogError write messages at different severity levels.

Logging levels help you filter messages. You can configure your logger to show only warnings and errors in production, but include debug messages during development.

Structured Logging

Instead of writing plain text, you can log structured data using message templates. This makes logs easier to search and analyze:

logger.LogInformation("User {UserId} logged in at {Time}", userId, DateTime.Now);

This logs a message like:

User 42 logged in at 9/10/2025 10:15:00 AM

Structured logging is especially useful when using log aggregation tools like Seq, ELK, or Azure Monitor, which can parse and filter logs by fields.

Logging Exceptions

When an exception occurs, you should log it with full context:

try
{
    ProcessData();
}
catch (Exception ex)
{
    logger.LogError(ex, "Error while processing data.");
}

This logs both the message and the stack trace, helping you diagnose the issue later. You can also include additional context:

logger.LogError(ex, "Error processing user {UserId}", userId);

This makes your logs more actionable and easier to correlate with user activity.

What Is Tracing?

Tracing is similar to logging, but it’s focused on recording the flow of execution - especially across components, threads, or services. Tracing is often used in performance diagnostics, distributed systems, and telemetry.

In .NET, tracing is supported by the System.Diagnostics.Trace class. It lets you write messages to listeners - such as log files, event logs, or custom outputs.

Basic Tracing with Trace

Here’s a simple example:

using System.Diagnostics;

class Program
{
    static void Main()
    {
        Trace.Listeners.Add(new TextWriterTraceListener("trace.log"));
        Trace.AutoFlush = true;

        Trace.WriteLine("Starting application...");
        Trace.TraceWarning("Low memory warning.");
        Trace.TraceError("Unhandled exception.");
    }
}

This writes trace messages to a file named trace.log. You can add multiple listeners - for example, one for the console and one for a file.

Tracing is lower-level than logging. It’s useful for legacy systems or when you need fine-grained control over output.

Trace vs Debug

The System.Diagnostics namespace also includes the Debug class, which works like Trace but only in debug builds. Use Debug.WriteLine for temporary diagnostics during development:

Debug.WriteLine("Value of x: " + x);

These messages won’t appear in release builds, making them safe for exploratory debugging.

Using TraceSource for Advanced Tracing

For more control, use TraceSource. It lets you categorize and filter trace messages by source:

TraceSource ts = new TraceSource("MyApp");

ts.TraceInformation("App started.");
ts.TraceEvent(TraceEventType.Warning, 1001, "Low disk space.");
ts.TraceEvent(TraceEventType.Error, 1002, "Unhandled exception.");

You can configure TraceSource in app.config to control output without changing code. This is useful for enterprise applications with complex logging requirements.

Logging in ASP.NET Core

In ASP.NET Core, logging is built into the framework. You can inject ILogger<T> into controllers, services, or middleware:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

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

    public IActionResult Index()
    {
        _logger.LogInformation("Home page visited.");
        return View();
    }
}

The logging system is configured in Program.cs or appsettings.json, and supports multiple providers - including console, debug, event log, and third-party systems.

Best Practices for Logging and Tracing

Logging and tracing are powerful tools - but they can also create noise or performance issues if misused. Here are some guidelines:

  • Use appropriate log levels: Information for general events, Warning for potential issues, Error for failures.
  • Include context: user IDs, request IDs, timestamps, and method names help you trace issues.
  • Avoid logging sensitive data: passwords, tokens, and personal information should never appear in logs.
  • Use structured logging: message templates make logs easier to parse and search.
  • Log exceptions with stack traces: don’t just log the message - include the full error.
  • Configure output: use appsettings.json or environment variables to control log levels and destinations.
  • Monitor performance: excessive logging can slow down your app - especially in tight loops or high-traffic endpoints.

Treat logs as part of your product. Design them for clarity, consistency, and usefulness. Good logs reduce support time and improve user experience.

Summary

Logging and tracing are essential tools for building observable applications. They help you understand what your code is doing, diagnose issues, and support users effectively. In this article, you’ve learned how to use ILogger for structured logging, Trace for low-level diagnostics, and TraceSource for advanced scenarios. You’ve seen how to log exceptions, use message templates, and configure output in different environments.

With thoughtful logging and tracing, you’ll spend less time debugging and more time building. In the next article, we’ll explore Unit Testing Introduction - how to write tests that verify your code works correctly, prevent regressions, and support refactoring with confidence.