An Introduction to Windows Event Log and ETW — Putting Your Business App's Logs on the OS's Standard Mechanisms

· · CSharp, .NET, Event Log, ETW, EventSource, Log Design, Bug Investigation, Windows Development

When you develop business apps or Windows services in C#, you’re typically already outputting day-to-day logs through a homegrown file logger or a library like Serilog or NLog. So does that mean the Windows event log and ETW (Event Tracing for Windows) have become unnecessary? Not at all. These two aren’t “substitutes for file logging” — they’re records visible on a different layer, to operations staff and OS-standard tools.

This article covers when to use file logs, the event log, and ETW, how to implement each from .NET, the minimum essentials of ETW, the practicalities of collecting and investigating logs, and the pitfalls that come up often in the field.

1. The Bottom Line First

  • The three mechanisms aren’t substitutes for each other — they’re a division of labor. File logs are records for developers to trace details after the fact; the event log is a record for operations staff and OS-standard tools like Event Viewer and monitoring software to notice something’s wrong; ETW is a high-frequency trace you enable on demand for performance investigations or diagnosing hard-to-reproduce bugs. As a rule, use all three together, and vary how much information you write to each.
  • Write only “start, stop, and fatal errors” to the event log. If you route every log line to the event log as well, the anomalies operations staff actually need to see get buried. Keep the file log as detailed as ever, while narrowing down what goes to the event log.
  • Registering an event source requires administrator privileges. Since Windows Vista, EventLog.CreateEventSource can’t be called without administrator privileges. Avoid designs that try to register the source on first access at runtime — register it ahead of time in the installer instead. 1
  • ETW isn’t “always-on collection.” Even if you’ve defined events with EventSource, nothing gets recorded unless a subscriber (dotnet-trace, PerfView, or an ETW-based tool) is listening. The basic usage pattern is to specify the target provider and collect only when you’re doing performance analysis or investigating a specific bug. 2
  • Write messages in template form, not through string concatenation or interpolation. Writing something like logger.LogInformation("Order {OrderId} failed", orderId) lets you search the structured log by the OrderId placeholder name. Building the message through concatenation or interpolation loses this structural information. 3
  • Log bloat problems are often caused by how small the default size limit is. The event log’s default maximum size is a mere 512 KB. If your business app is going to use the event log on a routine basis, expand it explicitly, and decide at design time what should happen once the limit is reached (overwrite old entries, or discard new ones). 4

Here’s a decision table.

Aspect File log Event log ETW
Primary audience Developers and maintainers Operations staff, monitoring tools, OS-standard Event Viewer Performance investigators, support engineers
Rough output volume Detailed (Trace/Debug included is fine) Low (start, stop, and fatal errors only) High volume only while enabled; nothing recorded by default
Lifespan / retention Easy to retain long-term depending on rotation settings Oldest entries overwritten or discarded once the size limit is reached Session-scoped; saved as an .etl/.nettrace file once collection ends
Integration with OS-standard tools None (needs your own viewer) Viewable through Event Viewer, wevtutil, and Get-WinEvent as standard Viewed with dotnet-trace, PerfView, WPR/WPA
Privileges Only the write permission of the app’s running user Registering a source requires administrator privileges (writing itself works under a standard user once the source is registered) Starting collection often requires administrator privileges

2. The Basics of the Windows Event Log

Windows has three logs by default — Application, System, and Security — and Security is read-only. Business apps and services write to the Application log, or to their own custom log (channel). By convention, device drivers write to the System log. 5

The unit of writing is the “event source.” A source is a name that identifies your application, and you register it ahead of time with EventLog.CreateEventSource. Since Windows Vista, registering a source requires administrator privileges. That’s because confirming that the source name is unique requires searching every event log, including the Security log, and standard users don’t have access to the Security log. 1

using System.Diagnostics;

// Run this from an installer or setup process, while you still have administrator privileges
const string SourceName = "KomuraSoft.OrderService";
const string LogName = "Application";

if (!EventLog.SourceExists(SourceName))
{
    EventLog.CreateEventSource(SourceName, LogName);
}

If you try to create the source on first access at runtime, this call fails whenever the app runs as a standard user in production. The general question of when administrator privileges are actually required is covered in “When Do You Actually Need Administrator Privileges on Windows?,” and registering an event source is exactly this pattern — the safe design is to register it in the installer, and have the app itself only write to the already-registered source.

Every event carries two pieces of identifying information: a “level” and an “event ID.” The level is an EventLogEntryType (Information, Warning, Error, SuccessAudit, FailureAudit), and it drives the icon shown in Event Viewer and the default filters. The event ID is an application-defined integer that serves as a key for later searching and aggregating occurrences of the same kind of event.

3. Writing to the Event Log from .NET

There are broadly two routes for writing to the event log from .NET.

