Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion MetricFlow.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Expand All @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MetricFlow.Tests", "tests\M
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetKit.MetricFlow.Tracker", "src\DotnetKit.MetricFlow.Tracker\DotnetKit.MetricFlow.Tracker.csproj", "{60112D90-095C-44EA-90E9-846B4EDBF758}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiExample", "examples\WebApiExample\WebApiExample.csproj", "{62617707-AE70-492E-B36D-63D0108EC269}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,6 +34,10 @@ Global
{60112D90-095C-44EA-90E9-846B4EDBF758}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60112D90-095C-44EA-90E9-846B4EDBF758}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60112D90-095C-44EA-90E9-846B4EDBF758}.Release|Any CPU.Build.0 = Release|Any CPU
{62617707-AE70-492E-B36D-63D0108EC269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62617707-AE70-492E-B36D-63D0108EC269}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62617707-AE70-492E-B36D-63D0108EC269}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62617707-AE70-492E-B36D-63D0108EC269}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -40,6 +46,7 @@ Global
{78058871-2DC0-6810-ED9E-588E6E3A1265} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{3897CD1F-6CA5-41EC-9845-1979426CD7B8} = {01644C81-1609-407B-A964-A15BFA59E822}
{60112D90-095C-44EA-90E9-846B4EDBF758} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{62617707-AE70-492E-B36D-63D0108EC269} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7CE4E738-181F-42CA-B63C-365DBD1BC7B9}
Expand Down
86 changes: 86 additions & 0 deletions examples/WebApiExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using DotnetKit.MetricFlow.Tracker;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton(sp => new MetricTracker("WebApiExample"));
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();

//add a middleware to track API calls
app.Use(async (context, next) =>
{
//skip tracking metrics for the metrics endpoint
if (context.Request.Path == "/metrics")
{
await next();
return;
}
var tracker = context.RequestServices.GetRequiredService<MetricTracker>();
tracker.In(context.Request.Path);
//tracking requests with errors
try
{
await next();
}
catch (Exception e)
{
tracker.Out(context.Request.Path, new Dictionary<string, string>() { ["error_message"] = e.Message }, failed: true);
throw;
}
tracker.Out(context.Request.Path);
});
}

app.UseHttpsRedirection();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.MapGet("/throw_exception", () =>
{
throw new Exception("This is an exception");
})
.WithName("Exception")
.WithOpenApi();

app.MapGet("/metrics", () =>
{
var tracker = app.Services.GetRequiredService<MetricTracker>();
return tracker.ToString();

})
.WithName("Metrics")
.WithOpenApi();

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
41 changes: 41 additions & 0 deletions examples/WebApiExample/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:64475",
"sslPort": 44327
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5045",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7286;http://localhost:5045",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
156 changes: 156 additions & 0 deletions examples/WebApiExample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Web API Example with MetricFlow

This example demonstrates how to use the `MetricFlow` library to track and measure metrics in a .NET Web API application. The example includes a `Program` class that sets up the web application, configures middleware to track API calls, and exposes metrics via an endpoint.

## Goal

The goal of this example is to showcase how to integrate `MetricFlow` into a .NET Web API application to track various metrics such as request counts and durations for different API endpoints and indicate failed request count.

This helps in monitoring the performance and usage of the API.

## Overview of Implementation

### Key Components

- **MetricTracker:** Tracks metrics for different API endpoints.
- **Middleware:** Automatically tracks metrics for incoming requests.
- **Endpoints:** Demonstrates how to track metrics for specific API endpoints and expose metrics.

### Implementation Details

1. **Service Registration:**
- The `MetricTracker` service is registered as a global service in the dependency injection container.

```csharp
builder.Services.AddSingleton<MetricTracker>(sp => new MetricTracker("WebApiExample"));
```

2. **Middleware Configuration:**
- Middleware is added to track metrics for incoming requests, excluding the `/metrics` endpoint.
- This oveview doesn't track failed requests, please look source code of this example for complete implementation.

```csharp
app.Use(async (context, next) =>
{
if (context.Request.Path != "/metrics")
{
var tracker = context.RequestServices.GetRequiredService<MetricTracker>();
tracker.In(context.Request.Path);
await next();
tracker.Out(context.Request.Path);
}
else
{
await next();
}
});
```

3. **Endpoints:**
- The `/weatherforecast` endpoint simulates a weather forecast API and tracks metrics for the endpoint.
- The `/metrics` endpoint exposes the tracked metrics.

```csharp
app.MapGet("/weatherforecast", (MetricTracker tracker) =>
{
tracker.In("GetWeatherForecast");
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
tracker.Out("GetWeatherForecast");
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.MapGet("/metrics", (MetricTracker tracker) =>
{
return tracker.ToString();
})
.WithName("Metrics")
.WithOpenApi();
```

### Code Example

```csharp
using System.Diagnostics;
using DotnetKit.MetricFlow.Tracker;
using DotnetKit.MetricFlow.Tracker.Abstractions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSinglton<MetricTracker>(sp => new MetricTracker("WebApiExample"));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.Use(async (context, next) =>
{
if (context.Request.Path != "/metrics")
{
var tracker = context.RequestServices.GetRequiredService<MetricTracker>();
tracker.In(context.Request.Path);
await next();
tracker.Out(context.Request.Path);
}
else
{
await next();
}
});

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", (MetricTracker tracker) =>
{
tracker.In("GetWeatherForecast");
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
tracker.Out("GetWeatherForecast");
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.MapGet("/metrics", (MetricTracker tracker) =>
{
return tracker.ToString();
})
.WithName("Metrics")
.WithOpenApi();

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

![alt text](image.png)
18 changes: 18 additions & 0 deletions examples/WebApiExample/WebApiExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DotnetKit.MetricFlow.Tracker\DotnetKit.MetricFlow.Tracker.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions examples/WebApiExample/WebApiExample.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@WebApiExample_HostAddress = http://localhost:5045

GET {{WebApiExample_HostAddress}}/weatherforecast/
Accept: application/json

###
8 changes: 8 additions & 0 deletions examples/WebApiExample/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions examples/WebApiExample/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Binary file added examples/WebApiExample/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion src/DotnetKit.MetricFlow.Tracker/Abstractions/CounterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public abstract class CounterBase(string name, Dictionary<string, string>? metri
private DateTime? _startedAt = null;
private DateTime? _endedAt = null;
private long _inCount = 0;
private long _failedCount = 0;
private long _outCount = 0;
private long _totalDuration = 0;

Expand All @@ -21,6 +22,7 @@ public abstract class CounterBase(string name, Dictionary<string, string>? metri
public CounterValues Values => new CounterValues(
Interlocked.Read(ref _inCount),
Interlocked.Read(ref _outCount),
Interlocked.Read(ref _failedCount),
TimeSpan.FromMilliseconds(Interlocked.Read(ref _totalDuration)),
TimeSpan.FromMilliseconds(Interlocked.Read(ref _averageDuration)),
TimeSpan.FromMilliseconds(Interlocked.Read(ref _minDuration)),
Expand All @@ -39,9 +41,14 @@ public long Inc()
return Interlocked.Increment(ref _inCount);
}

public long Dec()
public long Dec(bool? failed = false)
{
FinalizeState(Stop());

if (failed == true)
{
Interlocked.Increment(ref _failedCount);
}
return Interlocked.Increment(ref _outCount);
}

Expand Down
Loading