create sample for custom metrics (#30183)
* create sample for custom metrics * simplify example and fix snippet to the original example * fix Get_RequestCounterIncreased testpull/30212/head
parent
a9355807af
commit
b866bc0f57
|
@ -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:
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -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();
|
|
@ -0,0 +1 @@
|
|||
public record SaleModel(string ProductName, int QuantitySold);
|
|
@ -0,0 +1,11 @@
|
|||
GET http://localhost:5231/
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:5231/complete-sale
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"productName": "Product One",
|
||||
"quantitySold": 1
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -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.
|
Loading…
Reference in New Issue