Table of Contents

Observability and Diagnostics

WebDriverBiDi.NET provides comprehensive observability through System.Diagnostics.Tracing.EventSource, enabling you to monitor, troubleshoot, and optimize your WebDriver BiDi automation.

Overview

The library emits structured diagnostic events that can be consumed by:

  • EventListener - Custom in-process listeners for real-time monitoring
  • ETW (Event Tracing for Windows) - Windows performance monitoring
  • EventPipe - Cross-platform event collection and analysis
  • dotnet-trace - CLI diagnostics tool for production troubleshooting
  • OpenTelemetry - Distributed tracing and metrics collection
  • APM Tools - Application Insights, Dynatrace, New Relic (via bridges)

Key Benefits:

  • ✅ Zero dependencies - Uses only core .NET APIs
  • ✅ Low overhead - Minimal performance impact when not actively listening
  • ✅ Production-ready - Designed for high-throughput scenarios
  • ✅ Extensible - Easy to integrate with existing logging infrastructure

Quick Start

Console Logging

The simplest way to see diagnostic events is a small EventListener that enables the WebDriverBiDi source and writes each event to the console:

public class ObservabilityConsoleEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource source)
    {
        if (source.Name == "WebDriverBiDi")
        {
            EnableEvents(source, EventLevel.Informational);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        Console.WriteLine($"[{eventData.Level}] {eventData.EventName}");
    }
}

Create it before the driver so that it sees the source as soon as the library touches it:

// Create a simple console event listener (see ObservabilityConsoleEventListener below)
using var listener = new ObservabilityConsoleEventListener();

// Use WebDriverBiDi normally - events will be logged to console
await using var driver = new BiDiDriver();
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");

Custom EventListener

Create a custom listener for more control:

public class MyEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource source)
    {
        // Enable WebDriverBiDi events at Informational level
        if (source.Name == "WebDriverBiDi")
        {
            EnableEvents(source, EventLevel.Informational);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        Console.WriteLine($"[{eventData.Level}] {eventData.EventName}");

        // Access structured payload
        if (eventData.PayloadNames != null)
        {
            for (int i = 0; i < eventData.PayloadNames.Count; i++)
            {
                Console.WriteLine($"  {eventData.PayloadNames[i]}: {eventData.Payload?[i]}");
            }
        }
    }
}
// Usage
using var listener = new MyEventListener();

Event Levels

Choose the appropriate level based on your needs:

Level Use Case Events Included
Critical Production alerts Critical failures (none currently emitted)
Error Error tracking Command errors, protocol errors, connection errors
Warning Operational monitoring Command timeouts, commands that failed before transmission, event handler errors, unknown messages
Informational General monitoring Connection lifecycle, command completion, responses discarded for canceled commands, transport start/stop, custom registrations
Verbose Development/debugging All events including command sending, event receipt, statistics

Recommendation for Production: Use EventLevel.Informational or EventLevel.Warning to balance observability with overhead.

Available Events

The ID column is the numeric EventSource event ID, which the Microsoft.Extensions.Logging bridge surfaces as EventId on every forwarded entry. IDs are stable; gaps in the sequence are events that existed in an earlier version and have since been retired.

Connection Lifecycle

Event ID Level Description Payload
ConnectionOpening 1 Info Connection is being established connectionId, sessionId, url
ConnectionOpened 2 Info Connection successfully established connectionId, sessionId, url
ConnectionClosing 3 Info Connection is being closed connectionId, sessionId, reason
ConnectionClosed 4 Info Connection fully closed connectionId, sessionId
ConnectionError 5 Error Connection error occurred connectionId, sessionId, errorMessage

Every session the transport opens is closed out the same way, however it ends: a session that raised ConnectionOpened and TransportStarted always ends with exactly one ConnectionClosed and one TransportStopped. A local stop raises ConnectionClosing first, with the termination reason, and raises the closing pair even if the connection's own stop fails. A session the remote end closes, or that ends in a connection error, raises no ConnectionClosing; its TransportStopped reason names the cause ("Remote end closed the connection", or "Connection error: " followed by the error). Application code that needs to react to such a loss observes driver.OnConnectionLost (see OnConnectionLost). A connect attempt that fails after ConnectionOpening raises ConnectionError with the failure, and no ConnectionOpened.

Every event except AsyncHandlerTaskCount begins its payload with connectionId and sessionId, and its rendered message with [connectionId/sessionId], so a line identifies the driver it came from without cross-referencing anything.

connectionId is Connection.Id, a GUID string assigned when the Connection is constructed and stable for its lifetime, including across reconnects; read it from the connection object to correlate your own logging. sessionId is a GUID assigned to each session the transport opens, so it changes on every reconnect and distinguishes the work of one session from the next on the same connection. It is empty on ConnectionOpening, which precedes the session it opens, on the module and event registration events, which are only legal while the transport is disconnected, and on events raised by a Connection used without a Transport, which has no sessions. AsyncHandlerTaskCount carries neither: it is a process-wide counter shared by every driver.

Command Execution

Event ID Level Description Payload
CommandSending 6 Verbose Command being sent to remote end connectionId, sessionId, commandId, method
CommandCompleted 7 Info Command completed successfully connectionId, sessionId, commandId, method, elapsedMilliseconds
CommandTimeout 8 Warning Command timed out connectionId, sessionId, commandId, method, timeoutMilliseconds
CommandError 9 Error Command failed with error response connectionId, sessionId, commandId, method, errorCode, errorType, errorMessage
CommandSendFailed 22 Warning Command could not be transmitted to the remote end connectionId, sessionId, commandId, method, failureType, failureMessage, elapsedMilliseconds
CanceledCommandResponseDiscarded 24 Info A response arrived for a command the local end had already stopped waiting for (timed out, canceled, or pending at connection close) and was discarded connectionId, sessionId, commandId, method, reason, millisecondsSinceCancellation

Event Handling

Event ID Level Description Payload
EventReceived 10 Verbose Protocol event received connectionId, sessionId, eventMethod
EventHandlerError 15 Warning User event handler threw exception connectionId, sessionId, eventMethod, errorMessage

Protocol Processing

Event ID Level Description Payload
UnknownMessageReceived 13 Warning Message that is not valid JSON, or not a command response, error response or registered event connectionId, sessionId, messageType, messageLength
ProtocolError 14 Error Error response or registered event whose payload could not be deserialized, or a fault in the message processing loop connectionId, sessionId, errorMessage, messageSnippet

Transport & Statistics

Event ID Level Description Payload
TransportStarted 17 Info Transport message processing started connectionId, sessionId
TransportStopped 18 Info Transport message processing stopped connectionId, sessionId, reason
PendingCommandCount 16 Verbose Current pending command count connectionId, sessionId, pendingCount
AsyncHandlerTaskCount 23 Verbose Number of in-flight asynchronous event handler tasks (see Performance) inFlightCount
MessageStatistics 21 Verbose Message statistics for a session, raised when the session ends; messagesSent counts commands and messagesReceived command responses connectionId, sessionId, messagesSent, messagesReceived, eventsReceived, errorsReceived

Module & Extensibility

Event ID Level Description Payload
CustomModuleRegistered 19 Info Custom module registered connectionId, sessionId, moduleName
CustomEventRegistered 20 Info Custom event type registered connectionId, sessionId, eventName, eventType

Common Scenarios

Performance Monitoring

Track command execution times to identify bottlenecks:

