Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add endpoints for getting servicecontext usage #168

Merged
merged 1 commit into from
Feb 10, 2025
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
3 changes: 3 additions & 0 deletions Dan.Core/Config/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,9 @@ public static string GetConsentRedirectUrl(string accreditationId, string hmac)
public const int MaxReferenceLength = 50;

public static int DefaultHarvestTaskCancellation = 35;

public static string ApplicationInsightsResourceId => GetSetting("ApplicationInsightsResourceId");
public static string ApplicationInsightsCloudRoleName => GetSetting("ApplicationInsightsCloudRoleName");

private static string GetSetting(string settingKey)
{
Expand Down
1 change: 1 addition & 0 deletions Dan.Core/Dan.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Azure.Monitor.Query" Version="1.6.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" />
<PackageReference Include="GitInfo" Version="3.3.5">
<PrivateAssets>all</PrivateAssets>
Expand Down
44 changes: 44 additions & 0 deletions Dan.Core/FuncUsageStatistics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Net;
using Dan.Core.Attributes;
using Dan.Core.Extensions;
using Dan.Core.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace Dan.Core;

public class FuncUsageStatistics(IUsageStatisticsService usageStatisticsService)
{
[Function("MonthlyUsageStatistics"), NoAuthentication]
public async Task<HttpResponseData> MonthlyUsageStatistics(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "metadata/usage/month")]
HttpRequestData req)
{
var content = await usageStatisticsService.GetMonthlyUsage();
var response = req.CreateExternalResponse(HttpStatusCode.OK, content);
response.Headers.Add("Access-Control-Allow-Origin", "*");
return response;
}

[Function("LastYearUsageStatistics"), NoAuthentication]
public async Task<HttpResponseData> LastYearUsageStatistics(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "metadata/usage/lastYear")]
HttpRequestData req)
{
var content = await usageStatisticsService.GetLastYearsUsage();
var response = req.CreateExternalResponse(HttpStatusCode.OK, content);
response.Headers.Add("Access-Control-Allow-Origin", "*");
return response;
}

[Function("AllUsageStatistics"), NoAuthentication]
public async Task<HttpResponseData> AllUsageStatistics(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "metadata/usage/all")]
HttpRequestData req)
{
var content = await usageStatisticsService.GetAllUsage();
var response = req.CreateExternalResponse(HttpStatusCode.OK, content);
response.Headers.Add("Access-Control-Allow-Origin", "*");
return response;
}
}
8 changes: 8 additions & 0 deletions Dan.Core/Models/MonthlyUsageStatistics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Dan.Core.Models;

[Serializable]
public class MonthlyUsageStatistics
{
public string? ServiceContext { get; set; }
public long? Count { get; set; }
}
9 changes: 9 additions & 0 deletions Dan.Core/Models/YearlyUsageStatistics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Dan.Core.Models;

[Serializable]
public class YearlyUsageStatistics
{
public string? Time { get; set; }
public string? ServiceContext { get; set; }
public long? Count { get; set; }
}
1 change: 1 addition & 0 deletions Dan.Core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
services.AddScoped<IRequirementValidationService, RequirementValidationService>();
services.AddScoped<IAuthorizationRequestValidatorService, AuthorizationRequestValidatorService>();
services.AddScoped<IRequestContextService, RequestContextService>();
services.AddScoped<IUsageStatisticsService, UsageStatisticsService>();

services.AddTransient<ExceptionDelegatingHandler>();

Expand Down
165 changes: 165 additions & 0 deletions Dan.Core/Services/UsageStatisticsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using Azure.Core;
using Azure.Identity;
using Azure.Monitor.Query;
using Dan.Core.Config;
using Dan.Core.Models;

namespace Dan.Core.Services;

public interface IUsageStatisticsService
{
Task<List<MonthlyUsageStatistics>> GetMonthlyUsage();
Task<List<IGrouping<string?, YearlyUsageStatistics>>> GetLastYearsUsage();
Task<List<IGrouping<string?, YearlyUsageStatistics>>> GetAllUsage();
}

