Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
780d51e
refactor: refactor services from static to instance classes
Dejmenek Sep 18, 2026
26fa5c5
feat: add distributed tracing to core TournamentAPI services
Dejmenek Sep 18, 2026
4286b8b
feat: add UserMetrics for tracking and logging user actions
Dejmenek Sep 18, 2026
c997a84
feat: add metrics tracking for bracket generation outcomes
Dejmenek Sep 18, 2026
6363a92
feat: add MatchMetrics for tracking match-related metrics
Dejmenek Sep 18, 2026
b999eb0
feat: add ParticipantMetrics service and integrate with mutations
Dejmenek Sep 18, 2026
77ff4cd
feat: add metrics for closed and deleted tournaments
Dejmenek Sep 18, 2026
89d7276
refactor: inject services into mutation methods instead of using stat…
Dejmenek Sep 18, 2026
61b5f7e
feat: add GraphQLMetrics for OpenTelemetry metrics tracking
Dejmenek Sep 18, 2026
8cc118a
feat: add PushEntityContext for enhanced entity-aware logging
Dejmenek Sep 18, 2026
ba95f38
fix: use IDbContextFactory instead of DbContext in services
Dejmenek Sep 18, 2026
19a293b
feat: register meters for Brackets, Matches, Participants, Users, GQL
Dejmenek Sep 18, 2026
7abcd7a
feat: add custom OnRejected handler for rate limiter with logger
Dejmenek Sep 18, 2026
025094f
feat: enhance ExecutionEventListener logging and metrics
Dejmenek Sep 18, 2026
fa474eb
feat: register TournamentActivitySource for tracing integration
Dejmenek Sep 18, 2026
9ad4aee
build: add OpenTelemetry EF Core instrumentation and update deps
Dejmenek Sep 18, 2026
6749f36
feat: add UnhandledExceptionErrorFilter for GraphQL errors
Dejmenek Sep 18, 2026
8287d55
feat: add ParticipantMetrics for tournament join tracking
Dejmenek Sep 18, 2026
b839917
feat: add logging and remove requestId from HttpRequestInterceptor
Dejmenek Sep 18, 2026
7a80d0c
refactor: switch to Serilog with OpenTelemetry for logging
Dejmenek Sep 18, 2026
ca55b9b
test: refactor tests to use instance methods and DI
Dejmenek Sep 18, 2026
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 @@ -24,6 +24,7 @@
<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.EntityFrameworkCore" Version="1.18.0-beta.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
Expand Down
22 changes: 17 additions & 5 deletions TournamentAPI.Benchmarks/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1314,19 +1314,19 @@
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==",
"resolved": "1.18.0",
"contentHash": "Dhzm5oughIY9EWckt2J1yXcEbXpki5WCL2q7prJoYxzH6C7jdkfGtPMTkUVZZ4AhjJKzQnlC+kLSIlmIVwC4Vw==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "10.0.0"
}
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==",
"resolved": "1.18.0",
"contentHash": "io6A6PJFp3vIObRGf6oPjdHsdglkinXt/0SpTDJHrVnsLxUtOzwh/QwTsnhBJ43wJ6QcGMkyNzsSSY8XEB1Q5Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"OpenTelemetry.Api": "1.15.3"
"OpenTelemetry.Api": "1.18.0"
}
},
"Perfolizer": {
Expand Down Expand Up @@ -1577,6 +1577,7 @@
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.18.0-beta.1, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
"Serilog": "[4.3.1, )",
"Serilog.AspNetCore": "[9.0.0, )",
Expand Down Expand Up @@ -1791,6 +1792,17 @@
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.EntityFrameworkCore": {
"type": "CentralTransitive",
"requested": "[1.18.0-beta.1, )",
"resolved": "1.18.0-beta.1",
"contentHash": "n15cNXxkSl9G6KD41wZYmujjERYOBiIgn2VeesTFhLtHZ+QQQE3baLrCk7UyzidEw/uGObRcW3SKhqpyMpGe1w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.18.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Http": {
"type": "CentralTransitive",
"requested": "[1.15.1, )",
Expand Down
22 changes: 17 additions & 5 deletions TournamentAPI.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1345,19 +1345,19 @@
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==",
"resolved": "1.18.0",
"contentHash": "Dhzm5oughIY9EWckt2J1yXcEbXpki5WCL2q7prJoYxzH6C7jdkfGtPMTkUVZZ4AhjJKzQnlC+kLSIlmIVwC4Vw==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "10.0.0"
}
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==",
"resolved": "1.18.0",
"contentHash": "io6A6PJFp3vIObRGf6oPjdHsdglkinXt/0SpTDJHrVnsLxUtOzwh/QwTsnhBJ43wJ6QcGMkyNzsSSY8XEB1Q5Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"OpenTelemetry.Api": "1.15.3"
"OpenTelemetry.Api": "1.18.0"
}
},
"Serilog.Extensions.Hosting": {
Expand Down Expand Up @@ -1641,6 +1641,7 @@
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.18.0-beta.1, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
"Serilog": "[4.3.1, )",
"Serilog.AspNetCore": "[9.0.0, )",
Expand Down Expand Up @@ -1855,6 +1856,17 @@
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.EntityFrameworkCore": {
"type": "CentralTransitive",
"requested": "[1.18.0-beta.1, )",
"resolved": "1.18.0-beta.1",
"contentHash": "n15cNXxkSl9G6KD41wZYmujjERYOBiIgn2VeesTFhLtHZ+QQQE3baLrCk7UyzidEw/uGObRcW3SKhqpyMpGe1w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.18.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Http": {
"type": "CentralTransitive",
"requested": "[1.15.1, )",
Expand Down
22 changes: 17 additions & 5 deletions TournamentAPI.LoadTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1595,19 +1595,19 @@
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==",
"resolved": "1.18.0",
"contentHash": "Dhzm5oughIY9EWckt2J1yXcEbXpki5WCL2q7prJoYxzH6C7jdkfGtPMTkUVZZ4AhjJKzQnlC+kLSIlmIVwC4Vw==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "10.0.0"
}
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.3",
"contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==",
"resolved": "1.18.0",
"contentHash": "io6A6PJFp3vIObRGf6oPjdHsdglkinXt/0SpTDJHrVnsLxUtOzwh/QwTsnhBJ43wJ6QcGMkyNzsSSY8XEB1Q5Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"OpenTelemetry.Api": "1.15.3"
"OpenTelemetry.Api": "1.18.0"
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
Expand Down Expand Up @@ -2742,6 +2742,7 @@
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.3, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )",
"OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.18.0-beta.1, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.1, )",
"Serilog": "[4.3.1, )",
"Serilog.AspNetCore": "[9.0.0, )",
Expand Down Expand Up @@ -2956,6 +2957,17 @@
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.EntityFrameworkCore": {
"type": "CentralTransitive",
"requested": "[1.18.0-beta.1, )",
"resolved": "1.18.0-beta.1",
"contentHash": "n15cNXxkSl9G6KD41wZYmujjERYOBiIgn2VeesTFhLtHZ+QQQE3baLrCk7UyzidEw/uGObRcW3SKhqpyMpGe1w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.18.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Http": {
"type": "CentralTransitive",
"requested": "[1.15.1, )",
Expand Down
33 changes: 18 additions & 15 deletions TournamentAPI.UnitTests/Services/BracketServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
using Microsoft.Extensions.Logging.Abstractions;
using TournamentAPI.Brackets;

namespace TournamentAPI.UnitTests.Services;

public class BracketServiceTests
{
private readonly BracketService _sut = new(NullLogger<BracketService>.Instance);

[Fact]
public void CreateBracket_SetsTournamentId()
{
var bracket = BracketService.CreateBracket(tournamentId: 42, participantIds: [1, 2]);
var bracket = _sut.CreateBracket(tournamentId: 42, participantIds: [1, 2]);

Assert.Equal(42, bracket.TournamentId);
}

[Fact]
public void CreateBracket_WithEvenParticipantCount_CreatesCorrectNumberOfMatches()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3, 4]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3, 4]);

Assert.Equal(2, bracket.Matches.Count);
}

[Fact]
public void CreateBracket_WithOddParticipantCount_CreatesCorrectNumberOfMatches()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);

Assert.Equal(2, bracket.Matches.Count);
}

[Fact]
public void CreateBracket_WithOddParticipantCount_LastMatchHasNullPlayer2()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);

var byeMatch = bracket.Matches.SingleOrDefault(m => m.Player2Id == null);
Assert.NotNull(byeMatch);
Expand All @@ -40,7 +43,7 @@ public void CreateBracket_WithOddParticipantCount_LastMatchHasNullPlayer2()
[Fact]
public void CreateBracket_WithOddParticipantCount_ByeMatchIsAutoResolved()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3]);

var byeMatch = bracket.Matches.Single(m => m.Player2Id == null);
Assert.Equal(byeMatch.Player1Id, byeMatch.WinnerId);
Expand All @@ -49,7 +52,7 @@ public void CreateBracket_WithOddParticipantCount_ByeMatchIsAutoResolved()
[Fact]
public void CreateBracket_AllMatchesAreInRoundOne()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3, 4]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2, 3, 4]);

Assert.All(bracket.Matches, m => Assert.Equal(1, m.Round));
}
Expand All @@ -58,7 +61,7 @@ public void CreateBracket_AllMatchesAreInRoundOne()
public void CreateBracket_AllParticipantIdsAppearInMatches()
{
var participantIds = new List<int> { 1, 2, 3, 4 };
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: participantIds);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: participantIds);

var usedIds = bracket.Matches
.SelectMany(m => new[] { m.Player1Id, m.Player2Id ?? -1 })
Expand All @@ -71,23 +74,23 @@ public void CreateBracket_AllParticipantIdsAppearInMatches()
[Fact]
public void CreateBracket_SetsMatchBracketReference()
{
var bracket = BracketService.CreateBracket(tournamentId: 1, participantIds: [1, 2]);
var bracket = _sut.CreateBracket(tournamentId: 1, participantIds: [1, 2]);

Assert.All(bracket.Matches, m => Assert.Same(bracket, m.Bracket));
}

[Fact]
public void CreateNextRoundMatches_WithEvenWinnerCount_CreatesCorrectNumberOfMatches()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3, 4]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3, 4]);

Assert.Equal(2, matches.Count);
}

[Fact]
public void CreateNextRoundMatches_WithOddWinnerCount_LastMatchHasNullPlayer2()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3]);

var byeMatch = matches.SingleOrDefault(m => m.Player2Id == null);
Assert.NotNull(byeMatch);
Expand All @@ -96,7 +99,7 @@ public void CreateNextRoundMatches_WithOddWinnerCount_LastMatchHasNullPlayer2()
[Fact]
public void CreateNextRoundMatches_WithOddWinnerCount_ByeMatchIsAutoResolved()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 2, 3]);

var byeMatch = matches.Single(m => m.Player2Id == null);
Assert.Equal(byeMatch.Player1Id, byeMatch.WinnerId);
Expand All @@ -105,15 +108,15 @@ public void CreateNextRoundMatches_WithOddWinnerCount_ByeMatchIsAutoResolved()
[Fact]
public void CreateNextRoundMatches_SetsCorrectRoundNumber()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 2, winners: [1, 2]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 2, winners: [1, 2]);

Assert.All(matches, m => Assert.Equal(3, m.Round));
}

[Fact]
public void CreateNextRoundMatches_SetsBracketId()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 7, roundNumber: 1, winners: [1, 2]);
var matches = _sut.CreateNextRoundMatches(bracketId: 7, roundNumber: 1, winners: [1, 2]);

Assert.All(matches, m => Assert.Equal(7, m.BracketId));
}
Expand All @@ -122,7 +125,7 @@ public void CreateNextRoundMatches_SetsBracketId()
public void CreateNextRoundMatches_NormalizesPlayerOrder_LowerIdIsPlayer1()
{
// Winners [3, 1] — higher ID first, so after normalization Player1Id should be 1
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [3, 1]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [3, 1]);

var match = matches.Single();
Assert.Equal(1, match.Player1Id);
Expand All @@ -132,7 +135,7 @@ public void CreateNextRoundMatches_NormalizesPlayerOrder_LowerIdIsPlayer1()
[Fact]
public void CreateNextRoundMatches_WhenPlayerOrderIsAlreadyNormalized_DoesNotSwap()
{
var matches = BracketService.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 3]);
var matches = _sut.CreateNextRoundMatches(bracketId: 1, roundNumber: 1, winners: [1, 3]);

var match = matches.Single();
Assert.Equal(1, match.Player1Id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using Microsoft.Extensions.Logging.Abstractions;
using TournamentAPI.Matches;

namespace TournamentAPI.UnitTests.Services;

public class MatchCascadePositionCalculatorTests
{
private readonly MatchCascadePositionCalculator _sut = new(NullLogger<MatchCascadePositionCalculator>.Instance);

[Fact]
public void GetDownstreamMatchId_WhenMatchIsAtEvenPosition_ReturnsFirstNextRoundMatch()
{
var currentRound = new List<int> { 10, 11, 12, 13 };
var nextRound = new List<int> { 20, 21 };

var result = MatchCascadePositionCalculator.GetDownstreamMatchId(currentRound, nextRound, 10);
var result = _sut.GetDownstreamMatchId(currentRound, nextRound, 10);

Assert.Equal(20, result);
}
Expand All @@ -21,7 +24,7 @@ public void GetDownstreamMatchId_WhenMatchIsAtOddPosition_ReturnsSameNextRoundMa
var currentRound = new List<int> { 10, 11, 12, 13 };
var nextRound = new List<int> { 20, 21 };

var result = MatchCascadePositionCalculator.GetDownstreamMatchId(currentRound, nextRound, 11);
var result = _sut.GetDownstreamMatchId(currentRound, nextRound, 11);

Assert.Equal(20, result);
}
Expand All @@ -32,7 +35,7 @@ public void GetDownstreamMatchId_WhenNextRoundNotGenerated_ReturnsNull()
var currentRound = new List<int> { 10, 11 };
var nextRound = new List<int>();

var result = MatchCascadePositionCalculator.GetDownstreamMatchId(currentRound, nextRound, 10);
var result = _sut.GetDownstreamMatchId(currentRound, nextRound, 10);

Assert.Null(result);
}
Expand All @@ -43,7 +46,7 @@ public void GetDownstreamMatchId_WhenMatchIdNotInCurrentRound_ReturnsNull()
var currentRound = new List<int> { 10, 11 };
var nextRound = new List<int> { 20 };

var result = MatchCascadePositionCalculator.GetDownstreamMatchId(currentRound, nextRound, 999);
var result = _sut.GetDownstreamMatchId(currentRound, nextRound, 999);

Assert.Null(result);
}
Expand All @@ -54,7 +57,7 @@ public void GetDownstreamMatchId_WhenPositionIsTrailingByeNotYetGenerated_Return
var currentRound = new List<int> { 10, 11, 12 };
var nextRound = new List<int> { 20 };

var result = MatchCascadePositionCalculator.GetDownstreamMatchId(currentRound, nextRound, 12);
var result = _sut.GetDownstreamMatchId(currentRound, nextRound, 12);

Assert.Null(result);
}
Expand Down
Loading
Loading