Skip to content

Commit 5e51d87

Browse files
committed
copilot feedback fixes
1 parent 4583a53 commit 5e51d87

4 files changed

Lines changed: 33 additions & 15 deletions

File tree

docs/reference/telemetry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Every instrument is **zero-cost when no listener is attached**: counters and act
66

77
## Activity (Distributed Tracing)
88

9-
A single span is emitted from `ExpressiveSharp.ActivitySource`:
9+
A single span is emitted from the `ExpressiveSharp` `ActivitySource` (the source's name is also exposed as the constant `ExpressiveDiagnostics.SourceName`):
1010

1111
| Span | Emitted from | Tags |
1212
|------|--------------|------|

docs/reference/troubleshooting.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,12 +324,17 @@ public double TotalWithTax => Total * (1 + TaxRate); // Total is inlined
324324

325325
### How do I see what ExpressiveSharp is doing at runtime?
326326

327-
Subscribe to the `ExpressiveSharp` `ActivitySource`, `Meter`, or `EventSource`. The metrics surface cache hit/miss ratios and per-expansion timings; the EventSource surfaces failure paths that ExpressiveSharp recovers from silently — registry static-ctor failures, hot-reload reset failures, and `[ExpressiveFor]` collisions.
328-
329-
The fastest path is `dotnet-trace`, which needs no code changes:
327+
ExpressiveSharp emits three independent signals on the `ExpressiveSharp` source name — each requires a different out-of-process tool:
330328

331329
```sh
330+
# Failure events (registry static-ctor failures, hot-reload reset failures, [ExpressiveFor] collisions)
332331
dotnet-trace collect -n MyApp --providers ExpressiveSharp::Verbose
332+
333+
# Metrics (cache hit/miss ratios, expansion timings, reflection-fallback rate)
334+
dotnet-counters monitor -n MyApp ExpressiveSharp
335+
336+
# Distributed tracing (the Expressive.Expand activity span) — wire via OpenTelemetry,
337+
# see the Telemetry reference for the AddSource("ExpressiveSharp") snippet.
333338
```
334339

335340
See [Telemetry](./telemetry) for the full instrument and event reference.

src/ExpressiveSharp/Diagnostics/ExpressiveEventSource.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ public void RegistryInitializationFailed(string assemblyName, string exceptionTy
2020
public void RegistryInitializationFailed(Assembly assembly, Exception exception)
2121
{
2222
if (!IsEnabled()) return;
23+
// The caller catches TypeInitializationException, but the actionable cause is the
24+
// static-ctor exception nested inside it — unwrap so consumers see the real failure.
25+
var actual = exception is TypeInitializationException && exception.InnerException is { } inner
26+
? inner : exception;
2327
RegistryInitializationFailed(
2428
assembly.GetName().Name ?? "<unknown>",
25-
exception.GetType().FullName ?? exception.GetType().Name,
26-
exception.Message);
29+
actual.GetType().FullName ?? actual.GetType().Name,
30+
actual.Message);
2731
}
2832

2933
[Event(2, Level = EventLevel.Warning,

src/ExpressiveSharp/Services/ExpressiveResolver.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,23 @@ internal static void SetAssemblyScanFilter(Func<Assembly, bool>? filter)
106106
public LambdaExpression FindGeneratedExpression(MemberInfo expressiveMemberInfo,
107107
ExpressiveAttribute? expressiveAttribute = null)
108108
{
109-
if (ExpressiveDiagnostics.CacheHits.Enabled || ExpressiveDiagnostics.CacheMisses.Enabled)
109+
// Hits are counted on the fast path; misses are counted inside the factory so that
110+
// concurrent racing TryGetValue calls don't both pre-increment when only one factory
111+
// actually runs. ConcurrentDictionary may still invoke the factory multiple times
112+
// under contention, which can over-count by a tiny margin — acceptable for an
113+
// observability counter.
114+
if (ExpressiveDiagnostics.CacheHits.Enabled
115+
&& _expressionCache.TryGetValue(expressiveMemberInfo, out var cached))
110116
{
111-
if (_expressionCache.TryGetValue(expressiveMemberInfo, out var cached))
112-
{
113-
ExpressiveDiagnostics.CacheHits.Add(1);
114-
return cached;
115-
}
116-
ExpressiveDiagnostics.CacheMisses.Add(1);
117+
ExpressiveDiagnostics.CacheHits.Add(1);
118+
return cached;
117119
}
118120

119-
return _expressionCache.GetOrAdd(expressiveMemberInfo,
120-
static (mi, _) => ResolveExpressionCore(mi), (object?)null);
121+
return _expressionCache.GetOrAdd(expressiveMemberInfo, static (mi, _) =>
122+
{
123+
ExpressiveDiagnostics.CacheMisses.Add(1);
124+
return ResolveExpressionCore(mi);
125+
}, (object?)null);
121126
}
122127

123128
/// <inheritdoc/>
@@ -256,6 +261,10 @@ private static LambdaExpression ResolveExpressionCore(MemberInfo expressiveMembe
256261
/// </summary>
257262
public static LambdaExpression? FindGeneratedExpressionViaReflection(MemberInfo expressiveMemberInfo)
258263
{
264+
// ConcurrentDictionary.GetOrAdd may invoke the factory more than once under
265+
// contention, which can over-count the counter by a small margin on cold concurrent
266+
// lookups. Acceptable for an observability signal — the alternative (Lazy<T> wrapper)
267+
// adds allocation on every lookup.
259268
var result = _reflectionCache.GetOrAdd(expressiveMemberInfo, static mi =>
260269
{
261270
var built = BuildReflectionExpression(mi);

0 commit comments

Comments
 (0)