PulseURL.AspNetCore 0.2.0-beta

Prefix Reserved
This is a prerelease version of PulseURL.AspNetCore.
There is a newer version of this package available.
See the version list below for details.
dotnet add package PulseURL.AspNetCore --version 0.2.0-beta
                    
NuGet\Install-Package PulseURL.AspNetCore -Version 0.2.0-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PulseURL.AspNetCore" Version="0.2.0-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PulseURL.AspNetCore" Version="0.2.0-beta" />
                    
Directory.Packages.props
<PackageReference Include="PulseURL.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PulseURL.AspNetCore --version 0.2.0-beta
                    
#r "nuget: PulseURL.AspNetCore, 0.2.0-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PulseURL.AspNetCore@0.2.0-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PulseURL.AspNetCore&version=0.2.0-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=PulseURL.AspNetCore&version=0.2.0-beta&prerelease
                    
Install as a Cake Tool

PulseURL .NET Client

.NET License

.NET client library and ASP.NET Core middleware for PulseURL - a lightweight, purpose-built traffic event logging service for Kubernetes.

Features

✨ v0.1.0 Features

  • πŸ”„ Async Buffering - Non-blocking event queue using Channel<T>
  • πŸ” Retry Logic - Exponential backoff with Polly
  • ⚑ Circuit Breaker - Prevents cascading failures when service is unavailable
  • πŸ“¦ Batch Streaming - Automatic batching for 10x+ throughput
  • πŸ“Š Observability - Automatic stats logging + System.Diagnostics.Metrics
  • 🎯 Sampling - Configurable sample rate (0.0 to 1.0)
  • 🎨 Multi-Targeting - Supports .NET 6.0, 8.0, and 9.0
  • πŸ”Œ ASP.NET Core Middleware - One-line integration
  • 🏷️ Custom Labels - Add metadata to all events
  • 🚫 Path Skipping - Exclude health checks, metrics endpoints, etc.
  • 🌐 Client IP Detection - Supports X-Forwarded-For headers

Quick Start

Installation

# Install the client library
dotnet add package PulseURL.Client

# For ASP.NET Core middleware
dotnet add package PulseURL.AspNetCore
using PulseURL.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Add PulseURL client
builder.Services.AddPulseURL(options =>
{
    options.ServiceUrl = "pulseurl-service:9090";
    options.EnableMetrics = true;
});

var app = builder.Build();

// Add PulseURL middleware (logs all HTTP requests automatically)
app.UsePulseURL(options =>
{
    options.SkipPaths = new HashSet<string> { "/health", "/metrics" };
    options.CustomLabels = new Dictionary<string, string>
    {
        ["app"] = "my-api",
        ["environment"] = "production"
    };
});

app.MapGet("/", () => "Hello World!");
app.Run();

That's it! All HTTP requests are now logged to PulseURL with automatic retry, circuit breaker, and batch streaming.

Manual Client Usage

using PulseURL.Client;

// Create client
var options = new PulseURLOptions
{
    ServiceUrl = "localhost:9090",
    BatchSize = 100,
    EnableMetrics = true
};

using var client = new PulseURLClient(options);

// Log events (async, non-blocking)
var eventBuilder = new TrafficEventBuilder()
    .WithService("user-api")
    .WithUrl("/api/users/123")
    .WithRoute("/api/users/{id}")
    .WithHttpMethod("GET")
    .WithStatusCode(200)
    .WithDuration(TimeSpan.FromMilliseconds(45))
    .AddLabel("region", "us-east-1");

client.LogEvent(eventBuilder.Build());

// Or log synchronously
await client.LogEventAsync(eventBuilder.Build());

// Batch logging for high throughput
var events = new List<Pulseurl.TrafficEvent> { /* ... */ };
await client.LogEventBatchAsync(events);

Configuration

Client Options

var options = new PulseURLOptions
{
    // Required
    ServiceUrl = "pulseurl-service:9090",

    // Performance
    BufferSize = 1000,              // Event queue size
    FlushInterval = TimeSpan.FromSeconds(1),
    Timeout = TimeSpan.FromSeconds(2),

    // Batching (for high throughput)
    BatchSize = 100,                 // Events per batch (0 = disable)
    BatchTimeout = TimeSpan.FromMilliseconds(500),

    // Resilience
    MaxRetries = 2,
    CircuitBreakerThreshold = 5,    // Failures before opening
    CircuitBreakerDuration = TimeSpan.FromSeconds(30),

    // Sampling
    SampleRate = 1.0,                // 0.0 to 1.0 (1.0 = 100%)

    // Observability
    EnableMetrics = true
};

Middleware Options

app.UsePulseURL(options =>
{
    // Paths to skip logging
    options.SkipPaths = new HashSet<string>
    {
        "/health",
        "/metrics",
        "/swagger"
    };

    // Custom labels for all events
    options.CustomLabels = new Dictionary<string, string>
    {
        ["app"] = "my-api",
        ["version"] = "v1.2.0",
        ["environment"] = "production"
    };

    // Service identification
    options.ServiceName = "my-api";      // Default: SERVICE_NAME env var or "unknown-service"
    options.Namespace = "production";     // Default: POD_NAMESPACE env var or "default"

    // What to capture
    options.CaptureClientIp = true;
    options.CaptureUserAgent = true;

    // Error handling
    options.LogErrors = false;            // Fire-and-forget by default
});

appsettings.json Configuration

{
  "PulseURL": {
    "ServiceUrl": "pulseurl-service:9090",
    "BufferSize": 1000,
    "BatchSize": 100,
    "SampleRate": 1.0,
    "EnableMetrics": true,
    "CircuitBreakerThreshold": 5,
    "CircuitBreakerDuration": "00:00:30"
  }
}

Then register with:

builder.Services.AddPulseURL(); // Reads from "PulseURL" section

Architecture

Client Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Your ASP.NET Core App                  β”‚
β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Middleware  │────────▢│  PulseURL Client  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                      β”‚              β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                           β”‚  Channel<Event>    β”‚   β”‚
β”‚                           β”‚  (Async Buffer)    β”‚   β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                      β”‚              β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                           β”‚  Background Worker β”‚   β”‚
β”‚                           β”‚  - Batching        β”‚   β”‚
β”‚                           β”‚  - Retry           β”‚   β”‚
β”‚                           β”‚  - Circuit Breaker β”‚   β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚ gRPC
                                        β–Ό
                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                            β”‚ PulseURL Service  β”‚
                            β”‚     :9090         β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Decisions

  1. Fire-and-Forget - Logging never blocks your application
  2. Circuit Breaker - Automatically stops trying when service is down
  3. Batching - Reduces gRPC overhead by 10x+ in high-throughput scenarios
  4. Metrics - Uses System.Diagnostics.Metrics (works with OpenTelemetry)
  5. Multi-Targeting - Single NuGet package works on .NET 6, 8, and 9

Observability

Built-in Stats Logging

The client automatically logs comprehensive stats every 30 seconds at Information level:

PulseURL stats: created=5000, queued=5000, sent=4739, dropped=261, in_queue=0

Metrics Explained:

  • created - Total events created (before sampling)
  • queued - Events accepted into the queue (TryWrite succeeded)
  • sent - Events successfully delivered to PulseURL
  • dropped - Events dropped due to buffer overflow
  • in_queue - Real-time count of events waiting in queue

Drop Warnings: When events are dropped due to buffer overflow, warnings are logged:

Channel dropped event (buffer_size=1000, url=/api/users/123)

This provides production-ready visibility into:

  • βœ… Event sampling effectiveness
  • βœ… Buffer overflow detection
  • βœ… Queue backpressure
  • βœ… Processing throughput

System.Diagnostics.Metrics

The client exposes standardized metrics via System.Diagnostics.Metrics (introduced in .NET 6) for integration with observability platforms like Prometheus, Grafana, Azure Monitor, Datadog, and more.

Why System.Diagnostics.Metrics?

  • βœ… Zero dependencies - Built into .NET 6+ runtime
  • βœ… OpenTelemetry compatible - Standard OTEL metric types
  • βœ… Efficient - Minimal overhead, designed for high-throughput scenarios
  • βœ… Vendor neutral - Works with any metrics backend

Available Metrics:

Metric Name Type Unit Description Tags
pulseurl.events.queued Counter {event} Total events queued for sending -
pulseurl.events.sent Counter {event} Total events successfully sent -
pulseurl.events.dropped Counter {event} Events dropped (buffer full) -
pulseurl.errors.total Counter {error} Total send errors exception.type
pulseurl.circuit_breaker.state_changes Counter {change} Circuit breaker transitions state (open/closed)
pulseurl.send.duration Histogram ms Send operation latency success (true/false)
pulseurl.queue.size ObservableGauge {event} Current queue depth -

Consuming Metrics:

using OpenTelemetry.Metrics;

var builder = WebApplication.CreateBuilder(args);

// Add OpenTelemetry with PulseURL metrics
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics.AddMeter("PulseURL.Client");  // Enable PulseURL metrics
        metrics.AddPrometheusExporter();      // Export to Prometheus
        // or
        metrics.AddOtlpExporter();            // Export via OTLP (Grafana, etc.)
    });
