create sample for custom metrics (#30183)

* create sample for custom metrics

* simplify example and fix snippet to the original example

* fix Get_RequestCounterIncreased test
pull/30212/head
Tim Deschryver 2023-09-05 21:19:34 +02:00 committed by GitHub
parent a9355807af
commit b866bc0f57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 175 additions and 70 deletions

View File

@ -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":::
<!-- Review TODO: Add link to available meters -->
@ -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 <xref:System.Diagnostics.Metrics.IMeterFactory> 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<int> _productSoldCounter;
public ContosoMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.CreateMeter("Contoso.Web");
_productSoldCounter = meter.CreateCounter<int>("contoso.product.sold");
}
public void ProductSold(string productName, int quantity)
{
_productSoldCounter.Add(quantity,
new KeyValuePair<string, object?>("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<ContosoMetrics>();
```
:::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 <xref:Microsoft.Extensions.Telemetry.Testing.Metering.MetricCollector%601>.
```cs
public class BasicTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory) => _factory = factory;
[Fact]
public async Task Get_RequestCounterIncreased()
{
// Arrange
var client = _factory.CreateClient();
var meterFactory = _factory.Services.GetRequiredService<IMeterFactory>();
var collector = new MetricCollector<double>(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:

View File

@ -0,0 +1,21 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics;
// <snippet_ContosoMetrics>
public class ContosoMetrics
{
private readonly Counter<int> _productSoldCounter;
public ContosoMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("Contoso.Web");
_productSoldCounter = meter.CreateCounter<int>("contoso.product.sold");
}
public void ProductSold(string productName, int quantity)
{
_productSoldCounter.Add(quantity,
new KeyValuePair<string, object?>("contoso.product.name", productName));
}
}
// </snippet_ContosoMetrics>

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,17 @@
// <snippet_RegisterMetrics>
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ContosoMetrics>();
// </snippet_RegisterMetrics>
var app = builder.Build();
// <snippet_InjectAndUseMetrics>
app.MapPost("/complete-sale", (SaleModel model, ContosoMetrics metrics) =>
{
// ... business logic such as saving the sale to a database ...
metrics.ProductSold(model.ProductName, model.QuantitySold);
});
// </snippet_InjectAndUseMetrics>
app.Run();

View File

@ -0,0 +1 @@
public record SaleModel(string ProductName, int QuantitySold);

View File

@ -0,0 +1,11 @@
GET http://localhost:5231/
###
POST http://localhost:5231/complete-sale
Content-Type: application/json
{
"productName": "Product One",
"quantitySold": 1
}

View File

@ -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;
// <snippet_TestClass>
public class BasicTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory) => _factory = factory;
[Fact]
public async Task Get_RequestCounterIncreased()
{
// Arrange
var client = _factory.CreateClient();
var meterFactory = _factory.Services.GetRequiredService<IMeterFactory>();
var collector = new MetricCollector<double>(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"]);
});
}
}
// </snippet_TestClass>

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Telemetry.Testing" Version="8.0.0-preview.7.23407.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0-preview.7.23375.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0-preview-23424-02" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.5.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.5.1" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.6.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0-rc.1" />
</ItemGroup>
</Project>
</Project>

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -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=<job_name>` 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.