Skip to content

Commit 26636f1

Browse files
committed
feat(telemetry): add telemetry support and documentation
1 parent 3d1abc6 commit 26636f1

File tree

11 files changed

+630
-82
lines changed

11 files changed

+630
-82
lines changed

.vscode/tasks.json

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,84 @@
11
{
2-
"version": "2.0.0",
3-
"tasks": [
4-
{
5-
"label": "dotnet: restore",
6-
"type": "shell",
7-
"command": "dotnet",
8-
"args": ["restore"],
9-
"problemMatcher": "$msCompile",
10-
"presentation": {
11-
"reveal": "silent",
12-
"panel": "dedicated",
13-
"close": true,
14-
"showReuseMessage": false
15-
}
16-
},
17-
{
18-
"label": "dotnet: build",
19-
"type": "shell",
20-
"command": "dotnet",
21-
"args": ["build", "--no-restore"],
22-
"problemMatcher": "$msCompile",
23-
"dependsOn": "dotnet: restore",
24-
"presentation": {
25-
"reveal": "always",
26-
"panel": "dedicated",
27-
"close": true,
28-
"showReuseMessage": false
29-
},
30-
"group": "build"
31-
},
32-
{
33-
"label": "dotnet: test",
34-
"type": "shell",
35-
"command": "dotnet",
36-
"args": [
37-
"test",
38-
"--no-build",
39-
"--nologo"
40-
],
41-
"problemMatcher": "$msCompile",
42-
"dependsOn": "dotnet: build",
43-
"presentation": {
44-
"reveal": "always",
45-
"panel": "dedicated",
46-
"close": true,
47-
"showReuseMessage": false
48-
}
49-
}
50-
]
51-
}
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "dotnet: restore",
6+
"type": "shell",
7+
"command": "dotnet",
8+
"args": [
9+
"restore"
10+
],
11+
"problemMatcher": "$msCompile",
12+
"presentation": {
13+
"reveal": "silent",
14+
"panel": "dedicated",
15+
"close": true,
16+
"showReuseMessage": false
17+
}
18+
},
19+
{
20+
"label": "dotnet: build",
21+
"type": "shell",
22+
"command": "dotnet",
23+
"args": [
24+
"build",
25+
"--no-restore"
26+
],
27+
"problemMatcher": "$msCompile",
28+
"dependsOn": "dotnet: restore",
29+
"presentation": {
30+
"reveal": "always",
31+
"panel": "dedicated",
32+
"close": true,
33+
"showReuseMessage": false
34+
},
35+
"group": "build"
36+
},
37+
{
38+
"label": "dotnet: test",
39+
"type": "shell",
40+
"command": "dotnet",
41+
"args": [
42+
"test",
43+
"--no-build",
44+
"--nologo"
45+
],
46+
"problemMatcher": "$msCompile",
47+
"dependsOn": "dotnet: build",
48+
"presentation": {
49+
"reveal": "always",
50+
"panel": "dedicated",
51+
"close": true,
52+
"showReuseMessage": false
53+
}
54+
},
55+
{
56+
"label": "test",
57+
"type": "shell",
58+
"command": "dotnet test --nologo",
59+
"args": [],
60+
"isBackground": false
61+
},
62+
{
63+
"label": "test",
64+
"type": "shell",
65+
"command": "dotnet test --nologo",
66+
"args": [],
67+
"isBackground": false
68+
},
69+
{
70+
"label": "test",
71+
"type": "shell",
72+
"command": "dotnet test --nologo",
73+
"args": [],
74+
"isBackground": false
75+
},
76+
{
77+
"label": "test",
78+
"type": "shell",
79+
"command": "dotnet test --nologo",
80+
"args": [],
81+
"isBackground": false
82+
}
83+
]
84+
}