With Prometheus
// Add Prometheus endpoint
app.MapPrometheusScrapingEndpoint();

// PulseURL metrics automatically available at /metrics endpoint:
// pulseurl_events_queued_total 1234
// pulseurl_events_sent_total 1230
// pulseurl_events_dropped_total 4
// pulseurl_send_duration_ms_bucket{success="true",le="50"} 1150
Direct Listening (For Testing)
using System.Diagnostics.Metrics;

using var listener = new MeterListener();
listener.InstrumentPublished = (instrument, meterListener) =>
{
    if (instrument.Meter.Name == "PulseURL.Client")
    {
        meterListener.EnableMeasurementEvents(instrument);
    }
};

listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
    Console.WriteLine($"{instrument.Name}: {measurement}");
});

listener.Start();

Meter Name: PulseURL.Client Version: 0.1.0

Examples

C# Minimal API

See examples/BasicWebApi for a complete example with:

  • Health endpoints (skipped)
  • REST API endpoints (logged)
  • Route parameters
  • Custom labels
  • Error handling

F# Web API

See examples/FSharpWebApi (coming soon)

Performance

Benchmarks

  • Middleware Overhead: < 1ms per request
  • Throughput (single event): 10,000 req/sec
  • Throughput (batch mode): 50,000+ events/sec
  • Memory: Stable under load (~50MB for 10k events buffered)

Tips for High-Throughput Scenarios

  1. Enable Batching: Set BatchSize to 100-500
  2. Adjust Buffer: Increase BufferSize if you see drops
  3. Sampling: Use SampleRate < 1.0 to sample traffic
  4. Circuit Breaker: Protects your app when PulseURL is down

Troubleshooting

Events Not Appearing

  1. Check stats logs - Look for periodic stats in your application logs:

    PulseURL stats: created=100, queued=100, sent=0, dropped=0, in_queue=100
    
    • If sent=0, check PulseURL service health
    • If dropped > 0, increase BufferSize
    • If in_queue is growing, check service latency
  2. Check PulseURL service is running: curl http://pulseurl-service:9090/health

  3. Check circuit breaker state: client.IsCircuitBreakerOpen

  4. Check metrics: pulseurl.events.dropped and pulseurl.errors.total

  5. Enable error logging: options.LogErrors = true

High Memory Usage

  • Reduce BufferSize (default: 1000)
  • Lower SampleRate (e.g., 0.5 for 50%)
  • Ensure PulseURL service is healthy

Circuit Breaker Keeps Opening

  • PulseURL service may be down or slow
  • Check service logs
  • Increase CircuitBreakerThreshold or CircuitBreakerDuration

Development

Building

dotnet build

Testing

# Unit tests
dotnet test

# Integration tests (requires Kubernetes cluster)
cd tests/integration
./test-middleware.sh               # Run all tests
./test-middleware.sh baseline      # Run specific test
./test-middleware.sh buffer-overflow  # Test drop handling

# See docs/TESTING.md for detailed testing guide

Running the Example

cd examples/BasicWebApi
dotnet run

# In another terminal
curl http://localhost:5000/weatherforecast

Project Structure

pulseurl-dotnet/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ PulseURL.Client/           # Core gRPC client
β”‚   └── PulseURL.AspNetCore/       # ASP.NET Core middleware
β”œβ”€β”€ examples/
β”‚   β”œβ”€β”€ BasicWebApi/               # C# example
β”‚   └── FSharpWebApi/              # F# example (coming soon)
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ PulseURL.Client.Tests/
β”‚   └── PulseURL.AspNetCore.Tests/ (coming soon)
β”œβ”€β”€ proto/
β”‚   └── traffic.proto              # Protocol buffer definitions
└── docs/
    └── PLANNING.md                # Development roadmap

Roadmap

v0.1.0 (Current) βœ…

  • Core client with async buffering
  • Retry logic + circuit breaker
  • Batch streaming
  • Built-in stats logging and observability
  • Metrics (System.Diagnostics.Metrics)
  • ASP.NET Core middleware
  • Multi-targeting (.NET 6, 8, 9)
  • C# example
  • Integration tests with Kubernetes
  • F# example

v0.2.0 (Next)

  • F# example
  • Unit tests with full coverage
  • Performance benchmarks
  • NuGet package publishing

v2.0 (Future)

  • YARP middleware
  • SignalR hub filter
  • OpenTelemetry integration
  • Source generators for zero-allocation

Contributing

Contributions welcome! This is a learning project following the patterns from pulseurl-go.

License

MIT License - see LICENSE for details

Support

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 95 2/1/2026
0.2.0 103 1/25/2026
0.2.0-beta 100 1/25/2026
0.1.1-beta 96 1/25/2026