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 theProgram
class.LogInformation
,LogWarning
, andLogError
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.