3.1 Using System.Diagnostics.EventLog Directly

This is a low-level API suited to cases where you need fine-grained control (categories, attaching binary data, and so on). On .NET (as opposed to .NET Framework), you need to reference the System.Diagnostics.EventLog NuGet package.

using System.Diagnostics;

public sealed class OrderServiceEventLog
{
    private const string SourceName = "KomuraSoft.OrderService";

    public void WriteServiceStarted()
    {
        EventLog.WriteEntry(SourceName, "OrderService started.",
            EventLogEntryType.Information, eventID: 1000);
    }

    public void WriteFatalError(Exception ex)
    {
        EventLog.WriteEntry(SourceName,
            $"A fatal error occurred and processing cannot continue. Check the file log for details. {ex.GetType().Name}: {ex.Message}",
            EventLogEntryType.Error, eventID: 1999);
    }
}

3.2 Using ILogger + AddEventLog

If you’ve already built structured logging around ILogger, using AddEventLog from the Microsoft.Extensions.Logging.EventLog package fits more naturally into your existing logging pipeline. In ASP.NET Core and Generic-Host-based Worker Services, the EventLog provider is enabled by default when running on Windows. 6

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddEventLog(settings =>
{
    settings.SourceName = "KomuraSoft.OrderService";
    settings.LogName = "Application";
    // On top of the default category filters, only route Warning and above
    // to the event log (leave the details to the file log)
    settings.Filter = (_, level) => level >= LogLevel.Warning;
});

using IHost host = builder.Build();
await host.RunAsync();

If you omit EventLogSettings, LogName defaults to "Application" and SourceName defaults to ".NET Runtime". 7 Leaving the generic source name .NET Runtime in place makes it hard to tell, in Event Viewer, whether a log entry belongs to your app or some other .NET app, so always set SourceName explicitly for your own app.

For batch processing that runs as a Windows service or through Task Scheduler, the practical balance is to write only “start,” “stop,” and “fatal error” to the event log, and leave everything else to the file log. Building and operating a service is covered in “Building and Operating Windows Services,” and troubleshooting execution via Task Scheduler is covered in “When a Task Scheduler Task Doesn’t Run, or Exits with 0x1.” In both cases, the event log is often the first thing operations staff check when something goes wrong, and whether the essentials are left there determines how fast the initial investigation goes.

4. What Is ETW? — A Tracing Foundation That Runs Through the Entire OS

Event Tracing for Windows (ETW) is a tracing mechanism that lets you instrument everything from the kernel to user-mode applications on a shared foundation. Its biggest strength is that it lets you record and analyze kernel events (disk I/O, process creation, and so on) alongside your application’s own custom events, all on the same timeline. 8

In .NET, you can define your own ETW provider by creating a class that inherits from System.Diagnostics.Tracing.EventSource. ETW works on a publish-subscribe model, and events aren’t recorded unless there’s a subscriber. In other words, simply implementing EventSource doesn’t do anything by itself — events only get recorded once you explicitly enable it through a collection tool. 2

using System.Diagnostics.Tracing;

[EventSource(Name = "KomuraSoft-OrderService")]
internal sealed class OrderServiceEventSource : EventSource
{
    public static readonly OrderServiceEventSource Log = new();

    [Event(1, Level = EventLevel.Informational, Message = "Order processing started. OrderId={0}")]
    public void OrderProcessingStart(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(1, orderId);
        }
    }

    [Event(2, Level = EventLevel.Informational, Message = "Order processing completed. OrderId={0}")]
    public void OrderProcessingStop(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(2, orderId);
        }
    }
}

The caller uses it like this. Checking whether it’s enabled with IsEnabled() before actually writing the event lets you avoid unnecessary overhead when nothing is collecting.

OrderServiceEventSource.Log.OrderProcessingStart(order.Id);
try
{
    ProcessOrder(order);
}
finally
{
    OrderServiceEventSource.Log.OrderProcessingStop(order.Id);
}

The easiest way to collect this event is to use the cross-platform dotnet-trace tool. 9

dotnet-trace collect --providers KomuraSoft-OrderService -- OrderService.exe

You open the collected .nettrace file in Visual Studio or PerfView to inspect its contents. For more serious performance investigations — for example, analysis that includes kernel events — the setup becomes using Windows Performance Recorder (WPR), included in the Windows Assessment and Deployment Kit (ADK), and viewing the result in Windows Performance Analyzer (WPA). 10 That said, this article’s scope stops at “what ETW can do and when to use it,” and doesn’t go into the detailed analysis procedures in PerfView or WPA. From the standpoint of developing and maintaining a business app, it’s enough to be able to define your own provider with EventSource and collect it with dotnet-trace.

5. The Practice of Structured Logging