public class UsageStatisticsService : IUsageStatisticsService
{
private const string CountKey = "Antall";
private const string ServiceContextKey = "ServiceContext";
private const string SortingfieldKey = "Sortingfield";

private readonly LogsQueryClient _logsQueryClient = new(new DefaultAzureCredential());

public async Task<List<MonthlyUsageStatistics>> GetMonthlyUsage()
{
const int days = 30;
var queryTime = GetQueryTime(QueryTimeType.Monthly, days);
var groupingValue = GetQueryGrouping(QueryTimeType.Monthly);
var results = await _logsQueryClient.QueryResourceAsync(
new ResourceIdentifier(Settings.ApplicationInsightsResourceId),
GetQuery(queryTime, groupingValue),
new QueryTimeRange(TimeSpan.FromDays(days)));
var rows = results.Value.Table.Rows;
var usage = rows
.Select(r =>
new MonthlyUsageStatistics
{
ServiceContext = r[ServiceContextKey].ToString(),
Count = r[CountKey] as long?
})
.ToList();
return usage;
}

public async Task<List<IGrouping<string?,YearlyUsageStatistics>>> GetLastYearsUsage()
{
var lastYear = DateTime.UtcNow.Year - 1;
var lastJanuaryFirst = new DateTime(lastYear, 1, 1);
var timeSinceLastYearsStart = lastJanuaryFirst - DateTime.UtcNow;
var queryTime = GetQueryTime(QueryTimeType.Yearly, lastYear);
var groupingValue = GetQueryGrouping(QueryTimeType.Yearly);

var results = await _logsQueryClient.QueryResourceAsync(
new ResourceIdentifier(Settings.ApplicationInsightsResourceId),
GetQuery(queryTime, groupingValue),
new QueryTimeRange(timeSinceLastYearsStart));
var rows = results.Value.Table.Rows;
var usage = rows
.Select(r =>
new YearlyUsageStatistics
{
Time = r[SortingfieldKey].ToString(),
ServiceContext = r[ServiceContextKey].ToString(),
Count = r[CountKey] as long?
})
.ToList();
var grouped = usage.GroupBy(u => u.Time).ToList();
return grouped;
}

public async Task<List<IGrouping<string?, YearlyUsageStatistics>>> GetAllUsage()
{
var queryTime = GetQueryTime(QueryTimeType.All, default);
var groupingValue = GetQueryGrouping(QueryTimeType.All);

var results = await _logsQueryClient.QueryResourceAsync(
new ResourceIdentifier(Settings.ApplicationInsightsResourceId),
GetQuery(queryTime, groupingValue),
QueryTimeRange.All);
var rows = results.Value.Table.Rows;
var usage = rows
.Select(r =>
new YearlyUsageStatistics
{
Time = r[SortingfieldKey].ToString(),
ServiceContext = r[ServiceContextKey].ToString(),
Count = r[CountKey] as long?
})
.ToList();
var grouped = usage.GroupBy(u => u.Time).ToList();
return grouped;
}

private static string GetQuery(string timeValue, string groupingValue)
{
// First bit is just to make the sorting field out result more readable
return $$"""
let f=(a: int, b:int) {
case(
a == 1,
strcat("Januar ", b),
a == 2,
strcat("Februar ", b),
a == 3,
strcat("Mars ", b),
a == 4,
strcat("April ", b),
a == 5,
strcat("Mai ", b),
a == 6,
strcat("Juni ", b),
a == 7,
strcat("Juli ", b),
a == 8,
strcat("August ", b),
a == 9,
strcat("September ", b),
a == 10,
strcat("Oktober " , b),
a == 11,
strcat("November ", b),
a == 12,
strcat("Desember ", b),
"Error"
)
};
traces
{{timeValue}}
| where cloud_RoleName == "{{Settings.ApplicationInsightsCloudRoleName}}"
| where tostring(customDimensions["action"]) == "DatasetRetrieved"
| project timestamp = timestamp, action = tostring(customDimensions["action"]), dataset = tostring(customDimensions["evidenceCodeName"]), {{ServiceContextKey}} = tostring(customDimensions["serviceContext"]), Konsument = tostring(customDimensions["requestor"])
| summarize {{CountKey}}=count() by {{groupingValue}}
| order by {{ServiceContextKey}}
""";
}

private static string GetQueryTime(QueryTimeType timeType, int value)
{
return timeType switch
{
QueryTimeType.Monthly => $"| where timestamp > ago({value}d)",
QueryTimeType.Yearly => $"| where getyear(timestamp) == {value}",
QueryTimeType.All => string.Empty,
_ => throw new ArgumentOutOfRangeException(nameof(timeType), timeType, null)
};
}

private static string GetQueryGrouping(QueryTimeType timeType)
{
return timeType switch
{
QueryTimeType.Monthly => $"{ServiceContextKey}",
QueryTimeType.Yearly => $"{SortingfieldKey}=f(getmonth(timestamp), getyear(timestamp)), {ServiceContextKey}",
QueryTimeType.All => $"{SortingfieldKey}=f(getmonth(timestamp), getyear(timestamp)), {ServiceContextKey}",
_ => throw new ArgumentOutOfRangeException(nameof(timeType), timeType, null)
};
}

private enum QueryTimeType
{
Monthly,
Yearly,
All
}
}
2 changes: 2 additions & 0 deletions Dan.Core/local.settings.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"FunctionKeyValue": null,
"MaskinportenClientId": null,
"ConsentValidationSecrets": "PRIMARY_SECRET,SECONDARY_SECRET",
"ApplicationInsightsResourceId": null,
"ApplicationInsightsCloudRoleName": null,
// Secrets end here. All the above secrets should have a corresponding entry below with KeyVault-prefix, which Settings.cs should then use/cache instead.

// Keyvault secret names begin here
Expand Down
Loading