public class PerformanceMonitor : EventListener
{
    private readonly Dictionary<string, List<long>> timings = new();

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (source.Name == "WebDriverBiDi")
        {
            EnableEvents(source, EventLevel.Informational);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventName == "CommandCompleted")
        {
            // Payload is [connectionId, sessionId, commandId, method, elapsedMilliseconds].
            string method = eventData.Payload?[3]?.ToString() ?? "unknown";
            long elapsed = Convert.ToInt64(eventData.Payload?[4]);

            if (!timings.ContainsKey(method))
            {
                timings[method] = new List<long>();
            }
            timings[method].Add(elapsed);

            // Alert on slow commands
            if (elapsed > 1000)
            {
                Console.WriteLine($"SLOW: {method} took {elapsed}ms");
            }
        }
    }

    public void PrintStatistics()
    {
        foreach (var kvp in timings.OrderByDescending(x => x.Value.Average()))
        {
            Console.WriteLine($"{kvp.Key}: avg={kvp.Value.Average():F2}ms, max={kvp.Value.Max()}ms");
        }
    }
}

Error Tracking

Monitor errors for debugging and alerting:

public class ErrorTracker : EventListener
{
    private int errorCount = 0;
    private readonly List<string> recentErrors = new();

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (source.Name == "WebDriverBiDi")
        {
            EnableEvents(source, EventLevel.Warning); // Warning and above
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.Level == EventLevel.Error)
        {
            errorCount++;
            // Pair each payload name with its value; joining PayloadNames alone records the shape of
            // the event and none of its detail, so every error of a given kind looks identical.
            ReadOnlyCollection<string>? names = eventData.PayloadNames;
            ReadOnlyCollection<object?>? values = eventData.Payload;
            IEnumerable<string> payload = Enumerable.Range(0, Math.Min(names?.Count ?? 0, values?.Count ?? 0))
                .Select(index => $"{names![index]}={values![index]}");
            string error = $"{eventData.EventName}: {string.Join(", ", payload)}";
            recentErrors.Add(error);

            // Keep only last 100 errors
            if (recentErrors.Count > 100)
            {
                recentErrors.RemoveAt(0);
            }

            // Send to monitoring service
            if (errorCount % 10 == 0)
            {
                Console.WriteLine($"ALERT: {errorCount} errors occurred!");
            }
        }
    }
}

Integration with Microsoft.Extensions.Logging

For ILogger integration, use the WebDriverBiDi.Logging NuGet package:

dotnet add package WebDriverBiDi.Logging

