├── .github └── workflows │ ├── publish-nuget-packages.yaml │ └── run-tests.yaml ├── .gitignore ├── LICENSE.txt ├── README.md ├── TODO.txt ├── docs ├── grafana-example.PNG ├── metrics-exposed-3.1.md ├── metrics-exposed-5.0.md └── metrics-exposed.md ├── examples ├── AspNetCoreExample │ ├── AspNetCoreExample.csproj │ ├── Controllers │ │ ├── CollectorController.cs │ │ ├── NoOverheadController.cs │ │ └── SimulateController.cs │ ├── Dockerfile │ ├── Options.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── docker-compose.yml ├── grafana │ └── provisioning │ │ ├── dashboards │ │ ├── Debugging_metrics.json │ │ ├── NET_runtime_metrics_dashboard.json │ │ └── dashboard.yml │ │ └── datasources │ │ └── datasource.yml └── prometheus │ └── prometheus.yml ├── prometheus-net.DotNetRuntime.sln ├── src ├── Benchmarks │ ├── Benchmarks.csproj │ ├── Benchmarks │ │ ├── AspNetBenchmarkBase.cs │ │ ├── BaselineBenchmark.cs │ │ ├── DefaultBenchmark.cs │ │ ├── DictBenchmark.cs │ │ ├── DotNetRuntimeStatsBenchmarkBase.cs │ │ ├── EventCounterParserBenchmark.cs │ │ ├── PrometheusMTBenchmark.cs │ │ ├── PrometheusSTBenchmark.cs │ │ └── ReactiveBenchmark.cs │ ├── Controllers │ │ └── BenchmarkController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── appsettings.json ├── Common.csproj ├── prometheus-net.DotNetRuntime.Tests │ ├── DotNetRuntimeStatsBuilderTests.cs │ ├── DotNetRuntimeStatsCollectorTests.cs │ ├── EventListening │ │ ├── EventCounterParserBaseTests.cs │ │ ├── EventParserTypes.cs │ │ ├── IEventParserTests.cs │ │ ├── Parsers │ │ │ ├── EventListenerIntegrationTestBase.cs │ │ │ ├── SystemRuntimeCounterTests.cs │ │ │ └── Util │ │ │ │ ├── EventPairTimerTests.cs │ │ │ │ └── SamplingRateTests.cs │ │ └── TestHelpers.cs │ ├── ExtensionTests.cs │ ├── IntegrationTests │ │ ├── ContentionTests.cs │ │ ├── ExceptionTests.cs │ │ ├── GcTests.cs │ │ ├── Helpers.cs │ │ ├── IntegrationTestBase.cs │ │ ├── JitCompilerTests.cs │ │ ├── SocketsTests.cs │ │ └── ThreadPoolTests.cs │ ├── Metrics │ │ └── Producers │ │ │ └── Util │ │ │ ├── LabelGeneratorTests.cs │ │ │ ├── RatioTests.cs │ │ │ └── StringExtensionsTests.cs │ ├── Properties.cs │ └── prometheus-net.DotNetRuntime.Tests.csproj └── prometheus-net.DotNetRuntime │ ├── CaptureLevel.cs │ ├── DotNetRuntimeStatsBuilder.cs │ ├── DotNetRuntimeStatsCollector.cs │ ├── EventConsumption.cs │ ├── EventListening │ ├── CounterNameAttribute.cs │ ├── Counters.cs │ ├── DotNetEventListener.cs │ ├── EventCounterParserBase.cs │ ├── EventParserTypes.cs │ ├── IEventCounterListener.cs │ ├── IEventCounterParser.cs │ ├── IEventListener.cs │ ├── IEventParser.cs │ ├── IEvents.cs │ ├── Parsers │ │ ├── ContentionEventParser.cs │ │ ├── ExceptionEventParser.cs │ │ ├── GcEventParser.cs │ │ ├── JitEventParser.cs │ │ ├── RuntimeEventParser.cs │ │ ├── SocketsEventParser.cs │ │ ├── ThreadPoolEventParser.cs │ │ └── Util │ │ │ ├── Cache.cs │ │ │ ├── EventExtensions.cs │ │ │ ├── EventPairTimer.cs │ │ │ └── SamplingRate.cs │ └── Sources │ │ ├── DotNetRuntimeEventSource.cs │ │ ├── FrameworkEventSource.cs │ │ └── SystemRuntimeEventSource.cs │ ├── Extensions.cs │ ├── ListenerRegistration.cs │ ├── Metrics │ ├── IMetricProducer.cs │ ├── MetricExtensions.cs │ └── Producers │ │ ├── ContentionMetricsProducer.cs │ │ ├── ExceptionMetricsProducer.cs │ │ ├── GcMetricsProducer.cs │ │ ├── JitMetricsProducer.cs │ │ ├── SocketsMetricProducer.cs │ │ ├── ThreadPoolMetricsProducer.cs │ │ └── Util │ │ ├── Constants.cs │ │ ├── LabelGenerator.cs │ │ ├── Ratio.cs │ │ └── StringExtensions.cs │ ├── Properties.cs │ ├── SampleRate.cs │ └── prometheus-net.DotNetRuntime.csproj └── tools └── DocsGenerator ├── DocsGenerator.csproj ├── Program.cs ├── README.md └── XmlDocReading.cs /.github/workflows/publish-nuget-packages.yaml: -------------------------------------------------------------------------------- 1 | name: publish-nuget-packages 2 | 3 | on: 4 | release: 5 | types: [published, prereleased] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-dotnet@v1 13 | with: 14 | dotnet-version: '6.0.x' 15 | - run: | 16 | echo "Github ref is ${GITHUB_REF}" 17 | arrTag=(${GITHUB_REF//\// }) 18 | VERSION="${arrTag[2]}" 19 | echo "Version: $VERSION" 20 | dotnet pack src/prometheus-net.DotNetRuntime --include-symbols -c "Release" -p:Version=$VERSION --output "build/" 21 | dotnet nuget push "build/prometheus-net.DotNetRuntime.*.symbols.nupkg" -k ${{ secrets.NUGET_API_KEY }} -s "https://api.nuget.org/v3/index.json" -n true 22 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yaml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup .NET Core 3.1 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 3.1.x 16 | - name: Setup .NET Core 5.0 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.x 20 | - name: Setup .NET Core 6.0 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: '6.0.x' 24 | include-prerelease: true 25 | - uses: actions/checkout@v1 26 | # This test constantly passes localy (windows + linux) but fails in the test environment. Don't have the time/ inclination to figure out why this is right now.. 27 | - run: dotnet test -c "Debug" --filter Name!=When_blocking_work_is_executed_on_the_thread_pool_then_thread_pool_delays_are_measured 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | build/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Oo]ut/ 29 | msbuild.log 30 | msbuild.err 31 | msbuild.wrn 32 | bin/ 33 | obj/ 34 | 35 | # Visual Studio 2015 36 | .vs/ 37 | 38 | # Rider 39 | .idea/ 40 | BenchmarkDotNet.Artifacts/ 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prometheus-net.DotNetMetrics 2 | A plugin for the [prometheus-net](https://github.com/prometheus-net/prometheus-net) package, [exposing .NET core runtime metrics](docs/metrics-exposed.md) including: 3 | - Garbage collection collection frequencies and timings by generation/ type, pause timings and GC CPU consumption ratio 4 | - Heap size by generation 5 | - Bytes allocated by small/ large object heap 6 | - JIT compilations and JIT CPU consumption ratio 7 | - Thread pool size, scheduling delays and reasons for growing/ shrinking 8 | - Lock contention 9 | - Exceptions thrown, broken down by type 10 | 11 | These metrics are essential for understanding the performance of any non-trivial application. Even if your application is well instrumented, you're only getting half the story- what the runtime is doing completes the picture. 12 | 13 | ## Using this package 14 | ### Requirements 15 | - .NET 5.0+ recommended, .NET core 3.1+ is supported 16 | - The [prometheus-net](https://github.com/prometheus-net/prometheus-net) package 17 | 18 | ### Install it 19 | The package can be installed from [nuget](https://www.nuget.org/packages/prometheus-net.DotNetRuntime): 20 | ```powershell 21 | dotnet add package prometheus-net.DotNetRuntime 22 | ``` 23 | 24 | ### Start collecting metrics 25 | You can start metric collection with: 26 | ```csharp 27 | IDisposable collector = DotNetRuntimeStatsBuilder.Default().StartCollecting() 28 | ``` 29 | 30 | You can customize the types of .NET metrics collected via the `Customize` method: 31 | ```csharp 32 | IDisposable collector = DotNetRuntimeStatsBuilder 33 | .Customize() 34 | .WithContentionStats() 35 | .WithJitStats() 36 | .WithThreadPoolStats() 37 | .WithGcStats() 38 | .WithExceptionStats() 39 | .StartCollecting(); 40 | ``` 41 | 42 | Once the collector is registered, you should see metrics prefixed with `dotnet_` visible in your metric output (make sure you are [exporting your metrics](https://github.com/prometheus-net/prometheus-net#http-handler)). 43 | 44 | ### Choosing a `CaptureLevel` 45 | By default the library will default generate metrics based on [event counters](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/event-counters). This allows for basic instrumentation of applications with very little performance overhead. 46 | 47 | You can enable higher-fidelity metrics by providing a custom `CaptureLevel`, e.g: 48 | ``` 49 | DotNetRuntimeStatsBuilder 50 | .Customize() 51 | .WithGcStats(CaptureLevel.Informational) 52 | .WithExceptionStats(CaptureLevel.Errors) 53 | ... 54 | ``` 55 | 56 | Most builder methods allow the passing of a custom `CaptureLevel`- see the [documentation on exposed metrics](docs/metrics-exposed.md) for more information. 57 | 58 | ### Performance impact of `CaptureLevel.Errors`+ 59 | The harder you work the .NET core runtime, the more events it generates. Event generation and processing costs can stack up, especially around these types of events: 60 | - **JIT stats**: each method compiled by the JIT compiler emits two events. Most JIT compilation is performed at startup and depending on the size of your application, this could impact your startup performance. 61 | - **GC stats with `CaptureLevel.Verbose`**: every 100KB of allocations, an event is emitted. If you are consistently allocating memory at a rate > 1GB/sec, you might like to disable GC stats. 62 | - **Exception stats with `CaptureLevel.Errors`**: for every exception throw, an event is generated. 63 | 64 | #### Recycling collectors 65 | There have been long-running [performance issues since .NET core 3.1](https://github.com/dotnet/runtime/issues/43985#issuecomment-800629516) that could see CPU consumption grow over time when long-running trace sessions are used. 66 | While many of the performance issues have been addressed now in .NET 6.0, a workaround was identified: stopping and starting (AKA recycling) collectors periodically helped reduce CPU consumption: 67 | ``` 68 | IDisposable collector = DotNetRuntimeStatsBuilder.Default() 69 | // Recycles all collectors once every day 70 | .RecycleCollectorsEvery(TimeSpan.FromDays(1)) 71 | .StartCollecting() 72 | ``` 73 | 74 | While this [has been observed to reduce CPU consumption](https://github.com/djluck/prometheus-net.DotNetRuntime/issues/6#issuecomment-784540220) this technique has been identified as a [possible culprit that can lead 75 | to application instability](https://github.com/djluck/prometheus-net.DotNetRuntime/issues/72). 76 | 77 | Behaviour on different runtime versions is: 78 | - .NET core 3.1: recycling verified to cause massive instability, cannot enable recycling. 79 | - .NET 5.0: recycling verified to be beneficial, recycling every day enabled by default. 80 | - .NET 6.0+: recycling verified to be less necesarry due to long-standing issues being addressed although [some users report recycling to be beneficial](https://github.com/djluck/prometheus-net.DotNetRuntime/pull/73#issuecomment-1308558226), 81 | disabled by default but recycling can be enabled. 82 | 83 | > TLDR: If you observe increasing CPU over time, try enabling recycling. If you see unexpected crashes after using this application, try disabling recycling. 84 | 85 | 86 | ## Examples 87 | An example `docker-compose` stack is available in the [`examples/`](examples/) folder. Start it with: 88 | 89 | ``` 90 | docker-compose up -d 91 | ``` 92 | 93 | You can then visit [`http://localhost:3000`](http://localhost:3000) to view metrics being generated by a sample application. 94 | 95 | ### Grafana dashboard 96 | The metrics exposed can drive a rich dashboard, giving you a graphical insight into the performance of your application ( [exported dashboard available here](examples/grafana/provisioning/dashboards/NET_runtime_metrics_dashboard.json)): 97 | 98 | ![Grafana dashboard sample](docs/grafana-example.PNG) 99 | 100 | ## Further reading 101 | - The mechanism for listening to runtime events is outlined in the [.NET core 2.2 release notes](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-2-2#core). 102 | - A partial list of core CLR events is available in the [ETW events documentation](https://docs.microsoft.com/en-us/dotnet/framework/performance/clr-etw-events). -------------------------------------------------------------------------------- /docs/grafana-example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djluck/prometheus-net.DotNetRuntime/ccafba9c677aabebd081267e651893a0c2e5849b/docs/grafana-example.PNG -------------------------------------------------------------------------------- /docs/metrics-exposed.md: -------------------------------------------------------------------------------- 1 | # Metrics exposed 2 | 3 | The documents below catalog the metrics exposed, depending on the .NET runtime version an application is using: 4 | - [.NET 5.0](./metrics-exposed-5.0.md) 5 | - [.NET core 3.1](./metrics-exposed-3.1.md) 6 | -------------------------------------------------------------------------------- /examples/AspNetCoreExample/AspNetCoreExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net5.0 4 | 8 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Controllers/CollectorController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Net; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace AspNetCoreExample.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class CollectorController : ControllerBase 16 | { 17 | // GET api/values 18 | [HttpGet] 19 | [Route("enable")] 20 | public async Task Enable() 21 | { 22 | 23 | if (Startup.Collector != null) 24 | return new JsonResult(new { Status = "Failed - already enabled"}) { StatusCode = (int)HttpStatusCode.InternalServerError}; 25 | 26 | Startup.Collector = Startup.CreateCollector(); 27 | 28 | return new JsonResult(new { Status = "Ok- started and assigned collector"}); 29 | } 30 | 31 | [HttpGet] 32 | [Route("disable")] 33 | public async Task Disable() 34 | { 35 | if (Startup.Collector == null) 36 | return new JsonResult(new { Status = "Failed - already disable"}) { StatusCode = (int)HttpStatusCode.InternalServerError}; 37 | 38 | Startup.Collector.Dispose(); 39 | Startup.Collector = null; 40 | 41 | return new JsonResult(new { Status = "Ok- stopped the collector"}); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Controllers/NoOverheadController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace AspNetCoreExample.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class NoOverheadController : ControllerBase 14 | { 15 | [HttpGet] 16 | public async Task>> Get2() 17 | { 18 | return new string[] {"value1", "value2"}; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Controllers/SimulateController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace AspNetCoreExample.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class SimulateController : ControllerBase 15 | { 16 | private readonly IHttpClientFactory _httpClientFactory; 17 | 18 | public SimulateController(IHttpClientFactory httpClientFactory) 19 | { 20 | _httpClientFactory = httpClientFactory; 21 | } 22 | 23 | [HttpGet] 24 | public async Task>> Get( 25 | bool simulateAlloc = true, 26 | bool simulateJit = true, 27 | bool simulateException = true, 28 | bool simulateBlocking = false, 29 | bool simulateOutgoingNetwork = true) 30 | { 31 | var r = new Random(); 32 | if (simulateAlloc) 33 | { 34 | // assign some SOH memory 35 | var x = new byte[r.Next(1024, 1024 * 64)]; 36 | 37 | // assign some LOH memory 38 | x = new byte[r.Next(1024 * 90, 1024 * 100)]; 39 | } 40 | 41 | // await a task (will result in a Task being scheduled on the thread pool) 42 | await Task.Yield(); 43 | 44 | if (simulateJit) 45 | { 46 | var val = r.Next(); 47 | CompileMe(() => val); 48 | } 49 | 50 | if (simulateException) 51 | { 52 | try 53 | { 54 | var divide = 0; 55 | var result = 1 / divide; 56 | } 57 | catch 58 | { 59 | } 60 | } 61 | 62 | if (simulateBlocking) 63 | { 64 | Thread.Sleep(100); 65 | } 66 | 67 | if (simulateOutgoingNetwork) 68 | { 69 | using var client = _httpClientFactory.CreateClient(); 70 | using var _ = await client.GetAsync("https://httpstat.us/200"); 71 | } 72 | 73 | return new string[] {"value1" + r.Next(), "value2"+ r.Next()}; 74 | } 75 | 76 | private void CompileMe(Expression> func) 77 | { 78 | func.Compile()(); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM mcr.microsoft.com/dotnet/core/sdk:3.1.406 AS build 2 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 3 | WORKDIR /src 4 | COPY . . 5 | RUN dotnet publish "examples/AspNetCoreExample" -c Release -o /app 6 | 7 | #FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.3 AS final 8 | #FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.10 AS final 9 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 as final 10 | WORKDIR /app 11 | COPY --from=build /app /app 12 | ENTRYPOINT ["dotnet", "AspNetCoreExample.dll"] 13 | -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Options.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AspNetCoreExample 4 | { 5 | public class Options 6 | { 7 | public bool EnableMetrics { get; set; } = true; 8 | public bool UseDefaultMetrics { get; set; } = false; 9 | public bool UseDebuggingMetrics { get; set; } = false; 10 | public TimeSpan RecycleEvery { get; set; } = TimeSpan.FromDays(1); 11 | public int? MinThreadPoolSize { get; set; } = null; 12 | } 13 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Prometheus.DotNetRuntime; 6 | 7 | namespace AspNetCoreExample 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateWebHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 17 | WebHost.CreateDefaultBuilder(args) 18 | .ConfigureAppConfiguration(opts => 19 | { 20 | opts.AddEnvironmentVariables("Example"); 21 | }) 22 | .ConfigureKestrel(opts => 23 | { 24 | opts.AllowSynchronousIO = true; 25 | }) 26 | .UseStartup(); 27 | } 28 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "AspNetCoreExample": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "launchUrl": "metrics", 8 | "applicationUrl": "http://localhost:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "Example__UseDebuggingMetrics": "true", 12 | "Example__UseDefaultMetrics": "true" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Tracing; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.HttpsPolicy; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | using Prometheus; 16 | using Prometheus.DotNetRuntime; 17 | 18 | namespace AspNetCoreExample 19 | { 20 | public class Startup 21 | { 22 | private static Options _options; 23 | public static IDisposable Collector; 24 | private static ILogger _logger; 25 | 26 | public Startup(IConfiguration configuration, ILogger logger) 27 | { 28 | Configuration = configuration; 29 | 30 | _options = new Options(); 31 | _logger = logger; 32 | configuration.Bind("Example", _options); 33 | 34 | if (_options.EnableMetrics) 35 | { 36 | Collector = CreateCollector(); 37 | } 38 | else 39 | logger.LogWarning($"prometheus-net.DotNetRuntime was NOT started- {_options.EnableMetrics} was set to false"); 40 | 41 | if (_options.MinThreadPoolSize.HasValue) 42 | { 43 | logger.LogInformation($"Setting minimum thread pool size of {_options.MinThreadPoolSize.Value}"); 44 | ThreadPool.SetMinThreads(_options.MinThreadPoolSize.Value, 1); 45 | } 46 | } 47 | 48 | public IConfiguration Configuration { get; } 49 | 50 | // This method gets called by the runtime. Use this method to add services to the container. 51 | public void ConfigureServices(IServiceCollection services) 52 | { 53 | services.AddMvc(); 54 | services.AddHttpClient(); 55 | } 56 | 57 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 58 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 59 | { 60 | if (env.IsDevelopment()) 61 | { 62 | app.UseDeveloperExceptionPage(); 63 | } 64 | 65 | app.UseRouting(); 66 | 67 | app.UseEndpoints(endpoints => 68 | { 69 | // Mapping of endpoints goes here: 70 | endpoints.MapControllers(); 71 | }); 72 | 73 | app.UseMetricServer(); 74 | } 75 | 76 | public static IDisposable CreateCollector() 77 | { 78 | _logger.LogInformation($"Configuring prometheus-net.DotNetRuntime: will recycle event listeners every {_options.RecycleEvery} ({_options.RecycleEvery.TotalSeconds:N0} seconds)."); 79 | 80 | var builder = DotNetRuntimeStatsBuilder.Default(); 81 | 82 | if (!_options.UseDefaultMetrics) 83 | { 84 | builder = DotNetRuntimeStatsBuilder.Customize() 85 | .WithContentionStats(CaptureLevel.Informational) 86 | .WithGcStats(CaptureLevel.Verbose) 87 | .WithThreadPoolStats(CaptureLevel.Informational) 88 | .WithExceptionStats(CaptureLevel.Errors) 89 | .WithJitStats(); 90 | } 91 | 92 | builder 93 | #if NET5_0_OR_GREATER 94 | .RecycleCollectorsEvery(_options.RecycleEvery) 95 | #endif 96 | .WithErrorHandler(ex => _logger.LogError(ex, "Unexpected exception occurred in prometheus-net.DotNetRuntime")); 97 | 98 | if (_options.UseDebuggingMetrics) 99 | { 100 | _logger.LogInformation("Using debugging metrics."); 101 | builder.WithDebuggingMetrics(true); 102 | } 103 | 104 | _logger.LogInformation("Starting prometheus-net.DotNetRuntime..."); 105 | 106 | return builder 107 | .StartCollecting(); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /examples/AspNetCoreExample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "System": "Warning", 6 | "Microsoft": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/AspNetCoreExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "AspNetCoreExample" : "Information" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # With thanks to https://github.com/stefanprodan/dockprom for this excellent template for providing prom + grafana 2 | version: '2.1' 3 | 4 | networks: 5 | monitor-net: 6 | driver: bridge 7 | 8 | volumes: 9 | prometheus_data: {} 10 | grafana_data: {} 11 | 12 | services: 13 | 14 | prometheus: 15 | image: prom/prometheus:v2.22.0 16 | container_name: prometheus 17 | volumes: 18 | - ./prometheus:/etc/prometheus 19 | - prometheus_data:/prometheus 20 | command: 21 | - '--config.file=/etc/prometheus/prometheus.yml' 22 | - '--storage.tsdb.path=/prometheus' 23 | - '--web.console.libraries=/etc/prometheus/console_libraries' 24 | - '--web.console.templates=/etc/prometheus/consoles' 25 | - '--storage.tsdb.retention.time=200h' 26 | - '--web.enable-lifecycle' 27 | restart: unless-stopped 28 | expose: 29 | - 9090 30 | ports: 31 | - 9090:9090 32 | networks: 33 | - monitor-net 34 | labels: 35 | org.label-schema.group: "monitoring" 36 | 37 | grafana: 38 | image: grafana/grafana:7.3.1 39 | container_name: grafana 40 | volumes: 41 | - grafana_data:/var/lib/grafana 42 | - ./grafana/provisioning:/etc/grafana/provisioning 43 | environment: 44 | - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin} 45 | - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} 46 | - GF_USERS_ALLOW_SIGN_UP=false 47 | - GF_AUTH_ANONYMOUS_ENABLED=true 48 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Editor 49 | restart: unless-stopped 50 | ports: 51 | - 3000:3000 52 | networks: 53 | - monitor-net 54 | labels: 55 | org.label-schema.group: "monitoring" 56 | 57 | aspexample: 58 | build: 59 | context: ../ 60 | dockerfile: examples/AspNetCoreExample/Dockerfile 61 | expose: 62 | - 5000 63 | environment: 64 | - ASPNETCORE_URLS=http://+:5000 65 | # Additional vars that can be set to tweak behaviour 66 | - Example__UseDefaultMetrics=true 67 | #- Example__EnableMetrics=false 68 | #- Example__UseDebuggingMetrics=true 69 | #- Example__RecycleEvery=00:10:00 70 | #- Example__MinThreadPoolSize=100 71 | ports: 72 | - 5001:5000 73 | mem_limit: "200M" 74 | networks: 75 | - monitor-net 76 | 77 | bombardier: 78 | image: alpine/bombardier 79 | command: -c 25 -d 1000h -r 100 -t 15s http://aspexample:5000/api/simulate 80 | # High intensity 81 | # command: -c 1000 -d 1000h -r 2000 -t 10s http://aspexample:5000/api/simulate 82 | networks: 83 | - monitor-net -------------------------------------------------------------------------------- /examples/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | allowUiUpdates: true 11 | options: 12 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /examples/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | orgId: 1 8 | url: http://prometheus:9090 9 | basicAuth: false 10 | isDefault: true 11 | editable: true -------------------------------------------------------------------------------- /examples/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | 6 | # A scrape configuration containing exactly one endpoint to scrape. 7 | scrape_configs: 8 | - job_name: 'dotnetruntime' 9 | scrape_interval: 5s 10 | static_configs: 11 | - targets: ['aspexample:5000', 'host.docker.internal:5000'] 12 | 13 | 14 | - job_name: 'prometheus' 15 | scrape_interval: 10s 16 | static_configs: 17 | - targets: ['localhost:9090'] 18 | 19 | -------------------------------------------------------------------------------- /prometheus-net.DotNetRuntime.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "prometheus-net.DotNetRuntime", "src\prometheus-net.DotNetRuntime\prometheus-net.DotNetRuntime.csproj", "{A40AD08A-53CB-40F3-A6D8-6FFCEC024289}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "prometheus-net.DotNetRuntime.Tests", "src\prometheus-net.DotNetRuntime.Tests\prometheus-net.DotNetRuntime.Tests.csproj", "{7F4E2E72-5745-4312-B238-CD7B731957B0}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{31AD912F-A1DC-434A-8C8D-049F4BBD67D4}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExample", "examples\AspNetCoreExample\AspNetCoreExample.csproj", "{D01E9ED3-E35C-4F44-A5AD-5350E43AA636}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "src\Benchmarks\Benchmarks.csproj", "{DD607E45-45AD-4F9D-9102-82BD99E49BEC}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{D8594A14-5AC8-40F3-B346-38A266B235E0}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocsGenerator", "tools\DocsGenerator\DocsGenerator.csproj", "{193B461A-49E4-4178-B88C-BA0EF6B4FC55}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Release|Any CPU = Release|Any CPU 20 | Debug|Any CPU = Debug|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {D01E9ED3-E35C-4F44-A5AD-5350E43AA636}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D01E9ED3-E35C-4F44-A5AD-5350E43AA636}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {D01E9ED3-E35C-4F44-A5AD-5350E43AA636}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {D01E9ED3-E35C-4F44-A5AD-5350E43AA636}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {DD607E45-45AD-4F9D-9102-82BD99E49BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {DD607E45-45AD-4F9D-9102-82BD99E49BEC}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {DD607E45-45AD-4F9D-9102-82BD99E49BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {DD607E45-45AD-4F9D-9102-82BD99E49BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {A40AD08A-53CB-40F3-A6D8-6FFCEC024289}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {A40AD08A-53CB-40F3-A6D8-6FFCEC024289}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {A40AD08A-53CB-40F3-A6D8-6FFCEC024289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {A40AD08A-53CB-40F3-A6D8-6FFCEC024289}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {7F4E2E72-5745-4312-B238-CD7B731957B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {7F4E2E72-5745-4312-B238-CD7B731957B0}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {7F4E2E72-5745-4312-B238-CD7B731957B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {7F4E2E72-5745-4312-B238-CD7B731957B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {193B461A-49E4-4178-B88C-BA0EF6B4FC55}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {193B461A-49E4-4178-B88C-BA0EF6B4FC55}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {193B461A-49E4-4178-B88C-BA0EF6B4FC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {193B461A-49E4-4178-B88C-BA0EF6B4FC55}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {D01E9ED3-E35C-4F44-A5AD-5350E43AA636} = {31AD912F-A1DC-434A-8C8D-049F4BBD67D4} 46 | {193B461A-49E4-4178-B88C-BA0EF6B4FC55} = {D8594A14-5AC8-40F3-B346-38A266B235E0} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netcoreapp3.0 6 | InProcess 7 | true 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | true 34 | PreserveNewest 35 | Never 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/AspNetBenchmarkBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using BenchmarkDotNet.Attributes; 8 | using Microsoft.AspNetCore; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Prometheus.DotNetRuntime; 11 | 12 | namespace Benchmarks.Benchmarks 13 | { 14 | [BenchmarkCategory("aspnet")] 15 | public abstract class AspNetBenchmarkBase 16 | { 17 | public const int NumRequests = 10_000; 18 | 19 | private IWebHost webHost; 20 | private CancellationTokenSource ctSource = new CancellationTokenSource(); 21 | private HttpClient client; 22 | private byte[][] buffers; 23 | private Task webHostTask; 24 | 25 | public int NumHttpConnections { get; set; } = 50; 26 | 27 | [GlobalSetup] 28 | public void GlobalSetup() 29 | { 30 | PreGlobalSetup(); 31 | webHost = WebHost.CreateDefaultBuilder() 32 | .UseStartup() 33 | .ConfigureKestrel(cfg => { cfg.ListenLocalhost(5000); }) 34 | .Build(); 35 | webHostTask = webHost.RunAsync(ctSource.Token); 36 | 37 | // preallocate buffers to avoid having them counted as part of each benchmark run 38 | buffers = new byte[NumHttpConnections][]; 39 | for (int i = 0; i < buffers.Length; i++) 40 | buffers[i] = new byte[1024 * 64]; 41 | 42 | client = new HttpClient(new SocketsHttpHandler() { MaxConnectionsPerServer = NumHttpConnections }); 43 | } 44 | 45 | protected virtual void PreGlobalSetup() 46 | { 47 | } 48 | 49 | protected virtual void PostGlobalCleanup() 50 | { 51 | } 52 | 53 | [GlobalCleanup] 54 | public void GlobalCleanup() 55 | { 56 | ctSource.Cancel(); 57 | client.Dispose(); 58 | webHostTask.Wait(); 59 | PostGlobalCleanup(); 60 | } 61 | 62 | protected async Task MakeHttpRequests() 63 | { 64 | var requestsRemaining = NumRequests; 65 | var tasks = Enumerable.Range(0, NumHttpConnections) 66 | .Select(async n => 67 | { 68 | var buffer = buffers[n]; 69 | while (Interlocked.Decrement(ref requestsRemaining) > 0) 70 | { 71 | await using var response = await client.GetStreamAsync("http://localhost:5000/api/benchmark"); 72 | while (await response.ReadAsync(buffer, 0, buffer.Length) > 0) 73 | { 74 | } 75 | } 76 | }); 77 | 78 | await Task.WhenAll(tasks); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/BaselineBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using Prometheus.DotNetRuntime; 5 | 6 | namespace Benchmarks.Benchmarks 7 | { 8 | public class BaselineBenchmark : AspNetBenchmarkBase 9 | { 10 | [Benchmark(Description = "No stats collectors enabled", Baseline = true, OperationsPerInvoke = NumRequests)] 11 | public async Task Make_Requests() 12 | { 13 | await MakeHttpRequests(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/DefaultBenchmark.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime; 2 | 3 | namespace Benchmarks.Benchmarks 4 | { 5 | public class DefaultBenchmark : DotNetRuntimeStatsBenchmarkBase 6 | { 7 | protected override DotNetRuntimeStatsBuilder.Builder GetStatsBuilder() 8 | { 9 | return DotNetRuntimeStatsBuilder.Default(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/DictBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Reactive; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using System.Reactive.Subjects; 8 | using System.Threading; 9 | using System.Threading.Channels; 10 | using BenchmarkDotNet.Attributes; 11 | 12 | namespace Benchmarks.Benchmarks 13 | { 14 | public class DictBenchmark 15 | { 16 | private Dictionary _dict ; 17 | private ConcurrentDictionary _connDict; 18 | 19 | public DictBenchmark() 20 | { 21 | } 22 | 23 | [IterationSetup] 24 | public void Setup() 25 | { 26 | _dict = new Dictionary(12000); 27 | _connDict = new (concurrencyLevel: Environment.ProcessorCount, 20000); 28 | 29 | } 30 | 31 | [Benchmark] 32 | public void AddToDict() 33 | { 34 | for (int i = 0; i < 10000; i++) 35 | _dict.Add(i, "test value"); 36 | } 37 | 38 | [Benchmark] 39 | public void AddToConcurrentDict() 40 | { 41 | for (int i = 0; i < 10000; i++) 42 | _connDict.TryAdd(i, "test value"); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/DotNetRuntimeStatsBenchmarkBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using BenchmarkDotNet.Attributes; 5 | using Prometheus.DotNetRuntime; 6 | 7 | namespace Benchmarks.Benchmarks 8 | { 9 | public abstract class DotNetRuntimeStatsBenchmarkBase : AspNetBenchmarkBase 10 | { 11 | private IDisposable collector; 12 | 13 | protected override void PreGlobalSetup() 14 | { 15 | collector = GetStatsBuilder().StartCollecting(); 16 | } 17 | 18 | protected override void PostGlobalCleanup() 19 | { 20 | collector.Dispose(); 21 | } 22 | 23 | [Benchmark(Baseline = false, OperationsPerInvoke = NumRequests)] 24 | public async Task Make_Requests() 25 | { 26 | await MakeHttpRequests(); 27 | } 28 | 29 | protected abstract DotNetRuntimeStatsBuilder.Builder GetStatsBuilder(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/EventCounterParserBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Diagnostics.Tracing; 4 | using System.Linq; 5 | using BenchmarkDotNet.Attributes; 6 | using Fasterflect; 7 | using Prometheus.DotNetRuntime.EventListening; 8 | 9 | namespace Benchmarks.Benchmarks 10 | { 11 | public class EventCounterParserBenchmark 12 | { 13 | private EventWrittenEventArgs _meanValue; 14 | private EventWrittenEventArgs _incrCounter; 15 | private DummyTypeEventCounterParser _parser; 16 | 17 | public EventCounterParserBenchmark() 18 | { 19 | _meanValue = CreateCounterEventWrittenEventArgs( 20 | ("Name", "test-mean-counter"), 21 | ("DisplayName", "some value"), 22 | ("Mean", 5.0), 23 | ("StandardDeviation", 1), 24 | ("Count", 1), 25 | ("Min", 1), 26 | ("Max", 1), 27 | ("IntervalSec", 1), 28 | ("Series", 1), 29 | ("CounterType", "Mean"), 30 | ("Metadata", ""), 31 | ("DisplayUnits", "") 32 | ); 33 | 34 | _incrCounter = CreateCounterEventWrittenEventArgs( 35 | ("Name", "test-incrementing-counter"), 36 | ("DisplayName", "some value"), 37 | ("DisplayRateTimeScale", TimeSpan.FromSeconds(10)), 38 | ("Increment", 6.0), 39 | ("IntervalSec", 1), 40 | ("Series", 1), 41 | ("CounterType", "Sum"), 42 | ("Metadata", ""), 43 | ("DisplayUnits", "") 44 | ); 45 | 46 | _parser = new DummyTypeEventCounterParser(); 47 | var total = 0.0; 48 | _parser.TestIncrementingCounter += e => total += e.IncrementedBy; 49 | 50 | var last = 0.0; 51 | _parser.TestMeanCounter += e => last = e.Mean; 52 | } 53 | 54 | [Benchmark] 55 | public void ParseIncrementingCounter() 56 | { 57 | _parser.ProcessEvent(_incrCounter); 58 | } 59 | 60 | [Benchmark] 61 | public void ParseMeanCounter() 62 | { 63 | _parser.ProcessEvent(_meanValue); 64 | } 65 | 66 | public static EventWrittenEventArgs CreateEventWrittenEventArgs(int eventId, DateTime? timestamp = null, params object[] payload) 67 | { 68 | var args = (EventWrittenEventArgs)typeof(EventWrittenEventArgs).CreateInstance(new []{ typeof(EventSource)}, Flags.NonPublic | Flags.Instance, new object[] { null}); 69 | args.SetPropertyValue("EventId", eventId); 70 | args.SetPropertyValue("Payload", new ReadOnlyCollection(payload)); 71 | 72 | if (timestamp.HasValue) 73 | { 74 | args.SetPropertyValue("TimeStamp", timestamp.Value); 75 | } 76 | 77 | return args; 78 | } 79 | 80 | public static EventWrittenEventArgs CreateCounterEventWrittenEventArgs(params (string key, object val)[] payload) 81 | { 82 | var counterPayload = payload.ToDictionary(k => k.key, v => v.val); 83 | 84 | var e = CreateEventWrittenEventArgs(-1, DateTime.UtcNow, new[] { counterPayload }); 85 | e.SetPropertyValue("EventName", "EventCounters"); 86 | return e; 87 | } 88 | 89 | public interface TestCounters : ICounterEvents 90 | { 91 | #pragma disable warning 92 | public event Action TestIncrementingCounter; 93 | public event Action TestMeanCounter; 94 | #pragma warning restore 95 | } 96 | 97 | public class DummyTypeEventCounterParser : EventCounterParserBase, TestCounters 98 | { 99 | public override string EventSourceName { get; } 100 | public override EventKeywords Keywords { get; } 101 | public override int RefreshIntervalSeconds { get; set; } 102 | 103 | [CounterName("test-incrementing-counter")] 104 | public event Action TestIncrementingCounter; 105 | [CounterName("test-mean-counter")] 106 | public event Action TestMeanCounter; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/PrometheusMTBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Reports; 7 | using Prometheus; 8 | 9 | namespace Benchmarks.Benchmarks 10 | { 11 | public class PrometheusMTBenchmark 12 | { 13 | private Counter _counter = Metrics.CreateCounter("test_counter", ""); 14 | private Counter _counterLabelled1 = Metrics.CreateCounter("test_counter_labelled_1", "", "label1"); 15 | private Counter _counterLabelled2 = Metrics.CreateCounter("test_counter_labelled_2", "", "label1", "label2"); 16 | private Histogram _histogram = Metrics.CreateHistogram("test_histo", ""); 17 | private Histogram _histogramLabelled1 = Metrics.CreateHistogram("test_histo_labeled1", "", "label1"); 18 | private Histogram _histogramLabelled2 = Metrics.CreateHistogram("test_histo_labeled2", "", "label1", "label2"); 19 | private CancellationTokenSource _ctSource; 20 | private Task[] _tasks; 21 | 22 | [GlobalSetup] 23 | public void GlobalSetup() 24 | { 25 | _ctSource = new CancellationTokenSource(); 26 | _tasks = Enumerable.Range(1, Environment.ProcessorCount - 4) 27 | .Select(async _ => 28 | { 29 | while (!_ctSource.IsCancellationRequested) 30 | { 31 | for (int i = 0; i < 500_000; i++) 32 | { 33 | IncrementUnlabeled(); 34 | IncrementLabeled1(); 35 | IncrementUnlabeled2(); 36 | ObserveUnlabeled(); 37 | ObserveLabeled1(); 38 | ObserveUnlabeled2(); 39 | ObserveHighUnlabeled(); 40 | ObserveHighLabeled1(); 41 | ObserveHighUnlabeled2(); 42 | } 43 | 44 | await Task.Delay(1); 45 | } 46 | }) 47 | .ToArray(); 48 | } 49 | 50 | [GlobalCleanup] 51 | public void GlobalCleanup() 52 | { 53 | _ctSource.Cancel(); 54 | } 55 | 56 | [Benchmark] 57 | public void IncrementUnlabeled() 58 | { 59 | _counter.Inc(1); 60 | } 61 | 62 | [Benchmark] 63 | public void IncrementLabeled1() 64 | { 65 | _counterLabelled1.WithLabels("test_label1").Inc(1); 66 | } 67 | 68 | [Benchmark] 69 | public void IncrementUnlabeled2() 70 | { 71 | _counterLabelled2.WithLabels("test_label1", "test_label2").Inc(1); 72 | } 73 | 74 | [Benchmark] 75 | public void ObserveUnlabeled() 76 | { 77 | _histogram.Observe(0); 78 | } 79 | 80 | [Benchmark] 81 | public void ObserveLabeled1() 82 | { 83 | _histogramLabelled1.WithLabels("test_label1").Observe(0); 84 | } 85 | 86 | [Benchmark] 87 | public void ObserveUnlabeled2() 88 | { 89 | _histogramLabelled2.WithLabels("test_label1", "test_label2").Observe(0); 90 | } 91 | 92 | [Benchmark] 93 | public void ObserveHighUnlabeled() 94 | { 95 | _histogram.Observe(int.MaxValue); 96 | } 97 | 98 | [Benchmark] 99 | public void ObserveHighLabeled1() 100 | { 101 | _histogramLabelled1.WithLabels("test_label1").Observe(int.MaxValue); 102 | } 103 | 104 | [Benchmark] 105 | public void ObserveHighUnlabeled2() 106 | { 107 | _histogramLabelled2.WithLabels("test_label1", "test_label2").Observe(int.MaxValue); 108 | } 109 | 110 | } 111 | } -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks/PrometheusSTBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Reports; 3 | using Prometheus; 4 | 5 | namespace Benchmarks.Benchmarks 6 | { 7 | public class PrometheusSTBenchmark 8 | { 9 | private Counter _counter = Metrics.CreateCounter("test_counter", ""); 10 | private Counter _counterLabelled1 = Metrics.CreateCounter("test_counter_labelled_1", "", "label1"); 11 | private Counter _counterLabelled2 = Metrics.CreateCounter("test_counter_labelled_2", "", "label1", "label2"); 12 | private Histogram _histogram = Metrics.CreateHistogram("test_histo", ""); 13 | private Histogram _histogramLabelled1 = Metrics.CreateHistogram("test_histo_labeled1", "", "label1"); 14 | private Histogram _histogramLabelled2 = Metrics.CreateHistogram("test_histo_labeled2", "", "label1", "label2"); 15 | 16 | [Benchmark] 17 | public void IncrementUnlabeled() 18 | { 19 | _counter.Inc(1); 20 | } 21 | 22 | [Benchmark] 23 | public void IncrementLabeled1() 24 | { 25 | _counterLabelled1.WithLabels("test_label1").Inc(1); 26 | } 27 | 28 | [Benchmark] 29 | public void IncrementUnlabeled2() 30 | { 31 | _counterLabelled2.WithLabels("test_label1", "test_label2").Inc(1); 32 | } 33 | 34 | [Benchmark] 35 | public void ObserveUnlabeled() 36 | { 37 | _histogram.Observe(0); 38 | } 39 | 40 | [Benchmark] 41 | public void ObserveLabeled1() 42 | { 43 | _histogramLabelled1.WithLabels("test_label1").Observe(0); 44 | } 45 | 46 | [Benchmark] 47 | public void ObserveUnlabeled2() 48 | { 49 | _histogramLabelled2.WithLabels("test_label1", "test_label2").Observe(0); 50 | } 51 | 52 | [Benchmark] 53 | public void ObserveHighUnlabeled() 54 | { 55 | _histogram.Observe(int.MaxValue); 56 | } 57 | 58 | [Benchmark] 59 | public void ObserveHighLabeled1() 60 | { 61 | _histogramLabelled1.WithLabels("test_label1").Observe(int.MaxValue); 62 | } 63 | 64 | [Benchmark] 65 | public void ObserveHighUnlabeled2() 66 | { 67 | _histogramLabelled2.WithLabels("test_label1", "test_label2").Observe(int.MaxValue); 68 | } 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /src/Benchmarks/Controllers/BenchmarkController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace Benchmarks.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class BenchmarkController : ControllerBase 14 | { 15 | [HttpGet] 16 | public async Task>> Get() 17 | { 18 | var r = new Random(); 19 | // assign some SOH memory 20 | var soh = new char[1024]; 21 | 22 | // assign some LOH memory 23 | var loh = new char[1024 * 100]; 24 | 25 | // Compile a method 26 | var result = CompileMe(() => r.Next()); 27 | 28 | return new string[] {"value1" + soh[^1] + loh[^1] + result }; 29 | } 30 | 31 | private int CompileMe(Expression> func) 32 | { 33 | return func.Compile()(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet.Attributes; 9 | using BenchmarkDotNet.Columns; 10 | using BenchmarkDotNet.Configs; 11 | using BenchmarkDotNet.Diagnosers; 12 | using BenchmarkDotNet.Engines; 13 | using BenchmarkDotNet.Environments; 14 | using BenchmarkDotNet.Jobs; 15 | using BenchmarkDotNet.Mathematics; 16 | using BenchmarkDotNet.Order; 17 | using BenchmarkDotNet.Running; 18 | using Benchmarks.Benchmarks; 19 | using Microsoft.AspNetCore; 20 | using Microsoft.AspNetCore.Hosting; 21 | using Microsoft.Extensions.Configuration; 22 | using Microsoft.Extensions.Logging; 23 | using Perfolizer.Horology; 24 | using Perfolizer.Mathematics.OutlierDetection; 25 | using Prometheus.DotNetRuntime; 26 | 27 | namespace Benchmarks 28 | { 29 | public class Program 30 | { 31 | public static void Main(string[] args) 32 | { 33 | BenchmarkRunner.Run( 34 | DefaultConfig.Instance 35 | .With( 36 | new Job() 37 | .With(RunStrategy.Throughput) 38 | .WithWarmupCount(1) 39 | .WithIterationTime(TimeInterval.FromMilliseconds(300)) 40 | .WithMaxIterationCount(30) 41 | .WithCustomBuildConfiguration("Release") 42 | .WithOutlierMode(OutlierMode.DontRemove) 43 | ) 44 | .With(MemoryDiagnoser.Default) 45 | ); 46 | // BenchmarkSwitcher.FromTypes(new []{typeof(BaselineBenchmark), typeof(NoSamplingBenchmark), typeof(DefaultBenchmark)}).RunAllJoined( 47 | // DefaultConfig.Instance 48 | // .With( 49 | // new Job() 50 | // .With(RunStrategy.Monitoring) 51 | // .WithLaunchCount(3) 52 | // .WithWarmupCount(1) 53 | // .WithIterationTime(TimeInterval.FromSeconds(10)) 54 | // .WithCustomBuildConfiguration("Release") 55 | // .WithOutlierMode(OutlierMode.DontRemove) 56 | // ) 57 | // .With(MemoryDiagnoser.Default) 58 | // .With(HardwareCounter.TotalCycles) 59 | // ); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Benchmarks/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "AspNetCoreExample": { 5 | "commandName": "Project", 6 | "applicationUrl": "http://localhost:5000", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Production" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Benchmarks/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Prometheus; 14 | 15 | namespace Benchmarks 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddMvc(); 30 | } 31 | 32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 33 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 34 | { 35 | app.UseRouting(); 36 | 37 | app.UseEndpoints(endpoints => 38 | { 39 | // Mapping of endpoints goes here: 40 | endpoints.MapControllers(); 41 | }); 42 | 43 | app.UseMetricServer(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Benchmarks/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /src/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Release;Debug; 4 | AnyCPU 5 | 9 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | true 14 | false 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/DotNetRuntimeStatsCollectorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Tracing; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using NUnit.Framework; 9 | using Prometheus.DotNetRuntime.EventListening; 10 | using Prometheus.DotNetRuntime.EventListening.Parsers; 11 | 12 | namespace Prometheus.DotNetRuntime.Tests.EventListening 13 | { 14 | [TestFixture] 15 | public class DotNetRuntimeStatsCollectorTests 16 | { 17 | [Test] 18 | [Timeout(20_000)] 19 | public async Task After_Recycling_Then_Events_Can_Still_Be_Processed_Correctly() 20 | { 21 | // arrange 22 | var parser = new RuntimeEventParser() { RefreshIntervalSeconds = 1}; 23 | var eventAssertion = TestHelpers.ArrangeEventAssertion(e => parser.ExceptionCount += e); 24 | var services = new ServiceCollection(); 25 | var parserRego = ListenerRegistration.Create(CaptureLevel.Counters, _ => parser); 26 | parserRego.RegisterServices(services); 27 | services.AddSingleton, HashSet>(_ => new[] { parserRego }.ToHashSet()); 28 | 29 | // act 30 | using var l = new DotNetRuntimeStatsCollector(services.BuildServiceProvider(), new CollectorRegistry(), new DotNetRuntimeStatsCollector.Options() { RecycleListenersEvery = TimeSpan.FromSeconds(3)}); 31 | Assert.That(() => eventAssertion.Fired, Is.True.After(2000, 10)); 32 | await Task.Delay(TimeSpan.FromSeconds(10)); 33 | 34 | // Why do we expected this value of events? Although we are waiting for 10 seconds for events, recycles may cause a counter period 35 | // to not fire. As counter events fire each second, as long as this value is greater than the recycle period this test can veryify 36 | // recycling is working correctly. 37 | const int expectedCounterEvents = 6; 38 | Assert.That(eventAssertion.History.Count, Is.GreaterThanOrEqualTo(expectedCounterEvents)); 39 | Assert.That(l.EventListenerRecycles.Value, Is.InRange(3, 5)); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/EventCounterParserBaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using NUnit.Framework; 4 | using Prometheus.DotNetRuntime.EventListening; 5 | 6 | namespace Prometheus.DotNetRuntime.Tests.EventListening 7 | { 8 | [TestFixture] 9 | public class Given_An_Implementation_Of_EventCounterParserBaseTest_That_Has_No_Decorated_Events 10 | { 11 | [Test] 12 | public void When_Attributes_Are_Missing_From_Interface_Events_Then_Throw_Exception() 13 | { 14 | var ex = Assert.Throws(() => new NoAttributesEventCounterParser()); 15 | Assert.That(ex, Has.Message.Contains("All events part of an ICounterEvents interface require a [CounterNameAttribute] attribute. Events without attribute: TestIncrementingCounter, TestMeanCounter.")); 16 | } 17 | 18 | public class NoAttributesEventCounterParser : EventCounterParserBase, TestCounters 19 | { 20 | public override string EventSourceName { get; } 21 | public override EventKeywords Keywords { get; } 22 | public override int RefreshIntervalSeconds { get; set; } 23 | 24 | public event Action TestIncrementingCounter; 25 | public event Action TestMeanCounter; 26 | public event Action UnrelatedEvent; 27 | } 28 | } 29 | 30 | [TestFixture] 31 | public class Given_An_Implementation_Of_EventCounterParserBaseTest_That_Has_Decorated_Events 32 | { 33 | private DummyTypeEventCounterParser _parser; 34 | 35 | [SetUp] 36 | public void SetUp() 37 | { 38 | _parser = new DummyTypeEventCounterParser(); 39 | } 40 | 41 | [Test] 42 | public void When_An_Empty_Event_Is_Passed_Then_No_Exception_Occurs_And_No_Event_Is_Raised() 43 | { 44 | // arrange 45 | var eventAssertionMean = TestHelpers.ArrangeEventAssertion(h => _parser.TestMeanCounter += h); 46 | var eventAssertionIncr = TestHelpers.ArrangeEventAssertion(h => _parser.TestIncrementingCounter += h); 47 | 48 | // act 49 | _parser.ProcessEvent(TestHelpers.CreateCounterEventWrittenEventArgs()); 50 | 51 | // assert 52 | Assert.IsFalse(eventAssertionIncr.Fired); 53 | Assert.IsFalse(eventAssertionMean.Fired); 54 | } 55 | 56 | [Test] 57 | public void When_A_MeanCounter_Is_Passed_For_A_Matching_MeanCounterValue_Event_Then_The_Event_Is_Fired() 58 | { 59 | // arrange 60 | var eventAssertion = TestHelpers.ArrangeEventAssertion(h => _parser.TestMeanCounter += h); 61 | var e = TestHelpers.CreateCounterEventWrittenEventArgs( 62 | ("CounterType", "Mean"), 63 | ("Name", "test-mean-counter"), 64 | ("Mean", 5.0), 65 | ("Count", 1) 66 | ); 67 | 68 | // act 69 | _parser.ProcessEvent(e); 70 | 71 | // assert 72 | Assert.That(eventAssertion.Fired, Is.True); 73 | Assert.That(eventAssertion.LastEvent.Mean, Is.EqualTo(5.0)); 74 | Assert.That(eventAssertion.LastEvent.Count, Is.EqualTo(1)); 75 | } 76 | 77 | [Test] 78 | public void When_A_IncrementingCounter_Is_Passed_For_A_Matching_IncrementingCounterValue_Event_Then_The_Event_Is_Fired() 79 | { 80 | // arrange 81 | var eventAssertion = TestHelpers.ArrangeEventAssertion(h => _parser.TestIncrementingCounter += h); 82 | var e = TestHelpers.CreateCounterEventWrittenEventArgs( 83 | ("CounterType", "Sum"), 84 | ("Name", "test-incrementing-counter"), 85 | ("Increment", 10.0) 86 | ); 87 | 88 | // act 89 | _parser.ProcessEvent(e); 90 | 91 | // assert 92 | Assert.That(eventAssertion.Fired, Is.True); 93 | Assert.That(eventAssertion.LastEvent.IncrementedBy, Is.EqualTo(10.0)); 94 | } 95 | 96 | [Test] 97 | public void When_A_IncrementingCounter_Is_Passed_For_A_Mismatching_MeanCounterValue_Event_Then_An_Exception_Is_Thrown() 98 | { 99 | // arrange 100 | var meanEventAssertion = TestHelpers.ArrangeEventAssertion(h => _parser.TestMeanCounter += h); 101 | var incrEventAssertion = TestHelpers.ArrangeEventAssertion(h => _parser.TestIncrementingCounter += h); 102 | 103 | var e = TestHelpers.CreateCounterEventWrittenEventArgs( 104 | ("CounterType", "Mean"), 105 | // refers to the incrementing counter 106 | ("Name", "test-incrementing-counter"), 107 | ("Mean", 5.0), 108 | ("Count", 1) 109 | ); 110 | 111 | // act 112 | Assert.Throws(() => _parser.ProcessEvent(e)); 113 | } 114 | 115 | public class DummyTypeEventCounterParser : EventCounterParserBase, TestCounters 116 | { 117 | public override string EventSourceName { get; } 118 | public override EventKeywords Keywords { get; } 119 | public override int RefreshIntervalSeconds { get; set; } 120 | 121 | [CounterName("test-incrementing-counter")] 122 | public event Action TestIncrementingCounter; 123 | [CounterName("test-mean-counter")] 124 | public event Action TestMeanCounter; 125 | } 126 | } 127 | 128 | public interface TestCounters : ICounterEvents 129 | { 130 | #pragma disable warning 131 | public event Action TestIncrementingCounter; 132 | public event Action TestMeanCounter; 133 | #pragma warning restore 134 | } 135 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/EventParserTypes.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Tracing; 2 | using NUnit.Framework; 3 | using NUnit.Framework.Internal.Execution; 4 | using Prometheus.DotNetRuntime.EventListening; 5 | using Prometheus.DotNetRuntime.EventListening.Parsers; 6 | 7 | namespace Prometheus.DotNetRuntime.Tests.EventListening 8 | { 9 | [TestFixture] 10 | public class EventParserTypesTests 11 | { 12 | [Test] 13 | public void Given_A_Type_That_Implements_All_IEvent_Interfaces_When_Calling_GetEventInterfaces_Then_Returns_All_Interfaces_Except_IEvents() 14 | { 15 | var interfaces = EventParserTypes.GetEventInterfaces(typeof(AllEvents)); 16 | 17 | Assert.That(interfaces, Is.EquivalentTo(new[] 18 | { 19 | typeof(AllEvents.Events.Verbose), 20 | typeof(AllEvents.Events.Info), 21 | typeof(AllEvents.Events.Warning), 22 | typeof(AllEvents.Events.Error), 23 | typeof(AllEvents.Events.Always), 24 | typeof(AllEvents.Events.Critical), 25 | typeof(AllEvents.Events.Counters) 26 | })); 27 | } 28 | 29 | [Test] 30 | public void Given_A_Type_That_Implements_All_IEvent_Interfaces_When_Calling_GetLevelsFromType_Then_Returns_All_Interfaces_Except_IEvents() 31 | { 32 | var levels = EventParserTypes.GetLevelsFromParser(typeof(AllEvents)); 33 | 34 | Assert.That(levels, Is.EquivalentTo(new[] 35 | { 36 | EventLevel.LogAlways, 37 | EventLevel.Verbose, 38 | EventLevel.Informational, 39 | EventLevel.Warning, 40 | EventLevel.Error, 41 | EventLevel.Critical 42 | })); 43 | } 44 | 45 | [Test] 46 | public void When_Calling_GetEventParsers_Then_Returns_All_Event_Parsers_Defined_In_The_DotNetRuntime_Library() 47 | { 48 | var parsers = EventParserTypes.GetEventParsers(); 49 | 50 | Assert.That(parsers, Is.SupersetOf(new[] 51 | { 52 | typeof(GcEventParser), 53 | typeof(JitEventParser), 54 | typeof(ThreadPoolEventParser), 55 | typeof(RuntimeEventParser), 56 | typeof(ContentionEventParser), 57 | typeof(ExceptionEventParser) 58 | })); 59 | } 60 | 61 | #if NETCOREAPP3_1 62 | 63 | [Test] 64 | public void When_Calling_GetEventInterfacesForCurrentRuntime_On_Net31_Then_Returns_Interfaces_For_Net31_Runtime_And_Below() 65 | { 66 | var interfaces = EventParserTypes.GetEventInterfacesForCurrentRuntime(typeof(VersionedEvents), EventLevel.LogAlways); 67 | Assert.That(interfaces, Is.EquivalentTo(new [] { typeof(VersionedEvents.Events.Counters), typeof(VersionedEvents.Events.CountersV3_1) })); 68 | } 69 | #endif 70 | 71 | #if NET5_0_OR_GREATER 72 | 73 | [Test] 74 | public void When_Calling_GetEventInterfacesForCurrentRuntime_On_Net50_Then_Returns_Interfaces_For_Net50_Runtime_And_Below() 75 | { 76 | var interfaces = EventParserTypes.GetEventInterfacesForCurrentRuntime(typeof(VersionedEvents), EventLevel.LogAlways); 77 | Assert.That(interfaces, Is.EquivalentTo(new [] { typeof(VersionedEvents.Events.Counters), typeof(VersionedEvents.Events.CountersV3_1), typeof(VersionedEvents.Events.CountersV5_0) })); 78 | } 79 | #endif 80 | 81 | public class AllEvents : AllEvents.Events.Verbose, AllEvents.Events.Info, AllEvents.Events.Warning, AllEvents.Events.Error, AllEvents.Events.Always, AllEvents.Events.Counters, AllEvents.Events.Critical, IEvents 82 | { 83 | public static class Events 84 | { 85 | public interface Verbose : IVerboseEvents{} 86 | public interface Info : IInfoEvents{} 87 | public interface Warning : IWarningEvents {} 88 | public interface Error : IErrorEvents{} 89 | public interface Always : IAlwaysEvents{} 90 | public interface Critical : ICriticalEvents{} 91 | public interface Counters : ICounterEvents{} 92 | public interface CountersV3_1 : ICounterEvents{} 93 | public interface CountersV5_0 : ICounterEvents{} 94 | } 95 | } 96 | 97 | public class VersionedEvents : VersionedEvents.Events.Counters, VersionedEvents.Events.CountersV3_1, VersionedEvents.Events.CountersV5_0 98 | { 99 | public static class Events 100 | { 101 | public interface Counters : ICounterEvents{} 102 | public interface CountersV3_1 : ICounterEvents{} 103 | public interface CountersV5_0 : ICounterEvents{} 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/IEventParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using NUnit.Framework; 4 | using Prometheus.DotNetRuntime.EventListening; 5 | using Prometheus.DotNetRuntime.EventListening.Parsers; 6 | 7 | namespace Prometheus.DotNetRuntime.Tests.EventListening 8 | { 9 | [TestFixture] 10 | public class IEventParserTests 11 | { 12 | [Test] 13 | public void Given_A_Parser_Implements_One_Or_More_IEvents_Interface_Then_Can_Get_Appropriate_Levels() 14 | { 15 | IEventListener gcParser = new GcEventParser(); 16 | Assert.That(gcParser.SupportedLevels, Is.EquivalentTo(new[] { EventLevel.Informational, EventLevel.Verbose })); 17 | } 18 | 19 | [Test] 20 | public void Given_A_Parser_Implements_ICounterEvents_Then_Can_Get_Appropriate_Levels() 21 | { 22 | IEventListener runtimeEventParser = new RuntimeEventParser(); 23 | Assert.That(runtimeEventParser.SupportedLevels, Is.EquivalentTo(new[] { EventLevel.LogAlways })); 24 | } 25 | 26 | [Test] 27 | public void Given_A_Parser_Only_Implements_IEvents_Returns_No_Levels() 28 | { 29 | IEventListener noEventsParser = new TestParserNoEvents(); 30 | Assert.That(noEventsParser.SupportedLevels, Is.Empty); 31 | } 32 | 33 | private class TestParserNoEvents : IEventParser, IEvents 34 | { 35 | public string EventSourceName { get; } 36 | public EventKeywords Keywords { get; } 37 | public void ProcessEvent(EventWrittenEventArgs e) 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/Parsers/EventListenerIntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using System.Threading; 4 | using NUnit.Framework; 5 | using Prometheus.DotNetRuntime.EventListening; 6 | 7 | namespace Prometheus.DotNetRuntime.Tests.EventListening.Parsers 8 | { 9 | [TestFixture] 10 | public abstract class EventListenerIntegrationTestBase 11 | where TEventListener : IEventListener 12 | { 13 | private DotNetEventListener _eventListener; 14 | protected TEventListener Parser { get; private set; } 15 | 16 | [SetUp] 17 | public void SetUp() 18 | { 19 | Parser = CreateListener(); 20 | _eventListener = new DotNetEventListener(Parser, EventLevel.LogAlways, new DotNetEventListener.GlobalOptions{ ErrorHandler = ex => Assert.Fail($"Unexpected exception occurred: {ex}")}); 21 | 22 | // wait for event listener thread to spin up 23 | while (!_eventListener.StartedReceivingEvents) 24 | { 25 | Thread.Sleep(10); 26 | Console.Write("Waiting.. "); 27 | 28 | } 29 | Console.WriteLine("EventListener should be active now."); 30 | } 31 | 32 | [TearDown] 33 | public void TearDown() 34 | { 35 | Console.WriteLine("Disposing event listener.."); 36 | _eventListener.Dispose(); 37 | } 38 | 39 | protected abstract TEventListener CreateListener(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/Parsers/SystemRuntimeCounterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using NUnit.Framework; 4 | using Prometheus.DotNetRuntime.EventListening.Parsers; 5 | 6 | namespace Prometheus.DotNetRuntime.Tests.EventListening.Parsers 7 | { 8 | [TestFixture] 9 | public class SystemRuntimeCounterTests : EventListenerIntegrationTestBase 10 | { 11 | [Test] 12 | public void TestEvent() 13 | { 14 | var resetEvent = new AutoResetEvent(false); 15 | Parser.AllocRate += e => 16 | { 17 | resetEvent.Set(); 18 | Assert.That(e.IncrementedBy, Is.GreaterThan(0)); 19 | }; 20 | 21 | Assert.IsTrue(resetEvent.WaitOne(TimeSpan.FromSeconds(10))); 22 | } 23 | 24 | protected override RuntimeEventParser CreateListener() 25 | { 26 | return new (); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/Parsers/Util/EventPairTimerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Diagnostics.Tracing; 4 | using Fasterflect; 5 | using NUnit.Framework; 6 | using Prometheus.DotNetRuntime.EventListening.Parsers.Util; 7 | 8 | namespace Prometheus.DotNetRuntime.Tests.EventListening.Parsers.Util 9 | { 10 | [TestFixture] 11 | public class Given_An_EventPairTimer_That_Samples_Every_Event : EventPairTimerBaseClass 12 | { 13 | private EventPairTimer _eventPairTimer; 14 | 15 | [SetUp] 16 | public void SetUp() 17 | { 18 | _eventPairTimer = new EventPairTimer(EventIdStart, EventIdEnd, x => (long)x.Payload[0], SampleEvery.OneEvent); 19 | } 20 | 21 | [Test] 22 | public void TryGetEventPairDuration_ignores_events_that_its_not_configured_to_look_for() 23 | { 24 | var nonMonitoredEvent = TestHelpers.CreateEventWrittenEventArgs(3); 25 | Assert.That(_eventPairTimer.TryGetDuration(nonMonitoredEvent, out var duration), Is.EqualTo(DurationResult.Unrecognized)); 26 | Assert.That(duration, Is.EqualTo(TimeSpan.Zero)); 27 | } 28 | 29 | [Test] 30 | public void TryGetEventPairDuration_ignores_end_events_if_it_never_saw_the_start_event() 31 | { 32 | var nonMonitoredEvent = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, payload: 1L); 33 | Assert.That(_eventPairTimer.TryGetDuration(nonMonitoredEvent, out var duration),Is.EqualTo(DurationResult.FinalWithoutDuration)); 34 | Assert.That(duration, Is.EqualTo(TimeSpan.Zero)); 35 | } 36 | 37 | [Test] 38 | public void TryGetEventPairDuration_calculates_duration_between_configured_events() 39 | { 40 | // arrange 41 | var now = DateTime.UtcNow; 42 | var startEvent = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 1L); 43 | Assert.That(_eventPairTimer.TryGetDuration(startEvent, out var _), Is.EqualTo(DurationResult.Start)); 44 | var endEvent = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(100), payload: 1L); 45 | 46 | // act 47 | Assert.That(_eventPairTimer.TryGetDuration(endEvent, out var duration), Is.EqualTo(DurationResult.FinalWithDuration)); 48 | Assert.That(duration.TotalMilliseconds, Is.EqualTo(100)); 49 | } 50 | 51 | [Test] 52 | public void TryGetEventPairDuration_calculates_duration_between_configured_events_that_occur_simultaneously() 53 | { 54 | // arrange 55 | var now = DateTime.UtcNow; 56 | var startEvent = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 1L); 57 | Assert.That(_eventPairTimer.TryGetDuration(startEvent, out var _), Is.EqualTo(DurationResult.Start)); 58 | var endEvent = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now, payload: 1L); 59 | 60 | // act 61 | Assert.That(_eventPairTimer.TryGetDuration(endEvent, out var duration), Is.EqualTo(DurationResult.FinalWithDuration)); 62 | Assert.That(duration, Is.EqualTo(TimeSpan.Zero)); 63 | } 64 | 65 | [Test] 66 | public void TryGetEventPairDuration_calculates_duration_between_multiple_out_of_order_configured_events() 67 | { 68 | // arrange 69 | var now = DateTime.UtcNow; 70 | var startEvent1 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 1L); 71 | var endEvent1 = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(300), payload: 1L); 72 | var startEvent2 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 2L); 73 | var endEvent2 = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(200), payload: 2L); 74 | var startEvent3 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 3L); 75 | var endEvent3 = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(100), payload: 3L); 76 | 77 | _eventPairTimer.TryGetDuration(startEvent1, out var _); 78 | _eventPairTimer.TryGetDuration(startEvent2, out var _); 79 | _eventPairTimer.TryGetDuration(startEvent3, out var _); 80 | 81 | // act 82 | Assert.That(_eventPairTimer.TryGetDuration(endEvent3, out var event3Duration), Is.EqualTo(DurationResult.FinalWithDuration)); 83 | Assert.That(_eventPairTimer.TryGetDuration(endEvent2, out var event2Duration), Is.EqualTo(DurationResult.FinalWithDuration)); 84 | Assert.That(_eventPairTimer.TryGetDuration(endEvent1, out var event1Duration), Is.EqualTo(DurationResult.FinalWithDuration)); 85 | 86 | Assert.That(event1Duration.TotalMilliseconds, Is.EqualTo(300)); 87 | Assert.That(event2Duration.TotalMilliseconds, Is.EqualTo(200)); 88 | Assert.That(event3Duration.TotalMilliseconds, Is.EqualTo(100)); 89 | } 90 | } 91 | 92 | 93 | [TestFixture] 94 | public class Given_An_EventPairTimer_That_Samples_Every_2nd_Event : EventPairTimerBaseClass 95 | { 96 | 97 | private EventPairTimer _eventPairTimer; 98 | 99 | [SetUp] 100 | public void SetUp() 101 | { 102 | _eventPairTimer = new EventPairTimer(EventIdStart, EventIdEnd, x => (long)x.Payload[0], SampleEvery.TwoEvents); 103 | } 104 | 105 | [Test] 106 | public void TryGetEventPairDuration_recognizes_start_events_that_will_be_discarded() 107 | { 108 | var startEvent1 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, DateTime.UtcNow, payload: 1L); 109 | Assert.That(_eventPairTimer.TryGetDuration(startEvent1, out var duration),Is.EqualTo(DurationResult.Start)); 110 | Assert.That(duration, Is.EqualTo(TimeSpan.Zero)); 111 | } 112 | 113 | [Test] 114 | public void TryGetEventPairDuration_will_discard_1_event_and_calculate_duration_for_the_2nd_event() 115 | { 116 | // arrange 117 | var now = DateTime.UtcNow; 118 | var startEvent1 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 1L); 119 | var endEvent1 = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(300), payload: 1L); 120 | var startEvent2 = TestHelpers.CreateEventWrittenEventArgs(EventIdStart, now, payload: 2L); 121 | var endEvent2 = TestHelpers.CreateEventWrittenEventArgs(EventIdEnd, now.AddMilliseconds(200), payload: 2L); 122 | 123 | _eventPairTimer.TryGetDuration(startEvent1, out var _); 124 | _eventPairTimer.TryGetDuration(startEvent2, out var _); 125 | 126 | // act 127 | Assert.That(_eventPairTimer.TryGetDuration(endEvent1, out var _), Is.EqualTo(DurationResult.FinalWithoutDuration)); 128 | Assert.That(_eventPairTimer.TryGetDuration(endEvent2, out var _), Is.EqualTo(DurationResult.FinalWithDuration)); 129 | } 130 | 131 | } 132 | 133 | public class EventPairTimerBaseClass 134 | { 135 | protected const int EventIdStart = 1, EventIdEnd = 2; 136 | 137 | 138 | } 139 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/Parsers/Util/SamplingRateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Prometheus.DotNetRuntime.EventListening.Parsers.Util; 3 | 4 | namespace Prometheus.DotNetRuntime.Tests.EventListening.Parsers.Util 5 | { 6 | [TestFixture] 7 | public class SamplingRateTests 8 | { 9 | [Test] 10 | [TestCase(SampleEvery.OneEvent, 1)] 11 | [TestCase(SampleEvery.TwoEvents, 2)] 12 | [TestCase(SampleEvery.FiveEvents, 5)] 13 | [TestCase(SampleEvery.TenEvents, 10)] 14 | [TestCase(SampleEvery.TwentyEvents, 20)] 15 | [TestCase(SampleEvery.FiftyEvents, 50)] 16 | [TestCase(SampleEvery.HundredEvents, 100)] 17 | public void SampleEvery_Reflects_The_Ratio_Of_Every_100_Events_That_Will_Be_Sampled(SampleEvery samplingRate, int expected) 18 | { 19 | var sr = new SamplingRate(samplingRate); 20 | Assert.That(sr.SampleEvery, Is.EqualTo(expected)); 21 | } 22 | 23 | [Test] 24 | [TestCase(SampleEvery.OneEvent, 1000)] 25 | [TestCase(SampleEvery.TwoEvents, 500)] 26 | [TestCase(SampleEvery.FiveEvents, 200)] 27 | [TestCase(SampleEvery.TenEvents, 100)] 28 | [TestCase(SampleEvery.TwentyEvents, 50)] 29 | [TestCase(SampleEvery.FiftyEvents, 20)] 30 | [TestCase(SampleEvery.HundredEvents, 10)] 31 | public void Given_1000_Events_ShouldSampleEvent_Returns_True_Every_Nth_Event(SampleEvery samplingRate, int expectedEvents) 32 | { 33 | var eventsSampled = 0; 34 | var sr = new SamplingRate(samplingRate); 35 | 36 | for (int i = 0; i < 1_000; i++) 37 | { 38 | if (sr.ShouldSampleEvent()) 39 | { 40 | eventsSampled++; 41 | } 42 | } 43 | 44 | Assert.That(eventsSampled, Is.EqualTo(expectedEvents)); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/EventListening/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Diagnostics.Tracing; 5 | using System.Linq; 6 | using Fasterflect; 7 | using NUnit.Framework; 8 | 9 | namespace Prometheus.DotNetRuntime.Tests.EventListening 10 | { 11 | public class TestHelpers 12 | { 13 | public static EventWrittenEventArgs CreateEventWrittenEventArgs(int eventId, DateTime? timestamp = null, params object[] payload) 14 | { 15 | EventWrittenEventArgs args; 16 | var bindFlags = Flags.NonPublic | Flags.Instance; 17 | 18 | // In .NET 6.0, they changed the signature of these constructors- handle this annoyance 19 | if (typeof(EventWrittenEventArgs).GetConstructors(bindFlags).Any(x => x.GetParameters().Length == 1)) 20 | { 21 | args = (EventWrittenEventArgs)typeof(EventWrittenEventArgs).CreateInstance(new[] { typeof(EventSource)}, Flags.NonPublic | Flags.Instance, new object[] { null }); 22 | args.SetPropertyValue("EventId", eventId); 23 | } 24 | else 25 | { 26 | args = (EventWrittenEventArgs)typeof(EventWrittenEventArgs).CreateInstance(new[] { typeof(EventSource), typeof(int) }, Flags.NonPublic | Flags.Instance, new object[] { null, eventId }); 27 | } 28 | 29 | args.SetPropertyValue("Payload", new ReadOnlyCollection(payload)); 30 | 31 | if (timestamp.HasValue) 32 | { 33 | args.SetPropertyValue("TimeStamp", timestamp.Value); 34 | } 35 | 36 | return args; 37 | } 38 | 39 | public static EventWrittenEventArgs CreateCounterEventWrittenEventArgs(params (string key, object val)[] payload) 40 | { 41 | var counterPayload = payload.ToDictionary(k => k.key, v => v.val); 42 | 43 | var e = CreateEventWrittenEventArgs(-1, DateTime.UtcNow, new[] { counterPayload }); 44 | e.SetPropertyValue("EventName", "EventCounters"); 45 | return e; 46 | } 47 | 48 | public static EventAssertion ArrangeEventAssertion(Action> wireUp) 49 | { 50 | return new EventAssertion(wireUp); 51 | } 52 | 53 | public class EventAssertion 54 | { 55 | private Action _handler; 56 | 57 | public EventAssertion(Action> wireUp) 58 | { 59 | _handler = e => { History.Add(e); }; 60 | 61 | wireUp(_handler); 62 | } 63 | 64 | public bool Fired => History.Count > 0; 65 | public List History { get; } = new List(); 66 | public T LastEvent => History.Last(); 67 | 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/ExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NUnit.Framework; 3 | using Prometheus.DotNetRuntime.Metrics; 4 | 5 | namespace Prometheus.DotNetRuntime.Tests 6 | { 7 | [TestFixture] 8 | public class ExtensionTests 9 | { 10 | [Test] 11 | public void CollectAllValues_Extracts_All_Labeled_And_Unlabeled_Values_From_A_Counter() 12 | { 13 | // arrange 14 | var counter = Prometheus.Metrics.CreateCounter("test_counter", "", "label1", "label2"); 15 | counter.Inc(); // unlabeled 16 | counter.Labels("1", "2").Inc(); 17 | counter.Labels("1", "3").Inc(2); 18 | 19 | // act 20 | var values = MetricExtensions.CollectAllValues(counter); 21 | 22 | // assert 23 | Assert.That(values.Count(), Is.EqualTo(3)); 24 | Assert.That(values.Sum(), Is.EqualTo(4)); 25 | } 26 | 27 | [Test] 28 | public void CollectAllValues_Extracts_All_Labeled_Values_From_A_Counter_When_excludeUnlabeled_Is_True() 29 | { 30 | // arrange 31 | var counter = Prometheus.Metrics.CreateCounter("test_counter2", "", "label1", "label2"); 32 | counter.Inc(); // unlabeled 33 | counter.Labels("1", "2").Inc(); 34 | counter.Labels("1", "3").Inc(2); 35 | 36 | // act 37 | var values = MetricExtensions.CollectAllValues(counter, excludeUnlabeled: true); 38 | 39 | // assert 40 | Assert.That(values.Count(), Is.EqualTo(2)); 41 | Assert.That(values.Sum(), Is.EqualTo(3)); 42 | } 43 | 44 | [Test] 45 | public void CollectAllValues_Extracts_All_Labeled_And_Unlabeled_Summed_Values_From_A_Histogram() 46 | { 47 | // arrange 48 | var histo = Prometheus.Metrics.CreateHistogram("test_histo", "", labelNames: new [] {"label1", "label2"}); 49 | histo.Observe(1); // unlabeled 50 | histo.Labels("1", "2").Observe(2); 51 | histo.Labels("1", "2").Observe(3); 52 | histo.Labels("1", "3").Observe(4); 53 | 54 | // act 55 | var values = MetricExtensions.CollectAllSumValues(histo); 56 | 57 | // assert 58 | Assert.That(values.Count(), Is.EqualTo(3)); 59 | Assert.That(values.Sum(), Is.EqualTo(10)); 60 | } 61 | 62 | [Test] 63 | public void CollectAllValues_Extracts_Labeled_Summed_Values_From_A_Histogram_When_excludeUnlabeled_Is_True() 64 | { 65 | // arrange 66 | var histo = Prometheus.Metrics.CreateHistogram("test_histo2", "", labelNames: new [] {"label1", "label2"}); 67 | histo.Observe(1); // unlabeled 68 | histo.Labels("1", "2").Observe(2); 69 | histo.Labels("1", "2").Observe(3); 70 | histo.Labels("1", "3").Observe(4); 71 | 72 | // act 73 | var values = MetricExtensions.CollectAllSumValues(histo, excludeUnlabeled: true); 74 | 75 | // assert 76 | Assert.That(values.Count(), Is.EqualTo(2)); 77 | Assert.That(values.Sum(), Is.EqualTo(9)); 78 | } 79 | 80 | [Test] 81 | public void CollectAllValues_Extracts_All_Labeled_And_Unlabeled_Count_Values_From_A_Histogram() 82 | { 83 | // arrange 84 | var histo = Prometheus.Metrics.CreateHistogram("test_histo3", "", labelNames: new []{ "label1", "label2"}); 85 | histo.Observe(1); // unlabeled 86 | histo.Labels("1", "2").Observe(2); 87 | histo.Labels("1", "2").Observe(3); 88 | histo.Labels("1", "3").Observe(4); 89 | 90 | // act 91 | var values = MetricExtensions.CollectAllCountValues(histo); 92 | 93 | // assert 94 | Assert.That(values.Count(), Is.EqualTo(3)); 95 | Assert.That(values.Sum(x => (long)x), Is.EqualTo(4)); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/ContentionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using Prometheus.DotNetRuntime.Metrics.Producers; 7 | 8 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 9 | { 10 | [TestFixture] 11 | internal class Given_Contention_Events_Are_Enabled_For_Contention_Stats : IntegrationTestBase 12 | { 13 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 14 | { 15 | return toConfigure.WithContentionStats(CaptureLevel.Informational, SampleEvery.OneEvent); 16 | } 17 | 18 | [Test] 19 | public void Will_measure_no_contention_on_an_uncontested_lock() 20 | { 21 | // arrange 22 | var key = new Object(); 23 | 24 | // act 25 | lock (key) 26 | { 27 | } 28 | 29 | // assert 30 | Assert.That(MetricProducer.ContentionTotal.Value, Is.EqualTo(0)); 31 | Assert.That(MetricProducer.ContentionSecondsTotal.Value, Is.EqualTo(0)); 32 | } 33 | 34 | /// 35 | /// This test has the potential to be flaky (due to attempting to simulate lock contention across multiple threads in the thread pool), 36 | /// may have to revisit this in the future.. 37 | /// 38 | /// 39 | [Test] 40 | [Repeat(3)] 41 | public async Task Will_measure_contention_on_a_contested_lock() 42 | { 43 | // arrange 44 | const int numThreads = 10; 45 | const int sleepForMs = 50; 46 | var key = new object(); 47 | // Increase the min. thread pool size so that when we use Thread.Sleep, we don't run into scheduling delays 48 | ThreadPool.SetMinThreads(numThreads * 2, 1); 49 | 50 | // act 51 | var tasks = Enumerable.Range(1, numThreads) 52 | .Select(_ => Task.Run(() => 53 | { 54 | lock (key) 55 | { 56 | Thread.Sleep(sleepForMs); 57 | } 58 | })); 59 | 60 | await Task.WhenAll(tasks); 61 | 62 | // assert 63 | 64 | // Why -1? The first thread will not contend the lock 65 | const int numLocksContended = numThreads - 1; 66 | Assert.That(() => MetricProducer.ContentionTotal.Value, Is.GreaterThanOrEqualTo(numLocksContended).After(3000, 10)); 67 | 68 | // Pattern of expected contention times is: 50ms, 100ms, 150ms, etc. 69 | var expectedDelay = TimeSpan.FromMilliseconds(Enumerable.Range(1, numLocksContended).Aggregate(sleepForMs, (acc, next) => acc + (sleepForMs * next))); 70 | Assert.That(MetricProducer.ContentionSecondsTotal.Value, Is.EqualTo(expectedDelay.TotalSeconds).Within(sleepForMs)); 71 | } 72 | } 73 | 74 | [TestFixture] 75 | internal class Given_Only_Counters_Are_Enabled_For_Contention_Stats : IntegrationTestBase 76 | { 77 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 78 | { 79 | return toConfigure.WithContentionStats(CaptureLevel.Counters, SampleEvery.OneEvent); 80 | } 81 | 82 | /// 83 | /// This test has the potential to be flaky (due to attempting to simulate lock contention across multiple threads in the thread pool), 84 | /// may have to revisit this in the future.. 85 | /// 86 | /// 87 | [Test] 88 | public async Task Will_measure_contention_on_a_contested_lock() 89 | { 90 | // arrange 91 | const int numThreads = 10; 92 | const int sleepForMs = 50; 93 | var key = new object(); 94 | // Increase the min. thread pool size so that when we use Thread.Sleep, we don't run into scheduling delays 95 | ThreadPool.SetMinThreads(numThreads * 2, 1); 96 | 97 | // act 98 | var tasks = Enumerable.Range(1, numThreads) 99 | .Select(_ => Task.Run(() => 100 | { 101 | lock (key) 102 | { 103 | Thread.Sleep(sleepForMs); 104 | } 105 | })); 106 | 107 | await Task.WhenAll(tasks); 108 | 109 | // assert 110 | 111 | // Why -1? The first thread will not contend the lock 112 | const int numLocksContended = numThreads - 1; 113 | Assert.That(() => MetricProducer.ContentionTotal.Value, Is.GreaterThanOrEqualTo(numLocksContended).After(3000, 10)); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/ExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using Prometheus.DotNetRuntime.Metrics.Producers; 4 | 5 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 6 | { 7 | [TestFixture] 8 | internal class Given_Exception_Events_Are_Enabled_For_Exception_Stats : IntegrationTestBase 9 | { 10 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 11 | { 12 | return toConfigure.WithExceptionStats(CaptureLevel.Errors); 13 | } 14 | 15 | [Test] 16 | [MaxTime(10_000)] 17 | public void Will_measure_when_occurring_an_exception() 18 | { 19 | // act 20 | var divider = 0; 21 | const int numToThrow = 10; 22 | 23 | for (int i = 0; i < numToThrow; i++) 24 | { 25 | try 26 | { 27 | _ = 1 / divider; 28 | } 29 | catch (DivideByZeroException) 30 | { 31 | } 32 | } 33 | 34 | // assert 35 | Assert.That(() => MetricProducer.ExceptionCount.Labels("System.DivideByZeroException").Value, Is.GreaterThanOrEqualTo(numToThrow).After(100, 1000)); 36 | } 37 | } 38 | 39 | [TestFixture] 40 | internal class Given_Only_Runtime_Counters_Are_Enabled_For_Exception_Stats : IntegrationTestBase 41 | { 42 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 43 | { 44 | return toConfigure.WithExceptionStats(CaptureLevel.Counters); 45 | } 46 | 47 | [Test] 48 | [MaxTime(10_000)] 49 | public void Will_measure_when_occurring_an_exception() 50 | { 51 | // act 52 | var divider = 0; 53 | const int numToThrow = 10; 54 | 55 | for (int i = 0; i < numToThrow; i++) 56 | { 57 | try 58 | { 59 | _ = 1 / divider; 60 | } 61 | catch (DivideByZeroException) 62 | { 63 | } 64 | } 65 | 66 | // assert 67 | Assert.That(() => MetricProducer.ExceptionCount.Value, Is.GreaterThanOrEqualTo(numToThrow).After(3_000, 100)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 5 | { 6 | public static class RuntimeEventHelper 7 | { 8 | public static void CompileMethods(Expression> toCompile, int times = 100) 9 | { 10 | for (int i = 0; i < 100; i++) 11 | { 12 | toCompile.Compile(); 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using NUnit.Framework; 7 | using Prometheus.DotNetRuntime.Metrics; 8 | 9 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 10 | { 11 | [TestFixture] 12 | public abstract class IntegrationTestBase 13 | where TMetricProducer : IMetricProducer 14 | { 15 | private DotNetRuntimeStatsCollector _collector; 16 | protected TMetricProducer MetricProducer { get; private set; } 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | _collector = (DotNetRuntimeStatsCollector) ConfigureBuilder(DotNetRuntimeStatsBuilder.Customize()) 22 | .StartCollecting(Prometheus.Metrics.NewCustomRegistry()); 23 | 24 | MetricProducer = (TMetricProducer)_collector.ServiceProvider.GetServices().Single(x => x is TMetricProducer); 25 | 26 | // wait for event listener thread to spin up 27 | var waitingFor = Stopwatch.StartNew(); 28 | var waitFor = TimeSpan.FromSeconds(10); 29 | 30 | while (!_collector.EventListeners.All(x => x.StartedReceivingEvents)) 31 | { 32 | Thread.Sleep(10); 33 | Console.Write("Waiting for event listeners to be active.. "); 34 | 35 | if (waitingFor.Elapsed > waitFor) 36 | { 37 | Assert.Fail($"Waited {waitFor} and still not all event listeners were ready! Event listeners not ready: {string.Join(", ", _collector.EventListeners.Where(x => !x.StartedReceivingEvents))}"); 38 | return; 39 | } 40 | } 41 | 42 | Console.WriteLine("All event listeners should be active now."); 43 | } 44 | 45 | [TearDown] 46 | public void TearDown() 47 | { 48 | _collector.Dispose(); 49 | } 50 | 51 | protected abstract DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure); 52 | } 53 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/JitCompilerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq.Expressions; 4 | using System.Runtime.CompilerServices; 5 | using NUnit.Framework; 6 | using Prometheus.DotNetRuntime.Metrics.Producers; 7 | 8 | 9 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 10 | { 11 | internal class Given_A_JitStatsCollector_That_Samples_Every_Jit_Event : IntegrationTestBase 12 | { 13 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 14 | { 15 | return toConfigure.WithJitStats(CaptureLevel.Verbose, SampleEvery.OneEvent); 16 | } 17 | 18 | [Test] 19 | public void When_a_method_is_jitted_then_its_compilation_is_measured() 20 | { 21 | // arrange 22 | var methodsJitted = MetricProducer.MethodsJittedTotal.Labels("false").Value; 23 | var methodsJittedSeconds = MetricProducer.MethodsJittedSecondsTotal.Labels("false").Value; 24 | 25 | // act (call a method, JIT'ing it) 26 | ToJit(); 27 | 28 | // assert 29 | Assert.That(() => MetricProducer.MethodsJittedTotal.Labels("false").Value, Is.GreaterThanOrEqualTo(methodsJitted + 1).After(100, 10)); 30 | Assert.That(MetricProducer.MethodsJittedSecondsTotal.Labels("false").Value, Is.GreaterThan(methodsJittedSeconds )); 31 | } 32 | 33 | [Test] 34 | public void When_a_method_is_jitted_then_the_CPU_ratio_can_be_measured() 35 | { 36 | // act (call a method, JIT'ing it) 37 | ToJit(); 38 | MetricProducer.UpdateMetrics(); 39 | 40 | // assert 41 | Assert.That(() => MetricProducer.CpuRatio.Value, Is.GreaterThanOrEqualTo(0.0).After(100, 10)); 42 | } 43 | 44 | [Test] 45 | public void When_a_dynamic_method_is_jitted_then_its_compilation_is_measured() 46 | { 47 | // arrange 48 | var dynamicMethodsJitted = MetricProducer.MethodsJittedTotal.Labels("true").Value; 49 | var dynamicMethodsJittedSeconds = MetricProducer.MethodsJittedSecondsTotal.Labels("true").Value; 50 | 51 | // act (call a method, JIT'ing it) 52 | ToJitDynamic(); 53 | 54 | // assert 55 | Assert.That(() => MetricProducer.MethodsJittedTotal.Labels("true").Value, Is.GreaterThanOrEqualTo(dynamicMethodsJitted + 1).After(100, 10)); 56 | Assert.That(MetricProducer.MethodsJittedSecondsTotal.Labels("true").Value, Is.GreaterThan(dynamicMethodsJittedSeconds )); 57 | } 58 | 59 | [MethodImpl(MethodImplOptions.NoInlining)] 60 | private int ToJit() 61 | { 62 | return 1; 63 | } 64 | 65 | [MethodImpl(MethodImplOptions.NoInlining)] 66 | private int ToJitDynamic() 67 | { 68 | dynamic o = "string"; 69 | return o.Length; 70 | } 71 | } 72 | 73 | internal class Given_A_JitStatsCollector_That_Samples_Every_Fifth_Jit_Event : IntegrationTestBase 74 | { 75 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 76 | { 77 | return toConfigure.WithJitStats(CaptureLevel.Verbose, SampleEvery.FiveEvents); 78 | } 79 | 80 | [Test] 81 | public void When_many_methods_are_jitted_then_their_compilation_is_measured() 82 | { 83 | // arrange 84 | var methodsJitted = MetricProducer.MethodsJittedTotal.Labels("true").Value; 85 | var methodsJittedSeconds = MetricProducer.MethodsJittedSecondsTotal.Labels("true").Value; 86 | 87 | // act 88 | var sp = Stopwatch.StartNew(); 89 | RuntimeEventHelper.CompileMethods(() => 1, 100); 90 | sp.Stop(); 91 | 92 | // assert 93 | Assert.That(() => MetricProducer.MethodsJittedTotal.Labels("true").Value, Is.GreaterThanOrEqualTo(methodsJitted + 20).After(100, 10)); 94 | Assert.That(MetricProducer.MethodsJittedSecondsTotal.Labels("true").Value, Is.GreaterThan(methodsJittedSeconds + sp.Elapsed.TotalSeconds).Within(0.1)); 95 | } 96 | } 97 | 98 | internal class Given_Only_Counters_Are_Enabled_For_JitStats : IntegrationTestBase 99 | { 100 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 101 | { 102 | return toConfigure.WithJitStats(CaptureLevel.Counters, SampleEvery.OneEvent); 103 | } 104 | 105 | #if NET5_0_OR_GREATER 106 | 107 | [Test] 108 | public void When_Running_On_NET50_Then_Counts_Of_Methods_Are_Recorded() 109 | { 110 | // arrage 111 | var methodsJittedPrevious = MetricProducer.MethodsJittedTotal.Value; 112 | var bytesJittedPrevious = MetricProducer.BytesJitted.Value; 113 | 114 | // act 115 | RuntimeEventHelper.CompileMethods(() => 1, 100); 116 | 117 | Assert.That(MetricProducer.BytesJitted, Is.Not.Null); 118 | Assert.That(MetricProducer.MethodsJittedTotal, Is.Not.Null); 119 | Assert.That(() => MetricProducer.BytesJitted.Value, Is.GreaterThan(bytesJittedPrevious).After(2_000, 10)); 120 | Assert.That(() => MetricProducer.MethodsJittedTotal.Value, Is.GreaterThan(methodsJittedPrevious + 100).After(2_000, 10)); 121 | } 122 | #endif 123 | 124 | #if NETCOREAPP3_1 125 | 126 | [Test] 127 | public void When_Running_On_NETCOREAPP31_Then_No_Metrics_Are_Available() 128 | { 129 | Assert.That(MetricProducer.BytesJitted, Is.Null); 130 | Assert.That(MetricProducer.MethodsJittedTotal, Is.Null); 131 | } 132 | #endif 133 | } 134 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/SocketsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using Prometheus.DotNetRuntime.Metrics.Producers; 7 | 8 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 9 | { 10 | #if !NETCOREAPP3_1 11 | // TODO need to test incoming network activity 12 | public class SocketsTests : IntegrationTestBase 13 | { 14 | [Test] 15 | public void Given_No_Network_Activity_Then_Metrics_Should_Not_Increase() 16 | { 17 | Assert.That(MetricProducer.BytesReceived.Value, Is.Zero); 18 | Assert.That(MetricProducer.BytesSent.Value, Is.Zero); 19 | Assert.That(MetricProducer.IncomingConnectionEstablished.Value, Is.Zero); 20 | Assert.That(MetricProducer.OutgoingConnectionEstablished.Value, Is.Zero); 21 | } 22 | 23 | [Test] 24 | public async Task Given_A_HTTP_Request_Then_Outgoing_metrics_Should_Increase() 25 | { 26 | // arrange 27 | using var client = new HttpClient(new SocketsHttpHandler() 28 | { 29 | PooledConnectionLifetime = TimeSpan.MaxValue, 30 | MaxConnectionsPerServer = 10 31 | }); 32 | 33 | // act 34 | var requests = Enumerable.Range(1, 20) 35 | .Select(n => client.GetAsync("https://httpstat.us/200?sleep=3000")) 36 | .ToArray(); 37 | 38 | // assert 39 | Assert.That(() => MetricProducer.OutgoingConnectionEstablished.Value, Is.GreaterThanOrEqualTo(10).After(2_000, 100)); 40 | Assert.That(MetricProducer.BytesSent.Value, Is.GreaterThan(0)); 41 | 42 | await Task.WhenAll(requests); 43 | client.Dispose(); 44 | } 45 | 46 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 47 | { 48 | return toConfigure.WithSocketStats(); 49 | } 50 | } 51 | #endif 52 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/IntegrationTests/ThreadPoolTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Runtime.InteropServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using NUnit.Framework; 8 | using Prometheus.DotNetRuntime.Metrics; 9 | using Prometheus.DotNetRuntime.Metrics.Producers; 10 | 11 | namespace Prometheus.DotNetRuntime.Tests.IntegrationTests 12 | { 13 | internal class Given_Only_Runtime_Counters_Are_Enabled_For_ThreadPoolStats : IntegrationTestBase 14 | { 15 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 16 | { 17 | return toConfigure.WithThreadPoolStats(CaptureLevel.Counters); 18 | } 19 | 20 | [Test] 21 | public async Task When_work_is_executed_on_the_thread_pool_then_executed_work_is_measured() 22 | { 23 | var startingThroughput = MetricProducer.Throughput.Value; 24 | const int numTasksToSchedule = 100; 25 | // schedule a bunch of tasks 26 | var tasks = Enumerable.Range(1, numTasksToSchedule) 27 | .Select(_ => Task.Run(() => { })); 28 | 29 | await Task.WhenAll(tasks); 30 | 31 | Assert.That(() => MetricProducer.NumThreads.Value, Is.GreaterThanOrEqualTo(Environment.ProcessorCount).After(2_000, 10)); 32 | Assert.That(() => MetricProducer.Throughput.Value, Is.GreaterThanOrEqualTo(startingThroughput + numTasksToSchedule).After(2_000, 10)); 33 | } 34 | 35 | [Test] 36 | public async Task When_timers_are_active_then_they_are_measured() 37 | { 38 | var startingTimers = MetricProducer.NumTimers.Value; 39 | const int numTimersToSchedule = 100; 40 | // schedule a bunch of timers 41 | var tasks = Enumerable.Range(1, numTimersToSchedule) 42 | .Select(n => Task.Delay(3000 + n)) 43 | .ToArray(); 44 | 45 | Assert.That(() => MetricProducer.NumTimers.Value, Is.GreaterThanOrEqualTo(startingTimers + numTimersToSchedule).After(2_000, 10)); 46 | } 47 | 48 | [Test] 49 | public async Task When_blocking_work_is_executed_on_the_thread_pool_then_thread_pool_delays_are_measured() 50 | { 51 | var startingQueueLength = MetricProducer.QueueLength.Sum; 52 | var sleepDelay = TimeSpan.FromMilliseconds(250); 53 | int desiredSecondsToBlock = 5; 54 | int numTasksToSchedule = (int)(Environment.ProcessorCount / sleepDelay.TotalSeconds) * desiredSecondsToBlock; 55 | 56 | Console.WriteLine($"Scheduling {numTasksToSchedule} blocking tasks..."); 57 | // schedule a bunch of blocking tasks that will make the thread pool will grow 58 | var tasks = Enumerable.Range(1, numTasksToSchedule) 59 | .Select(_ => Task.Run(() => Thread.Sleep(sleepDelay))) 60 | .ToArray(); 61 | 62 | // dont' wait for the tasks to complete- we want to see stats present during a period of thread pool starvation 63 | 64 | Assert.That(() => MetricProducer.NumThreads.Value, Is.GreaterThan(Environment.ProcessorCount).After(desiredSecondsToBlock * 1000, 10)); 65 | Assert.That(() => MetricProducer.QueueLength.Sum, Is.GreaterThan(startingQueueLength).After(desiredSecondsToBlock * 1000, 10)); 66 | } 67 | } 68 | 69 | internal class Given_Runtime_Counters_And_ThreadPool_Info_Events_Are_Enabled_For_ThreadPoolStats : IntegrationTestBase 70 | { 71 | protected override DotNetRuntimeStatsBuilder.Builder ConfigureBuilder(DotNetRuntimeStatsBuilder.Builder toConfigure) 72 | { 73 | return toConfigure.WithThreadPoolStats(CaptureLevel.Informational); 74 | } 75 | 76 | [Test] 77 | public async Task When_work_is_executed_on_the_thread_pool_then_executed_work_is_measured() 78 | { 79 | // schedule a bunch of blocking tasks that will make the thread pool will grow 80 | var tasks = Enumerable.Range(1, 1000) 81 | .Select(_ => Task.Run(() => Thread.Sleep(20))); 82 | 83 | await Task.WhenAll(tasks); 84 | 85 | Assert.That(() => MetricProducer.NumThreads.Value, Is.GreaterThanOrEqualTo(Environment.ProcessorCount).After(2000, 10)); 86 | Assert.That(MetricProducer.AdjustmentsTotal.CollectAllValues().Sum(), Is.GreaterThanOrEqualTo(1)); 87 | } 88 | 89 | [Test] 90 | public async Task When_IO_work_is_executed_on_the_thread_pool_then_the_number_of_io_threads_is_measured() 91 | { 92 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 93 | Assert.Inconclusive("Cannot run this test on non-windows platforms."); 94 | 95 | // need to schedule a bunch of IO work to make the IO pool grow 96 | using (var client = new HttpClient()) 97 | { 98 | var httpTasks = Enumerable.Range(1, 50) 99 | .Select(_ => client.GetAsync("http://google.com")); 100 | 101 | await Task.WhenAll(httpTasks); 102 | } 103 | 104 | Assert.That(() => MetricProducer.NumIocThreads.Value, Is.GreaterThanOrEqualTo(1).After(2000, 10)); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/Metrics/Producers/Util/LabelGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Prometheus.DotNetRuntime.EventListening.EventSources; 3 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 4 | 5 | namespace Prometheus.DotNetRuntime.Tests.Metrics.Producers.Util 6 | { 7 | [TestFixture] 8 | public class LabelGeneratorTests 9 | { 10 | [Test] 11 | public void MapEnumToLabelValues_will_generate_labels_with_snake_cased_names() 12 | { 13 | var labels = LabelGenerator.MapEnumToLabelValues(); 14 | 15 | Assert.That(labels[DotNetRuntimeEventSource.GCReason.AllocLarge], Is.EqualTo("alloc_large")); 16 | Assert.That(labels[DotNetRuntimeEventSource.GCReason.OutOfSpaceLOH], Is.EqualTo("out_of_space_loh")); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/Metrics/Producers/Util/RatioTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using NUnit.Framework; 6 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 7 | 8 | namespace Prometheus.DotNetRuntime.Tests.Metrics.Producers.Util 9 | { 10 | [TestFixture] 11 | public class RatioTests 12 | { 13 | private MetricFactory _metricFactory; 14 | 15 | [SetUp] 16 | public void SetUp() 17 | { 18 | _metricFactory = Prometheus.Metrics.WithCustomRegistry(Prometheus.Metrics.NewCustomRegistry()); 19 | } 20 | 21 | [Test] 22 | public void CalculateConsumedRatio_returns_zero_if_no_time_has_been_consumed() 23 | { 24 | // arrange 25 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); 26 | 27 | Assert.That(ratio.CalculateConsumedRatio(0.0), Is.EqualTo(0)); 28 | } 29 | 30 | [Test] 31 | [TestCase(1.0, 0.1)] 32 | [TestCase(5.0, 0.5)] 33 | [TestCase(10.0, 1.0)] 34 | public void CalculateConsumedRatio_returns_ratio_if_time_has_been_consumed(double secondsConsumedByEvents, double expectedRatio) 35 | { 36 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(10)); 37 | 38 | Assert.That(ratio.CalculateConsumedRatio(secondsConsumedByEvents), Is.EqualTo(expectedRatio)); 39 | } 40 | 41 | [Test] 42 | public void CalculateConsumedRatio_accounts_for_initial_offset_consumption() 43 | { 44 | var ratio = Arrange_ratio(TimeSpan.FromSeconds(8), TimeSpan.FromSeconds(10)); 45 | 46 | Assert.That(ratio.CalculateConsumedRatio(0.5), Is.EqualTo(0.25)); 47 | } 48 | 49 | [Test] 50 | public void CalculateConsumedRatio_stores_previous_process_and_event_time_consumption() 51 | { 52 | // arrange 53 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(8)); 54 | 55 | // act + assert 56 | Assert.That(ratio.CalculateConsumedRatio(0.99), Is.EqualTo(0.99)); 57 | Assert.That(ratio.CalculateConsumedRatio(0.99 + 0.1), Is.EqualTo(0.025).Within(0.000001)); 58 | Assert.That(ratio.CalculateConsumedRatio(0.99 + 0.1 + 0.5), Is.EqualTo(0.1666666).Within(0.000001)); 59 | } 60 | 61 | [Test] 62 | public void CalculateConsumedRatio_returns_zero_if_negative_time_has_been_consumed() 63 | { 64 | // arrange 65 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); 66 | 67 | // act + assert 68 | Assert.That(ratio.CalculateConsumedRatio(0.5), Is.EqualTo(0.5)); 69 | Assert.That(ratio.CalculateConsumedRatio(0.49), Is.EqualTo(0.0)); 70 | } 71 | 72 | [Test] 73 | public void CalculateConsumedRatio_truncates_the_value_if_more_than_100_percent_has_been_consumed() 74 | { 75 | // arrange 76 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(1)); 77 | 78 | // act + assert 79 | Assert.That(ratio.CalculateConsumedRatio(1.1), Is.EqualTo(1.0)); 80 | } 81 | 82 | [Test] 83 | public void CalculateConsumedRatio_can_extract_the_consumed_event_time_from_an_unlabeled_counter() 84 | { 85 | // arrange 86 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(5)); 87 | var counter = _metricFactory.CreateCounter("event_time_total_seconds", ""); 88 | counter.Inc(2.5); 89 | 90 | // act + assert 91 | Assert.That(ratio.CalculateConsumedRatio(counter), Is.EqualTo(0.5)); 92 | } 93 | 94 | [Test] 95 | public void CalculateConsumedRatio_can_extract_the_consumed_event_time_from_a_labeled_counter() 96 | { 97 | // arrange 98 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(5)); 99 | var counter = _metricFactory.CreateCounter("event_time_total_seconds", "", "label_1", "label_2"); 100 | counter.Labels("a", "b").Inc(1.0); 101 | counter.Labels("a", "c").Inc(0.25); 102 | counter.Labels("d", "e").Inc(1.25); 103 | 104 | // act + assert 105 | Assert.That(ratio.CalculateConsumedRatio(counter), Is.EqualTo(0.5)); 106 | } 107 | 108 | [Test] 109 | public void CalculateConsumedRatio_can_extract_the_consumed_event_time_from_an_unlabeled_histogram() 110 | { 111 | // arrange 112 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(5)); 113 | var histo = _metricFactory.CreateHistogram("event_time_seconds", ""); 114 | histo.Observe(1.5); 115 | histo.Observe(1.0); 116 | 117 | // act + assert 118 | Assert.That(ratio.CalculateConsumedRatio(histo), Is.EqualTo(0.5)); 119 | } 120 | 121 | [Test] 122 | public void CalculateConsumedRatio_can_extract_the_consumed_event_time_from_a_labeled_histogram() 123 | { 124 | // arrange 125 | var ratio = Arrange_ratio(TimeSpan.Zero, TimeSpan.FromSeconds(5)); 126 | var histo = _metricFactory.CreateHistogram("event_time_total_seconds", "", labelNames: new [] {"label1", "label2"}); 127 | histo.Labels("a", "b").Observe(1.0); 128 | histo.Labels("a", "c").Observe(0.25); 129 | histo.Labels("d", "e").Observe(1.25); 130 | 131 | // act + assert 132 | Assert.That(ratio.CalculateConsumedRatio(histo), Is.EqualTo(0.5)); 133 | } 134 | 135 | [Test] 136 | public void ProcessTime_CalculateConsumedRatio_initalises_using_the_current_time_at_creation() 137 | { 138 | // arrange 139 | var processTimeRatio = Ratio.ProcessTime(); 140 | Thread.Sleep(100); 141 | 142 | // act + assert 143 | Assert.That(processTimeRatio.CalculateConsumedRatio(0.01), Is.EqualTo(0.1).Within(0.05)); 144 | } 145 | 146 | private Ratio Arrange_ratio(params TimeSpan[] processorTimes) 147 | { 148 | var enumerator = processorTimes.AsEnumerable().GetEnumerator(); 149 | 150 | return new Ratio(() => 151 | { 152 | if (!enumerator.MoveNext()) 153 | { 154 | Assert.Fail("Did not pass sufficient processor times!"); 155 | } 156 | 157 | return enumerator.Current; 158 | }); 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/Metrics/Producers/Util/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 3 | 4 | namespace Prometheus.DotNetRuntime.Tests.Metrics.Producers.Util 5 | { 6 | public class StringExtensionsTests 7 | { 8 | [TestCase("", "")] 9 | [TestCase("myGreatVariableName", "my_great_variable_name")] 10 | [TestCase("my_great_variable_name", "my_great_variable_name")] 11 | [TestCase("MyGreatVariableName", "my_great_variable_name")] 12 | public void ToSnakeCase_Should_Convert_To_Snake_Case(string given, string expected) 13 | { 14 | Assert.AreEqual(given.ToSnakeCase(), expected); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/Properties.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | [assembly: Timeout(30_000)] -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime.Tests/prometheus-net.DotNetRuntime.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netcoreapp3.1;net5.0;net6.0 6 | false 7 | Prometheus.DotNetRuntime.Tests 8 | AnyCPU 9 | true 10 | 10 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/CaptureLevel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Tracing; 2 | 3 | namespace Prometheus.DotNetRuntime 4 | { 5 | /// 6 | /// Specifies the fidelity of events captured. 7 | /// 8 | /// 9 | /// In order to produce metrics this library collects events from the .NET runtime. The level chosen impacts both the performance of 10 | /// your application (the more detailed events .NET produces the more CPU it consumes to produce them) and the level of detail present in the metrics 11 | /// produced by this library (the more detailed events prometheus-net.DotNetRuntime captures, the more analysis it can perform). 12 | /// 13 | public enum CaptureLevel 14 | { 15 | /// 16 | /// Collect event counters only- limited metrics will be available. 17 | /// 18 | Counters = EventLevel.LogAlways, 19 | Errors = EventLevel.Error, 20 | Informational = EventLevel.Informational, 21 | /// 22 | /// Collects events at level Verbose and all other levels- produces the highest level of metric detail. 23 | /// 24 | Verbose = EventLevel.Verbose, 25 | } 26 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventConsumption.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening; 2 | using Prometheus.DotNetRuntime.Metrics; 3 | 4 | namespace Prometheus.DotNetRuntime 5 | { 6 | /// 7 | /// Used to communicate that a depends on the events generated by an or . 8 | /// 9 | /// 10 | public interface Consumes 11 | where TEvents : IEvents 12 | { 13 | public TEvents Events { get; } 14 | 15 | /// 16 | /// Indicates if the events of will be produced and can be listened to. 17 | /// 18 | /// 19 | /// As event parsers may or may not be enabled (or enabled at lower event levels), we need a mechanism to indicate if 20 | /// events are available or not to generate metrics from. 21 | /// 22 | public bool Enabled { get; } 23 | } 24 | 25 | internal class EventConsumer : Consumes 26 | where T : IEvents 27 | { 28 | public EventConsumer() 29 | { 30 | Enabled = false; 31 | } 32 | 33 | public EventConsumer(T events) 34 | { 35 | Events = events; 36 | Enabled = true; 37 | } 38 | 39 | public T Events { get; } 40 | public bool Enabled { get; set; } 41 | } 42 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/CounterNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening 4 | { 5 | [AttributeUsage(AttributeTargets.Event)] 6 | public class CounterNameAttribute : Attribute 7 | { 8 | public CounterNameAttribute(string name) 9 | { 10 | Name = name; 11 | } 12 | 13 | public string Name { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Counters.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.DotNetRuntime.EventListening 2 | { 3 | public readonly struct MeanCounterValue 4 | { 5 | public MeanCounterValue(int count, double mean) 6 | { 7 | Count = count; 8 | Mean = mean; 9 | } 10 | 11 | public int Count { get; } 12 | public double Mean { get; } 13 | public double Total => Count * Mean; 14 | } 15 | 16 | public readonly struct IncrementingCounterValue 17 | { 18 | public IncrementingCounterValue(double value) 19 | { 20 | IncrementedBy = value; 21 | } 22 | 23 | public double IncrementedBy { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/DotNetEventListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.Tracing; 5 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 6 | 7 | namespace Prometheus.DotNetRuntime.EventListening 8 | { 9 | internal sealed class DotNetEventListener : EventListener 10 | { 11 | private readonly GlobalOptions _globalOptions; 12 | private readonly string _nameSnakeCase; 13 | private readonly HashSet _enabledEventSources = new(); 14 | private readonly Stopwatch _sp; 15 | private HashSet _threadIdsPublishingEvents; 16 | 17 | internal DotNetEventListener(IEventListener eventListener, EventLevel level, GlobalOptions globalOptions) 18 | { 19 | Level = level; 20 | EventListener = eventListener; 21 | _globalOptions = globalOptions; 22 | 23 | if (_globalOptions.EnabledDebuggingMetrics) 24 | { 25 | _nameSnakeCase = eventListener.GetType().Name.ToSnakeCase(); 26 | _sp = new Stopwatch(); 27 | _threadIdsPublishingEvents = new HashSet(); 28 | } 29 | 30 | EventSourceCreated += OnEventSourceCreated; 31 | } 32 | 33 | public EventLevel Level { get; } 34 | internal bool StartedReceivingEvents { get; private set; } 35 | internal IEventListener EventListener { get; private set; } 36 | 37 | private void OnEventSourceCreated(object sender, EventSourceCreatedEventArgs e) 38 | { 39 | var es = e.EventSource; 40 | if (es.Name == EventListener.EventSourceName) 41 | { 42 | EnableEvents(es, Level, EventListener.Keywords, GetEventListenerArguments(EventListener)); 43 | _enabledEventSources.Add(e.EventSource); 44 | StartedReceivingEvents = true; 45 | } 46 | } 47 | 48 | private Dictionary GetEventListenerArguments(IEventListener listener) 49 | { 50 | var args = new Dictionary(); 51 | if (listener is IEventCounterListener counterListener) 52 | { 53 | args["EventCounterIntervalSec"] = counterListener.RefreshIntervalSeconds.ToString(); 54 | } 55 | 56 | return args; 57 | } 58 | 59 | protected override void OnEventWritten(EventWrittenEventArgs eventData) 60 | { 61 | try 62 | { 63 | if (_globalOptions.EnabledDebuggingMetrics) 64 | { 65 | _globalOptions.DebuggingMetrics.EventTypeCounts.Labels(_nameSnakeCase, eventData.EventSource.Name, eventData.EventName ?? "unknown").Inc(); 66 | _sp.Restart(); 67 | _threadIdsPublishingEvents.Add(eventData.OSThreadId); 68 | _globalOptions.DebuggingMetrics.ThreadCount.Labels(_nameSnakeCase).Set(_threadIdsPublishingEvents.Count); 69 | } 70 | 71 | // Event counters are present in every EventListener, regardless of if they subscribed to them. 72 | // Kind of odd but just filter them out by source here. 73 | if (eventData.EventSource.Name == EventListener.EventSourceName) 74 | EventListener.ProcessEvent(eventData); 75 | 76 | if (_globalOptions.EnabledDebuggingMetrics) 77 | { 78 | _sp.Stop(); 79 | _globalOptions.DebuggingMetrics.TimeConsumed.Labels(_nameSnakeCase, eventData.EventSource.Name, eventData.EventName ?? "unknown").Inc(_sp.Elapsed.TotalSeconds); 80 | } 81 | } 82 | catch (Exception e) 83 | { 84 | _globalOptions.ErrorHandler(e); 85 | } 86 | } 87 | 88 | public override void Dispose() 89 | { 90 | EventSourceCreated -= OnEventSourceCreated; 91 | EventListener.Dispose(); 92 | base.Dispose(); 93 | } 94 | 95 | internal class GlobalOptions 96 | { 97 | internal GlobalOptions() 98 | { 99 | } 100 | 101 | internal static GlobalOptions CreateFrom(DotNetRuntimeStatsCollector.Options opts, MetricFactory factory) 102 | { 103 | var instance = new GlobalOptions(); 104 | if (opts.EnabledDebuggingMetrics) 105 | { 106 | instance.DebuggingMetrics = new( 107 | factory.CreateCounter($"dotnet_debug_event_count_total", "The total number of .NET diagnostic events processed", "listener_name", "event_source_name", "event_name"), 108 | factory.CreateCounter("dotnet_debug_event_seconds_total", 109 | "The total time consumed by processing .NET diagnostic events (does not include the CPU cost to generate the events)", 110 | "listener_name", "event_source_name", "event_name"), 111 | factory.CreateGauge("dotnet_debug_publish_thread_count", "The number of threads that have published events", "listener_name") 112 | ); 113 | } 114 | 115 | instance.EnabledDebuggingMetrics = opts.EnabledDebuggingMetrics; 116 | instance.ErrorHandler = opts.ErrorHandler; 117 | 118 | return instance; 119 | } 120 | 121 | public Action ErrorHandler { get; set; } = (e => { }); 122 | public bool EnabledDebuggingMetrics { get; set; } = false; 123 | public DebugMetrics DebuggingMetrics { get; set; } 124 | 125 | public class DebugMetrics 126 | { 127 | public DebugMetrics(Counter eventTypeCounts, Counter timeConsumed, Gauge threadCount) 128 | { 129 | EventTypeCounts = eventTypeCounts; 130 | TimeConsumed = timeConsumed; 131 | ThreadCount = threadCount; 132 | } 133 | public Counter TimeConsumed { get; } 134 | public Counter EventTypeCounts { get; } 135 | public Gauge ThreadCount { get; } 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/EventCounterParserBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.Tracing; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading; 8 | using Prometheus.DotNetRuntime.EventListening; 9 | 10 | namespace Prometheus.DotNetRuntime.EventListening 11 | { 12 | /// 13 | /// A reflection-based event parser that can extract typed counter values for a given event counter source. 14 | /// 15 | /// 16 | /// While using reflection isn't ideal from a performance standpoint, this is fine for now- event counters are collected at most 17 | /// every second so won't have to deal with high throughput of events. 18 | /// 19 | /// 20 | public abstract class EventCounterParserBase : IEventCounterParser 21 | where T : ICounterEvents 22 | { 23 | private readonly Dictionary>> _countersToParsers; 24 | private long _timeSinceLastCounter; 25 | 26 | protected EventCounterParserBase() 27 | { 28 | var eventNames = GetType().GetInterfaces() 29 | .Where(x => typeof(ICounterEvents).IsAssignableFrom(x)) 30 | .SelectMany(x => x.GetEvents(), (t, e) => e.Name) 31 | .ToHashSet(); 32 | 33 | var eventsAndNameAttrs = GetType() 34 | .GetEvents() 35 | .Where(e => eventNames.Contains(e.Name)) 36 | .Select(x => (@event: x, nameAttr: x.GetCustomAttribute())) 37 | .ToArray(); 38 | 39 | if (eventsAndNameAttrs.Length == 0) 40 | throw new Exception("Could not locate any events to map to event counters!"); 41 | 42 | var eventsWithoutAttrs = eventsAndNameAttrs.Where(x => x.nameAttr == null).ToArray(); 43 | if (eventsWithoutAttrs.Length > 0) 44 | throw new Exception($"All events part of an {nameof(ICounterEvents)} interface require a [{nameof(CounterNameAttribute)}] attribute. Events without attribute: {string.Join(", ", eventsWithoutAttrs.Select(x => x.@event.Name))}."); 45 | 46 | _countersToParsers = eventsAndNameAttrs.ToDictionary( 47 | k => k.nameAttr.Name, 48 | v => GetParseFunction(v.@event, v.nameAttr.Name) 49 | ); 50 | } 51 | 52 | public abstract string EventSourceName { get; } 53 | public virtual EventKeywords Keywords { get; } = EventKeywords.All; 54 | public virtual int RefreshIntervalSeconds { get; set; } = 1; 55 | 56 | public void ProcessEvent(EventWrittenEventArgs e) 57 | { 58 | if (e.EventName == null || !e.EventName.Equals("EventCounters")) 59 | return; 60 | 61 | var eventPayload = e.Payload[0] as IDictionary; 62 | if (eventPayload == null) 63 | return; 64 | 65 | Interlocked.Exchange(ref _timeSinceLastCounter, Stopwatch.GetTimestamp()); 66 | 67 | if (eventPayload.TryGetValue("Name", out var p) && p is string counterName) 68 | { 69 | if (!_countersToParsers.TryGetValue(counterName, out var parser)) 70 | return; 71 | 72 | parser(eventPayload); 73 | } 74 | } 75 | 76 | private Action> GetParseFunction(EventInfo @event, string counterName) 77 | { 78 | var eventField = GetType().GetField(@event.Name, BindingFlags.NonPublic | BindingFlags.Instance); 79 | 80 | if (eventField == null) 81 | throw new Exception($"Unable to locate backing field for event '{@event.Name}'."); 82 | 83 | var type = @event.EventHandlerType.GetGenericArguments().Single(); 84 | Func, (bool, object)> parseCounterFunc; 85 | 86 | if (type == typeof(IncrementingCounterValue)) 87 | { 88 | parseCounterFunc = TryParseIncrementingCounter; 89 | } 90 | else if (type == typeof(MeanCounterValue)) 91 | { 92 | parseCounterFunc = TryParseCounter; 93 | } 94 | else 95 | { 96 | throw new Exception($"Unexpected counter type '{type}'!"); 97 | } 98 | 99 | return payload => 100 | { 101 | var eventDelegate = (MulticastDelegate)eventField.GetValue(this); 102 | 103 | // No-one is listening to this event 104 | if (eventDelegate == null) 105 | return; 106 | 107 | foreach (var handler in eventDelegate.GetInvocationList()) 108 | { 109 | var (success, value) = parseCounterFunc(payload); 110 | if (success) 111 | handler.Method.Invoke(handler.Target, new []{ value }); 112 | else 113 | { 114 | throw new MismatchedCounterTypeException($"Counter '{counterName}' could not be parsed by function {parseCounterFunc.Method} indicating the counter has been declared as the wrong type."); 115 | } 116 | } 117 | }; 118 | } 119 | 120 | private (bool, object) TryParseIncrementingCounter(IDictionary payload) 121 | { 122 | if (payload.TryGetValue("Increment", out var increment)) 123 | return (true, new IncrementingCounterValue((double)increment)); 124 | 125 | return (false, new IncrementingCounterValue()); 126 | } 127 | 128 | private (bool, object) TryParseCounter(IDictionary payload) 129 | { 130 | if (payload.TryGetValue("Mean", out var mean) && payload.TryGetValue("Count", out var count)) 131 | return (true, new MeanCounterValue((int)count, (double)mean)); 132 | 133 | return (false, new MeanCounterValue()); 134 | } 135 | 136 | public void Dispose() 137 | { 138 | } 139 | } 140 | 141 | public class MismatchedCounterTypeException : Exception 142 | { 143 | public MismatchedCounterTypeException(string message) : base(message) 144 | { 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/EventParserTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Diagnostics.Tracing; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.InteropServices; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace Prometheus.DotNetRuntime.EventListening 11 | { 12 | internal static class EventParserTypes 13 | { 14 | private static readonly ImmutableHashSet InterfaceTypesToIgnore = new[] 15 | { 16 | typeof(IEvents), 17 | typeof(IVerboseEvents), 18 | typeof(IInfoEvents), 19 | typeof(IWarningEvents), 20 | typeof(IErrorEvents), 21 | typeof(IAlwaysEvents), 22 | typeof(ICriticalEvents), 23 | typeof(ICounterEvents), 24 | }.ToImmutableHashSet(); 25 | 26 | internal static IEnumerable GetEventInterfaces(Type t) 27 | { 28 | return t.GetInterfaces() 29 | .Where(i => typeof(IEvents).IsAssignableFrom(i) && !InterfaceTypesToIgnore.Contains(i)); 30 | } 31 | 32 | internal static IEnumerable GetEventInterfaces(Type t, EventLevel atLevelAndBelow) 33 | { 34 | return GetEventInterfaces(t) 35 | .Where(t => GetEventLevel(t) <= atLevelAndBelow); 36 | } 37 | 38 | internal static IEnumerable GetEventInterfacesForCurrentRuntime(Type t, EventLevel atLevelAndBelow) 39 | { 40 | return GetEventInterfaces(t, atLevelAndBelow) 41 | .Where(AreEventsSupportedByRuntime); 42 | } 43 | 44 | internal static ImmutableHashSet GetLevelsFromParser(Type type) 45 | { 46 | return GetEventInterfaces(type) 47 | .Select(GetEventLevel) 48 | .ToImmutableHashSet(); 49 | } 50 | 51 | private static EventLevel GetEventLevel(Type t) 52 | { 53 | // Captures ICounterEvents too as it inherits from IAlwaysEvents 54 | if (typeof(IAlwaysEvents).IsAssignableFrom(t)) 55 | return EventLevel.LogAlways; 56 | 57 | if (typeof(IVerboseEvents).IsAssignableFrom(t)) 58 | return EventLevel.Verbose; 59 | 60 | if (typeof(IInfoEvents).IsAssignableFrom(t)) 61 | return EventLevel.Informational; 62 | 63 | if (typeof(IWarningEvents).IsAssignableFrom(t)) 64 | return EventLevel.Warning; 65 | 66 | if (typeof(IErrorEvents).IsAssignableFrom(t)) 67 | return EventLevel.Error; 68 | 69 | if (typeof(ICriticalEvents).IsAssignableFrom(t)) 70 | return EventLevel.Critical; 71 | 72 | throw new InvalidOperationException($"Unexpected type '{t}'"); 73 | } 74 | 75 | internal static IEnumerable GetEventParsers() 76 | { 77 | return GetEventParsers(typeof(IEventListener).Assembly); 78 | } 79 | 80 | internal static IEnumerable GetEventParsers(Assembly fromAssembly) 81 | { 82 | return fromAssembly 83 | .GetTypes() 84 | .Where(x => x.IsClass && x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEventParser<>))); 85 | } 86 | 87 | internal static Lazy CurrentRuntimeVerison = new Lazy(() => 88 | { 89 | var split = RuntimeInformation.FrameworkDescription.Split(' '); 90 | if (split.Length < 2) 91 | return null; 92 | 93 | var versionPart = split[^1]; 94 | // Handle preview version strings, e.g. .NET 6.0.0-preview.7.21377.19. 95 | var hyphenIndex = versionPart.IndexOf('-'); 96 | if (hyphenIndex > -1) 97 | versionPart = versionPart.Substring(0, hyphenIndex); 98 | 99 | if (Version.TryParse(versionPart, out var version)) 100 | return new Version(version.Major, version.Minor); 101 | 102 | return null; 103 | }); 104 | 105 | internal static bool AreEventsSupportedByRuntime(Type type) 106 | { 107 | var eventVer = GetVersionOfEvents(type); 108 | 109 | if (CurrentRuntimeVerison.Value == null) 110 | // Assume if this is being run, it's on .net core 3.1+ 111 | return eventVer == LowestSupportedVersion; 112 | 113 | return eventVer <= CurrentRuntimeVerison.Value; 114 | } 115 | 116 | 117 | private static readonly Version LowestSupportedVersion = new Version(3, 1); 118 | private static readonly Regex VersionRegex = new Regex("V(?[0-9]+)_(?[0-9]+)", RegexOptions.Compiled); 119 | private static Version GetVersionOfEvents(Type type) 120 | { 121 | if (!typeof(IEvents).IsAssignableFrom(type)) 122 | throw new ArgumentException($"Type {type} does not implement {nameof(IEvents)}"); 123 | 124 | var match = VersionRegex.Match(type.Name); 125 | 126 | if (match == null || !match.Success) 127 | // Defaults to 3.0 (haven't converted all existed interfaces into type interfaces) 128 | return new Version(3, 0); 129 | 130 | return new Version(int.Parse(match.Groups["major"].Value), int.Parse(match.Groups["minor"].Value)); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/IEventCounterListener.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.DotNetRuntime.EventListening 2 | { 3 | /// 4 | /// An that listens for event counters. 5 | /// 6 | public interface IEventCounterListener : IEventListener 7 | { 8 | public int RefreshIntervalSeconds { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/IEventCounterParser.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening 4 | { 5 | /// 6 | /// An that turns untyped counter values into strongly-typed counter events. 7 | /// 8 | /// 9 | public interface IEventCounterParser : IEventParser, IEventCounterListener 10 | where TCounters : ICounterEvents 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/IEventListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Diagnostics.Tracing; 4 | 5 | namespace Prometheus.DotNetRuntime.EventListening 6 | { 7 | public interface IEventListener : IDisposable 8 | { 9 | /// 10 | /// The name of the event source to receive events from. 11 | /// 12 | string EventSourceName { get; } 13 | 14 | /// 15 | /// The keywords to enable in the event source. 16 | /// 17 | /// 18 | /// Keywords act as a "if-any-match" filter- specify multiple keywords to obtain multiple categories of events 19 | /// from the event source. 20 | /// 21 | EventKeywords Keywords { get; } 22 | 23 | /// 24 | /// The levels of events supported. 25 | /// 26 | ImmutableHashSet SupportedLevels { get; } 27 | 28 | /// 29 | /// Process a received event. 30 | /// 31 | /// 32 | /// Implementors should listen to events and perform some kind of processing. 33 | /// 34 | void ProcessEvent(EventWrittenEventArgs e); 35 | 36 | void IDisposable.Dispose() 37 | { 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/IEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Diagnostics.Tracing; 4 | using Prometheus.DotNetRuntime.EventListening; 5 | 6 | namespace Prometheus.DotNetRuntime.EventListening 7 | { 8 | /// 9 | /// A that receives "untyped" events of into strongly-typed events. 10 | /// 11 | /// 12 | /// Represents the set of strongly-typed events emitted by this parser. Implementors should not directly implement , rather 13 | /// implement inheriting interfaces such as , , etc. 14 | /// 15 | public interface IEventParser : IEventListener 16 | where TEvents : IEvents 17 | { 18 | ImmutableHashSet IEventListener.SupportedLevels => EventParserDefaults.GetSupportedLevels(this); 19 | 20 | private static class EventParserDefaults 21 | { 22 | private static ImmutableHashSet SupportedLevels; 23 | 24 | public static ImmutableHashSet GetSupportedLevels(IEventParser listener) 25 | { 26 | if (SupportedLevels == null) 27 | SupportedLevels = EventParserTypes.GetLevelsFromParser(listener.GetType()); 28 | 29 | return SupportedLevels; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/IEvents.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.DotNetRuntime.EventListening 2 | { 3 | public interface IEvents 4 | { 5 | } 6 | 7 | public interface IInfoEvents : IEvents 8 | { 9 | } 10 | 11 | public interface IVerboseEvents : IEvents 12 | { 13 | } 14 | 15 | public interface IErrorEvents : IEvents 16 | { 17 | } 18 | 19 | public interface ICriticalEvents : IEvents 20 | { 21 | } 22 | 23 | public interface IWarningEvents : IEvents 24 | { 25 | } 26 | 27 | public interface IAlwaysEvents : IEvents 28 | { 29 | } 30 | 31 | public interface ICounterEvents : IAlwaysEvents 32 | { 33 | } 34 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/ContentionEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using Prometheus.DotNetRuntime.EventListening.EventSources; 4 | using Prometheus.DotNetRuntime.EventListening.Parsers.Util; 5 | 6 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 7 | { 8 | public class ContentionEventParser : IEventParser, ContentionEventParser.Events.Info 9 | { 10 | private readonly SamplingRate _samplingRate; 11 | private const int EventIdContentionStart = 81, EventIdContentionStop = 91; 12 | private readonly EventPairTimer _eventPairTimer; 13 | 14 | public event Action ContentionStart; 15 | public event Action ContentionEnd; 16 | 17 | public ContentionEventParser(SamplingRate samplingRate) 18 | { 19 | _samplingRate = samplingRate; 20 | _eventPairTimer = new EventPairTimer( 21 | EventIdContentionStart, 22 | EventIdContentionStop, 23 | x => x.OSThreadId, 24 | samplingRate 25 | ); 26 | } 27 | 28 | public EventKeywords Keywords => (EventKeywords)DotNetRuntimeEventSource.Keywords.Contention; 29 | public string EventSourceName => DotNetRuntimeEventSource.Name; 30 | 31 | public void ProcessEvent(EventWrittenEventArgs e) 32 | { 33 | switch (_eventPairTimer.TryGetDuration(e, out var duration)) 34 | { 35 | case DurationResult.Start: 36 | ContentionStart?.Invoke(Events.ContentionStartEvent.Instance); 37 | return; 38 | 39 | case DurationResult.FinalWithDuration: 40 | ContentionEnd?.InvokeManyTimes(_samplingRate.SampleEvery, Events.ContentionEndEvent.GetFrom(duration)); 41 | return; 42 | 43 | default: 44 | return; 45 | } 46 | } 47 | 48 | public static class Events 49 | { 50 | public interface Info : IInfoEvents 51 | { 52 | event Action ContentionStart; 53 | event Action ContentionEnd; 54 | } 55 | 56 | 57 | 58 | public class ContentionStartEvent 59 | { 60 | public static readonly ContentionStartEvent Instance = new(); 61 | 62 | private ContentionStartEvent() 63 | { 64 | } 65 | } 66 | 67 | public class ContentionEndEvent 68 | { 69 | private static readonly ContentionEndEvent Instance = new(); 70 | 71 | private ContentionEndEvent() 72 | { 73 | } 74 | 75 | public TimeSpan ContentionDuration { get; private set; } 76 | 77 | public static ContentionEndEvent GetFrom(TimeSpan contentionDuration) 78 | { 79 | Instance.ContentionDuration = contentionDuration; 80 | return Instance; 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/ExceptionEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using Prometheus.DotNetRuntime.EventListening.EventSources; 4 | 5 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 6 | { 7 | public class ExceptionEventParser : IEventParser, ExceptionEventParser.Events.Error 8 | { 9 | public event Action ExceptionThrown; 10 | 11 | public string EventSourceName => DotNetRuntimeEventSource.Name; 12 | public EventKeywords Keywords => (EventKeywords) DotNetRuntimeEventSource.Keywords.Exception; 13 | 14 | public void ProcessEvent(EventWrittenEventArgs e) 15 | { 16 | const int EventIdExceptionThrown = 80; 17 | 18 | if (e.EventId == EventIdExceptionThrown) 19 | { 20 | ExceptionThrown?.Invoke(Events.ExceptionThrownEvent.ParseFrom(e)); 21 | } 22 | } 23 | 24 | public static class Events 25 | { 26 | public interface Error : IErrorEvents 27 | { 28 | event Action ExceptionThrown; 29 | } 30 | 31 | public class ExceptionThrownEvent 32 | { 33 | private static readonly ExceptionThrownEvent Instance = new(); 34 | 35 | private ExceptionThrownEvent() 36 | { 37 | } 38 | 39 | public string ExceptionType { get; private set; } 40 | 41 | public static ExceptionThrownEvent ParseFrom(EventWrittenEventArgs e) 42 | { 43 | Instance.ExceptionType = (string) e.Payload[0]; 44 | 45 | return Instance; 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/JitEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using Prometheus.DotNetRuntime.EventListening.EventSources; 4 | using Prometheus.DotNetRuntime.EventListening.Parsers; 5 | using Prometheus.DotNetRuntime.EventListening.Parsers.Util; 6 | 7 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 8 | { 9 | public class JitEventParser : IEventParser, JitEventParser.Events.Verbose 10 | { 11 | private readonly SamplingRate _samplingRate; 12 | private const int EventIdMethodJittingStarted = 145, EventIdMethodLoadVerbose = 143; 13 | private readonly EventPairTimer _eventPairTimer; 14 | 15 | public event Action CompilationComplete; 16 | 17 | public JitEventParser(SamplingRate samplingRate) 18 | { 19 | _samplingRate = samplingRate; 20 | _eventPairTimer = new EventPairTimer( 21 | EventIdMethodJittingStarted, 22 | EventIdMethodLoadVerbose, 23 | x => (ulong)x.Payload[0], 24 | samplingRate 25 | ); 26 | } 27 | 28 | public EventKeywords Keywords => (EventKeywords) DotNetRuntimeEventSource.Keywords.Jit; 29 | public string EventSourceName => DotNetRuntimeEventSource.Name; 30 | 31 | public void ProcessEvent(EventWrittenEventArgs e) 32 | { 33 | if (_eventPairTimer.TryGetDuration(e, out var duration) == DurationResult.FinalWithDuration) 34 | { 35 | CompilationComplete?.InvokeManyTimes(_samplingRate.SampleEvery, Events.CompilationCompleteEvent.ParseFrom(e, duration)); 36 | } 37 | } 38 | 39 | public static class Events 40 | { 41 | public interface Verbose : IVerboseEvents 42 | { 43 | event Action CompilationComplete; 44 | } 45 | 46 | public class CompilationCompleteEvent 47 | { 48 | private static readonly CompilationCompleteEvent Instance = new(); 49 | 50 | private CompilationCompleteEvent() { } 51 | 52 | public TimeSpan CompilationDuration { get; private set; } 53 | public bool IsMethodDynamic { get; private set; } 54 | 55 | public static CompilationCompleteEvent ParseFrom(EventWrittenEventArgs e, TimeSpan compilationDuration) 56 | { 57 | // dynamic methods are of special interest to us- only a certain number of JIT'd dynamic methods 58 | // will be cached. Frequent use of dynamic can cause methods to be evicted from the cache and re-JIT'd 59 | var methodFlags = (uint)e.Payload[5]; 60 | Instance.IsMethodDynamic = (methodFlags & 0x1) == 0x1; 61 | Instance.CompilationDuration = compilationDuration; 62 | 63 | return Instance; 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/RuntimeEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using Prometheus.DotNetRuntime.EventListening; 4 | 5 | #nullable enable 6 | 7 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 8 | { 9 | public class RuntimeEventParser : EventCounterParserBase, 10 | RuntimeEventParser.Events.CountersV3_0, 11 | RuntimeEventParser.Events.CountersV5_0 12 | { 13 | #pragma warning disable CS0067 14 | [CounterName("threadpool-thread-count")] 15 | public event Action? ThreadPoolThreadCount; 16 | 17 | [CounterName("threadpool-queue-length")] 18 | public event Action? ThreadPoolQueueLength; 19 | 20 | [CounterName("threadpool-completed-items-count")] 21 | public event Action? ThreadPoolCompletedItemsCount; 22 | 23 | [CounterName("monitor-lock-contention-count")] 24 | public event Action? MonitorLockContentionCount; 25 | 26 | [CounterName("active-timer-count")] 27 | public event Action? ActiveTimerCount; 28 | 29 | [CounterName("exception-count")] 30 | public event Action? ExceptionCount; 31 | 32 | [CounterName("assembly-count")] 33 | public event Action? NumAssembliesLoaded; 34 | 35 | [CounterName("il-bytes-jitted")] 36 | public event Action? IlBytesJitted; 37 | 38 | [CounterName("methods-jitted-count")] 39 | public event Action? MethodsJittedCount; 40 | 41 | [CounterName("alloc-rate")] 42 | public event Action? AllocRate; 43 | 44 | [CounterName("gc-heap-size")] 45 | public event Action? GcHeapSize; 46 | [CounterName("gen-0-gc-count")] 47 | public event Action? Gen0GcCount; 48 | [CounterName("gen-1-gc-count")] 49 | public event Action? Gen1GcCount; 50 | [CounterName("gen-2-gc-count")] 51 | public event Action? Gen2GcCount; 52 | [CounterName("time-in-gc")] 53 | public event Action? TimeInGc; 54 | [CounterName("gen-0-size")] 55 | public event Action? Gen0Size; 56 | [CounterName("gen-1-size")] 57 | public event Action? Gen1Size; 58 | [CounterName("gen-2-size")] 59 | public event Action? Gen2Size; 60 | [CounterName("loh-size")] 61 | public event Action? LohSize; 62 | #pragma warning restore CS0067 63 | 64 | public override string EventSourceName => EventSources.SystemRuntimeEventSource.Name; 65 | 66 | public static class Events 67 | { 68 | public interface CountersV3_0 : ICounterEvents 69 | { 70 | event Action ThreadPoolThreadCount; 71 | event Action ThreadPoolQueueLength; 72 | event Action ThreadPoolCompletedItemsCount; 73 | event Action MonitorLockContentionCount; 74 | event Action ActiveTimerCount; 75 | event Action ExceptionCount; 76 | event Action NumAssembliesLoaded; 77 | event Action AllocRate; 78 | event Action GcHeapSize; 79 | event Action Gen0GcCount; 80 | event Action Gen1GcCount; 81 | event Action Gen2GcCount; 82 | event Action TimeInGc; 83 | event Action Gen0Size; 84 | event Action Gen1Size; 85 | event Action Gen2Size; 86 | event Action LohSize; 87 | } 88 | 89 | public interface CountersV5_0 : ICounterEvents 90 | { 91 | event Action IlBytesJitted; 92 | event Action MethodsJittedCount; 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/SocketsEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | 4 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 5 | { 6 | public class SocketsEventParser : EventCounterParserBase, SocketsEventParser.Events.CountersV5_0 7 | { 8 | 9 | #pragma warning disable CS0067 10 | [CounterName("outgoing-connections-established")] 11 | public event Action OutgoingConnectionsEstablished; 12 | 13 | [CounterName("incoming-connections-established")] 14 | public event Action IncomingConnectionsEstablished; 15 | 16 | [CounterName("bytes-sent")] 17 | public event Action BytesSent; 18 | 19 | [CounterName("bytes-received")] 20 | public event Action BytesReceived; 21 | 22 | [CounterName("datagrams-received")] 23 | public event Action DatagramsReceived; 24 | 25 | [CounterName("datagrams-sent")] 26 | public event Action DatagramsSent; 27 | #pragma warning restore CS0067 28 | 29 | public override string EventSourceName => "System.Net.Sockets"; 30 | 31 | public static class Events 32 | { 33 | public interface CountersV5_0 : ICounterEvents 34 | { 35 | event Action OutgoingConnectionsEstablished; 36 | event Action IncomingConnectionsEstablished; 37 | event Action BytesSent; 38 | event Action BytesReceived; 39 | event Action DatagramsReceived; 40 | event Action DatagramsSent; 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/ThreadPoolEventParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | using Prometheus.DotNetRuntime.EventListening.EventSources; 4 | 5 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 6 | { 7 | public class ThreadPoolEventParser : IEventParser, ThreadPoolEventParser.Events.Info 8 | { 9 | private const int 10 | EventIdThreadPoolSample = 54, 11 | EventIdThreadPoolAdjustment = 55, 12 | EventIdIoThreadCreate = 44, 13 | EventIdIoThreadRetire = 46, 14 | EventIdIoThreadUnretire = 47, 15 | EventIdIoThreadTerminate = 45; 16 | 17 | public event Action ThreadPoolAdjusted; 18 | public event Action IoThreadPoolAdjusted; 19 | 20 | public string EventSourceName => DotNetRuntimeEventSource.Name; 21 | public EventKeywords Keywords => (EventKeywords) DotNetRuntimeEventSource.Keywords.Threading; 22 | 23 | public void ProcessEvent(EventWrittenEventArgs e) 24 | { 25 | switch (e.EventId) 26 | { 27 | case EventIdThreadPoolAdjustment: 28 | ThreadPoolAdjusted?.Invoke(Events.ThreadPoolAdjustedEvent.ParseFrom(e)); 29 | return; 30 | 31 | case EventIdIoThreadCreate: 32 | case EventIdIoThreadRetire: 33 | case EventIdIoThreadUnretire: 34 | case EventIdIoThreadTerminate: 35 | IoThreadPoolAdjusted?.Invoke(Events.IoThreadPoolAdjustedEvent.ParseFrom(e)); 36 | return; 37 | } 38 | } 39 | 40 | public static class Events 41 | { 42 | public interface Info : IInfoEvents 43 | { 44 | event Action ThreadPoolAdjusted; 45 | event Action IoThreadPoolAdjusted; 46 | } 47 | 48 | public class ThreadPoolAdjustedEvent 49 | { 50 | private static readonly ThreadPoolAdjustedEvent Instance = new (); 51 | private ThreadPoolAdjustedEvent() { } 52 | 53 | public DotNetRuntimeEventSource.ThreadAdjustmentReason AdjustmentReason { get; private set; } 54 | public uint NumThreads { get; private set; } 55 | 56 | public static ThreadPoolAdjustedEvent ParseFrom(EventWrittenEventArgs e) 57 | { 58 | Instance.NumThreads = (uint) e.Payload[1]; 59 | Instance.AdjustmentReason = (DotNetRuntimeEventSource.ThreadAdjustmentReason) e.Payload[2]; 60 | return Instance; 61 | } 62 | } 63 | 64 | public class IoThreadPoolAdjustedEvent 65 | { 66 | private static readonly IoThreadPoolAdjustedEvent Instance = new (); 67 | 68 | private IoThreadPoolAdjustedEvent() { } 69 | 70 | public uint NumThreads { get; private set; } 71 | 72 | public static IoThreadPoolAdjustedEvent ParseFrom(EventWrittenEventArgs e) 73 | { 74 | Instance.NumThreads = (uint) e.Payload[0]; 75 | return Instance; 76 | } 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/Util/Cache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Prometheus.DotNetRuntime.EventListening.Parsers.Util 8 | { 9 | /// 10 | /// A strongly-typed cache that periodically evicts items. 11 | /// 12 | public sealed class Cache : IDisposable 13 | { 14 | private readonly ConcurrentDictionary> _cache; 15 | private readonly TimeSpan _expireItemsAfter; 16 | private readonly Task _cleanupTask; 17 | private readonly CancellationTokenSource _cancellationSource; 18 | 19 | internal Cache(TimeSpan expireItemsAfter, int initialCapacity = 32) 20 | { 21 | _expireItemsAfter = expireItemsAfter; 22 | if (expireItemsAfter == TimeSpan.Zero) 23 | throw new ArgumentNullException(nameof(expireItemsAfter)); 24 | 25 | _cache = new ConcurrentDictionary>(Environment.ProcessorCount, initialCapacity); 26 | _cancellationSource = new CancellationTokenSource(); 27 | 28 | _cleanupTask = Task.Run(async () => 29 | { 30 | while (!_cancellationSource.IsCancellationRequested) 31 | { 32 | await Task.Delay(expireItemsAfter); 33 | CleanupExpiredValues(); 34 | } 35 | }); 36 | } 37 | 38 | internal void Set(TKey key, TValue value, DateTime? timeStamp = null) 39 | { 40 | var cacheValue = new CacheValue(value, timeStamp); 41 | if (_cache.TryAdd(key, cacheValue)) 42 | return; 43 | 44 | // This is a very unthorough attempt to add a value to the cache if it already eixsts. 45 | // However, this cache is very unlikely to have to store keys that regularly clash (as most keys are event or process ids) 46 | _cache.TryRemove(key, out var unused); 47 | _cache.TryAdd(key, cacheValue); 48 | } 49 | 50 | internal bool TryGetValue(TKey key, out TValue value, out DateTime timeStamp) 51 | { 52 | CacheValue cacheValue; 53 | if (_cache.TryGetValue(key, out cacheValue)) 54 | { 55 | value = cacheValue.Value; 56 | timeStamp = cacheValue.TimeStamp; 57 | return true; 58 | } 59 | 60 | value = default(TValue); 61 | timeStamp = default(DateTime); 62 | return false; 63 | } 64 | 65 | internal bool TryRemove(TKey key, out TValue value, out DateTime timeStamp) 66 | { 67 | CacheValue cacheValue; 68 | if (_cache.TryRemove(key, out cacheValue)) 69 | { 70 | value = cacheValue.Value; 71 | timeStamp = cacheValue.TimeStamp; 72 | return true; 73 | } 74 | 75 | value = default(TValue); 76 | timeStamp = default(DateTime); 77 | return false; 78 | } 79 | 80 | internal struct CacheValue 81 | { 82 | public CacheValue(T value, DateTime? timeStamp) 83 | { 84 | Value = value; 85 | TimeStamp = timeStamp ?? DateTime.UtcNow; 86 | } 87 | 88 | public DateTime TimeStamp { get; } 89 | public T Value { get; } 90 | } 91 | 92 | public void Dispose() 93 | { 94 | _cancellationSource.Cancel(); 95 | } 96 | 97 | private void CleanupExpiredValues() 98 | { 99 | var earliestAddedTime = DateTime.UtcNow.Subtract(_expireItemsAfter); 100 | 101 | foreach (var key in _cache.Keys.ToArray()) 102 | { 103 | if (!_cache.TryGetValue(key, out var value)) 104 | continue; 105 | 106 | if (value.TimeStamp < earliestAddedTime) 107 | _cache.TryRemove(key, out _); 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/Util/EventExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening.Parsers 4 | { 5 | internal static class DelegateExtensions 6 | { 7 | internal static void InvokeManyTimes(this Action d, int count, T payload) 8 | { 9 | for (int i = 0; i < count; i++) 10 | { 11 | d(payload); 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/Util/EventPairTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Tracing; 3 | 4 | namespace Prometheus.DotNetRuntime.EventListening.Parsers.Util 5 | { 6 | /// 7 | /// To generate metrics, we are often interested in the duration between two events. This class 8 | /// helps time the duration between two events. 9 | /// 10 | /// A type of an identifier present on both events 11 | /// A struct that represents data of interest extracted from the first event 12 | public class EventPairTimer 13 | where TId : struct 14 | where TEventData : struct 15 | { 16 | private readonly Cache _eventStartedAtCache; 17 | private readonly int _startEventId; 18 | private readonly int _endEventId; 19 | private readonly Func _extractEventIdFn; 20 | private readonly Func _extractData; 21 | private readonly SamplingRate _samplingRate; 22 | 23 | public EventPairTimer( 24 | int startEventId, 25 | int endEventId, 26 | Func extractEventIdFn, 27 | Func extractData, 28 | SamplingRate samplingRate, 29 | Cache cache = null) 30 | { 31 | _startEventId = startEventId; 32 | _endEventId = endEventId; 33 | _extractEventIdFn = extractEventIdFn; 34 | _extractData = extractData; 35 | _samplingRate = samplingRate; 36 | _eventStartedAtCache = cache ?? new Cache(TimeSpan.FromMinutes(1)); 37 | } 38 | 39 | /// 40 | /// Checks if an event is an expected final event- if so, returns true, the duration between it and the start event and 41 | /// any data extracted from the first event. 42 | /// 43 | /// 44 | /// If the event id matches the supplied start event id, then we cache the event until the final event occurs. 45 | /// All other events are ignored. 46 | /// 47 | public DurationResult TryGetDuration(EventWrittenEventArgs e, out TimeSpan duration, out TEventData startEventData) 48 | { 49 | duration = TimeSpan.Zero; 50 | startEventData = default(TEventData); 51 | 52 | if (e.EventId == _startEventId) 53 | { 54 | if (_samplingRate.ShouldSampleEvent()) 55 | { 56 | _eventStartedAtCache.Set(_extractEventIdFn(e), _extractData(e), e.TimeStamp); 57 | } 58 | 59 | return DurationResult.Start; 60 | } 61 | 62 | if (e.EventId == _endEventId) 63 | { 64 | var id = _extractEventIdFn(e); 65 | if (_eventStartedAtCache.TryRemove(id, out startEventData, out var timeStamp)) 66 | { 67 | duration = e.TimeStamp - timeStamp; 68 | return DurationResult.FinalWithDuration; 69 | } 70 | else 71 | { 72 | return DurationResult.FinalWithoutDuration; 73 | } 74 | } 75 | 76 | return DurationResult.Unrecognized; 77 | } 78 | } 79 | 80 | public enum DurationResult 81 | { 82 | Unrecognized = 0, 83 | Start = 1, 84 | FinalWithoutDuration = 2, 85 | FinalWithDuration = 3 86 | } 87 | 88 | public sealed class EventPairTimer : EventPairTimer 89 | where TId : struct 90 | { 91 | public EventPairTimer(int startEventId, int endEventId, Func extractEventIdFn, SamplingRate samplingRate, Cache cache = null) 92 | : base(startEventId, endEventId, extractEventIdFn, e => 0, samplingRate, cache) 93 | { 94 | } 95 | 96 | public DurationResult TryGetDuration(EventWrittenEventArgs e, out TimeSpan duration) 97 | { 98 | return this.TryGetDuration(e, out duration, out _); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Parsers/Util/SamplingRate.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening.Parsers.Util 4 | { 5 | /// 6 | /// The rate at which high-frequency events are sampled 7 | /// 8 | /// 9 | /// In busy .NET applications, certain events are emitted at the rate of thousands/ tens of thousands per second. 10 | /// To track all the start and end pairs of events for these events can consume a significant amount of memory (100+ MB). 11 | /// Using a sampling rate allows us to reduce the memory requirements. 12 | /// 13 | public sealed class SamplingRate 14 | { 15 | private long _next; 16 | 17 | public SamplingRate(SampleEvery every) 18 | { 19 | SampleEvery = (int)every; 20 | _next = 0L; 21 | } 22 | 23 | /// 24 | /// Out of every 100 events, how many events we should observe. 25 | /// 26 | public int SampleEvery { get; } 27 | 28 | /// 29 | /// Determines if we should sample a given event. 30 | /// 31 | /// 32 | public bool ShouldSampleEvent() 33 | { 34 | if (SampleEvery == 1) 35 | return true; 36 | 37 | return (Interlocked.Increment(ref _next) % SampleEvery) == 0; 38 | } 39 | 40 | public static implicit operator SamplingRate(SampleEvery d) => new SamplingRate(d); 41 | } 42 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Sources/FrameworkEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening.EventSources 4 | { 5 | /// 6 | /// Provider name: System.Diagnostics.Eventing.FrameworkEventSource. Provides events generated by 7 | /// the CoreClr libraries. 8 | /// 9 | public class FrameworkEventSource 10 | { 11 | public static readonly Guid Id = Guid.Parse("8e9f5090-2d75-4d03-8a81-e5afbf85daf1"); 12 | 13 | [Flags] 14 | internal enum Keywords : long 15 | { 16 | Loader = 0x0001, 17 | ThreadPool = 0x0002, 18 | NetClient = 0x0004, 19 | DynamicTypeUsage = 0x0008, 20 | ThreadTransfer = 0x0010, 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/EventListening/Sources/SystemRuntimeEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Prometheus.DotNetRuntime.EventListening.EventSources 4 | { 5 | /// 6 | /// Provider name: System.Runtime. Provides counters generated by the .NET runtime. 7 | /// 8 | public class SystemRuntimeEventSource 9 | { 10 | public static readonly Guid Id = new ("49592C0F-5A05-516D-AA4B-A64E02026C89"); 11 | public static readonly string Name = "System.Runtime"; 12 | } 13 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.Diagnostics.Tracing; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.DependencyInjection.Extensions; 7 | 8 | namespace Prometheus.DotNetRuntime 9 | { 10 | internal static class Extensions 11 | { 12 | internal static void AddOrReplace(this ISet s, T toAddOrReplace) 13 | { 14 | if (!s.Add(toAddOrReplace)) 15 | { 16 | s.Remove(toAddOrReplace); 17 | s.Add(toAddOrReplace); 18 | } 19 | } 20 | 21 | internal static void TryAddSingletonEnumerable(this IServiceCollection services) 22 | where TService : class 23 | where TImplementation : class, TService 24 | { 25 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 26 | } 27 | 28 | internal static EventLevel ToEventLevel(this CaptureLevel level) 29 | { 30 | return (EventLevel) (int)level; 31 | } 32 | 33 | internal static CaptureLevel ToCaptureLevel(this EventLevel level) 34 | { 35 | return (CaptureLevel) (int)level; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/ListenerRegistration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Diagnostics.Tracing; 5 | using System.Linq; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Prometheus.DotNetRuntime.EventListening; 8 | 9 | namespace Prometheus.DotNetRuntime 10 | { 11 | internal class ListenerRegistration : IEquatable 12 | { 13 | private ListenerRegistration(EventLevel level, Type type, Func factory) 14 | { 15 | Level = level; 16 | Type = type; 17 | Factory = factory; 18 | } 19 | 20 | public static ListenerRegistration Create(CaptureLevel level, Func factory) 21 | where T : IEventListener 22 | { 23 | var supportedLevels = EventParserTypes.GetLevelsFromParser(typeof(T)); 24 | var eventLevel = level.ToEventLevel(); 25 | 26 | if (!supportedLevels.Contains(eventLevel)) 27 | throw new UnsupportedEventParserLevelException(typeof(T), level, supportedLevels); 28 | 29 | if (!EventParserTypes.AreEventsSupportedByRuntime(typeof(T))) 30 | throw new UnsupportedEventParserRuntimeException(typeof(T)); 31 | 32 | return new ListenerRegistration(eventLevel, typeof(T), sp => (object)factory(sp)); 33 | } 34 | 35 | internal void RegisterServices(IServiceCollection services) 36 | { 37 | services.AddSingleton(Type, Factory); 38 | services.AddSingleton(typeof(IEventListener), sp => sp.GetService(Type)); 39 | 40 | // Register each events interface exposed at the level specified 41 | foreach (var i in EventParserTypes.GetEventInterfacesForCurrentRuntime(Type, Level)) 42 | services.AddSingleton(i, sp => sp.GetService(Type)); 43 | } 44 | 45 | public EventLevel Level { get; set; } 46 | public Type Type { get; } 47 | public Func Factory { get; } 48 | 49 | public bool Equals(ListenerRegistration other) 50 | { 51 | if (ReferenceEquals(null, other)) return false; 52 | if (ReferenceEquals(this, other)) return true; 53 | return Equals(Type, other.Type); 54 | } 55 | 56 | public override bool Equals(object obj) 57 | { 58 | if (ReferenceEquals(null, obj)) return false; 59 | if (ReferenceEquals(this, obj)) return true; 60 | if (obj.GetType() != this.GetType()) return false; 61 | return Equals((ListenerRegistration) obj); 62 | } 63 | 64 | public override int GetHashCode() 65 | { 66 | return (Type != null ? Type.GetHashCode() : 0); 67 | } 68 | } 69 | 70 | internal class UnsupportedEventParserRuntimeException : Exception 71 | { 72 | public Type Type { get; } 73 | 74 | public UnsupportedEventParserRuntimeException(Type type) 75 | : base($"{EventParserTypes.AreEventsSupportedByRuntime(type)}") 76 | { 77 | Type = type; 78 | } 79 | } 80 | 81 | public class UnsupportedCaptureLevelException : Exception 82 | { 83 | public UnsupportedCaptureLevelException(CaptureLevel specifiedLevel, ISet supportedLevels) 84 | : base($"The level '{specifiedLevel}' is not supported- please use one of: {string.Join(", ", supportedLevels)}") 85 | { 86 | SpecifiedLevel = specifiedLevel; 87 | SupportedLevels = supportedLevels; 88 | } 89 | 90 | public UnsupportedCaptureLevelException(UnsupportedEventParserLevelException ex) 91 | : this (ex.SpecifiedLevel, ex.SupportedLevels.Select(x => x.ToCaptureLevel()).ToImmutableHashSet()) 92 | { 93 | } 94 | 95 | public static UnsupportedCaptureLevelException CreateWithCounterSupport(UnsupportedEventParserLevelException ex) 96 | { 97 | return new ( 98 | ex.SpecifiedLevel, 99 | ex.SupportedLevels 100 | .Select(x => x.ToCaptureLevel()) 101 | .ToImmutableHashSet() 102 | .Add(CaptureLevel.Counters) 103 | ); 104 | } 105 | 106 | public CaptureLevel SpecifiedLevel { get; } 107 | public ISet SupportedLevels { get; } 108 | } 109 | 110 | public class UnsupportedEventParserLevelException : Exception 111 | { 112 | public UnsupportedEventParserLevelException(Type eventParserType, CaptureLevel specifiedLevel, ISet supportedLevels) 113 | : base($"The event parser '{eventParserType.Name}' does not support the level '{specifiedLevel}'- please use one of: {string.Join(", ", supportedLevels)}") 114 | { 115 | EventParserType = eventParserType; 116 | SpecifiedLevel = specifiedLevel; 117 | SupportedLevels = supportedLevels; 118 | } 119 | 120 | public Type EventParserType { get; } 121 | public CaptureLevel SpecifiedLevel { get; } 122 | public ISet SupportedLevels { get; } 123 | } 124 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/IMetricProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Prometheus.DotNetRuntime.Metrics 6 | { 7 | public interface IMetricProducer 8 | { 9 | /// 10 | /// Called when the producer is associated with a metrics registry, allowing metrics to be created via the passed . 11 | /// 12 | void RegisterMetrics(MetricFactory metrics); 13 | 14 | /// 15 | /// Called before each metrics collection. Any metrics managed by this producer should now be brought up to date. 16 | /// 17 | void UpdateMetrics(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/MetricExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Prometheus.DotNetRuntime.Metrics 5 | { 6 | /// 7 | /// Provides helper functions for accessing values of metrics. 8 | /// 9 | internal static class MetricExtensions 10 | { 11 | /// 12 | /// Collects all values of a counter recorded across both unlabeled and labeled metrics. 13 | /// 14 | internal static IEnumerable CollectAllValues(this Counter counter, bool excludeUnlabeled = false) 15 | { 16 | return CollectAllMetrics(counter, c => c.Value, excludeUnlabeled); 17 | } 18 | 19 | /// 20 | /// Collects all sum values of a histogram recorded across both unlabeled and labeled metrics. 21 | /// 22 | internal static IEnumerable CollectAllSumValues(this Histogram histogram, bool excludeUnlabeled = false) 23 | { 24 | return CollectAllMetrics(histogram,c => c.Sum, excludeUnlabeled); 25 | } 26 | 27 | /// 28 | /// Collects all count values of a histogram recorded across both unlabeled and labeled metrics. 29 | /// 30 | internal static IEnumerable CollectAllCountValues(this Histogram histogram) 31 | { 32 | return CollectAllMetrics(histogram,c => (ulong)c.Count); 33 | } 34 | 35 | private static IEnumerable CollectAllMetrics(TCollector collector, Func getValue, bool excludeUnlabeled = false) 36 | where TCollector : Collector, TInterface 37 | where TChild : ChildBase, TInterface 38 | { 39 | var labels = GetLabelValues(collector); 40 | if (!excludeUnlabeled) 41 | yield return getValue((TInterface) collector); 42 | 43 | foreach (var l in labels) 44 | { 45 | yield return getValue(collector.Labels(l)); 46 | } 47 | } 48 | 49 | private static IEnumerable GetLabelValues(Collector collector) 50 | where TChild : ChildBase 51 | { 52 | return collector.GetAllLabelValues(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/ContentionMetricsProducer.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening.Parsers; 2 | 3 | namespace Prometheus.DotNetRuntime.Metrics.Producers 4 | { 5 | public class ContentionMetricsProducer : IMetricProducer 6 | { 7 | private readonly Consumes _contentionInfo; 8 | private readonly Consumes _runtimeCounters; 9 | 10 | public ContentionMetricsProducer(Consumes contentionInfo, Consumes runtimeCounters) 11 | { 12 | _contentionInfo = contentionInfo; 13 | _runtimeCounters = runtimeCounters; 14 | } 15 | 16 | internal Counter ContentionSecondsTotal { get; private set; } 17 | internal Counter ContentionTotal { get; private set; } 18 | 19 | public void RegisterMetrics(MetricFactory metrics) 20 | { 21 | if (!_contentionInfo.Enabled && !_runtimeCounters.Enabled) 22 | return; 23 | 24 | ContentionTotal = metrics.CreateCounter("dotnet_contention_total", "The number of locks contended"); 25 | _runtimeCounters.Events.MonitorLockContentionCount += e => ContentionTotal.Inc(e.IncrementedBy); 26 | 27 | if (_contentionInfo.Enabled) 28 | { 29 | ContentionSecondsTotal = metrics.CreateCounter("dotnet_contention_seconds_total", "The total amount of time spent contending locks"); 30 | _contentionInfo.Events.ContentionEnd += e => ContentionSecondsTotal.Inc(e.ContentionDuration.TotalSeconds); 31 | } 32 | } 33 | 34 | public void UpdateMetrics() { } 35 | } 36 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/ExceptionMetricsProducer.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening.Parsers; 2 | 3 | namespace Prometheus.DotNetRuntime.Metrics.Producers 4 | { 5 | public class ExceptionMetricsProducer : IMetricProducer 6 | { 7 | private readonly Consumes _exceptionError; 8 | private readonly Consumes _runtimeCounters; 9 | private const string LabelType = "type"; 10 | 11 | public ExceptionMetricsProducer(Consumes exceptionError, Consumes runtimeCounters) 12 | { 13 | _exceptionError = exceptionError; 14 | _runtimeCounters = runtimeCounters; 15 | } 16 | 17 | internal Counter ExceptionCount { get; private set; } 18 | 19 | public void RegisterMetrics(MetricFactory metrics) 20 | { 21 | if (!_exceptionError.Enabled && !_runtimeCounters.Enabled) 22 | return; 23 | 24 | if (_exceptionError.Enabled) 25 | { 26 | ExceptionCount = metrics.CreateCounter( 27 | "dotnet_exceptions_total", 28 | "Count of exceptions thrown, broken down by type", 29 | LabelType 30 | ); 31 | 32 | _exceptionError.Events.ExceptionThrown += e => ExceptionCount.Labels(e.ExceptionType).Inc(); 33 | } 34 | else if (_runtimeCounters.Enabled) 35 | { 36 | ExceptionCount = metrics.CreateCounter( 37 | "dotnet_exceptions_total", 38 | "Count of exceptions thrown" 39 | ); 40 | 41 | _runtimeCounters.Events.ExceptionCount += e => ExceptionCount.Inc(e.IncrementedBy); 42 | } 43 | } 44 | 45 | public void UpdateMetrics() { } 46 | } 47 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/JitMetricsProducer.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening.Parsers; 2 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 3 | 4 | namespace Prometheus.DotNetRuntime.Metrics.Producers 5 | { 6 | public class JitMetricsProducer : IMetricProducer 7 | { 8 | private const string DynamicLabel = "dynamic"; 9 | private const string LabelValueTrue = "true"; 10 | private const string LabelValueFalse = "false"; 11 | 12 | private readonly Consumes _jitVerbose; 13 | private readonly Consumes _runtimeCounters; 14 | private readonly Ratio _jitCpuRatio = Ratio.ProcessTotalCpu(); 15 | 16 | public JitMetricsProducer(Consumes jitVerbose, Consumes runtimeCounters) 17 | { 18 | _jitVerbose = jitVerbose; 19 | _runtimeCounters = runtimeCounters; 20 | } 21 | 22 | internal Counter MethodsJittedTotal { get; private set; } 23 | internal Counter MethodsJittedSecondsTotal { get; private set; } 24 | internal Gauge BytesJitted { get; private set; } 25 | internal Gauge CpuRatio { get; private set; } 26 | 27 | public void RegisterMetrics(MetricFactory metrics) 28 | { 29 | if (!_jitVerbose.Enabled && !_runtimeCounters.Enabled) 30 | return; 31 | 32 | if (_runtimeCounters.Enabled) 33 | { 34 | BytesJitted = metrics.CreateGauge("dotnet_jit_il_bytes", "Total bytes of IL compiled by the JIT compiler"); 35 | _runtimeCounters.Events.IlBytesJitted += e => BytesJitted.Set(e.Mean); 36 | } 37 | 38 | if (_jitVerbose.Enabled) 39 | { 40 | MethodsJittedTotal = metrics.CreateCounter("dotnet_jit_method_total", "Total number of methods compiled by the JIT compiler, broken down by compilation for dynamic code", DynamicLabel); 41 | MethodsJittedSecondsTotal = metrics.CreateCounter("dotnet_jit_method_seconds_total", "Total number of seconds spent in the JIT compiler, broken down by compilation for dynamic code", DynamicLabel); 42 | _jitVerbose.Events.CompilationComplete += e => 43 | { 44 | MethodsJittedTotal.Labels(e.IsMethodDynamic.ToLabel()).Inc(); 45 | MethodsJittedSecondsTotal.Labels(e.IsMethodDynamic.ToLabel()).Inc(e.CompilationDuration.TotalSeconds); 46 | }; 47 | 48 | CpuRatio = metrics.CreateGauge("dotnet_jit_cpu_ratio", "The amount of total CPU time consumed spent JIT'ing"); 49 | } 50 | else 51 | { 52 | MethodsJittedTotal = metrics.CreateCounter("dotnet_jit_method_total", "Total number of methods compiled by the JIT compiler"); 53 | _runtimeCounters.Events.MethodsJittedCount += e => MethodsJittedTotal.Inc(e.Mean - MethodsJittedTotal.Value); 54 | } 55 | } 56 | 57 | public void UpdateMetrics() 58 | { 59 | CpuRatio?.Set(_jitCpuRatio.CalculateConsumedRatio(MethodsJittedSecondsTotal)); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/SocketsMetricProducer.cs: -------------------------------------------------------------------------------- 1 | using Prometheus.DotNetRuntime.EventListening.Parsers; 2 | 3 | namespace Prometheus.DotNetRuntime.Metrics.Producers 4 | { 5 | public class SocketsMetricProducer : IMetricProducer 6 | { 7 | private readonly Consumes _socketCounters; 8 | 9 | public SocketsMetricProducer(Consumes socketCounters) 10 | { 11 | _socketCounters = socketCounters; 12 | } 13 | 14 | public void RegisterMetrics(MetricFactory metrics) 15 | { 16 | if (!_socketCounters.Enabled) 17 | return; 18 | 19 | OutgoingConnectionEstablished = metrics.CreateCounter("dotnet_sockets_connections_established_outgoing_total", "The total number of outgoing established TCP connections"); 20 | var lastEstablishedOutgoing = 0.0; 21 | _socketCounters.Events.OutgoingConnectionsEstablished += e => 22 | { 23 | OutgoingConnectionEstablished.Inc(e.Mean - lastEstablishedOutgoing); 24 | lastEstablishedOutgoing = e.Mean; 25 | }; 26 | 27 | IncomingConnectionEstablished = metrics.CreateCounter("dotnet_sockets_connections_established_incoming_total", "The total number of incoming established TCP connections"); 28 | var lastEstablishedIncoming = 0.0; 29 | _socketCounters.Events.IncomingConnectionsEstablished += e => 30 | { 31 | IncomingConnectionEstablished.Inc(e.Mean - lastEstablishedIncoming); 32 | lastEstablishedIncoming = e.Mean; 33 | }; 34 | 35 | BytesReceived = metrics.CreateCounter("dotnet_sockets_bytes_received_total", "The total number of bytes received over the network"); 36 | var lastReceived = 0.0; 37 | _socketCounters.Events.BytesReceived += e => 38 | { 39 | BytesReceived.Inc(e.Mean - lastReceived); 40 | lastReceived = e.Mean; 41 | }; 42 | 43 | var lastSent = 0.0; 44 | BytesSent = metrics.CreateCounter("dotnet_sockets_bytes_sent_total", "The total number of bytes sent over the network"); 45 | _socketCounters.Events.BytesSent += e => 46 | { 47 | BytesSent.Inc(e.Mean - lastSent); 48 | lastSent = e.Mean; 49 | }; 50 | } 51 | 52 | internal Counter BytesSent { get; private set; } 53 | internal Counter BytesReceived { get; private set; } 54 | internal Counter IncomingConnectionEstablished { get; private set; } 55 | internal Counter OutgoingConnectionEstablished { get; private set; } 56 | 57 | public void UpdateMetrics() 58 | { 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/ThreadPoolMetricsProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Prometheus.DotNetRuntime.EventListening.EventSources; 5 | using Prometheus.DotNetRuntime.EventListening.Parsers; 6 | using Prometheus.DotNetRuntime.Metrics.Producers.Util; 7 | 8 | namespace Prometheus.DotNetRuntime.Metrics.Producers 9 | { 10 | public class ThreadPoolMetricsProducer : IMetricProducer 11 | { 12 | private readonly Dictionary _adjustmentReasonToLabel = LabelGenerator.MapEnumToLabelValues(); 13 | private readonly Options _options; 14 | private readonly Consumes _threadPoolInfo; 15 | private readonly Consumes _runtimeCounters; 16 | 17 | public ThreadPoolMetricsProducer(Options options, Consumes threadPoolInfo, Consumes runtimeCounters) 18 | { 19 | _options = options; 20 | _threadPoolInfo = threadPoolInfo; 21 | _runtimeCounters = runtimeCounters; 22 | } 23 | 24 | internal Gauge NumThreads { get; private set; } 25 | internal Gauge NumIocThreads { get; private set; } 26 | internal Counter AdjustmentsTotal { get; private set; } 27 | internal Counter Throughput { get; private set; } 28 | internal Histogram QueueLength { get; private set; } 29 | internal Gauge NumTimers { get; private set; } 30 | 31 | public void RegisterMetrics(MetricFactory metrics) 32 | { 33 | if (!_threadPoolInfo.Enabled && !_runtimeCounters.Enabled) 34 | return; 35 | 36 | NumThreads = metrics.CreateGauge("dotnet_threadpool_num_threads", "The number of active threads in the thread pool"); 37 | _runtimeCounters.Events.ThreadPoolThreadCount += e => NumThreads.Set(e.Mean); 38 | 39 | Throughput = metrics.CreateCounter("dotnet_threadpool_throughput_total", "The total number of work items that have finished execution in the thread pool"); 40 | _runtimeCounters.Events.ThreadPoolCompletedItemsCount += e => Throughput.Inc(e.IncrementedBy); 41 | 42 | QueueLength = metrics.CreateHistogram("dotnet_threadpool_queue_length", 43 | "Measures the queue length of the thread pool. Values greater than 0 indicate a backlog of work for the threadpool to process.", 44 | new HistogramConfiguration {Buckets = _options.QueueLengthHistogramBuckets} 45 | ); 46 | _runtimeCounters.Events.ThreadPoolQueueLength += e => QueueLength.Observe(e.Mean); 47 | 48 | NumTimers = metrics.CreateGauge("dotnet_threadpool_timer_count", "The number of timers active"); 49 | _runtimeCounters.Events.ActiveTimerCount += e => NumTimers.Set(e.Mean); 50 | 51 | if (_threadPoolInfo.Enabled) 52 | { 53 | AdjustmentsTotal = metrics.CreateCounter( 54 | "dotnet_threadpool_adjustments_total", 55 | "The total number of changes made to the size of the thread pool, labeled by the reason for change", 56 | "adjustment_reason"); 57 | 58 | _threadPoolInfo.Events.ThreadPoolAdjusted += e => 59 | { 60 | AdjustmentsTotal.Labels(_adjustmentReasonToLabel[e.AdjustmentReason]).Inc(); 61 | }; 62 | 63 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 64 | { 65 | // IO threadpool only exists on windows 66 | NumIocThreads = metrics.CreateGauge("dotnet_threadpool_io_num_threads", "The number of active threads in the IO thread pool"); 67 | _threadPoolInfo.Events.IoThreadPoolAdjusted += e => NumIocThreads.Set(e.NumThreads); 68 | } 69 | } 70 | } 71 | 72 | 73 | public void UpdateMetrics() 74 | { 75 | } 76 | 77 | public class Options 78 | { 79 | public double[] QueueLengthHistogramBuckets { get; set; } = new double[] { 0, 1, 10, 100, 1000 }; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/Util/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.DotNetRuntime.Metrics.Producers.Util 2 | { 3 | internal class Constants 4 | { 5 | /// 6 | /// In seconds, the buckets to use when generating histogram. 7 | /// 8 | /// 9 | /// Default is: 1ms, 10ms, 50ms, 100ms, 500ms, 1 sec, 10 sec 10 | /// 11 | internal static readonly double[] DefaultHistogramBuckets = {0.001, 0.01, 0.05, 0.1, 0.5, 1, 10}; 12 | } 13 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/Util/LabelGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Prometheus.DotNetRuntime.Metrics.Producers.Util 6 | { 7 | /// 8 | /// Generating tags often involves heavy use of String.Format, which takes CPU time and needlessly re-allocates 9 | /// strings. Pre-generating these labels helps keep resource use to a minimum. 10 | /// 11 | internal static class LabelGenerator 12 | { 13 | internal static Dictionary MapEnumToLabelValues() 14 | where TEnum : Enum 15 | { 16 | return Enum.GetValues(typeof(TEnum)).Cast() 17 | .ToDictionary(k => k, v => Enum.GetName(typeof(TEnum), v).ToSnakeCase()); 18 | } 19 | 20 | internal static string ToLabel(this bool b) 21 | { 22 | const string LabelValueTrue = "true", LabelValueFalse = "false"; 23 | return b ? LabelValueTrue : LabelValueFalse; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/Util/Ratio.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using Prometheus.DotNetRuntime.Metrics; 5 | 6 | namespace Prometheus.DotNetRuntime.Metrics.Producers.Util 7 | { 8 | /// 9 | /// Helps calculate the ratio of process resources consumed by some activity. 10 | /// 11 | public class Ratio 12 | { 13 | private readonly Func _getElapsedTime; 14 | private TimeSpan _lastProcessTime; 15 | private double _lastEventTotalSeconds; 16 | 17 | internal Ratio(Func getElapsedTime) 18 | { 19 | _getElapsedTime = getElapsedTime; 20 | _lastProcessTime = _getElapsedTime(); 21 | } 22 | 23 | /// 24 | /// Calculates the ratio of CPU time consumed by an activity. 25 | /// 26 | /// 27 | public static Ratio ProcessTotalCpu() 28 | { 29 | var p = Process.GetCurrentProcess(); 30 | return new Ratio(() => 31 | { 32 | p.Refresh(); 33 | return p.TotalProcessorTime; 34 | }); 35 | } 36 | 37 | /// 38 | /// Calculates the ratio of process time consumed by an activity. 39 | /// 40 | /// 41 | public static Ratio ProcessTime() 42 | { 43 | var startTime = DateTime.UtcNow; 44 | return new Ratio(() => DateTime.UtcNow - startTime); 45 | } 46 | 47 | public double CalculateConsumedRatio(double eventsCpuTimeTotalSeconds) 48 | { 49 | var currentProcessTime = _getElapsedTime(); 50 | var consumedProcessTime = currentProcessTime - _lastProcessTime; 51 | var eventsConsumedTimeSeconds = eventsCpuTimeTotalSeconds - _lastEventTotalSeconds; 52 | 53 | if (eventsConsumedTimeSeconds < 0.0) 54 | { 55 | // In this case, the difference between our last observed events CPU time and the current events CPU time is negative. 56 | // This means that we are being passed a non-incrementing value (which the caller should not be doing). 57 | // Rather than throwing an exception which could jeopardize the stability of event collection, we'll return a zero 58 | // TODO re-visit this and consider how to notify the user this is occurring 59 | return 0.0; 60 | } 61 | 62 | _lastProcessTime = currentProcessTime; 63 | _lastEventTotalSeconds = eventsCpuTimeTotalSeconds; 64 | 65 | if (consumedProcessTime == TimeSpan.Zero) 66 | { 67 | // Avoid divide by zero 68 | return 0.0; 69 | } 70 | else 71 | { 72 | // We want to avoid a situation where we could return more than 100%. This could occur 73 | // if a delay is introduced between events being published and processed. 74 | // TODO need to potentially discard old events? 75 | return Math.Min(1.0, eventsConsumedTimeSeconds / consumedProcessTime.TotalSeconds); 76 | } 77 | } 78 | 79 | public double CalculateConsumedRatio(Counter eventCpuConsumedTotalSeconds) 80 | { 81 | return CalculateConsumedRatio(eventCpuConsumedTotalSeconds.CollectAllValues().Sum(x => x)); 82 | } 83 | 84 | public double CalculateConsumedRatio(Histogram eventCpuConsumedSeconds) 85 | { 86 | return CalculateConsumedRatio(eventCpuConsumedSeconds.CollectAllSumValues().Sum(x => x)); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Metrics/Producers/Util/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Prometheus.DotNetRuntime.Metrics.Producers.Util 4 | { 5 | public static class StringExtensions 6 | { 7 | public static string ToSnakeCase(this string str) 8 | { 9 | var sb = new StringBuilder(); 10 | var lastCharWasUpper = false; 11 | 12 | for(var i = 0 ; i < str.Length ; i++) 13 | { 14 | if (char.IsUpper(str[i])) 15 | { 16 | if (!lastCharWasUpper && i != 0) 17 | { 18 | sb.Append("_"); 19 | } 20 | 21 | sb.Append(char.ToLower(str[i])); 22 | lastCharWasUpper = true; 23 | } 24 | else 25 | { 26 | sb.Append(str[i]); 27 | lastCharWasUpper = false; 28 | } 29 | } 30 | 31 | return sb.ToString(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/Properties.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly:InternalsVisibleTo("prometheus-net.DotNetRuntime.Tests")] 3 | [assembly:InternalsVisibleTo("DocsGenerator")] -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/SampleRate.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.DotNetRuntime 2 | { 3 | /// 4 | /// Determines the level of sampling stats collectors will perform. offers the highest level 5 | /// of accuracy while offers the lowest level of precision but least amount of overhead. 6 | /// 7 | public enum SampleEvery 8 | { 9 | /// 10 | /// The highest level of accuracy- every event will be sampled. 11 | /// 12 | OneEvent = 1, 13 | TwoEvents = 2, 14 | FiveEvents = 5, 15 | TenEvents = 10, 16 | TwentyEvents = 20, 17 | FiftyEvents = 50, 18 | /// 19 | /// The lowest level of precision- only 1 in 100 events will be sampled. 20 | /// 21 | HundredEvents = 100 22 | } 23 | } -------------------------------------------------------------------------------- /src/prometheus-net.DotNetRuntime/prometheus-net.DotNetRuntime.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Prometheus.DotNetRuntime 6 | prometheus-net.DotNetRuntime 7 | prometheus-net.DotNetRuntime 8 | James Luck 9 | Prometheus prometheus-net IOnDemandCollector runtime metrics gc jit threadpool contention stats 10 | https://github.com/djluck/prometheus-net.DotNetRuntime 11 | 12 | Exposes .NET core runtime metrics (GC, JIT, lock contention, thread pool, exceptions) using the prometheus-net package. 13 | 14 | https://github.com/djluck/prometheus-net.DotNetRuntime/blob/master/LICENSE.txt 15 | AnyCPU 16 | net5.0;net6.0;netcoreapp3.1;netstandard2.1 17 | true 18 | 1701;1702;CS1591; 19 | disable 20 | 9 21 | true 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tools/DocsGenerator/DocsGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1;net5.0 6 | 9 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tools/DocsGenerator/README.md: -------------------------------------------------------------------------------- 1 | # DocsGenerator 2 | A small tool used to generate [docs/metrics-exposed.md](docs/metrics-exposed.md). 3 | -------------------------------------------------------------------------------- /tools/DocsGenerator/XmlDocReading.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text.RegularExpressions; 7 | using System.Xml; 8 | using System.Xml.Linq; 9 | 10 | namespace DocsGenerator 11 | { 12 | public static class Extensions 13 | { 14 | private static string GetDirectoryPath(this Assembly assembly) 15 | { 16 | string codeBase = assembly.CodeBase; 17 | UriBuilder uri = new UriBuilder(codeBase); 18 | string path = Uri.UnescapeDataString(uri.Path); 19 | return Path.GetDirectoryName(path); 20 | } 21 | 22 | public static AssemblyXmlDocs LoadXmlDocumentation(this Assembly assembly) 23 | { 24 | string directoryPath = assembly.GetDirectoryPath(); 25 | string xmlFilePath = Path.Combine(directoryPath, assembly.GetName().Name + ".xml"); 26 | if (File.Exists(xmlFilePath)) { 27 | return new AssemblyXmlDocs(assembly, (File.ReadAllText(xmlFilePath))); 28 | } 29 | 30 | throw new FileNotFoundException("Unable to locate xmldoc", xmlFilePath); 31 | } 32 | 33 | } 34 | 35 | /// 36 | /// Super hacky! 37 | /// 38 | public class AssemblyXmlDocs 39 | { 40 | private Dictionary _loadedXmlDocumentation = new(); 41 | 42 | public AssemblyXmlDocs(Assembly assembly, string xml) 43 | { 44 | var doc = XDocument.Parse(xml); 45 | 46 | _loadedXmlDocumentation = doc.Descendants("member") 47 | .Select(m => 48 | { 49 | var nameAttr = m.Attribute("name"); 50 | // e.g. M:Prometheus.DotNetRuntime.DotNetRuntimeStatsBuilder.Customize 51 | var match = Regex.Match(nameAttr.Value, @"(?[\w]):(?[^\(]+)\.(?[^\.\(]+)"); 52 | 53 | var memberType = match.Groups["member_type"].Value switch 54 | { 55 | "T" => MemberTypes.TypeInfo, 56 | "M" => MemberTypes.Method, 57 | "P" => MemberTypes.Property, 58 | "F" => MemberTypes.Field 59 | }; 60 | 61 | var parentTypeName = match.Groups["parent_type"].Value; 62 | var parentType = assembly.GetType(parentTypeName); 63 | 64 | if (parentType == null && memberType != MemberTypes.TypeInfo) // TypeInfo doesn't currently work 65 | { 66 | // SO. HACKY. 67 | parentType = assembly.GetType("Prometheus.DotNetRuntime.DotNetRuntimeStatsBuilder+Builder"); 68 | } 69 | if (parentType == null && memberType != MemberTypes.TypeInfo) // TypeInfo doesn't currently work 70 | { 71 | throw new InvalidOperationException($"Could not locate type '{parentTypeName}'"); 72 | } 73 | var key = new DocKey(memberType, parentType, match.Groups["member_name"].Value); 74 | 75 | return (key, memberDocs: new MemberDocs() 76 | { 77 | Summary = m.Descendants("summary").SingleOrDefault()?.Value?.Trim() 78 | }); 79 | }) 80 | .GroupBy(x => x.key) 81 | // TODO if we ever need rely on override docs working correctly, fix this 82 | .ToDictionary(k => k.Key, v => v.First().memberDocs); 83 | 84 | } 85 | 86 | public MemberDocs GetDocumentation(MethodInfo methodInfo) 87 | { 88 | if (_loadedXmlDocumentation.TryGetValue((new DocKey(MemberTypes.Method, methodInfo.DeclaringType, methodInfo.Name)), out var documentation)) 89 | { 90 | return documentation; 91 | } 92 | 93 | throw new KeyNotFoundException($"Could not locate docs for {methodInfo}"); 94 | } 95 | 96 | public class MemberDocs 97 | { 98 | public string Summary { get; set; } 99 | } 100 | } 101 | 102 | internal record DocKey(MemberTypes MemberType, Type ParentType, string Name) 103 | { 104 | public MemberTypes MemberType { get; } = MemberType; 105 | public Type ParentType { get; } = ParentType; 106 | public string Name { get; } = Name; 107 | } 108 | } --------------------------------------------------------------------------------