diff --git a/Digdir.Domain.Dialogporten.sln b/Digdir.Domain.Dialogporten.sln index 04d95ab2f..d10c879e8 100644 --- a/Digdir.Domain.Dialogporten.sln +++ b/Digdir.Domain.Dialogporten.sln @@ -39,8 +39,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Digdir.Domain.Dialogporten. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Digdir.Domain.Dialogporten.Application.Integration.Tests", "tests\Digdir.Domain.Dialogporten.Application.Integration.Tests\Digdir.Domain.Dialogporten.Application.Integration.Tests.csproj", "{A1CC5E4E-020C-4ABA-BD66-48819259BDB3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Digdir.Tool.Dialogporten.SlackNotifier", "src\Digdir.Tool.Dialogporten.SlackNotifier\Digdir.Tool.Dialogporten.SlackNotifier.csproj", "{4CB06F0D-598E-4D2E-B26F-ECC488F64A04}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{3C2C775D-F2D1-42A2-B53F-CC6D5FF59633}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Tool.Dialogporten.Benchmarks", "src\Digdir.Tool.Dialogporten.Benchmarks\Digdir.Tool.Dialogporten.Benchmarks.csproj", "{3ED713E8-6E38-4E0F-B9D9-11726FB9F427}" @@ -65,8 +63,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Domain.Dialogporten. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Library.Utils.AspNet", "src\Digdir.Library.Utils.AspNet\Digdir.Library.Utils.AspNet.csproj", "{6A485C65-3613-4A49-A16F-2789119F6F38}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Tool.Dialogporten.SlackNotifier.Tests", "tests\Digdir.Tool.Dialogporten.SlackNotifier.Tests\Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj", "{F7DF2792-9C83-49F7-B7DD-556E8EC577DB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,10 +109,6 @@ Global {A1CC5E4E-020C-4ABA-BD66-48819259BDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1CC5E4E-020C-4ABA-BD66-48819259BDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1CC5E4E-020C-4ABA-BD66-48819259BDB3}.Release|Any CPU.Build.0 = Release|Any CPU - {4CB06F0D-598E-4D2E-B26F-ECC488F64A04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4CB06F0D-598E-4D2E-B26F-ECC488F64A04}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4CB06F0D-598E-4D2E-B26F-ECC488F64A04}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4CB06F0D-598E-4D2E-B26F-ECC488F64A04}.Release|Any CPU.Build.0 = Release|Any CPU {3ED713E8-6E38-4E0F-B9D9-11726FB9F427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3ED713E8-6E38-4E0F-B9D9-11726FB9F427}.Debug|Any CPU.Build.0 = Debug|Any CPU {3ED713E8-6E38-4E0F-B9D9-11726FB9F427}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -161,10 +153,6 @@ Global {6A485C65-3613-4A49-A16F-2789119F6F38}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A485C65-3613-4A49-A16F-2789119F6F38}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A485C65-3613-4A49-A16F-2789119F6F38}.Release|Any CPU.Build.0 = Release|Any CPU - {F7DF2792-9C83-49F7-B7DD-556E8EC577DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7DF2792-9C83-49F7-B7DD-556E8EC577DB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F7DF2792-9C83-49F7-B7DD-556E8EC577DB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F7DF2792-9C83-49F7-B7DD-556E8EC577DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -182,7 +170,6 @@ Global {9420D9DA-6C2F-4664-B361-FA5E2E1FB6EB} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA} {45A63837-99BC-469C-A2DE-FEE6975516C3} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA} {A1CC5E4E-020C-4ABA-BD66-48819259BDB3} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7} - {4CB06F0D-598E-4D2E-B26F-ECC488F64A04} = {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633} {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA} {3ED713E8-6E38-4E0F-B9D9-11726FB9F427} = {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633} {0A905E16-4602-4607-A75B-B1F7CA21179C} = {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633} @@ -195,7 +182,6 @@ Global {0900E3CF-F9D8-4B29-957F-484B3B028D6D} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA} {E389C7C8-9610-40AC-86DC-769B1B7DC78E} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7} {6A485C65-3613-4A49-A16F-2789119F6F38} = {096E9B69-6783-4446-A895-0B6D7729A0D9} - {F7DF2792-9C83-49F7-B7DD-556E8EC577DB} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B2FE67FF-7622-4AFB-AD8E-961B6A39D888} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/.gitignore b/src/Digdir.Tool.Dialogporten.SlackNotifier/.gitignore deleted file mode 100644 index ff5b00c50..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/.gitignore +++ /dev/null @@ -1,264 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# Azure Functions localsettings file -local.settings.json - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/AsciiTableFormatter.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/AsciiTableFormatter.cs deleted file mode 100644 index 674b4154e..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/AsciiTableFormatter.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Text; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.Common; - -public static class AsciiTableFormatter -{ - public static string ToAsciiTable(this IEnumerable> rows, int maxColumnWidth = 90) => - rows.Select(x => x.ToList()) - .ToList() - .ToAsciiTable(maxColumnWidth); - - private static readonly string[] StringArray = [""]; - - private static string ToAsciiTable(this List> rows, int maxColumnWidth) - { - var builder = new StringBuilder(); - - // Determine the maximum number of columns - var maxColumns = rows.Max(r => r.Count); - - // Determine column types before modifying cell contents - var types = GetColumnTypes(rows, maxColumns); - - AddLineBreaks(rows, maxColumnWidth); - - var sizes = MaxLengthInEachColumn(rows, maxColumns); - - // Top border - AppendLine(builder, sizes); - - // For each row - for (var rowNum = 0; rowNum < rows.Count; rowNum++) - { - var row = rows[rowNum]; - - // For each cell, split the content into lines - var cellLines = new List(); - for (var i = 0; i < maxColumns; i++) - { - if (i < row.Count) - { - var cell = row[i]; - cellLines.Add(cell?.ToString()?.Split('\n') ?? StringArray); - } - else - { - // Empty cell - cellLines.Add(StringArray); - } - } - - // Determine the maximum number of lines in this row - var maxLines = cellLines.Max(lines => lines.Length); - - // For each line index - for (var lineIndex = 0; lineIndex < maxLines; lineIndex++) - { - // For each cell - for (var i = 0; i < maxColumns; i++) - { - var lines = cellLines[i]; - var size = sizes[i]; - var type = types[i]; - - builder.Append("| "); - - var line = lineIndex < lines.Length ? lines[lineIndex] : ""; - - builder.Append(type == ColumnType.Numeric ? line.PadLeft(size) : line.PadRight(size)); - - builder.Append(' '); - - if (i == maxColumns - 1) - { - // Add right border for last column - builder.Append('|'); - } - } - builder.Append('\n'); - } - - // Add separator between rows - AppendLine(builder, sizes); - } - - return builder.ToString(); - } - - private static void AddLineBreaks(List> rows, int maxColumnWidth) - { - foreach (var row in rows) - { - for (var colIndex = 0; colIndex < row.Count; colIndex++) - { - if (row[colIndex] is string str) - { - var words = str.Split(' '); - var lines = new List(); - var currentLine = string.Empty; - - foreach (var word in words) - { - if ((currentLine + word).Length > maxColumnWidth) - { - lines.Add(currentLine.TrimEnd()); - - if (word.Length > maxColumnWidth) - { - maxColumnWidth = word.Length; - } - - currentLine = ""; - } - currentLine += word + " "; - } - if (currentLine.Length > 0) - { - lines.Add(currentLine.TrimEnd()); - } - - row[colIndex] = string.Join("\n", lines).Trim(); - } - else if (row[colIndex]?.ToString()?.Length > maxColumnWidth) - { - var strValue = row[colIndex].ToString(); - var brokenLines = new List(); - for (var i = 0; i < strValue?.Length; i += maxColumnWidth) - { - brokenLines.Add(strValue.Substring(i, Math.Min(maxColumnWidth, strValue.Length - i))); - if (brokenLines.Last().Length > maxColumnWidth) - { - maxColumnWidth = brokenLines.Last().Length; - } - } - row[colIndex] = string.Join("\n", brokenLines); - } - } - } - } - - private static void AppendLine(StringBuilder builder, IReadOnlyList sizes) - { - builder.Append('o'); - - foreach (var i in sizes) - { - builder.Append(new string('-', i + 2)); - builder.Append('o'); - } - builder.Append('\n'); - } - - private static List MaxLengthInEachColumn(IReadOnlyList> rows, int maxColumns) - { - var sizes = new List(); - for (var i = 0; i < maxColumns; i++) - { - var max = rows.Max(row => i < row.Count ? row[i]?.ToString()?.Split('\n').Max(line => line.Length) ?? 0 : 0); - sizes.Add(max); - } - return sizes; - } - - private static List GetColumnTypes(List> rows, int maxColumns) - { - var types = new List(); - for (var i = 0; i < maxColumns; i++) - { - var isNumeric = rows.Skip(1).All(row => i < row.Count && (row[i]?.GetType()?.IsNumericType() ?? false)); - var columnType = isNumeric ? ColumnType.Numeric : ColumnType.Text; - types.Add(columnType); - } - return types; - } - - /// - /// https://stackoverflow.com/a/5182747/2513761 - /// - private static bool IsNumericType(this Type type) - { - if (type == null) - { - return false; - } - - switch (Type.GetTypeCode(type)) - { - case TypeCode.Byte: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return true; - case TypeCode.Object: - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return Nullable.GetUnderlyingType(type)!.IsNumericType(); - } - return false; - case TypeCode.Empty: - case TypeCode.DBNull: - case TypeCode.Boolean: - case TypeCode.Char: - case TypeCode.DateTime: - case TypeCode.String: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return false; - } - - private enum ColumnType - { - Numeric, - Text - } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/Extensions.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/Extensions.cs deleted file mode 100644 index b6a97a0ba..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Common/Extensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Digdir.Tool.Dialogporten.SlackNotifier.External.AppInsights; -using Digdir.Tool.Dialogporten.SlackNotifier.Features.AzureAlertToSlackForwarder; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.Common; - -internal static class Extensions -{ - public static string ToAsciiTableExceptionReport(this IEnumerable responses) - { - var asciiTables = responses.SelectMany(x => x.Tables) - .Select(table => Enumerable.Empty>() - .Append(table.Columns.Select(x => (object)x.Name)) - .Concat(table.Rows) - .ToAsciiTable()); - return string.Join(Environment.NewLine, asciiTables); - } - - public static string ToQueryLink(this AzureAlertDto azureAlertRequest) - { - // This is a predefined KQL query that will get all exceptions for the last 24h, ordered by timestamp descending. - const string encodedKqlQuery = "H4sIAAAAAAAAA0utSE4tKMnMzyvmqlHIL0pJLVJIqlQoycxNLS5JzC1QSEktTgYAbgDhFSQAAAA%253D/timespan/P1D"; - var link = azureAlertRequest.Data.AlertContext.Condition.AllOf - .Select(x => x.LinkToFilteredSearchResultsUI) - .First(x => !string.IsNullOrWhiteSpace(x)); - link = RemoveQuery(link) + encodedKqlQuery; - return link; - } - - private static string RemoveQuery(string inputUrl) - { - var index = inputUrl.IndexOf("q/", StringComparison.Ordinal); - - if (index >= 0) - { - return inputUrl[..(index + 2)]; // Include "q/" - } - - return inputUrl; // "q/" not found, return the original URL - } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Digdir.Tool.Dialogporten.SlackNotifier.csproj b/src/Digdir.Tool.Dialogporten.SlackNotifier/Digdir.Tool.Dialogporten.SlackNotifier.csproj deleted file mode 100644 index 0b6bb41f5..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Digdir.Tool.Dialogporten.SlackNotifier.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - v4 - Exe - dd6841cd-1cfd-4f1c-a63e-edc687bff657 - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - PreserveNewest - Never - - - - - - diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/AppInsightsQueryResponseDto.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/AppInsightsQueryResponseDto.cs deleted file mode 100644 index 1c815e354..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/AppInsightsQueryResponseDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.AppInsights; - -internal sealed class AppInsightsQueryResponseDto -{ - public required Table[] Tables { get; set; } -} - -internal sealed class Table -{ - public required Column[] Columns { get; set; } - public required List> Rows { get; set; } -} - -internal sealed class Column -{ - public required string Name { get; set; } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/IAppInsightsClient.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/IAppInsightsClient.cs deleted file mode 100644 index c250ee9e2..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/AppInsights/IAppInsightsClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Azure.Core; -using Digdir.Tool.Dialogporten.SlackNotifier.Features.AzureAlertToSlackForwarder; -using System.Net.Http.Headers; -using System.Net.Http.Json; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.AppInsights; - -internal interface IAppInsightsClient -{ - Task QueryAppInsights(AzureAlertDto azureAlertRequest, CancellationToken cancellationToken); -} - -internal sealed class AppInsightsClient : IAppInsightsClient -{ - private readonly HttpClient _httpClient; - private readonly TokenCredential _credentials; - - public AppInsightsClient(HttpClient httpClient, TokenCredential credentials) - { - _httpClient = httpClient; - _credentials = credentials; - } - - public async Task QueryAppInsights(AzureAlertDto azureAlertRequest, CancellationToken cancellationToken) - { - const string appInsightsTokenScope = "https://api.applicationinsights.io"; - var token = await _credentials.GetTokenAsync(new TokenRequestContext([appInsightsTokenScope]), cancellationToken); - var requests = azureAlertRequest.Data.AlertContext.Condition.AllOf - .Select(x => - { - var request = new HttpRequestMessage(HttpMethod.Get, x.LinkToFilteredSearchResultsAPI); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); - return request; - }) - .Select(_httpClient.SendAsync); - var responses = await Task.WhenAll(requests); - - foreach (var httpResponseMessage in responses) - { - httpResponseMessage.EnsureSuccessStatusCode(); - } - - var typedResponses = await Task.WhenAll(responses.Select(x => - x.Content.ReadFromJsonAsync(cancellationToken: cancellationToken))); - return typedResponses!; - } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/ISlackClient.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/ISlackClient.cs deleted file mode 100644 index 3a55a7ec1..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/ISlackClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; - -internal interface ISlackClient -{ - Task SendAsync(SlackRequestDto message, CancellationToken cancellationToken); -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackClient.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackClient.cs deleted file mode 100644 index deaa2f0db..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackClient.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.Options; -using System.Net.Http.Json; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; - -internal sealed class SlackClient : ISlackClient -{ - private readonly HttpClient _httpClient; - private readonly IOptions _slackOptions; - - public SlackClient(HttpClient httpClient, IOptions slackOptions) - { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _slackOptions = slackOptions ?? throw new ArgumentNullException(nameof(slackOptions)); - } - - public async Task SendAsync(SlackRequestDto message, CancellationToken cancellationToken) - { - // TEMP DEBUG - Console.WriteLine($"Slack WebhookURL: {_slackOptions.Value.WebhookUrl}"); - if (!Uri.TryCreate(_slackOptions.Value.WebhookUrl, UriKind.Absolute, out var uri)) - { - return; - } - - await _httpClient.PostAsJsonAsync(uri, message, cancellationToken); - } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackOptions.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackOptions.cs deleted file mode 100644 index ce3c9eb6e..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; - -internal sealed class SlackOptions -{ - public const string ConfigurationSection = "Slack"; - - public required string WebhookUrl { get; init; } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackRequestDto.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackRequestDto.cs deleted file mode 100644 index c166e23ba..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/External/Slack/SlackRequestDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; - -internal sealed class SlackRequestDto -{ - public required string ExceptionReport { get; init; } - public required string Link { get; init; } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/AzureAlertDto.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/AzureAlertDto.cs deleted file mode 100644 index 4c8605d03..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/AzureAlertDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Digdir.Tool.Dialogporten.SlackNotifier.Features.AzureAlertToSlackForwarder; - -public class AzureAlertDto -{ - public required AzureAlertDataDto Data { get; set; } -} - -public class AzureAlertDataDto -{ - public required AzureAlertContextDto AlertContext { get; set; } -} - -public class AzureAlertContextDto -{ - public required AzureAlertConditionDto Condition { get; set; } -} - -public class AzureAlertConditionDto -{ - public required AzureAlertAllofDto[] AllOf { get; set; } -} - -public class AzureAlertAllofDto -{ - public required string LinkToFilteredSearchResultsUI { get; set; } - public required string LinkToFilteredSearchResultsAPI { get; set; } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/ForwardAlertToSlack.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/ForwardAlertToSlack.cs deleted file mode 100644 index 1c12da0fe..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Features/AzureAlertToSlackForwarder/ForwardAlertToSlack.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using System.Net; -using Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; -using Digdir.Tool.Dialogporten.SlackNotifier.External.AppInsights; -using System.Diagnostics; -using Digdir.Tool.Dialogporten.SlackNotifier.Common; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.Features.AzureAlertToSlackForwarder; - -internal sealed class ForwardAlertToSlack -{ - private readonly ISlackClient _slack; - private readonly IAppInsightsClient _appInsights; - - public ForwardAlertToSlack(ISlackClient slack, IAppInsightsClient appInsights) - { - _slack = slack; - _appInsights = appInsights; - } - - [Function("ForwardAlertToSlack")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, - CancellationToken cancellationToken) - { - var azureAlertRequest = await req.ReadFromJsonAsync(cancellationToken) ?? throw new UnreachableException(); - var appInsightsResponses = await _appInsights.QueryAppInsights(azureAlertRequest, cancellationToken); - - await _slack.SendAsync(new SlackRequestDto - { - ExceptionReport = appInsightsResponses.ToAsciiTableExceptionReport(), - Link = azureAlertRequest.ToQueryLink() - }, cancellationToken); - - return req.CreateResponse(HttpStatusCode.OK); - } -} diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Program.cs b/src/Digdir.Tool.Dialogporten.SlackNotifier/Program.cs deleted file mode 100644 index 9000e4d62..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Azure.Core; -using Azure.Identity; -using Digdir.Tool.Dialogporten.SlackNotifier.External.AppInsights; -using Digdir.Tool.Dialogporten.SlackNotifier.External.Slack; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -await new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureAppConfiguration(x => x.AddUserSecrets(optional: true, reloadOnChange: false)) - .ConfigureServices((hostContext, services) => - { - services.AddSingleton(); - services.AddHttpClient(); - services.AddHttpClient(); - services.Configure(hostContext.Configuration.GetSection(SlackOptions.ConfigurationSection)); - }) - .StartAsync(); diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/launchSettings.json b/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/launchSettings.json deleted file mode 100644 index 099a31ae6..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "profiles": { - "Digdir.Tool.Dialogporten.SlackNotifier": { - "commandName": "Project", - "commandLineArgs": "--port 7137", - "launchBrowser": false - } - } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.json b/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.json deleted file mode 100644 index df4dcc9d8..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights" - }, - "storage1": { - "type": "storage", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.local.json b/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.local.json deleted file mode 100644 index b804a2893..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights.sdk" - }, - "storage1": { - "type": "storage.emulator", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/README.md b/src/Digdir.Tool.Dialogporten.SlackNotifier/README.md deleted file mode 100644 index 5726f5748..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Slack notifier -This function app is designed to convert Azure log alert v2 to a Slack message formatted as an ASCII table. - -When a Azure log alert triggers it notifies every consumer of the configured Azure action group. One of the consumers is this Azure function app which receives a HTTP Post request with the [log alert v2 format](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-common-schema#sample-log-alert-when-the-monitoringservice--log-alerts-v2). See [AzureAlertDto.cs](./Features/AzureAlertToSlackForwarder/AzureAlertDto.cs) for the format this function app expects. - -The log alert v2 format does not include the actual query data which triggered the alert. Therefore the function app must fetch it by calling application insight. The data is then transformed to an ASCII table and pushed to the configured Slack webhook URL through the field `exceptionReport`. It will also include a link to the application insight log with the following predefined query in the field named `link`: -```KQL -exceptions -| order by timestamp desc -``` - -The configured Slack webhook will receive the following request: -```HTTP -HTTP POST [Slack_Webhook_Url] -{ - "exceptionReport": "Ascii_table_as_string", - "link": "Link_to_application_insight", -} -``` - -## Local development -1. [Login to azure](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/?tabs=command-line#exploring-the-sequence-of-defaultazurecredential-authentication-methods) -2. Configure the Slack webhook URL - ```powerhell - dotnet user-secrets set -p .\src\Digdir.Tool.Dialogporten.SlackNotifier\ "Slack:WebhookUrl" "SLACK_WEBHOOK_URL_HERE" - ``` -3. Start the function app -4. Send a log alert v2 format to the app - -The configured URL doesn't have to be an actual Slack workflow webhook URL. It could point to an online webhook tester like https://webhook.site or a homemade webhook tester on your local machine. - -### Get a valid log alert v2 request -This function app uses the links in the incoming alerts request to fetch data. Therefore the requests are app instance and time specific. The provided example request is most likely to be invalid by the time this article is read. Do the following to get a valid request: -1. Go to https://webhook.site and copy your unique URL -2. Add the URL as a webhook action of the azure action group -3. Trigger the alert -4. Copy the request from https://webhook.site into Postman. It may take several minutes for the alert to produce a request to the webhook. -5. Delete the webhook action from the azure action group - -Example log alert v2 request: -```jsonc -{ - "schemaId": "azureMonitorCommonAlertSchema", - "data": { - "essentials": { - "alertId": "/subscriptions/052982ed-1e94-4e26-bfd4-a65252931325/providers/Microsoft.AlertsManagement/alerts/3de19cbd-afe1-1d68-219c-25a339960013", - "alertRule": "Exception occured", - "severity": "Sev1", - "signalType": "Log", - "monitorCondition": "Fired", - "monitoringService": "Log Alerts V2", - "alertTargetIDs": [ - "/subscriptions/052982ed-1e94-4e26-bfd4-a65252931325/resourcegroups/dppoc-rg/providers/microsoft.insights/components/dppoc-applicationinsights" - ], - "configurationItems": [ - "/subscriptions/052982ed-1e94-4e26-bfd4-a65252931325/resourceGroups/dppoc-rg/providers/microsoft.insights/components/dppoc-applicationInsights" - ], - "originAlertId": "b7bbd427-e2a0-4891-8bcc-c689f7bf30e4", - "firedDateTime": "2023-11-03T11:48:02.7701858Z", - "description": "", - "essentialsVersion": "1.0", - "alertContextVersion": "1.0" - }, - "alertContext": { - "conditionType": "LogQueryCriteria", - "condition": { - "windowSize": "PT5M", - "allOf": [ - { - "searchQuery": "exceptions\n| summarize count = count() by environment = tostring(customDimensions.AspNetCoreEnvironment), problemId\n\n", - "metricMeasureColumn": null, - "targetResourceTypes": "['microsoft.insights/components']", - "operator": "GreaterThan", - "threshold": "0", - "timeAggregation": "Count", - "dimensions": [], - "metricValue": 1.0, - "failingPeriods": { - "numberOfEvaluationPeriods": 1, - "minFailingPeriodsToAlert": 1 - }, - "linkToSearchResultsUI": "https://portal.azure.com#@cd0026d8-283b-4a55-9bfa-d0ef4a8ba21c/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2F052982ed-1e94-4e26-bfd4-a65252931325%2FresourceGroups%2Fdppoc-rg%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2Fdppoc-applicationInsights%22%7D%5D%7D/q/eJxLrUhOLSjJzM8r5qpRKC7NzU0syqxKVUjOL80rUbCF0BqaCkmVCql5ZZlF%2BXm5qWCJkvzikqLMvHSN5NLikvxcl0ygeDHIGD3H4gK%2F1BLn%2FKJUV4QOTR2FgqL8pJzUXM8UAA%3D%3D/prettify/1/timespan/2023-11-03T11%3a42%3a25.0000000Z%2f2023-11-03T11%3a47%3a25.0000000Z", - "linkToFilteredSearchResultsUI": "https://portal.azure.com#@cd0026d8-283b-4a55-9bfa-d0ef4a8ba21c/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2F052982ed-1e94-4e26-bfd4-a65252931325%2FresourceGroups%2Fdppoc-rg%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2Fdppoc-applicationInsights%22%7D%5D%7D/q/eJxLrUhOLSjJzM8r5qpRKC7NzU0syqxKVUjOL80rUbCF0BqaCkmVCql5ZZlF%2BXm5qWCJkvzikqLMvHSN5NLikvxcl0ygeDHIGD3H4gK%2F1BLn%2FKJUV4QOTR2FgqL8pJzUXM8UAA%3D%3D/prettify/1/timespan/2023-11-03T11%3a42%3a25.0000000Z%2f2023-11-03T11%3a47%3a25.0000000Z", - "linkToSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/3a744853-dbe6-4ad8-91ff-c585c79c4ce5/query?query=exceptions%0A%7C%20summarize%20count%20%3D%20count%28%29%20by%20environment%20%3D%20tostring%28customDimensions.AspNetCoreEnvironment%29%2C%20problemId×pan=2023-11-03T11%3a42%3a25.0000000Z%2f2023-11-03T11%3a47%3a25.0000000Z", - "linkToFilteredSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/3a744853-dbe6-4ad8-91ff-c585c79c4ce5/query?query=exceptions%0A%7C%20summarize%20count%20%3D%20count%28%29%20by%20environment%20%3D%20tostring%28customDimensions.AspNetCoreEnvironment%29%2C%20problemId×pan=2023-11-03T11%3a42%3a25.0000000Z%2f2023-11-03T11%3a47%3a25.0000000Z" - } - ], - "windowStartTime": "2023-11-03T11:42:25Z", - "windowEndTime": "2023-11-03T11:47:25Z" - } - }, - "customProperties": { - "CanISendProblemId": "$problemId", - "SomeStaticPayload": "staticPayload" - } - } -} - -``` diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/host.json b/src/Digdir.Tool.Dialogporten.SlackNotifier/host.json deleted file mode 100644 index ee5cf5f83..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/host.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - }, - "enableLiveMetricsFilters": true - } - } -} \ No newline at end of file diff --git a/src/Digdir.Tool.Dialogporten.SlackNotifier/local.settings.json.COPYME b/src/Digdir.Tool.Dialogporten.SlackNotifier/local.settings.json.COPYME deleted file mode 100644 index 48024b222..000000000 --- a/src/Digdir.Tool.Dialogporten.SlackNotifier/local.settings.json.COPYME +++ /dev/null @@ -1,8 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "Slack__WebhookUrl": "TODO: Add to local managed user secrets. Remember - different separator (Slack:WebhookUrl)." - } -} diff --git a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/AsciiTableFormatterTests.cs b/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/AsciiTableFormatterTests.cs deleted file mode 100644 index d0f4837e7..000000000 --- a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/AsciiTableFormatterTests.cs +++ /dev/null @@ -1,214 +0,0 @@ -using Digdir.Tool.Dialogporten.SlackNotifier.Common; - -namespace Digdir.Tool.Dialogporten.SlackNotifier.Tests; - -public class AsciiTableFormatterTests -{ - [Fact] - public void AddLineBreaks_TextLongerThanMaxColumnWidth_ShouldWrapText() - { - // Arrange - List> rows = - [ - ["Header1"], - ["This is a very long text that exceeds the max column width"] - ]; - - const int maxColumnWidth = 20; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o----------------------o - | Header1 | - o----------------------o - | This is a very long | - | text that exceeds | - | the max column width | - o----------------------o - - """; - Assert.Equal(expected, result); - } - - [Fact] - public void ToAsciiTable_WordLongerThanMaxColumnWidth_ShouldNotBreakWord() - { - // Arrange - List> rows = - [ - ["Header1"], - ["Supercalifragilisticexpialidocious"] - ]; - - const int maxColumnWidth = 10; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o------------------------------------o - | Header1 | - o------------------------------------o - | Supercalifragilisticexpialidocious | - o------------------------------------o - - """; - - Assert.Equal(expected, result); - } - - [Fact] - public void ToAsciiTable_CellWithNull_ShouldHandleNullGracefully() - { - // Arrange - List> rows = - [ - ["Header1"], - [null!] - ]; - - const int maxColumnWidth = 10; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o---------o - | Header1 | - o---------o - | | - o---------o - - """; - - Assert.Equal(expected, result); - } - - [Fact] - public void ToAsciiTable_CellWithEmptyString_ShouldHandleEmptyString() - { - // Arrange - List> rows = - [ - ["Header1"], - [""] - ]; - - const int maxColumnWidth = 10; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o---------o - | Header1 | - o---------o - | | - o---------o - - """; - Assert.Equal(expected, result); - } - - [Fact] - public void ToAsciiTable_CellWithNumeric_ShouldRightAlign() - { - // Arrange - List> rows = - [ - ["Header1"], - [12345] - ]; - - const int maxColumnWidth = 5; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o---------o - | Header1 | - o---------o - | 12345 | - o---------o - - """; - - Assert.Equal(expected, result); - } - - - [Fact] - public void ToAsciiTable_CellWithNonStringObject_ShouldConvertAndWrap() - { - // Arrange - List> rows = - [ - ["Header1"], - [1234567890] - ]; - - const int maxColumnWidth = 5; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o---------o - | Header1 | - o---------o - | 1234567 | - | 890 | - o---------o - - """; - - Assert.Equal(expected, result); - } - - [Fact] - public void ToAsciiTable_JaggedTable_ShouldPadWithEmptyCells() - { - // Arrange - List> rows = - [ - ["Header1", "Header2"], - ["a", "b", "c"], - ["a", "b"] - ]; - - const int maxColumnWidth = 5; - - // Act - var result = rows.ToAsciiTable(maxColumnWidth); - - // Assert - const string expected = - """ - o---------o---------o---o - | Header1 | Header2 | | - o---------o---------o---o - | a | b | c | - o---------o---------o---o - | a | b | | - o---------o---------o---o - - """; - - Assert.Equal(expected, result); - } -} diff --git a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj b/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj deleted file mode 100644 index f14af31e1..000000000 --- a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - false - true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - -