|
| 1 | +# Telemetry |
| 2 | + |
| 3 | +ExpressiveSharp emits runtime telemetry through the standard .NET diagnostics primitives — `ActivitySource` for distributed traces, `Meter` for metrics, and `EventSource` for previously-silent failure paths. All three share the source name `ExpressiveSharp` (also exposed as the constant `ExpressiveDiagnostics.SourceName`). |
| 4 | + |
| 5 | +Every instrument is **zero-cost when no listener is attached**: counters and activities short-circuit on the no-listener path, and the more expensive observations (node-counting, `MemberInfo.ToString()` for tags) are guarded explicitly. |
| 6 | + |
| 7 | +## Activity (Distributed Tracing) |
| 8 | + |
| 9 | +A single span is emitted from the `ExpressiveSharp` `ActivitySource` (the source's name is also exposed as the constant `ExpressiveDiagnostics.SourceName`): |
| 10 | + |
| 11 | +| Span | Emitted from | Tags | |
| 12 | +|------|--------------|------| |
| 13 | +| `Expressive.Expand` | `expression.ExpandExpressives()` (and the EF Core / MongoDB query compiler decorators that call into it) | `transformer.count`, `expansion.node_count`, `expansion.duration_ms` | |
| 14 | + |
| 15 | +Because the span runs synchronously inside the EF Core query pipeline, it nests cleanly under EF Core's `Microsoft.EntityFrameworkCore` activity. Trace viewers will show: |
| 16 | + |
| 17 | +``` |
| 18 | +EF query → Expressive.Expand → SQL execution |
| 19 | +``` |
| 20 | + |
| 21 | +### OpenTelemetry |
| 22 | + |
| 23 | +```csharp |
| 24 | +using var tracerProvider = Sdk.CreateTracerProviderBuilder() |
| 25 | + .AddSource("ExpressiveSharp") |
| 26 | + .AddSource("Microsoft.EntityFrameworkCore") // optional — nests Expressive.Expand under EF activities |
| 27 | + .AddOtlpExporter() |
| 28 | + .Build(); |
| 29 | +``` |
| 30 | + |
| 31 | +::: tip |
| 32 | +The `expansion.duration_ms` tag is recorded redundantly with the activity's intrinsic duration so consumers that only run a metrics pipeline (no tracer) still get the timing. |
| 33 | +::: |
| 34 | + |
| 35 | +## Meter (Metrics) |
| 36 | + |
| 37 | +Five instruments on `Meter("ExpressiveSharp")`: |
| 38 | + |
| 39 | +| Instrument | Type | Unit | Tags | What it tells you | |
| 40 | +|------------|------|------|------|-------------------| |
| 41 | +| `expressive.cache.hits` | Counter<long> | — | — | The runtime resolver cache served the lookup without computing it again. | |
| 42 | +| `expressive.cache.misses` | Counter<long> | — | — | The resolver had to compute the lambda for a member it hadn't seen before. | |
| 43 | +| `expressive.reflection_fallback.count` | Counter<long> | — | `member` | The slow reflection-based fallback ran. Used for open-generic class members and generic methods not in the static registry. **Sustained activity here is a perf bug.** | |
| 44 | +| `expressive.expansion.node_count` | Histogram<int> | nodes | — | Total expression-tree node count after expansion + transformers. Surfaces members that explode into massive trees and bloat SQL. | |
| 45 | +| `expressive.expansion.duration_ms` | Histogram<double> | ms | — | Wall-clock cost of one `ExpandExpressives()` call. | |
| 46 | + |
| 47 | +### Live monitoring with `dotnet-counters` |
| 48 | + |
| 49 | +```sh |
| 50 | +dotnet-counters monitor -n MyApp ExpressiveSharp |
| 51 | +``` |
| 52 | + |
| 53 | +### OpenTelemetry |
| 54 | + |
| 55 | +```csharp |
| 56 | +using var meterProvider = Sdk.CreateMeterProviderBuilder() |
| 57 | + .AddMeter("ExpressiveSharp") |
| 58 | + .AddOtlpExporter() |
| 59 | + .Build(); |
| 60 | +``` |
| 61 | + |
| 62 | +::: info |
| 63 | +A healthy steady-state has **`cache.hits` ≫ `cache.misses`** after warmup, and **`reflection_fallback.count` should be flat** unless your code uses open-generic `[Expressive]` members. A non-trivial slope on `reflection_fallback.count` means the runtime is paying reflection costs that the static registry could have served — please file an issue with a reproducer. |
| 64 | +::: |
| 65 | + |
| 66 | +## EventSource (Failure Diagnostics) |
| 67 | + |
| 68 | +Three events on `EventSource("ExpressiveSharp")`. They surface failure paths that ExpressiveSharp deliberately recovers from but that historically left users debugging by rubber duck. |
| 69 | + |
| 70 | +| Event ID | Name | Level | Payload | When it fires | |
| 71 | +|----------|------|-------|---------|---------------| |
| 72 | +| `1` | `RegistryInitializationFailed` | Error | `assemblyName`, `exceptionType`, `message` | An assembly's generated `ExpressionRegistry` static constructor threw. The registry is marked inert (no `[ExpressiveFor]` lookups will succeed against it) but the process keeps running. | |
| 73 | +| `2` | `HotReloadResetFailed` | Warning | `assemblyName`, `exceptionType`, `message` | A hot-reload edit-and-continue update arrived but invalidating the assembly's expression registry threw. The stale registry stays cached, so the next lookup may return pre-edit lambdas. | |
| 74 | +| `3` | `MultipleExpressiveForMappings` | Error | `memberInfoString`, `firstAssembly`, `secondAssembly` | Two different assemblies both registered an `[ExpressiveFor]` mapping for the same member. An `InvalidOperationException` is thrown immediately after; the event fires first so it survives even if the exception is later caught. | |
| 75 | + |
| 76 | +### Collecting with `dotnet-trace` |
| 77 | + |
| 78 | +The most common path. No code changes, no app restart: |
| 79 | + |
| 80 | +```sh |
| 81 | +dotnet-trace collect -n MyApp --providers ExpressiveSharp::Verbose |
| 82 | +``` |
| 83 | + |
| 84 | +Open the resulting `.nettrace` file in [PerfView](https://github.com/microsoft/perfview) or Visual Studio's diagnostic tools to inspect the events. |
| 85 | + |
| 86 | +## Stability |
| 87 | + |
| 88 | +The source name `ExpressiveSharp`, the instrument names, and the EventSource event IDs are part of the public API surface and follow the same stability rules as the rest of ExpressiveSharp. Tags carried on activities and counters may be extended (new tags added) but existing tags will not be renamed within a major version. |
0 commit comments