This package bridges WebDriverBiDi EventSource events (not the driver's OnLogMessage messages; see What the Bridge Does Not Forward) to the standard .NET logging infrastructure, enabling integration with Application Insights, Serilog, and other logging providers. The AddWebDriverBiDi() extension method is available on ILoggingBuilder (in the Microsoft.Extensions.Logging namespace) once the package is referenced.

Basic Console Application

// Setup dependency injection
var services = new ServiceCollection();

// Configure logging with WebDriverBiDi events
services.AddLogging(builder =>
{
    builder.AddConsole()
        .SetMinimumLevel(LogLevel.Debug);

    // Add WebDriverBiDi diagnostic event logging
    builder.AddWebDriverBiDi(EventLevel.Informational);
});

await using var serviceProvider = services.BuildServiceProvider();

// The bridge starts listening when the logging pipeline is built, which happens the first time
// ILoggerFactory (or an ILogger) is resolved. A generic or web host does this at startup.
_ = serviceProvider.GetRequiredService<ILoggerFactory>();

// Use WebDriverBiDi - events will be automatically logged
await using var driver = new BiDiDriver();
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");

// Logs will show:
// Every message is prefixed with [connectionId/sessionId]. connectionId is Connection.Id, a GUID
// assigned when the connection is created; sessionId identifies the transport session, a new GUID
// for each connect, left empty on ConnectionOpening, which is raised before that session exists.
// info: WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger[1]
//       [3f2a9c81-5d64-4b0e-9a77-1c8e6b2d4f05/] Opening connection to ws://localhost:9515/session/YOUR-SESSION-ID
// info: WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger[2]
//       [3f2a9c81-5d64-4b0e-9a77-1c8e6b2d4f05/7b41e0d2-9c35-4a18-8f60-2d7e1b9a3c44] Connection opened to ws://localhost:9515/session/YOUR-SESSION-ID
// info: WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger[17]
//       [3f2a9c81-5d64-4b0e-9a77-1c8e6b2d4f05/7b41e0d2-9c35-4a18-8f60-2d7e1b9a3c44] Transport started

await driver.Session.StatusAsync();

// Logs will show:
// info: WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger[7]
//       [3f2a9c81-5d64-4b0e-9a77-1c8e6b2d4f05/7b41e0d2-9c35-4a18-8f60-2d7e1b9a3c44] Command 1 (session.status) completed in 42ms
// (CommandSending is a Verbose-level event and is not emitted at EventLevel.Informational;
//  TransportStarted is raised once, when the transport connects, and is already shown above)

Logs will show connection lifecycle and command completion events (e.g., ConnectionOpening, ConnectionOpened, CommandCompleted); CommandSending is a Verbose-level event and appears only if you raise the minimum level to EventLevel.Verbose.

ASP.NET Core Web Application

var builder = WebApplication.CreateBuilder(args);

// Add WebDriverBiDi event logging to the ASP.NET Core logging pipeline
builder.Logging.AddWebDriverBiDi(EventLevel.Informational);

// Configure services
builder.Services.AddScoped<IBrowserAutomationService, BrowserAutomationService>();

var app = builder.Build();

app.MapGet("/test", async (IBrowserAutomationService automation) =>
{
    // WebDriverBiDi events will be logged through the ASP.NET Core logging infrastructure
    var result = await automation.RunTestAsync();
    return Results.Ok(result);
});

app.Run();

Add your services (e.g., IBrowserAutomationService) and endpoints as needed. WebDriverBiDi events will be logged through the ASP.NET Core logging infrastructure.

With Serilog Structured Logging

// Configure Serilog with structured logging
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug() // the bridge logs EventLevel.Verbose events at LogLevel.Debug
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}")
    .Enrich.FromLogContext()
    .CreateLogger();

var services = new ServiceCollection();

services.AddLogging(builder =>
{
    builder.ClearProviders();
    builder.AddSerilog();
    builder.AddWebDriverBiDi(EventLevel.Verbose); // Capture all events including verbose
});

await using var serviceProvider = services.BuildServiceProvider();

// The bridge starts listening when the logging pipeline is built, which happens the first time
// ILoggerFactory (or an ILogger) is resolved. A generic or web host does this at startup.
_ = serviceProvider.GetRequiredService<ILoggerFactory>();

// Structured properties will be captured by Serilog
await using var driver = new BiDiDriver();
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");

// Serilog renders the message from the event's message template. The properties that template uses
// (connectionId, sessionId, commandId, method, elapsedMilliseconds) are captured on the log event,
// so {Properties} does not repeat them:
// [12:34:56 INF] [3f2a9c81-5d64-4b0e-9a77-1c8e6b2d4f05/7b41e0d2-9c35-4a18-8f60-2d7e1b9a3c44] Command 1 (session.status) completed in 42ms {"EventId": {"Id": 7, "Name": "CommandCompleted"}, "EventName": "CommandCompleted", "EventSource": "WebDriverBiDi", "SourceContext": "WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger"}

Serilog receives each event's message template, so it renders the message from the template and captures the payload values (e.g., commandId, method, elapsedMilliseconds) as structured properties.

With Application Insights

var services = new ServiceCollection();

// Add Application Insights
services.AddApplicationInsightsTelemetry();

// Configure logging with WebDriverBiDi events
services.AddLogging(builder =>
{
    builder.AddApplicationInsights();
    builder.AddWebDriverBiDi(EventLevel.Informational);
});

using var serviceProvider = services.BuildServiceProvider();

// The bridge starts listening when the logging pipeline is built, which happens the first time
// ILoggerFactory (or an ILogger) is resolved. A generic or web host does this at startup.
_ = serviceProvider.GetRequiredService<ILoggerFactory>();