ILogger message templates are written as fixed strings containing named placeholders like {PlaceHolder}. This isn’t just a stylistic convention — passing only the arguments while leaving the template itself unchanged is what lets the logging framework preserve the mapping between placeholder names and values (structured data). 11

// Recommended: keep the template fixed, pass the arguments separately
logger.LogWarning("Failed to reserve stock for order {OrderId}. Warehouse={WarehouseId}", orderId, warehouseId);

// Not recommended: string concatenation/interpolation breaks the mapping between the template and its values
logger.LogWarning("Order " + orderId + " failed to reserve stock. Warehouse=" + warehouseId);
logger.LogWarning($"Order {orderId} failed to reserve stock. Warehouse={warehouseId}");

The code analyzer rule CA2254 detects this discouraged pattern (where the template ends up changing on every call). 3 On hot paths that get called frequently, you can go further and use source generation via LoggerMessageAttribute to avoid the cost of boxing and template parsing. 12

using Microsoft.Extensions.Logging;

internal static partial class Log
{
    [LoggerMessage(
        EventId = 2001,
        Level = LogLevel.Warning,
        Message = "Failed to reserve stock for order {OrderId}. Warehouse={WarehouseId}")]
    public static partial void StockReservationFailed(
        this ILogger logger, string orderId, string warehouseId);
}

// Caller side
logger.StockReservationFailed(orderId, warehouseId);

With this setup, a single log call can be routed to different destinations: detail to the file log (via a sink like Serilog/NLog), a narrowed-down version to the event log, and low-cost tracing to ETW via the EventSourceLoggerProvider. The basic pattern is to register multiple ILoggerProvider instances and vary the output level per provider using Filter. For the minimum requirements of a homegrown logger, see “Minimum Requirements for a Custom Logger and an Integration Test Checklist”; for how to tie logs and dumps together when a crash happens, see “Designing Windows Apps to Preserve Logs and Dumps on Crash.”

6. Collection and Investigation — The Reading Side of the Logs You Wrote

In Event Viewer, you can use “Filter” or “Custom Views” from the log list to extract only a specific source, level, or event ID. Saving conditions you use often as a custom view speeds up future investigations. Custom views are defined with XPath queries; for example, a query that extracts only a specific event name looks like this. 13

<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">
      *[System[Provider[@Name='KomuraSoft.OrderService'] and (Level=2 or Level=3)]]
    </Select>
  </Query>
</QueryList>

From the command line, wevtutil lets you export logs and change settings. As part of a failure investigation, you can also use it to export the event log around the time an incident occurred and send it to support. 14

:: Export the entire Application log to an evtx file
wevtutil epl Application C:\logs\application_20260707.evtx

:: Show only events from a specific source (the most recent 10)
wevtutil qe Application /q:"*[System[Provider[@Name='KomuraSoft.OrderService']]]" /c:10 /rd:true /f:text

For crash investigations, the basic approach is to cross-reference three things by timestamp: the event log, the file log, and the crash dump. Starting from the timestamp of a “fatal error” left in the event log, you compare it against the details in the file log from the same moment and a dump captured with WER/ProcDump. How to capture a dump is covered in “An Introduction to Collecting Windows Crash Dumps — WER/ProcDump/WinDbg,” and the actual analysis procedure for a captured dump is covered in “Reading a Crash Dump with WinDbg + SOS.”

7. Pitfalls

  • Trying to write with an unregistered source, and failing. If you make a call equivalent to RegisterEventSource with an unregistered source name, the event itself does get written to the Application log, but because there’s no corresponding message resource DLL, Event Viewer can’t display the description and shows an error instead. 15 What’s more, a design that tries to auto-register the source at runtime with CreateEventSource tends to crash with an exception the first time a process running as a non-administrator user starts up. Always finish registering the source in the installer.
  • The confusion of “you can write under a different source name.” As long as you have write permission, the event log lets you write under a source name your app never even registered. Source names must be unique on a given computer, but the OS provides no mechanism to enforce that — it’s left entirely to application-side discipline. 5 Choosing an overly generic source name (a short name that doesn’t include your company or product name) is a common cause of colliding with another vendor’s app, or unintentionally reusing another app’s source name.
  • Log bloat and retention policy. The event log’s default maximum size is a mere 512 KB. 4 If a business app is going to use the event log routinely, explicitly expand the size using wevtutil sl or the installer, and consciously set whether reaching the limit should “overwrite the oldest entries” or “discard new ones.” Also note that OverwriteOlder has been deprecated, and specifying it can end up effectively behaving as “don’t overwrite” at all. 16
  • Message display in multilingual environments. If you set up localized message resource files (MessageResourceFile), then viewing the log in an environment where that resource DLL is missing or a different version shows “The description for Event ID XX cannot be found.” This tends to happen when you view a forwarded event log on a different server, or when the log entries are all that’s left after uninstalling the app. A setup that writes strings directly with WriteEntry (without using a resource file) avoids introducing this class of problem in the first place.
  • Cases where you can’t write due to insufficient privileges. It’s commonly misunderstood, but while registering a source requires administrator privileges, writing to an already-registered source works fine for a standard user. Before jumping to the conclusion “it doesn’t work without administrator privileges” and running the whole app elevated, separate out exactly which operation actually requires those privileges.

