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

Feature/add exception handling #562

Merged
merged 9 commits into from
Nov 1, 2024
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
7 changes: 5 additions & 2 deletions .azure/infrastructure/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ param maskinportenClientId string
@secure()
param platformSubscriptionKey string
@secure()
param notificationEmail string
param slackUrl string

import { Sku as KeyVaultSku } from '../modules/keyvault/create.bicep'
param keyVaultSku KeyVaultSku
Expand All @@ -45,6 +45,10 @@ var secrets = [
name: 'platform-subscription-key'
value: platformSubscriptionKey
}
{
name: 'slack-url'
value: slackUrl
}
]

// Create resource groups
Expand Down Expand Up @@ -124,7 +128,6 @@ module containerAppEnv '../modules/containerAppEnvironment/main.bicep' = {
location: location
namePrefix: namePrefix
migrationsStorageAccountName: migrationsStorageAccountName
emailReceiver: notificationEmail
}
}

Expand Down
2 changes: 1 addition & 1 deletion .azure/infrastructure/params.bicepparam
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ param migrationsStorageAccountName = readEnvironmentVariable('MIGRATION_STORAGE_
param maskinportenJwk = readEnvironmentVariable('MASKINPORTEN_JWK')
param maskinportenClientId = readEnvironmentVariable('MASKINPORTEN_CLIENT_ID')
param platformSubscriptionKey = readEnvironmentVariable('PLATFORM_SUBSCRIPTION_KEY')
param notificationEmail = readEnvironmentVariable('NOTIFICATION_EMAIL')
param slackUrl = readEnvironmentVariable('SLACK_URL')

// SKUs
param keyVaultSku = {
Expand Down
6 changes: 6 additions & 0 deletions .azure/modules/containerApp/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var containerAppEnvVars = [
value: 'true'
}
{ name: 'MaskinportenSettings__EncodedJwk', secretRef: 'maskinporten-jwk' }
{ name: 'GeneralSettings__SlackUrl', secretRef: 'slack-url' }
]
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: '${namePrefix}-app'
Expand Down Expand Up @@ -97,6 +98,11 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
keyVaultUrl: '${keyVaultUrl}/secrets/application-insights-connection-string'
name: 'application-insights-connection-string'
}
{
identity: principal_id
keyVaultUrl: '${keyVaultUrl}/secrets/slack-url'
name: 'slack-url'
}
]
}

Expand Down
55 changes: 1 addition & 54 deletions .azure/modules/containerAppEnvironment/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ param location string
param namePrefix string
@secure()
param keyVaultName string
@secure()
param emailReceiver string
param migrationsStorageAccountName string

resource log_analytics_workspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
Expand Down Expand Up @@ -41,58 +39,7 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01'
}
}
}
resource application_insights_action 'Microsoft.Insights/actionGroups@2023-01-01' =
if (emailReceiver != null && emailReceiver != '') {
name: '${namePrefix}-action'
location: 'global' // action group locations is limited, change to use location variable when new locations is added
dependsOn: [application_insights, containerAppEnvironment]
properties: {
groupShortName: 'broker-alert'
enabled: true
emailReceivers: [
{
name: 'emailReceiverForAlert'
emailAddress: emailReceiver
}
]
}
}
resource exceptionOccuredAlertRule 'Microsoft.Insights/scheduledQueryRules@2023-03-15-preview' =
if (emailReceiver != null && emailReceiver != '') {
name: '${namePrefix}-500-exception-occured'
location: location
properties: {
description: 'Alert for 500 errors in broker'
enabled: true
severity: 1
evaluationFrequency: 'PT5M'
windowSize: 'PT5M'
scopes: [log_analytics_workspace.id]
autoMitigate: false
targetResourceTypes: [
'microsoft.insights/components'
]
criteria: {
allOf: [
{
query: 'AppExceptions | where Properties.StatusCode startswith "5" '
operator: 'GreaterThan'
threshold: 0
timeAggregation: 'Count'
failingPeriods: {
numberOfEvaluationPeriods: 1
minFailingPeriodsToAlert: 1
}
}
]
}
actions: {
actionGroups: [
application_insights_action.id
]
}
}
}


resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
name: migrationsStorageAccountName
Expand Down
6 changes: 3 additions & 3 deletions .github/actions/update-infrastructure/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ inputs:
PLATFORM_SUBSCRIPTION_KEY:
description: "Subscription key for platform"
required: true
NOTIFICATION_EMAIL:
description: "Email for notifications"
SLACK_URL:
description: "URL for Slack channel to post to"
required: true


Expand Down Expand Up @@ -86,7 +86,7 @@ runs:
MASKINPORTEN_JWK: ${{ inputs.MASKINPORTEN_JWK }}
MASKINPORTEN_CLIENT_ID: ${{ inputs.MASKINPORTEN_CLIENT_ID }}
PLATFORM_SUBSCRIPTION_KEY: ${{ inputs.PLATFORM_SUBSCRIPTION_KEY }}
NOTIFICATION_EMAIL: ${{ inputs.NOTIFICATION_EMAIL }}
SLACK_URL: ${{ inputs.SLACK_URL }}
with:
scope: subscription
template: ./.azure/infrastructure/main.bicep
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-to-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
MASKINPORTEN_JWK: ${{ secrets.MASKINPORTEN_JWK }}
MASKINPORTEN_CLIENT_ID: ${{ secrets.MASKINPORTEN_CLIENT_ID }}
PLATFORM_SUBSCRIPTION_KEY: ${{ secrets.PLATFORM_SUBSCRIPTION_KEY }}
NOTIFICATION_EMAIL: ${{ secrets.NOTIFICATION_EMAIL }}
SLACK_URL: ${{ secrets.SLACK_URL }}

- name: Migrate database
uses: ./.github/actions/migrate-database
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.Broker.API/Helpers/SecurityHeadersMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Primitives;

namespace Altinn.Broker.Helpers;
public class SecurityHeadersMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
Expand Down
67 changes: 67 additions & 0 deletions src/Altinn.Broker.API/Helpers/SlackExceptionNotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Diagnostics;
using Slack.Webhooks;

namespace Altinn.Broker.Helpers;
public class SlackExceptionNotification : IExceptionHandler
{
private readonly ILogger<SlackExceptionNotification> _logger;
private readonly ISlackClient _slackClient;
private const string TestChannel = "#test-varslinger";
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
private readonly IHostEnvironment _hostEnvironment;

public SlackExceptionNotification(ILogger<SlackExceptionNotification> logger, ISlackClient slackClient, IHostEnvironment hostEnvironment)
{
_logger = logger;
_slackClient = slackClient;
_hostEnvironment = hostEnvironment;

}
public ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var exceptionMessage = FormatExceptionMessage(exception, httpContext);

_logger.LogError(
exception,
"Unhandled exception occurred. Type: {ExceptionType}, Message: {Message}, Path: {Path}",
exception.GetType().Name,
exception.Message,
httpContext.Request.Path);
Dismissed Show dismissed Hide dismissed
mSunberg marked this conversation as resolved.
Show resolved Hide resolved

try
{
SendSlackNotificationWithMessage(exceptionMessage);
}
catch (Exception slackEx)
{
_logger.LogError(
slackEx,
"Failed to send Slack notification");
}

return ValueTask.FromResult(false);
}

private string FormatExceptionMessage(Exception exception, HttpContext context)
{
return $":warning: *Unhandled Exception*\n" +
$"*Environment:* {_hostEnvironment.EnvironmentName}\n" +
$"*System:* Broker\n" +
$"*Type:* {exception.GetType().Name}\n" +
$"*Message:* {exception.Message}\n" +
$"*Path:* {context.Request.Path}\n" +
$"*Time:* {DateTime.UtcNow:u}\n" +
$"*Stacktrace:* \n{exception.StackTrace}";
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
private void SendSlackNotificationWithMessage(string message)
{
var slackMessage = new SlackMessage
{
Text = message,
Channel = TestChannel,
};
_slackClient.Post(slackMessage);
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 3 additions & 0 deletions src/Altinn.Broker.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Altinn.Broker.Persistence;
using Altinn.Broker.Persistence.Options;
using Altinn.Common.PEP.Authorization;
using Altinn.Broker.Helpers;

using Hangfire;

Expand Down Expand Up @@ -71,6 +72,7 @@ static void BuildAndRun(string[] args)
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseSerilogRequestLogging();
app.UseExceptionHandler();
mSunberg marked this conversation as resolved.
Show resolved Hide resolved

if (app.Environment.IsDevelopment())
{
Expand All @@ -96,6 +98,7 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddApplicationInsightsTelemetry();
services.AddExceptionHandler<SlackExceptionNotification>();

services.Configure<DatabaseOptions>(config.GetSection(key: nameof(DatabaseOptions)));
services.Configure<AzureResourceManagerOptions>(config.GetSection(key: nameof(AzureResourceManagerOptions)));
Expand Down
4 changes: 4 additions & 0 deletions src/Altinn.Broker.API/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization/authorize.admin",
"EncodedJwk": "",
"ExhangeToAltinnToken": true
},
"GeneralSettings": {
"SlackUrl": "",
"CorrespondenceBaseUrl": "https://localhost:7241/"
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Slack.Webhooks" Version="1.1.5" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/Altinn.Broker.Core/Options/GeneralSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Altinn.Broker.Core.Options;

public class GeneralSettings
{
public string SlackUrl { get; set; } = string.Empty;

public string CorrespondenceBaseUrl { get; set; } = string.Empty;
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.9" />
<PackageReference Include="Microsoft.Azure.Management.Storage" Version="25.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.6.3" />
<PackageReference Include="Slack.Webhooks" Version="1.1.5" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 12 additions & 1 deletion src/Altinn.Broker.Integrations/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
using Altinn.Broker.Integrations.Altinn.ResourceRegistry;
using Altinn.Broker.Integrations.Azure;
using Altinn.Broker.Persistence.Repositories;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Altinn.Broker.Integrations.Slack;
using Slack.Webhooks;

namespace Altinn.Broker.Integrations;
public static class DependencyInjection
Expand Down Expand Up @@ -51,5 +52,15 @@ public static void AddIntegrations(this IServiceCollection services, IConfigurat
services.AddHttpClient<IAuthorizationService, AltinnAuthorizationService>((client) => client.BaseAddress = new Uri(altinnOptions.PlatformGatewayUrl))
.AddMaskinportenHttpMessageHandler<SettingsJwkClientDefinition, IAuthorizationService>();
}
var generalSettings = new GeneralSettings();
configuration.GetSection(nameof(GeneralSettings)).Bind(generalSettings);
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
if (string.IsNullOrWhiteSpace(generalSettings.SlackUrl))
{
services.AddSingleton<ISlackClient>(new SlackDevClient(""));
}
else
{
services.AddSingleton<ISlackClient>(new SlackClient(generalSettings.SlackUrl));
}
}
}
46 changes: 46 additions & 0 deletions src/Altinn.Broker.Integrations/Slack/SlackDevClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Slack.Webhooks;

namespace Altinn.Broker.Integrations.Slack
{
public class SlackDevClient : ISlackClient
{
private readonly HttpClient _httpClient;
private readonly Uri _webhookUri;
private const string POST_SUCCESS = "ok";
public SlackDevClient(string webhookUrl, HttpClient httpClient = null)
{
_httpClient = httpClient ?? new HttpClient();
if (!Uri.TryCreate(webhookUrl, UriKind.Absolute, out _webhookUri))
// throw new ArgumentException("Please enter a valid webhook url"); Commented out from source code to avoid throwing exception when testing without providing a webhook url
return;
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
public virtual bool Post(SlackMessage slackMessage)
{
return PostAsync(slackMessage, false).Result;
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
public async Task<bool> PostAsync(SlackMessage slackMessage)
{
return await PostAsync(slackMessage, true);
}
public async Task<bool> PostAsync(SlackMessage slackMessage, bool configureAwait = true)
{
if (_webhookUri == null) return true; // Mock success if no webhook url is provided

using (var request = new HttpRequestMessage(HttpMethod.Post, _webhookUri))
{
request.Content = new StringContent(slackMessage.AsJson(), System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request).ConfigureAwait(configureAwait);
var content = await response.Content.ReadAsStringAsync();
return content.Equals(POST_SUCCESS, StringComparison.OrdinalIgnoreCase);
}
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
public bool PostToChannels(SlackMessage message, IEnumerable<string> channels)
{
return true;
}
public IEnumerable<Task<bool>> PostToChannelsAsync(SlackMessage message, IEnumerable<string> channels)
{
return [];
}
mSunberg marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading