On this page

Skip to content

A Brief Discussion on .NET Default Logger and Optimization Techniques

Log Levels

The following table defines the various levels and recommended usage scenarios based on Logging in C# and .NET. The None value is the highest, primarily used to completely disable all logs in filter settings.

LevelValueMethodDescription
Trace0LogTraceContains the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should not be enabled in production environments.
Debug1LogDebugUsed for debugging and development. Due to high volume, use with caution in production environments.
Information2LogInformationTracks general application flow. May have long-term value.
Warning3LogWarningUsed for handling abnormal or unexpected events. Usually contains errors or conditions that do not cause the application to fail.
Error4LogErrorUnhandled errors and exceptions. These messages indicate a failure in the current operation or request, rather than a failure of the entire application.
Critical5LogCriticalFailures that require immediate attention. Examples: data loss, insufficient disk space.
None6N/ASpecifies that no messages should be written.

Basic Injection Configuration

WebApplication.CreateBuilder() automatically configures the default Logger upon creation. By examining the .NET source code, specifically the AddDefaultServices() method in HostingHostBuilderExtensions.cs, we can see that the system automatically sets up basic Logger Providers:

csharp
// From .NET source code
services.AddLogging(logging => {
    bool isWindows =
#if NET
        OperatingSystem.IsWindows();
#elif NETFRAMEWORK
        Environment.OSVersion.Platform == PlatformID.Win32NT;
#else
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif

    // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
    // the defaults be overridden by the configuration.
    if (isWindows) {
        // Default the EventLogLoggerProvider to warning or above
        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
    }

    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
#if NET
    if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi())
#endif
    {
        logging.AddConsole();
    }
    logging.AddDebug();
    logging.AddEventSourceLogger();

    if (isWindows) {
        // Add the EventLogLoggerProvider on windows machines
        logging.AddEventLog();
    }

    logging.Configure(options => {
        options.ActivityTrackingOptions =
            ActivityTrackingOptions.SpanId |
            ActivityTrackingOptions.TraceId |
            ActivityTrackingOptions.ParentId;
    });
});

This means that by default, WebApplication.CreateBuilder() has already configured the following Logger Providers for us:

  • Reads settings from the "Logging" section of the Configuration.
  • Console Logger (outputs to the console)
  • Debug Logger (outputs to the debugger)
  • EventSource Logger (used for EventSource tracing)

In practice, we can customize Logger settings as follows:

csharp
// CreateBuilder itself has default settings
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// You can adjust added LoggerProviders via DI
builder.Services.AddLogging(logging => {
    logging.ClearProviders(); // Clear default providers
    logging.AddConsole();     // Add Console Provider
    logging.AddDebug();       // Add Debug Provider
});

// Or simplify it as follows
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

How to Inject Logger in Code

In ASP.NET Core, Logger is typically used via dependency injection. Below is a simplified example:

csharp
namespace LoggerTest.Controllers {
    [ApiController]
    [Route("api/[controller]")]
    public class UserController : ControllerBase {
        private readonly ILogger<UserController> logger;

        public UserController(ILogger<UserController> logger) {
            this.logger = logger;
            this.logger.LogInformation("UserController initialized");
        }
    }
}

Controlling Log Levels Using Appsettings.json

We can configure log levels for different components using the appsettings.json file. Here is an example of a specific scenario using multiple types of Loggers:

csharp
namespace LoggerTest.Controllers {
    [ApiController]
    [Route("api/[controller]")]
    public class TestController : Controller {
        public TestController(ILogger<TestService1> logger1, ILogger<TestService2> logger2) {
            logger1.LogInformation("TestController successfully initialized and injected with Service1's Logger.");
            logger2.LogInformation("TestController successfully initialized and injected with Service2's Logger.");
        }
    }
}

namespace LoggerTest.Services {
    public class TestService1 {
        public TestService1() {
        }

        public void TestMethod() {
            // Do something
        }
    }

    public class TestService2 {
        public TestService2() {
        }

        public void TestMethod() {
            // Do something
        }
    }
}

Combined with the following appsettings.json configuration:

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "LoggerTest.Services.TestService1": "Warning"
    },
    "Debug": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.Hosting": "Trace"
      }
    },
    "EventSource": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "AllowedHosts": "*"
}

In this configuration file:

  • The outer "LogLevel" setting applies to all Logger Providers.
  • Settings in the "Debug" and "EventSource" sections apply only to the specified Providers.
  • "Default" indicates the default minimum log level.
  • Specific namespaces like "Microsoft.AspNetCore" can have their own log levels.