// WebDriverBiDi events will be sent to Application Insights with structured properties
// allowing you to query and analyze automation telemetry

WebDriverBiDi events will be sent to Application Insights with structured properties for querying and analysis.

Filtering by Event Level

services.AddLogging(builder =>
{
    builder.AddConsole();

    // Only capture Warning and Error events
    builder.AddWebDriverBiDi(EventLevel.Warning);

    // Or use ILogger filtering
    builder.AddFilter("WebDriverBiDi.Logging", LogLevel.Warning);
});

Configuration-based Setup

appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "WebDriverBiDi.Logging.WebDriverBiDiEventSourceLogger": "Debug"
    },
    "Console": {
      "IncludeScopes": true
    }
  }
}

Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Configuration is loaded from appsettings.json
builder.Logging.AddWebDriverBiDi(); // Respects configured log levels

var app = builder.Build();

Custom Event Processing

For scenarios requiring custom processing beyond standard logging:

public class CustomWebDriverEventListener : EventListener
{
    private readonly ILogger logger;

    public CustomWebDriverEventListener(ILogger logger)
    {
        this.logger = logger;
    }

    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name == "WebDriverBiDi")
        {
            EnableEvents(eventSource, EventLevel.Informational);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // Custom processing here
        if (eventData.EventName == "CommandCompleted")
        {
            // Payload is [connectionId, sessionId, commandId, method, elapsedMilliseconds].
            long elapsedMs = Convert.ToInt64(eventData.Payload?[4]);
            if (elapsedMs > 1000)
            {
                logger.LogWarning("Slow command detected: {Method} took {ElapsedMs}ms",
                    eventData.Payload?[3], elapsedMs);
            }
        }
    }
}
// Register as singleton
services.AddSingleton(sp =>
{
    var logger = sp.GetRequiredService<ILogger<CustomWebDriverEventListener>>();
    return new CustomWebDriverEventListener(logger);
});

See the WebDriverBiDi.Logging package for details.

CLI Tools

Using dotnet-trace

Collect events from a running process without code changes:

# List running .NET processes
dotnet-trace ps

# Collect WebDriverBiDi events
dotnet-trace collect --process-id <pid> --providers WebDriverBiDi

# Collect at a specific event level (5=Verbose, 4=Info, 3=Warning, 2=Error). The provider format is
# name[:keywords[:level]], so the level comes after a keyword mask; every mask matches these events.
dotnet-trace collect --process-id <pid> --providers WebDriverBiDi:0xFFFFFFFFFFFFFFFF:4

# Convert to other formats
dotnet-trace convert trace.nettrace --format speedscope

Using PerfView (Windows)

For Windows-specific ETW collection:

# Collect ETW events
PerfView.exe /OnlyProviders=*WebDriverBiDi collect

# Stop collection
# (Press 's' in PerfView window)

# View events
# Open .etl file in PerfView and navigate to Events view

OpenTelemetry Integration

The library emits EventSource events only; it does not define a System.Diagnostics.ActivitySource, so subscribing OpenTelemetry to a source named WebDriverBiDi collects nothing. Bridge the events into an ActivitySource your application owns with an EventListener, and subscribe to that:

// WebDriverBiDi emits EventSource events, not System.Diagnostics activities, so there is
// nothing for AddSource("WebDriverBiDi") to collect. Bridge the events into an
// ActivitySource owned by your application with an EventListener, and subscribe to that.
ActivitySource activitySource = new ActivitySource("MyApp.WebDriverBiDi");
using ObservabilityActivityBridge bridge = new ObservabilityActivityBridge(activitySource);

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource("MyApp.WebDriverBiDi")
    .AddConsoleExporter()
    .Build();

// Now use WebDriverBiDi - each diagnostic event is recorded as an activity

Requires: OpenTelemetry, OpenTelemetry.Exporter.Console.