KomuraSoft LLC handles technical consulting on designing the logging, event log, and ETW foundation for Windows business apps, and on designing observability points with failure investigation in mind.

References

  1. Microsoft Learn, EventLog.CreateEventSource Method. On why creating an event source has required administrator privileges since Windows Vista (because it requires searching every log, including the Security log).  2

  2. Microsoft Learn, Getting Started with EventSource. On EventSource working on a publish-subscribe pattern where events aren’t recorded without a subscriber (ETW or EventPipe), and on how to collect events with dotnet-trace collect.  2

  3. Microsoft Learn, CA2254: Template should be a static expression. On how using string concatenation or string interpolation in a log message template loses the mapping between placeholder names and values.  2

  4. Microsoft Learn, EventLog.MaximumKilobytes Property. On the event log’s default maximum size being 512 kilobytes.  2

  5. Microsoft Learn, EventLog Class. On the three default logs (Application/System/Security), the requirement that a source be unique on a given computer, and the mechanism by which any registered source name can be written to as long as you have write permission.  2

  6. Microsoft Learn, Logging providers in .NET. On how the Generic Host’s default logging providers include Console, Debug, EventSource, and EventLog (Windows only). 

  7. Microsoft Learn, EventLogSettings Class. On AddEventLog’s default values (LogName is “Application”, SourceName is “.NET Runtime”). 

  8. Microsoft Learn, Event Tracing. On Event Tracing for Windows (ETW) being a mechanism for starting, stopping, instrumenting, and consuming an application’s trace events. 

  9. Microsoft Learn, dotnet-trace performance analysis utility. On dotnet-trace, a cross-platform trace collection tool based on EventPipe, and its collect command. 

  10. Microsoft Learn, Introduction to WPR. On Windows Performance Recorder (WPR) being a tool that extends ETW to provide detailed recording of the system and applications. 

  11. Microsoft Learn, Logging in C# and .NET. On the {PlaceHolder} syntax in log message templates, and the mechanism by which the key names in a template become the log’s property names. 

  12. Microsoft Learn, High-performance logging in .NET. On how source-generated logging via LoggerMessageAttribute avoids the cost of boxing and template parsing. 

  13. Microsoft Learn, Comparison of ETW and EventLog logger functionality. On how to create a custom view in Event Viewer using an XPath query, filtered by event name and other criteria. 

  14. Microsoft Learn, wevtutil. On command-line operations for the event log, such as export (epl), query (qe), and changing settings (sl). 

  15. Microsoft Learn, Event Sources. On how writing under an unregistered source name falls back to the Application log, but because there’s no message file, Event Viewer can’t display the description and shows an error. 

  16. Microsoft Learn, OverflowAction Enum. On the behavior when the event log reaches its size limit (overwrite or discard), and on OverwriteOlder having been deprecated. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

If I already have file logs, do I still need the event log?
For detailed investigation purposes, file logs alone are often enough. However, if you want operations staff to be able to notice app problems through OS-standard tools like Event Viewer or monitoring tools (Task Scheduler failure notifications, SCOM, and so on), it's practical to also output just the essential points to the event log. Think of the two not as substitutes for each other, but as records on different layers aimed at different readers.
How should I decide the numbering rules for event IDs?
There's no single right answer, but following three practical rules cuts down on mistakes: divide ranges by functional area (say, the 1000s for startup-related events and the 2000s for communication-related ones), never change the meaning of or reuse an ID once it's been issued, and allow for gaps in the numbering. Because ILogger's EventId also serves as a search key for structured logs, choosing a numbering scheme that's easy to document later pays off during maintenance.
I'm using Serilog or NLog — how does this article relate to that?
Serilog and NLog are implementations that sit underneath ILogger and route the same log entries to multiple sinks — files, the event log, ETW, an external SaaS, and so on. The design principles covered in this article — keep the event log down to the essentials, and don't break the message template — apply equally whether you use ILogger directly or go through Serilog/NLog. Whether you use an event-log-specific sink (such as Serilog.Sinks.EventLog) or the standard AddEventLog is just an implementation choice.
How much of ETW do I need to learn?
If your main goal is developing and maintaining a business app, it's enough to be able to define your own provider with EventSource and collect its events with dotnet-trace. Call stack analysis in PerfView and kernel event collection with WPR are areas you can pick up once you specialize in performance investigation — this article deliberately leaves them out of scope.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog