Updated October 2026: the original 2013 post configured log4net in web.config and Global.asax for .NET Framework. This version covers log4net 3.x on .NET 8 and .NET 10, including ASP.NET Core. The configuration below was run and verified to write the files shown.
What Is log4net?
log4net is the Apache Software Foundation's logging library for .NET, a port of Java's log4j. You log messages and exceptions at a level (DEBUG, INFO, WARN, ERROR, FATAL), and appenders defined in an XML file decide where they go: console, rolling files, a database, the event log, and more. It is still actively maintained, and many existing apps and libraries use it.
Quick Start (Console App)
dotnet add package log4net- Add a
log4net.configfile (below) and copy it to the output folder. - Load it at startup and get a logger per class.
// dotnet add package log4net
using System.Reflection;
using log4net;
using log4net.Config;
// Load log4net.config (set "Copy to Output Directory" = "Copy if newer").
var repository = LogManager.GetRepository(Assembly.GetEntryAssembly()!);
XmlConfigurator.ConfigureAndWatch(repository, new FileInfo("log4net.config"));
ILog log = LogManager.GetLogger(typeof(Program));
log.Info("Application started");
try
{
throw new InvalidOperationException("Something went wrong");
}
catch (Exception ex)
{
log.Error("Failed to process order 1042", ex); // the exception + stack trace are logged
}
LogManager.Shutdown(); // flush buffered appenders before exit
Make sure the config file is copied next to the executable:
<ItemGroup>
<None Update="log4net.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
A Complete log4net.config Example
This configuration writes everything at INFO and above to a rolling file (a new file each day, and whenever it exceeds 10 MB, keeping 10 backups), sends only warnings and errors to a separate file, echoes to the console, and quiets the noisy Microsoft.* loggers:
<?xml version="1.0" encoding="utf-8"?>
<log4net>
<!-- Daily + size-based rolling file: logs/app.log, logs/app.log.20261029, ... -->
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="logs/app.log" />
<appendToFile value="true" />
<rollingStyle value="Composite" />
<datePattern value=".yyyyMMdd" />
<maximumFileSize value="10MB" />
<maxSizeRollBackups value="10" />
<staticLogFileName value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%utcdate{yyyy-MM-dd HH:mm:ss.fff} [%thread] %-5level %logger - %message%newline%exception" />
</layout>
</appender>
<!-- Only warnings and errors go to a separate file. -->
<appender name="ErrorFile" type="log4net.Appender.FileAppender">
<file value="logs/errors.log" />
<appendToFile value="true" />
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="WARN" />
<levelMax value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%utcdate %-5level %logger - %message%newline%exception" />
</layout>
</appender>
<appender name="Console" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="RollingFile" />
<appender-ref ref="ErrorFile" />
<appender-ref ref="Console" />
</root>
<!-- Quieter output from a noisy namespace -->
<logger name="Microsoft">
<level value="WARN" />
</logger>
</log4net>
Running the console example above with this file produces logs/app.log with both messages and logs/errors.log with only the error and its stack trace. Relative paths are resolved from the application's base directory (the folder containing the exe), not the current working directory.
Configuration notes
- rollingStyle Composite rolls by date and size. Use
DateorSizefor just one. - MinimalLock lets other processes (and log viewers) open the file between writes, at a small cost in speed. Leave it out if only one process writes the file.
- LevelRangeFilter with
levelMin/levelMaxsends a range of levels to one appender. Filters are per appender; the logger'slevelapplies first. - %utcdate writes timestamps in UTC, which avoids confusion when servers and users are in different time zones.
log4net in ASP.NET Core
In ASP.NET Core you normally log through ILogger<T>. The Microsoft.Extensions.Logging.Log4Net.AspNetCore package routes those calls to log4net, so the same log4net.config works:
// dotnet add package Microsoft.Extensions.Logging.Log4Net.AspNetCore
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // optional: log4net only
builder.Logging.AddLog4Net("log4net.config");
var app = builder.Build();
app.MapGet("/", (ILogger<Program> logger) =>
{
// Standard ILogger<T> calls are routed to your log4net appenders.
logger.LogInformation("Home page requested at {Utc}", DateTime.UtcNow);
return "Hello";
});
app.Run();
Your code stays on the standard ILogger API, and log4net handles the output. Remove ClearProviders() if you also want the default console and debug output.
Logging to SQL Server
The AdoNetAppender writes each event as a row. Create a table:
CREATE TABLE dbo.Log (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
[Date] DATETIME2 NOT NULL,
[Thread] NVARCHAR(255) NOT NULL,
[Level] NVARCHAR(50) NOT NULL,
[Logger] NVARCHAR(255) NOT NULL,
[Message] NVARCHAR(4000) NOT NULL,
[Exception] NVARCHAR(MAX) NULL
);
Then add an appender to log4net.config and reference it from <root> with <appender-ref ref="Database" />. On .NET 8+, use Microsoft.Data.SqlClient as the connection type:
<!-- Optional: also log to SQL Server (dotnet add package Microsoft.Data.SqlClient) -->
<appender name="Database" type="log4net.Appender.AdoNetAppender">
<bufferSize value="1" /> <!-- write each event immediately; raise to batch for performance -->
<connectionType value="Microsoft.Data.SqlClient.SqlConnection, Microsoft.Data.SqlClient" />
<connectionString value="Server=.\SQLEXPRESS;Database=MyApp;Integrated Security=true;TrustServerCertificate=true" />
<commandText value="INSERT INTO dbo.Log ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" />
<parameter>
<parameterName value="@log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawUtcTimeStampLayout" />
</parameter>
<parameter>
<parameterName value="@thread" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout"><conversionPattern value="%thread" /></layout>
</parameter>
<parameter>
<parameterName value="@log_level" />
<dbType value="String" />
<size value="50" />
<layout type="log4net.Layout.PatternLayout"><conversionPattern value="%level" /></layout>
</parameter>
<parameter>
<parameterName value="@logger" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout"><conversionPattern value="%logger" /></layout>
</parameter>
<parameter>
<parameterName value="@message" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout"><conversionPattern value="%message" /></layout>
</parameter>
<parameter>
<parameterName value="@exception" />
<dbType value="String" />
<size value="-1" />
<layout type="log4net.Layout.ExceptionLayout" />
</parameter>
</appender>
A bufferSize of 1 writes every message immediately, which is less confusing while you set things up (with a larger buffer, nothing appears until the buffer fills). Increase it for busy apps, and call LogManager.Shutdown() on exit so buffered events are flushed.
Common Problems
- Nothing is logged: the config file isn't in the output folder, or
XmlConfiguratorwas never called. Adddebug="true"to the<log4net>element and log4net prints its own setup messages to the console. - Database logging is silent: with
debug="true"you will see connection errors. Also check the buffer size. - Access denied writing the log file: on IIS the app pool identity needs Modify permission on the log folder.
- Third-party libraries: a referenced library that also uses log4net (as many older SDKs do) writes to your appenders too, as long as the logger level allows it. Use a
<logger name="TheirNamespace">element to tune it.
log4net vs. Serilog vs. NLog
For an existing app that already uses log4net, there is no need to switch: it works on modern .NET and plugs into ILogger. For new projects, Serilog and NLog are more common today, mostly because of first-class structured logging (named properties you can query, not just text) and richer sinks. Whichever you choose, log through ILogger<T> so you can change the backend later.
Frequently Asked Questions
Does log4net work with .NET 8 and .NET 10?
Yes. log4net 3.x targets .NET Standard 2.0 (plus .NET Framework 4.6.2), so it runs on .NET 8 and .NET 10, on Windows and Linux. The example config above was tested with log4net 3.5 on .NET 10.
How do I configure log4net without web.config?
Put the <log4net> section in its own log4net.config file, copy it to the output folder, and load it with XmlConfigurator.ConfigureAndWatch (console and services) or builder.Logging.AddLog4Net("log4net.config") (ASP.NET Core).
How do I log only errors to a separate file?
Add a second appender with a LevelRangeFilter whose levelMin is WARN or ERROR, and reference both appenders from <root>, as in the example config.
What does maxSizeRollBackups do?
It is the number of rolled-over files to keep (10 in the example). Older files are deleted automatically.
Related: scheduling jobs in a Quartz.NET Windows Service.

