Skip to content

Commit

Permalink
Added health endpoint and application insights configuration (#60)
Browse files Browse the repository at this point in the history
* Added health endpoint and application insights configuration

* Added health check test

---------

Co-authored-by: acn-dgopa <acn-dgopa@dev-acn-tje-14>
  • Loading branch information
acn-dgopa and acn-dgopa authored Dec 27, 2023
1 parent 754b6ec commit fd9bae9
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 6 deletions.
19 changes: 19 additions & 0 deletions src/Altinn.Auth.AuditLog.Core/Models/KeyVaultSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Altinn.Auth.AuditLog.Core.Models
{
/// <summary>
/// General configuration settings
/// </summary>
public class KeyVaultSettings
{
/// <summary>
/// Gets or sets the secret uri
/// </summary>
public string SecretUri { get; set; }

Check warning on line 17 in src/Altinn.Auth.AuditLog.Core/Models/KeyVaultSettings.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Non-nullable property 'SecretUri' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 17 in src/Altinn.Auth.AuditLog.Core/Models/KeyVaultSettings.cs

View workflow job for this annotation

GitHub Actions / Analyze

Non-nullable property 'SecretUri' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
}
7 changes: 7 additions & 0 deletions src/Altinn.Auth.AuditLog/Altinn.Auth.AuditLog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@

<ItemGroup>
<PackageReference Include="Altinn.Authorization.ABAC" Version="0.0.7" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.5.0" />
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.IdentityModel.Logging" Version="7.0.3" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.2" />
<PackageReference Include="Npgsql.DependencyInjection" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;

namespace Altinn.Auth.AuditLog.Configuration
{
/// <summary>
/// Set up custom telemetry for Application Insights
/// </summary>
public class CustomTelemetryInitializer : ITelemetryInitializer
{
/// <summary>
/// Custom TelemetryInitializer that sets some specific values for the component
/// </summary>
public void Initialize(ITelemetry telemetry)
{
if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName))
{
telemetry.Context.Cloud.RoleName = "auth-auditlog";
}
}
}
}
23 changes: 23 additions & 0 deletions src/Altinn.Auth.AuditLog/Health/HealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Altinn.Auth.AuditLog.Health
{
/// <summary>
/// Health check service configured in startup. See https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
/// Listen to
/// </summary>
public class HealthCheck : IHealthCheck
{
/// <summary>
/// Verifies the health status
/// </summary>
/// <param name="context">The healtcheck context</param>
/// <param name="cancellationToken">The cancellationtoken</param>
/// <returns>The health check result</returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(
HealthCheckResult.Healthy("A healthy result."));
}
}
}
47 changes: 47 additions & 0 deletions src/Altinn.Auth.AuditLog/Health/HealthTelemetryFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using System.Diagnostics.CodeAnalysis;

namespace Altinn.Auth.AuditLog.Health
{
/// <summary>
/// Filter to exclude health check request from Application Insights
/// </summary>
[ExcludeFromCodeCoverage]
public class HealthTelemetryFilter : ITelemetryProcessor
{
private ITelemetryProcessor Next { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="HealthTelemetryFilter"/> class.
/// </summary>
public HealthTelemetryFilter(ITelemetryProcessor next)
{
Next = next;
}

/// <inheritdoc/>
public void Process(ITelemetry item)
{
if (ExcludeItemTelemetry(item))
{
return;
}

Next.Process(item);
}

private bool ExcludeItemTelemetry(ITelemetry item)
{
RequestTelemetry request = item as RequestTelemetry;

if (request != null && request.Url.ToString().EndsWith("/health/"))
{
return true;
}

return false;
}
}
}
134 changes: 128 additions & 6 deletions src/Altinn.Auth.AuditLog/Program.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
using Altinn.Auth.AuditLog.Configuration;
using Altinn.Auth.AuditLog.Core.Models;
using Altinn.Auth.AuditLog.Core.Repositories.Interfaces;
using Altinn.Auth.AuditLog.Core.Services;
using Altinn.Auth.AuditLog.Core.Services.Interfaces;
using Altinn.Auth.AuditLog.Filters;
using Altinn.Auth.AuditLog.Health;
using Altinn.Auth.AuditLog.Persistence;
using Altinn.Auth.AuditLog.Persistence.Configuration;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel;
using Microsoft.Extensions.Logging.ApplicationInsights;
using Yuniql.AspNetCore;
using Yuniql.PostgreSql;
var builder = WebApplication.CreateBuilder(args);

ILogger logger;

string applicationInsightsKeySecretName = "ApplicationInsights--InstrumentationKey";
string applicationInsightsConnectionString = string.Empty;

ConfigureSetupLogging();

await SetConfigurationProviders(builder.Configuration);

ConfigureLogging(builder.Logging);


// Add services to the container.
ConfigureServices(builder.Services, builder.Configuration);

Expand All @@ -21,19 +42,17 @@

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.MapHealthChecks("/health");

ConfigurePostgreSql();

app.Run();
Expand Down Expand Up @@ -74,10 +93,113 @@ void ConfigureServices(IServiceCollection services, IConfiguration config)
builder.Configuration.GetValue<string>("PostgreSQLSettings:AdminConnectionString"),
builder.Configuration.GetValue<string>("PostgreSQLSettings:AuthAuditLogDbAdminPwd"));
bool logParameters = builder.Configuration.GetValue<bool>("PostgreSQLSettings:LogParameters");
services.AddHealthChecks().AddCheck<HealthCheck>("auditlog_ui_health_check");
services.AddSingleton<IAuthenticationEventService, AuthenticationEventService>();
services.AddSingleton<IAuthenticationEventRepository, AuthenticationEventRepository>();
services.AddSingleton<IAuthorizationEventService, AuthorizationEventService>();
services.AddSingleton<IAuthorizationEventRepository, AuthorizationEventRepository>();
services.Configure<PostgreSQLSettings>(config.GetSection("PostgreSQLSettings"));
services.Configure<KeyVaultSettings>(config.GetSection("KeyVaultSettings"));
services.AddNpgsqlDataSource(connectionString, builder => builder.EnableParameterLogging(logParameters));

