Skip to content

Commit 6abf2bd

Browse files
committed
feat(telemetry): add fluent API for meter telemetry configuration
1 parent 58bf7c5 commit 6abf2bd

15 files changed

+501
-4
lines changed

README.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public void ConfigureServices(IServiceCollection services)
159159
Core (`MyCSharp.HttpUserAgentParser`)
160160

161161
- `parse-requests`
162-
- `parse-duration` (ms)
162+
- `parse-duration` (s)
163163
- `cache-concurrentdictionary-hit`
164164
- `cache-concurrentdictionary-miss`
165165
- `cache-concurrentdictionary-size`
@@ -185,6 +185,92 @@ dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.MemoryCa
185185
dotnet-counters monitor --process-id <pid> MyCSharp.HttpUserAgentParser.AspNetCore
186186
```
187187

188+
## Telemetry (Meters)
189+
190+
Native `System.Diagnostics.Metrics` instruments are **opt-in** per package.
191+
192+
### Enable meters (Fluent API)
193+
194+
Core parser meters:
195+
196+
```csharp
197+
public void ConfigureServices(IServiceCollection services)
198+
{
199+
services
200+
.AddHttpUserAgentParser()
201+
.WithMeterTelemetry();
202+
}
203+
```
204+
205+
MemoryCache meters:
206+
207+
```csharp
208+
public void ConfigureServices(IServiceCollection services)
209+
{
210+
services
211+
.AddHttpUserAgentMemoryCachedParser()
212+
.WithMeterTelemetry() // core meters (optional)
213+
.WithMemoryCacheMeterTelemetry();
214+
}
215+
```
216+
217+
ASP.NET Core meters:
218+
219+
```csharp
220+
public void ConfigureServices(IServiceCollection services)
221+
{
222+
services
223+
.AddHttpUserAgentMemoryCachedParser()
224+
.AddHttpUserAgentParserAccessor()
225+
.WithAspNetCoreMeterTelemetry();
226+
}
227+
```
228+
229+
### Meter names and instruments
230+
231+
Core meter (default: `mycsharp.http_user_agent_parser`)
232+
233+
- `parse.requests` (counter, `{call}`)
234+
- `parse.duration` (histogram, `s`)
235+
- `cache.hit` (counter, `{call}`)
236+
- `cache.miss` (counter, `{call}`)
237+
- `cache.size` (observable gauge, `{entry}`)
238+
239+
MemoryCache meter (default: `mycsharp.http_user_agent_parser.memorycache`)
240+
241+
- `cache.hit` (counter, `{call}`)
242+
- `cache.miss` (counter, `{call}`)
243+
- `cache.size` (observable gauge, `{entry}`)
244+
245+
ASP.NET Core meter (default: `mycsharp.http_user_agent_parser.aspnetcore`)
246+
247+
- `user_agent.present` (counter, `{call}`)
248+
- `user_agent.missing` (counter, `{call}`)
249+
250+
### Meter prefix configuration
251+
252+
The default prefix is `mycsharp.`. The prefix can be configured via DI:
253+
254+
```csharp
255+
public void ConfigureServices(IServiceCollection services)
256+
{
257+
services
258+
.AddHttpUserAgentParser()
259+
.WithMeterTelemetryPrefix("acme.");
260+
}
261+
```
262+
263+
Rules:
264+
265+
- `""` (empty) is allowed and removes the prefix.
266+
- Otherwise the prefix must be **alphanumeric** and **end with `.`**.
267+
268+
Example results:
269+
270+
- Prefix `"mycsharp."` -> `mycsharp.http_user_agent_parser`
271+
- Prefix `""` -> `http_user_agent_parser`
272+
- Prefix `"acme."` -> `acme.http_user_agent_parser`
273+
188274
### Export to OpenTelemetry
189275

190276
You can collect these EventCounters via OpenTelemetry metrics.

src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsTelemetryExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,19 @@ public static HttpUserAgentParserDependencyInjectionOptions WithAspNetCoreMeterT
3131
HttpUserAgentParserAspNetCoreTelemetry.EnableMeters(meter);
3232
return options;
3333
}
34+
35+
/// <summary>
36+
/// Enables native System.Diagnostics.Metrics telemetry for the AspNetCore package using a custom meter prefix.
37+
/// </summary>
38+
/// <param name="options">The options container.</param>
39+
/// <param name="meterPrefix">The prefix to use for the meter name.</param>
40+
/// <exception cref="ArgumentException">Thrown when the prefix is not empty and does not match the required format.</exception>
41+
public static HttpUserAgentParserDependencyInjectionOptions WithAspNetCoreMeterTelemetryPrefix(
42+
this HttpUserAgentParserDependencyInjectionOptions options,
43+
string meterPrefix)
44+
{
45+
Meter meter = new(HttpUserAgentParserAspNetCoreMeters.GetMeterName(meterPrefix));
46+
HttpUserAgentParserAspNetCoreTelemetry.EnableMeters(meter);
47+
return options;
48+
}
3449
}

src/HttpUserAgentParser.AspNetCore/HttpUserAgentParser.AspNetCore.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@
2424
<ProjectReference Include="..\HttpUserAgentParser\HttpUserAgentParser.csproj" />
2525
</ItemGroup>
2626

27+
<ItemGroup>
28+
<InternalsVisibleTo Include="MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests" PublicKey="$(PublicKey)" />
29+
</ItemGroup>
30+
2731
</Project>

src/HttpUserAgentParser.AspNetCore/Telemetry/HttpUserAgentParserAspNetCoreMeters.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,51 @@ internal static class HttpUserAgentParserAspNetCoreMeters
2020
/// <summary>
2121
/// Name of the meter used to publish AspNetCore User-Agent metrics.
2222
/// </summary>
23-
public const string MeterName = "mycsharp.http_user_agent_parser.aspnetcore";
23+
private const string MeterNameSuffix = "http_user_agent_parser.aspnetcore";
24+
25+
/// <summary>
26+
/// Name of the meter used to publish AspNetCore User-Agent metrics.
27+
/// </summary>
28+
public const string MeterName = "mycsharp." + MeterNameSuffix;
29+
30+
/// <summary>
31+
/// Builds a meter name from a custom prefix.
32+
/// </summary>
33+
/// <param name="meterPrefix">
34+
/// The prefix to use. When null, the default prefix is used. When empty,
35+
/// no prefix is applied. Otherwise, the prefix must be alphanumeric and end with '.'.
36+
/// </param>
37+
/// <returns>The full meter name.</returns>
38+
/// <exception cref="ArgumentException">Thrown when the prefix is not empty and does not match the required format.</exception>
39+
public static string GetMeterName(string? meterPrefix)
40+
{
41+
if (meterPrefix is null)
42+
{
43+
return MeterName;
44+
}
45+
46+
meterPrefix = meterPrefix.Trim();
47+
if (meterPrefix.Length == 0)
48+
{
49+
return MeterNameSuffix;
50+
}
51+
52+
if (!meterPrefix.EndsWith('.'))
53+
{
54+
throw new ArgumentException("Meter prefix must end with '.'.", nameof(meterPrefix));
55+
}
56+
57+
for (int i = 0; i < meterPrefix.Length - 1; i++)
58+
{
59+
char c = meterPrefix[i];
60+
if (!char.IsLetterOrDigit(c))
61+
{
62+
throw new ArgumentException("Meter prefix must be alphanumeric.", nameof(meterPrefix));
63+
}
64+
}
65+
66+
return meterPrefix + MeterNameSuffix;
67+
}
2468

2569
/// <summary>
2670
/// Indicates whether the meter and its instruments have been initialized.
@@ -91,4 +135,16 @@ public static void UserAgentPresent()
91135
[MethodImpl(MethodImplOptions.AggressiveInlining)]
92136
public static void UserAgentMissing()
93137
=> s_userAgentMissing?.Add(1);
138+
139+
/// <summary>
140+
/// Resets static state to support isolated unit tests.
141+
/// </summary>
142+
public static void ResetForTests()
143+
{
144+
Volatile.Write(ref s_initialized, 0);
145+
146+
s_meter = null;
147+
s_userAgentPresent = null;
148+
s_userAgentMissing = null;
149+
}
94150
}

src/HttpUserAgentParser.AspNetCore/Telemetry/HttpUserAgentParserAspNetCoreTelemetry.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,13 @@ public static void UserAgentMissing()
125125
HttpUserAgentParserAspNetCoreMeters.UserAgentMissing();
126126
}
127127
}
128+
129+
/// <summary>
130+
/// Resets telemetry state for unit tests.
131+
/// </summary>
132+
public static void ResetForTests()
133+
{
134+
Volatile.Write(ref s_enabledFlags, 0);
135+
HttpUserAgentParserAspNetCoreMeters.ResetForTests();
136+
}
128137
}

src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsTelemetryExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,19 @@ public static HttpUserAgentParserDependencyInjectionOptions WithMemoryCacheMeter
3131
HttpUserAgentParserMemoryCacheTelemetry.EnableMeters(meter);
3232
return options;
3333
}
34+
35+
/// <summary>
36+
/// Enables native System.Diagnostics.Metrics telemetry for the MemoryCache provider using a custom meter prefix.
37+
/// </summary>
38+
/// <param name="options">The options container.</param>
39+
/// <param name="meterPrefix">The prefix to use for the meter name.</param>
40+
/// <exception cref="ArgumentException">Thrown when the prefix is not empty and does not match the required format.</exception>
41+
public static HttpUserAgentParserDependencyInjectionOptions WithMemoryCacheMeterTelemetryPrefix(
42+
this HttpUserAgentParserDependencyInjectionOptions options,
43+
string meterPrefix)
44+
{
45+
Meter meter = new(HttpUserAgentParserMemoryCacheMeters.GetMeterName(meterPrefix));
46+
HttpUserAgentParserMemoryCacheTelemetry.EnableMeters(meter);
47+
return options;
48+
}
3449
}

src/HttpUserAgentParser.MemoryCache/HttpUserAgentParser.MemoryCache.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@
2525
<ProjectReference Include="..\HttpUserAgentParser\HttpUserAgentParser.csproj" />
2626
</ItemGroup>
2727

28+
<ItemGroup>
29+
<InternalsVisibleTo Include="MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests" PublicKey="$(PublicKey)" />
30+
</ItemGroup>
31+
2832
</Project>

src/HttpUserAgentParser.MemoryCache/Telemetry/HttpUserAgentParserMemoryCacheMeters.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,48 @@ namespace MyCSharp.HttpUserAgentParser.MemoryCache.Telemetry;
1212
[ExcludeFromCodeCoverage]
1313
internal static class HttpUserAgentParserMemoryCacheMeters
1414
{
15-
public const string MeterName = HttpUserAgentParserMemoryCachedProvider.MeterName;
15+
private const string MeterNameSuffix = "http_user_agent_parser.memorycache";
16+
17+
public const string MeterName = "mycsharp." + MeterNameSuffix;
18+
19+
/// <summary>
20+
/// Builds a meter name from a custom prefix.
21+
/// </summary>
22+
/// <param name="meterPrefix">
23+
/// The prefix to use. When null, the default prefix is used. When empty,
24+
/// no prefix is applied. Otherwise, the prefix must be alphanumeric and end with '.'.
25+
/// </param>
26+
/// <returns>The full meter name.</returns>
27+
/// <exception cref="ArgumentException">Thrown when the prefix is not empty and does not match the required format.</exception>
28+
public static string GetMeterName(string? meterPrefix)
29+
{
30+
if (meterPrefix is null)
31+
{
32+
return MeterName;
33+
}
34+
35+
meterPrefix = meterPrefix.Trim();
36+
if (meterPrefix.Length == 0)
37+
{
38+
return MeterNameSuffix;
39+
}
40+
41+
if (!meterPrefix.EndsWith('.'))
42+
{
43+
throw new ArgumentException("Meter prefix must end with '.'.", nameof(meterPrefix));
44+
}
45+
46+
for (int i = 0; i < meterPrefix.Length - 1; i++)
47+
{
48+
char c = meterPrefix[i];
49+
if (!char.IsLetterOrDigit(c))
50+
{
51+
throw new ArgumentException("Meter prefix must be alphanumeric.", nameof(meterPrefix));
52+
}
53+
}
54+
55+
return meterPrefix + MeterNameSuffix;
56+
}
1657

1758
private static int s_initialized;
1859

@@ -54,4 +95,14 @@ public static void Enable(Meter? meter = null)
5495

5596
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5697
public static void CacheMiss() => s_cacheMiss?.Add(1);
98+
99+
public static void ResetForTests()
100+
{
101+
Volatile.Write(ref s_initialized, 0);
102+
103+
s_meter = null;
104+
s_cacheHit = null;
105+
s_cacheMiss = null;
106+
s_cacheSize = null;
107+
}
57108
}

src/HttpUserAgentParser.MemoryCache/Telemetry/HttpUserAgentParserMemoryCacheTelemetry.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,14 @@ public static void CacheSizeIncrement()
120120
[MethodImpl(MethodImplOptions.AggressiveInlining)]
121121
public static void CacheSizeDecrement()
122122
=> HttpUserAgentParserMemoryCacheTelemetryState.CacheSizeDecrement();
123+
124+
/// <summary>
125+
/// Resets telemetry state for unit tests.
126+
/// </summary>
127+
public static void ResetForTests()
128+
{
129+
Volatile.Write(ref s_enabledFlags, 0);
130+
HttpUserAgentParserMemoryCacheTelemetryState.ResetForTests();
131+
HttpUserAgentParserMemoryCacheMeters.ResetForTests();
132+
}
123133
}

src/HttpUserAgentParser.MemoryCache/Telemetry/HttpUserAgentParserMemoryCacheTelemetryState.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ internal static class HttpUserAgentParserMemoryCacheTelemetryState
3939
/// Uses an atomic operation to remain safe in concurrent scenarios.
4040
/// </remarks>
4141
public static void CacheSizeDecrement() => Interlocked.Decrement(ref s_cacheSize);
42+
43+
/// <summary>
44+
/// Resets the cache size for unit tests.
45+
/// </summary>
46+
public static void ResetForTests() => Volatile.Write(ref s_cacheSize, 0);
4247
}

0 commit comments

Comments
 (0)