Skip to content

EventSource & EventPipe Authoring

Key Points

  • EventSource is the in-process, ultra-low-overhead structured-event API the runtime itself uses (Microsoft-System-Net-Http, System.Runtime, etc.).
  • EventPipe is the cross-platform transport: in-process tracing exposed over a Unix socket / named pipe; consumed by dotnet-trace, dotnet-counters, OTel's ClrEventListener.
  • Inside the process, EventListener subscribes 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. EventSource shines 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

dotnet-counters monitor --name MyApp Acme-OrderService System.Runtime

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 ILogger injection is awkward.
  • Runtime/BCL-level events you want exposed via the same channel as System.Runtime (dotnet-counters/dotnet-trace).
  • Performance-critical hot paths — EventSource writes are ~10× faster than ILogger allocations 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(1)]
public void Foo(int x) { Compute(x); WriteEvent(1, x); }     // breaks ETW codegen

[Event] methods must contain only WriteEvent(id, args...) — the runtime emits non-event helpers correctly.

✅ Correct: keep methods bare

[Event(1)] public void Foo(int x) => WriteEvent(1, x);

Design patterns for this topic

Pattern 1 — One EventSource per logical component

Acme-OrderService     — order operations
Acme-OrderService-Db  — database events

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 EventSource for library/runtime-level events, performance-critical or BCL-aligned diagnostics.
  • Use [LoggerMessage] for app-level structured logs.
  • Use Meter for metrics emitted to OTel.
  • Avoid EventSource when 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 EventSource for 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 EventListener callbacks run on the firing thread — keep them fast.

Further reading