if (!string.IsNullOrEmpty(applicationInsightsConnectionString))
{
services.AddSingleton(typeof(ITelemetryChannel), new ServerTelemetryChannel
{ StorageFolder = "/tmp/logtelemetry" });
services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions
{
ConnectionString = applicationInsightsConnectionString,
});

services.AddApplicationInsightsTelemetryProcessor<HealthTelemetryFilter>();
services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();

logger.LogInformation("Startup // ApplicationInsightsConnectionString = {applicationInsightsConnectionString}", applicationInsightsConnectionString);
}
}

void ConfigureSetupLogging()
{
// Setup logging for the web host creation
ILoggerFactory logFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddFilter("Altinn.AccessManagement.UI.Program", LogLevel.Debug)
.AddConsole();
});

logger = logFactory.CreateLogger<Program>();
}

void ConfigureLogging(ILoggingBuilder logging)
{
// Clear log providers
logging.ClearProviders();

// Setup up application insight if ApplicationInsightsConnectionString is available
if (!string.IsNullOrEmpty(applicationInsightsConnectionString))
{
// Add application insights https://docs.microsoft.com/en-us/azure/azure-monitor/app/ilogger
logging.AddApplicationInsights(
configureTelemetryConfiguration: config => config.ConnectionString = applicationInsightsConnectionString,
configureApplicationInsightsLoggerOptions: options => { });

// Optional: Apply filters to control what logs are sent to Application Insights.
// The following configures LogLevel Information or above to be sent to
// Application Insights for all categories.
logging.AddFilter<ApplicationInsightsLoggerProvider>(string.Empty, LogLevel.Warning);

// Adding the filter below to ensure logs of all severity from Program.cs
// is sent to ApplicationInsights.
logging.AddFilter<ApplicationInsightsLoggerProvider>(typeof(Program).FullName, LogLevel.Trace);
}
else
{
// If not application insight is available log to console
logging.AddFilter("Microsoft", LogLevel.Warning);
logging.AddFilter("System", LogLevel.Warning);
logging.AddConsole();
}
}

async Task SetConfigurationProviders(ConfigurationManager config)
{
config.AddEnvironmentVariables();

config.AddCommandLine(args);

if (!builder.Environment.IsDevelopment())
{
await ConnectToKeyVaultAndSetApplicationInsights(config);
}
}

async Task ConnectToKeyVaultAndSetApplicationInsights(ConfigurationManager config)
{
logger.LogInformation("Program // Connect to key vault and set up application insights");

KeyVaultSettings keyVaultSettings = new KeyVaultSettings();

config.GetSection("KeyVaultSettings").Bind(keyVaultSettings);
try
{
SecretClient client = new SecretClient(new Uri(keyVaultSettings.SecretUri), new DefaultAzureCredential());
KeyVaultSecret secret = await client.GetSecretAsync(applicationInsightsKeySecretName);
applicationInsightsConnectionString = string.Format("InstrumentationKey={0}", secret.Value);
}
catch (Exception vaultException)
{
logger.LogError(vaultException, "Unable to read application insights key.");
}

try
{
config.AddAzureKeyVault(new Uri(keyVaultSettings.SecretUri), new DefaultAzureCredential());
}
catch (Exception vaultException)
{
logger.LogError(vaultException, "Unable to add key vault secrets to config.");
}
}
57 changes: 57 additions & 0 deletions src/test/Altinn.Auth.AuditLog.Tests/Health/HealthCheckTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Altinn.Auth.AuditLog.Health;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace Altinn.Auth.AuditLog.Tests.Health
{
/// <summary>
/// Health check
/// </summary>
public class HealthCheckTests : IClassFixture<CustomWebApplicationFactory<HealthCheck>>
{
private readonly CustomWebApplicationFactory<HealthCheck> _factory;

/// <summary>
/// Default constructor
/// </summary>
/// <param name="fixture">The web application fixture</param>
public HealthCheckTests(CustomWebApplicationFactory<HealthCheck> fixture)
{
_factory = fixture;
}

/// <summary>
/// Verify that component responds on health check
/// </summary>
/// <returns></returns>
[Fact]
public async Task VerifyHealthCheck_OK()
{
HttpClient client = GetTestClient();

HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "/health");

HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
string content = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

private HttpClient GetTestClient()
{
HttpClient client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
});
}).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });

return client;
}
}
}

0 comments on commit fd9bae9

Please sign in to comment.