Skip to content
Merged
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [General Info](#general-info)
- [Technologies](#technologies)
- [Running the API](#running-the-api)
- [Detailed API Documentation](#detailed-api-documentation)
- [Authentication](#authentication)
- [Tournament Management](#tournament-management)
Expand Down Expand Up @@ -36,6 +37,46 @@

---


## Running the API

### Prerequisites

- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
- [Docker](https://www.docker.com/) (for Prometheus and Grafana)

### Start Prometheus and Grafana

The API exports metrics via OpenTelemetry's OTLP exporter directly to Prometheus. Before starting the API, bring up the observability stack with Docker Compose:

```bash
docker-compose up -d
```

This starts:

| Service | URL |
| --- | --- |
| Prometheus | http://localhost:5431 |
| Grafana | http://localhost:3000 |

Prometheus is configured with `--web.enable-otlp-receiver` so it accepts OTLP pushes from the API. The scrape interval is set to 15 s globally (10 s for the Prometheus self-scrape job).

### Start the API

```bash
dotnet run --project TournamentAPI
```

The API will push metrics to Prometheus automatically once running.

### Grafana

Open `http://localhost:3000`, log in with the default credentials (`admin` / `admin`), and add Prometheus (`http://prometheus:9090`) as a data source to build dashboards from the collected metrics.

---


## Detailed API Documentation

### Authentication
Expand Down
10 changes: 10 additions & 0 deletions TournamentAPI.Benchmarks/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,7 @@
"Microsoft.EntityFrameworkCore.SqlServer": "[9.0.11, )",
"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[9.0.11, )",
"OpenTelemetry.Exporter.Console": "[1.15.3, )",
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
Expand Down Expand Up @@ -1511,6 +1512,15 @@
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
"dependencies": {
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
Expand Down
10 changes: 10 additions & 0 deletions TournamentAPI.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,7 @@
"Microsoft.EntityFrameworkCore.SqlServer": "[9.0.11, )",
"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[9.0.11, )",
"OpenTelemetry.Exporter.Console": "[1.15.3, )",
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
Expand Down Expand Up @@ -1527,6 +1528,15 @@
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
"dependencies": {
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
Expand Down
10 changes: 10 additions & 0 deletions TournamentAPI.LoadTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2499,6 +2499,7 @@
"Microsoft.EntityFrameworkCore.SqlServer": "[9.0.11, )",
"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[9.0.11, )",
"OpenTelemetry.Exporter.Console": "[1.15.3, )",
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
Expand Down Expand Up @@ -2654,6 +2655,15 @@
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
"dependencies": {
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
Expand Down
10 changes: 10 additions & 0 deletions TournamentAPI.UnitTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@
"Microsoft.EntityFrameworkCore.SqlServer": "[9.0.11, )",
"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[9.0.11, )",
"OpenTelemetry.Exporter.Console": "[1.15.3, )",
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
Expand Down Expand Up @@ -1295,6 +1296,15 @@
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
"dependencies": {
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
Expand Down
2 changes: 0 additions & 2 deletions TournamentAPI/Configuration/Extensions/MetricsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.Extensions.DependencyInjection;
using TournamentAPI.Metrics;

namespace TournamentAPI.Configuration.Extensions;
Expand All @@ -8,7 +7,6 @@ internal static class MetricsExtensions
internal static IServiceCollection AddApplicationMetrics(this IServiceCollection services)
{
services.AddSingleton<TournamentMetrics>();
services.AddHostedService(sp => sp.GetRequiredService<TournamentMetrics>());

return services;
}
Expand Down
7 changes: 5 additions & 2 deletions TournamentAPI/Configuration/Extensions/TelemetryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
Expand All @@ -23,7 +22,11 @@ internal static IServiceCollection AddApplicationTelemetry(this IServiceCollecti
{
metrics.AddMeter(MetricConstants.TournamentMeterName);
metrics.AddAspNetCoreInstrumentation();
metrics.AddConsoleExporter();
metrics.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://localhost:5431/api/v1/otlp/v1/metrics");
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
});
});

return services;
Expand Down
32 changes: 7 additions & 25 deletions TournamentAPI/Metrics/TournamentMetrics.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
using System.Diagnostics.Metrics;
using TournamentAPI.Data;
using TournamentAPI.Data.Models;

namespace TournamentAPI.Metrics;

public class TournamentMetrics : IHostedService
public class TournamentMetrics
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Counter<int> _tournamentsCreated;
private readonly UpDownCounter<int> _activeTournaments;

public TournamentMetrics(IMeterFactory meterFactory, IServiceScopeFactory serviceScopeFactory)
public TournamentMetrics(IMeterFactory meterFactory)
{
_serviceScopeFactory = serviceScopeFactory;

var meter = meterFactory.Create(MetricConstants.TournamentMeterName);
_tournamentsCreated = meter.CreateCounter<int>("tournamentapi.tournaments.tournaments_created", description: "Number of tournaments created");

_ = meter.CreateObservableGauge(
"tournamentapi.tournaments.active_tournaments",
() => GetActiveTournamentsCount(),
description: "Number of currently active tournaments");
_activeTournaments = meter.CreateUpDownCounter<int>("tournamentapi.tournaments.active_tournaments", description: "Number of currently active tournaments");
}

public void IncrementTournamentsCreated()
{
_tournamentsCreated.Add(1);
}

private int GetActiveTournamentsCount()
{
using var scope = _serviceScopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
return context.Tournaments.Count(t => t.Status == TournamentStatus.Open);
}
public void IncrementTournamentsCreated() => _tournamentsCreated.Add(1);

public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void TournamentOpened() => _activeTournaments.Add(1);

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void TournamentClosed() => _activeTournaments.Add(-1);
}
3 changes: 2 additions & 1 deletion TournamentAPI/TournamentAPI.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
Expand Down
19 changes: 18 additions & 1 deletion TournamentAPI/Tournaments/TournamentMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
await context.SaveChangesAsync(token);

tournamentMetrics.IncrementTournamentsCreated();
if (tournament.Status == TournamentStatus.Open)
tournamentMetrics.TournamentOpened();

return context.Tournaments.Where(t => t.Id == tournament.Id);
}
Expand All @@ -95,6 +97,7 @@
ClaimsPrincipal userClaims,
ApplicationDbContext context,
IResolverContext resolverContext,
TournamentMetrics tournamentMetrics,
CancellationToken token)
{
var userId = userClaims.GetUserId();
Expand All @@ -110,15 +113,22 @@

if (resolverContext.TryReportError(TournamentValidations.ValidateTournamentNameNotEmpty(input.Name)))
return null;
tournament.Name = input.Name;

Check warning on line 116 in TournamentAPI/Tournaments/TournamentMutations.cs

View workflow job for this annotation

GitHub Actions / build-test

Possible null reference assignment.


if (input.StartDate != null)
tournament.StartDate = input.StartDate.Value;

if (input.Status != null)
{
var previousStatus = tournament.Status;
tournament.Status = input.Status.Value;

if (previousStatus != TournamentStatus.Open && tournament.Status == TournamentStatus.Open)
tournamentMetrics.TournamentOpened();
else if (previousStatus == TournamentStatus.Open && tournament.Status != TournamentStatus.Open)
tournamentMetrics.TournamentClosed();
}

await context.SaveChangesAsync(token);

return context.Tournaments.Where(t => t.Id == tournament.Id);
Expand All @@ -130,13 +140,14 @@
ClaimsPrincipal userClaims,
ApplicationDbContext context,
IResolverContext resolverContext,
TournamentMetrics tournamentMetrics,
CancellationToken token)
{
var userId = userClaims.GetUserId();

var tournament = await context.Tournaments
.Include(t => t.Bracket)
.ThenInclude(b => b.Matches)

Check warning on line 150 in TournamentAPI/Tournaments/TournamentMutations.cs

View workflow job for this annotation

GitHub Actions / build-test

Dereference of a possibly null reference.
.Include(t => t.Participants)
.FirstOrDefaultAsync(t => t.Id == tournamentId, token);

Expand All @@ -146,6 +157,8 @@
if (resolverContext.TryReportError(TournamentValidations.ValidateIsOwner(tournament!.OwnerId, userId, tournamentId)))
return null;

var wasOpen = tournament!.Status == TournamentStatus.Open;

tournament.IsDeleted = true;

if (tournament.Bracket != null)
Expand All @@ -163,6 +176,10 @@
}

await context.SaveChangesAsync(token);

if (wasOpen)
tournamentMetrics.TournamentClosed();

return true;
}
}
9 changes: 9 additions & 0 deletions TournamentAPI/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "Direct",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
"dependencies": {
"OpenTelemetry": "1.15.3"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "Direct",
"requested": "[1.15.3, )",
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--web.enable-otlp-receiver'
ports:
- 5431:9090
volumes:
- ./prometheus/:/etc/prometheus/
- prometheus:/prometheus

grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- 3000:3000
volumes:
- grafana:/var/lib/grafana

volumes:
prometheus:
grafana:
10 changes: 10 additions & 0 deletions prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global:
scrape_interval: 15s

scrape_configs:
- job_name: 'prometheus'
scheme: 'http'
scrape_interval: 10s
scrape_timeout: 5s
static_configs:
- targets: ['localhost:9090']
Loading