After running the program with the above settings, you will find that only Service2's logs are recorded, because Service1's log level is set to Warning.

bash
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7109
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5164
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Programming\Projects\LoggerTest\LoggerTest
info: LoggerTest.Services.TestService2[0]
      TestController successfully initialized and injected with Service2's Logger.

Using Wildcards for Configuration

If you want to apply the same settings to multiple namespaces, you can use the wildcard *. For example, "*.Services": "Warning" will set the log level to Warning for all namespaces ending in .Services.

Advanced Filtering in Code

If more granular control is needed, you can use Filters in your code:

csharp
// Filter by namespace
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);

// Filter by provider type
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Warning);
builder.Logging.AddFilter<DebugLoggerProvider>("Microsoft.AspNetCore.Mvc", LogLevel.Debug);

// Use wildcards
builder.Logging.AddFilter("Microsoft.*", LogLevel.Warning);
builder.Logging.AddFilter("*.Repository", LogLevel.Debug);

// For a specific class logger
builder.Logging.AddFilter(typeof(Program).FullName, LogLevel.Debug);

// Use more granular control - using functional filters
builder.Logging.AddFilter((provider, category, logLevel) => {
    // Show more detailed logs on weekdays
    if (DateTime.Now.DayOfWeek != DayOfWeek.Saturday
        && DateTime.Now.DayOfWeek != DayOfWeek.Sunday) {
        return logLevel >= LogLevel.Debug;
    }

    // Only show Warning and above on weekends
    return logLevel >= LogLevel.Warning;
});

Improving Performance with Logger.IsEnabled

In situations where expensive operations are required to generate log messages, we can check if a specific log level is enabled first to avoid unnecessary computation:

csharp
if (logger.IsEnabled(LogLevel.Information)) {
    // Potentially resource-intensive operations here, such as database queries or complex calculations
    int processedRecords = await database.GetProcessedRecordsCount();
    logger.LogInformation("System has finished data update, processed {Count} records in total.", processedRecords);
}

The benefit of this is that when the configuration file sets the log level to Warning or higher, we can completely skip these resource-intensive operations.

Advantages and Usage of Structured Logging

Key Benefits of Structured Logging

  1. Performance Advantage: The biggest advantage of structured logging is avoiding unnecessary string concatenation. The Logger only performs string composition when it actually needs to write the log; this is known as "deferred execution." When the log level is set to not record certain levels, the related string processing is never executed, thereby improving application performance.

  2. Data Integration Convenience: Generated logs can be easily parsed by tools like Elasticsearch, Kibana, and Logstash (ELK), facilitating centralized log management.

How to Use Structured Logging

csharp
// Visual Studio and .NET analyzers will warn against this via CA2254
logger.LogInformation(
    "User " + user.Id + " has logged in, department: " + user.Department
);

// Correct structured logging approach
logger.LogInformation(
    "User {UserId} has logged in, department: {Department}",
    user.Id,
    user.Department
);

Special Usage of Curly Braces

If you need to include curly brace characters {} in your log message, you need to use double curly braces to escape them. For example:

csharp
// If you want to output text containing curly braces, you need to use double curly braces
logger.LogInformation("This is a JSON example: {{\"name\": \"value\"}}");

// Output result: This is a JSON example: {"name": "value"}

// Note: XXX in {XXX} is used as a variable placeholder, related to the position of the passed parameters
// It is not mapped by parameter name, but by parameter order
logger.LogInformation("Name: {Name}, Age: {Age}", "Xiao Ming", 25);
// Output: Name: Xiao Ming, Age: 25

Using JSON Format Output

By replacing AddConsole() with AddJsonConsole(), we can obtain structured JSON output:

csharp
builder.Logging.AddConsole();
// Change to
builder.Logging.AddJsonConsole();

The generated JSON log format looks like this (formatted for readability):

json
{
    "EventId": 0,
    "LogLevel": "Information",
    "Category": "LoggerTest.Controllers.TestController",
    "Message": "User U12345 has logged in, department: R&D",
    "State": {
        "UserId": "U12345",
        "Department": "R&D",
        "{OriginalFormat}": "User {UserId} has logged in, department: {Department}"
    },
    "Timestamp": "2025-03-23T15:30:00.123Z"
}

This structured JSON format is particularly suitable for integration with log analysis systems like ELK, allowing us to search, filter, and analyze logs more effectively. This is also one of the main reasons why Serilog has gradually replaced NLog as the preferred .NET logging framework in recent years.

Change Log

  • 2025-03-23 Initial version created.