Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3d1abc6
feat(telemetry): add telemetry support for user agent parsing
BenjaminAbt Dec 27, 2025
26636f1
feat(telemetry): add telemetry support and documentation
BenjaminAbt Dec 27, 2025
770ece1
feat(telemetry): add native metrics support for user agent parser
BenjaminAbt Dec 27, 2025
081727e
feat(telemetry): enhance metrics and telemetry documentation
BenjaminAbt Dec 27, 2025
055ed40
feat(telemetry): enhance telemetry documentation and add meters support
BenjaminAbt Dec 27, 2025
3ac5975
refactor(telemetry): simplify method signatures and improve readability
BenjaminAbt Dec 27, 2025
44f030f
refactor(telemetry): remove DEBUG conditional compilation
BenjaminAbt Dec 27, 2025
1e97351
refactor(telemetry): update metric naming conventions
BenjaminAbt Jan 31, 2026
067b733
docs(copilot): add comprehensive instructions for Copilot usage
BenjaminAbt Jan 31, 2026
0478999
refactor(telemetry): update duration metrics to seconds
BenjaminAbt Feb 5, 2026
e79daff
refactor(telemetry): standardize meter names and descriptions
BenjaminAbt Feb 6, 2026
1a109d0
refactor(meters): simplify initialization logic in Enable method
BenjaminAbt Feb 6, 2026
58bf7c5
refactor(tests): standardize meter names in telemetry tests
BenjaminAbt Feb 6, 2026
6abf2bd
feat(telemetry): add fluent API for meter telemetry configuration
BenjaminAbt Feb 6, 2026
deb5cc7
feat(tests): enhance telemetry tests with EventCounter listener
BenjaminAbt Feb 13, 2026
d93adb6
feat(telemetry): ensure EventCounter logging is initialized
BenjaminAbt Feb 13, 2026
9b3f2a6
feat(telemetry): initialize EventSource in Enable method
BenjaminAbt Feb 13, 2026
462267a
feat(telemetry): ensure deterministic EventSource construction
BenjaminAbt Feb 13, 2026
6a1e03b
feat(telemetry): refactor meter name generation logic
BenjaminAbt Feb 19, 2026
f658e00
docs(license): update copyright year to 2026
BenjaminAbt Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 75 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,84 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "dotnet: restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": "$msCompile",
"presentation": {
"reveal": "silent",
"panel": "dedicated",
"close": true,
"showReuseMessage": false
}
},
{
"label": "dotnet: build",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"--no-restore"
],
"problemMatcher": "$msCompile",
"dependsOn": "dotnet: restore",
"presentation": {
"reveal": "always",
"panel": "dedicated",
"close": true,
"showReuseMessage": false
},
"group": "build"
},
{
"label": "dotnet: test",
"type": "shell",
"command": "dotnet",
"args": [
"test",
"--no-build",
"--nologo"
],
"problemMatcher": "$msCompile",
"dependsOn": "dotnet: build",
"presentation": {
"reveal": "always",
"panel": "dedicated",
"close": true,
"showReuseMessage": false
}
},
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"args": [],
"problemMatcher": [
"$msCompile"
],
"group": "build"
"isBackground": false
},
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"args": [],
"isBackground": false
},
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"args": [],
"isBackground": false
},
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"args": [],
"isBackground": false
}
]
}
179 changes: 178 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Parsing HTTP User Agents with .NET
| NuGet |
|-|
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) |
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` |
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCache` |
| [![MyCSharp.HttpUserAgentParser.AspNetCore](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.AspNetCore.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.AspNetCore)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) | `dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore` |


Expand Down Expand Up @@ -110,6 +110,183 @@ public void MyMethod(IHttpUserAgentParserAccessor parserAccessor)
}
```

## Telemetry (EventCounters)

Telemetry is **opt-in** and **modular per package**.

- Opt-in: no telemetry overhead unless you explicitly enable it.
- Modular: each package has its own `EventSource` name, so you can monitor only what you use.

### Enable telemetry (Fluent API)

Core parser telemetry:

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHttpUserAgentParser()
.WithTelemetry();
}
```

MemoryCache telemetry (in addition to core, optional):

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHttpUserAgentMemoryCachedParser()
.WithTelemetry() // core counters (optional)
.WithMemoryCacheTelemetry();
}
```

ASP.NET Core telemetry (header present/missing):

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHttpUserAgentMemoryCachedParser()
.AddHttpUserAgentParserAccessor()
.WithAspNetCoreTelemetry();
}
```

### EventSource names and counters

Core (`MyCSharp.HttpUserAgentParser`)

- `parse-requests`
- `parse-duration` (ms)
- `cache-concurrentdictionary-hit`
- `cache-concurrentdictionary-miss`
- `cache-concurrentdictionary-size`

MemoryCache (`MyCSharp.HttpUserAgentParser.MemoryCache`)

- `cache-hit`
- `cache-miss`
- `cache-size`

ASP.NET Core (`MyCSharp.HttpUserAgentParser.AspNetCore`)

- `useragent-present`
- `useragent-missing`

### Monitor counters

Using `dotnet-counters`:

```bash
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.MemoryCache
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.AspNetCore
```

### Export to OpenTelemetry

You can collect these EventCounters via OpenTelemetry metrics.

Packages you typically need:

- `OpenTelemetry`
- `OpenTelemetry.Instrumentation.EventCounters`
- an exporter (e.g. `OpenTelemetry.Exporter.OpenTelemetryProtocol`)

Example (minimal):

```csharp
using OpenTelemetry.Metrics;
using MyCSharp.HttpUserAgentParser.Telemetry;
using MyCSharp.HttpUserAgentParser.MemoryCache.Telemetry;
using MyCSharp.HttpUserAgentParser.AspNetCore.Telemetry;

builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddEventCountersInstrumentation(options =>
{
options.AddEventSources(
HttpUserAgentParserEventSource.EventSourceName,
HttpUserAgentParserMemoryCacheEventSource.EventSourceName,
HttpUserAgentParserAspNetCoreEventSource.EventSourceName);
})
.AddOtlpExporter();
});
```

### Export to Application Insights

Two common approaches:

1) OpenTelemetry → Application Insights (recommended)
- Collect counters with OpenTelemetry (see above)
- Export using an Azure Monitor / Application Insights exporter (API varies by package/version)

2) Custom `EventListener` → `TelemetryClient`
- Attach an `EventListener`
- Parse the `EventCounters` payload
- Forward values as custom metrics

### OpenTelemetry listener (recommended)

You can collect EventCounters as OpenTelemetry metrics.

Typical packages:

- `OpenTelemetry`
- `OpenTelemetry.Instrumentation.EventCounters`
- An exporter, e.g. `OpenTelemetry.Exporter.OpenTelemetryProtocol`

Example:

```csharp
using OpenTelemetry.Metrics;

builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddEventCountersInstrumentation(options =>
{
options.AddEventSources(
HttpUserAgentParserEventSource.EventSourceName,
HttpUserAgentParserMemoryCacheEventSource.EventSourceName,
HttpUserAgentParserAspNetCoreEventSource.EventSourceName);
})
.AddOtlpExporter();
});
```

From there you can route metrics to:

- OpenTelemetry Collector
- Prometheus
- Azure Monitor / Application Insights (via an Azure Monitor exporter)

### Application Insights listener (custom)

If you want a direct listener, you can attach an `EventListener` and forward counter values into Application Insights custom metrics.

High-level steps:

1) Enable telemetry via `.WithTelemetry()` / `.WithMemoryCacheTelemetry()` / `.WithAspNetCoreTelemetry()`
2) Register an `EventListener` that enables the corresponding EventSources
3) On `EventCounters` payload, forward values to `TelemetryClient.GetMetric(...).TrackValue(...)`

Notes:

- This is best-effort telemetry.
- Prefer longer polling intervals (e.g. 10s) to reduce noise.

> Notes
>
> - Counters are only emitted when telemetry is enabled and a listener is attached.
> - Values are best-effort and may include cache races.

## Benchmark

```shell
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright © https://myCSharp.de - all rights reserved

using System.Diagnostics.Metrics;
using MyCSharp.HttpUserAgentParser.AspNetCore.Telemetry;
using MyCSharp.HttpUserAgentParser.DependencyInjection;

namespace MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection;

/// <summary>
/// Fluent extensions to enable telemetry for the AspNetCore package.
/// </summary>
public static class HttpUserAgentParserDependencyInjectionOptionsTelemetryExtensions
{
/// <summary>
/// Enables EventCounter telemetry for the AspNetCore package.
/// </summary>
public static HttpUserAgentParserDependencyInjectionOptions WithAspNetCoreTelemetry(
this HttpUserAgentParserDependencyInjectionOptions options)
{
HttpUserAgentParserAspNetCoreTelemetry.Enable();
return options;
}

/// <summary>
/// Enables native System.Diagnostics.Metrics telemetry for the AspNetCore package.
/// </summary>
public static HttpUserAgentParserDependencyInjectionOptions WithAspNetCoreMeterTelemetry(
this HttpUserAgentParserDependencyInjectionOptions options,
Meter? meter = null)
{
HttpUserAgentParserAspNetCoreTelemetry.EnableMeters(meter);
return options;
}
}
13 changes: 13 additions & 0 deletions src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using MyCSharp.HttpUserAgentParser.AspNetCore.Telemetry;

namespace MyCSharp.HttpUserAgentParser.AspNetCore;

Expand All @@ -16,7 +17,19 @@ public static class HttpContextExtensions
public static string? GetUserAgentString(this HttpContext httpContext)
{
if (httpContext.Request.Headers.TryGetValue("User-Agent", out StringValues value))
{
if (HttpUserAgentParserAspNetCoreTelemetry.IsEnabled)
{
HttpUserAgentParserAspNetCoreTelemetry.UserAgentPresent();
}

return value;
}

if (HttpUserAgentParserAspNetCoreTelemetry.IsEnabled)
{
HttpUserAgentParserAspNetCoreTelemetry.UserAgentMissing();
}

return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore;
public class HttpUserAgentParserAccessor(IHttpUserAgentParserProvider httpUserAgentParser)
: IHttpUserAgentParserAccessor
{
/// <summary>
/// The name of the Meter used for metrics.
/// </summary>
public const string MeterName = "MyCSharp.HttpUserAgentParser.AspNetCore";

private readonly IHttpUserAgentParserProvider _httpUserAgentParser = httpUserAgentParser;

/// <summary>
Expand Down
Loading