Best Practices

Production Environments

  1. Use Informational or Warning Level See MyEventListener and PerformanceMonitor in the samples—they use EnableEvents(source, EventLevel.Informational) in OnEventSourceCreated.

  2. Process Events Quickly

    • OnEventWritten is called synchronously
    • Avoid blocking operations
    • Queue events for async processing if needed
  3. Filter by Event Name

    if (eventData.EventName == "CommandError")
    {
        // Handle only command errors
    }
    
  4. Monitor Key Metrics

    • Command latency (CommandCompleted)
    • Error rate (CommandError, ProtocolError)
    • Connection stability (ConnectionError)

Development Environments

  1. Use Verbose Level Use EventLevel.Verbose in EnableEvents(source, EventLevel.Verbose) within your EventListener's OnEventSourceCreated.

  2. Enable Detailed Payload Logging

    • Log full payload for debugging
    • Use eventData.PayloadNames and eventData.Payload
  3. Combine with Existing Logging

    • Bridge to your preferred logging framework
    • Maintain consistent log format

Resource Management

Always dispose EventListener instances:

using var listener = new MyEventListener();
// or
var listener = new MyEventListener();
try
{
    // use listener
}
finally
{
    listener?.Dispose();
}

Performance Considerations

  • Cheap when not enabled - each event method checks IsEnabled before writing, so nothing is emitted without a listener. It is not allocation-free: a few call sites build their arguments first, such as the exception type name in CommandSendFailed
  • Low overhead - Minimal impact even with Verbose logging
  • ETW optimized - On Windows, uses highly optimized ETW infrastructure
  • Works with the standard logging pipeline - the WebDriverBiDi.Logging bridge forwards events to ILogger, whose Log method is synchronous

Overhead:

  • With no listener attached, event emission is effectively free: the EventSource short-circuits before building any event data.
  • With a listener attached, the per-event cost scales with the configured level and the amount of data captured, and is small relative to the network round-trips the events describe.

The repository does not include a benchmark for EventSource emission, so specific per-event timings are intentionally not quoted here.

Troubleshooting

Events Not Appearing

  1. Verify EventSource is enabled:

    if (WebDriverBiDiEventSource.RaiseEvent.IsEnabled())
    {
        Console.WriteLine("EventSource is enabled");
    }
    
  2. Check event level:

    if (WebDriverBiDiEventSource.RaiseEvent.IsEnabled(EventLevel.Verbose, EventKeywords.None))
    {
        Console.WriteLine("Verbose events are enabled");
    }
    
  3. Ensure listener is created before WebDriverBiDi use:

    // CORRECT: Listener created first
    using var listener1 = new MyEventListener();
    await using var driver1 = new BiDiDriver();
    
    // INCORRECT: Listener created after driver
    await using var driver2 = new BiDiDriver();
    using var listener2 = new MyEventListener(); // May miss early events
    
  4. If the application is published with Native AOT, enable EventSource support. The ILCompiler sets EventSourceSupport to false by default, which makes EventSource.IsEnabled() return false permanently: no events are emitted, no listener is ever called, and the Microsoft.Extensions.Logging bridge produces nothing. Nothing throws and nothing is logged about it, so it presents exactly as "no events". The Web SDK (Microsoft.NET.Sdk.Web) sets it to true when the property is empty, so an ASP.NET Core application already has it; every other SDK needs the opt-in. Opt back in from the project file:

    <PropertyGroup>
      <PublishAot>true</PublishAot>
      <EventSourceSupport>true</EventSourceSupport>
    </PropertyGroup>
    

    This applies only to Native AOT publishing; a normal build, including a trimmed one, is unaffected.

High Event Volume

If experiencing performance issues:

  1. Increase event level (Info instead of Verbose)
  2. Filter specific events in OnEventWritten
  3. Async processing - Queue events for background processing
  4. Sampling - Process only 1 in N events for high-frequency events

See Also