diff --git a/aspnetcore/log-mon/metrics/metrics.md b/aspnetcore/log-mon/metrics/metrics.md index 9cc7dde291..4a69fa8e6e 100644 --- a/aspnetcore/log-mon/metrics/metrics.md +++ b/aspnetcore/log-mon/metrics/metrics.md @@ -49,7 +49,7 @@ dotnet add package OpenTelemetry.Extensions.Hosting Replace the contents of `Program.cs` with the following code: -:::code language="csharp" source="~/log-mon/metrics/metrics/samples/Program.cs"::: +:::code language="csharp" source="~/log-mon/metrics/metrics/samples/web-metrics/Program.cs"::: @@ -84,7 +84,7 @@ Press p to pause, r to resume, q to quit. host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro 0.001 host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro 0 host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro 0 - host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro 0 12 + host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro 0 ``` For more information, see [dotnet-counters](/dotnet/core/diagnostics/dotnet-counters). @@ -101,42 +101,15 @@ ASP.NET Core registers in depend To use `IMeterFactory` in an app, create a type that uses `IMeterFactory` to create the app's custom metrics: -```cs -public class ContosoMetrics -{ - private readonly Counter _productSoldCounter; - - public ContosoMetrics(IMeterFactory meterFactory) - { - var meter = meterFactory.CreateMeter("Contoso.Web"); - _productSoldCounter = meter.CreateCounter("contoso.product.sold"); - } - - public void ProductSold(string productName, int quantity) - { - _productSoldCounter.Add(quantity, - new KeyValuePair("contoso.product.name", productName)); - } -} -``` +:::code language="csharp" source="~/log-mon/metrics/metrics/samples/custom-metrics/ContosoMetrics.cs" id="snippet_ContosoMetrics"::: Register the metrics type with DI in `Program.cs`: -```cs -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddSingleton(); -``` +:::code language="csharp" source="~/log-mon/metrics/metrics/samples/custom-metrics/Program.cs" id="snippet_RegisterMetrics"::: Inject the metrics type and record values where needed. Because the metrics type is registered in DI it can be use with MVC controllers, minimal APIs, or any other type that is created by DI: -```cs -app.MapPost("/complete-sale", ([FromBody] SaleModel model, ContosoMetrics metrics) => -{ - // ... business logic such as saving the sale to a database ... - - metrics.ProductSold(model.QuantitySold, model.ProductName); -}); -``` +:::code language="csharp" source="~/log-mon/metrics/metrics/samples/custom-metrics/Program.cs" id="snippet_InjectAndUseMetrics"::: ## View metrics in Grafana with OpenTelemetry and Prometheus @@ -180,7 +153,7 @@ Follow the [Prometheus first steps](https://prometheus.io/docs/introduction/firs Modify the *prometheus.yml* configuration file so that Prometheus scrapes the metrics endpoint that the example app is exposing. Add the following highlighted text in the `scrape_configs` section: -:::code language="yaml" source="~/log-mon/metrics/metrics/samples/prometheus.yml" highlight="31-99"::: +:::code language="yaml" source="~/log-mon/metrics/metrics/samples/web-metrics/prometheus.yml" highlight="31-99"::: In the preceding highlighted YAML, replace `5045` with the port number that the example app is running on. @@ -195,11 +168,11 @@ Select the **Open metric explorer** icon to see available metrics: ![Prometheus open_metric_exp](~/log-mon/metrics/metrics/static/open_metric_exp.png) -Enter counter category such as `http_` in the **Expression** input box to see the available metrics: +Enter counter category such as `http_` in the **Expression** input box to see the available metrics: ![available metrics](~/log-mon/metrics/metrics/static/metrics2.png) -Alternatively, enter counter category such as `kestrel` in the **Expression** input box to see the available metrics: +Alternatively, enter counter category such as `kestrel` in the **Expression** input box to see the available metrics: ![Prometheus kestrel](~/log-mon/metrics/metrics/static/kestrel.png) @@ -215,38 +188,7 @@ Alternatively, enter counter category such as `kestrel` in the **Expression** in It's possible to test metrics in ASP.NET Core apps. One way to do that is collect and assert metrics values in [ASP.NET Core integration tests](xref:test/integration-tests) using . -```cs -public class BasicTests : IClassFixture> -{ - private readonly WebApplicationFactory _factory; - public BasicTests(WebApplicationFactory factory) => _factory = factory; - - [Fact] - public async Task Get_RequestCounterIncreased() - { - // Arrange - var client = _factory.CreateClient(); - var meterFactory = _factory.Services.GetRequiredService(); - var collector = new MetricCollector(meterFactory, - "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); - - // Act - var response = await client.GetAsync("/"); - - // Assert - Assert.Equal("Hello World!", await response.Content.ReadAsStringAsync()); - - await collector.WaitForMeasurementsAsync(minCount: 1).WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Collection(collector.GetMeasurementSnapshot(), - measurement => - { - Assert.Equal("http", measurement.Tags["url.scheme"]); - Assert.Equal("GET", measurement.Tags["http.request.method"]); - Assert.Equal("/", measurement.Tags["http.route"]); - }); - } -} -``` +:::code language="csharp" source="~/log-mon/metrics/metrics/samples/metric-tests/BasicTests.cs" id="snippet_TestClass"::: The proceeding test: diff --git a/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/ContosoMetrics.cs b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/ContosoMetrics.cs new file mode 100644 index 0000000000..e14c4d9eb0 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/ContosoMetrics.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; + +// +public class ContosoMetrics +{ + private readonly Counter _productSoldCounter; + + public ContosoMetrics(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("Contoso.Web"); + _productSoldCounter = meter.CreateCounter("contoso.product.sold"); + } + + public void ProductSold(string productName, int quantity) + { + _productSoldCounter.Add(quantity, + new KeyValuePair("contoso.product.name", productName)); + } +} +// diff --git a/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/CustomMetrics.csproj b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/CustomMetrics.csproj new file mode 100644 index 0000000000..ad115556d0 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/CustomMetrics.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + enable + enable + + \ No newline at end of file diff --git a/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/Program.cs b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/Program.cs new file mode 100644 index 0000000000..24efc1b12d --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/Program.cs @@ -0,0 +1,17 @@ +// +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(); +// + +var app = builder.Build(); + +// +app.MapPost("/complete-sale", (SaleModel model, ContosoMetrics metrics) => +{ + // ... business logic such as saving the sale to a database ... + + metrics.ProductSold(model.ProductName, model.QuantitySold); +}); +// + +app.Run(); diff --git a/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/SaleModel.cs b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/SaleModel.cs new file mode 100644 index 0000000000..3bffe486d7 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/SaleModel.cs @@ -0,0 +1 @@ +public record SaleModel(string ProductName, int QuantitySold); \ No newline at end of file diff --git a/aspnetcore/log-mon/metrics/metrics/samples/appsettings.json b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/appsettings.json similarity index 100% rename from aspnetcore/log-mon/metrics/metrics/samples/appsettings.json rename to aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/appsettings.json diff --git a/aspnetcore/log-mon/metrics/metrics/samples/prometheus.yml b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/prometheus.yml similarity index 100% rename from aspnetcore/log-mon/metrics/metrics/samples/prometheus.yml rename to aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/prometheus.yml diff --git a/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/tests.http b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/tests.http new file mode 100644 index 0000000000..0c2336b4a1 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/custom-metrics/tests.http @@ -0,0 +1,11 @@ +GET http://localhost:5231/ + +### + +POST http://localhost:5231/complete-sale +Content-Type: application/json + +{ + "productName": "Product One", + "quantitySold": 1 +} diff --git a/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/BasicTests.cs b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/BasicTests.cs new file mode 100644 index 0000000000..54ad4b025a --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/BasicTests.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.VisualStudio.TestPlatform.TestHost; +using Microsoft.Extensions.Telemetry.Testing.Metering; + +// +public class BasicTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + public BasicTests(WebApplicationFactory factory) => _factory = factory; + + [Fact] + public async Task Get_RequestCounterIncreased() + { + // Arrange + var client = _factory.CreateClient(); + var meterFactory = _factory.Services.GetRequiredService(); + var collector = new MetricCollector(meterFactory, + "Microsoft.AspNetCore.Hosting", "http-server-request-duration"); + + // Act + var response = await client.GetAsync("/"); + + // Assert + Assert.Contains("Hello OpenTelemetry!", await response.Content.ReadAsStringAsync()); + + await collector.WaitForMeasurementsAsync(minCount: 1).WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Collection(collector.GetMeasurementSnapshot(), + measurement => + { + Assert.Equal("http", measurement.Tags["scheme"]); + Assert.Equal("GET", measurement.Tags["method"]); + Assert.Equal("/", measurement.Tags["route"]); + }); + } +} +// diff --git a/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/GlobalUsings.cs b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/GlobalUsings.cs new file mode 100644 index 0000000000..8c927eb747 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/MetricTests.csproj b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/MetricTests.csproj new file mode 100644 index 0000000000..98ecf843a1 --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/metric-tests/MetricTests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + \ No newline at end of file diff --git a/aspnetcore/log-mon/metrics/metrics/samples/Program.cs b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/Program.cs similarity index 100% rename from aspnetcore/log-mon/metrics/metrics/samples/Program.cs rename to aspnetcore/log-mon/metrics/metrics/samples/web-metrics/Program.cs diff --git a/aspnetcore/log-mon/metrics/metrics/samples/WebMetric.csproj b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/WebMetric.csproj similarity index 83% rename from aspnetcore/log-mon/metrics/metrics/samples/WebMetric.csproj rename to aspnetcore/log-mon/metrics/metrics/samples/web-metrics/WebMetric.csproj index 05ed0c3cbc..8d9444e4e8 100644 --- a/aspnetcore/log-mon/metrics/metrics/samples/WebMetric.csproj +++ b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/WebMetric.csproj @@ -7,8 +7,8 @@ - - + + - + \ No newline at end of file diff --git a/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/appsettings.json b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/prometheus.yml b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/prometheus.yml new file mode 100644 index 0000000000..e1ef3ec0bc --- /dev/null +++ b/aspnetcore/log-mon/metrics/metrics/samples/web-metrics/prometheus.yml @@ -0,0 +1,34 @@ +# my global config +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +# Alertmanager configuration +alerting: + alertmanagers: + - static_configs: + - targets: + # - alertmanager:9093 + +# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: "prometheus" + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ["localhost:9090"] + + - job_name: 'MyASPNETApp' + scrape_interval: 5s # Poll every 5 seconds for a more responsive demo. + static_configs: + - targets: ["localhost:5045"] ## Enter the HTTP port number of the demo app.