An Introduction to Windows Event Log and ETW — Putting Your Business App's Logs on the OS's Standard Mechanisms
· Go Komura · 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.CreateEventSourcecan’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
RegisterEventSourcewith 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 withCreateEventSourcetends 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 slor the installer, and consciously set whether reaching the limit should “overwrite the oldest entries” or “discard new ones.” Also note thatOverwriteOlderhas 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 withWriteEntry(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.
Related Articles
- When Do You Actually Need Administrator Privileges on Windows? — UAC, Protected Areas, and How to Tell by Design
- Building and Operating Windows Services — From Choosing Between Task Scheduler to Turning a BackgroundService into a Service
- When a Task Scheduler Task Doesn’t Run, or Exits with 0x1 — Isolating the Cause and Designing for Reliable Operation
- Minimum Requirements for a Custom Logger and an Integration Test Checklist
- Designing Windows Apps to Preserve Logs and Dumps on Crash
- An Introduction to Collecting Windows Crash Dumps — WER/ProcDump/WinDbg
- Reading a Crash Dump with WinDbg + SOS — A Practical Introduction to Analysis After Collection
Related Consulting Areas
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.
- Windows App Development
- Bug Investigation & Root Cause Analysis
- Technical Consulting & Design Review
- Contact
References
-
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
-
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
-
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
-
Microsoft Learn, EventLog.MaximumKilobytes Property. On the event log’s default maximum size being 512 kilobytes. ↩ ↩2
-
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
-
Microsoft Learn, Logging providers in .NET. On how the Generic Host’s default logging providers include Console, Debug, EventSource, and EventLog (Windows only). ↩
-
Microsoft Learn, EventLogSettings Class. On AddEventLog’s default values (LogName is “Application”, SourceName is “.NET Runtime”). ↩
-
Microsoft Learn, Event Tracing. On Event Tracing for Windows (ETW) being a mechanism for starting, stopping, instrumenting, and consuming an application’s trace events. ↩
-
Microsoft Learn, dotnet-trace performance analysis utility. On dotnet-trace, a cross-platform trace collection tool based on EventPipe, and its collect command. ↩
-
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. ↩
-
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. ↩
-
Microsoft Learn, High-performance logging in .NET. On how source-generated logging via LoggerMessageAttribute avoids the cost of boxing and template parsing. ↩
-
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. ↩
-
Microsoft Learn, wevtutil. On command-line operations for the event log, such as export (epl), query (qe), and changing settings (sl). ↩
-
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. ↩
-
Microsoft Learn, OverflowAction Enum. On the behavior when the event log reaches its size limit (overwrite or discard), and on OverwriteOlder having been deprecated. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Pinpointing "Slow" with PerfView and dotnet-trace — A Practical Introduction to .NET Performance Investigation
When a business app is "slow," "pegs the CPU," or "occasionally freezes," which tool should you reach for, and what should you look at? T...
Printing and PDF Output in Windows Business Apps — Choosing Between System.Drawing.Printing, WPF, and Report Libraries
Organizes WinForms printing with PrintDocument, WPF FlowDocument/FixedDocument printing, and PDF output options into a requirement-based ...
Reading Crash Dumps with WinDbg + SOS — A Practical Guide to Analysis After Collection
Explains how to actually read a collected Windows crash dump using WinDbg and the SOS extension. Covers symbol path configuration, tracki...
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
More Than appsettings.json — A Practical Guide to Configuration Management in Windows Business Apps (Per-Environment Settings, Secrets, and Where to Write)
A practical look at configuration management for Windows business apps: layering appsettings.json, choosing between IConfiguration and th...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Bug Investigation & Long-Run Failures
Topic page for intermittent failures, communication diagnosis, long-run crashes, and failure-path test foundations.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
Designing the logging, event log, and ETW foundation falls within the scope of Windows app development consulting.
Bug Investigation & Root Cause Analysis
Log design with an eye toward failure investigation ties directly into bug investigation and root cause analysis consulting.
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.
Public links