README.md

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Parsing HTTP User Agents with .NET
77
| NuGet |
88
|-|
99
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) |
10-
| [![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` |
10+
| [![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` |
1111
| [![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` |
1212

1313

@@ -110,6 +110,183 @@ public void MyMethod(IHttpUserAgentParserAccessor parserAccessor)
110110
}
111111
```
112112

113+
## Telemetry (EventCounters)
114+
115+
Telemetry is **opt-in** and **modular per package**.
116+
117+
- Opt-in: no telemetry overhead unless you explicitly enable it.
118+
- Modular: each package has its own `EventSource` name, so you can monitor only what you use.
119+
120+
### Enable telemetry (Fluent API)
121+
122+
Core parser telemetry:
123+
124+
```csharp
125+
public void ConfigureServices(IServiceCollection services)
126+
{
127+
services
128+
.AddHttpUserAgentParser()
129+
.WithTelemetry();
130+
}
131+
```
132+
133+
MemoryCache telemetry (in addition to core, optional):
134+
135+
```csharp
136+
public void ConfigureServices(IServiceCollection services)
137+
{
138+
services
139+
.AddHttpUserAgentMemoryCachedParser()
140+
.WithTelemetry() // core counters (optional)
141+
.WithMemoryCacheTelemetry();
142+
}
143+
```
144+
145+
ASP.NET Core telemetry (header present/missing):
146+
147+
```csharp
148+
public void ConfigureServices(IServiceCollection services)
149+
{
150+
services
151+
.AddHttpUserAgentMemoryCachedParser()
152+
.AddHttpUserAgentParserAccessor()
153+
.WithAspNetCoreTelemetry();
154+
}
155+
```
156+
157+
### EventSource names and counters
158+
159+
Core (`MyCSharp.HttpUserAgentParser`)
160+
161+
- `parse-requests`
162+
- `parse-duration` (ms)
163+
- `cache-concurrentdictionary-hit`
164+
- `cache-concurrentdictionary-miss`
165+
- `cache-concurrentdictionary-size`
166+
167+
MemoryCache (`MyCSharp.HttpUserAgentParser.MemoryCache`)
168+
169+
- `cache-hit`
170+
- `cache-miss`
171+
- `cache-size`
172+
173+
ASP.NET Core (`MyCSharp.HttpUserAgentParser.AspNetCore`)
174+
175+
- `useragent-present`
176+
- `useragent-missing`
177+
178+
### Monitor counters
179+
180+
Using `dotnet-counters`:
181+
182+
```bash
183+
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser
184+
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.MemoryCache
185+
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.AspNetCore
186+
```
187+
188+
### Export to OpenTelemetry
189+
190+
You can collect these EventCounters via OpenTelemetry metrics.
191+
192+
Packages you typically need:
193+
194+
- `OpenTelemetry`
195+
- `OpenTelemetry.Instrumentation.EventCounters`
196+
- an exporter (e.g. `OpenTelemetry.Exporter.OpenTelemetryProtocol`)
197+
198+
Example (minimal):
199+
200+
```csharp
201+
using OpenTelemetry.Metrics;
202+
using MyCSharp.HttpUserAgentParser.Telemetry;
203+
using MyCSharp.HttpUserAgentParser.MemoryCache.Telemetry;
204+
using MyCSharp.HttpUserAgentParser.AspNetCore.Telemetry;
205+
206+
builder.Services.AddOpenTelemetry()
207+
.WithMetrics(metrics =>
208+
{
209+
metrics
210+
.AddEventCountersInstrumentation(options =>
211+
{
212+
options.AddEventSources(
213+
HttpUserAgentParserEventSource.EventSourceName,
214+
HttpUserAgentParserMemoryCacheEventSource.EventSourceName,
215+
HttpUserAgentParserAspNetCoreEventSource.EventSourceName);
216+
})
217+
.AddOtlpExporter();
218+
});
219+
```
220+
221+
### Export to Application Insights
222+
223+
Two common approaches:
224+
225+
1) OpenTelemetry → Application Insights (recommended)
226+
- Collect counters with OpenTelemetry (see above)
227+
- Export using an Azure Monitor / Application Insights exporter (API varies by package/version)
228+
229+
2) Custom `EventListener``TelemetryClient`
230+
- Attach an `EventListener`
231+
- Parse the `EventCounters` payload
232+
- Forward values as custom metrics
233+
234+
### OpenTelemetry listener (recommended)
235+
236+
You can collect EventCounters as OpenTelemetry metrics.
237+
238+
Typical packages:
239+
240+
- `OpenTelemetry`
241+
- `OpenTelemetry.Instrumentation.EventCounters`
242+
- An exporter, e.g. `OpenTelemetry.Exporter.OpenTelemetryProtocol`
243+
244+
Example:
245+
246+
```csharp
247+
using OpenTelemetry.Metrics;
248+
249+
builder.Services.AddOpenTelemetry()
250+
.WithMetrics(metrics =>
251+
{
252+
metrics
253+
.AddEventCountersInstrumentation(options =>
254+
{
255+
options.AddEventSources(
256+
HttpUserAgentParserEventSource.EventSourceName,
257+
HttpUserAgentParserMemoryCacheEventSource.EventSourceName,
258+
HttpUserAgentParserAspNetCoreEventSource.EventSourceName);
259+
})
260+
.AddOtlpExporter();
261+
});
262+
```
263+
264+
From there you can route metrics to:
265+
266+
- OpenTelemetry Collector
267+
- Prometheus
268+
- Azure Monitor / Application Insights (via an Azure Monitor exporter)
269+
270+
### Application Insights listener (custom)
271+
272+
If you want a direct listener, you can attach an `EventListener` and forward counter values into Application Insights custom metrics.
273+
274+
High-level steps:
275+
276+
1) Enable telemetry via `.WithTelemetry()` / `.WithMemoryCacheTelemetry()` / `.WithAspNetCoreTelemetry()`
277+
2) Register an `EventListener` that enables the corresponding EventSources
278+
3) On `EventCounters` payload, forward values to `TelemetryClient.GetMetric(...).TrackValue(...)`
279+
280+
Notes:
281+
282+
- This is best-effort telemetry.
283+
- Prefer longer polling intervals (e.g. 10s) to reduce noise.
284+
285+
> Notes
286+
>
287+
> - Counters are only emitted when telemetry is enabled and a listener is attached.
288+
> - Values are best-effort and may include cache races.
289+
113290
## Benchmark
114291

115292
```shell

src/HttpUserAgentParser.AspNetCore/Telemetry/HttpUserAgentParserAspNetCoreEventSource.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore.Telemetry;
88
/// <summary>
99
/// EventSource for EventCounters emitted by MyCSharp.HttpUserAgentParser.AspNetCore.
1010
/// </summary>
11-
[EventSource(Name = "MyCSharp.HttpUserAgentParser.AspNetCore")]
11+
[EventSource(Name = EventSourceName)]
1212
[ExcludeFromCodeCoverage]
13-
internal sealed class HttpUserAgentParserAspNetCoreEventSource : EventSource
13+
public sealed class HttpUserAgentParserAspNetCoreEventSource : EventSource
1414
{
15-
public static readonly HttpUserAgentParserAspNetCoreEventSource Log = new();
15+
/// <summary>
16+
/// The EventSource name used for EventCounters.
17+
/// </summary>
18+
public const string EventSourceName = "MyCSharp.HttpUserAgentParser.AspNetCore";
19+
20+
internal static HttpUserAgentParserAspNetCoreEventSource Log { get; } = new();
1621

1722
private readonly IncrementingEventCounter _userAgentPresent;
1823
private readonly IncrementingEventCounter _userAgentMissing;
@@ -33,19 +38,20 @@ private HttpUserAgentParserAspNetCoreEventSource()
3338
}
3439

3540
[NonEvent]
36-
public void UserAgentPresent()
41+
internal void UserAgentPresent()
3742
{
3843
if (!IsEnabled()) return;
3944
_userAgentPresent?.Increment();
4045
}
4146

4247
[NonEvent]
43-
public void UserAgentMissing()
48+
internal void UserAgentMissing()
4449
{
4550
if (!IsEnabled()) return;
4651
_userAgentMissing?.Increment();
4752
}
4853

54+
/// <inheritdoc />
4955
protected override void Dispose(bool disposing)
5056
{
5157
if (disposing)

0 commit comments

Comments
 (0)