Skip to content

Commit 03ee3d3

Browse files
author
Jonas Gauffin
committed
Added the recommendation service
1 parent 51ef62e commit 03ee3d3

File tree

9 files changed

+298
-27
lines changed

9 files changed

+298
-27
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Coderr.Server.App.Modules.Mine
4+
{
5+
/// <summary>
6+
/// A provider that will suggest incidents
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// A provider can recommend several incident. If they do, it's recommended that each incident get a different
11+
/// score.
12+
/// If all providers are equally worth, they can give a score of 100 to the most recommended incident. However,
13+
/// some providers affect the business more (like partitions) and could therefore specify a score multiplier for
14+
/// all suggested incidents.
15+
/// </para>
16+
/// <para>
17+
/// The provider will be executed in a container scope.
18+
/// </para>
19+
/// </remarks>
20+
public interface IRecommendationProvider
21+
{
22+
/// <summary>
23+
/// Suggest an incident.
24+
/// </summary>
25+
/// <param name="context">Context information</param>
26+
/// <returns>task</returns>
27+
Task Recommend(RecommendIncidentContext context);
28+
}
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
4+
namespace Coderr.Server.App.Modules.Mine
5+
{
6+
/// <summary>
7+
/// Will gather all recommendations and sort them by the number of points.
8+
/// </summary>
9+
public interface IRecommendationService
10+
{
11+
Task<List<RecommendedIncident>> GetRecommendations(int accountId, int? applicationId = null);
12+
}
13+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Coderr.Server.App.Modules.Mine
6+
{
7+
/// <summary>
8+
/// Context for <see cref="IRecommendationProvider" />
9+
/// </summary>
10+
public class RecommendIncidentContext
11+
{
12+
private readonly List<RecommendedIncident> _items;
13+
14+
public RecommendIncidentContext(List<RecommendedIncident> items)
15+
{
16+
_items = items ?? throw new ArgumentNullException(nameof(items));
17+
}
18+
19+
/// <summary>
20+
/// User that want a suggestion
21+
/// </summary>
22+
public int AccountId { get; set; }
23+
24+
/// <summary>
25+
/// If the user have selected a specific application
26+
/// </summary>
27+
public int? ApplicationId { get; set; }
28+
29+
/// <summary>
30+
/// Number of items each provider should suggest.
31+
/// </summary>
32+
public int NumberOfItems { get; set; } = 10;
33+
34+
/// <summary>
35+
/// Add another item.
36+
/// </summary>
37+
/// <param name="suggestion"></param>
38+
/// <param name="scoreMultiplier">Used by providers that are worth more. 1 = equally worth. Corresponds to "Weight" for partitions. At most 10.</param>
39+
public void Add(RecommendedIncident suggestion, double scoreMultiplier)
40+
{
41+
if (suggestion == null) throw new ArgumentNullException(nameof(suggestion));
42+
43+
// Should only suggest the same incident once.
44+
// thus if there are multiple suggestions for the same incident, increase the importance
45+
// and include both motivations.
46+
var first = _items.FirstOrDefault(x => x.IncidentId == suggestion.IncidentId);
47+
if (first != null)
48+
{
49+
first.Score += (int)(suggestion.Score * scoreMultiplier);
50+
first.Motivation += "\r\n" + suggestion.Motivation;
51+
return;
52+
}
53+
54+
_items.Add(suggestion);
55+
}
56+
}
57+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Coderr.Server.Abstractions.Boot;
6+
using log4net;
7+
8+
namespace Coderr.Server.App.Modules.Mine
9+
{
10+
[ContainerService]
11+
public class RecommendationService : IRecommendationService
12+
{
13+
private readonly IRecommendationProvider[] _providers;
14+
private ILog _logger = LogManager.GetLogger(typeof(RecommendationService));
15+
16+
public RecommendationService(IEnumerable<IRecommendationProvider> providers)
17+
{
18+
_providers = providers.ToArray();
19+
}
20+
21+
public async Task<List<RecommendedIncident>> GetRecommendations(int accountId, int? applicationId = null)
22+
{
23+
var items = new List<RecommendedIncident>();
24+
var context = new RecommendIncidentContext(items) { AccountId = accountId, ApplicationId = applicationId };
25+
foreach (var provider in _providers)
26+
{
27+
try
28+
{
29+
await provider.Recommend(context);
30+
}
31+
catch (Exception ex)
32+
{
33+
_logger.Error("Provider " + provider.GetType().FullName + " failed.", ex);
34+
}
35+
36+
}
37+
38+
return items.OrderByDescending(x => x.Score).Take(10).ToList();
39+
}
40+
}
41+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace Coderr.Server.App.Modules.Mine
2+
{
3+
/// <summary>
4+
/// A suggestion
5+
/// </summary>
6+
/// <remarks>
7+
/// <para>
8+
/// The suggested incident service will enrich the information with incident details.
9+
/// </para>
10+
/// </remarks>
11+
public class RecommendedIncident
12+
{
13+
/// <summary>
14+
/// Application that the incident belongs to
15+
/// </summary>
16+
/// <remarks>
17+
/// <para>
18+
/// Will be enriched by the suggestion service if left 0
19+
/// </para>
20+
/// </remarks>
21+
public int ApplicationId { get; set; }
22+
23+
/// <summary>
24+
/// Name of the application
25+
/// </summary>
26+
/// <remarks>
27+
/// <para>
28+
/// Will be enriched by the suggestion service if left 0
29+
/// </para>
30+
/// </remarks>
31+
public string ApplicationName { get; set; }
32+
33+
/// <summary>
34+
/// Suggested incident
35+
/// </summary>
36+
public int IncidentId { get; set; }
37+
38+
/// <summary>
39+
/// Why this item was suggested.
40+
/// </summary>
41+
public string Motivation { get; set; }
42+
43+
/// <summary>
44+
/// Calculated score.
45+
/// </summary>
46+
/// <remarks>
47+
/// 100 points should be distributed between all incidents that a provider recommends.
48+
/// </remarks>
49+
public int Score { get; set; }
50+
}
51+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.0</TargetFramework>
4+
<RootNamespace>Coderr.Server.SqlServer</RootNamespace>
5+
<AssemblyName>Coderr.Server.SqlServer</AssemblyName>
6+
<Configurations>Debug;Release;Premise</Configurations>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="DnsClient" Version="1.6.0" />
10+
<PackageReference Include="DotNetCqs" Version="2.1.1" />
11+
<PackageReference Include="Griffin.Framework" Version="2.1.1" />
12+
<PackageReference Include="log4net" Version="2.0.14" />
13+
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
14+
<PackageReference Include="System.Data.SqlClient" Version="4.8.3" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\Coderr.Server.Api\Coderr.Server.Api.csproj" />
19+
<ProjectReference Include="..\Coderr.Server.App\Coderr.Server.App.csproj" />
20+
<ProjectReference Include="..\Coderr.Server.Domain\Coderr.Server.Domain.csproj" />
21+
<ProjectReference Include="..\Coderr.Server.Infrastructure\Coderr.Server.Infrastructure.csproj" />
22+
<ProjectReference Include="..\Coderr.Server.Abstractions\Coderr.Server.Abstractions.csproj" />
23+
<ProjectReference Include="..\Coderr.Server.ReportAnalyzer.Abstractions\Coderr.Server.ReportAnalyzer.Abstractions.csproj" />
24+
<ProjectReference Include="..\Coderr.Server.ReportAnalyzer\Coderr.Server.ReportAnalyzer.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<EmbeddedResource Include="Schema\*.sql" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<None Remove="Schema\Coderr.v31.sql" />
33+
<None Remove="Schema\Coderr.v32.sql" />
34+
<None Remove="Schema\Coderr.v33.sql" />
35+
<None Remove="Schema\Coderr.v34.sql" />
36+
<None Remove="Schema\Coderr.v35.sql" />
37+
<None Remove="Schema\Coderr.v36.sql" />
38+
</ItemGroup>
39+
40+
<ItemGroup>
41+
<Folder Include="Modules\Mine\Incidents\" />
42+
</ItemGroup>
43+
</Project>

src/Server/Coderr.Server.SqlServer/Logs/LogsRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Griffin.Data;
77
using Newtonsoft.Json;
88

9-
namespace Coderr.Server.Common.Data.SqlServer.Logs
9+
namespace Coderr.Server.SqlServer.Logs
1010
{
1111
[ContainerService]
1212
public class LogsRepository : ILogsRepository
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using Coderr.Server.Abstractions.Boot;
4+
using Coderr.Server.App.Modules.Mine;
5+
using Griffin.Data;
6+
using Griffin.Data.Mapper;
7+
8+
namespace Coderr.Server.SqlServer.Modules.Mine.Incidents
9+
{
10+
[ContainerService]
11+
class IncidentWithMostReportsProvider : IRecommendationProvider
12+
{
13+
private readonly IAdoNetUnitOfWork _uow;
14+
15+
public IncidentWithMostReportsProvider(IAdoNetUnitOfWork uow)
16+
{
17+
_uow = uow;
18+
}
19+
20+
public async Task Recommend(RecommendIncidentContext context)
21+
{
22+
var sql = context.ApplicationId > 0
23+
? $@"SELECT TOP(5) Incidents.Id IncidentId, Applications.Id as ApplicationId, Applications.Name as ApplicationName, ReportCount as Score, 'Replace' as Motivation
24+
FROM Incidents
25+
JOIN Applications ON (Applications.Id = Incidents.ApplicationId)
26+
WHERE State = 0
27+
AND ApplicationId = {context.ApplicationId.Value}
28+
ORDER BY ReportCount DESC"
29+
: $@"SELECT TOP(5) Incidents.Id IncidentId, Applications.Id as ApplicationId, Applications.Name as ApplicationName, ReportCount as Score, 'Replace' as Motivation
30+
FROM Incidents
31+
JOIN Applications ON (Applications.Id = Incidents.ApplicationId)
32+
WHERE State = 0
33+
ORDER BY ReportCount DESC";
34+
35+
var items = await _uow.ToListAsync<RecommendedIncident>(new MirrorMapper<RecommendedIncident>(), sql);
36+
if (!items.Any())
37+
return;
38+
39+
var totalReports = (double)items.Sum(x => x.Score);
40+
foreach (var item in items)
41+
{
42+
var count = item.Score;
43+
item.Score = (int)((item.Score / totalReports) * 100);
44+
item.Motivation = $"Frequently reported ({count:N0} times)";
45+
context.Add(item, 1);
46+
}
47+
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)