EventSource & EventPipe Authoring
Key Points
EventSourceis the in-process, ultra-low-overhead structured-event API the runtime itself uses (Microsoft-System-Net-Http,System.Runtime, etc.).EventPipeis the cross-platform transport: in-process tracing exposed over a Unix socket / named pipe; consumed bydotnet-trace,dotnet-counters, OTel'sClrEventListener.- Inside the process,
EventListenersubscribes to events without a debugger or external tool. - Use it for: library-level diagnostics (HTTP client, ORM, custom queues) where ILogger is too heavyweight or unavailable. Don't replace
ILogger/OTel for app-level structured logs. - Modern alternative: source-generated
[LoggerMessage]for ILogger; OpenTelemetry for distributed traces.EventSourceshines as a low-overhead substrate underneath these.
Concepts (deep dive)
Why EventSource exists
ILogger → categorized text, formatters, allocations per message
EventSource → structured events, ~50 ns per event, kernel-level transports (ETW/EventPipe)
Used by the runtime and BCL for diagnostics that must not measurably slow the app.
Defining an EventSource
[EventSource(Name = "Acme-OrderService")]
public sealed class OrderEventSource : EventSource
{
public static readonly OrderEventSource Log = new();
[Event(1, Level = EventLevel.Informational, Message = "Order {0} submitted with total {1}")]
public void OrderSubmitted(Guid orderId, decimal total)
=> WriteEvent(1, orderId, total);
[Event(2, Level = EventLevel.Warning, Message = "Order {0} failed: {1}")]
public void OrderFailed(Guid orderId, string reason)
=> WriteEvent(2, orderId, reason);
}
// Usage
OrderEventSource.Log.OrderSubmitted(order.Id, order.Total);
Rules: - Sealed class, public methods, no fields. - Each method has [Event(N)] with a unique ID. - Method body must be exactly WriteEvent(id, args...).
[EventCounter] for metrics
Pre-source-gen approach. Modern apps prefer Meter from System.Diagnostics.Metrics, but EventCounters are still common in BCL.
private IncrementingPollingCounter? _completedOrders;
public OrderEventSource()
{
_completedOrders = new IncrementingPollingCounter("orders-completed", this, () => _count)
{
DisplayName = "Orders Completed",
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};
}
Visible in dotnet-counters monitor Acme-OrderService.
Levels and keywords
[EventSource(Name = "Acme-Foo")]
public sealed class FooSource : EventSource
{
public static class Keywords
{
public const EventKeywords Network = (EventKeywords)1;
public const EventKeywords Database = (EventKeywords)2;
}
[Event(10, Keywords = Keywords.Network, Level = EventLevel.Verbose)]
public void NetworkCall(string url, int ms) => WriteEvent(10, url, ms);
}
Listeners can subscribe to specific levels and keyword bitmasks — verbose events stay disabled by default for negligible cost when off.
EventPipe transport
EventPipe is the cross-platform mechanism (Windows/Linux/macOS) by which external tools attach without a debugger:
Process ─┬─ EventPipe ─ Unix socket /tmp/dotnet-diagnostic-<pid>
│
└─ ETW (Windows) — ETW providers light up if the kernel has the session
Tools:
dotnet-trace collect --providers Acme-OrderService:0xff:Verbose
dotnet-trace collect --providers System.Net.Http,System.Runtime --process-id <pid>
Output .nettrace viewable in PerfView, Speedscope, or Visual Studio's Performance Profiler.
dotnet-counters
Reads EventCounter polls in real time without affecting the app meaningfully.
EventListener — in-process consumer
public sealed class HttpListener : EventListener
{
protected override void OnEventSourceCreated(EventSource source)
{
if (source.Name == "System.Net.Http")
EnableEvents(source, EventLevel.Verbose, EventKeywords.All);
}
protected override void OnEventWritten(EventWrittenEventArgs e)
{
Console.WriteLine($"{e.EventName}: {string.Join(",", e.Payload!)}");
}
}
// Activate
new HttpListener();
The listener receives every event globally. Use it to bridge runtime events to your logging stack (e.g., capture failed HTTP requests for diagnostics dashboards).
When EventSource beats ILogger
- Library code where
ILoggerinjection is awkward. - Runtime/BCL-level events you want exposed via the same channel as
System.Runtime(dotnet-counters/dotnet-trace). - Performance-critical hot paths —
EventSourcewrites are ~10× faster thanILoggerallocations when no listener is attached.
When ILogger beats EventSource
- App code with structured logging requirements.
- Log scopes, filtering by category, formatters → ILogger.
- OTel exporters, Seq, App Insights → ILogger.
Source generation alternative
public partial class FooLogger
{
private readonly ILogger _logger;
public FooLogger(ILogger<FooLogger> logger) => _logger = logger;
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Order {OrderId} submitted")]
public partial void OrderSubmitted(Guid orderId);
}
Compile-time check, zero allocations on IsEnabled = false. First choice for app-level logs.
Code: correct vs wrong
❌ Wrong: heavy work in a verbose event
[Event(1, Level = EventLevel.Verbose)]
public void Trace(string json) => WriteEvent(1, json);
OrderEventSource.Log.Trace(JsonSerializer.Serialize(big)); // serializes even when no listener
✅ Correct: gate via IsEnabled
if (OrderEventSource.Log.IsEnabled(EventLevel.Verbose, EventKeywords.None))
OrderEventSource.Log.Trace(JsonSerializer.Serialize(big));
❌ Wrong: violating the body rule
[Event] methods must contain only WriteEvent(id, args...) — the runtime emits non-event helpers correctly.
✅ Correct: keep methods bare
Design patterns for this topic
Pattern 1 — One EventSource per logical component
Mirrors BCL conventions (System.Net.Http, System.Net.Sockets).
Pattern 2 — Bridge to ILogger via EventListener
class BridgeListener : EventListener
{
protected override void OnEventSourceCreated(EventSource s)
{ if (s.Name.StartsWith("Acme-")) EnableEvents(s, EventLevel.Informational); }
protected override void OnEventWritten(EventWrittenEventArgs e)
{
// Forward to ILogger or App Insights
}
}
Pattern 3 — Counter-only EventSource for metrics
If you only want dotnet-counters visibility for app-level metrics, define an EventSource with only EventCounter instances. Lighter than full ETW events.
Pattern 4 — Combine with Meter
private static readonly Meter _meter = new("Acme.OrderService");
private static readonly Counter<long> _ordersCompleted = _meter.CreateCounter<long>("orders.completed");
Meter (System.Diagnostics.Metrics) is the modern OTel-aligned API. Use it for new metrics; EventCounter survives for legacy.
Pros & cons / trade-offs
| Tool | Pros | Cons |
|---|---|---|
EventSource | Ultra-fast, ETW-compatible | Awkward DI, no scopes |
ILogger (+ source-gen) | Categorical, scopes, filters | Heavier per call |
Meter | Modern OTel metrics | Doesn't replace events |
| Direct ETW | Maximum control | Windows-only |
When to use / when to avoid
- Use
EventSourcefor library/runtime-level events, performance-critical or BCL-aligned diagnostics. - Use
[LoggerMessage]for app-level structured logs. - Use
Meterfor metrics emitted to OTel. - Avoid
EventSourcewhen you don't need ETW/EventPipe consumption.
Interview Q&A
Q1. What's the difference between EventSource and ILogger? A. EventSource writes structured ETW/EventPipe events at ~50 ns each (no listener); ILogger formats categorical text logs through providers. EventSource is for runtime/library-level diagnostics; ILogger for app logs.
Q2. What is EventPipe? A. The cross-platform .NET tracing transport. External tools (dotnet-trace, dotnet-counters) connect to a process-local Unix socket / named pipe and stream EventSource events without a debugger.
Q3. Why does an [Event] method's body need to be just WriteEvent? A. The runtime generates ETW manifests by inspecting the method via reflection; complex bodies break the generation and emit no event. Keep them as one-line forwarders.
Q4. How do you bridge runtime events into ILogger? A. Subclass EventListener, enable target sources in OnEventSourceCreated, and forward OnEventWritten payloads to the logger. Useful for capturing HTTP/SSL/DNS diagnostics.
Q5. EventCounter vs Meter? A. EventCounter is older, EventPipe-only, manually polled. Meter (System.Diagnostics.Metrics) is OTel-aligned, exports via OTLP/Prometheus naturally. Use Meter for new code; EventCounter survives for BCL compat.
Q6. What's EnableEvents doing? A. Subscribes a listener (in-process) or external session (ETW/EventPipe) to events at a given level and keyword bitmask. Until enabled, events fire-and-forget — almost free.
Q7. How does dotnet-trace consume events? A. Connects to the EventPipe Unix socket, sends a "start session" command listing providers and levels, then streams the resulting .nettrace to disk. Viewable in PerfView/Speedscope.
Q8. Can EventSource cause startup overhead? A. Trivial — instantiation registers a manifest. Calls without a listener are tens of nanoseconds. The cost surfaces only when a listener enables verbose events.
Gotchas / common mistakes
- Mutating fields in
[Event]methods — breaks manifest generation. - Forgetting to gate large payloads behind
IsEnabled. - Using
EventSourcefor app-level logs — ILogger is friendlier for production. - Conflicting event IDs across versions — use stable IDs forever.
- Listeners that subscribe to System.Runtime at Verbose during steady state — generates a flood.
- Not understanding that ETW (Windows) and EventPipe (cross-platform) consume the same provider events.
- Forgetting that
EventListenercallbacks run on the firing thread — keep them fast.