From d119300b5ab0b58bc083cb3c2fdb50df0dc9835c Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 14 Mar 2024 23:21:41 -0400 Subject: [PATCH 01/79] Initial commit of Java projects, including a shared module and a validation service with dependencies on Kafka and SQL server --- Java/.dockerignore | 15 +++ Java/.gitignore | 29 +++++ Java/Dockerfile | 22 ++++ Java/pom.xml | 104 ++++++++++++++++++ Java/shared/pom.xml | 36 ++++++ .../lantanagroup/link/shared/FhirHelper.java | 32 ++++++ Java/shared/src/main/resources/empty.txt | 0 .../link/core/FhirHelperTests.java | 10 ++ .../validation/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + Java/validation/pom.xml | 64 +++++++++++ .../validation/ValidationApplication.java | 38 +++++++ .../consumers/PatientEvaluatedConsumer.java | 15 +++ .../validation/kafka/KafkaProperties.java | 25 +++++ .../model/PatientEvaluatedModel.java | 9 ++ .../serdes/PatientEvaluatedDeserializer.java | 33 ++++++ .../src/main/resources/application.yml | 13 +++ 17 files changed, 447 insertions(+) create mode 100644 Java/.dockerignore create mode 100644 Java/.gitignore create mode 100644 Java/Dockerfile create mode 100644 Java/pom.xml create mode 100644 Java/shared/pom.xml create mode 100644 Java/shared/src/main/java/com/lantanagroup/link/shared/FhirHelper.java create mode 100644 Java/shared/src/main/resources/empty.txt create mode 100644 Java/shared/src/test/java/com/lantanagroup/link/core/FhirHelperTests.java create mode 100644 Java/validation/.mvn/wrapper/maven-wrapper.jar create mode 100644 Java/validation/.mvn/wrapper/maven-wrapper.properties create mode 100644 Java/validation/pom.xml create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java create mode 100644 Java/validation/src/main/resources/application.yml diff --git a/Java/.dockerignore b/Java/.dockerignore new file mode 100644 index 000000000..eaef1a37e --- /dev/null +++ b/Java/.dockerignore @@ -0,0 +1,15 @@ +# Ignore Maven build artifacts +target/ +*.iml + +# Ignore IDE-specific files and directories +.idea/ +.vscode/ + +# Ignore Docker-related files +Dockerfile* +docker-compose.yml + +# Ignore version control system files +.git/ +.gitignore \ No newline at end of file diff --git a/Java/.gitignore b/Java/.gitignore new file mode 100644 index 000000000..417aa7de6 --- /dev/null +++ b/Java/.gitignore @@ -0,0 +1,29 @@ +**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Java/Dockerfile b/Java/Dockerfile new file mode 100644 index 000000000..2c8dcaa1a --- /dev/null +++ b/Java/Dockerfile @@ -0,0 +1,22 @@ +FROM maven:3.8.7-openjdk-18-slim AS deps +WORKDIR /app +COPY pom.xml /app +COPY shared/pom.xml /app/shared/pom.xml +COPY validation/pom.xml /app/validation/pom.xml +RUN mvn dependency:go-offline + +# Stage 1: Build the Spring Boot application +FROM maven:3.8.7-openjdk-18-slim AS build +WORKDIR /app +COPY --from=deps /root/.m2 /root/.m2 +COPY . /app +RUN mvn clean install -DskipTests +RUN mv validation/target/validation-*.jar validation/target/validation.jar + +# Stage 2: Run the Spring Boot application +FROM openjdk:17-jdk-alpine +WORKDIR /app +# Extract the version from the JAR filename using shell scripting +COPY --from=build /app/validation/target/validation.jar validation.jar +EXPOSE 8080 +CMD ["java", "-jar", "validation.jar"] \ No newline at end of file diff --git a/Java/pom.xml b/Java/pom.xml new file mode 100644 index 000000000..4ca490946 --- /dev/null +++ b/Java/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + com.lantanagroup.link + link + 0.0.1-SNAPSHOT + pom + + + shared + validation + + + + UTF-8 + 17 + 17 + 3.2.3 + 12.6.1.jre11 + 1.18.30 + 3.7.0 + 3.1.2 + 4.13.2 + + + + + org.projectlombok + lombok + ${lombok} + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.springframework.boot + spring-boot-starter-test + test + ${spring.boot} + + + com.microsoft.sqlserver + mssql-jdbc + runtime + ${mssql} + + + org.apache.kafka + kafka-streams + ${kafka.streams} + + + org.springframework.kafka + spring-kafka + ${spring.kafka} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + 7.0.2 + + + org.springframework.kafka + spring-kafka-test + ${spring.kafka} + test + + + junit + junit + ${junit} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/Java/shared/pom.xml b/Java/shared/pom.xml new file mode 100644 index 000000000..06e2d7335 --- /dev/null +++ b/Java/shared/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + com.lantanagroup.link + link + 0.0.1-SNAPSHOT + + + shared + 0.0.1-SNAPSHOT + Shared Library + Shared functionality across all Link Java projects/modules + + + 17 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + none + + + + + + diff --git a/Java/shared/src/main/java/com/lantanagroup/link/shared/FhirHelper.java b/Java/shared/src/main/java/com/lantanagroup/link/shared/FhirHelper.java new file mode 100644 index 000000000..2cd7c75a9 --- /dev/null +++ b/Java/shared/src/main/java/com/lantanagroup/link/shared/FhirHelper.java @@ -0,0 +1,32 @@ +package com.lantanagroup.link.shared; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import org.hl7.fhir.r4.model.Resource; + +public class FhirHelper { + private static FhirContext context; + private static IParser parser; + + public static FhirContext getContext() { + if (context == null) { + context = FhirContext.forR4(); + } + return context; + } + + public static IParser getParser() { + if (parser == null) { + parser = getContext().newJsonParser(); + } + return parser; + } + + public static void deserialize(String json) { + getParser().parseResource(json); + } + + public static void serialize(Resource resource) { + getParser().encodeResourceToString(resource); + } +} diff --git a/Java/shared/src/main/resources/empty.txt b/Java/shared/src/main/resources/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/Java/shared/src/test/java/com/lantanagroup/link/core/FhirHelperTests.java b/Java/shared/src/test/java/com/lantanagroup/link/core/FhirHelperTests.java new file mode 100644 index 000000000..ca2629618 --- /dev/null +++ b/Java/shared/src/test/java/com/lantanagroup/link/core/FhirHelperTests.java @@ -0,0 +1,10 @@ +package com.lantanagroup.link.core; + +import org.junit.Test; + +public class FhirHelperTests { + @Test + public void test() { + + } +} diff --git a/Java/validation/.mvn/wrapper/maven-wrapper.jar b/Java/validation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/Java/validation/.mvn/wrapper/maven-wrapper.properties b/Java/validation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/Java/validation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/Java/validation/pom.xml b/Java/validation/pom.xml new file mode 100644 index 000000000..c04e7b5ee --- /dev/null +++ b/Java/validation/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + com.lantanagroup.link + link + 0.0.1-SNAPSHOT + + + validation + 0.0.1-SNAPSHOT + Validation Service + Micro service for validating report submission data + + + 17 + + + + + org.springframework.boot + spring-boot-starter-security + ${spring.boot} + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot} + + + org.springframework.security + spring-security-test + test + + + + + com.lantanagroup.link + shared + 0.0.1-SNAPSHOT + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java new file mode 100644 index 000000000..d437b5a53 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java @@ -0,0 +1,38 @@ +package com.lantanagroup.link.validation; + +import com.lantanagroup.link.validation.kafka.KafkaProperties; +import com.lantanagroup.link.validation.model.PatientEvaluatedModel; +import com.lantanagroup.link.validation.serdes.PatientEvaluatedDeserializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; + +import java.util.HashMap; +import java.util.Map; + +@SpringBootApplication +@PropertySource("classpath:application.yml") +public class ValidationApplication { + @Autowired + private KafkaProperties kafkaProperties; + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(ValidationApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setBannerMode(Banner.Mode.OFF); + application.run(args); + } + + @Bean + public ConsumerFactory patientEvaluatedConsumerFactory() { + Map props = new HashMap<>(kafkaProperties.buildConsumerProperties()); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new PatientEvaluatedDeserializer()); + } +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java new file mode 100644 index 000000000..55a3683ea --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java @@ -0,0 +1,15 @@ +package com.lantanagroup.link.validation.consumers; + +import com.lantanagroup.link.validation.model.PatientEvaluatedModel; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class PatientEvaluatedConsumer { + @KafkaListener(topics = "PatientEvaluated", containerFactory = "kafkaListenerContainerFactory") + public void listen(ConsumerRecord record) { + System.out.println("Received message key: " + record.key()); + // Process your message here + } +} \ No newline at end of file diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java new file mode 100644 index 000000000..428c433ce --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java @@ -0,0 +1,25 @@ +package com.lantanagroup.link.validation.kafka; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class KafkaProperties { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Value("${spring.kafka.consumer.group-id}") + private String groupId; + + public Map buildConsumerProperties() { + Map properties = new HashMap<>(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + properties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + return properties; + } +} \ No newline at end of file diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java new file mode 100644 index 000000000..f176a6cf7 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java @@ -0,0 +1,9 @@ +package com.lantanagroup.link.validation.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PatientEvaluatedModel { +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java new file mode 100644 index 000000000..180de22bf --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java @@ -0,0 +1,33 @@ +package com.lantanagroup.link.validation.serdes; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lantanagroup.link.validation.model.PatientEvaluatedModel; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Deserializer; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class PatientEvaluatedDeserializer implements Deserializer { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void configure(Map configs, boolean isKey) { + } + + @Override + public PatientEvaluatedModel deserialize(String topic, byte[] data) { + try { + if (data == null) { + return null; + } + return objectMapper.readValue(new String(data, StandardCharsets.UTF_8), PatientEvaluatedModel.class); + } catch (Exception e) { + throw new SerializationException("Error when deserializing byte[] to MessageDto"); + } + } + + @Override + public void close() { + } +} diff --git a/Java/validation/src/main/resources/application.yml b/Java/validation/src/main/resources/application.yml new file mode 100644 index 000000000..b4f199531 --- /dev/null +++ b/Java/validation/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: ValidationService + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: validation + producer: + client-id: validation-service + #properties: + # security.protocol: SASL_PLAINTEXT + # sasl.mechanism: PLAIN + # sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="your-username" password="your-password"; \ No newline at end of file From e31c7eec9c8d3ba55eec1fd62eebafe46e50f462 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 14 Mar 2024 23:24:36 -0400 Subject: [PATCH 02/79] Renaming Dockerfile to Dockerfile.validation because the docker file *is* specific to validation Unless we want to use maven repositories for the shared module like we do with the .NET SLN (with Nuget), we need to keep the Dockerfiles at the root of the Java projects so that they can build their dependencies --- Java/.dockerignore | 2 +- Java/{Dockerfile => Dockerfile.validation} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Java/{Dockerfile => Dockerfile.validation} (100%) diff --git a/Java/.dockerignore b/Java/.dockerignore index eaef1a37e..d03a94cdf 100644 --- a/Java/.dockerignore +++ b/Java/.dockerignore @@ -7,7 +7,7 @@ target/ .vscode/ # Ignore Docker-related files -Dockerfile* +Dockerfile.validation docker-compose.yml # Ignore version control system files diff --git a/Java/Dockerfile b/Java/Dockerfile.validation similarity index 100% rename from Java/Dockerfile rename to Java/Dockerfile.validation From 21f79be7366ac14734e214c851ef40fcdbb985f3 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 12:59:59 -0400 Subject: [PATCH 03/79] Initial project setup --- LinkAdmin.BFF/Dockerfile | 25 +++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 21 ++++++ LinkAdmin.BFF/LinkAdmin.BFF.http | 6 ++ LinkAdmin.BFF/Program.cs | 68 ++++++++++++++++++++ LinkAdmin.BFF/Properties/launchSettings.json | 52 +++++++++++++++ LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 22 +++++++ LinkAdmin.BFF/appsettings.Development.json | 8 +++ LinkAdmin.BFF/appsettings.json | 52 +++++++++++++++ LinkNet.sln | 9 +++ 9 files changed, 263 insertions(+) create mode 100644 LinkAdmin.BFF/Dockerfile create mode 100644 LinkAdmin.BFF/LinkAdmin.BFF.csproj create mode 100644 LinkAdmin.BFF/LinkAdmin.BFF.http create mode 100644 LinkAdmin.BFF/Program.cs create mode 100644 LinkAdmin.BFF/Properties/launchSettings.json create mode 100644 LinkAdmin.BFF/Settings/LinkAdminConstants.cs create mode 100644 LinkAdmin.BFF/appsettings.Development.json create mode 100644 LinkAdmin.BFF/appsettings.json diff --git a/LinkAdmin.BFF/Dockerfile b/LinkAdmin.BFF/Dockerfile new file mode 100644 index 000000000..ae9d2a85e --- /dev/null +++ b/LinkAdmin.BFF/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["LinkAdmin.BFF/LinkAdmin.BFF.csproj", "LinkAdmin.BFF/"] +RUN dotnet restore "./LinkAdmin.BFF/./LinkAdmin.BFF.csproj" +COPY . . +WORKDIR "/src/LinkAdmin.BFF" +RUN dotnet build "./LinkAdmin.BFF.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./LinkAdmin.BFF.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "LinkAdmin.BFF.dll"] \ No newline at end of file diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj new file mode 100644 index 000000000..bb4d07364 --- /dev/null +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + 8870e68f-fb67-4181-9714-8533f6f8db6b + LantanaGroup.Link.LinkAdmin.BFF + Linux + + + + + + + + + + + diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.http b/LinkAdmin.BFF/LinkAdmin.BFF.http new file mode 100644 index 000000000..93ba81cb5 --- /dev/null +++ b/LinkAdmin.BFF/LinkAdmin.BFF.http @@ -0,0 +1,6 @@ +@LinkAdmin.BFF_HostAddress = http://localhost:5218 + +GET {{LinkAdmin.BFF_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs new file mode 100644 index 000000000..fc1c36271 --- /dev/null +++ b/LinkAdmin.BFF/Program.cs @@ -0,0 +1,68 @@ +using Azure.Identity; +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; + +var builder = WebApplication.CreateBuilder(args); + +//load external configuration source if specified +var externalConfigurationSource = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); +if (!string.IsNullOrEmpty(externalConfigurationSource)) +{ + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("Link:AdminBFF*", LabelFilter.Null) + // Override with any configuration values specific to current hosting env + .Select("Link:AdminBFF*", builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } +} + + + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + + + +//app.MapGet("/weatherforecast", () => +//{ +// var forecast = Enumerable.Range(1, 5).Select(index => +// new WeatherForecast +// ( +// DateOnly.FromDateTime(DateTime.Now.AddDays(index)), +// Random.Shared.Next(-20, 55), +// summaries[Random.Shared.Next(summaries.Length)] +// )) +// .ToArray(); +// return forecast; +//}) +//.WithName("GetWeatherForecast") +//.WithOpenApi(); + +app.Run(); + diff --git a/LinkAdmin.BFF/Properties/launchSettings.json b/LinkAdmin.BFF/Properties/launchSettings.json new file mode 100644 index 000000000..d149e774c --- /dev/null +++ b/LinkAdmin.BFF/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5218" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7004;http://localhost:5218" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3744", + "sslPort": 44304 + } + } +} \ No newline at end of file diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs new file mode 100644 index 000000000..b5a769444 --- /dev/null +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -0,0 +1,22 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Settings +{ + public class LinkAdminConstants + { + public static class AppSettingsSectionNames + { + public const string ExternalConfigurationSource = "Link:AdminBFF:ExternalConfigurationSource"; + public const string ServiceInformation = "Link:AdminBFF:ServiceInformation"; + public const string IdentityProvider = "Link:AdminBFF:IdentityProviderConfig"; + public const string Telemetry = "Link:AdminBFF:TelemetryConfig"; + public const string Serilog = "Link:AdminBFF:Logging:Serilog"; + public const string EnableSwagger = "Link:AdminBFF:EnableSwagger"; + } + + public static class AuditLoggingIds + { + public const int RequestRecieved = 1000; + public const int RequestRecievedWarning = 1001; + public const int RequestRecievedException = 1002; + } + } +} diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json new file mode 100644 index 000000000..37e60c967 --- /dev/null +++ b/LinkAdmin.BFF/appsettings.json @@ -0,0 +1,52 @@ +{ + "Link:AdminBFF:ExternalConfigurationSource": "", + "Link:AdminBFF:ServiceInformation": { + "Name": "Link Admin BFF", + "Version": "0.1.0" + }, + "Link:AdminBFF:EnableSwagger": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "System": "Warning" + } + }, + "Link:AdminBFF:TelemetryConfig": { + "EnableRuntimeInstrumentation": false, + "TraceExporterEndpoint": "", + "MetricsEndpoint": "", + "TelemetryCollectorEndpoint": "http://localhost:55690" + }, + "Link:AdminBFF:Logging:Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Grafana.Loki" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" }, + { + "Name": "GrafanaLoki", + "Args": { + "uri": "http://localhost:3100", + "labels": [ + { + "key": "app", + "value": "Link-BoTW" + }, + { + "key": "component", + "value": "AdminBFF" + } + ], + "propertiesAsLabels": [ "app", "component" ] + } + } + ] + }, + "AllowedHosts": "*" +} diff --git a/LinkNet.sln b/LinkNet.sln index dc374e445..b82ef265d 100644 --- a/LinkNet.sln +++ b/LinkNet.sln @@ -66,6 +66,10 @@ Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "DemoApp", "DemoApp\DemoApp. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NormalizationTests", "NormalizationTests\NormalizationTests.csproj", "{538B399B-72CF-4160-A385-FCF0BAEC8FA4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BFF", "BFF", "{37C302AB-8C98-4CC8-828E-0647AF098586}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkAdmin.BFF", "LinkAdmin.BFF\LinkAdmin.BFF.csproj", "{C4070261-D248-4CA5-A532-6AAA71AACC35}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -182,6 +186,10 @@ Global {538B399B-72CF-4160-A385-FCF0BAEC8FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {538B399B-72CF-4160-A385-FCF0BAEC8FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {538B399B-72CF-4160-A385-FCF0BAEC8FA4}.Release|Any CPU.Build.0 = Release|Any CPU + {C4070261-D248-4CA5-A532-6AAA71AACC35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4070261-D248-4CA5-A532-6AAA71AACC35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4070261-D248-4CA5-A532-6AAA71AACC35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4070261-D248-4CA5-A532-6AAA71AACC35}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -189,6 +197,7 @@ Global GlobalSection(NestedProjects) = preSolution {FF846B98-2CB4-4D55-9EAE-0C62EBA3A2BC} = {C55EDC0D-9000-468C-B571-CB13AFE6AE41} {AD77CBE4-9EEA-47F2-AD4A-F89644EC07A5} = {C55EDC0D-9000-468C-B571-CB13AFE6AE41} + {C4070261-D248-4CA5-A532-6AAA71AACC35} = {37C302AB-8C98-4CC8-828E-0647AF098586} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E56BCCFA-9A01-455E-ABCF-6C044786FF55} From c84067205a62c6423087ff57e6359baecc053c3d Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 13:06:29 -0400 Subject: [PATCH 04/79] Add infrastructure --- .../Application/Models/ServiceInformation.cs | 8 +++++ .../Infrastructure/ServiceActivitySource.cs | 19 +++++++++++ LinkAdmin.BFF/Program.cs | 34 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 LinkAdmin.BFF/Application/Models/ServiceInformation.cs create mode 100644 LinkAdmin.BFF/Infrastructure/ServiceActivitySource.cs diff --git a/LinkAdmin.BFF/Application/Models/ServiceInformation.cs b/LinkAdmin.BFF/Application/Models/ServiceInformation.cs new file mode 100644 index 000000000..a5a82877b --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/ServiceInformation.cs @@ -0,0 +1,8 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models +{ + public class ServiceInformation + { + public string Name { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/ServiceActivitySource.cs b/LinkAdmin.BFF/Infrastructure/ServiceActivitySource.cs new file mode 100644 index 000000000..432e1b0fe --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/ServiceActivitySource.cs @@ -0,0 +1,19 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using System.Diagnostics; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure +{ + public static class ServiceActivitySource + { + private static string _version = string.Empty; + public static string ServiceName = "Link Admin BFF"; + public static ActivitySource Instance { get; private set; } = new ActivitySource(ServiceName, _version); + + public static void Initialize(ServiceInformation serviceInfo) + { + ServiceName = serviceInfo.Name; + _version = serviceInfo.Version; + Instance = new ActivitySource(ServiceName, _version); + } + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index fc1c36271..65ee40196 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,6 +1,9 @@ using Azure.Identity; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using System.Diagnostics; var builder = WebApplication.CreateBuilder(args); @@ -29,7 +32,38 @@ } } +var serviceInformation = builder.Configuration.GetRequiredSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); +if (serviceInformation != null) +{ + ServiceActivitySource.Initialize(serviceInformation); +} +else +{ + throw new NullReferenceException("Service Information was null."); +} + +//Add problem details +builder.Services.AddProblemDetails(options => { + options.CustomizeProblemDetails = ctx => + { + ctx.ProblemDetails.Detail = "An error occured in our API. Please use the trace id when requesting assistence."; + if (!ctx.ProblemDetails.Extensions.ContainsKey("traceId")) + { + string? traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; + ctx.ProblemDetails.Extensions.Add(new KeyValuePair("traceId", traceId)); + } + + if (builder.Environment.IsDevelopment()) + { + ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); + } + else + { + ctx.ProblemDetails.Extensions.Remove("exception"); + } + }; +}); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle From a958e19af61f89b56feab89c13cddb980eaa58c4 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 13:24:27 -0400 Subject: [PATCH 05/79] Add open telemetry service extension --- .../Application/Models/TelemetryConfig.cs | 12 ++++ .../Extensions/TelemetryServiceExtension.cs | 60 +++++++++++++++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 8 +++ 3 files changed, 80 insertions(+) create mode 100644 LinkAdmin.BFF/Application/Models/TelemetryConfig.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs diff --git a/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs b/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs new file mode 100644 index 000000000..57bf2952a --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs @@ -0,0 +1,12 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models +{ + public class TelemetryConfig + { + public bool EnableTracing { get; set; } = true; + public bool EnableMetrics { get; set; } = true; + public bool EnableRuntimeInstrumentation { get; set; } = false; + public string TraceExporterEndpoint { get; set; } = string.Empty; + public string MetricsEndpoint { get; set; } = string.Empty; + public string TelemetryCollectorEndpoint { get; set; } = string.Empty; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs new file mode 100644 index 000000000..39f781af7 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs @@ -0,0 +1,60 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class TelemetryServiceExtension + { + public static IServiceCollection AddOpenTelemetryService(this IServiceCollection services, TelemetryConfig telemetryConfig, IWebHostEnvironment env) + { + + var otel = services.AddOpenTelemetry(); + + //configure OpenTelemetry resources with application name + otel.ConfigureResource(resource => resource + .AddService( + serviceName: ServiceActivitySource.Instance.Name, + serviceVersion: ServiceActivitySource.Instance.Version + )); + + otel.WithTracing(tracerProviderBuilder => + tracerProviderBuilder + .AddSource(ServiceActivitySource.Instance.Name) + .AddAspNetCoreInstrumentation(options => + { + options.Filter = (httpContext) => httpContext.Request.Path != "/health"; //do not capture traces for the health check endpoint + }) + .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryConfig.TelemetryCollectorEndpoint); })); + + otel.WithMetrics(metricsProviderBuilder => + metricsProviderBuilder + .AddAspNetCoreInstrumentation() + .AddProcessInstrumentation() + .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryConfig.TelemetryCollectorEndpoint); })); + + if (telemetryConfig.EnableRuntimeInstrumentation) + { + otel.WithMetrics(metricsProviderBuilder => + metricsProviderBuilder + .AddRuntimeInstrumentation()); + } + + if (env.IsDevelopment()) + { + otel.WithTracing(tracerProviderBuilder => + tracerProviderBuilder + .AddConsoleExporter()); + + //metrics are very verbose, only enable console exporter if you really want to see metric details + //otel.WithMetrics(metricsProviderBuilder => + // metricsProviderBuilder + // .AddConsoleExporter()); + } + + return services; + } + + } +} diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index bb4d07364..f16c52b09 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -15,6 +15,14 @@ + + + + + + + + From 8f88a66222ff5fad1304d861c4e516cbcab2f748 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 13:28:32 -0400 Subject: [PATCH 06/79] Add cors service extension --- .../Extensions/CoresServiceExtension.cs | 21 +++++++++++++++++++ LinkAdmin.BFF/Program.cs | 11 ++++++++++ 2 files changed, 32 insertions(+) create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs new file mode 100644 index 000000000..3bd4db0e3 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs @@ -0,0 +1,21 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class CoresServiceExtension + { + public static IServiceCollection AddCorsService(this IServiceCollection services, IWebHostEnvironment env) + { + //TODO: Use env variable to control strictness of CORS policy + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowCredentials() + .SetIsOriginAllowed((host) => true) //lock this down, allows all atm + .AllowAnyHeader()); + }); + + return services; + } + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 65ee40196..146a53a02 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,6 +1,7 @@ using Azure.Identity; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using System.Diagnostics; @@ -42,6 +43,16 @@ throw new NullReferenceException("Service Information was null."); } +//configure CORS +builder.Services.AddCorsService(builder.Environment); + +// Add open telemetry +var telemetryConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Telemetry).Get(); +if (telemetryConfig != null) +{ + builder.Services.AddOpenTelemetryService(telemetryConfig, builder.Environment); +} + //Add problem details builder.Services.AddProblemDetails(options => { options.CustomizeProblemDetails = ctx => From 8b28dcd748c9bd8ad15cad2f5472340d7bf375bb Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 13:36:41 -0400 Subject: [PATCH 07/79] Add serilog --- LinkAdmin.BFF/LinkAdmin.BFF.csproj | 7 +++++++ LinkAdmin.BFF/Program.cs | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index f16c52b09..fef87c060 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -23,6 +23,13 @@ + + + + + + + diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 146a53a02..56fdcad7b 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -4,7 +4,12 @@ using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Serilog; +using Serilog.Enrichers.Span; +using Serilog.Exceptions; +using Serilog.Settings.Configuration; using System.Diagnostics; +using System.Reflection; var builder = WebApplication.CreateBuilder(args); @@ -79,7 +84,27 @@ // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); +}); + +// Logging using Serilog +builder.Logging.AddSerilog(); +var loggerOptions = new ConfigurationReaderOptions { SectionName = LinkAdminConstants.AppSettingsSectionNames.Serilog }; +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration, loggerOptions) + .Filter.ByExcluding("RequestPath like '/health%'") + .Filter.ByExcluding("RequestPath like '/swagger%'") + .Enrich.WithExceptionDetails() + .Enrich.FromLogContext() + .Enrich.WithSpan() + .Enrich.With() + .CreateLogger(); + +//Serilog.Debugging.SelfLog.Enable(Console.Error); var app = builder.Build(); From 4d7b7f1232ca6969723fcc1d5342a7b2b8386dc5 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 13:44:19 -0400 Subject: [PATCH 08/79] Re-organize and setup middleware --- LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + LinkAdmin.BFF/Program.cs | 227 ++++++++++++++++------------- 2 files changed, 128 insertions(+), 100 deletions(-) diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index fef87c060..7539d7950 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -11,6 +11,7 @@ + diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 56fdcad7b..2269b1133 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,8 +1,10 @@ using Azure.Identity; +using HealthChecks.UI.Client; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Serilog; using Serilog.Enrichers.Span; @@ -13,126 +15,151 @@ var builder = WebApplication.CreateBuilder(args); -//load external configuration source if specified -var externalConfigurationSource = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); -if (!string.IsNullOrEmpty(externalConfigurationSource)) +RegisterServices(builder); +var app = builder.Build(); +SetupMiddleware(app); + +app.Run(); + +#region Register Services +static void RegisterServices(WebApplicationBuilder builder) { - switch (externalConfigurationSource) + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + if (!string.IsNullOrEmpty(externalConfigurationSource)) { - case ("AzureAppConfiguration"): - builder.Configuration.AddAzureAppConfiguration(options => - { - options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) - // Load configuration values with no label - .Select("Link:AdminBFF*", LabelFilter.Null) - // Override with any configuration values specific to current hosting env - .Select("Link:AdminBFF*", builder.Environment.EnvironmentName); - - options.ConfigureKeyVault(kv => + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => { - kv.SetCredential(new DefaultAzureCredential()); + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("Link:AdminBFF*", LabelFilter.Null) + // Override with any configuration values specific to current hosting env + .Select("Link:AdminBFF*", builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + }); + break; + } + } - }); - break; + var serviceInformation = builder.Configuration.GetRequiredSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); + if (serviceInformation != null) + { + ServiceActivitySource.Initialize(serviceInformation); + } + else + { + throw new NullReferenceException("Service Information was null."); } -} -var serviceInformation = builder.Configuration.GetRequiredSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); -if (serviceInformation != null) -{ - ServiceActivitySource.Initialize(serviceInformation); -} -else -{ - throw new NullReferenceException("Service Information was null."); -} + //configure CORS + builder.Services.AddCorsService(builder.Environment); -//configure CORS -builder.Services.AddCorsService(builder.Environment); + //Add problem details + builder.Services.AddProblemDetails(options => { + options.CustomizeProblemDetails = ctx => + { + ctx.ProblemDetails.Detail = "An error occured in our API. Please use the trace id when requesting assistence."; + if (!ctx.ProblemDetails.Extensions.ContainsKey("traceId")) + { + string? traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; + ctx.ProblemDetails.Extensions.Add(new KeyValuePair("traceId", traceId)); + } -// Add open telemetry -var telemetryConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Telemetry).Get(); -if (telemetryConfig != null) -{ - builder.Services.AddOpenTelemetryService(telemetryConfig, builder.Environment); -} + if (builder.Environment.IsDevelopment()) + { + ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); + } + else + { + ctx.ProblemDetails.Extensions.Remove("exception"); + } + + }; + }); + + //Add health checks + builder.Services.AddHealthChecks(); -//Add problem details -builder.Services.AddProblemDetails(options => { - options.CustomizeProblemDetails = ctx => + // Add services to the container. + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(c => { - ctx.ProblemDetails.Detail = "An error occured in our API. Please use the trace id when requesting assistence."; - if (!ctx.ProblemDetails.Extensions.ContainsKey("traceId")) - { - string? traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; - ctx.ProblemDetails.Extensions.Add(new KeyValuePair("traceId", traceId)); - } + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + }); + + // Logging using Serilog + builder.Logging.AddSerilog(); + var loggerOptions = new ConfigurationReaderOptions { SectionName = LinkAdminConstants.AppSettingsSectionNames.Serilog }; + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration, loggerOptions) + .Filter.ByExcluding("RequestPath like '/health%'") + .Filter.ByExcluding("RequestPath like '/swagger%'") + .Enrich.WithExceptionDetails() + .Enrich.FromLogContext() + .Enrich.WithSpan() + .Enrich.With() + .CreateLogger(); + + //Serilog.Debugging.SelfLog.Enable(Console.Error); + + // Add open telemetry + var telemetryConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Telemetry).Get(); + if (telemetryConfig != null) + { + builder.Services.AddOpenTelemetryService(telemetryConfig, builder.Environment); + } +} - if (builder.Environment.IsDevelopment()) - { - ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); - } - else - { - ctx.ProblemDetails.Extensions.Remove("exception"); - } +#endregion - }; -}); -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => +#region Setup Middleware +static void SetupMiddleware(WebApplication app) { - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - c.IncludeXmlComments(xmlPath); -}); - -// Logging using Serilog -builder.Logging.AddSerilog(); -var loggerOptions = new ConfigurationReaderOptions { SectionName = LinkAdminConstants.AppSettingsSectionNames.Serilog }; -Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(builder.Configuration, loggerOptions) - .Filter.ByExcluding("RequestPath like '/health%'") - .Filter.ByExcluding("RequestPath like '/swagger%'") - .Enrich.WithExceptionDetails() - .Enrich.FromLogContext() - .Enrich.WithSpan() - .Enrich.With() - .CreateLogger(); - -//Serilog.Debugging.SelfLog.Enable(Console.Error); + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler(); + } -var app = builder.Build(); + //configure swagger + if (app.Configuration.GetValue(LinkAdminConstants.AppSettingsSectionNames.EnableSwagger)) + { + var serviceInformation = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); + app.UseSwagger(); + app.UseSwaggerUI(opts => opts.SwaggerEndpoint("/swagger/v1/swagger.json", serviceInformation != null ? $"{serviceInformation.Name} - {serviceInformation.Version}" : "Link Admin API")); + } -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} + app.UseHttpsRedirection(); -app.UseHttpsRedirection(); + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseAuthentication(); + //app.UseMiddleware(); + app.UseAuthorization(); + //map health check middleware + app.MapHealthChecks("/health", new HealthCheckOptions + { + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); +} +#endregion -//app.MapGet("/weatherforecast", () => -//{ -// var forecast = Enumerable.Range(1, 5).Select(index => -// new WeatherForecast -// ( -// DateOnly.FromDateTime(DateTime.Now.AddDays(index)), -// Random.Shared.Next(-20, 55), -// summaries[Random.Shared.Next(summaries.Length)] -// )) -// .ToArray(); -// return forecast; -//}) -//.WithName("GetWeatherForecast") -//.WithOpenApi(); -app.Run(); From 4ed0cf56385f8f75dc952941c8ec3366b08bd658 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 21:08:01 -0400 Subject: [PATCH 09/79] Updated telemetry and cors extension services --- .../Extensions/CoresServiceExtension.cs | 21 -------- .../Extensions/CorsServiceExtension.cs | 54 +++++++++++++++++++ .../Extensions/TelemetryServiceExtension.cs | 22 +++++--- LinkAdmin.BFF/Program.cs | 21 ++++++-- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 12 ++--- LinkAdmin.BFF/appsettings.Development.json | 48 ++++++++++++++++- LinkAdmin.BFF/appsettings.json | 19 +++++-- 7 files changed, 150 insertions(+), 47 deletions(-) delete mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs deleted file mode 100644 index 3bd4db0e3..000000000 --- a/LinkAdmin.BFF/Infrastructure/Extensions/CoresServiceExtension.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions -{ - public static class CoresServiceExtension - { - public static IServiceCollection AddCorsService(this IServiceCollection services, IWebHostEnvironment env) - { - //TODO: Use env variable to control strictness of CORS policy - services.AddCors(options => - { - options.AddPolicy("CorsPolicy", - builder => builder - .AllowAnyMethod() - .AllowCredentials() - .SetIsOriginAllowed((host) => true) //lock this down, allows all atm - .AllowAnyHeader()); - }); - - return services; - } - } -} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs new file mode 100644 index 000000000..527e79492 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs @@ -0,0 +1,54 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class CorsServiceExtension + { + public static IServiceCollection AddCorsService(this IServiceCollection services, Action? options = null) + { + var corsServiceOptions = new CorsServiceOptions(); + options?.Invoke(corsServiceOptions); + + services.AddCors(options => + { + if (corsServiceOptions.Environment.IsDevelopment()) + { + options.AddPolicy("DevCorsPolicy", + builder => builder + .WithMethods(corsServiceOptions.AllowedMethods) + .SetIsOriginAllowed((host) => true) + .AllowAnyHeader() + .WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders) + .AllowAnyMethod() + .AllowCredentials() + .SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)) + ); + } + else + { + options.AddPolicy(corsServiceOptions.CorsPolicyName, + builder => builder + .WithMethods(corsServiceOptions.AllowedMethods) + .WithOrigins(corsServiceOptions.AllowedOrigins) + .WithHeaders(corsServiceOptions.AllowedHeaders) + .WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders) + .AllowCredentials() + .SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)) + ); + } + }); + + return services; + } + } + + public class CorsServiceOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public bool AllowCredentials { get; set; } = true; + public string CorsPolicyName { get; set; } = "CorsPolicy"; + public string[] AllowedHeaders { get; set; } = new string[] { "Authorization, Content-Type, Accept, Origin, User-Agent, X-Requested-With" }; + public string[] AllowedExposedHeaders { get; set; } = new string[] { "X-Pagination" } ; + public string[] AllowedMethods { get; set; } = new string[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; + public string[] AllowedOrigins { get; set; } = new string[] { "https://localhost:7007", "http://localhost:5005" }; + public int MaxAge { get; set; } = 3600; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs index 39f781af7..667808d93 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs @@ -7,8 +7,10 @@ namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions { public static class TelemetryServiceExtension { - public static IServiceCollection AddOpenTelemetryService(this IServiceCollection services, TelemetryConfig telemetryConfig, IWebHostEnvironment env) - { + public static IServiceCollection AddOpenTelemetryService(this IServiceCollection services, Action options) + { + var telemetryServiceOptions = new TelemetryServiceOptions(); + options.Invoke(telemetryServiceOptions); var otel = services.AddOpenTelemetry(); @@ -26,22 +28,22 @@ public static IServiceCollection AddOpenTelemetryService(this IServiceCollection { options.Filter = (httpContext) => httpContext.Request.Path != "/health"; //do not capture traces for the health check endpoint }) - .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryConfig.TelemetryCollectorEndpoint); })); + .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryServiceOptions.TelemetryCollectorEndpoint); })); otel.WithMetrics(metricsProviderBuilder => metricsProviderBuilder .AddAspNetCoreInstrumentation() .AddProcessInstrumentation() - .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryConfig.TelemetryCollectorEndpoint); })); + .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryServiceOptions.TelemetryCollectorEndpoint); })); - if (telemetryConfig.EnableRuntimeInstrumentation) + if (telemetryServiceOptions.EnableRuntimeInstrumentation) { otel.WithMetrics(metricsProviderBuilder => metricsProviderBuilder .AddRuntimeInstrumentation()); } - if (env.IsDevelopment()) + if (telemetryServiceOptions.Environment.IsDevelopment()) { otel.WithTracing(tracerProviderBuilder => tracerProviderBuilder @@ -54,7 +56,11 @@ public static IServiceCollection AddOpenTelemetryService(this IServiceCollection } return services; - } - + } + } + + public class TelemetryServiceOptions : TelemetryConfig + { + public IWebHostEnvironment Environment { get; set; } = null!; } } diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 2269b1133..bfe0d93eb 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -57,10 +57,7 @@ static void RegisterServices(WebApplicationBuilder builder) else { throw new NullReferenceException("Service Information was null."); - } - - //configure CORS - builder.Services.AddCorsService(builder.Environment); + } //Add problem details builder.Services.AddProblemDetails(options => { @@ -88,6 +85,16 @@ static void RegisterServices(WebApplicationBuilder builder) //Add health checks builder.Services.AddHealthChecks(); + //configure CORS + builder.Services.AddCorsService(options => { + options.Environment = builder.Environment; + options.CorsPolicyName = ""; + options.AllowedHeaders = new[] { "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With" }; + options.AllowedExposedHeaders = new[] { "X-Pagination" }; + options.AllowedMethods = new string[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; + options.AllowedOrigins = new string[] { "https://localhost:7007", "http://localhost:5005" }; + }); + // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -117,7 +124,11 @@ static void RegisterServices(WebApplicationBuilder builder) var telemetryConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Telemetry).Get(); if (telemetryConfig != null) { - builder.Services.AddOpenTelemetryService(telemetryConfig, builder.Environment); + builder.Services.AddOpenTelemetryService(options => { + options.Environment = builder.Environment; + options.TelemetryCollectorEndpoint = telemetryConfig.TelemetryCollectorEndpoint; + options.EnableRuntimeInstrumentation = telemetryConfig.EnableRuntimeInstrumentation; + }); } } diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index b5a769444..191ee05fe 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -4,12 +4,12 @@ public class LinkAdminConstants { public static class AppSettingsSectionNames { - public const string ExternalConfigurationSource = "Link:AdminBFF:ExternalConfigurationSource"; - public const string ServiceInformation = "Link:AdminBFF:ServiceInformation"; - public const string IdentityProvider = "Link:AdminBFF:IdentityProviderConfig"; - public const string Telemetry = "Link:AdminBFF:TelemetryConfig"; - public const string Serilog = "Link:AdminBFF:Logging:Serilog"; - public const string EnableSwagger = "Link:AdminBFF:EnableSwagger"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; + public const string ServiceInformation = "ServiceInformation"; + public const string IdentityProvider = "IdentityProviderConfig"; + public const string Telemetry = "TelemetryConfig"; + public const string Serilog = "Logging:Serilog"; + public const string EnableSwagger = "EnableSwagger"; } public static class AuditLoggingIds diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index 0c208ae91..5c83a32c2 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -1,8 +1,52 @@ { + "ExternalConfigurationSource": "", + "ServiceInformation": { + "Name": "Link Admin BFF", + "Version": "0.1.0" + }, + "EnableSwagger": true, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft": "Warning", + "System": "Warning" } - } + }, + "TelemetryConfig": { + "EnableRuntimeInstrumentation": false, + "TraceExporterEndpoint": "", + "MetricsEndpoint": "", + "TelemetryCollectorEndpoint": "http://localhost:55690" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Grafana.Loki" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" }, + { + "Name": "GrafanaLoki", + "Args": { + "uri": "http://localhost:3100", + "labels": [ + { + "key": "app", + "value": "Link-BoTW" + }, + { + "key": "component", + "value": "AdminBFF" + } + ], + "propertiesAsLabels": [ "app", "component" ] + } + } + ] + }, + "AllowedHosts": "*" } diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 37e60c967..6a259dab6 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -1,10 +1,19 @@ { - "Link:AdminBFF:ExternalConfigurationSource": "", - "Link:AdminBFF:ServiceInformation": { + "ExternalConfigurationSource": "", + "ServiceInformation": { "Name": "Link Admin BFF", "Version": "0.1.0" }, - "Link:AdminBFF:EnableSwagger": true, + "CORS": { + "PolicyName": "LinkAdminCorsPolicy", + "AllowedOrigins": [ "https://localhost:7007", "http://localhost:5005" ], + "AllowedMethods": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ], + "AllowedHeaders": [ "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With" ], + "AllowedExposedHeaders": [ "X-Pagination" ], + "AllowCredentials": true, + "MaxAge": 3600 + } + "EnableSwagger": true, "Logging": { "LogLevel": { "Default": "Information", @@ -12,13 +21,13 @@ "System": "Warning" } }, - "Link:AdminBFF:TelemetryConfig": { + "TelemetryConfig": { "EnableRuntimeInstrumentation": false, "TraceExporterEndpoint": "", "MetricsEndpoint": "", "TelemetryCollectorEndpoint": "http://localhost:55690" }, - "Link:AdminBFF:Logging:Serilog": { + "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Grafana.Loki" ], "MinimumLevel": { "Default": "Information", From 0fa7699fed1ce881735da598bed3399fc4318240 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 18 Mar 2024 21:09:47 -0400 Subject: [PATCH 10/79] updated SetPreflightMaxAge to 600 (10 minutes) --- LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs index 527e79492..e78d153ca 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs @@ -49,6 +49,6 @@ public class CorsServiceOptions public string[] AllowedExposedHeaders { get; set; } = new string[] { "X-Pagination" } ; public string[] AllowedMethods { get; set; } = new string[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; public string[] AllowedOrigins { get; set; } = new string[] { "https://localhost:7007", "http://localhost:5005" }; - public int MaxAge { get; set; } = 3600; + public int MaxAge { get; set; } = 600; } } From 2ddbcfcad102cfb87e747a393524461a37e62c9c Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 09:58:19 -0400 Subject: [PATCH 11/79] Updated CORS configuration, added xml documentation --- .../Application/Models/CorsConfig.cs | 63 ++++++++++++++ .../Application/Models/ServiceInformation.cs | 10 +++ .../Application/Models/TelemetryConfig.cs | 26 ++++++ .../Extensions/CorsServiceExtension.cs | 84 ++++++++++++------- LinkAdmin.BFF/LinkAdmin.BFF.csproj | 5 ++ LinkAdmin.BFF/Program.cs | 34 +++++--- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 1 + LinkAdmin.BFF/appsettings.json | 4 +- 8 files changed, 182 insertions(+), 45 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Models/CorsConfig.cs diff --git a/LinkAdmin.BFF/Application/Models/CorsConfig.cs b/LinkAdmin.BFF/Application/Models/CorsConfig.cs new file mode 100644 index 000000000..4c85aae9a --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/CorsConfig.cs @@ -0,0 +1,63 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models +{ + /// + /// CORS configuration options + /// + public class CorsConfig + { + /// + /// Default CORS policy name + /// + public const string DefaultCorsPolicyName = "DefaultCorsPolicy"; + + /// + /// Default allowed headers + /// + public string[] DefaultAllowedHeaders { get; } = ["Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With"]; + + /// + /// Default allowed methods + /// + public string[] DefaultAllowedMethods { get; } = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]; + + /// + /// Default allowed exposed headers + /// + public string[] DefaultAllowedExposedHeaders { get; } = ["X-Pagination"]; + + /// + /// Whether to allow credentials + /// + public bool AllowCredentials { get; set; } = false; + + /// + /// The name of the CORS policy + /// + public string? CorsPolicyName { get; set; } + + /// + /// The allowed headers + /// + public string[]? AllowedHeaders { get; set; } + + /// + /// The allowed exposed headers + /// + public string[]? AllowedExposedHeaders { get; set; } + + /// + /// The allowed HTTP methods + /// + public string[]? AllowedMethods { get; set; } + + /// + /// The allowed origins + /// + public string[]? AllowedOrigins { get; set; } + + /// + /// The maximum age of the preflight request + /// + public int MaxAge { get; set; } = 600; + } +} diff --git a/LinkAdmin.BFF/Application/Models/ServiceInformation.cs b/LinkAdmin.BFF/Application/Models/ServiceInformation.cs index a5a82877b..81d7413d0 100644 --- a/LinkAdmin.BFF/Application/Models/ServiceInformation.cs +++ b/LinkAdmin.BFF/Application/Models/ServiceInformation.cs @@ -1,8 +1,18 @@ namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models { + /// + /// Information about the API service + /// public class ServiceInformation { + /// + /// The name of the service + /// public string Name { get; set; } = string.Empty; + + /// + /// The version of the service + /// public string Version { get; set; } = string.Empty; } } diff --git a/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs b/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs index 57bf2952a..8f9354b59 100644 --- a/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs +++ b/LinkAdmin.BFF/Application/Models/TelemetryConfig.cs @@ -1,12 +1,38 @@ namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models { + /// + /// Open Telemetry configuration options + /// public class TelemetryConfig { + /// + /// Whether to enable tracing + /// public bool EnableTracing { get; set; } = true; + + /// + /// Whether to enable metrics + /// public bool EnableMetrics { get; set; } = true; + + /// + /// Whether to enable runtime instrumentation + /// public bool EnableRuntimeInstrumentation { get; set; } = false; + + /// + /// The endpoint for the trace exporter + /// public string TraceExporterEndpoint { get; set; } = string.Empty; + + /// + /// The endpoint for the metrics exporter + /// public string MetricsEndpoint { get; set; } = string.Empty; + + /// + /// The endpoint for the telemetry collector + /// public string TelemetryCollectorEndpoint { get; set; } = string.Empty; } } diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs index e78d153ca..ecbb8cc52 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs @@ -1,4 +1,7 @@ -namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions { public static class CorsServiceExtension { @@ -8,47 +11,64 @@ public static IServiceCollection AddCorsService(this IServiceCollection services options?.Invoke(corsServiceOptions); services.AddCors(options => - { - if (corsServiceOptions.Environment.IsDevelopment()) + { + CorsPolicyBuilder cpb = new(); + + if(corsServiceOptions.AllowedOrigins?.Length > 0) { - options.AddPolicy("DevCorsPolicy", - builder => builder - .WithMethods(corsServiceOptions.AllowedMethods) - .SetIsOriginAllowed((host) => true) - .AllowAnyHeader() - .WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders) - .AllowAnyMethod() - .AllowCredentials() - .SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)) - ); + cpb.WithOrigins(corsServiceOptions.AllowedOrigins); + + if(corsServiceOptions.AllowCredentials) + { + cpb.AllowCredentials(); + cpb.WithHeaders(corsServiceOptions.AllowedHeaders is not null ? corsServiceOptions.AllowedHeaders : corsServiceOptions.DefaultAllowedHeaders); + } } else - { - options.AddPolicy(corsServiceOptions.CorsPolicyName, - builder => builder - .WithMethods(corsServiceOptions.AllowedMethods) - .WithOrigins(corsServiceOptions.AllowedOrigins) - .WithHeaders(corsServiceOptions.AllowedHeaders) - .WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders) - .AllowCredentials() - .SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)) - ); + { + cpb.SetIsOriginAllowed((Host) => true); + + if (corsServiceOptions.AllowedHeaders?.Length > 0) + { + cpb.WithHeaders(corsServiceOptions.AllowedHeaders); + } + else + { + cpb.AllowAnyHeader(); + } + } + + cpb.WithMethods(corsServiceOptions.AllowedMethods is not null ? corsServiceOptions.AllowedMethods : corsServiceOptions.DefaultAllowedMethods); + cpb.WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders is not null ? corsServiceOptions.AllowedExposedHeaders : corsServiceOptions.DefaultAllowedExposedHeaders); + cpb.SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)); + + options.AddPolicy(corsServiceOptions?.CorsPolicyName ?? CorsConfig.DefaultCorsPolicyName, cpb.Build()); + + //add health check endpoint to cors policy + options.AddPolicy("HealthCheckPolicy", policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); + + //add swagger endpoint to cors policy + options.AddPolicy("SwaggerPolicy", policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); + }); return services; } } - public class CorsServiceOptions + public class CorsServiceOptions : CorsConfig { - public IWebHostEnvironment Environment { get; set; } = null!; - public bool AllowCredentials { get; set; } = true; - public string CorsPolicyName { get; set; } = "CorsPolicy"; - public string[] AllowedHeaders { get; set; } = new string[] { "Authorization, Content-Type, Accept, Origin, User-Agent, X-Requested-With" }; - public string[] AllowedExposedHeaders { get; set; } = new string[] { "X-Pagination" } ; - public string[] AllowedMethods { get; set; } = new string[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; - public string[] AllowedOrigins { get; set; } = new string[] { "https://localhost:7007", "http://localhost:5005" }; - public int MaxAge { get; set; } = 600; + public IWebHostEnvironment Environment { get; set; } = null!; } } diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index 7539d7950..1f078d18a 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -10,6 +10,11 @@ Linux + + true + $(NoWarn);1591 + + diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index bfe0d93eb..90743995a 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -4,6 +4,7 @@ using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Serilog; @@ -86,14 +87,24 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddHealthChecks(); //configure CORS - builder.Services.AddCorsService(options => { - options.Environment = builder.Environment; - options.CorsPolicyName = ""; - options.AllowedHeaders = new[] { "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With" }; - options.AllowedExposedHeaders = new[] { "X-Pagination" }; - options.AllowedMethods = new string[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; - options.AllowedOrigins = new string[] { "https://localhost:7007", "http://localhost:5005" }; - }); + var corsConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); + if(corsConfig != null) + { + builder.Services.AddCorsService(options => + { + options.Environment = builder.Environment; + options.CorsPolicyName = corsConfig.CorsPolicyName; + options.AllowedHeaders = corsConfig.AllowedHeaders; + options.AllowedExposedHeaders = corsConfig.AllowedExposedHeaders; + options.AllowedMethods = corsConfig.AllowedMethods; + options.AllowedOrigins = corsConfig.AllowedOrigins; + }); + } + else + { + throw new NullReferenceException("CORS Configuration was null."); + } + // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -158,10 +169,11 @@ static void SetupMiddleware(WebApplication app) app.UseHttpsRedirection(); app.UseRouting(); - app.UseCors("CorsPolicy"); - app.UseAuthentication(); + var corsConfig = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); + app.UseCors(corsConfig?.CorsPolicyName ?? CorsConfig.DefaultCorsPolicyName); + //app.UseAuthentication(); //app.UseMiddleware(); - app.UseAuthorization(); + //app.UseAuthorization(); //map health check middleware app.MapHealthChecks("/health", new HealthCheckOptions diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 191ee05fe..760f01286 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -7,6 +7,7 @@ public static class AppSettingsSectionNames public const string ExternalConfigurationSource = "ExternalConfigurationSource"; public const string ServiceInformation = "ServiceInformation"; public const string IdentityProvider = "IdentityProviderConfig"; + public const string CORS = "CORS"; public const string Telemetry = "TelemetryConfig"; public const string Serilog = "Logging:Serilog"; public const string EnableSwagger = "EnableSwagger"; diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 6a259dab6..297edd7f5 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -11,8 +11,8 @@ "AllowedHeaders": [ "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With" ], "AllowedExposedHeaders": [ "X-Pagination" ], "AllowCredentials": true, - "MaxAge": 3600 - } + "MaxAge": 600 + }, "EnableSwagger": true, "Logging": { "LogLevel": { From 1ca8d83c86658700af2ec50d8768b0fd1309f423 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 13:59:05 -0400 Subject: [PATCH 12/79] updated azure app config label configuration --- LinkAdmin.BFF/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 90743995a..a15d15a68 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -36,9 +36,9 @@ static void RegisterServices(WebApplicationBuilder builder) { options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) // Load configuration values with no label - .Select("Link:AdminBFF*", LabelFilter.Null) + .Select("*", LabelFilter.Null) // Override with any configuration values specific to current hosting env - .Select("Link:AdminBFF*", builder.Environment.EnvironmentName); + .Select("*", "Link:AdminBFF:" + builder.Environment.EnvironmentName); options.ConfigureKeyVault(kv => { From fa4734f98a4dbc4074a2c3eb01c8fa954babf99e Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 14:15:56 -0400 Subject: [PATCH 13/79] removed swagger policy from cors policy builder, not needed --- .../Extensions/CorsServiceExtension.cs | 11 +---------- LinkAdmin.BFF/Program.cs | 13 ++++++++----- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs index ecbb8cc52..e285e88d3 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs @@ -51,16 +51,7 @@ public static IServiceCollection AddCorsService(this IServiceCollection services policy.AllowAnyHeader(); policy.AllowAnyMethod(); policy.AllowAnyOrigin(); - }); - - //add swagger endpoint to cors policy - options.AddPolicy("SwaggerPolicy", policy => - { - policy.AllowAnyHeader(); - policy.AllowAnyMethod(); - policy.AllowAnyOrigin(); - }); - + }); }); return services; diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index a15d15a68..0e8724b86 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -156,18 +156,20 @@ static void SetupMiddleware(WebApplication app) else { app.UseExceptionHandler(); - } + } + + app.UseHttpsRedirection(); //configure swagger if (app.Configuration.GetValue(LinkAdminConstants.AppSettingsSectionNames.EnableSwagger)) { var serviceInformation = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); app.UseSwagger(); - app.UseSwaggerUI(opts => opts.SwaggerEndpoint("/swagger/v1/swagger.json", serviceInformation != null ? $"{serviceInformation.Name} - {serviceInformation.Version}" : "Link Admin API")); + app.UseSwaggerUI(opts => { + opts.SwaggerEndpoint("/swagger/v1/swagger.json", serviceInformation != null ? $"{serviceInformation.Name} - {serviceInformation.Version}" : "Link Admin API") + }); } - app.UseHttpsRedirection(); - app.UseRouting(); var corsConfig = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); app.UseCors(corsConfig?.CorsPolicyName ?? CorsConfig.DefaultCorsPolicyName); @@ -179,7 +181,8 @@ static void SetupMiddleware(WebApplication app) app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); + }).RequireCors("HealthCheckPolicy"); + } #endregion From 24d6282dba44d9e1bc67ec842ca8f8e38a3f1ada Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 14:35:50 -0400 Subject: [PATCH 14/79] Added problem details extension, added yarp NuGET package --- .../Extensions/ProblemDetailsExtension.cs | 43 +++++++++++++++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + LinkAdmin.BFF/Program.cs | 26 +++-------- LinkAdmin.BFF/appsettings.json | 3 ++ 4 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs new file mode 100644 index 000000000..0e0fe4964 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class ProblemDetailsExtension + { + public static IServiceCollection AddProblemDetailsService(this IServiceCollection services, Action? options = null) + { + var problemDetailsOptions = new ProblemDetailsOptions(); + options?.Invoke(problemDetailsOptions); + + services.AddProblemDetails(options => { + options.CustomizeProblemDetails = ctx => + { + ctx.ProblemDetails.Detail = "An error occured in our API. Please use the trace id when requesting assistence."; + if (!ctx.ProblemDetails.Extensions.ContainsKey("traceId")) + { + string? traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; + ctx.ProblemDetails.Extensions.Add(new KeyValuePair("traceId", traceId)); + } + + if (problemDetailsOptions.Environment.IsDevelopment() || problemDetailsOptions.IncludeExceptionDetails) + { + ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); + } + else + { + ctx.ProblemDetails.Extensions.Remove("exception"); + } + + }; + }); + + return services; + } + } + + public class ProblemDetailsOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public bool IncludeExceptionDetails { get; set; } = false; + } +} diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index 1f078d18a..ea9c5348f 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -37,6 +37,7 @@ + diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 0e8724b86..4a511b48c 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -61,26 +61,10 @@ static void RegisterServices(WebApplicationBuilder builder) } //Add problem details - builder.Services.AddProblemDetails(options => { - options.CustomizeProblemDetails = ctx => - { - ctx.ProblemDetails.Detail = "An error occured in our API. Please use the trace id when requesting assistence."; - if (!ctx.ProblemDetails.Extensions.ContainsKey("traceId")) - { - string? traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; - ctx.ProblemDetails.Extensions.Add(new KeyValuePair("traceId", traceId)); - } - - if (builder.Environment.IsDevelopment()) - { - ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); - } - else - { - ctx.ProblemDetails.Extensions.Remove("exception"); - } - - }; + builder.Services.AddProblemDetailsService(options => + { + options.Environment = builder.Environment; + options.IncludeExceptionDetails = builder.Configuration.GetValue("ProblemDetails:IncludeExceptionDetails"); }); //Add health checks @@ -166,7 +150,7 @@ static void SetupMiddleware(WebApplication app) var serviceInformation = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); app.UseSwagger(); app.UseSwaggerUI(opts => { - opts.SwaggerEndpoint("/swagger/v1/swagger.json", serviceInformation != null ? $"{serviceInformation.Name} - {serviceInformation.Version}" : "Link Admin API") + opts.SwaggerEndpoint("/swagger/v1/swagger.json", serviceInformation != null ? $"{serviceInformation.Name} - {serviceInformation.Version}" : "Link Admin API"); }); } diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 297edd7f5..b01d184a5 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -14,6 +14,9 @@ "MaxAge": 600 }, "EnableSwagger": true, + "ProblemDetails": { + "IncludeExceptionDetails": false + }, "Logging": { "LogLevel": { "Default": "Information", From 9254476cff60dcb9621904c61724f954ae3787ce Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 14:52:11 -0400 Subject: [PATCH 15/79] Updated cors config, added minimal api user endpoint with temporary logic. --- LinkAdmin.BFF/Application/Models/CorsConfig.cs | 2 +- .../Extensions/CorsServiceExtension.cs | 2 +- LinkAdmin.BFF/Program.cs | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/LinkAdmin.BFF/Application/Models/CorsConfig.cs b/LinkAdmin.BFF/Application/Models/CorsConfig.cs index 4c85aae9a..8cfb8ecf5 100644 --- a/LinkAdmin.BFF/Application/Models/CorsConfig.cs +++ b/LinkAdmin.BFF/Application/Models/CorsConfig.cs @@ -33,7 +33,7 @@ public class CorsConfig /// /// The name of the CORS policy /// - public string? CorsPolicyName { get; set; } + public string? PolicyName { get; set; } /// /// The allowed headers diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs index e285e88d3..a3cdc4477 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/CorsServiceExtension.cs @@ -43,7 +43,7 @@ public static IServiceCollection AddCorsService(this IServiceCollection services cpb.WithExposedHeaders(corsServiceOptions.AllowedExposedHeaders is not null ? corsServiceOptions.AllowedExposedHeaders : corsServiceOptions.DefaultAllowedExposedHeaders); cpb.SetPreflightMaxAge(TimeSpan.FromSeconds(corsServiceOptions.MaxAge)); - options.AddPolicy(corsServiceOptions?.CorsPolicyName ?? CorsConfig.DefaultCorsPolicyName, cpb.Build()); + options.AddPolicy(corsServiceOptions?.PolicyName ?? CorsConfig.DefaultCorsPolicyName, cpb.Build()); //add health check endpoint to cors policy options.AddPolicy("HealthCheckPolicy", policy => diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 4a511b48c..d6477479e 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -6,6 +6,7 @@ using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Serilog; using Serilog.Enrichers.Span; @@ -13,6 +14,7 @@ using Serilog.Settings.Configuration; using System.Diagnostics; using System.Reflection; +using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); @@ -77,7 +79,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddCorsService(options => { options.Environment = builder.Environment; - options.CorsPolicyName = corsConfig.CorsPolicyName; + options.PolicyName = corsConfig.PolicyName; options.AllowedHeaders = corsConfig.AllowedHeaders; options.AllowedExposedHeaders = corsConfig.AllowedExposedHeaders; options.AllowedMethods = corsConfig.AllowedMethods; @@ -156,11 +158,19 @@ static void SetupMiddleware(WebApplication app) app.UseRouting(); var corsConfig = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); - app.UseCors(corsConfig?.CorsPolicyName ?? CorsConfig.DefaultCorsPolicyName); + app.UseCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName); //app.UseAuthentication(); //app.UseMiddleware(); //app.UseAuthorization(); + app.MapGet("/api/user", () => + { + return Results.Ok(new { Name = "John Doe" }); + }) + .WithName("GetUserInfomration") + .RequireCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName) + .WithOpenApi(); + //map health check middleware app.MapHealthChecks("/health", new HealthCheckOptions { From 3e1c7ee9895f411f4b9ebc40f4e0516366beebf3 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 16:17:05 -0400 Subject: [PATCH 16/79] Added basic yarp config for audit and notification service reverse proxy --- LinkAdmin.BFF/Program.cs | 12 +++++++----- LinkAdmin.BFF/appsettings.json | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index d6477479e..3313e3a24 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -4,17 +4,13 @@ using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Settings; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Serilog; using Serilog.Enrichers.Span; using Serilog.Exceptions; using Serilog.Settings.Configuration; -using System.Diagnostics; using System.Reflection; -using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); @@ -69,6 +65,10 @@ static void RegisterServices(WebApplicationBuilder builder) options.IncludeExceptionDetails = builder.Configuration.GetValue("ProblemDetails:IncludeExceptionDetails"); }); + //Add YARP (reverse proxy) + builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + //Add health checks builder.Services.AddHealthChecks(); @@ -169,7 +169,9 @@ static void SetupMiddleware(WebApplication app) }) .WithName("GetUserInfomration") .RequireCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName) - .WithOpenApi(); + .WithOpenApi(); + + app.MapReverseProxy(); //map health check middleware app.MapHealthChecks("/health", new HealthCheckOptions diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index b01d184a5..9d00d0e8f 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -60,5 +60,37 @@ } ] }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "route1": { + "ClusterId": "AuditService", + "Match": { + "Path": "api/audit/{**catch-all}" + } + }, + "route2": { + "ClusterId": "NotificationService", + "Match": { + "Path": "api/notification/{**catch-all}" + } + } + }, + "Clusters": { + "AuditService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:7334/" + } + } + }, + "NotificationService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:7434/" + } + } + } + } + } } From 846a4697919d1b649c92e9422b93c51c28eba5e6 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Tue, 19 Mar 2024 19:34:41 -0400 Subject: [PATCH 17/79] Added remaining services to yarp configuration --- LinkAdmin.BFF/appsettings.json | 116 ++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 3 deletions(-) diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 9d00d0e8f..bb17c193e 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -64,30 +64,140 @@ "ReverseProxy": { "Routes": { "route1": { + "ClusterId": "AccountService", + "Match": { + "Path": "api/account/{**catch-all}" + } + }, + "route2": { "ClusterId": "AuditService", "Match": { "Path": "api/audit/{**catch-all}" } }, - "route2": { + "route3": { + "ClusterId": "CensusService", + "Match": { + "Path": "api/census/{**catch-all}" + } + }, + "route4": { + "ClusterId": "DataAcquisitionService", + "Match": { + "Path": "api/data/{**catch-all}" + } + }, + "route5": { + "ClusterId": "MeasureEvaluationService", + "Match": { + "Path": "api/measureDef/{**catch-all}" + } + }, + "route6": { + "ClusterId": "NormalizationService", + "Match": { + "Path": "api/normalization/{**catch-all}" + } + }, + "route7": { "ClusterId": "NotificationService", "Match": { "Path": "api/notification/{**catch-all}" } + }, + "route8": { + "ClusterId": "ReportService", + "Match": { + "Path": "api/report/{**catch-all}" + } + }, + "route9": { + "ClusterId": "ReportService", + "Match": { + "Path": "api/reportconfig/{**catch-all}" + } + }, + "route10": { + "ClusterId": "SubmissionService", + "Match": { + "Path": "api/tenantsubmission/{**catch-all}" + } + }, + "route11": { + "ClusterId": "SubmissionService", + "Match": { + "Path": "api/facility/{**catch-all}" + } } }, "Clusters": { + "AccountService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:7221" + } + } + }, "AuditService": { "Destinations": { "destination1": { - "Address": "http://localhost:7334/" + "Address": "http://localhost:7334" + } + } + }, + "CensusService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5234" + } + } + }, + "DataAcquisitionService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5194" + } + } + }, + "MeasureEvaluationService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5135" + } + } + }, + "NormalizationService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5038" } } }, "NotificationService": { "Destinations": { "destination1": { - "Address": "http://localhost:7434/" + "Address": "http://localhost:7434" + } + } + }, + "ReportService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:7110" + } + } + }, + "SubmissionService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5264" + } + } + }, + "TenantService": { + "Destinations": { + "destination1": { + "Address": "http://localhost:7331" } } } From ce3f4e276539cbcf5911370b9d73cf5a68c5b2f5 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Wed, 20 Mar 2024 13:13:43 -0500 Subject: [PATCH 18/79] Convert Report to use Shared exceptions. Implement Shared exceptions in Submission. Add Topic and ServiceName properties to Shared exception interfaces. --- .../Error/Exceptions/TerminatingException.cs | 8 - .../Error/Exceptions/TransientException.cs | 8 - .../Error/Handlers/ReportExceptionHandler.cs | 55 ----- .../ReportTransientExceptionHandler.cs | 99 -------- .../Interfaces/IReportExceptionHandler.cs | 10 - .../IReportTransientExceptionHandler.cs | 10 - Report/Listeners/MeasureEvaluatedListener.cs | 198 ++++++++------- Report/Listeners/PatientsToQueryListener.cs | 79 +++--- Report/Listeners/ReportScheduledListener.cs | 226 +++++++++--------- Report/Listeners/ReportSubmittedListener.cs | 44 ++-- Report/Program.cs | 23 +- Report/Report.csproj | 2 +- .../Handlers/DeadLetterExceptionHandler.cs | 7 - .../Handlers/TransientExceptionHandler.cs | 7 - .../Interfaces/IDeadLetterExceptionHandler.cs | 10 + .../Interfaces/ITransientExceptionHandler.cs | 10 + .../Models/Configs/TenantApiSettings.cs | 4 +- Shared/Shared.csproj | 2 +- Submission/Listeners/SubmitReportListener.cs | 164 +++++++------ Submission/Program.cs | 17 +- Submission/Submission.csproj | 7 +- 21 files changed, 438 insertions(+), 552 deletions(-) delete mode 100644 Report/Application/Error/Exceptions/TerminatingException.cs delete mode 100644 Report/Application/Error/Exceptions/TransientException.cs delete mode 100644 Report/Application/Error/Handlers/ReportExceptionHandler.cs delete mode 100644 Report/Application/Error/Handlers/ReportTransientExceptionHandler.cs delete mode 100644 Report/Application/Error/Interfaces/IReportExceptionHandler.cs delete mode 100644 Report/Application/Error/Interfaces/IReportTransientExceptionHandler.cs diff --git a/Report/Application/Error/Exceptions/TerminatingException.cs b/Report/Application/Error/Exceptions/TerminatingException.cs deleted file mode 100644 index 307911601..000000000 --- a/Report/Application/Error/Exceptions/TerminatingException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LantanaGroup.Link.Report.Application.Error.Exceptions -{ - public class TerminatingException : Exception - { - public TerminatingException(string message) : base(message) { } - public TerminatingException(string message, Exception? innerEx) : base(message, innerEx) { } - } -} diff --git a/Report/Application/Error/Exceptions/TransientException.cs b/Report/Application/Error/Exceptions/TransientException.cs deleted file mode 100644 index 385799626..000000000 --- a/Report/Application/Error/Exceptions/TransientException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LantanaGroup.Link.Report.Application.Error.Exceptions -{ - public class TransientException : Exception - { - public TransientException(string message) : base(message) { } - public TransientException(string message, Exception? innerEx) : base(message, innerEx) { } - } -} diff --git a/Report/Application/Error/Handlers/ReportExceptionHandler.cs b/Report/Application/Error/Handlers/ReportExceptionHandler.cs deleted file mode 100644 index 6aae7220d..000000000 --- a/Report/Application/Error/Handlers/ReportExceptionHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Confluent.Kafka; -using LantanaGroup.Link.Report.Application.Error.Exceptions; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Shared.Application.Interfaces; -using LantanaGroup.Link.Shared.Application.Models; -using LantanaGroup.Link.Shared.Application.Models.Kafka; - -namespace LantanaGroup.Link.QueryDispatch.Application.Errors.Handlers -{ - public class ReportExceptionHandler : IReportExceptionHandler - { - private readonly ILogger> _logger; - private readonly IKafkaProducerFactory _auditProducerFactory; - - public ReportExceptionHandler(ILogger> logger, IKafkaProducerFactory auditProducerFactory) - { - _logger = logger; - _auditProducerFactory = auditProducerFactory; - } - - public void HandleException(ConsumeResult consumeResult, Exception ex) - { - try - { - _logger.LogError(message: "Failed to process Report Scheduled Event.", exception: ex); - - var auditValue = new AuditEventMessage - { - FacilityId = consumeResult.Message.Key as string, - Action = AuditEventType.Query, - ServiceName = "Report", - EventDate = DateTime.UtcNow, - Notes = $"Report Scheduled processing failure \nException Message: {ex.Message}", - }; - - ProduceAuditEvent(_auditProducerFactory, auditValue, consumeResult.Message.Headers); - } - catch (Exception e) - { - _logger.LogError(exception: e, message: "Error in ReportExceptionHandler.HandleException: " + e.Message); - } - } - - private void ProduceAuditEvent(IKafkaProducerFactory auditProducerFactory, AuditEventMessage auditValue, Headers headers) - { - using var producer = auditProducerFactory.CreateAuditEventProducer(); - producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), new Message - { - Value = auditValue, - Headers = headers - }); - producer.Flush(); - } - } -} \ No newline at end of file diff --git a/Report/Application/Error/Handlers/ReportTransientExceptionHandler.cs b/Report/Application/Error/Handlers/ReportTransientExceptionHandler.cs deleted file mode 100644 index 48e130e3a..000000000 --- a/Report/Application/Error/Handlers/ReportTransientExceptionHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Confluent.Kafka; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Report.Application.Models; -using LantanaGroup.Link.Shared.Application.Interfaces; -using LantanaGroup.Link.Shared.Application.Models; -using LantanaGroup.Link.Shared.Application.Models.Kafka; - -namespace LantanaGroup.Link.Report.Application.Error.Handlers -{ - public class ReportTransientExceptionHandler : IReportTransientExceptionHandler - { - private readonly ILogger> _logger; - private readonly IKafkaProducerFactory _auditProducerFactory; - private readonly IKafkaProducerFactory _producerFactory; - - public string Topic { get; set; } - - public ReportTransientExceptionHandler(ILogger> logger, - IKafkaProducerFactory auditProducerFactory, - IKafkaProducerFactory producerFactory) - { - _logger = logger; - _auditProducerFactory = auditProducerFactory; - _producerFactory = producerFactory; - - if (typeof(K) == typeof(MeasureReportScheduledKey)) - { - Topic = "ReportScheduled-Retry"; - } - else if (typeof(K) == typeof(ReportSubmittedKey)) - { - Topic = "ReportSubmitted-Retry"; - } - else if (typeof(K) == typeof(MeasureEvaluatedKey)) - { - Topic = "MeasureEvaluated-Retry"; - } - else if (typeof(V) == typeof(PatientsToQueryValue)) - { - Topic = "PatientsToQuery-Retry"; - } - else - { - throw new NotImplementedException("ReportTransientExceptionHandler: Type K is not configured for Retry."); - } - } - - public void HandleException(ConsumeResult consumeResult, Exception ex) - { - try - { - _logger.LogError(message: "Failed to process Report Event.", exception: ex); - - var auditValue = new AuditEventMessage - { - FacilityId = consumeResult.Message.Key as string, - Action = AuditEventType.Query, - ServiceName = "Report", - EventDate = DateTime.UtcNow, - Notes = $"Report processing failure \nException Message: {ex.Message}", - }; - - ProduceAuditEvent(_auditProducerFactory, auditValue, consumeResult.Message.Headers); - ProduceRetryReportScheduledEvent(consumeResult.Message.Key, consumeResult.Message.Value, - consumeResult.Message.Headers); - } - catch (Exception e) - { - _logger.LogError(exception: e, message: "Error in ReportTransientExceptionHandler.HandleException: " + e.Message); - throw; - } - } - - private void ProduceAuditEvent(IKafkaProducerFactory auditProducerFactory, AuditEventMessage auditValue, Headers headers) - { - using (var producer = auditProducerFactory.CreateAuditEventProducer()) - { - producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), new Message - { - Value = auditValue, - Headers = headers - }); - producer.Flush(); - } - } - - private void ProduceRetryReportScheduledEvent(K key, V value, Headers headers) - { - using var producer = _producerFactory.CreateProducer(new ProducerConfig()); - producer.Produce(Topic, new Message - { - Key = key, - Value = value, - Headers = headers - }); - producer.Flush(); - } - } -} \ No newline at end of file diff --git a/Report/Application/Error/Interfaces/IReportExceptionHandler.cs b/Report/Application/Error/Interfaces/IReportExceptionHandler.cs deleted file mode 100644 index 7a89ddebb..000000000 --- a/Report/Application/Error/Interfaces/IReportExceptionHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Confluent.Kafka; -using LantanaGroup.Link.Report.Application.Error.Exceptions; - -namespace LantanaGroup.Link.Report.Application.Error.Interfaces -{ - public interface IReportExceptionHandler - { - void HandleException(ConsumeResult consumeResult, Exception ex); - } -} diff --git a/Report/Application/Error/Interfaces/IReportTransientExceptionHandler.cs b/Report/Application/Error/Interfaces/IReportTransientExceptionHandler.cs deleted file mode 100644 index da3582fde..000000000 --- a/Report/Application/Error/Interfaces/IReportTransientExceptionHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Confluent.Kafka; -using LantanaGroup.Link.Report.Application.Error.Exceptions; - -namespace LantanaGroup.Link.Report.Application.Error.Interfaces -{ - public interface IReportTransientExceptionHandler - { - void HandleException(ConsumeResult consumeResult, Exception ex); - } -} diff --git a/Report/Listeners/MeasureEvaluatedListener.cs b/Report/Listeners/MeasureEvaluatedListener.cs index 2bba6e12a..67fa39ce9 100644 --- a/Report/Listeners/MeasureEvaluatedListener.cs +++ b/Report/Listeners/MeasureEvaluatedListener.cs @@ -1,5 +1,4 @@ -using System.Text; -using Confluent.Kafka; +using Confluent.Kafka; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Commands; @@ -7,14 +6,16 @@ using LantanaGroup.Link.Report.Application.MeasureReportSubmissionEntry.Commands; using LantanaGroup.Link.Report.Application.Models; using LantanaGroup.Link.Report.Entities; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using MediatR; +using System.Text; using System.Text.Json; using System.Transactions; +using LantanaGroup.Link.Shared.Application.Error.Handlers; using Task = System.Threading.Tasks.Task; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Report.Application.Error.Exceptions; namespace LantanaGroup.Link.Report.Listeners { @@ -26,13 +27,13 @@ public class MeasureEvaluatedListener : BackgroundService private readonly IKafkaProducerFactory _kafkaProducerFactory; private readonly IMediator _mediator; - private readonly IReportTransientExceptionHandler _reportTransientExceptionHandler; - private readonly IReportExceptionHandler _reportExceptionHandler; + private readonly ITransientExceptionHandler _transientExceptionHandler; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; public MeasureEvaluatedListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IKafkaProducerFactory kafkaProducerFactory, IMediator mediator, - IReportTransientExceptionHandler reportTransientExceptionHandler, - IReportExceptionHandler reportExceptionHandler) + ITransientExceptionHandler transientExceptionHandler, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -40,8 +41,16 @@ public MeasureEvaluatedListener(ILogger logger, IKafka _kafkaProducerFactory = kafkaProducerFactory ?? throw new ArgumentException(nameof(kafkaProducerFactory)); _mediator = mediator ?? throw new ArgumentException(nameof(mediator)); - _reportTransientExceptionHandler = reportTransientExceptionHandler ?? throw new ArgumentException(nameof(reportTransientExceptionHandler)); - _reportExceptionHandler = reportExceptionHandler ?? throw new ArgumentException(nameof(reportExceptionHandler)); + _transientExceptionHandler = transientExceptionHandler ?? throw new ArgumentException(nameof(transientExceptionHandler)); + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(deadLetterExceptionHandler)); + + var t = (TransientExceptionHandler)_transientExceptionHandler; + t.ServiceName = "Report"; + t.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Retry"; + + var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; + d.ServiceName = "Report"; + d.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -76,122 +85,125 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) { consumeResult = consumer.Consume(cancellationToken); - if (consumeResult != null) + if (consumeResult == null) { - MeasureEvaluatedKey key = consumeResult.Message.Key; - MeasureEvaluatedValue value = consumeResult.Message.Value; + consumeResult = new ConsumeResult(); + throw new DeadLetterException("MeasureEvaluatedListener: consumeResult is null"); + } - if (!consumeResult.Message.Headers.TryGetLastBytes("X-Correlation-Id", out var headerValue)) - { - throw new TerminatingException($"MeasureEvaluatedListener: Received message without correlation ID: {consumeResult.Topic}"); - } + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; - if (string.IsNullOrWhiteSpace(key.FacilityId) || - string.IsNullOrWhiteSpace(key.ReportType) || - key.StartDate == DateTime.MinValue || - key.EndDate == DateTime.MinValue) - { - throw new TerminatingException( - "MeasureEvaluatedListener: One or more required MeasureEvaluatedKey properties are null or empty."); - } + if (!consumeResult.Message.Headers.TryGetLastBytes("X-Correlation-Id", out var headerValue)) + { + throw new DeadLetterException($"MeasureEvaluatedListener: Received message without correlation ID: {consumeResult.Topic}"); + } - // find existing report scheduled for this facility, report type, and date range - var schedule = await _mediator.Send(new FindMeasureReportScheduleForReportTypeQuery { FacilityId = key.FacilityId, ReportStartDate = key.StartDate, ReportEndDate = key.EndDate, ReportType = key.ReportType }, cancellationToken) - ?? throw new TransactionException($"No report schedule found for Facility {key.FacilityId} and reporting period of {key.StartDate} - {key.EndDate} for {key.ReportType}"); - var measureReport = new MeasureReport(); + if (string.IsNullOrWhiteSpace(key.FacilityId) || + string.IsNullOrWhiteSpace(key.ReportType) || + key.StartDate == DateTime.MinValue || + key.EndDate == DateTime.MinValue) + { + throw new DeadLetterException( + "MeasureEvaluatedListener: One or more required MeasureEvaluatedKey properties are null or empty."); + } - try - { - measureReport = JsonSerializer.Deserialize(value.Result, - new JsonSerializerOptions().ForFhir(ModelInfo.ModelInspector)); - } - catch (Exception ex) - { - throw new TerminatingException( - "MeasureEvaluatedListener: Unable to deserialize MeasureEvaluatedValue.Result"); - } + // find existing report scheduled for this facility, report type, and date range + var schedule = await _mediator.Send(new FindMeasureReportScheduleForReportTypeQuery { FacilityId = key.FacilityId, ReportStartDate = key.StartDate, ReportEndDate = key.EndDate, ReportType = key.ReportType }, cancellationToken) + ?? throw new TransactionException($"No report schedule found for Facility {key.FacilityId} and reporting period of {key.StartDate} - {key.EndDate} for {key.ReportType}"); + var measureReport = new MeasureReport(); - // ensure measure report has an ID to avoid inserting duplicates during bundling - if (string.IsNullOrEmpty(measureReport.Id)) - { - measureReport.Id = Guid.NewGuid().ToString(); - } + try + { + measureReport = JsonSerializer.Deserialize(value.Result, + new JsonSerializerOptions().ForFhir(ModelInfo.ModelInspector)); + } + catch (Exception ex) + { + throw new DeadLetterException( + "MeasureEvaluatedListener: Unable to deserialize MeasureEvaluatedValue.Result: " + value.Result); + } - // add this measure report to the measure report entry collection - MeasureReportSubmissionEntryModel entry = new MeasureReportSubmissionEntryModel - { - FacilityId = key.FacilityId, - MeasureReportScheduleId = schedule.Id, - PatientId = value.PatientId, - MeasureReport = await new FhirJsonSerializer().SerializeToStringAsync(measureReport) - }; + // ensure measure report has an ID to avoid inserting duplicates during bundling + if (string.IsNullOrEmpty(measureReport.Id)) + { + measureReport.Id = Guid.NewGuid().ToString(); + } - await _mediator.Send(new CreateMeasureReportSubmissionEntryCommand - { - MeasureReportSubmissionEntry = entry - }, cancellationToken); + // add this measure report to the measure report entry collection + MeasureReportSubmissionEntryModel entry = new MeasureReportSubmissionEntryModel + { + FacilityId = key.FacilityId, + MeasureReportScheduleId = schedule.Id, + PatientId = value.PatientId, + MeasureReport = await new FhirJsonSerializer().SerializeToStringAsync(measureReport) + }; + + await _mediator.Send(new CreateMeasureReportSubmissionEntryCommand + { + MeasureReportSubmissionEntry = entry + }, cancellationToken); - #region Patients To Query & Submision Report Handling - if (schedule.PatientsToQueryDataRequested.GetValueOrDefault()) + #region Patients To Query & Submision Report Handling + if (schedule.PatientsToQueryDataRequested.GetValueOrDefault()) + { + if (schedule.PatientsToQuery?.Contains(value.PatientId) ?? false) { - if (schedule.PatientsToQuery?.Contains(value.PatientId) ?? false) + schedule.PatientsToQuery.Remove(value.PatientId); + + await _mediator.Send(new UpdateMeasureReportScheduleCommand { - schedule.PatientsToQuery.Remove(value.PatientId); + ReportSchedule = schedule + }, cancellationToken); + } - await _mediator.Send(new UpdateMeasureReportScheduleCommand + if (schedule.PatientsToQuery?.Count == 0) + { + using var prod = _kafkaProducerFactory.CreateProducer(producerConfig); + prod.Produce(nameof(KafkaTopic.SubmitReport), + new Message { - ReportSchedule = schedule - }, cancellationToken); - } - - if (schedule.PatientsToQuery?.Count == 0) - { - using var prod = _kafkaProducerFactory.CreateProducer(producerConfig); - prod.Produce(nameof(KafkaTopic.SubmitReport), - new Message + Key = new SubmissionReportKey() + { + FacilityId = schedule.FacilityId, + ReportType = schedule.ReportType + }, + Value = new SubmissionReportValue() + { + MeasureReportScheduleId = schedule.Id + }, + Headers = new Headers { - Key = new SubmissionReportKey() - { - FacilityId = schedule.FacilityId, - ReportType = schedule.ReportType - }, - Value = new SubmissionReportValue() - { - MeasureReportScheduleId = schedule.Id - }, - Headers = new Headers - { { "X-Correlation-Id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) } - } - }); + } + }); - prod.Flush(cancellationToken); - } + prod.Flush(cancellationToken); } - #endregion - - consumer.Commit(consumeResult); } + #endregion + + consumer.Commit(consumeResult); } catch (ConsumeException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); } - catch (TerminatingException ex) + catch (DeadLetterException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { - _reportTransientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex); consumer.Commit(consumeResult); } catch (Exception ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); } } } diff --git a/Report/Listeners/PatientsToQueryListener.cs b/Report/Listeners/PatientsToQueryListener.cs index f27a20c65..06f054646 100644 --- a/Report/Listeners/PatientsToQueryListener.cs +++ b/Report/Listeners/PatientsToQueryListener.cs @@ -1,15 +1,13 @@ using Confluent.Kafka; -using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Commands; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Queries; using LantanaGroup.Link.Report.Application.Models; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; +using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; - using MediatR; -using Quartz; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Report.Application.Error.Exceptions; -using static Confluent.Kafka.ConfigPropertyNames; namespace LantanaGroup.Link.Report.Listeners { @@ -19,20 +17,28 @@ public class PatientsToQueryListener : BackgroundService private readonly IKafkaConsumerFactory _kafkaConsumerFactory; private readonly IMediator _mediator; - private readonly IReportTransientExceptionHandler _reportTransientExceptionHandler; - private readonly IReportExceptionHandler _reportExceptionHandler; + private readonly ITransientExceptionHandler _transientExceptionHandler; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; public PatientsToQueryListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IMediator mediator, - IReportTransientExceptionHandler reportScheduledTransientExceptionhandler, - IReportExceptionHandler reportScheduledExceptionhandler) + ITransientExceptionHandler transientExceptionHandler, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentException(nameof(kafkaConsumerFactory)); _mediator = mediator ?? throw new ArgumentException(nameof(mediator)); - _reportTransientExceptionHandler = reportScheduledTransientExceptionhandler ?? throw new ArgumentException(nameof(reportScheduledTransientExceptionhandler)); - _reportExceptionHandler = reportScheduledExceptionhandler ?? throw new ArgumentException(nameof(reportScheduledExceptionhandler)); + _transientExceptionHandler = transientExceptionHandler ?? throw new ArgumentException(nameof(_transientExceptionHandler)); + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(_deadLetterExceptionHandler)); + + var t = (TransientExceptionHandler)_transientExceptionHandler; + t.ServiceName = "Report"; + t.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Retry"; + + var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; + d.ServiceName = "Report"; + d.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -61,51 +67,54 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) try { consumeResult = consumer.Consume(cancellationToken); - - if (consumeResult != null) + if (consumeResult == null) { - var key = consumeResult.Message.Key; - var value = consumeResult.Message.Value; + consumeResult = new ConsumeResult(); + throw new DeadLetterException( + "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); + } - if (string.IsNullOrWhiteSpace(key)) - { - throw new TerminatingException("PatientsToQueryListener: key value is null or empty"); - } + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; - var scheduledReports = await _mediator.Send(new FindMeasureReportScheduleForFacilityQuery() { FacilityId = key }, cancellationToken); - foreach (var scheduledReport in scheduledReports.Where(sr => !sr.PatientsToQueryDataRequested.GetValueOrDefault())) - { - scheduledReport.PatientsToQuery = value.PatientIds; + if (string.IsNullOrWhiteSpace(key)) + { + throw new DeadLetterException("PatientsToQueryListener: key value is null or empty"); + } - await _mediator.Send(new UpdateMeasureReportScheduleCommand() - { - ReportSchedule = scheduledReport + var scheduledReports = await _mediator.Send(new FindMeasureReportScheduleForFacilityQuery() { FacilityId = key }, cancellationToken); + foreach (var scheduledReport in scheduledReports.Where(sr => !sr.PatientsToQueryDataRequested.GetValueOrDefault())) + { + scheduledReport.PatientsToQuery = value.PatientIds; - }, cancellationToken); - } + await _mediator.Send(new UpdateMeasureReportScheduleCommand() + { + ReportSchedule = scheduledReport - consumer.Commit(consumeResult); + }, cancellationToken); } + + consumer.Commit(consumeResult); } catch (ConsumeException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); } - catch (TerminatingException ex) + catch (DeadLetterException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { - _reportTransientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex); consumer.Commit(consumeResult); } catch (Exception ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); } } } diff --git a/Report/Listeners/ReportScheduledListener.cs b/Report/Listeners/ReportScheduledListener.cs index b79dbeac3..6799fdb02 100644 --- a/Report/Listeners/ReportScheduledListener.cs +++ b/Report/Listeners/ReportScheduledListener.cs @@ -1,18 +1,17 @@ using Confluent.Kafka; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Commands; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Queries; using LantanaGroup.Link.Report.Application.Models; using LantanaGroup.Link.Report.Entities; using LantanaGroup.Link.Report.Services; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; +using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using MediatR; using Quartz; using Quartz.Impl.Triggers; -using SharpCompress.Common; -using LantanaGroup.Link.Report.Application.Error.Exceptions; -using LantanaGroup.Link.QueryDispatch.Application.Errors.Handlers; namespace LantanaGroup.Link.Report.Listeners { @@ -22,26 +21,34 @@ public class ReportScheduledListener : BackgroundService private readonly ILogger _logger; private readonly IKafkaConsumerFactory _kafkaConsumerFactory; private readonly IMediator _mediator; - private readonly IReportTransientExceptionHandler _reportTransientExceptionHandler; - private readonly IReportExceptionHandler _reportExceptionHandler; + private readonly ITransientExceptionHandler _transientExceptionHandler; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; private readonly ISchedulerFactory _schedulerFactory; public ReportScheduledListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IMediator mediator, ISchedulerFactory schedulerFactory, - IReportTransientExceptionHandler reportScheduledTransientExceptionhandler, - IReportExceptionHandler reportScheduledExceptionhandler) + ITransientExceptionHandler transientExceptionHandler, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentException(nameof(kafkaConsumerFactory)); _schedulerFactory = schedulerFactory ?? throw new ArgumentException(nameof(schedulerFactory)); _mediator = mediator ?? throw new ArgumentException(nameof(mediator)); - _reportTransientExceptionHandler = reportScheduledTransientExceptionhandler ?? - throw new ArgumentException(nameof(reportScheduledExceptionhandler)); + _transientExceptionHandler = transientExceptionHandler ?? + throw new ArgumentException(nameof(_deadLetterExceptionHandler)); + + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? + throw new ArgumentException(nameof(_deadLetterExceptionHandler)); + + var t = (TransientExceptionHandler)_transientExceptionHandler; + t.ServiceName = "Report"; + t.Topic = nameof(KafkaTopic.ReportScheduled) + "-Retry"; - _reportExceptionHandler = reportScheduledExceptionhandler ?? - throw new ArgumentException(nameof(reportScheduledExceptionhandler)); + var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; + d.ServiceName = "Report"; + d.Topic = nameof(KafkaTopic.ReportScheduled) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -70,137 +77,140 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) try { consumeResult = consumer.Consume(cancellationToken); + if (consumeResult == null) + { + consumeResult = new ConsumeResult(); + throw new DeadLetterException( + "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); + } + + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; - if (consumeResult != null) + if (string.IsNullOrWhiteSpace(key.FacilityId) || + string.IsNullOrWhiteSpace(key.ReportType)) { - var key = consumeResult.Message.Key; - var value = consumeResult.Message.Value; + throw new DeadLetterException( + "ReportScheduledListener: One or more required MeasureReportScheduledKey properties are null or empty."); + } - if (string.IsNullOrWhiteSpace(key.FacilityId) || - string.IsNullOrWhiteSpace(key.ReportType)) - { - throw new TerminatingException( - "ReportScheduledListener: One or more required MeasureReportScheduledKey properties are null or empty."); - } + DateTimeOffset startDateOffset; + if (!DateTimeOffset.TryParse( + value.Parameters.Single(x => x.Key.ToLower() == "startdate").Value, + out startDateOffset)) + { + throw new DeadLetterException("ReportScheduledListener: Start Date could not be parsed"); + } - DateTimeOffset startDateOffset; - if (!DateTimeOffset.TryParse( - value.Parameters.Single(x => x.Key.ToLower() == "startdate").Value, - out startDateOffset)) - { - throw new TerminatingException("ReportScheduledListener: Start Date could not be parsed"); - } + DateTimeOffset endDateOffset; + if (!DateTimeOffset.TryParse( + value.Parameters.Single(x => x.Key.ToLower() == "enddate").Value, + out endDateOffset)) + { + throw new DeadLetterException("ReportScheduledListener: End Date could not be parsed"); + } - DateTimeOffset endDateOffset; - if (!DateTimeOffset.TryParse( - value.Parameters.Single(x => x.Key.ToLower() == "enddate").Value, - out endDateOffset)) - { - throw new TerminatingException("ReportScheduledListener: End Date could not be parsed"); - } + var startDate = startDateOffset.UtcDateTime; + var endDate = endDateOffset.UtcDateTime; + + var scheduleTrigger = ""; + try + { + // There may eventually be a need to have the consumeResult.Message.Value contain a parameter indicating how often the job should run (Daily, Weekly, Monthly, etc) + // This will schedule the job to run once a month on the day, hour and minute specified on the endDate. + // However, when the job runs, it will delete itself from the schedule. + var cronSchedule = + CronScheduleBuilder + .MonthlyOnDayAndHourAndMinute(endDate.Day, endDate.Hour, endDate.Minute) + .Build() as CronTriggerImpl; + cronSchedule.StartTimeUtc = startDateOffset; + cronSchedule.EndTimeUtc = endDateOffset; + cronSchedule.SetNextFireTimeUtc(endDateOffset); + + scheduleTrigger = cronSchedule.CronExpressionString; + } + catch (Exception ex) + { + throw new DeadLetterException( + "ReportScheduledListener: Cron Schedule could not be created from provided dates.", ex.InnerException); + } - var startDate = startDateOffset.UtcDateTime; - var endDate = endDateOffset.UtcDateTime; - var scheduleTrigger = ""; - try - { - // There may eventually be a need to have the consumeResult.Message.Value contain a parameter indicating how often the job should run (Daily, Weekly, Monthly, etc) - // This will schedule the job to run once a month on the day, hour and minute specified on the endDate. - // However, when the job runs, it will delete itself from the schedule. - var cronSchedule = - CronScheduleBuilder - .MonthlyOnDayAndHourAndMinute(endDate.Day, endDate.Hour, endDate.Minute) - .Build() as CronTriggerImpl; - cronSchedule.StartTimeUtc = startDateOffset; - cronSchedule.EndTimeUtc = endDateOffset; - cronSchedule.SetNextFireTimeUtc(endDateOffset); - - scheduleTrigger = cronSchedule.CronExpressionString; - } - catch (Exception ex) + if (string.IsNullOrWhiteSpace(scheduleTrigger)) + { + throw new DeadLetterException( + "ReportScheduledListener: scheduleTrigger is null or empty."); + } + + // create or update the consumed report schedule + var existing = await _mediator.Send( + new FindMeasureReportScheduleForReportTypeQuery() { - throw new TerminatingException( - "ReportScheduledListener: Cron Schedule could not be created from provided dates.", ex.InnerException); - } + FacilityId = key.FacilityId, + ReportStartDate = startDate, + ReportEndDate = endDate, + ReportType = key.ReportType + }, cancellationToken); + if (existing != null) + { + existing.FacilityId = key.FacilityId; + existing.ReportStartDate = startDate; + existing.ReportEndDate = endDate; + existing.ScheduledTrigger = scheduleTrigger; + existing.ReportType = key.ReportType; - if (string.IsNullOrWhiteSpace(scheduleTrigger)) + await _mediator.Send(new UpdateMeasureReportScheduleCommand() { - throw new TerminatingException( - "ReportScheduledListener: scheduleTrigger is null or empty."); - } + ReportSchedule = existing + }, cancellationToken); - // create or update the consumed report schedule - var existing = await _mediator.Send( - new FindMeasureReportScheduleForReportTypeQuery() + if (existing.ScheduledTrigger != scheduleTrigger) + { + await MeasureReportScheduleService.RescheduleJob(existing, + await _schedulerFactory.GetScheduler(cancellationToken)); + } + } + else + { + var reportSchedule = await _mediator.Send(new CreateMeasureReportScheduleCommand + { + ReportSchedule = new MeasureReportScheduleModel { FacilityId = key.FacilityId, ReportStartDate = startDate, ReportEndDate = endDate, + ScheduledTrigger = scheduleTrigger, ReportType = key.ReportType - }, cancellationToken); - - if (existing != null) - { - existing.FacilityId = key.FacilityId; - existing.ReportStartDate = startDate; - existing.ReportEndDate = endDate; - existing.ScheduledTrigger = scheduleTrigger; - existing.ReportType = key.ReportType; - - await _mediator.Send(new UpdateMeasureReportScheduleCommand() - { - ReportSchedule = existing - }, cancellationToken); - - if (existing.ScheduledTrigger != scheduleTrigger) - { - await MeasureReportScheduleService.RescheduleJob(existing, - await _schedulerFactory.GetScheduler(cancellationToken)); } - } - else - { - var reportSchedule = await _mediator.Send(new CreateMeasureReportScheduleCommand - { - ReportSchedule = new MeasureReportScheduleModel - { - FacilityId = key.FacilityId, - ReportStartDate = startDate, - ReportEndDate = endDate, - ScheduledTrigger = scheduleTrigger, - ReportType = key.ReportType - } - }, cancellationToken); - - await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, - await _schedulerFactory.GetScheduler(cancellationToken)); + }, cancellationToken); + await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, + await _schedulerFactory.GetScheduler(cancellationToken)); - consumer.Commit(consumeResult); - } + + consumer.Commit(consumeResult); } } catch (ConsumeException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); } - catch (TerminatingException ex) + catch (DeadLetterException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { - _reportTransientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex); consumer.Commit(consumeResult); } catch (Exception ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); } } } diff --git a/Report/Listeners/ReportSubmittedListener.cs b/Report/Listeners/ReportSubmittedListener.cs index 18c28d29c..b21f1680d 100644 --- a/Report/Listeners/ReportSubmittedListener.cs +++ b/Report/Listeners/ReportSubmittedListener.cs @@ -1,16 +1,17 @@ using Confluent.Kafka; -using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Commands; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Queries; using LantanaGroup.Link.Report.Application.Models; using LantanaGroup.Link.Report.Entities; using LantanaGroup.Link.Report.Settings; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; +using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Kafka; using MediatR; using System.Text; -using LantanaGroup.Link.Report.Application.Error.Exceptions; -using LantanaGroup.Link.Report.Application.Error.Interfaces; +using LantanaGroup.Link.Shared.Application.Error.Handlers; namespace LantanaGroup.Link.Report.Listeners { @@ -24,24 +25,32 @@ public class ReportSubmittedListener : BackgroundService private readonly IKafkaProducerFactory _kafkaProducerFactory; private readonly IMediator _mediator; - private readonly IReportTransientExceptionHandler _reportTransientExceptionHandler; - private readonly IReportExceptionHandler _reportExceptionHandler; + private readonly ITransientExceptionHandler _transientExceptionHandler; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; public ReportSubmittedListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IKafkaProducerFactory kafkaProducerFactory, IMediator mediator, - IReportTransientExceptionHandler reportTransientExceptionHandler, - IReportExceptionHandler reportExceptionHandler) + ITransientExceptionHandler transientExceptionHandler, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentException(nameof(kafkaConsumerFactory)); _kafkaProducerFactory = kafkaProducerFactory ?? throw new ArgumentException(nameof(kafkaProducerFactory)); _mediator = mediator ?? throw new ArgumentException(nameof(mediator)); - _reportTransientExceptionHandler = reportTransientExceptionHandler ?? - throw new ArgumentException(nameof(reportExceptionHandler)); + _transientExceptionHandler = transientExceptionHandler ?? + throw new ArgumentException(nameof(deadLetterExceptionHandler)); + + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? + throw new ArgumentException(nameof(deadLetterExceptionHandler)); + + var t = (TransientExceptionHandler)_transientExceptionHandler; + t.ServiceName = "Report"; + t.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Retry"; - _reportExceptionHandler = reportExceptionHandler ?? - throw new ArgumentException(nameof(reportExceptionHandler)); + var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; + d.ServiceName = "Report"; + d.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -74,7 +83,8 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (consumeResult == null) { - throw new TerminatingException( + consumeResult = new ConsumeResult(); + throw new DeadLetterException( "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); } @@ -127,7 +137,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) } catch (Exception ex) { - throw new TerminatingException("ReportSubmittedListener: " + ex.Message, ex.InnerException); + throw new DeadLetterException("ReportSubmittedListener: " + ex.Message, ex.InnerException); } } catch (ConsumeException e) @@ -139,20 +149,20 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) break; } } - catch (TerminatingException ex) + catch (DeadLetterException ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { - _reportTransientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex); consumer.Commit(consumeResult); } catch (Exception ex) { consumer.Commit(consumeResult); - _reportExceptionHandler.HandleException(consumeResult, new TerminatingException("ReportSubmittedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportSubmittedListener: " + ex.Message, ex.InnerException)); } } } diff --git a/Report/Program.cs b/Report/Program.cs index 089be9db0..9dd6043a8 100644 --- a/Report/Program.cs +++ b/Report/Program.cs @@ -8,10 +8,13 @@ using LantanaGroup.Link.Report.Repositories; using LantanaGroup.Link.Report.Services; using LantanaGroup.Link.Report.Settings; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Factories; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Configs; using LantanaGroup.Link.Shared.Application.Models.Kafka; +using LantanaGroup.Link.Shared.Application.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -23,10 +26,6 @@ using Serilog.Enrichers.Span; using Serilog.Exceptions; using System.Reflection; -using LantanaGroup.Link.QueryDispatch.Application.Errors.Handlers; -using LantanaGroup.Link.Report.Application.Error.Handlers; -using LantanaGroup.Link.Report.Application.Error.Interfaces; -using LantanaGroup.Link.Shared.Application.Services; var builder = WebApplication.CreateBuilder(args); @@ -126,20 +125,20 @@ static void RegisterServices(WebApplicationBuilder builder) #region Exception Handling //Report Scheduled Listener - builder.Services.AddTransient, ReportExceptionHandler>(); - builder.Services.AddTransient, ReportTransientExceptionHandler>(); + builder.Services.AddTransient, DeadLetterExceptionHandler>(); + builder.Services.AddTransient, TransientExceptionHandler>(); //Report Submitted Listener - builder.Services.AddTransient, ReportExceptionHandler>(); - builder.Services.AddTransient, ReportTransientExceptionHandler>(); + builder.Services.AddTransient, DeadLetterExceptionHandler>(); + builder.Services.AddTransient, TransientExceptionHandler>(); //Patients To Query Listener - builder.Services.AddTransient, ReportExceptionHandler>(); - builder.Services.AddTransient, ReportTransientExceptionHandler>(); + builder.Services.AddTransient, DeadLetterExceptionHandler>(); + builder.Services.AddTransient, TransientExceptionHandler>(); //Measure Evaluated Listener - builder.Services.AddTransient, ReportExceptionHandler>(); - builder.Services.AddTransient, ReportTransientExceptionHandler>(); + builder.Services.AddTransient, DeadLetterExceptionHandler>(); + builder.Services.AddTransient, TransientExceptionHandler>(); #endregion // Logging using Serilog diff --git a/Report/Report.csproj b/Report/Report.csproj index d8638455e..2467fa770 100644 --- a/Report/Report.csproj +++ b/Report/Report.csproj @@ -22,7 +22,7 @@ - + diff --git a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs index 01bb1d5f1..a3f3d5d66 100644 --- a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs @@ -15,14 +15,7 @@ public class DeadLetterExceptionHandler : IDeadLetterExceptionHandler AuditProducerFactory; protected readonly IKafkaProducerFactory ProducerFactory; - /// - /// The Topic to use when publishing Retry Kafka events. - /// public string Topic { get; set; } = string.Empty; - - /// - /// The name of the service that is consuming the ExceptionHandler. - /// public string ServiceName { get; set; } = string.Empty; public DeadLetterExceptionHandler(ILogger> logger, diff --git a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs index 995974b11..e9b9b5b5e 100644 --- a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs @@ -14,14 +14,7 @@ public class TransientExceptionHandler : ITransientExceptionHandler protected readonly IKafkaProducerFactory AuditProducerFactory; protected readonly IKafkaProducerFactory ProducerFactory; - /// - /// The Topic to use when publishing Retry Kafka events. - /// public string Topic { get; set; } = string.Empty; - - /// - /// The name of the service that is consuming the TransientExceptionHandler. - /// public string ServiceName { get; set; } = string.Empty; protected TransientExceptionHandler(ILogger> logger, diff --git a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs index 32c4d7e9c..f133ea9c8 100644 --- a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs @@ -5,6 +5,16 @@ namespace LantanaGroup.Link.Shared.Application.Error.Interfaces { public interface IDeadLetterExceptionHandler { + /// + /// The Topic to use when publishing Retry Kafka events. + /// + public string Topic { get; set; } + + /// + /// The name of the service that is consuming the IDeadLetterExceptionHandler. + /// + public string ServiceName { get; set; } + void HandleException(ConsumeResult consumeResult, Exception ex); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceDeadLetter(K key, V value, Headers headers, string exceptionMessage); diff --git a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs index 0076ef2b8..7949752b5 100644 --- a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs @@ -6,6 +6,16 @@ namespace LantanaGroup.Link.Shared.Application.Error.Interfaces { public interface ITransientExceptionHandler { + /// + /// The Topic to use when publishing Retry Kafka events. + /// + public string Topic { get; set; } + + /// + /// The name of the service that is consuming the ITransientExceptionHandler. + /// + public string ServiceName { get; set; } + void HandleException(ConsumeResult consumeResult, Exception ex); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceRetryScheduledEvent(K key, V value, Headers headers); diff --git a/Shared/Application/Models/Configs/TenantApiSettings.cs b/Shared/Application/Models/Configs/TenantApiSettings.cs index 041209d22..43093a726 100644 --- a/Shared/Application/Models/Configs/TenantApiSettings.cs +++ b/Shared/Application/Models/Configs/TenantApiSettings.cs @@ -2,7 +2,7 @@ public class TenantApiSettings { - public string TenantServiceBaseEndpoint { get; set; } + public string? TenantServiceBaseEndpoint { get; set; } public bool CheckIfTenantExists { get; set; } - public string GetTenantRelativeEndpoint { get; set; } + public string? GetTenantRelativeEndpoint { get; set; } } diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index f66213ba4..a4ac764da 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -14,7 +14,7 @@ Shared library for Lantana Link NHSN Link Shared Library - 1.1.2 + 1.2.2 com.lantanagroup.link.Shared diff --git a/Submission/Listeners/SubmitReportListener.cs b/Submission/Listeners/SubmitReportListener.cs index a5d0e6097..70c5e097d 100644 --- a/Submission/Listeners/SubmitReportListener.cs +++ b/Submission/Listeners/SubmitReportListener.cs @@ -1,4 +1,8 @@ -using Confluent.Kafka; +using System.Transactions; +using Confluent.Kafka; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Submission.Application.Models; @@ -15,10 +19,15 @@ public class SubmitReportListener : BackgroundService private readonly IMediator _mediator; private readonly SubmissionServiceConfig _submissionConfig; private readonly FileSystemConfig _fileSystemConfig; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClient; + + private readonly ITransientExceptionHandler _transientExceptionHandler; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; public SubmitReportListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, - IMediator mediator, IOptions submissionConfig, IOptions fileSystemConfig, HttpClient httpClient) + IMediator mediator, IOptions submissionConfig, IOptions fileSystemConfig, IHttpClientFactory httpClient, + ITransientExceptionHandler transientExceptionHandler, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentException(nameof(kafkaConsumerFactory)); @@ -26,6 +35,19 @@ public SubmitReportListener(ILogger logger, IKafkaConsumer _submissionConfig = submissionConfig.Value; _fileSystemConfig = fileSystemConfig.Value; _httpClient = httpClient ?? throw new ArgumentNullException(nameof(HttpClient)); + + _transientExceptionHandler = transientExceptionHandler ?? + throw new ArgumentException(nameof(transientExceptionHandler)); + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? + throw new ArgumentException(nameof(deadLetterExceptionHandler)); + + var t = (TransientExceptionHandler)_transientExceptionHandler; + t.ServiceName = "Submission"; + t.Topic = nameof(KafkaTopic.SubmitReport) + "-Retry"; + + var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; + d.ServiceName = "Submission"; + d.Topic = nameof(KafkaTopic.SubmitReport) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -42,81 +64,89 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) EnableAutoCommit = false }; - using (var _submitReportConsumer = _kafkaConsumerFactory.CreateConsumer(config)) + using var consumer = _kafkaConsumerFactory.CreateConsumer(config); + try { - try - { - _submitReportConsumer.Subscribe(nameof(KafkaTopic.SubmitReport)); - _logger.LogInformation($"Started consumer for topic '{nameof(KafkaTopic.SubmitReport)}' at {DateTime.UtcNow}"); + consumer.Subscribe(nameof(KafkaTopic.SubmitReport)); + _logger.LogInformation($"Started consumer for topic '{nameof(KafkaTopic.SubmitReport)}' at {DateTime.UtcNow}"); - while (!cancellationToken.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested) + { + var consumeResult = new ConsumeResult(); + try { - try + consumeResult = consumer.Consume(cancellationToken); + if (consumeResult == null) { - var consumeResult = _submitReportConsumer.Consume(cancellationToken); - - try - { - var key = consumeResult.Message.Key; - var value = consumeResult.Message.Value; - - if (string.IsNullOrWhiteSpace(value.MeasureReportScheduleId)) - { - throw new InvalidOperationException("MeasureReportScheduleId is null or empty"); - } - - string requestUrl = _submissionConfig.ReportServiceUrl + $"?reportId={value.MeasureReportScheduleId}"; - - var response = await _httpClient.GetAsync(requestUrl, cancellationToken); - var measureReportSubmissionBundle = - JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); - - #region File IO - string facilityDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileSystemConfig.FilePath.Trim('/'), key.FacilityId); - if (!Directory.Exists(facilityDirectory)) - { - Directory.CreateDirectory(facilityDirectory); - } - - var dtu = DateTime.UtcNow; - string fullFilePath = facilityDirectory + $"/submission_{value.MeasureReportScheduleId.Replace("-", "_")}.txt"; - - await File.WriteAllTextAsync(fullFilePath, measureReportSubmissionBundle.SubmissionBundle, cancellationToken); - - if (!File.Exists(fullFilePath)) - { - throw new Exception("SubmitReportListener: Bundle File Not Created"); - } - #endregion - - _submitReportConsumer.Commit(consumeResult); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error occurred during SubmitReportListener: {ex.Message}"); - throw; - } + consumeResult = new ConsumeResult(); + throw new DeadLetterException( + "SubmitReportListener: Result of ConsumeResult.Consume is null"); } - catch (ConsumeException e) + + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; + + if (string.IsNullOrWhiteSpace(key.FacilityId) || + string.IsNullOrWhiteSpace(key.ReportType) || + string.IsNullOrWhiteSpace(value.MeasureReportScheduleId)) { - _logger.LogError(e, $"Consumer error: {e.Error.Reason}"); - if (e.Error.IsFatal) - { - break; - } + throw new DeadLetterException( + "SubmitReportListener: One or more required MeasureReportScheduledKey properties are null or empty."); } - catch (Exception ex) + + string requestUrl = _submissionConfig.ReportServiceUrl + $"?reportId={value.MeasureReportScheduleId}"; + + var response = await _httpClient.CreateClient().GetAsync(requestUrl, cancellationToken); + var measureReportSubmissionBundle = + JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + + #region File IO + string facilityDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileSystemConfig.FilePath.Trim('/'), key.FacilityId); + if (!Directory.Exists(facilityDirectory)) { - _logger.LogError(ex, $"An exception occurred in the Submit Report Consumer service: {ex.Message}", ex); + Directory.CreateDirectory(facilityDirectory); } + + var dtu = DateTime.UtcNow; + string fullFilePath = facilityDirectory + $"/submission_{value.MeasureReportScheduleId.Replace("-", "_")}.txt"; + + await File.WriteAllTextAsync(fullFilePath, measureReportSubmissionBundle.SubmissionBundle, cancellationToken); + + if (!File.Exists(fullFilePath)) + { + throw new TransientException("SubmitReportListener: Bundle File Not Created"); + } + #endregion + + consumer.Commit(consumeResult); + } + catch (ConsumeException ex) + { + consumer.Commit(consumeResult); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + } + catch (DeadLetterException ex) + { + consumer.Commit(consumeResult); + _deadLetterExceptionHandler.HandleException(consumeResult, ex); + } + catch (TransientException ex) + { + _transientExceptionHandler.HandleException(consumeResult, ex); + consumer.Commit(consumeResult); + } + catch (Exception ex) + { + consumer.Commit(consumeResult); + _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); } } - catch (OperationCanceledException oce) - { - _logger.LogError($"Operation Canceled: {oce.Message}", oce); - _submitReportConsumer.Close(); - _submitReportConsumer.Dispose(); - } + } + catch (OperationCanceledException oce) + { + _logger.LogError($"Operation Canceled: {oce.Message}", oce); + consumer.Close(); + consumer.Dispose(); } } diff --git a/Submission/Program.cs b/Submission/Program.cs index c6e52725c..9ac823d02 100644 --- a/Submission/Program.cs +++ b/Submission/Program.cs @@ -16,6 +16,8 @@ using LantanaGroup.Link.Submission.Application.Queries; using LantanaGroup.Link.Submission.Application.Repositories; using HealthChecks.UI.Client; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; var builder = WebApplication.CreateBuilder(args); @@ -77,16 +79,11 @@ static void RegisterServices(WebApplicationBuilder builder) // Add repositories // TODO - //// Setup CORS - //builder.Services.AddCors(options => - //{ - // options.AddPolicy("CorsPolicy", - // builder => builder - // .AllowAnyMethod() - // .AllowCredentials() - // .SetIsOriginAllowed((host) => true) //lock this down, allows all atm - // .AllowAnyHeader()); - //}); + #region Exception Handling + //Report Scheduled Listener + builder.Services.AddTransient, DeadLetterExceptionHandler>(); + builder.Services.AddTransient, TransientExceptionHandler>(); + #endregion // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/Submission/Submission.csproj b/Submission/Submission.csproj index f1833b8fc..4ce8dbacc 100644 --- a/Submission/Submission.csproj +++ b/Submission/Submission.csproj @@ -10,19 +10,23 @@ + + + + - + @@ -45,7 +49,6 @@ - From 0a20f51acdde869f2064308ec10016c3302c1be7 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Wed, 20 Mar 2024 14:15:30 -0500 Subject: [PATCH 19/79] Report and Submission --- Report/Listeners/MeasureEvaluatedListener.cs | 54 +++++++----- Report/Listeners/PatientsToQueryListener.cs | 37 +++++---- Report/Listeners/ReportScheduledListener.cs | 43 ++++++---- Report/Listeners/ReportSubmittedListener.cs | 86 ++++++++++---------- Report/Program.cs | 2 +- Submission/Listeners/SubmitReportListener.cs | 33 +++++--- 6 files changed, 146 insertions(+), 109 deletions(-) diff --git a/Report/Listeners/MeasureEvaluatedListener.cs b/Report/Listeners/MeasureEvaluatedListener.cs index 67fa39ce9..233825496 100644 --- a/Report/Listeners/MeasureEvaluatedListener.cs +++ b/Report/Listeners/MeasureEvaluatedListener.cs @@ -16,6 +16,7 @@ using System.Transactions; using LantanaGroup.Link.Shared.Application.Error.Handlers; using Task = System.Threading.Tasks.Task; +using LantanaGroup.Link.Report.Settings; namespace LantanaGroup.Link.Report.Listeners { @@ -30,6 +31,8 @@ public class MeasureEvaluatedListener : BackgroundService private readonly ITransientExceptionHandler _transientExceptionHandler; private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; + private string Name => this.GetType().Name; + public MeasureEvaluatedListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IKafkaProducerFactory kafkaProducerFactory, IMediator mediator, ITransientExceptionHandler transientExceptionHandler, @@ -45,11 +48,11 @@ public MeasureEvaluatedListener(ILogger logger, IKafka _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(deadLetterExceptionHandler)); var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = "Report"; + t.ServiceName = ReportConstants.ServiceName; t.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Retry"; var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = "Report"; + d.ServiceName = ReportConstants.ServiceName; d.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Error"; } @@ -80,15 +83,14 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - var consumeResult = new ConsumeResult(); + ConsumeResult? consumeResult = null; try { consumeResult = consumer.Consume(cancellationToken); if (consumeResult == null) { - consumeResult = new ConsumeResult(); - throw new DeadLetterException("MeasureEvaluatedListener: consumeResult is null"); + throw new DeadLetterException($"{Name}: consumeResult is null"); } var key = consumeResult.Message.Key; @@ -96,7 +98,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (!consumeResult.Message.Headers.TryGetLastBytes("X-Correlation-Id", out var headerValue)) { - throw new DeadLetterException($"MeasureEvaluatedListener: Received message without correlation ID: {consumeResult.Topic}"); + _logger.LogInformation($"{Name}: Received message without correlation ID: {consumeResult.Topic}"); } if (string.IsNullOrWhiteSpace(key.FacilityId) || @@ -105,12 +107,18 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) key.EndDate == DateTime.MinValue) { throw new DeadLetterException( - "MeasureEvaluatedListener: One or more required MeasureEvaluatedKey properties are null or empty."); + $"{Name}: One or more required Key/Value properties are null, empty, or otherwise invalid."); } // find existing report scheduled for this facility, report type, and date range - var schedule = await _mediator.Send(new FindMeasureReportScheduleForReportTypeQuery { FacilityId = key.FacilityId, ReportStartDate = key.StartDate, ReportEndDate = key.EndDate, ReportType = key.ReportType }, cancellationToken) - ?? throw new TransactionException($"No report schedule found for Facility {key.FacilityId} and reporting period of {key.StartDate} - {key.EndDate} for {key.ReportType}"); + var schedule = await _mediator.Send( + new FindMeasureReportScheduleForReportTypeQuery + { + FacilityId = key.FacilityId, ReportStartDate = key.StartDate, + ReportEndDate = key.EndDate, ReportType = key.ReportType + }, cancellationToken) + ?? throw new TransactionException( + $"{Name}: report schedule found for Facility {key.FacilityId} and reporting period of {key.StartDate} - {key.EndDate} for {key.ReportType}"); var measureReport = new MeasureReport(); try @@ -121,7 +129,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (Exception ex) { throw new DeadLetterException( - "MeasureEvaluatedListener: Unable to deserialize MeasureEvaluatedValue.Result: " + value.Result); + $"{Name}: Unable to deserialize MeasureEvaluatedValue.Result: " + value.Result); } // ensure measure report has an ID to avoid inserting duplicates during bundling @@ -145,6 +153,7 @@ await _mediator.Send(new CreateMeasureReportSubmissionEntryCommand }, cancellationToken); #region Patients To Query & Submision Report Handling + if (schedule.PatientsToQueryDataRequested.GetValueOrDefault()) { if (schedule.PatientsToQuery?.Contains(value.PatientId) ?? false) @@ -174,7 +183,7 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand }, Headers = new Headers { - { "X-Correlation-Id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) } + { "X-Correlation-Id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) } } }); @@ -182,28 +191,35 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand } } #endregion - - consumer.Commit(consumeResult); } catch (ConsumeException ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); } catch (DeadLetterException ex) { - consumer.Commit(consumeResult); _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { _transientExceptionHandler.HandleException(consumeResult, ex); - consumer.Commit(consumeResult); } catch (Exception ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("MeasureEvaluatedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + } + finally + { + if (consumeResult != null) + { + consumer.Commit(consumeResult); + } + else + { + consumer.Commit(); + } } } } diff --git a/Report/Listeners/PatientsToQueryListener.cs b/Report/Listeners/PatientsToQueryListener.cs index 06f054646..a2e007ad3 100644 --- a/Report/Listeners/PatientsToQueryListener.cs +++ b/Report/Listeners/PatientsToQueryListener.cs @@ -2,6 +2,7 @@ using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Commands; using LantanaGroup.Link.Report.Application.MeasureReportSchedule.Queries; using LantanaGroup.Link.Report.Application.Models; +using LantanaGroup.Link.Report.Settings; using LantanaGroup.Link.Shared.Application.Error.Exceptions; using LantanaGroup.Link.Shared.Application.Error.Handlers; using LantanaGroup.Link.Shared.Application.Error.Interfaces; @@ -20,6 +21,8 @@ public class PatientsToQueryListener : BackgroundService private readonly ITransientExceptionHandler _transientExceptionHandler; private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; + private string Name => this.GetType().Name; + public PatientsToQueryListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IMediator mediator, ITransientExceptionHandler transientExceptionHandler, @@ -33,11 +36,11 @@ public PatientsToQueryListener(ILogger logger, IKafkaCo _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(_deadLetterExceptionHandler)); var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = "Report"; + t.ServiceName = ReportConstants.ServiceName; t.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Retry"; var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = "Report"; + d.ServiceName = ReportConstants.ServiceName; d.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Error"; } @@ -63,15 +66,14 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - var consumeResult = new ConsumeResult(); + ConsumeResult? consumeResult = null; try { consumeResult = consumer.Consume(cancellationToken); if (consumeResult == null) { - consumeResult = new ConsumeResult(); throw new DeadLetterException( - "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); + $"{Name}: consumeResult is null"); } var key = consumeResult.Message.Key; @@ -79,7 +81,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (string.IsNullOrWhiteSpace(key)) { - throw new DeadLetterException("PatientsToQueryListener: key value is null or empty"); + throw new DeadLetterException($"{Name}: key value is null or empty"); } var scheduledReports = await _mediator.Send(new FindMeasureReportScheduleForFacilityQuery() { FacilityId = key }, cancellationToken); @@ -93,28 +95,35 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand() }, cancellationToken); } - - consumer.Commit(consumeResult); } catch (ConsumeException ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); } catch (DeadLetterException ex) { - consumer.Commit(consumeResult); _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { _transientExceptionHandler.HandleException(consumeResult, ex); - consumer.Commit(consumeResult); } catch (Exception ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("PatientsToQueryListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + } + finally + { + if (consumeResult != null) + { + consumer.Commit(consumeResult); + } + else + { + consumer.Commit(); + } } } } diff --git a/Report/Listeners/ReportScheduledListener.cs b/Report/Listeners/ReportScheduledListener.cs index 6799fdb02..8ddd50059 100644 --- a/Report/Listeners/ReportScheduledListener.cs +++ b/Report/Listeners/ReportScheduledListener.cs @@ -4,6 +4,7 @@ using LantanaGroup.Link.Report.Application.Models; using LantanaGroup.Link.Report.Entities; using LantanaGroup.Link.Report.Services; +using LantanaGroup.Link.Report.Settings; using LantanaGroup.Link.Shared.Application.Error.Exceptions; using LantanaGroup.Link.Shared.Application.Error.Handlers; using LantanaGroup.Link.Shared.Application.Error.Interfaces; @@ -25,6 +26,7 @@ public class ReportScheduledListener : BackgroundService private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; private readonly ISchedulerFactory _schedulerFactory; + private string Name => this.GetType().Name; public ReportScheduledListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IMediator mediator, ISchedulerFactory schedulerFactory, @@ -43,11 +45,11 @@ public ReportScheduledListener(ILogger logger, IKafkaCo throw new ArgumentException(nameof(_deadLetterExceptionHandler)); var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = "Report"; + t.ServiceName = ReportConstants.ServiceName; t.Topic = nameof(KafkaTopic.ReportScheduled) + "-Retry"; var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = "Report"; + d.ServiceName = ReportConstants.ServiceName; d.Topic = nameof(KafkaTopic.ReportScheduled) + "-Error"; } @@ -73,15 +75,14 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - var consumeResult = new ConsumeResult(); + ConsumeResult? consumeResult = null; try { consumeResult = consumer.Consume(cancellationToken); if (consumeResult == null) { - consumeResult = new ConsumeResult(); throw new DeadLetterException( - "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); + $"{Name}: consumeResult is null"); } var key = consumeResult.Message.Key; @@ -91,7 +92,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) string.IsNullOrWhiteSpace(key.ReportType)) { throw new DeadLetterException( - "ReportScheduledListener: One or more required MeasureReportScheduledKey properties are null or empty."); + $"{Name}: One or more required Key/Value properties are null or empty."); } DateTimeOffset startDateOffset; @@ -99,7 +100,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) value.Parameters.Single(x => x.Key.ToLower() == "startdate").Value, out startDateOffset)) { - throw new DeadLetterException("ReportScheduledListener: Start Date could not be parsed"); + throw new DeadLetterException($"{Name}: Start Date could not be parsed"); } DateTimeOffset endDateOffset; @@ -107,7 +108,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) value.Parameters.Single(x => x.Key.ToLower() == "enddate").Value, out endDateOffset)) { - throw new DeadLetterException("ReportScheduledListener: End Date could not be parsed"); + throw new DeadLetterException($"{Name}: End Date could not be parsed"); } var startDate = startDateOffset.UtcDateTime; @@ -186,31 +187,37 @@ await MeasureReportScheduleService.RescheduleJob(existing, }, cancellationToken); await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, - await _schedulerFactory.GetScheduler(cancellationToken)); - - - consumer.Commit(consumeResult); + await _schedulerFactory.GetScheduler(cancellationToken)); } } catch (ConsumeException ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); } catch (DeadLetterException ex) { - consumer.Commit(consumeResult); _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { _transientExceptionHandler.HandleException(consumeResult, ex); - consumer.Commit(consumeResult); } catch (Exception ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + } + finally + { + if (consumeResult != null) + { + consumer.Commit(consumeResult); + } + else + { + consumer.Commit(); + } } } } diff --git a/Report/Listeners/ReportSubmittedListener.cs b/Report/Listeners/ReportSubmittedListener.cs index b21f1680d..074208f4c 100644 --- a/Report/Listeners/ReportSubmittedListener.cs +++ b/Report/Listeners/ReportSubmittedListener.cs @@ -28,6 +28,8 @@ public class ReportSubmittedListener : BackgroundService private readonly ITransientExceptionHandler _transientExceptionHandler; private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; + private string Name => this.GetType().Name; + public ReportSubmittedListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IKafkaProducerFactory kafkaProducerFactory, IMediator mediator, ITransientExceptionHandler transientExceptionHandler, @@ -45,11 +47,11 @@ public ReportSubmittedListener(ILogger logger, IKafkaCo throw new ArgumentException(nameof(deadLetterExceptionHandler)); var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = "Report"; + t.ServiceName = ReportConstants.ServiceName; t.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Retry"; var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = "Report"; + d.ServiceName = ReportConstants.ServiceName; d.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Error"; } @@ -83,9 +85,8 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (consumeResult == null) { - consumeResult = new ConsumeResult(); throw new DeadLetterException( - "ReportSubmittedListener: Result of ConsumeResult.Consume is null"); + $"{Name}: consumeResult is null"); } ReportSubmittedKey key = consumeResult.Message.Key; @@ -97,72 +98,69 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (schedule is null) { throw new TransientException( - $"No report schedule found for submission bundle with ID {value.ReportBundleId}"); + $"{Name}: No report schedule found for submission bundle with ID {value.ReportBundleId}"); } // update report schedule with submitted date schedule.SubmittedDate = DateTime.UtcNow; await _mediator.Send(new UpdateMeasureReportScheduleCommand { ReportSchedule = schedule }); - consumer.Commit(consumeResult); - // produce audit message signalling the report service acknowledged the report has been submitted using var producer = _kafkaProducerFactory.CreateAuditEventProducer(); - try + + string notes = + $"{ReportConstants.ServiceName} has processed the {nameof(KafkaTopic.ReportSubmitted)} event for report bundle with ID {value.ReportBundleId} with report schedule ID {schedule.Id}"; + var val = new AuditEventMessage { - string notes = - $"{ReportConstants.ServiceName} has processed the {nameof(KafkaTopic.ReportSubmitted)} event for report bundle with ID {value.ReportBundleId} with report schedule ID {schedule.Id}"; - var val = new AuditEventMessage - { - FacilityId = schedule.FacilityId, - ServiceName = ReportConstants.ServiceName, - Action = AuditEventType.Submit, - EventDate = DateTime.UtcNow, - Resource = typeof(MeasureReportScheduleModel).Name, - Notes = notes - }; - var headers = new Headers + FacilityId = schedule.FacilityId, + ServiceName = ReportConstants.ServiceName, + Action = AuditEventType.Submit, + EventDate = DateTime.UtcNow, + Resource = typeof(MeasureReportScheduleModel).Name, + Notes = notes + }; + var headers = new Headers { { "X-Correlation-Id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) } }; - producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), - new Message - { - Value = val, - Headers = headers - }); - producer.Flush(); - _logger.LogInformation(notes); - } - catch (Exception ex) - { - throw new DeadLetterException("ReportSubmittedListener: " + ex.Message, ex.InnerException); - } + producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), + new Message + { + Value = val, + Headers = headers + }); + producer.Flush(); + _logger.LogInformation(notes); } - catch (ConsumeException e) + catch (ConsumeException ex) { - _logger.LogError($"Consumer error: {e.Error.Reason}"); - _logger.LogError(e.InnerException?.Message); - if (e.Error.IsFatal) - { - break; - } + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); } catch (DeadLetterException ex) { - consumer.Commit(consumeResult); _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { _transientExceptionHandler.HandleException(consumeResult, ex); - consumer.Commit(consumeResult); } catch (Exception ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportSubmittedListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + } + finally + { + if (consumeResult != null) + { + consumer.Commit(consumeResult); + } + else + { + consumer.Commit(); + } } } } diff --git a/Report/Program.cs b/Report/Program.cs index 9dd6043a8..7f31b0c7c 100644 --- a/Report/Program.cs +++ b/Report/Program.cs @@ -73,7 +73,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddTransient, KafkaProducerFactory>(); builder.Services.AddTransient, KafkaProducerFactory>(); - //Producers for Retry + //Producers for Retry/Deadletter builder.Services.AddTransient, KafkaProducerFactory>(); builder.Services.AddTransient, KafkaProducerFactory>(); builder.Services.AddTransient, KafkaProducerFactory>(); diff --git a/Submission/Listeners/SubmitReportListener.cs b/Submission/Listeners/SubmitReportListener.cs index 70c5e097d..9a9786d2b 100644 --- a/Submission/Listeners/SubmitReportListener.cs +++ b/Submission/Listeners/SubmitReportListener.cs @@ -24,6 +24,8 @@ public class SubmitReportListener : BackgroundService private readonly ITransientExceptionHandler _transientExceptionHandler; private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; + private string Name => this.GetType().Name; + public SubmitReportListener(ILogger logger, IKafkaConsumerFactory kafkaConsumerFactory, IMediator mediator, IOptions submissionConfig, IOptions fileSystemConfig, IHttpClientFactory httpClient, ITransientExceptionHandler transientExceptionHandler, @@ -78,9 +80,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) consumeResult = consumer.Consume(cancellationToken); if (consumeResult == null) { - consumeResult = new ConsumeResult(); - throw new DeadLetterException( - "SubmitReportListener: Result of ConsumeResult.Consume is null"); + throw new DeadLetterException($"{Name}: consumeResult is null"); } var key = consumeResult.Message.Key; @@ -91,7 +91,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) string.IsNullOrWhiteSpace(value.MeasureReportScheduleId)) { throw new DeadLetterException( - "SubmitReportListener: One or more required MeasureReportScheduledKey properties are null or empty."); + $"{Name}: One or more required Key/Value properties are null or empty."); } string requestUrl = _submissionConfig.ReportServiceUrl + $"?reportId={value.MeasureReportScheduleId}"; @@ -114,31 +114,38 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (!File.Exists(fullFilePath)) { - throw new TransientException("SubmitReportListener: Bundle File Not Created"); + throw new TransientException($"{Name}: Bundle File Not Created"); } #endregion - - consumer.Commit(consumeResult); } catch (ConsumeException ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); } catch (DeadLetterException ex) { - consumer.Commit(consumeResult); _deadLetterExceptionHandler.HandleException(consumeResult, ex); } catch (TransientException ex) { _transientExceptionHandler.HandleException(consumeResult, ex); - consumer.Commit(consumeResult); } catch (Exception ex) { - consumer.Commit(consumeResult); - _deadLetterExceptionHandler.HandleException(consumeResult, new DeadLetterException("ReportScheduledListener: " + ex.Message, ex.InnerException)); + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + } + finally + { + if (consumeResult != null) + { + consumer.Commit(consumeResult); + } + else + { + consumer.Commit(); + } } } } From 96b263df4dfe0c40133be642d9288201d69034c8 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Wed, 20 Mar 2024 15:20:39 -0500 Subject: [PATCH 20/79] Refactor To Shared Exceptions --- Report/Listeners/MeasureEvaluatedListener.cs | 20 ++++++++-------- Report/Listeners/PatientsToQueryListener.cs | 20 ++++++++-------- Report/Listeners/ReportScheduledListener.cs | 20 ++++++++-------- Report/Listeners/ReportSubmittedListener.cs | 24 ++++++++++---------- Submission/Listeners/SubmitReportListener.cs | 20 ++++++++-------- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Report/Listeners/MeasureEvaluatedListener.cs b/Report/Listeners/MeasureEvaluatedListener.cs index 233825496..b0736a8cc 100644 --- a/Report/Listeners/MeasureEvaluatedListener.cs +++ b/Report/Listeners/MeasureEvaluatedListener.cs @@ -47,13 +47,11 @@ public MeasureEvaluatedListener(ILogger logger, IKafka _transientExceptionHandler = transientExceptionHandler ?? throw new ArgumentException(nameof(transientExceptionHandler)); _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(deadLetterExceptionHandler)); - var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = ReportConstants.ServiceName; - t.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Retry"; + _transientExceptionHandler.ServiceName = ReportConstants.ServiceName; + _transientExceptionHandler.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Retry"; - var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = ReportConstants.ServiceName; - d.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Error"; + _deadLetterExceptionHandler.ServiceName = ReportConstants.ServiceName; + _deadLetterExceptionHandler.Topic = nameof(KafkaTopic.MeasureEvaluated) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -84,6 +82,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { ConsumeResult? consumeResult = null; + var facilityId = string.Empty; try { consumeResult = consumer.Consume(cancellationToken); @@ -95,6 +94,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) var key = consumeResult.Message.Key; var value = consumeResult.Message.Value; + facilityId = key.FacilityId; if (!consumeResult.Message.Headers.TryGetLastBytes("X-Correlation-Id", out var headerValue)) { @@ -195,20 +195,20 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } catch (DeadLetterException ex) { - _deadLetterExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (TransientException ex) { - _transientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/PatientsToQueryListener.cs b/Report/Listeners/PatientsToQueryListener.cs index a2e007ad3..22dbf46b2 100644 --- a/Report/Listeners/PatientsToQueryListener.cs +++ b/Report/Listeners/PatientsToQueryListener.cs @@ -35,13 +35,11 @@ public PatientsToQueryListener(ILogger logger, IKafkaCo _transientExceptionHandler = transientExceptionHandler ?? throw new ArgumentException(nameof(_transientExceptionHandler)); _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(_deadLetterExceptionHandler)); - var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = ReportConstants.ServiceName; - t.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Retry"; + _transientExceptionHandler.ServiceName = ReportConstants.ServiceName; + _transientExceptionHandler.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Retry"; - var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = ReportConstants.ServiceName; - d.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Error"; + _deadLetterExceptionHandler.ServiceName = ReportConstants.ServiceName; + _deadLetterExceptionHandler.Topic = nameof(KafkaTopic.PatientsToQuery) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -67,6 +65,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { ConsumeResult? consumeResult = null; + string facilityId = string.Empty; try { consumeResult = consumer.Consume(cancellationToken); @@ -78,6 +77,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) var key = consumeResult.Message.Key; var value = consumeResult.Message.Value; + facilityId = key; if (string.IsNullOrWhiteSpace(key)) { @@ -99,20 +99,20 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand() catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } catch (DeadLetterException ex) { - _deadLetterExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (TransientException ex) { - _transientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/ReportScheduledListener.cs b/Report/Listeners/ReportScheduledListener.cs index 8ddd50059..0c19f6d23 100644 --- a/Report/Listeners/ReportScheduledListener.cs +++ b/Report/Listeners/ReportScheduledListener.cs @@ -44,13 +44,11 @@ public ReportScheduledListener(ILogger logger, IKafkaCo _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(_deadLetterExceptionHandler)); - var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = ReportConstants.ServiceName; - t.Topic = nameof(KafkaTopic.ReportScheduled) + "-Retry"; + _transientExceptionHandler.ServiceName = ReportConstants.ServiceName; + _transientExceptionHandler.Topic = nameof(KafkaTopic.ReportScheduled) + "-Retry"; - var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = ReportConstants.ServiceName; - d.Topic = nameof(KafkaTopic.ReportScheduled) + "-Error"; + _deadLetterExceptionHandler.ServiceName = ReportConstants.ServiceName; + _deadLetterExceptionHandler.Topic = nameof(KafkaTopic.ReportScheduled) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -76,6 +74,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { ConsumeResult? consumeResult = null; + string facilityId = string.Empty; try { consumeResult = consumer.Consume(cancellationToken); @@ -87,6 +86,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) var key = consumeResult.Message.Key; var value = consumeResult.Message.Value; + facilityId = key.FacilityId; if (string.IsNullOrWhiteSpace(key.FacilityId) || string.IsNullOrWhiteSpace(key.ReportType)) @@ -193,20 +193,20 @@ await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } catch (DeadLetterException ex) { - _deadLetterExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (TransientException ex) { - _transientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/ReportSubmittedListener.cs b/Report/Listeners/ReportSubmittedListener.cs index 074208f4c..055b8a10d 100644 --- a/Report/Listeners/ReportSubmittedListener.cs +++ b/Report/Listeners/ReportSubmittedListener.cs @@ -46,13 +46,11 @@ public ReportSubmittedListener(ILogger logger, IKafkaCo _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(deadLetterExceptionHandler)); - var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = ReportConstants.ServiceName; - t.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Retry"; + _transientExceptionHandler.ServiceName = ReportConstants.ServiceName; + _transientExceptionHandler.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Retry"; - var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = ReportConstants.ServiceName; - d.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Error"; + _deadLetterExceptionHandler.ServiceName = ReportConstants.ServiceName; + _deadLetterExceptionHandler.Topic = nameof(KafkaTopic.ReportSubmitted) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -79,6 +77,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { var consumeResult = new ConsumeResult(); + string facilityId = string.Empty; try { consumeResult = consumer.Consume(cancellationToken); @@ -89,8 +88,9 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) $"{Name}: consumeResult is null"); } - ReportSubmittedKey key = consumeResult.Message.Key; - ReportSubmittedValue value = consumeResult.Message.Value; + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; + facilityId = key.FacilityId; // find existing report schedule MeasureReportScheduleModel schedule = await _mediator.Send(new GetMeasureReportScheduleByBundleIdQuery { ReportBundleId = value.ReportBundleId }); @@ -136,20 +136,20 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } catch (DeadLetterException ex) { - _deadLetterExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (TransientException ex) { - _transientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } finally { diff --git a/Submission/Listeners/SubmitReportListener.cs b/Submission/Listeners/SubmitReportListener.cs index 9a9786d2b..a225a8aee 100644 --- a/Submission/Listeners/SubmitReportListener.cs +++ b/Submission/Listeners/SubmitReportListener.cs @@ -43,13 +43,11 @@ public SubmitReportListener(ILogger logger, IKafkaConsumer _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentException(nameof(deadLetterExceptionHandler)); - var t = (TransientExceptionHandler)_transientExceptionHandler; - t.ServiceName = "Submission"; - t.Topic = nameof(KafkaTopic.SubmitReport) + "-Retry"; + _transientExceptionHandler.ServiceName = "Submission"; + _transientExceptionHandler.Topic = nameof(KafkaTopic.SubmitReport) + "-Retry"; - var d = (DeadLetterExceptionHandler)_deadLetterExceptionHandler; - d.ServiceName = "Submission"; - d.Topic = nameof(KafkaTopic.SubmitReport) + "-Error"; + _deadLetterExceptionHandler.ServiceName = "Submission"; + _deadLetterExceptionHandler.Topic = nameof(KafkaTopic.SubmitReport) + "-Error"; } protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -75,6 +73,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { var consumeResult = new ConsumeResult(); + string facilityId = string.Empty; try { consumeResult = consumer.Consume(cancellationToken); @@ -85,6 +84,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) var key = consumeResult.Message.Key; var value = consumeResult.Message.Value; + facilityId = key.FacilityId; if (string.IsNullOrWhiteSpace(key.FacilityId) || string.IsNullOrWhiteSpace(key.ReportType) || @@ -121,20 +121,20 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } catch (DeadLetterException ex) { - _deadLetterExceptionHandler.HandleException(consumeResult, ex); + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (TransientException ex) { - _transientExceptionHandler.HandleException(consumeResult, ex); + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); } catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException)); + new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); } finally { From 057aad682a3f145d31735b422f7bf9cccf59ba9f Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Wed, 20 Mar 2024 15:57:50 -0500 Subject: [PATCH 21/79] Submission program.cs services --- Submission/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Submission/Program.cs b/Submission/Program.cs index 9ac823d02..3ea33ae79 100644 --- a/Submission/Program.cs +++ b/Submission/Program.cs @@ -18,6 +18,7 @@ using HealthChecks.UI.Client; using LantanaGroup.Link.Shared.Application.Error.Handlers; using LantanaGroup.Link.Shared.Application.Error.Interfaces; +using LantanaGroup.Link.Shared.Application.Models.Kafka; var builder = WebApplication.CreateBuilder(args); @@ -57,6 +58,8 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient, KafkaProducerFactory>(); + builder.Services.AddTransient, KafkaProducerFactory>(); // Add Controllers builder.Services.AddControllers(); From a1a46ff39bd349de9c1d68d658c2c0aeeecc8997 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Wed, 20 Mar 2024 15:59:18 -0500 Subject: [PATCH 22/79] move to factories section --- Submission/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Submission/Program.cs b/Submission/Program.cs index 3ea33ae79..a17c3a2e4 100644 --- a/Submission/Program.cs +++ b/Submission/Program.cs @@ -58,8 +58,6 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); - builder.Services.AddTransient, KafkaProducerFactory>(); - builder.Services.AddTransient, KafkaProducerFactory>(); // Add Controllers builder.Services.AddControllers(); @@ -78,6 +76,8 @@ static void RegisterServices(WebApplicationBuilder builder) // Add factories builder.Services.AddTransient, KafkaConsumerFactory>(); + builder.Services.AddTransient, KafkaProducerFactory>(); + builder.Services.AddTransient, KafkaProducerFactory>(); // Add repositories // TODO From 13552ed8c65eb5fa150b84a600dfdd5db4261698 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Wed, 20 Mar 2024 21:16:57 -0400 Subject: [PATCH 23/79] Added minimal api auth endpoints --- .../Presentation/Endpoints/AuthEndpoints.cs | 59 +++++++++++++++++++ LinkAdmin.BFF/Program.cs | 9 +-- 2 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs new file mode 100644 index 000000000..b119434d7 --- /dev/null +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -0,0 +1,59 @@ + +using Microsoft.OpenApi.Models; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints +{ + public static class AuthEndpoints + { + public static void RegisterAuthEndpoints(this WebApplication app) + { + var authEndpoints = app.MapGroup("/") + .WithOpenApi(x => new OpenApiOperation(x) + { + Tags = new List { new() { Name = "Auth" } } + }); + + authEndpoints.MapGet("/login", Login) + .AllowAnonymous() + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Login to Link", + Description = "Initiates the login process for link" + }); + + authEndpoints.MapGet("/user", GetUser) + .RequireAuthorization() + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Get user information", + Description = "Retrieves information about the current logged in user" + }); + + authEndpoints.MapGet("/logout", Logout) + .RequireAuthorization() + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Logout of Link", + Description = "Initiates the logout process for link" + }); + + } + + static async Task Login() + { + return Results.Ok(new { Message = "Login" }); + } + + static async Task GetUser() + { + return Results.Ok(new { Message = "Get User" }); + } + + static async Task Logout() + { + return Results.Ok(new { Message = "Logout" }); + } + + + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 3313e3a24..dd562c3c3 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -3,6 +3,7 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; +using LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration.AzureAppConfiguration; @@ -163,13 +164,7 @@ static void SetupMiddleware(WebApplication app) //app.UseMiddleware(); //app.UseAuthorization(); - app.MapGet("/api/user", () => - { - return Results.Ok(new { Name = "John Doe" }); - }) - .WithName("GetUserInfomration") - .RequireCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName) - .WithOpenApi(); + app.RegisterAuthEndpoints(); app.MapReverseProxy(); From 52b42d99f5201760ec4ff0b1e664216d2f290372 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 08:35:41 -0400 Subject: [PATCH 24/79] Added logging infrastructure, added api interface for minimal apis registration. --- LinkAdmin.BFF/Application/Interfaces/IApi.cs | 7 ++++ .../Extensions/RedactionServiceExtension.cs | 39 +++++++++++++++++++ .../Infrastructure/Logging/DataTaxonomy.cs | 35 +++++++++++++++++ .../Infrastructure/Logging/Logging.cs | 31 +++++++++++++++ .../Infrastructure/Logging/StarRedactor.cs | 18 +++++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 2 + .../Presentation/Endpoints/AuthEndpoints.cs | 22 ++++++++--- LinkAdmin.BFF/Program.cs | 21 +++++++++- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 3 +- 9 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Interfaces/IApi.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/RedactionServiceExtension.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Logging/DataTaxonomy.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Logging/Logging.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Logging/StarRedactor.cs diff --git a/LinkAdmin.BFF/Application/Interfaces/IApi.cs b/LinkAdmin.BFF/Application/Interfaces/IApi.cs new file mode 100644 index 000000000..121d314bc --- /dev/null +++ b/LinkAdmin.BFF/Application/Interfaces/IApi.cs @@ -0,0 +1,7 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces +{ + public interface IApi + { + void RegisterEndpoints(WebApplication app); + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/RedactionServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/RedactionServiceExtension.cs new file mode 100644 index 000000000..0c4566fcc --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/RedactionServiceExtension.cs @@ -0,0 +1,39 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using System.Text; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class RedactionServiceExtension + { + + public static IServiceCollection AddRedactionService(this IServiceCollection services, Action? options = null) + { + var redactionServiceOptions = new RedactionServiceOptions(); + options?.Invoke(redactionServiceOptions); + + services.AddRedaction(x => { + + x.SetRedactor(new DataClassificationSet(DataTaxonomy.SensitiveData)); + + + if (!string.IsNullOrEmpty(redactionServiceOptions.HmacKey)) + { + x.SetHmacRedactor(opts => { + opts.Key = Convert.ToBase64String(Encoding.UTF8.GetBytes(redactionServiceOptions.HmacKey)); + opts.KeyId = 808; + }, new DataClassificationSet(DataTaxonomy.PiiData)); + } + }); + + return services; + } + + public class RedactionServiceOptions + { + public string? HmacKey { get; set; } = null; + } + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Logging/DataTaxonomy.cs b/LinkAdmin.BFF/Infrastructure/Logging/DataTaxonomy.cs new file mode 100644 index 000000000..3da0167b7 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Logging/DataTaxonomy.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Compliance.Classification; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging +{ + public static class DataTaxonomy + { + public static string TaxonomyName { get; } = typeof(DataTaxonomy).FullName!; + + public static DataClassification SensitiveData { get; } = new(TaxonomyName, nameof(SensitiveData)); + public static DataClassification PiiData { get; } = new(TaxonomyName, nameof(PiiData)); + public static DataClassification PhiData { get; } = new(TaxonomyName, nameof(PhiData)); + } + + public class SensitiveDataAttribute : DataClassificationAttribute + { + public SensitiveDataAttribute() : base(DataTaxonomy.SensitiveData) + { + } + } + + //prefix key = 808 + public class PiiDataAttribute : DataClassificationAttribute + { + public PiiDataAttribute() : base(DataTaxonomy.PiiData) + { + } + } + + public class PhiDataAttribute : DataClassificationAttribute + { + public PhiDataAttribute() : base(DataTaxonomy.PhiData) + { + } + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs new file mode 100644 index 000000000..f206e6f45 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs @@ -0,0 +1,31 @@ +using static LantanaGroup.Link.LinkAdmin.BFF.Settings.LinkAdminConstants; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging +{ + public static partial class Logging + { + [LoggerMessage( + LinkAdminLoggingIds.ApiRegistered, + LogLevel.Information, + "API {api} registered.")] + public static partial void LogApiRegistration(this ILogger logger, string api); + + [LoggerMessage( + LinkAdminLoggingIds.RequestRecieved, + LogLevel.Information, + "Request Recieved for {api} at {timestamp}.")] + public static partial void LogRequestRecieved(this ILogger logger, string api, DateTime timestamp); + + [LoggerMessage( + LinkAdminLoggingIds.RequestRecievedWarning, + LogLevel.Warning, + "Request Recieved for {api} at {timestamp}.")] + public static partial void LogRequestRecievedWarning(this ILogger logger, string api, DateTime timestamp); + + [LoggerMessage( + LinkAdminLoggingIds.RequestRecievedException, + LogLevel.Critical, + "Request Recieved for {api} at {timestamp}.")] + public static partial void LogRequestRecievedException(this ILogger logger, string api, DateTime timestamp); + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Logging/StarRedactor.cs b/LinkAdmin.BFF/Infrastructure/Logging/StarRedactor.cs new file mode 100644 index 000000000..c5cfbed9e --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Logging/StarRedactor.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Compliance.Redaction; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging +{ + public class StarRedactor : Redactor + { + public override int GetRedactedLength(ReadOnlySpan input) + { + return input.Length; + } + + public override int Redact(ReadOnlySpan source, Span destination) + { + destination.Fill('*'); + return destination.Length; + } + } +} diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index ea9c5348f..ca215e874 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -19,7 +19,9 @@ + + diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index b119434d7..b05429852 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -1,11 +1,19 @@  +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using Microsoft.OpenApi.Models; namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints { - public static class AuthEndpoints + public class AuthEndpoints : IApi { - public static void RegisterAuthEndpoints(this WebApplication app) + private readonly ILogger _logger; + + public AuthEndpoints(ILogger logger) + { + _logger = logger; + } + + public void RegisterEndpoints(WebApplication app) { var authEndpoints = app.MapGroup("/") .WithOpenApi(x => new OpenApiOperation(x) @@ -30,26 +38,28 @@ public static void RegisterAuthEndpoints(this WebApplication app) }); authEndpoints.MapGet("/logout", Logout) - .RequireAuthorization() + .RequireAuthorization() .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Logout of Link", Description = "Initiates the logout process for link" }); + _logger.LogInformation("Auth Endpoints Registered"); + } - static async Task Login() + public async Task Login() { return Results.Ok(new { Message = "Login" }); } - static async Task GetUser() + public async Task GetUser() { return Results.Ok(new { Message = "Get User" }); } - static async Task Logout() + public async Task Logout() { return Results.Ok(new { Message = "Logout" }); } diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index dd562c3c3..c0392e1f5 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,5 +1,6 @@ using Azure.Identity; using HealthChecks.UI.Client; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; @@ -16,6 +17,7 @@ var builder = WebApplication.CreateBuilder(args); RegisterServices(builder); + var app = builder.Build(); SetupMiddleware(app); @@ -66,6 +68,9 @@ static void RegisterServices(WebApplicationBuilder builder) options.IncludeExceptionDetails = builder.Configuration.GetValue("ProblemDetails:IncludeExceptionDetails"); }); + //Add APIs + builder.Services.AddTransient(); + //Add YARP (reverse proxy) builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); @@ -103,6 +108,12 @@ static void RegisterServices(WebApplicationBuilder builder) c.IncludeXmlComments(xmlPath); }); + // Add logging redaction services + builder.Services.AddRedactionService(options => + { + options.HmacKey = builder.Configuration.GetValue("Logging:HmacKey"); + }); + // Logging using Serilog builder.Logging.AddSerilog(); var loggerOptions = new ConfigurationReaderOptions { SectionName = LinkAdminConstants.AppSettingsSectionNames.Serilog }; @@ -162,9 +173,15 @@ static void SetupMiddleware(WebApplication app) app.UseCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName); //app.UseAuthentication(); //app.UseMiddleware(); - //app.UseAuthorization(); + //app.UseAuthorization(); - app.RegisterAuthEndpoints(); + //register endpoints + var apis = app.Services.GetServices(); + foreach (var api in apis) + { + if(api is null) throw new InvalidProgramException("API was not found."); + api.RegisterEndpoints(app); + } app.MapReverseProxy(); diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 760f01286..d0a385444 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -13,11 +13,12 @@ public static class AppSettingsSectionNames public const string EnableSwagger = "EnableSwagger"; } - public static class AuditLoggingIds + public static class LinkAdminLoggingIds { public const int RequestRecieved = 1000; public const int RequestRecievedWarning = 1001; public const int RequestRecievedException = 1002; + public const int ApiRegistered = 1003; } } } From f79517d611ec300174f74998f9034e30e15c9718 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 13:20:37 -0400 Subject: [PATCH 25/79] - Added the Shared NuGet package - Added integration endpoints - Added endpoint to generate a PatientEvent --- .../CreatePatientEvent/CreatePatientEvent.cs | 72 +++++++++++++++++++ .../CreatePatientEvent/ICreatePatientEvent.cs | 9 +++ .../Interfaces/Models/IPatientEvent.cs | 9 +++ .../Models/Integration/PatientEvent.cs | 28 ++++++++ .../Infrastructure/Logging/Logging.cs | 19 +++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 8 +++ .../Endpoints/IntegrationTestingEndpoints.cs | 45 ++++++++++++ LinkAdmin.BFF/Program.cs | 18 ++++- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 4 ++ LinkAdmin.BFF/appsettings.Development.json | 3 + LinkAdmin.BFF/appsettings.json | 3 + 11 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs create mode 100644 LinkAdmin.BFF/Application/Interfaces/Models/IPatientEvent.cs create mode 100644 LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs create mode 100644 LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs new file mode 100644 index 000000000..1e3d0f615 --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs @@ -0,0 +1,72 @@ +using Confluent.Kafka; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging; +using LantanaGroup.Link.Shared.Application.Interfaces; +using LantanaGroup.Link.Shared.Application.Models; +using System.Diagnostics; +using System.Text; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent +{ + public class CreatePatientEvent : ICreatePatientEvent + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public CreatePatientEvent(ILogger logger, IServiceScopeFactory scopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + } + + public async Task Execute(PatientEvent model) + { + using Activity? activity = ServiceActivitySource.Instance.StartActivity("Producing Patient Event"); + using var scope = _scopeFactory.CreateScope(); + var _kafkaProducerFactory = scope.ServiceProvider.GetRequiredService>(); + + string correlationId = Guid.NewGuid().ToString(); + + try + { + var producerConfig = new ProducerConfig(); + + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + { + try + { + var headers = new Headers(); + headers.Add("X-Correlation-Id", Encoding.ASCII.GetBytes(correlationId)); + + var message = new Message + { + Key = model.Key, + Value = new PatientEventMessage { PatientId = model.PatientId, EventType = model.EventType }, + Headers = headers + }; + + await producer.ProduceAsync(nameof(KafkaTopic.PatientEvent), message); + _logger.LogKafkaProducerPatientEvent(correlationId); + + return correlationId; + + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(nameof(KafkaTopic.PatientEvent), ex.Message); + throw; + } + + } + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(nameof(KafkaTopic.PatientEvent), ex.Message); + throw; + } + } + } +} diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs new file mode 100644 index 000000000..ed5412af8 --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs @@ -0,0 +1,9 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent +{ + public interface ICreatePatientEvent + { + Task Execute(PatientEvent model); + } +} diff --git a/LinkAdmin.BFF/Application/Interfaces/Models/IPatientEvent.cs b/LinkAdmin.BFF/Application/Interfaces/Models/IPatientEvent.cs new file mode 100644 index 000000000..cd4820d59 --- /dev/null +++ b/LinkAdmin.BFF/Application/Interfaces/Models/IPatientEvent.cs @@ -0,0 +1,9 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models +{ + public interface IPatientEvent + { + string Key { get; set; } + string PatientId { get; set; } + string EventType { get; set; } + } +} diff --git a/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs b/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs new file mode 100644 index 000000000..4fa5c9004 --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs @@ -0,0 +1,28 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration +{ + public class PatientEvent : IPatientEvent + { + /// + /// Key for the patient event (FacilityId) + /// + public string Key { get; set; } = string.Empty; + + /// + /// The id of the patient subject to the event + /// + public string PatientId { get; set; } = string.Empty; + + /// + /// The type of event that occurred + /// + public string EventType { get; set; } = string.Empty; + } + + public class PatientEventMessage + { + public string PatientId { get; set; } = string.Empty; + public string EventType { get; set; } = string.Empty; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs index f206e6f45..e9c050018 100644 --- a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs +++ b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs @@ -27,5 +27,24 @@ public static partial class Logging LogLevel.Critical, "Request Recieved for {api} at {timestamp}.")] public static partial void LogRequestRecievedException(this ILogger logger, string api, DateTime timestamp); + + [LoggerMessage( + LinkAdminLoggingIds.KafkaProducerCreated, + LogLevel.Information, + "Kafka Producer for {topic} was created at {timestamp}.")] + public static partial void LogKafkaProducerCreation(this ILogger logger, string topic, DateTime timestamp); + + [LoggerMessage( + LinkAdminLoggingIds.KafkaProducerException, + LogLevel.Critical, + "An exception occured in the Kafka Producer for {topic}: {exception}")] + public static partial void LogKafkaProducerException(this ILogger logger, string topic, string exception); + + [LoggerMessage( + LinkAdminLoggingIds.KafkaProducerPatientEvent, + LogLevel.Information, + "New Patient Event with a correlation id of {correlationId} was created.")] + public static partial void LogKafkaProducerPatientEvent(this ILogger logger, string correlationId); + } } diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index ca215e874..c49dc682c 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -18,6 +18,9 @@ + + + @@ -42,4 +45,9 @@ + + + + + diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs new file mode 100644 index 000000000..9313d71c8 --- /dev/null +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -0,0 +1,45 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; +using Microsoft.OpenApi.Models; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints +{ + public class IntegrationTestingEndpoints : IApi + { + private readonly ILogger _logger; + private readonly ICreatePatientEvent _createPatientEvent; + + public IntegrationTestingEndpoints(ILogger logger, ICreatePatientEvent createPatientEvent) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _createPatientEvent = createPatientEvent; + } + + public void RegisterEndpoints(WebApplication app) + { + var integrationEndpoints = app.MapGroup("/api/integration") + .WithOpenApi(x => new OpenApiOperation(x) + { + Tags = new List { new() { Name = "Integration" } } + }); + + integrationEndpoints.MapPost("/create-patient-event", CreatePatientEvent) + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Integration Testing - Produce Patient Event", + Description = "Produces a new patient event that will be sent to the broker. Allows for testing processes outside of scheduled events." + }); + + } + + public async Task CreatePatientEvent(HttpContext context, PatientEvent model) + { + var correlationId = await _createPatientEvent.Execute(model); + return Results.Ok(new { + Id = correlationId, + Message = $"The patient event was created succcessfully with a correlation id of '{correlationId}'." + }); + } + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index c0392e1f5..4166b4e37 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,11 +1,16 @@ using Azure.Identity; using HealthChecks.UI.Client; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints; using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using LantanaGroup.Link.Shared.Application.Factories; +using LantanaGroup.Link.Shared.Application.Interfaces; +using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Serilog; @@ -68,8 +73,18 @@ static void RegisterServices(WebApplicationBuilder builder) options.IncludeExceptionDetails = builder.Configuration.GetValue("ProblemDetails:IncludeExceptionDetails"); }); - //Add APIs + //Add IOptions + builder.Services.Configure(builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Kafka)); + + //Add Kafka Producer Factories + builder.Services.AddSingleton, KafkaProducerFactory>(); + + //Add Endpoints builder.Services.AddTransient(); + builder.Services.AddTransient(); + + //Add commands + builder.Services.AddTransient(); //Add YARP (reverse proxy) builder.Services.AddReverseProxy() @@ -97,7 +112,6 @@ static void RegisterServices(WebApplicationBuilder builder) throw new NullReferenceException("CORS Configuration was null."); } - // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index d0a385444..19e2a3f01 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -10,6 +10,7 @@ public static class AppSettingsSectionNames public const string CORS = "CORS"; public const string Telemetry = "TelemetryConfig"; public const string Serilog = "Logging:Serilog"; + public const string Kafka = "KafkaConnection"; public const string EnableSwagger = "EnableSwagger"; } @@ -19,6 +20,9 @@ public static class LinkAdminLoggingIds public const int RequestRecievedWarning = 1001; public const int RequestRecievedException = 1002; public const int ApiRegistered = 1003; + public const int KafkaProducerCreated = 1004; + public const int KafkaProducerException = 1005; + public const int KafkaProducerPatientEvent = 1006; } } } diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index 5c83a32c2..0b014f32f 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -4,6 +4,9 @@ "Name": "Link Admin BFF", "Version": "0.1.0" }, + "KafkaConnection": { + "BootstrapServers": [ ] + }, "EnableSwagger": true, "Logging": { "LogLevel": { diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index bb17c193e..84525f95a 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -4,6 +4,9 @@ "Name": "Link Admin BFF", "Version": "0.1.0" }, + "KafkaConnection": { + "BootstrapServers": [ "localhost:9092/" ] + }, "CORS": { "PolicyName": "LinkAdminCorsPolicy", "AllowedOrigins": [ "https://localhost:7007", "http://localhost:5005" ], From b90584f60af79d3dc7b1f99c239e7aec5c88b8e0 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 13:35:39 -0400 Subject: [PATCH 26/79] Fixed serilog reference --- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 19e2a3f01..7b8f6164d 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -9,7 +9,7 @@ public static class AppSettingsSectionNames public const string IdentityProvider = "IdentityProviderConfig"; public const string CORS = "CORS"; public const string Telemetry = "TelemetryConfig"; - public const string Serilog = "Logging:Serilog"; + public const string Serilog = "Serilog"; public const string Kafka = "KafkaConnection"; public const string EnableSwagger = "EnableSwagger"; } From ddb1db4214ed49b8780075fcf5a34cf7b9f4dddf Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 14:49:07 -0400 Subject: [PATCH 27/79] Added crate report scheduled endpoint and methods. Added userId as a nullable paramter to the create event commands. --- .../CreatePatientEvent/CreatePatientEvent.cs | 2 +- .../CreatePatientEvent/ICreatePatientEvent.cs | 2 +- .../CreateReportScheduled.cs | 94 +++++++++++++++++++ .../ICreateReportScheduled.cs | 9 ++ .../Interfaces/Models/IReportScheduled.cs | 10 ++ .../Models/Integration/ReportScheduled.cs | 43 +++++++++ .../Infrastructure/Logging/Logging.cs | 6 ++ .../Endpoints/IntegrationTestingEndpoints.cs | 26 ++++- LinkAdmin.BFF/Program.cs | 3 +- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 1 + 10 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs create mode 100644 LinkAdmin.BFF/Application/Interfaces/Models/IReportScheduled.cs create mode 100644 LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs index 1e3d0f615..5616b7dac 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs @@ -20,7 +20,7 @@ public CreatePatientEvent(ILogger logger, IServiceScopeFacto _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); } - public async Task Execute(PatientEvent model) + public async Task Execute(PatientEvent model, string? userId = null) { using Activity? activity = ServiceActivitySource.Instance.StartActivity("Producing Patient Event"); using var scope = _scopeFactory.CreateScope(); diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs index ed5412af8..63949306c 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs @@ -4,6 +4,6 @@ namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.Creat { public interface ICreatePatientEvent { - Task Execute(PatientEvent model); + Task Execute(PatientEvent model, string? userId = null); } } diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs new file mode 100644 index 000000000..2f59a5b62 --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs @@ -0,0 +1,94 @@ +using Confluent.Kafka; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging; +using LantanaGroup.Link.Shared.Application.Interfaces; +using LantanaGroup.Link.Shared.Application.Models; +using System.Diagnostics; +using System.Text.Json; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled +{ + public class CreateReportScheduled : ICreateReportScheduled + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public CreateReportScheduled(ILogger logger, IServiceScopeFactory scopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + } + + public async Task Execute(ReportScheduled model, string? userId = null) + { + using Activity? activity = ServiceActivitySource.Instance.StartActivity("Producing Report Scheduled Event"); + using var scope = _scopeFactory.CreateScope(); + var _kafkaProducerFactory = scope.ServiceProvider.GetRequiredService>(); + + string correlationId = Guid.NewGuid().ToString(); + + List> parameters = []; + if (model.StartDate is not null && model.EndDate is not null) + { + parameters.Add(new KeyValuePair("StartDate", model.StartDate)); + parameters.Add(new KeyValuePair("EndDate", model.EndDate)); + } + else + { + throw new ArgumentNullException("Start and End date for report period cannot be null"); + } + + try + { + var producerConfig = new ProducerConfig(); + + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + { + try + { + var headers = new Headers(); + headers.Add("X-Correlation-Id", System.Text.Encoding.ASCII.GetBytes(correlationId)); + + ReportScheduledKey Key = new ReportScheduledKey() + { + FacilityId = model.FacilityId, + ReportType = model.ReportType + }; + + var message = new Message + { + Key = JsonSerializer.Serialize(Key), + Headers = headers, + Value = new ReportScheduledMessage() + { + Parameters = parameters + }, + }; + + await producer.ProduceAsync(nameof(KafkaTopic.ReportScheduled), message); + + _logger.LogInformation($"New Report Scheduled ({correlationId}) created."); + return correlationId; + + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(correlationId, ex.Message); + throw; + } + + } + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(nameof(KafkaTopic.PatientEvent), ex.Message); + throw; + } + + + } + } +} diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs new file mode 100644 index 000000000..7ad82c6f7 --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs @@ -0,0 +1,9 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled +{ + public interface ICreateReportScheduled + { + Task Execute(ReportScheduled model, string? userId = null); + } +} diff --git a/LinkAdmin.BFF/Application/Interfaces/Models/IReportScheduled.cs b/LinkAdmin.BFF/Application/Interfaces/Models/IReportScheduled.cs new file mode 100644 index 000000000..ce73c4d77 --- /dev/null +++ b/LinkAdmin.BFF/Application/Interfaces/Models/IReportScheduled.cs @@ -0,0 +1,10 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models +{ + public interface IReportScheduled + { + string FacilityId { get; set; } + string ReportType { get; set; } + DateTime? StartDate { get; set; } + DateTime? EndDate { get; set; } + } +} diff --git a/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs new file mode 100644 index 000000000..e434c2fde --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs @@ -0,0 +1,43 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration +{ + public class ReportScheduled : IReportScheduled + { + /// + /// The facility id for the report + /// + public string FacilityId { get; set; } = string.Empty; + + /// + /// The type of measure report to be generated + /// + public string ReportType { get; set; } = string.Empty; + + /// + /// The start date for the report period + /// + public DateTime? StartDate { get; set; } + + /// + /// The end date for the report period + /// + public DateTime? EndDate { get; set; } + } + + public class ReportScheduledMessage + { + public List>? Parameters { get; set; } + } + + public class ReportScheduledKey + { + public string? FacilityId { get; set; } + public string? ReportType { get; set; } + + public static implicit operator string(ReportScheduledKey v) + { + throw new NotImplementedException(); + } + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs index e9c050018..c1fb52594 100644 --- a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs +++ b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs @@ -46,5 +46,11 @@ public static partial class Logging "New Patient Event with a correlation id of {correlationId} was created.")] public static partial void LogKafkaProducerPatientEvent(this ILogger logger, string correlationId); + [LoggerMessage( + LinkAdminLoggingIds.KafkaProducerReportScheduled, + LogLevel.Information, + "New Report Scheduled event with a correlation id of {correlationId} was created.")] + public static partial void LogKafkaProducerReportScheduled(this ILogger logger, string correlationId); + } } diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs index 9313d71c8..07b3dd7ec 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -1,4 +1,5 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; using Microsoft.OpenApi.Models; @@ -9,11 +10,13 @@ public class IntegrationTestingEndpoints : IApi { private readonly ILogger _logger; private readonly ICreatePatientEvent _createPatientEvent; + private readonly ICreateReportScheduled _createReportScheduled; - public IntegrationTestingEndpoints(ILogger logger, ICreatePatientEvent createPatientEvent) + public IntegrationTestingEndpoints(ILogger logger, ICreatePatientEvent createPatientEvent, ICreateReportScheduled createReportScheduled) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _createPatientEvent = createPatientEvent; + _createPatientEvent = createPatientEvent ?? throw new ArgumentNullException(nameof(createPatientEvent)); + _createReportScheduled = createReportScheduled ?? throw new ArgumentNullException(nameof(createReportScheduled)); } public void RegisterEndpoints(WebApplication app) @@ -24,13 +27,20 @@ public void RegisterEndpoints(WebApplication app) Tags = new List { new() { Name = "Integration" } } }); - integrationEndpoints.MapPost("/create-patient-event", CreatePatientEvent) + integrationEndpoints.MapPost("/patient-event", CreatePatientEvent) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Patient Event", Description = "Produces a new patient event that will be sent to the broker. Allows for testing processes outside of scheduled events." }); + integrationEndpoints.MapPost("/report-scheduled", CreateReportScheduled) + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Integration Testing - Produce Report Scheduled Event", + Description = "Produces a new report scheduled event that will be sent to the broker. Allows for testing processes outside of scheduled events." + }); + } public async Task CreatePatientEvent(HttpContext context, PatientEvent model) @@ -41,5 +51,15 @@ public async Task CreatePatientEvent(HttpContext context, PatientEvent Message = $"The patient event was created succcessfully with a correlation id of '{correlationId}'." }); } + + public async Task CreateReportScheduled(HttpContext context, ReportScheduled model) + { + var correlationId = await _createReportScheduled.Execute(model); + return Results.Ok(new + { + Id = correlationId, + Message = $"The report scheduled event was created succcessfully with a correlation id of '{correlationId}'." + }); + } } } diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 4166b4e37..201552177 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,9 +1,9 @@ using Azure.Identity; using HealthChecks.UI.Client; using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; -using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints; @@ -85,6 +85,7 @@ static void RegisterServices(WebApplicationBuilder builder) //Add commands builder.Services.AddTransient(); + builder.Services.AddTransient(); //Add YARP (reverse proxy) builder.Services.AddReverseProxy() diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 7b8f6164d..3dd026da2 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -23,6 +23,7 @@ public static class LinkAdminLoggingIds public const int KafkaProducerCreated = 1004; public const int KafkaProducerException = 1005; public const int KafkaProducerPatientEvent = 1006; + public const int KafkaProducerReportScheduled = 1007; } } } From b6757960a47cbecf1c87f154b7f0499274c27399 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Thu, 21 Mar 2024 14:25:57 -0500 Subject: [PATCH 28/79] Update Shared Exception objects to handle AuditEventType and provide nonexception overloads --- .../Error/Exceptions/DeadLetterException.cs | 17 +++++-- .../Error/Exceptions/TransientException.cs | 17 +++++-- .../Handlers/DeadLetterExceptionHandler.cs | 46 +++++++++++++++--- .../Handlers/TransientExceptionHandler.cs | 47 ++++++++++++++++--- .../Interfaces/IDeadLetterExceptionHandler.cs | 5 +- .../Interfaces/ITransientExceptionHandler.cs | 5 +- 6 files changed, 115 insertions(+), 22 deletions(-) diff --git a/Shared/Application/Error/Exceptions/DeadLetterException.cs b/Shared/Application/Error/Exceptions/DeadLetterException.cs index 016d5ec96..8793cfd8c 100644 --- a/Shared/Application/Error/Exceptions/DeadLetterException.cs +++ b/Shared/Application/Error/Exceptions/DeadLetterException.cs @@ -1,8 +1,19 @@ -namespace LantanaGroup.Link.Shared.Application.Error.Exceptions +using LantanaGroup.Link.Shared.Application.Models; + +namespace LantanaGroup.Link.Shared.Application.Error.Exceptions { public class DeadLetterException : Exception { - public DeadLetterException(string message) : base(message) { } - public DeadLetterException(string message, Exception? innerEx) : base(message, innerEx) { } + public AuditEventType AuditEventType { get; set; } + + public DeadLetterException(string message, AuditEventType auditEventType) : base(message) + { + AuditEventType = auditEventType; + } + + public DeadLetterException(string message, AuditEventType auditEventType, Exception? innerEx) : base(message, innerEx) + { + AuditEventType = auditEventType; + } } } diff --git a/Shared/Application/Error/Exceptions/TransientException.cs b/Shared/Application/Error/Exceptions/TransientException.cs index 05a80ff08..65800e16b 100644 --- a/Shared/Application/Error/Exceptions/TransientException.cs +++ b/Shared/Application/Error/Exceptions/TransientException.cs @@ -1,8 +1,19 @@ -namespace LantanaGroup.Link.Shared.Application.Error.Exceptions +using LantanaGroup.Link.Shared.Application.Models; + +namespace LantanaGroup.Link.Shared.Application.Error.Exceptions { public class TransientException : Exception { - public TransientException(string message) : base(message) { } - public TransientException(string message, Exception? innerEx) : base(message, innerEx) { } + public AuditEventType AuditEventType { get; set; } + + public TransientException(string message, AuditEventType auditEventType) : base(message) + { + AuditEventType = auditEventType; + } + + public TransientException(string message, AuditEventType auditEventType, Exception? innerEx) : base(message, innerEx) + { + AuditEventType = auditEventType; + } } } diff --git a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs index 22d8c0d3a..22f34796c 100644 --- a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs @@ -5,6 +5,7 @@ using LantanaGroup.Link.Shared.Application.Models.Kafka; using Microsoft.Extensions.Logging; using System.Text; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; namespace LantanaGroup.Link.Shared.Application.Error.Handlers { @@ -27,25 +28,56 @@ public DeadLetterExceptionHandler(ILogger> logg ProducerFactory = producerFactory; } - public virtual void HandleException(ConsumeResult consumeResult, Exception ex, string facilityId) + public void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = "") { try { if (consumeResult == null) { - Logger.LogError(message: $"DeadLetterExceptionHandler|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or DeadLetter events", exception: ex); + Logger.LogError($"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or DeadLetter events: " + message); return; } - Logger.LogError(message: $"DeadLetterExceptionHandler: Failed to process {ServiceName} Event.", exception: ex); + Logger.LogError($"{GetType().Name}: Failed to process {ServiceName} Event: " + message); var auditValue = new AuditEventMessage { FacilityId = facilityId, - Action = AuditEventType.Query, + Action = auditEventType, ServiceName = ServiceName, EventDate = DateTime.UtcNow, - Notes = $"DeadLetterExceptionHandler: processing failure in {ServiceName} \nException Message: {ex.Message}", + Notes = $"{GetType().Name}: processing failure in {ServiceName} \nException Message: {message}", + }; + + ProduceAuditEvent(auditValue, consumeResult.Message.Headers); + ProduceDeadLetter(consumeResult.Message.Key, consumeResult.Message.Value, consumeResult.Message.Headers, message); + } + catch (Exception e) + { + Logger.LogError(e, $"Error in {GetType().Name}.HandleException: " + e.Message); + throw; + } + } + + public virtual void HandleException(ConsumeResult consumeResult, DeadLetterException ex, string facilityId) + { + try + { + if (consumeResult == null) + { + Logger.LogError(message: $"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or DeadLetter events", exception: ex); + return; + } + + Logger.LogError(message: $"{GetType().Name}: Failed to process {ServiceName} Event.", exception: ex); + + var auditValue = new AuditEventMessage + { + FacilityId = facilityId, + Action = ex.AuditEventType, + ServiceName = ServiceName, + EventDate = DateTime.UtcNow, + Notes = $"{GetType().Name}: processing failure in {ServiceName} \nException Message: {ex.Message}", }; ProduceAuditEvent(auditValue, consumeResult.Message.Headers); @@ -53,7 +85,7 @@ public virtual void HandleException(ConsumeResult consumeResult, Exception } catch (Exception e) { - Logger.LogError(e,"Error in DeadLetterExceptionHandler.HandleException: " + e.Message); + Logger.LogError(e, $"Error in {GetType().Name}.HandleException: " + e.Message); throw; } } @@ -74,7 +106,7 @@ public virtual void ProduceDeadLetter(K key, V value, Headers headers, string ex if (string.IsNullOrWhiteSpace(Topic)) { throw new Exception( - $"DeadLetterExceptionHandler.Topic has not been configured. Cannot Produce Dead Letter Event for {ServiceName}"); + $"{GetType().Name}.Topic has not been configured. Cannot Produce Dead Letter Event for {ServiceName}"); } headers.Add("X-Exception-Message", Encoding.UTF8.GetBytes(exceptionMessage)); diff --git a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs index 06b4d13a7..4654b1e75 100644 --- a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs @@ -1,4 +1,5 @@ using Confluent.Kafka; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; @@ -26,25 +27,25 @@ public TransientExceptionHandler(ILogger> logger ProducerFactory = producerFactory; } - public virtual void HandleException(ConsumeResult? consumeResult, Exception ex, string facilityId) + public void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = "") { try { if (consumeResult == null) { - Logger.LogError(message: $"TransientExceptionHandler|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or Retry events", exception: ex); + Logger.LogError($"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or Retry events: " + message); return; } - Logger.LogError(message: $"TransientExceptionHandler: Failed to process {ServiceName} Event.", exception: ex); + Logger.LogError($"{GetType().Name}: Failed to process {ServiceName} Event: " + message); var auditValue = new AuditEventMessage { FacilityId = facilityId, - Action = AuditEventType.Query, + Action = auditEventType, ServiceName = ServiceName, EventDate = DateTime.UtcNow, - Notes = $"TransientExceptionHandler: processing failure in {ServiceName} \nException Message: {ex.Message}", + Notes = $"{GetType().Name}: processing failure in {ServiceName} \nException Message: {message}", }; ProduceAuditEvent(auditValue, consumeResult.Message.Headers); @@ -53,7 +54,39 @@ public virtual void HandleException(ConsumeResult? consumeResult, Exceptio } catch (Exception e) { - Logger.LogError(e, "Error in TransientExceptionHandler.HandleException: " + e.Message); + Logger.LogError(e, $"Error in {GetType().Name}.HandleException: " + e.Message); + throw; + } + } + + public virtual void HandleException(ConsumeResult? consumeResult, TransientException ex, string facilityId) + { + try + { + if (consumeResult == null) + { + Logger.LogError(message: $"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or Retry events", exception: ex); + return; + } + + Logger.LogError(message: $"{GetType().Name}: Failed to process {ServiceName} Event.", exception: ex); + + var auditValue = new AuditEventMessage + { + FacilityId = facilityId, + Action = ex.AuditEventType, + ServiceName = ServiceName, + EventDate = DateTime.UtcNow, + Notes = $"{GetType().Name}: processing failure in {ServiceName} \nException Message: {ex.Message}", + }; + + ProduceAuditEvent(auditValue, consumeResult.Message.Headers); + ProduceRetryScheduledEvent(consumeResult.Message.Key, consumeResult.Message.Value, + consumeResult.Message.Headers); + } + catch (Exception e) + { + Logger.LogError(e, $"Error in {GetType().Name}.HandleException: " + e.Message); throw; } } @@ -74,7 +107,7 @@ public virtual void ProduceRetryScheduledEvent(K key, V value, Headers headers) if (string.IsNullOrWhiteSpace(Topic)) { throw new Exception( - $"TransientExceptionHandler.Topic has not been configured. Cannot Produce Retry Event for {ServiceName}"); + $"{GetType().Name}.Topic has not been configured. Cannot Produce Retry Event for {ServiceName}"); } using var producer = ProducerFactory.CreateProducer(new ProducerConfig()); diff --git a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs index 076b3bf3e..aacbe4eda 100644 --- a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs @@ -1,4 +1,6 @@ using Confluent.Kafka; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Kafka; namespace LantanaGroup.Link.Shared.Application.Error.Interfaces @@ -15,7 +17,8 @@ public interface IDeadLetterExceptionHandler /// public string ServiceName { get; set; } - void HandleException(ConsumeResult consumeResult, Exception ex, string facilityId); + void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = ""); + void HandleException(ConsumeResult consumeResult, DeadLetterException ex, string facilityId); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceDeadLetter(K key, V value, Headers headers, string exceptionMessage); } diff --git a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs index 10bef73fe..f93154b36 100644 --- a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs @@ -1,4 +1,6 @@ using Confluent.Kafka; +using LantanaGroup.Link.Shared.Application.Error.Exceptions; +using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Kafka; namespace LantanaGroup.Link.Shared.Application.Error.Interfaces @@ -15,7 +17,8 @@ public interface ITransientExceptionHandler /// public string ServiceName { get; set; } - void HandleException(ConsumeResult consumeResult, Exception ex, string facilityId); + void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = ""); + void HandleException(ConsumeResult consumeResult, TransientException ex, string facilityId); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceRetryScheduledEvent(K key, V value, Headers headers); } From 5a6523cc27d2eb9ae5d6b813e759921e6ba1addc Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Thu, 21 Mar 2024 14:28:14 -0500 Subject: [PATCH 29/79] handle null case for message param --- Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs | 1 + Shared/Application/Error/Handlers/TransientExceptionHandler.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs index 22f34796c..f080de87f 100644 --- a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs @@ -32,6 +32,7 @@ public void HandleException(ConsumeResult consumeResult, string facilityId { try { + message = message ?? ""; if (consumeResult == null) { Logger.LogError($"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or DeadLetter events: " + message); diff --git a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs index 4654b1e75..411fae886 100644 --- a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs @@ -31,6 +31,7 @@ public void HandleException(ConsumeResult consumeResult, string facilityId { try { + message = message ?? ""; if (consumeResult == null) { Logger.LogError($"{GetType().Name}|{ServiceName}|{Topic}: consumeResult is null, cannot produce Audit or Retry events: " + message); From bf48e72826c76a905528670b7636cfe57eb713c0 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Thu, 21 Mar 2024 14:32:58 -0500 Subject: [PATCH 30/79] Extra overload for lazy people --- .../Error/Handlers/DeadLetterExceptionHandler.cs | 6 ++++++ .../Application/Error/Handlers/TransientExceptionHandler.cs | 6 ++++++ .../Error/Interfaces/IDeadLetterExceptionHandler.cs | 1 + .../Error/Interfaces/ITransientExceptionHandler.cs | 1 + 4 files changed, 14 insertions(+) diff --git a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs index f080de87f..3de0e2bd5 100644 --- a/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/DeadLetterExceptionHandler.cs @@ -60,6 +60,12 @@ public void HandleException(ConsumeResult consumeResult, string facilityId } } + public virtual void HandleException(ConsumeResult consumeResult, Exception ex, AuditEventType auditEventType, string facilityId) + { + var dlEx = new DeadLetterException(ex.Message, auditEventType, ex.InnerException); + HandleException(consumeResult, dlEx, facilityId); + } + public virtual void HandleException(ConsumeResult consumeResult, DeadLetterException ex, string facilityId) { try diff --git a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs index 411fae886..3debc4972 100644 --- a/Shared/Application/Error/Handlers/TransientExceptionHandler.cs +++ b/Shared/Application/Error/Handlers/TransientExceptionHandler.cs @@ -60,6 +60,12 @@ public void HandleException(ConsumeResult consumeResult, string facilityId } } + public virtual void HandleException(ConsumeResult consumeResult, Exception ex, AuditEventType auditEventType, string facilityId) + { + var tEx = new TransientException(ex.Message, auditEventType, ex.InnerException); + HandleException(consumeResult, tEx, facilityId); + } + public virtual void HandleException(ConsumeResult? consumeResult, TransientException ex, string facilityId) { try diff --git a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs index aacbe4eda..9c6b91a34 100644 --- a/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/IDeadLetterExceptionHandler.cs @@ -18,6 +18,7 @@ public interface IDeadLetterExceptionHandler public string ServiceName { get; set; } void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = ""); + void HandleException(ConsumeResult consumeResult, Exception ex, AuditEventType auditEventType, string facilityId); void HandleException(ConsumeResult consumeResult, DeadLetterException ex, string facilityId); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceDeadLetter(K key, V value, Headers headers, string exceptionMessage); diff --git a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs index f93154b36..fb31497d3 100644 --- a/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs +++ b/Shared/Application/Error/Interfaces/ITransientExceptionHandler.cs @@ -18,6 +18,7 @@ public interface ITransientExceptionHandler public string ServiceName { get; set; } void HandleException(ConsumeResult consumeResult, string facilityId, AuditEventType auditEventType, string message = ""); + void HandleException(ConsumeResult consumeResult, Exception ex, AuditEventType auditEventType, string facilityId); void HandleException(ConsumeResult consumeResult, TransientException ex, string facilityId); void ProduceAuditEvent(AuditEventMessage auditValue, Headers headers); void ProduceRetryScheduledEvent(K key, V value, Headers headers); From 95cd057e40d9774219be5d8a83ae30a97643828f Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 16:00:47 -0400 Subject: [PATCH 31/79] Added create data acquisition requested event endpoint and methods, fixed a namespace issue with commands. --- .../CreateDataAcquisitionRequested.cs | 72 +++++++++++++++++++ .../ICreateDataAcquisitionRequested.cs | 9 +++ .../CreatePatientEvent/CreatePatientEvent.cs | 2 +- .../CreatePatientEvent/ICreatePatientEvent.cs | 2 +- .../CreateReportScheduled.cs | 2 +- .../ICreateReportScheduled.cs | 2 +- .../Models/IDataAcquisitionRequested.cs | 11 +++ .../Integration/DataAcquisitionRequested.cs | 28 ++++++++ .../Models/Integration/ReportScheduled.cs | 7 ++ .../Infrastructure/Logging/Logging.cs | 6 ++ .../Endpoints/IntegrationTestingEndpoints.cs | 35 +++++++-- LinkAdmin.BFF/Program.cs | 4 +- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 1 + 13 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs create mode 100644 LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/ICreateDataAcquisitionRequested.cs create mode 100644 LinkAdmin.BFF/Application/Interfaces/Models/IDataAcquisitionRequested.cs create mode 100644 LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs new file mode 100644 index 000000000..c633091f4 --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs @@ -0,0 +1,72 @@ +using Confluent.Kafka; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Logging; +using LantanaGroup.Link.Shared.Application.Interfaces; +using LantanaGroup.Link.Shared.Application.Models; +using System.Diagnostics; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration +{ + public class CreateDataAcquisitionRequested : ICreateDataAcquisitionRequested + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public CreateDataAcquisitionRequested(ILogger logger, IServiceScopeFactory scopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + } + + public async Task Execute(DataAcquisitionRequested model, string? userId = null) + { + using Activity? activity = ServiceActivitySource.Instance.StartActivity("Producing Report Scheduled Event"); + using var scope = _scopeFactory.CreateScope(); + var _kafkaProducerFactory = scope.ServiceProvider.GetRequiredService>(); + + string correlationId = Guid.NewGuid().ToString(); + + try + { + var producerConfig = new ProducerConfig(); + + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + { + try + { + var headers = new Headers(); + headers.Add("X-Correlation-Id", System.Text.Encoding.ASCII.GetBytes(correlationId)); + + var message = new Message + { + Key = model.Key, + Value = new DataAcquisitionRequestedMessage { PatientId = model.PatientId, reports = model.Reports }, + Headers = headers + }; + + await producer.ProduceAsync(nameof(KafkaTopic.DataAcquisitionRequested), message); + + _logger.LogKafkaProducerDataAcquisitionRequested(correlationId); + return correlationId; + + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(nameof(KafkaTopic.PatientEvent), ex.Message); + throw; + } + + } + } + catch (Exception ex) + { + Activity.Current?.SetStatus(ActivityStatusCode.Error); + _logger.LogKafkaProducerException(nameof(KafkaTopic.PatientEvent), ex.Message); + throw; + } + + } + } +} diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/ICreateDataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/ICreateDataAcquisitionRequested.cs new file mode 100644 index 000000000..c33bf28ca --- /dev/null +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/ICreateDataAcquisitionRequested.cs @@ -0,0 +1,9 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration +{ + public interface ICreateDataAcquisitionRequested + { + Task Execute(DataAcquisitionRequested model, string? userId = null); + } +} diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs index 5616b7dac..aa23a515b 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using System.Text; -namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration { public class CreatePatientEvent : ICreatePatientEvent { diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs index 63949306c..874eee392 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/ICreatePatientEvent.cs @@ -1,6 +1,6 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; -namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration { public interface ICreatePatientEvent { diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs index 2f59a5b62..cf9efbe9b 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using System.Text.Json; -namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration { public class CreateReportScheduled : ICreateReportScheduled { diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs index 7ad82c6f7..7e2e53ef0 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/ICreateReportScheduled.cs @@ -1,6 +1,6 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; -namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration { public interface ICreateReportScheduled { diff --git a/LinkAdmin.BFF/Application/Interfaces/Models/IDataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Interfaces/Models/IDataAcquisitionRequested.cs new file mode 100644 index 000000000..5bfdc1d4d --- /dev/null +++ b/LinkAdmin.BFF/Application/Interfaces/Models/IDataAcquisitionRequested.cs @@ -0,0 +1,11 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models +{ + public interface IDataAcquisitionRequested + { + string Key { get; set; } + string PatientId { get; set; } + List Reports { get; set; } + } +} diff --git a/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs new file mode 100644 index 000000000..13d82f412 --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs @@ -0,0 +1,28 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Models; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration +{ + public class DataAcquisitionRequested : IDataAcquisitionRequested + { + /// + /// The key for the data acquisition request (facility id) + /// + public string Key { get; set; } = string.Empty; + + /// + /// The patient id for the data acquisition request + /// + public string PatientId { get; set; } = string.Empty; + + /// + /// The scheduled reports for the facility, used to generate the requirements for data acquisition + /// + public List Reports { get; set; } = []; + } + + public class DataAcquisitionRequestedMessage + { + public string PatientId { get; set; } = string.Empty; + public List reports { get; set; } = new List(); + } +} diff --git a/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs index e434c2fde..3f15ee2d5 100644 --- a/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs +++ b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs @@ -40,4 +40,11 @@ public static implicit operator string(ReportScheduledKey v) throw new NotImplementedException(); } } + + public class ScheduledReport + { + public string ReportType { get; set; } = string.Empty; + public DateTime StartDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + } } diff --git a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs index c1fb52594..be3801ed5 100644 --- a/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs +++ b/LinkAdmin.BFF/Infrastructure/Logging/Logging.cs @@ -52,5 +52,11 @@ public static partial class Logging "New Report Scheduled event with a correlation id of {correlationId} was created.")] public static partial void LogKafkaProducerReportScheduled(this ILogger logger, string correlationId); + [LoggerMessage( + LinkAdminLoggingIds.KafkaProducerDataAcquisitionRequested, + LogLevel.Information, + "New Data Acquisition Requested event with a correlation id of {correlationId} was created.")] + public static partial void LogKafkaProducerDataAcquisitionRequested(this ILogger logger, string correlationId); + } } diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs index 07b3dd7ec..93484a5df 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -1,8 +1,8 @@ -using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; -using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; using Microsoft.OpenApi.Models; +using System.Security.Claims; namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints { @@ -11,12 +11,14 @@ public class IntegrationTestingEndpoints : IApi private readonly ILogger _logger; private readonly ICreatePatientEvent _createPatientEvent; private readonly ICreateReportScheduled _createReportScheduled; + private readonly ICreateDataAcquisitionRequested _createDataAcquisitionRequested; - public IntegrationTestingEndpoints(ILogger logger, ICreatePatientEvent createPatientEvent, ICreateReportScheduled createReportScheduled) + public IntegrationTestingEndpoints(ILogger logger, ICreatePatientEvent createPatientEvent, ICreateReportScheduled createReportScheduled, ICreateDataAcquisitionRequested createDataAcquisitionRequested) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _createPatientEvent = createPatientEvent ?? throw new ArgumentNullException(nameof(createPatientEvent)); _createReportScheduled = createReportScheduled ?? throw new ArgumentNullException(nameof(createReportScheduled)); + _createDataAcquisitionRequested = createDataAcquisitionRequested ?? throw new ArgumentNullException(nameof(createDataAcquisitionRequested)); } public void RegisterEndpoints(WebApplication app) @@ -41,11 +43,20 @@ public void RegisterEndpoints(WebApplication app) Description = "Produces a new report scheduled event that will be sent to the broker. Allows for testing processes outside of scheduled events." }); + integrationEndpoints.MapPost("/data-acquisition-requested", CreateDataAcquisitionRequested) + .WithOpenApi(x => new OpenApiOperation(x) + { + Summary = "Integration Testing - Produce Data Acquisition Requested Event", + Description = "Produces a new data acquisition requested event that will be sent to the broker. Allows for testing processes outside of scheduled events." + }); + } public async Task CreatePatientEvent(HttpContext context, PatientEvent model) { - var correlationId = await _createPatientEvent.Execute(model); + var user = context.User; + + var correlationId = await _createPatientEvent.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); return Results.Ok(new { Id = correlationId, Message = $"The patient event was created succcessfully with a correlation id of '{correlationId}'." @@ -54,12 +65,26 @@ public async Task CreatePatientEvent(HttpContext context, PatientEvent public async Task CreateReportScheduled(HttpContext context, ReportScheduled model) { - var correlationId = await _createReportScheduled.Execute(model); + var user = context.User; + + var correlationId = await _createReportScheduled.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); return Results.Ok(new { Id = correlationId, Message = $"The report scheduled event was created succcessfully with a correlation id of '{correlationId}'." }); } + + public async Task CreateDataAcquisitionRequested(HttpContext context, DataAcquisitionRequested model) + { + var user = context.User; + + var correlationId = await _createDataAcquisitionRequested.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); + return Results.Ok(new + { + Id = correlationId, + Message = $"The data acquisition requested event was created succcessfully with a correlation id of '{correlationId}'." + }); + } } } diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 201552177..7d8fc0f41 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,7 +1,6 @@ using Azure.Identity; using HealthChecks.UI.Client; -using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreatePatientEvent; -using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration.CreateReportScheduled; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure; @@ -86,6 +85,7 @@ static void RegisterServices(WebApplicationBuilder builder) //Add commands builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); //Add YARP (reverse proxy) builder.Services.AddReverseProxy() diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 3dd026da2..5024590ac 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -24,6 +24,7 @@ public static class LinkAdminLoggingIds public const int KafkaProducerException = 1005; public const int KafkaProducerPatientEvent = 1006; public const int KafkaProducerReportScheduled = 1007; + public const int KafkaProducerDataAcquisitionRequested = 1008; } } } From 59ab5e2e3d93b70a86c21b9e0348af3fc8336a3b Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 16:20:43 -0400 Subject: [PATCH 32/79] Updated telemetry to include kafka instrumentation, updated shared NuGet --- .../CreateDataAcquisitionRequested.cs | 2 +- .../Integration/CreatePatientEvent/CreatePatientEvent.cs | 2 +- .../CreateReportScheduled/CreateReportScheduled.cs | 2 +- .../Infrastructure/Extensions/TelemetryServiceExtension.cs | 4 +++- LinkAdmin.BFF/LinkAdmin.BFF.csproj | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs index c633091f4..a206656c1 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateDataAcquisitionRequested/CreateDataAcquisitionRequested.cs @@ -31,7 +31,7 @@ public async Task Execute(DataAcquisitionRequested model, string? userId { var producerConfig = new ProducerConfig(); - using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig, useOpenTelemetry: true)) { try { diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs index aa23a515b..79a75d0c9 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreatePatientEvent/CreatePatientEvent.cs @@ -32,7 +32,7 @@ public async Task Execute(PatientEvent model, string? userId = null) { var producerConfig = new ProducerConfig(); - using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig, useOpenTelemetry: true)) { try { diff --git a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs index cf9efbe9b..d0cfb0578 100644 --- a/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs +++ b/LinkAdmin.BFF/Application/Commands/Integration/CreateReportScheduled/CreateReportScheduled.cs @@ -43,7 +43,7 @@ public async Task Execute(ReportScheduled model, string? userId = null) { var producerConfig = new ProducerConfig(); - using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig)) + using (var producer = _kafkaProducerFactory.CreateProducer(producerConfig, useOpenTelemetry: true)) { try { diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs index 667808d93..acc25d426 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/TelemetryServiceExtension.cs @@ -1,4 +1,5 @@ -using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; +using Confluent.Kafka.Extensions.OpenTelemetry; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -28,6 +29,7 @@ public static IServiceCollection AddOpenTelemetryService(this IServiceCollection { options.Filter = (httpContext) => httpContext.Request.Path != "/health"; //do not capture traces for the health check endpoint }) + .AddConfluentKafkaInstrumentation() .AddOtlpExporter(opts => { opts.Endpoint = new Uri(telemetryServiceOptions.TelemetryCollectorEndpoint); })); otel.WithMetrics(metricsProviderBuilder => diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index c49dc682c..0254d6086 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -18,7 +18,7 @@ - + From 7ecc649d25088e3339a791868c815d4eaf50576d Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Thu, 21 Mar 2024 15:35:57 -0500 Subject: [PATCH 33/79] Version Update --- Shared/Shared.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 59def70e7..9e6ab4dc2 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -14,7 +14,7 @@ Shared library for Lantana Link NHSN Link Shared Library - 1.2.1 + 2.1.1 com.lantanagroup.link.Shared From c39063f1a5326033ebcddf02e105c2737dec0dca Mon Sep 17 00:00:00 2001 From: edward-miller-lcg <119338797+edward-miller-lcg@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:39:39 -0500 Subject: [PATCH 34/79] Dead Letter processing --- DataAcquisition/DataAcquisition.csproj | 2 +- DataAcquisition/Listeners/QueryListener.cs | 79 ++++++++++++++-------- DataAcquisition/Program.cs | 3 + 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/DataAcquisition/DataAcquisition.csproj b/DataAcquisition/DataAcquisition.csproj index a24eb98ed..2a92f2835 100644 --- a/DataAcquisition/DataAcquisition.csproj +++ b/DataAcquisition/DataAcquisition.csproj @@ -23,7 +23,7 @@ - + diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index 39d1898b1..2d2f6aa97 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -7,6 +7,7 @@ using LantanaGroup.Link.DataAcquisition.Application.Serializers; using LantanaGroup.Link.DataAcquisition.Application.Settings; using LantanaGroup.Link.DataAcquisition.Entities; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Kafka; @@ -23,19 +24,24 @@ public class QueryListener : BackgroundService private readonly IKafkaConsumerFactory _kafkaConsumerFactory; private readonly IKafkaProducerFactory _kafkaProducerFactory; + private readonly IDeadLetterExceptionHandler _deadLetterConsumerHandler; + private readonly ILogger _logger; private readonly IMediator _mediator; - + public QueryListener( ILogger logger, IMediator mediator, IKafkaConsumerFactory kafkaConsumerFactory, - IKafkaProducerFactory kafkaProducerFactory) + IKafkaProducerFactory kafkaProducerFactory, + IDeadLetterExceptionHandler deadLetterConsumerHandler, + IDeadLetterExceptionHandler deadLetterProducerHandler) { _logger = logger; _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentNullException(nameof(kafkaConsumerFactory)); _kafkaProducerFactory = kafkaProducerFactory ?? throw new ArgumentNullException(nameof(kafkaProducerFactory)); + _deadLetterConsumerHandler = deadLetterConsumerHandler ?? throw new ArgumentNullException(nameof(deadLetterConsumerHandler)); } public override async Task StartAsync(CancellationToken cancellationToken) @@ -66,19 +72,51 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca while (!cancellationToken.IsCancellationRequested) { - rawmessage = consumer.Consume(cancellationToken); + try + { + rawmessage = consumer.Consume(cancellationToken); + } + catch (Exception ex) + { + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + continue; + } + IBaseMessage deserializedMessage = null; (string facilityId, string correlationId) messageMetaData = (string.Empty, string.Empty); if (rawmessage != null) { - var message = MessageDeserializer.DeserializeMessage(rawmessage.Topic, rawmessage.Message.Value); + IBaseMessage? message = null; + try + { + message = MessageDeserializer.DeserializeMessage(rawmessage.Topic, rawmessage.Message.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deserializing message: {1}", ex.Message); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + continue; + } + deserializedMessage = message; - messageMetaData = ExtractFacilityIdAndCorrelationIdFromMessage(rawmessage.Message); + + try + { + messageMetaData = ExtractFacilityIdAndCorrelationIdFromMessage(rawmessage.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting facility id and correlation id from message"); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + continue; + } + if (string.IsNullOrWhiteSpace(messageMetaData.facilityId)) { - var errorMessage = $"No Facility ID provided. Unable to process message: {message}"; - _logger.LogWarning(errorMessage); + var errorMessage = "No Facility ID provided. Unable to process message: {1}"; + _logger.LogWarning(errorMessage, message); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), ""); continue; } @@ -101,16 +139,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - ProduceAuditMessage(new AuditEventMessage - { - CorrelationId = messageMetaData.correlationId, - FacilityId = messageMetaData.facilityId, - Action = AuditEventType.Query, - //Resource = string.Join(',', deserializedMessage.Type), - EventDate = DateTime.UtcNow, - ServiceName = DataAcquisitionConstants.ServiceName, - Notes = $"Failed to get {rawmessage.Topic}\nException Message: {ex}\nRaw Message: {JsonConvert.SerializeObject(rawmessage)}", - }); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); _logger.LogError(ex,"Error producing message: {1}", ex.Message); responseMessages = null; continue; @@ -132,6 +161,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca Key = messageMetaData.facilityId, Value = (PatientIDsAcquiredMessage)responseMessages[0] }; + await producer.ProduceAsync(KafkaTopic.PatientIDsAcquired.ToString(), produceMessage, cancellationToken); } else @@ -166,16 +196,8 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - ProduceAuditMessage(new AuditEventMessage - { - CorrelationId = messageMetaData.correlationId, - FacilityId = messageMetaData.facilityId, - Action = AuditEventType.Query, - ServiceName = DataAcquisitionConstants.ServiceName, - EventDate = DateTime.UtcNow, - Notes = $"Failed to produce message. \nException Message: {ex}\nRaw Kafka Message: {rawmessage}", - }); - _logger.LogError($"Failed to produce message", ex); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _logger.LogError(ex, "Failed to produce message"); continue; } consumer.Commit(rawmessage); @@ -192,7 +214,8 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca EventDate = DateTime.UtcNow, Notes = $"Message with topic: {rawmessage.Topic} meets no condition for processing. full message: {rawmessage.Message}", }); - _logger.LogWarning($"Message with topic: {rawmessage.Topic} meets no condition for processing. full message: {rawmessage.Message}"); + _logger.LogWarning("Message with topic: {1} meets no condition for processing. full message: {2}", rawmessage.Topic, rawmessage.Message); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), messageMetaData.facilityId); } } } diff --git a/DataAcquisition/Program.cs b/DataAcquisition/Program.cs index 1fa091f86..dfd1507bd 100644 --- a/DataAcquisition/Program.cs +++ b/DataAcquisition/Program.cs @@ -12,6 +12,8 @@ using LantanaGroup.Link.DataAcquisition.Listeners; using LantanaGroup.Link.DataAcquisition.Services; using LantanaGroup.Link.DataAcquisition.Services.Auth; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Factories; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Configs; @@ -97,6 +99,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton, DeadLetterExceptionHandler>(); builder.Services.AddScoped, KafkaConsumerFactory>(); builder.Services.AddScoped, KafkaProducerFactory>(); builder.Services.AddScoped, KafkaProducerFactory>(); From acdc2eb11268148fec76ef2e33eab93312770238 Mon Sep 17 00:00:00 2001 From: edward-miller-lcg <119338797+edward-miller-lcg@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:54:29 -0500 Subject: [PATCH 35/79] minor fixes --- DataAcquisition/Listeners/QueryListener.cs | 3 +-- DataAcquisition/Program.cs | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index 2d2f6aa97..58d0e0397 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -34,8 +34,7 @@ public QueryListener( IMediator mediator, IKafkaConsumerFactory kafkaConsumerFactory, IKafkaProducerFactory kafkaProducerFactory, - IDeadLetterExceptionHandler deadLetterConsumerHandler, - IDeadLetterExceptionHandler deadLetterProducerHandler) + IDeadLetterExceptionHandler deadLetterConsumerHandler) { _logger = logger; _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); diff --git a/DataAcquisition/Program.cs b/DataAcquisition/Program.cs index dfd1507bd..1b5c5f7ee 100644 --- a/DataAcquisition/Program.cs +++ b/DataAcquisition/Program.cs @@ -17,6 +17,7 @@ using LantanaGroup.Link.Shared.Application.Factories; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Configs; +using LantanaGroup.Link.Shared.Application.Models.Kafka; using LantanaGroup.Link.Shared.Application.Wrappers; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using OpenTelemetry.Metrics; @@ -93,16 +94,19 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddSingleton, DeadLetterExceptionHandler>(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton, DeadLetterExceptionHandler>(); + builder.Services.AddScoped, KafkaConsumerFactory>(); builder.Services.AddScoped, KafkaProducerFactory>(); builder.Services.AddScoped, KafkaProducerFactory>(); + builder.Services.AddScoped, KafkaProducerFactory>(); builder.Services.AddScoped, DataAcquisitionKafkaService>(); builder.Services.AddScoped, KafkaWrapper>(); From 30d8ce52fd64b66974f46fcbd8d864295b31d050 Mon Sep 17 00:00:00 2001 From: edward-miller-lcg <119338797+edward-miller-lcg@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:10:52 -0500 Subject: [PATCH 36/79] Remove Shared as dependency --- DataAcquisitionTests/DataAcquisitionTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/DataAcquisitionTests/DataAcquisitionTests.csproj b/DataAcquisitionTests/DataAcquisitionTests.csproj index 50a88d63f..c8380c4f6 100644 --- a/DataAcquisitionTests/DataAcquisitionTests.csproj +++ b/DataAcquisitionTests/DataAcquisitionTests.csproj @@ -20,7 +20,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - From 2f04edbb6a9035449fe791dc548b65369d905558 Mon Sep 17 00:00:00 2001 From: edward-miller-lcg <119338797+edward-miller-lcg@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:25:13 -0500 Subject: [PATCH 37/79] Update autocomment-on-pr-shared.yaml --- .github/workflows/autocomment-on-pr-shared.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/autocomment-on-pr-shared.yaml b/.github/workflows/autocomment-on-pr-shared.yaml index cbb05adf0..416d525d8 100644 --- a/.github/workflows/autocomment-on-pr-shared.yaml +++ b/.github/workflows/autocomment-on-pr-shared.yaml @@ -19,8 +19,8 @@ jobs: run: | # Use GitHub API to create a comment on the PR PR_NUMBER=${{ github.event.pull_request.number }} - COMMENT="****THERE IS A NEW VERSIONING APPROACH TO VERSIONING OF THE SHARED PROJECT*****\nPlease refere here for guidance: https://lantana.atlassian.net/wiki/spaces/LSD/pages/493322243/Shared+NuGet+Project+Versioning+and+Releases" + COMMENT="****THERE IS A NEW APPROACH TO VERSIONING OF THE SHARED PROJECT*****\nPlease refer here for guidance: https://lantana.atlassian.net/wiki/spaces/LSD/pages/493322243/Shared+NuGet+Project+Versioning+and+Releases" GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} COMMENT_URL="https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" - curl -s -H "Authorization: token ${GITHUB_TOKEN}" -X POST $COMMENT_URL -d "{\"body\":\"$COMMENT\"}" \ No newline at end of file + curl -s -H "Authorization: token ${GITHUB_TOKEN}" -X POST $COMMENT_URL -d "{\"body\":\"$COMMENT\"}" From f743b00c97b855ed1bccc993db38aa9e758e267c Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 21 Mar 2024 21:15:20 -0400 Subject: [PATCH 38/79] Added fluent validation for the integration endpoints --- .../Application/Filters/ValidationFilter.cs | 35 +++++++++++++ .../Models/Responses/EventProducerResponse.cs | 15 ++++++ .../Responses/ValidationFailureResponse.cs | 24 +++++++++ .../DataAcquisitionRequestedValidator.cs | 51 +++++++++++++++++++ .../Validators/PatientEventValidator.cs | 25 +++++++++ .../Validators/ReportScheduledValidator.cs | 31 +++++++++++ .../ValidationExceptionMiddleware.cs | 35 +++++++++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + .../Endpoints/IntegrationTestingEndpoints.cs | 12 +++-- LinkAdmin.BFF/Program.cs | 5 ++ 10 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Filters/ValidationFilter.cs create mode 100644 LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs create mode 100644 LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs create mode 100644 LinkAdmin.BFF/Application/Validators/DataAcquisitionRequestedValidator.cs create mode 100644 LinkAdmin.BFF/Application/Validators/PatientEventValidator.cs create mode 100644 LinkAdmin.BFF/Application/Validators/ReportScheduledValidator.cs create mode 100644 LinkAdmin.BFF/Application/Validators/ValidationExceptionMiddleware.cs diff --git a/LinkAdmin.BFF/Application/Filters/ValidationFilter.cs b/LinkAdmin.BFF/Application/Filters/ValidationFilter.cs new file mode 100644 index 000000000..577c2e515 --- /dev/null +++ b/LinkAdmin.BFF/Application/Filters/ValidationFilter.cs @@ -0,0 +1,35 @@ + +using FluentValidation; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Responses; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Filters +{ + public class ValidationFilter : IEndpointFilter where T : class + { + private readonly IValidator _validator; + + public ValidationFilter(IValidator validator) + { + _validator = validator; + } + + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var validatableObject = context.Arguments.OfType().FirstOrDefault(t => t?.GetType() == typeof(T)); + + if (validatableObject is not null) + { + var validationResult = _validator.Validate(validatableObject); + + if (validationResult.IsValid) + { + return await next(context); + } + + return Results.BadRequest(validationResult.Errors.ToResponse()); + } + + return await next(context); + } + } +} diff --git a/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs b/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs new file mode 100644 index 000000000..3f2f18b68 --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs @@ -0,0 +1,15 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Responses +{ + public class EventProducerResponse + { + /// + /// The Correlation Id of the event + /// + public string Id { get; init; } = string.Empty; + + /// + /// The message about the event creation + /// + public string Message { get; init; } = string.Empty; + } +} diff --git a/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs b/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs new file mode 100644 index 000000000..aed18ea48 --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs @@ -0,0 +1,24 @@ +using FluentValidation.Results; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Responses +{ + public class ValidationFailureResponse + { + /// + /// A list of validation errors + /// + public List Errors { get; init; } = []; + } + + public static class ValidationFailureMapper + { + public static ValidationFailureResponse ToResponse(this IEnumerable errors) + { + var messages = errors.Select(x => x.ErrorMessage).ToList(); + return new ValidationFailureResponse + { + Errors = messages + }; + } + } +} diff --git a/LinkAdmin.BFF/Application/Validators/DataAcquisitionRequestedValidator.cs b/LinkAdmin.BFF/Application/Validators/DataAcquisitionRequestedValidator.cs new file mode 100644 index 000000000..034b2d6d3 --- /dev/null +++ b/LinkAdmin.BFF/Application/Validators/DataAcquisitionRequestedValidator.cs @@ -0,0 +1,51 @@ +using FluentValidation; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Validators +{ + public class DataAcquisitionRequestedValidator : AbstractValidator + { + public DataAcquisitionRequestedValidator() + { + RuleFor(x => x.Key) + .NotEmpty() + .WithMessage("Key is required"); + + RuleFor(x => x.PatientId) + .NotEmpty() + .WithMessage("PatientId is required"); + + RuleFor(x => x.Reports) + .NotEmpty() + .WithMessage("Reports are required"); + + RuleForEach(x => x.Reports) + .SetValidator(new ScheduledReportValidator()); + + } + } + + public class ScheduledReportValidator : AbstractValidator + { + public ScheduledReportValidator() + { + RuleFor(x => x.ReportType) + .NotEmpty() + .WithMessage("ReportType is required"); + + RuleFor(x => x.StartDate) + .NotEmpty() + .WithMessage("StartDate is required") + .Must((x, y) => x.StartDate < x.EndDate) + .WithMessage("StartDate must be before EndDate") + .Must((x, y) => x.StartDate < DateTime.Now) + .WithMessage("StartDate must be in the past"); + + RuleFor(x => x.EndDate) + .NotEmpty() + .WithMessage("EndDate is required"); + + } + + } +} diff --git a/LinkAdmin.BFF/Application/Validators/PatientEventValidator.cs b/LinkAdmin.BFF/Application/Validators/PatientEventValidator.cs new file mode 100644 index 000000000..88f59e2e7 --- /dev/null +++ b/LinkAdmin.BFF/Application/Validators/PatientEventValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Validation +{ + public class PatientEventValidator : AbstractValidator + { + public PatientEventValidator() + { + RuleFor(x => x.Key) + .NotEmpty() + .WithMessage("Key is required"); + + RuleFor(x => x.PatientId) + .NotEmpty() + .WithMessage("PatientId is required"); + + RuleFor(x => x.EventType) + .NotEmpty() + .WithMessage("EventType is required") + .Must(x => x == "Admission" || x == "Discharge") + .WithMessage("EventType must be 'Admission' or 'Discharge'"); + } + } +} diff --git a/LinkAdmin.BFF/Application/Validators/ReportScheduledValidator.cs b/LinkAdmin.BFF/Application/Validators/ReportScheduledValidator.cs new file mode 100644 index 000000000..47180708d --- /dev/null +++ b/LinkAdmin.BFF/Application/Validators/ReportScheduledValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Validators +{ + public class ReportScheduledValidator : AbstractValidator + { + public ReportScheduledValidator() + { + RuleFor(x => x.FacilityId) + .NotEmpty() + .WithMessage("FacilityId is required"); + + RuleFor(x => x.ReportType) + .NotEmpty() + .WithMessage("ReportType is required"); + + RuleFor(x => x.StartDate) + .NotEmpty() + .WithMessage("StartDate is required") + .Must((x, y) => x.StartDate < x.EndDate) + .WithMessage("StartDate must be before EndDate") + .Must((x, y) => x.StartDate < DateTime.Now) + .WithMessage("StartDate must be in the past"); + + RuleFor(x => x.EndDate) + .NotEmpty() + .WithMessage("EndDate is required"); + } + } +} diff --git a/LinkAdmin.BFF/Application/Validators/ValidationExceptionMiddleware.cs b/LinkAdmin.BFF/Application/Validators/ValidationExceptionMiddleware.cs new file mode 100644 index 000000000..1502dab1b --- /dev/null +++ b/LinkAdmin.BFF/Application/Validators/ValidationExceptionMiddleware.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Responses; +using System.Net; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Validation +{ + public class ValidationExceptionMiddleware + { + private readonly RequestDelegate _next; + + public ValidationExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (ValidationException ex) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + var messages = ex.Errors.Select(x => x.ErrorMessage).ToList(); + var validationFailureResposne = new ValidationFailureResponse + { + Errors = messages + }; + + await context.Response.WriteAsJsonAsync(validationFailureResposne); + } + } + } +} diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index 0254d6086..4b9aa6af9 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -21,6 +21,7 @@ + diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs index 93484a5df..bdaf98f62 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -1,6 +1,8 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Filters; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Integration; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Responses; using Microsoft.OpenApi.Models; using System.Security.Claims; @@ -30,6 +32,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/patient-event", CreatePatientEvent) + .AddEndpointFilter>() .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Patient Event", @@ -37,6 +40,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/report-scheduled", CreateReportScheduled) + .AddEndpointFilter>() .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Report Scheduled Event", @@ -44,6 +48,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/data-acquisition-requested", CreateDataAcquisitionRequested) + .AddEndpointFilter>() .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Data Acquisition Requested Event", @@ -57,7 +62,8 @@ public async Task CreatePatientEvent(HttpContext context, PatientEvent var user = context.User; var correlationId = await _createPatientEvent.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); - return Results.Ok(new { + return Results.Ok(new EventProducerResponse + { Id = correlationId, Message = $"The patient event was created succcessfully with a correlation id of '{correlationId}'." }); @@ -68,7 +74,7 @@ public async Task CreateReportScheduled(HttpContext context, ReportSche var user = context.User; var correlationId = await _createReportScheduled.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); - return Results.Ok(new + return Results.Ok(new EventProducerResponse { Id = correlationId, Message = $"The report scheduled event was created succcessfully with a correlation id of '{correlationId}'." @@ -80,7 +86,7 @@ public async Task CreateDataAcquisitionRequested(HttpContext context, D var user = context.User; var correlationId = await _createDataAcquisitionRequested.Execute(model, user?.FindFirst(ClaimTypes.Email)?.Value); - return Results.Ok(new + return Results.Ok(new EventProducerResponse { Id = correlationId, Message = $"The data acquisition requested event was created succcessfully with a correlation id of '{correlationId}'." diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 7d8fc0f41..0605600b8 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -12,11 +12,13 @@ using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using FluentValidation; using Serilog; using Serilog.Enrichers.Span; using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Validation; var builder = WebApplication.CreateBuilder(args); @@ -82,6 +84,9 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); + //Add fluent validation + builder.Services.AddValidatorsFromAssemblyContaining(typeof(PatientEventValidator)); + //Add commands builder.Services.AddTransient(); builder.Services.AddTransient(); From 528dcc7240c62d07deb2e0c70b4e22423be7b488 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Fri, 22 Mar 2024 07:37:42 -0400 Subject: [PATCH 39/79] Refactored external configuration into an extension method --- .../ExternalConfigurationExtension.cs | 44 +++++++++++++++++++ LinkAdmin.BFF/Program.cs | 25 +++-------- 2 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs new file mode 100644 index 000000000..5c9f0aecd --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs @@ -0,0 +1,44 @@ +using Azure.Identity; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class ExternalConfigurationExtension + { + public static WebApplicationBuilder AddExternalConfiguration(this WebApplicationBuilder builder, Action? options = null) + { + var externalConfigurationOptions = new ExternalConfigurationOptions(); + options?.Invoke(externalConfigurationOptions); + + if (!string.IsNullOrEmpty(externalConfigurationOptions.ExternalConfigurationSource)) + { + switch (externalConfigurationOptions.ExternalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(externalConfigurationOptions.ExternalConfigurationConnectionString) + .Select("*", LabelFilter.Null) + .Select("*", "Link:AdminBFF:" + externalConfigurationOptions.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + + return builder; + } + } + + public class ExternalConfigurationOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public string? ExternalConfigurationSource { get; set; } + public string? ExternalConfigurationConnectionString { get; set; } + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 0605600b8..a4d2fc6f3 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -1,4 +1,3 @@ -using Azure.Identity; using HealthChecks.UI.Client; using LantanaGroup.Link.LinkAdmin.BFF.Application.Commands.Integration; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; @@ -11,7 +10,6 @@ using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.Configuration.AzureAppConfiguration; using FluentValidation; using Serilog; using Serilog.Enrichers.Span; @@ -36,25 +34,12 @@ static void RegisterServices(WebApplicationBuilder builder) var externalConfigurationSource = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); if (!string.IsNullOrEmpty(externalConfigurationSource)) { - switch (externalConfigurationSource) + builder.AddExternalConfiguration(options => { - case ("AzureAppConfiguration"): - builder.Configuration.AddAzureAppConfiguration(options => - { - options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) - // Load configuration values with no label - .Select("*", LabelFilter.Null) - // Override with any configuration values specific to current hosting env - .Select("*", "Link:AdminBFF:" + builder.Environment.EnvironmentName); - - options.ConfigureKeyVault(kv => - { - kv.SetCredential(new DefaultAzureCredential()); - }); - - }); - break; - } + options.ExternalConfigurationSource = externalConfigurationSource; + options.ExternalConfigurationConnectionString = builder.Configuration.GetConnectionString("AzureAppConfiguration"); + options.Environment = builder.Environment; + }); } var serviceInformation = builder.Configuration.GetRequiredSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); From f9d93c658d0992fae146948a751c168f454d419b Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Fri, 22 Mar 2024 07:43:32 -0400 Subject: [PATCH 40/79] code cleanup --- LinkAdmin.BFF/Program.cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index a4d2fc6f3..abfcf0c5b 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -30,7 +30,7 @@ #region Register Services static void RegisterServices(WebApplicationBuilder builder) { - //load external configuration source if specified + // load external configuration source if specified var externalConfigurationSource = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); if (!string.IsNullOrEmpty(externalConfigurationSource)) { @@ -42,6 +42,7 @@ static void RegisterServices(WebApplicationBuilder builder) }); } + // Add service information var serviceInformation = builder.Configuration.GetRequiredSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { @@ -52,39 +53,39 @@ static void RegisterServices(WebApplicationBuilder builder) throw new NullReferenceException("Service Information was null."); } - //Add problem details + // Add problem details builder.Services.AddProblemDetailsService(options => { options.Environment = builder.Environment; options.IncludeExceptionDetails = builder.Configuration.GetValue("ProblemDetails:IncludeExceptionDetails"); }); - //Add IOptions + // Add IOptions builder.Services.Configure(builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.Kafka)); - //Add Kafka Producer Factories + // Add Kafka Producer Factories builder.Services.AddSingleton, KafkaProducerFactory>(); - //Add Endpoints + // Add Endpoints builder.Services.AddTransient(); builder.Services.AddTransient(); - //Add fluent validation + // Add fluent validation builder.Services.AddValidatorsFromAssemblyContaining(typeof(PatientEventValidator)); - //Add commands + // Add commands builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); - //Add YARP (reverse proxy) + // Add YARP (reverse proxy) builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); - //Add health checks + // Add health checks builder.Services.AddHealthChecks(); - //configure CORS + // Configure CORS var corsConfig = builder.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); if(corsConfig != null) { @@ -103,8 +104,7 @@ static void RegisterServices(WebApplicationBuilder builder) throw new NullReferenceException("CORS Configuration was null."); } - // Add services to the container. - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + // Add swagger generation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { @@ -163,7 +163,7 @@ static void SetupMiddleware(WebApplication app) app.UseHttpsRedirection(); - //configure swagger + // Configure swagger if (app.Configuration.GetValue(LinkAdminConstants.AppSettingsSectionNames.EnableSwagger)) { var serviceInformation = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.ServiceInformation).Get(); @@ -180,17 +180,17 @@ static void SetupMiddleware(WebApplication app) //app.UseMiddleware(); //app.UseAuthorization(); - //register endpoints + // Register endpoints var apis = app.Services.GetServices(); foreach (var api in apis) { - if(api is null) throw new InvalidProgramException("API was not found."); + if(api is null) throw new InvalidProgramException("No Endpoints were not found."); api.RegisterEndpoints(app); } app.MapReverseProxy(); - //map health check middleware + // Map health check middleware app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse From 24bebf3817574c710dc4d407efaa66161274e7ee Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Fri, 22 Mar 2024 07:49:04 -0400 Subject: [PATCH 41/79] made the integration endpoints registration configurable (enable/disable) --- LinkAdmin.BFF/Program.cs | 5 ++++- LinkAdmin.BFF/appsettings.Development.json | 3 ++- LinkAdmin.BFF/appsettings.json | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index abfcf0c5b..1c30c9152 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -68,7 +68,10 @@ static void RegisterServices(WebApplicationBuilder builder) // Add Endpoints builder.Services.AddTransient(); - builder.Services.AddTransient(); + if (builder.Configuration.GetValue("EnableIntegrationFeature")) + { + builder.Services.AddTransient(); + } // Add fluent validation builder.Services.AddValidatorsFromAssemblyContaining(typeof(PatientEventValidator)); diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index 0b014f32f..859b886f4 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -5,9 +5,10 @@ "Version": "0.1.0" }, "KafkaConnection": { - "BootstrapServers": [ ] + "BootstrapServers": [] }, "EnableSwagger": true, + "EnableIntegrationFeature": true, "Logging": { "LogLevel": { "Default": "Information", diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 84525f95a..407a9debc 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -17,6 +17,7 @@ "MaxAge": 600 }, "EnableSwagger": true, + "EnableIntegrationFeature": true, "ProblemDetails": { "IncludeExceptionDetails": false }, From eb240d0207296bdc8961bf9e1d31aaa49619b6f7 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Fri, 22 Mar 2024 09:26:54 -0400 Subject: [PATCH 42/79] Added additional swagger documentation --- .../Integration/DataAcquisitionRequested.cs | 2 ++ .../Models/Integration/PatientEvent.cs | 3 +++ .../Models/Integration/ReportScheduled.cs | 18 ++++++++++++++++++ .../Models/Responses/EventProducerResponse.cs | 2 ++ .../Responses/ValidationFailureResponse.cs | 1 + .../Presentation/Endpoints/AuthEndpoints.cs | 8 ++++++++ .../Endpoints/IntegrationTestingEndpoints.cs | 12 ++++++++++++ 7 files changed, 46 insertions(+) diff --git a/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs b/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs index 13d82f412..2d5990e73 100644 --- a/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs +++ b/LinkAdmin.BFF/Application/Models/Integration/DataAcquisitionRequested.cs @@ -7,11 +7,13 @@ public class DataAcquisitionRequested : IDataAcquisitionRequested /// /// The key for the data acquisition request (facility id) /// + /// TestFacility01 public string Key { get; set; } = string.Empty; /// /// The patient id for the data acquisition request /// + /// TestPatient01 public string PatientId { get; set; } = string.Empty; /// diff --git a/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs b/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs index 4fa5c9004..23c5e9f63 100644 --- a/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs +++ b/LinkAdmin.BFF/Application/Models/Integration/PatientEvent.cs @@ -7,16 +7,19 @@ public class PatientEvent : IPatientEvent /// /// Key for the patient event (FacilityId) /// + /// TestFacility01 public string Key { get; set; } = string.Empty; /// /// The id of the patient subject to the event /// + /// TestPatient01 public string PatientId { get; set; } = string.Empty; /// /// The type of event that occurred /// + /// Discharge public string EventType { get; set; } = string.Empty; } diff --git a/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs index 3f15ee2d5..f18f6e493 100644 --- a/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs +++ b/LinkAdmin.BFF/Application/Models/Integration/ReportScheduled.cs @@ -7,21 +7,25 @@ public class ReportScheduled : IReportScheduled /// /// The facility id for the report /// + /// TestFacility01 public string FacilityId { get; set; } = string.Empty; /// /// The type of measure report to be generated /// + /// NHSNGlycemicControlHypoglycemicInitialPopulation public string ReportType { get; set; } = string.Empty; /// /// The start date for the report period /// + /// 2024-01-01 public DateTime? StartDate { get; set; } /// /// The end date for the report period /// + /// 2024-01-31 public DateTime? EndDate { get; set; } } @@ -43,8 +47,22 @@ public static implicit operator string(ReportScheduledKey v) public class ScheduledReport { + /// + /// The type of measure report to be generated + /// + /// NHSNGlycemicControlHypoglycemicInitialPopulation public string ReportType { get; set; } = string.Empty; + + /// + /// The start date for the reporting period + /// + /// 2024-01-01 public DateTime StartDate { get; set; } = DateTime.Now; + + /// + /// The end date for the reporting period + /// + /// 2024-01-31 public DateTime EndDate { get; set; } = DateTime.Now; } } diff --git a/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs b/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs index 3f2f18b68..491d04c6f 100644 --- a/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs +++ b/LinkAdmin.BFF/Application/Models/Responses/EventProducerResponse.cs @@ -5,11 +5,13 @@ public class EventProducerResponse /// /// The Correlation Id of the event /// + /// fad42853-ac1f-45ad-94ae-f858b1d41945 public string Id { get; init; } = string.Empty; /// /// The message about the event creation /// + /// The patient event was created succcessfully with a correlation id of 'fad42853-ac1f-45ad-94ae-f858b1d41945'. public string Message { get; init; } = string.Empty; } } diff --git a/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs b/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs index aed18ea48..25386784e 100644 --- a/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs +++ b/LinkAdmin.BFF/Application/Models/Responses/ValidationFailureResponse.cs @@ -7,6 +7,7 @@ public class ValidationFailureResponse /// /// A list of validation errors /// + /// "PatientId is required" public List Errors { get; init; } = []; } diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index b05429852..bff7634c5 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -23,6 +23,8 @@ public void RegisterEndpoints(WebApplication app) authEndpoints.MapGet("/login", Login) .AllowAnonymous() + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Login to Link", @@ -31,6 +33,9 @@ public void RegisterEndpoints(WebApplication app) authEndpoints.MapGet("/user", GetUser) .RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Get user information", @@ -39,6 +44,9 @@ public void RegisterEndpoints(WebApplication app) authEndpoints.MapGet("/logout", Logout) .RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Logout of Link", diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs index bdaf98f62..bdad9dc48 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -33,6 +33,10 @@ public void RegisterEndpoints(WebApplication app) integrationEndpoints.MapPost("/patient-event", CreatePatientEvent) .AddEndpointFilter>() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Patient Event", @@ -41,6 +45,10 @@ public void RegisterEndpoints(WebApplication app) integrationEndpoints.MapPost("/report-scheduled", CreateReportScheduled) .AddEndpointFilter>() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Report Scheduled Event", @@ -49,6 +57,10 @@ public void RegisterEndpoints(WebApplication app) integrationEndpoints.MapPost("/data-acquisition-requested", CreateDataAcquisitionRequested) .AddEndpointFilter>() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) { Summary = "Integration Testing - Produce Data Acquisition Requested Event", From bbb1a677a9b5dc403497ed3afcc7ae706035ae48 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 09:21:39 -0500 Subject: [PATCH 43/79] Update all shared references --- Account/Account.csproj | 2 +- Audit/Audit.csproj | 2 +- Census/Census.csproj | 2 +- DataAcquisition/DataAcquisition.csproj | 2 +- .../DataAcquisitionTests.csproj | 2 +- DemoApiGateway/DemoApiGateway.csproj | 2 +- MeasureEval/MeasureEval.csproj | 2 +- Normalization/Normalization.csproj | 2 +- Notification/Notification.csproj | 2 +- PatientsToQuery/PatientsToQuery.csproj | 2 +- QueryDispatch/QueryDispatch.csproj | 2 +- QueryDispatchTests/QueryDispatchTests.csproj | 2 +- Report/Listeners/MeasureEvaluatedListener.cs | 10 +- Report/Listeners/PatientsToQueryListener.cs | 8 +- Report/Listeners/ReportScheduledListener.cs | 16 +- Report/Listeners/ReportSubmittedListener.cs | 167 +++++++++--------- Report/Report.csproj | 2 +- Submission/Listeners/SubmitReportListener.cs | 11 +- Submission/Submission.csproj | 2 +- Tenant/Tenant.csproj | 2 +- TenantTests/TenantTests.csproj | 2 +- 21 files changed, 119 insertions(+), 125 deletions(-) diff --git a/Account/Account.csproj b/Account/Account.csproj index 3857a16ed..4045d913d 100644 --- a/Account/Account.csproj +++ b/Account/Account.csproj @@ -21,7 +21,7 @@ - + diff --git a/Audit/Audit.csproj b/Audit/Audit.csproj index 1e6296917..a87e3d463 100644 --- a/Audit/Audit.csproj +++ b/Audit/Audit.csproj @@ -23,7 +23,7 @@ - + diff --git a/Census/Census.csproj b/Census/Census.csproj index 6b0f42bdd..955c51878 100644 --- a/Census/Census.csproj +++ b/Census/Census.csproj @@ -24,7 +24,7 @@ - + diff --git a/DataAcquisition/DataAcquisition.csproj b/DataAcquisition/DataAcquisition.csproj index a24eb98ed..4bfad5304 100644 --- a/DataAcquisition/DataAcquisition.csproj +++ b/DataAcquisition/DataAcquisition.csproj @@ -23,7 +23,7 @@ - + diff --git a/DataAcquisitionTests/DataAcquisitionTests.csproj b/DataAcquisitionTests/DataAcquisitionTests.csproj index 50a88d63f..6b4799f75 100644 --- a/DataAcquisitionTests/DataAcquisitionTests.csproj +++ b/DataAcquisitionTests/DataAcquisitionTests.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/DemoApiGateway/DemoApiGateway.csproj b/DemoApiGateway/DemoApiGateway.csproj index c72bb643f..ff6ad6f76 100644 --- a/DemoApiGateway/DemoApiGateway.csproj +++ b/DemoApiGateway/DemoApiGateway.csproj @@ -10,7 +10,7 @@ - + diff --git a/MeasureEval/MeasureEval.csproj b/MeasureEval/MeasureEval.csproj index e826a3088..6eeaf70cc 100644 --- a/MeasureEval/MeasureEval.csproj +++ b/MeasureEval/MeasureEval.csproj @@ -27,7 +27,7 @@ - + diff --git a/Normalization/Normalization.csproj b/Normalization/Normalization.csproj index cbeb7df62..310b87ccf 100644 --- a/Normalization/Normalization.csproj +++ b/Normalization/Normalization.csproj @@ -21,7 +21,7 @@ - + diff --git a/Notification/Notification.csproj b/Notification/Notification.csproj index db432f507..32afa6c45 100644 --- a/Notification/Notification.csproj +++ b/Notification/Notification.csproj @@ -25,7 +25,7 @@ - + diff --git a/PatientsToQuery/PatientsToQuery.csproj b/PatientsToQuery/PatientsToQuery.csproj index 1a7eb5fe0..dfc42c30b 100644 --- a/PatientsToQuery/PatientsToQuery.csproj +++ b/PatientsToQuery/PatientsToQuery.csproj @@ -10,7 +10,7 @@ - + diff --git a/QueryDispatch/QueryDispatch.csproj b/QueryDispatch/QueryDispatch.csproj index 5374f48ea..7bb9d13ed 100644 --- a/QueryDispatch/QueryDispatch.csproj +++ b/QueryDispatch/QueryDispatch.csproj @@ -19,7 +19,7 @@ - + diff --git a/QueryDispatchTests/QueryDispatchTests.csproj b/QueryDispatchTests/QueryDispatchTests.csproj index 220a99a43..ff18ede99 100644 --- a/QueryDispatchTests/QueryDispatchTests.csproj +++ b/QueryDispatchTests/QueryDispatchTests.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Report/Listeners/MeasureEvaluatedListener.cs b/Report/Listeners/MeasureEvaluatedListener.cs index b0736a8cc..a4bff94be 100644 --- a/Report/Listeners/MeasureEvaluatedListener.cs +++ b/Report/Listeners/MeasureEvaluatedListener.cs @@ -89,7 +89,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (consumeResult == null) { - throw new DeadLetterException($"{Name}: consumeResult is null"); + throw new DeadLetterException($"{Name}: consumeResult is null", AuditEventType.Create); } var key = consumeResult.Message.Key; @@ -107,7 +107,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) key.EndDate == DateTime.MinValue) { throw new DeadLetterException( - $"{Name}: One or more required Key/Value properties are null, empty, or otherwise invalid."); + $"{Name}: One or more required Key/Value properties are null, empty, or otherwise invalid.", AuditEventType.Create); } // find existing report scheduled for this facility, report type, and date range @@ -129,7 +129,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (Exception ex) { throw new DeadLetterException( - $"{Name}: Unable to deserialize MeasureEvaluatedValue.Result: " + value.Result); + $"{Name}: Unable to deserialize MeasureEvaluatedValue.Result: " + value.Result, AuditEventType.Create); } // ensure measure report has an ID to avoid inserting duplicates during bundling @@ -195,7 +195,7 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Create, ex.InnerException), facilityId); } catch (DeadLetterException ex) { @@ -208,7 +208,7 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Query, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/PatientsToQueryListener.cs b/Report/Listeners/PatientsToQueryListener.cs index 22dbf46b2..0244129f4 100644 --- a/Report/Listeners/PatientsToQueryListener.cs +++ b/Report/Listeners/PatientsToQueryListener.cs @@ -72,7 +72,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (consumeResult == null) { throw new DeadLetterException( - $"{Name}: consumeResult is null"); + $"{Name}: consumeResult is null", AuditEventType.Create); } var key = consumeResult.Message.Key; @@ -81,7 +81,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (string.IsNullOrWhiteSpace(key)) { - throw new DeadLetterException($"{Name}: key value is null or empty"); + throw new DeadLetterException($"{Name}: key value is null or empty", AuditEventType.Create); } var scheduledReports = await _mediator.Send(new FindMeasureReportScheduleForFacilityQuery() { FacilityId = key }, cancellationToken); @@ -99,7 +99,7 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand() catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Create, ex.InnerException), facilityId); } catch (DeadLetterException ex) { @@ -112,7 +112,7 @@ await _mediator.Send(new UpdateMeasureReportScheduleCommand() catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Query, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/ReportScheduledListener.cs b/Report/Listeners/ReportScheduledListener.cs index 0c19f6d23..56488c81e 100644 --- a/Report/Listeners/ReportScheduledListener.cs +++ b/Report/Listeners/ReportScheduledListener.cs @@ -81,7 +81,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (consumeResult == null) { throw new DeadLetterException( - $"{Name}: consumeResult is null"); + $"{Name}: consumeResult is null", AuditEventType.Create); } var key = consumeResult.Message.Key; @@ -92,7 +92,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) string.IsNullOrWhiteSpace(key.ReportType)) { throw new DeadLetterException( - $"{Name}: One or more required Key/Value properties are null or empty."); + $"{Name}: One or more required Key/Value properties are null or empty.", AuditEventType.Create); } DateTimeOffset startDateOffset; @@ -100,7 +100,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) value.Parameters.Single(x => x.Key.ToLower() == "startdate").Value, out startDateOffset)) { - throw new DeadLetterException($"{Name}: Start Date could not be parsed"); + throw new DeadLetterException($"{Name}: Start Date could not be parsed", AuditEventType.Create); } DateTimeOffset endDateOffset; @@ -108,7 +108,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) value.Parameters.Single(x => x.Key.ToLower() == "enddate").Value, out endDateOffset)) { - throw new DeadLetterException($"{Name}: End Date could not be parsed"); + throw new DeadLetterException($"{Name}: End Date could not be parsed", AuditEventType.Create); } var startDate = startDateOffset.UtcDateTime; @@ -133,14 +133,14 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (Exception ex) { throw new DeadLetterException( - "ReportScheduledListener: Cron Schedule could not be created from provided dates.", ex.InnerException); + "ReportScheduledListener: Cron Schedule could not be created from provided dates.", AuditEventType.Create, ex.InnerException); } if (string.IsNullOrWhiteSpace(scheduleTrigger)) { throw new DeadLetterException( - "ReportScheduledListener: scheduleTrigger is null or empty."); + "ReportScheduledListener: scheduleTrigger is null or empty.", AuditEventType.Create); } // create or update the consumed report schedule @@ -193,7 +193,7 @@ await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Create, ex.InnerException), facilityId); } catch (DeadLetterException ex) { @@ -206,7 +206,7 @@ await MeasureReportScheduleService.CreateJobAndTrigger(reportSchedule, catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Query, ex.InnerException), facilityId); } finally { diff --git a/Report/Listeners/ReportSubmittedListener.cs b/Report/Listeners/ReportSubmittedListener.cs index 055b8a10d..80a3bf32e 100644 --- a/Report/Listeners/ReportSubmittedListener.cs +++ b/Report/Listeners/ReportSubmittedListener.cs @@ -11,7 +11,6 @@ using LantanaGroup.Link.Shared.Application.Models.Kafka; using MediatR; using System.Text; -using LantanaGroup.Link.Shared.Application.Error.Handlers; namespace LantanaGroup.Link.Report.Listeners { @@ -67,112 +66,108 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) EnableAutoCommit = false }; - using (var consumer = _kafkaConsumerFactory.CreateConsumer(config)) + using var consumer = _kafkaConsumerFactory.CreateConsumer(config); + try { - try - { - consumer.Subscribe(nameof(KafkaTopic.ReportSubmitted)); - _logger.LogInformation($"Started report submitted consumer for topic '{nameof(KafkaTopic.ReportSubmitted)}' at {DateTime.UtcNow}"); + consumer.Subscribe(nameof(KafkaTopic.ReportSubmitted)); + _logger.LogInformation($"Started report submitted consumer for topic '{nameof(KafkaTopic.ReportSubmitted)}' at {DateTime.UtcNow}"); - while (!cancellationToken.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested) + { + var consumeResult = new ConsumeResult(); + string facilityId = string.Empty; + try { - var consumeResult = new ConsumeResult(); - string facilityId = string.Empty; - try - { - consumeResult = consumer.Consume(cancellationToken); + consumeResult = consumer.Consume(cancellationToken); - if (consumeResult == null) - { - throw new DeadLetterException( - $"{Name}: consumeResult is null"); - } + if (consumeResult == null) + { + throw new DeadLetterException( + $"{Name}: consumeResult is null", AuditEventType.Create); + } - var key = consumeResult.Message.Key; - var value = consumeResult.Message.Value; - facilityId = key.FacilityId; + var key = consumeResult.Message.Key; + var value = consumeResult.Message.Value; + facilityId = key.FacilityId; - // find existing report schedule - MeasureReportScheduleModel schedule = await _mediator.Send(new GetMeasureReportScheduleByBundleIdQuery { ReportBundleId = value.ReportBundleId }); + // find existing report schedule + MeasureReportScheduleModel schedule = await _mediator.Send(new GetMeasureReportScheduleByBundleIdQuery { ReportBundleId = value.ReportBundleId }); - if (schedule is null) - { - throw new TransientException( - $"{Name}: No report schedule found for submission bundle with ID {value.ReportBundleId}"); - } + if (schedule is null) + { + throw new TransientException( + $"{Name}: No report schedule found for submission bundle with ID {value.ReportBundleId}", AuditEventType.Query); + } - // update report schedule with submitted date - schedule.SubmittedDate = DateTime.UtcNow; - await _mediator.Send(new UpdateMeasureReportScheduleCommand { ReportSchedule = schedule }); + // update report schedule with submitted date + schedule.SubmittedDate = DateTime.UtcNow; + await _mediator.Send(new UpdateMeasureReportScheduleCommand { ReportSchedule = schedule }); - // produce audit message signalling the report service acknowledged the report has been submitted - using var producer = _kafkaProducerFactory.CreateAuditEventProducer(); + // produce audit message signalling the report service acknowledged the report has been submitted + using var producer = _kafkaProducerFactory.CreateAuditEventProducer(); - string notes = - $"{ReportConstants.ServiceName} has processed the {nameof(KafkaTopic.ReportSubmitted)} event for report bundle with ID {value.ReportBundleId} with report schedule ID {schedule.Id}"; - var val = new AuditEventMessage - { - FacilityId = schedule.FacilityId, - ServiceName = ReportConstants.ServiceName, - Action = AuditEventType.Submit, - EventDate = DateTime.UtcNow, - Resource = typeof(MeasureReportScheduleModel).Name, - Notes = notes - }; - var headers = new Headers + string notes = + $"{ReportConstants.ServiceName} has processed the {nameof(KafkaTopic.ReportSubmitted)} event for report bundle with ID {value.ReportBundleId} with report schedule ID {schedule.Id}"; + var val = new AuditEventMessage + { + FacilityId = schedule.FacilityId, + ServiceName = ReportConstants.ServiceName, + Action = AuditEventType.Submit, + EventDate = DateTime.UtcNow, + Resource = typeof(MeasureReportScheduleModel).Name, + Notes = notes + }; + var headers = new Headers { { "X-Correlation-Id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) } }; - producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), - new Message - { - Value = val, - Headers = headers - }); - producer.Flush(); - _logger.LogInformation(notes); - } - catch (ConsumeException ex) - { - _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); - } - catch (DeadLetterException ex) - { - _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); - } - catch (TransientException ex) - { - _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); - } - catch (Exception ex) + producer.Produce(nameof(KafkaTopic.AuditableEventOccurred), + new Message + { + Value = val, + Headers = headers + }); + producer.Flush(); + _logger.LogInformation(notes); + } + catch (ConsumeException ex) + { + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Create, ex.InnerException), facilityId); + } + catch (DeadLetterException ex) + { + _deadLetterExceptionHandler.HandleException(consumeResult, ex, facilityId); + } + catch (TransientException ex) + { + _transientExceptionHandler.HandleException(consumeResult, ex, facilityId); + } + catch (Exception ex) + { + _deadLetterExceptionHandler.HandleException(consumeResult, + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Query, ex.InnerException), facilityId); + } + finally + { + if (consumeResult != null) { - _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + consumer.Commit(consumeResult); } - finally + else { - if (consumeResult != null) - { - consumer.Commit(consumeResult); - } - else - { - consumer.Commit(); - } + consumer.Commit(); } } } - catch (OperationCanceledException oce) - { - _logger.LogError($"Operation Canceled: {oce.Message}", oce); - consumer.Close(); - consumer.Dispose(); - } } - + catch (OperationCanceledException oce) + { + _logger.LogError($"Operation Canceled: {oce.Message}", oce); + consumer.Close(); + consumer.Dispose(); + } } - } } diff --git a/Report/Report.csproj b/Report/Report.csproj index 2467fa770..8789c9f79 100644 --- a/Report/Report.csproj +++ b/Report/Report.csproj @@ -22,7 +22,7 @@ - + diff --git a/Submission/Listeners/SubmitReportListener.cs b/Submission/Listeners/SubmitReportListener.cs index a225a8aee..04fe7d267 100644 --- a/Submission/Listeners/SubmitReportListener.cs +++ b/Submission/Listeners/SubmitReportListener.cs @@ -79,7 +79,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) consumeResult = consumer.Consume(cancellationToken); if (consumeResult == null) { - throw new DeadLetterException($"{Name}: consumeResult is null"); + throw new DeadLetterException($"{Name}: consumeResult is null", AuditEventType.Create); } var key = consumeResult.Message.Key; @@ -91,7 +91,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) string.IsNullOrWhiteSpace(value.MeasureReportScheduleId)) { throw new DeadLetterException( - $"{Name}: One or more required Key/Value properties are null or empty."); + $"{Name}: One or more required Key/Value properties are null or empty.", AuditEventType.Create); } string requestUrl = _submissionConfig.ReportServiceUrl + $"?reportId={value.MeasureReportScheduleId}"; @@ -114,14 +114,14 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) if (!File.Exists(fullFilePath)) { - throw new TransientException($"{Name}: Bundle File Not Created"); + throw new TransientException($"{Name}: Bundle File Not Created", AuditEventType.Create); } #endregion } catch (ConsumeException ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Create, ex.InnerException), facilityId); } catch (DeadLetterException ex) { @@ -134,7 +134,7 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) catch (Exception ex) { _deadLetterExceptionHandler.HandleException(consumeResult, - new DeadLetterException($"{Name}: " + ex.Message, ex.InnerException), facilityId); + new DeadLetterException($"{Name}: " + ex.Message, AuditEventType.Query, ex.InnerException), facilityId); } finally { @@ -155,7 +155,6 @@ private async void StartConsumerLoop(CancellationToken cancellationToken) consumer.Close(); consumer.Dispose(); } - } } } diff --git a/Submission/Submission.csproj b/Submission/Submission.csproj index 4ce8dbacc..bfac4d3c8 100644 --- a/Submission/Submission.csproj +++ b/Submission/Submission.csproj @@ -26,7 +26,7 @@ - + diff --git a/Tenant/Tenant.csproj b/Tenant/Tenant.csproj index 720c89fe5..66b0130d4 100644 --- a/Tenant/Tenant.csproj +++ b/Tenant/Tenant.csproj @@ -25,7 +25,7 @@ - + diff --git a/TenantTests/TenantTests.csproj b/TenantTests/TenantTests.csproj index 052c8b77b..8068b4e92 100644 --- a/TenantTests/TenantTests.csproj +++ b/TenantTests/TenantTests.csproj @@ -21,7 +21,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 5eeff6153872e43de16f36a4f43c40867cc4bbc4 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 09:35:11 -0500 Subject: [PATCH 44/79] hard set Shared version in Data Acq tests --- DataAcquisitionTests/DataAcquisitionTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DataAcquisitionTests/DataAcquisitionTests.csproj b/DataAcquisitionTests/DataAcquisitionTests.csproj index 6b4799f75..d6259c7e9 100644 --- a/DataAcquisitionTests/DataAcquisitionTests.csproj +++ b/DataAcquisitionTests/DataAcquisitionTests.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 0fe7922db6b5fc8723ad91ebc7ff14d61dbb6d06 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 09:40:46 -0500 Subject: [PATCH 45/79] Update QueryListener --- DataAcquisition/Listeners/QueryListener.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index 58d0e0397..abc908f79 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -77,7 +77,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); continue; } @@ -94,7 +94,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error deserializing message: {1}", ex.Message); - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); continue; } @@ -107,7 +107,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error extracting facility id and correlation id from message"); - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); continue; } @@ -115,7 +115,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca { var errorMessage = "No Facility ID provided. Unable to process message: {1}"; _logger.LogWarning(errorMessage, message); - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), ""); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), AuditEventType.Create, ""); continue; } @@ -138,7 +138,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, messageMetaData.facilityId); _logger.LogError(ex,"Error producing message: {1}", ex.Message); responseMessages = null; continue; @@ -195,7 +195,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, messageMetaData.facilityId); _logger.LogError(ex, "Failed to produce message"); continue; } @@ -214,7 +214,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca Notes = $"Message with topic: {rawmessage.Topic} meets no condition for processing. full message: {rawmessage.Message}", }); _logger.LogWarning("Message with topic: {1} meets no condition for processing. full message: {2}", rawmessage.Topic, rawmessage.Message); - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), AuditEventType.Create, messageMetaData.facilityId); } } } From 5de3ae535ca1e6c343c47e372059862381ced5d8 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 09:44:27 -0500 Subject: [PATCH 46/79] Remove Shared from Data Acq tests --- DataAcquisitionTests/DataAcquisitionTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/DataAcquisitionTests/DataAcquisitionTests.csproj b/DataAcquisitionTests/DataAcquisitionTests.csproj index 6b4799f75..c8380c4f6 100644 --- a/DataAcquisitionTests/DataAcquisitionTests.csproj +++ b/DataAcquisitionTests/DataAcquisitionTests.csproj @@ -20,7 +20,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - From fc4b458910e93321fc31031451e28ae3d3f90358 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 09:48:26 -0500 Subject: [PATCH 47/79] Remove Shared from Query Dispatch tests --- QueryDispatchTests/QueryDispatchTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/QueryDispatchTests/QueryDispatchTests.csproj b/QueryDispatchTests/QueryDispatchTests.csproj index ff18ede99..3445107e5 100644 --- a/QueryDispatchTests/QueryDispatchTests.csproj +++ b/QueryDispatchTests/QueryDispatchTests.csproj @@ -20,7 +20,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - From 4ac18d48c2b9c5caf49e7382e3197a4dc32c3c13 Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 10:02:34 -0500 Subject: [PATCH 48/79] Copy/Paste Ed's data acq test csproj --- .../DataAcquisitionTests.csproj | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/DataAcquisitionTests/DataAcquisitionTests.csproj b/DataAcquisitionTests/DataAcquisitionTests.csproj index c8380c4f6..1c7c7938f 100644 --- a/DataAcquisitionTests/DataAcquisitionTests.csproj +++ b/DataAcquisitionTests/DataAcquisitionTests.csproj @@ -1,29 +1,29 @@  - - net8.0 - enable - enable + + net8.0 + enable + enable - false - + false + - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - + + + - + \ No newline at end of file From aec4a0a7ae3a0ab40b3f6ce62d8af8d83ad7231f Mon Sep 17 00:00:00 2001 From: MontaltoNick Date: Fri, 22 Mar 2024 10:14:24 -0500 Subject: [PATCH 49/79] Revert Data Acq Shared changes --- DataAcquisition/DataAcquisition.csproj | 100 ++++++++++----------- DataAcquisition/Listeners/QueryListener.cs | 22 ++--- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/DataAcquisition/DataAcquisition.csproj b/DataAcquisition/DataAcquisition.csproj index 4bfad5304..96d339d87 100644 --- a/DataAcquisition/DataAcquisition.csproj +++ b/DataAcquisition/DataAcquisition.csproj @@ -1,15 +1,15 @@  - - net8.0 - enable - enable - 40b6b0d0-fb4c-4650-a7c8-146c74214acb - Linux - ..\docker-compose.dcproj - LantanaGroup.Link.DataAcquisition - $(MSBuildProjectName) - + + net8.0 + enable + enable + 40b6b0d0-fb4c-4650-a7c8-146c74214acb + Linux + ..\docker-compose.dcproj + LantanaGroup.Link.DataAcquisition + $(MSBuildProjectName) + true @@ -17,47 +17,47 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - Always - - + + + Always + + diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index abc908f79..6a7b45a43 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -25,7 +25,7 @@ public class QueryListener : BackgroundService private readonly IKafkaProducerFactory _kafkaProducerFactory; private readonly IDeadLetterExceptionHandler _deadLetterConsumerHandler; - + private readonly ILogger _logger; private readonly IMediator _mediator; @@ -55,7 +55,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken cancellationToken) { - var settings = new ConsumerConfig + var settings = new ConsumerConfig { EnableAutoCommit = false, GroupId = "DataAcquisition-Query" @@ -77,7 +77,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } @@ -94,7 +94,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error deserializing message: {1}", ex.Message); - _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } @@ -107,15 +107,15 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error extracting facility id and correlation id from message"); - _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } - + if (string.IsNullOrWhiteSpace(messageMetaData.facilityId)) { var errorMessage = "No Facility ID provided. Unable to process message: {1}"; _logger.LogWarning(errorMessage, message); - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), AuditEventType.Create, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), ""); continue; } @@ -138,8 +138,8 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, messageMetaData.facilityId); - _logger.LogError(ex,"Error producing message: {1}", ex.Message); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _logger.LogError(ex, "Error producing message: {1}", ex.Message); responseMessages = null; continue; } @@ -195,7 +195,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { - _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Create, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); _logger.LogError(ex, "Failed to produce message"); continue; } @@ -214,7 +214,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca Notes = $"Message with topic: {rawmessage.Topic} meets no condition for processing. full message: {rawmessage.Message}", }); _logger.LogWarning("Message with topic: {1} meets no condition for processing. full message: {2}", rawmessage.Topic, rawmessage.Message); - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), AuditEventType.Create, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), messageMetaData.facilityId); } } } From 9e2556977100e4c68466681b1f96efaedf059662 Mon Sep 17 00:00:00 2001 From: edward-miller-lcg <119338797+edward-miller-lcg@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:52:57 -0500 Subject: [PATCH 50/79] Add topic and service name --- DataAcquisition/Listeners/QueryListener.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index 58d0e0397..23c60e221 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -41,6 +41,7 @@ public QueryListener( _kafkaConsumerFactory = kafkaConsumerFactory ?? throw new ArgumentNullException(nameof(kafkaConsumerFactory)); _kafkaProducerFactory = kafkaProducerFactory ?? throw new ArgumentNullException(nameof(kafkaProducerFactory)); _deadLetterConsumerHandler = deadLetterConsumerHandler ?? throw new ArgumentNullException(nameof(deadLetterConsumerHandler)); + _deadLetterConsumerHandler.ServiceName = DataAcquisitionConstants.ServiceName; } public override async Task StartAsync(CancellationToken cancellationToken) @@ -77,6 +78,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } @@ -94,6 +96,8 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error deserializing message: {1}", ex.Message); + + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } @@ -107,6 +111,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _logger.LogError(ex, "Error extracting facility id and correlation id from message"); + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); continue; } @@ -138,6 +143,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); _logger.LogError(ex,"Error producing message: {1}", ex.Message); responseMessages = null; @@ -195,6 +201,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca } catch (Exception ex) { + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); _logger.LogError(ex, "Failed to produce message"); continue; @@ -214,6 +221,8 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca Notes = $"Message with topic: {rawmessage.Topic} meets no condition for processing. full message: {rawmessage.Message}", }); _logger.LogWarning("Message with topic: {1} meets no condition for processing. full message: {2}", rawmessage.Topic, rawmessage.Message); + + _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), messageMetaData.facilityId); } } From c61175ec907dcfac8db17fb50acefb96270a060a Mon Sep 17 00:00:00 2001 From: Ariana D Mihailescu <82962995+arianamihailescu@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:44:05 -0400 Subject: [PATCH 51/79] LNK-2160-added app settings development and check in case the connection to ACA fails --- Tenant/Program.cs | 43 +++++++------ Tenant/appsettings.Development.json | 97 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 Tenant/appsettings.Development.json diff --git a/Tenant/Program.cs b/Tenant/Program.cs index 9f1715d50..311bb06d7 100644 --- a/Tenant/Program.cs +++ b/Tenant/Program.cs @@ -1,6 +1,4 @@ -using LantanaGroup.Link.Tenant; -using LantanaGroup.Link.Tenant.Listeners; using LantanaGroup.Link.Tenant.Services; using Quartz; using Serilog; @@ -56,28 +54,33 @@ static void RegisterServices(WebApplicationBuilder builder) //load external configuration source if specified var externalConfigurationSource = builder.Configuration.GetSection(TenantConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); - if (!string.IsNullOrEmpty(externalConfigurationSource)) + + if ("Development" != builder.Environment.EnvironmentName) { - switch (externalConfigurationSource) + + if (!string.IsNullOrEmpty(externalConfigurationSource)) { - case ("AzureAppConfiguration"): - builder.Configuration.AddAzureAppConfiguration(options => - { - options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) - // Load configuration values with no label - .Select("*", LabelFilter.Null) - // Load configuration values for service name - .Select("*", TenantConstants.ServiceName) - // Load configuration values for service name and environment - .Select("*", TenantConstants.ServiceName + ":" + builder.Environment.EnvironmentName); - - options.ConfigureKeyVault(kv => + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => { - kv.SetCredential(new DefaultAzureCredential()); - }); + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", TenantConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", TenantConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); - }); - break; + }); + break; + } } } diff --git a/Tenant/appsettings.Development.json b/Tenant/appsettings.Development.json new file mode 100644 index 000000000..b67e869f8 --- /dev/null +++ b/Tenant/appsettings.Development.json @@ -0,0 +1,97 @@ +{ + "ExternalConfigurationSource": "AzureAppConfiguration", + + "AllowReflection": true, + + "EnableSwagger": true, + + "EnableHealthChecks": true, + + "MongoDB": { + "ConnectionString": "mongodb://localhost:27017/", + "DatabaseName": "TenantStore", + "CollectionName": "Facilities" + }, + + "AllowedHosts": "*", + + "KafkaConnection": { + "BootstrapServers": [ "localhost:9092" ], + "GroupId": "Tenant" + }, + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "LokiLoggingProvider": "Trace" + } + }, + + "Link:Tenant:MeasureServiceRegistry": { + "MeasureServiceApiUrl": "http://localhost:5135/api/measureDef" + }, + + "Link:Tenant:ServiceInformation": { + "Name": "Link Tenant Service", + "Version": "1.1.0-beta" + }, + + "Link:Tenant:TelemetryConfig": { + "EnableRuntimeInstrumentation": false, + "TraceExporterEndpoint": "http://localhost:4317/", + "MetricsEndpoint": "http://localhost:9101", + "TelemetryCollectorEndpoint": "http://localhost:4317" + }, + + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7331" + }, + "https": { + "Url": "https://localhost:7332" + }, + "Grpc": { + "Url": "http://localhost:7333", + "Protocols": "Http2" + } + } + }, + "Link:Tenant:Serilog": { + "Using": [ + "Serilog.Sinks.Grafana.Loki" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "GrafanaLoki", + "Args": { + "labels": [ + { + "key": "app", + "value": "Link" + }, + { + "key": "component", + "value": "Tenant" + } + ], + "propertiesAsLabels": [ + "app", + "component" + ] + } + } + ] + } +} From 4da055e1a382f1f1c49afb4201b79648e323dbc5 Mon Sep 17 00:00:00 2001 From: Ariana D Mihailescu <82962995+arianamihailescu@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:52:45 -0400 Subject: [PATCH 52/79] LNK-2160-added app settings development and check in case the connection to ACA fails --- Tenant/appsettings.Development.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tenant/appsettings.Development.json b/Tenant/appsettings.Development.json index b67e869f8..41ecb8547 100644 --- a/Tenant/appsettings.Development.json +++ b/Tenant/appsettings.Development.json @@ -28,16 +28,16 @@ } }, - "Link:Tenant:MeasureServiceRegistry": { + "MeasureServiceRegistry": { "MeasureServiceApiUrl": "http://localhost:5135/api/measureDef" }, - "Link:Tenant:ServiceInformation": { + "ServiceInformation": { "Name": "Link Tenant Service", "Version": "1.1.0-beta" }, - "Link:Tenant:TelemetryConfig": { + "TelemetryConfig": { "EnableRuntimeInstrumentation": false, "TraceExporterEndpoint": "http://localhost:4317/", "MetricsEndpoint": "http://localhost:9101", @@ -58,7 +58,7 @@ } } }, - "Link:Tenant:Serilog": { + "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" ], From bf290138786f69cedea1376a76c8b17ecffa526a Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Fri, 22 Mar 2024 21:04:34 -0400 Subject: [PATCH 53/79] Initial stages of adding authentication and authorization --- .../JwtBearerAuthSchemeExtension.cs | 44 +++++++++++++ LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + LinkAdmin.BFF/Program.cs | 62 ++++++++++++++++++- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 6 ++ LinkAdmin.BFF/appsettings.Development.json | 15 +++++ LinkAdmin.BFF/appsettings.json | 16 ++++- 6 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/JwtBearerAuthSchemeExtension.cs diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/JwtBearerAuthSchemeExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/JwtBearerAuthSchemeExtension.cs new file mode 100644 index 000000000..672220121 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/JwtBearerAuthSchemeExtension.cs @@ -0,0 +1,44 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Authentication; +using System.IdentityModel.Tokens.Jwt; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class JwtBearerAuthSchemeExtension + { + public static AuthenticationBuilder AddJwTBearerAuthentication(this AuthenticationBuilder builder, Action? options) + { + var jwtBearerOptions = new JwTBearerOptions(); + options?.Invoke(jwtBearerOptions); + + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + builder.AddJwtBearer(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken, options => + { + options.Authority = jwtBearerOptions.Authority; + options.Audience = jwtBearerOptions.Audience; + options.RequireHttpsMetadata = !jwtBearerOptions.Environment.IsDevelopment(); + + options.TokenValidationParameters = new() + { + NameClaimType = jwtBearerOptions.NameClaimType, + RoleClaimType = jwtBearerOptions.RoleClaimType, + //avoid jwt confustion attacks (ie: circumvent token signature checking) + ValidTypes = jwtBearerOptions.ValidTypes + }; + }); + + return builder; + + } + } + + public class JwTBearerOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public string? Authority { get; set; } = null!; + public string? Audience { get; set; } = null!; + public string? NameClaimType { get; set; } = null!; + public string? RoleClaimType { get; set; } = null!; + public string[]? ValidTypes { get; set; } = ["at+jwt", "JWT"]; + } +} diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index 4b9aa6af9..0cb6bc33a 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -22,6 +22,7 @@ + diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 1c30c9152..97cfa888a 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -6,6 +6,7 @@ using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions; using LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints; using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Validation; using LantanaGroup.Link.Shared.Application.Factories; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Configs; @@ -16,7 +17,10 @@ using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; -using LantanaGroup.Link.LinkAdmin.BFF.Application.Validation; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.Identity.Client; +using Microsoft.Extensions.Azure; + var builder = WebApplication.CreateBuilder(args); @@ -66,6 +70,56 @@ static void RegisterServices(WebApplicationBuilder builder) // Add Kafka Producer Factories builder.Services.AddSingleton, KafkaProducerFactory>(); + // Add Authentication + List authSchemes = [ LinkAdminConstants.AuthenticationSchemes.Cookie ]; + var authBuilder = builder.Services.AddAuthentication(); + authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => + { + options.Cookie.Name = LinkAdminConstants.AuthenticationSchemes.Cookie; + options.Cookie.SameSite = SameSiteMode.Strict; + options.ExpireTimeSpan = TimeSpan.FromMinutes(30); + }); + + if(builder.Configuration.GetValue("Authentication:Schemes:Jwt:Enabled")) + { + authSchemes.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); + + authBuilder.AddJwTBearerAuthentication(options => + { + options.Environment = builder.Environment; + options.Authority = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Authority"); + options.Audience = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Audience"); + options.NameClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:NameClaimType"); + options.RoleClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:RoleClaimType"); + }); + //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + //authBuilder.Services.AddAuthentication() + // .AddJwtBearer(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken, options => + // { + // options.Authority = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Authority"); + // options.Audience = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Audience"); + // options.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); + + // options.TokenValidationParameters = new() + // { + // NameClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:NameClaimType"), + // RoleClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:RoleClaimType"), + // //avoid jwt confustion attacks (ie: circumvent token signature checking) + // ValidTypes = builder.Configuration.GetValue("Authentication:Schemes:Jwt:ValidTypes") + // }; + // }); + } + + // Add Authorization + builder.Services.AddAuthorization(builder => + { + builder.AddPolicy("AuthenticatedUser", pb => { + pb.RequireAuthenticatedUser() + .AddAuthenticationSchemes([.. authSchemes]); + }); + }); + + // Add Endpoints builder.Services.AddTransient(); if (builder.Configuration.GetValue("EnableIntegrationFeature")) @@ -176,12 +230,14 @@ static void SetupMiddleware(WebApplication app) }); } + + app.UseRouting(); var corsConfig = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); app.UseCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName); - //app.UseAuthentication(); + app.UseAuthentication(); //app.UseMiddleware(); - //app.UseAuthorization(); + app.UseAuthorization(); // Register endpoints var apis = app.Services.GetServices(); diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 5024590ac..37943625f 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -14,6 +14,12 @@ public static class AppSettingsSectionNames public const string EnableSwagger = "EnableSwagger"; } + public static class AuthenticationSchemes + { + public const string Cookie = "link_cookie"; + public const string JwtBearerToken = "link_jwt_bearer"; + } + public static class LinkAdminLoggingIds { public const int RequestRecieved = 1000; diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index 859b886f4..5d8313013 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -9,6 +9,21 @@ }, "EnableSwagger": true, "EnableIntegrationFeature": true, + "Authentication": { + "DefaultScheme": "Bearer", + "DefaultChallengeScheme": "Bearer", + "DefaultAuthenticateScheme": "Bearer", + "Schemes": { + "Jwt": { + "Enabled": false, + "Authority": "", + "Audience": "", + "NameClaimType": "email", + "RoleClaimType": "roles", + "ValidTypes": [ "at+jwt", "JWT" ] + } + } + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 407a9debc..0c21c1a1b 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -17,10 +17,24 @@ "MaxAge": 600 }, "EnableSwagger": true, - "EnableIntegrationFeature": true, + "EnableIntegrationFeature": true, "ProblemDetails": { "IncludeExceptionDetails": false }, + "Authentication": { + "DefaultScheme": "Bearer", + "DefaultChallengeScheme": "Bearer", + "DefaultAuthenticateScheme": "Bearer", + "Schemes": { + "Jwt": { + "Enabled": false, + "Authority": "", + "Audience": "", + "NameClaimType": "email", + "RoleClaimType": "roles" + } + } + }, "Logging": { "LogLevel": { "Default": "Information", From 2bc20cf693d3c82d157a263a0431f8ba1b442405 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Sat, 23 Mar 2024 20:31:13 -0400 Subject: [PATCH 54/79] Added initial attempt at SAMS OAuthOptions --- .../Authentication/Options/SamsOptions.cs | 31 +++++++++++++++++++ LinkAdmin.BFF/Program.cs | 6 ++-- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs diff --git a/LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs b/LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs new file mode 100644 index 000000000..ce4ea9436 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.Options +{ + public class SamsOptions : OAuthOptions + { + public SamsOptions() + { + CallbackPath = "/auth/signin-sams"; + AuthorizationEndpoint = ""; + TokenEndpoint = ""; + UserInformationEndpoint = ""; + Scope.Add("openid"); + Scope.Add("profile"); + Scope.Add("email"); + + ClaimActions.MapJsonKey("sub", "sub"); + ClaimActions.MapJsonSubKey("account_type", "profile", "account_type"); + ClaimActions.MapJsonSubKey("account_id", "profile", "account_id"); + ClaimActions.MapJsonSubKey("name", "profile", "name"); + ClaimActions.MapJsonSubKey("family_name", "profile", "family_name"); + ClaimActions.MapJsonSubKey("middle_name", "profile", "middle_name"); + ClaimActions.MapJsonSubKey("given_name", "profile", "given_name"); + ClaimActions.MapJsonSubKey("preferred_name", "profile", "preferred_name"); + ClaimActions.MapJsonSubKey("name_suffix", "profile", "name_suffix"); + ClaimActions.MapJsonKey("email", "email"); + + } + } +} diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 97cfa888a..98fab8180 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -71,7 +71,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton, KafkaProducerFactory>(); // Add Authentication - List authSchemes = [ LinkAdminConstants.AuthenticationSchemes.Cookie ]; + List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie ]; var authBuilder = builder.Services.AddAuthentication(); authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => { @@ -82,7 +82,7 @@ static void RegisterServices(WebApplicationBuilder builder) if(builder.Configuration.GetValue("Authentication:Schemes:Jwt:Enabled")) { - authSchemes.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); + authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); authBuilder.AddJwTBearerAuthentication(options => { @@ -115,7 +115,7 @@ static void RegisterServices(WebApplicationBuilder builder) { builder.AddPolicy("AuthenticatedUser", pb => { pb.RequireAuthenticatedUser() - .AddAuthenticationSchemes([.. authSchemes]); + .AddAuthenticationSchemes([.. authSchemas]); }); }); From ea249041b9bef8107a0901a800220759accecc69 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Sun, 24 Mar 2024 10:00:33 -0400 Subject: [PATCH 55/79] Further refinement of the SAMS CDC oauth extension --- .../Authentication/CdcSams/SamsDefaults.cs | 35 +++++++++++++ .../Authentication/CdcSams/SamsExtensions.cs | 50 +++++++++++++++++++ .../Authentication/CdcSams/SamsHandler.cs | 46 +++++++++++++++++ .../{Options => CdcSams}/SamsOptions.cs | 32 +++++++++--- LinkAdmin.BFF/Program.cs | 3 ++ 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsDefaults.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsExtensions.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsHandler.cs rename LinkAdmin.BFF/Infrastructure/Authentication/{Options => CdcSams}/SamsOptions.cs (63%) diff --git a/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsDefaults.cs b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsDefaults.cs new file mode 100644 index 000000000..a401fd9d9 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsDefaults.cs @@ -0,0 +1,35 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams +{ + public static class SamsDefaults + { + /// + /// The default scheme used for CDC SAMS authentication + /// + public const string AuthenticationScheme = "CDC-SAMS"; + + /// + /// The display name used for CDC SAMS authentication + /// + public static readonly string DisplayName = "CDC-SAMS"; + + /// + /// The default endpoint used to perform CDC SAMS authentication. + /// + public static readonly string AuthorizationEndpoint = ""; + + /// + /// The default endpoint used to retrieve the token. + /// + public static readonly string TokenEndpoint = ""; + + /// + /// The default endpoint used to to validate the token. + /// + public static readonly string IntrospectionEndpoint = ""; + + /// + /// The default endpoint used to retrieve user information. + /// + public static readonly string UserInformationEndpoint = ""; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsExtensions.cs b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsExtensions.cs new file mode 100644 index 000000000..3ab9f785a --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsExtensions.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Authentication; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams +{ + public static class SamsAuthenticationOptionsExtensions + { + /// + /// Add CDC SAMS authentication to the specified , which enables CDC SAMS authentication capabilities. + /// The default scheme is set to . + /// + /// The . + /// A reference to after the operation as completed. + public static AuthenticationBuilder AddCdcSams(this AuthenticationBuilder builder) + => builder.AddCdcSams(SamsDefaults.AuthenticationScheme, _ => { }); + + /// + /// Add CDC SAMS authentication to the specified , which enables CDC SAMS authentication capabilities. + /// The default scheme is set to . + /// + /// The . + /// A delegate to configure . + /// + public static AuthenticationBuilder AddCdcSams(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddCdcSams(SamsDefaults.AuthenticationScheme, configureOptions => { }); + + /// + /// Add CDC SAMS authentication to the specified , which enables CDC SAMS authentication capabilities. + /// The default scheme is set to . + /// + /// The . + /// The authentication scheme. + /// A delegate to configure . + /// + public static AuthenticationBuilder AddCdcSams(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddCdcSams(authenticationScheme, SamsDefaults.DisplayName, configureOptions); + + /// + /// Add CDC SAMS authentication to the specified , which enables CDC SAMS authentication capabilities. + /// The default scheme is set to . + /// + /// The . + /// The authentication scheme. + /// A display name for the authentication handler + /// A delegate to configure . + /// + public static AuthenticationBuilder AddCdcSams(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsHandler.cs b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsHandler.cs new file mode 100644 index 000000000..71f80ecd7 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsHandler.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams +{ + public class SamsHandler : OAuthHandler + { + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] + public SamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) { } + + public SamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) { } + + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + { + var endpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, "access_token", tokens.AccessToken!); + + var resposne = await Backchannel.GetAsync(endpoint, Context.RequestAborted); + if(!resposne.IsSuccessStatusCode) + { + throw new HttpRequestException($"An error occurred when retrieving SAMS user information ({resposne.StatusCode}). Please check the logs for more information."); + } + + using (var payload = JsonDocument.Parse(await resposne.Content.ReadAsStringAsync(Context.RequestAborted))) + { + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); + context.RunClaimActions(); + await Options.Events.CreatingTicket(context); + + //TODO: Get Application specific claims + + return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); + } + + } + + + + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsOptions.cs similarity index 63% rename from LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs rename to LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsOptions.cs index ce4ea9436..3e8e77477 100644 --- a/LinkAdmin.BFF/Infrastructure/Authentication/Options/SamsOptions.cs +++ b/LinkAdmin.BFF/Infrastructure/Authentication/CdcSams/SamsOptions.cs @@ -1,16 +1,13 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OAuth; -namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.Options +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams { public class SamsOptions : OAuthOptions { public SamsOptions() { - CallbackPath = "/auth/signin-sams"; - AuthorizationEndpoint = ""; - TokenEndpoint = ""; - UserInformationEndpoint = ""; + CallbackPath = "/signin-sams"; Scope.Add("openid"); Scope.Add("profile"); Scope.Add("email"); @@ -23,9 +20,28 @@ public SamsOptions() ClaimActions.MapJsonSubKey("middle_name", "profile", "middle_name"); ClaimActions.MapJsonSubKey("given_name", "profile", "given_name"); ClaimActions.MapJsonSubKey("preferred_name", "profile", "preferred_name"); - ClaimActions.MapJsonSubKey("name_suffix", "profile", "name_suffix"); - ClaimActions.MapJsonKey("email", "email"); - + ClaimActions.MapJsonSubKey("name_suffix", "profile", "name_suffix"); + ClaimActions.MapJsonKey("email", "email"); } + + public override void Validate() + { + ArgumentException.ThrowIfNullOrEmpty(AppId); + ArgumentException.ThrowIfNullOrEmpty(AppSecret); + + base.Validate(); + } + + public string AppId + { + get { return ClientId; } + set { ClientId = value; } + } + + public string AppSecret + { + get { return ClientSecret; } + set { ClientSecret = value; } + } } } diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 98fab8180..589f69f0f 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -20,6 +20,7 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.Identity.Client; using Microsoft.Extensions.Azure; +using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams; var builder = WebApplication.CreateBuilder(args); @@ -109,6 +110,8 @@ static void RegisterServices(WebApplicationBuilder builder) // }; // }); } + + // Add Authorization builder.Services.AddAuthorization(builder => From ec4d4e5e1c092f949e80423994d04a2fa51cf17b Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Sun, 24 Mar 2024 14:23:17 -0400 Subject: [PATCH 56/79] In progress authentication confgiruation --- LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + .../Presentation/Endpoints/AuthEndpoints.cs | 9 +- LinkAdmin.BFF/Program.cs | 82 +++++++++++++++---- LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 2 + LinkAdmin.BFF/appsettings.Development.json | 22 +++-- LinkAdmin.BFF/appsettings.json | 19 +++-- 6 files changed, 101 insertions(+), 34 deletions(-) diff --git a/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/LinkAdmin.BFF/LinkAdmin.BFF.csproj index 0cb6bc33a..41e7ea8c9 100644 --- a/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -23,6 +23,7 @@ + diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index bff7634c5..42b656d53 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -32,7 +32,7 @@ public void RegisterEndpoints(WebApplication app) }); authEndpoints.MapGet("/user", GetUser) - .RequireAuthorization() + .RequireAuthorization("AuthenticatedUser") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .ProducesProblem(StatusCodes.Status500InternalServerError) @@ -57,9 +57,10 @@ public void RegisterEndpoints(WebApplication app) } - public async Task Login() - { - return Results.Ok(new { Message = "Login" }); + public IResult Login() + { + //TODO: DI authentication schema options from settings + return Results.Challenge(authenticationSchemes: ["link_oauth2"]); } public async Task GetUser() diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 589f69f0f..0b20a26ba 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -17,10 +17,10 @@ using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; -using System.IdentityModel.Tokens.Jwt; -using Microsoft.Identity.Client; -using Microsoft.Extensions.Azure; -using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Authentication.CdcSams; +using System.Net.Http.Headers; +using Newtonsoft.Json.Linq; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(args); @@ -72,26 +72,82 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton, KafkaProducerFactory>(); // Add Authentication - List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie ]; - var authBuilder = builder.Services.AddAuthentication(); + List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie, LinkAdminConstants.AuthenticationSchemes.Oauth2]; + var authBuilder = builder.Services.AddAuthentication(options => { + options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; + options.DefaultChallengeScheme = LinkAdminConstants.AuthenticationSchemes.Oauth2; + }); + authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => { options.Cookie.Name = LinkAdminConstants.AuthenticationSchemes.Cookie; options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); }); + + authBuilder.AddOAuth(LinkAdminConstants.AuthenticationSchemes.Oauth2, options => + { + options.SignInScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; + + options.AuthorizationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Authorization")!; + options.TokenEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Token")!; + options.UserInformationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:UserInformation")!; + options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientId")!; + options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientSecret")!; + options.CallbackPath = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:CallbackPath"); + options.SaveTokens = false; + options.Scope.Add("email"); + options.Scope.Add("profile"); + + options.ClaimActions.MapJsonKey("sub", "sub"); + options.ClaimActions.MapJsonKey("email", "email"); + options.ClaimActions.MapJsonKey("name", "name"); + options.ClaimActions.MapJsonKey("given_name", "given_name"); + options.ClaimActions.MapJsonKey("family_name", "family_name"); + + options.Events.OnCreatingTicket = async context => + { + var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); + + var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); + response.EnsureSuccessStatusCode(); + + var user = await response.Content.ReadFromJsonAsync(); + + //TODO: Store token in bff associated with the user + + //TODO: add application specific claims + - if(builder.Configuration.GetValue("Authentication:Schemes:Jwt:Enabled")) + context.RunClaimActions(user); + }; + }); + + //authBuilder.AddOpenIdConnect(LinkAdminConstants.AuthenticationSchemes.OpenIdConnect, options => + //{ + // options.Authority = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:Authority"); + + // options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientId"); + // options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientSecret"); + // options.Scope.Add("email"); // openId and profile scopes are included by default + // options.SaveTokens = false; + // options.ResponseType = "code"; + //}); + + if (builder.Configuration.GetValue("Authentication:Schemas:Jwt:Enabled")) { authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); authBuilder.AddJwTBearerAuthentication(options => { options.Environment = builder.Environment; - options.Authority = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Authority"); - options.Audience = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Audience"); - options.NameClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:NameClaimType"); - options.RoleClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:RoleClaimType"); + options.Authority = builder.Configuration.GetValue("Authentication:Schemas:Jwt:Authority"); + options.Audience = builder.Configuration.GetValue("Authentication:Schemas:Jwt:Audience"); + options.NameClaimType = builder.Configuration.GetValue("Authentication:Schemas:Jwt:NameClaimType"); + options.RoleClaimType = builder.Configuration.GetValue("Authentication:Schemas:Jwt:RoleClaimType"); + }); //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //authBuilder.Services.AddAuthentication() @@ -109,10 +165,8 @@ static void RegisterServices(WebApplicationBuilder builder) // ValidTypes = builder.Configuration.GetValue("Authentication:Schemes:Jwt:ValidTypes") // }; // }); - } + } - - // Add Authorization builder.Services.AddAuthorization(builder => { diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 37943625f..62f98bc5d 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -18,6 +18,8 @@ public static class AuthenticationSchemes { public const string Cookie = "link_cookie"; public const string JwtBearerToken = "link_jwt_bearer"; + public const string Oauth2 = "link_oauth2"; + public const string OpenIdConnect = "link_openid_connect"; } public static class LinkAdminLoggingIds diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index 5d8313013..a38779482 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -10,25 +10,29 @@ "EnableSwagger": true, "EnableIntegrationFeature": true, "Authentication": { - "DefaultScheme": "Bearer", - "DefaultChallengeScheme": "Bearer", - "DefaultAuthenticateScheme": "Bearer", - "Schemes": { + "DefaultScheme": "", + "DefaultChallengeScheme": "", + "DefaultAuthenticateScheme": "", + "Schemas": { "Jwt": { - "Enabled": false, + "Enabled": true, "Authority": "", "Audience": "", "NameClaimType": "email", "RoleClaimType": "roles", "ValidTypes": [ "at+jwt", "JWT" ] + }, + "Oauth2": { + "Enabled": true, + "CallbackPath": "/signin-oauth2" } } }, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "System": "Warning" + "Microsoft": "Information", + "System": "Information" } }, "TelemetryConfig": { @@ -42,8 +46,8 @@ "MinimumLevel": { "Default": "Information", "Override": { - "Microsoft": "Warning", - "System": "Warning" + "Microsoft": "Information", + "System": "Information" } }, "WriteTo": [ diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 0c21c1a1b..724b37b0c 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -22,18 +22,23 @@ "IncludeExceptionDetails": false }, "Authentication": { - "DefaultScheme": "Bearer", - "DefaultChallengeScheme": "Bearer", - "DefaultAuthenticateScheme": "Bearer", - "Schemes": { + "DefaultScheme": "", + "DefaultChallengeScheme": "", + "DefaultAuthenticateScheme": "", + "Schemas": { "Jwt": { - "Enabled": false, + "Enabled": true, "Authority": "", "Audience": "", "NameClaimType": "email", - "RoleClaimType": "roles" + "RoleClaimType": "roles", + "ValidTypes": [ "at+jwt", "JWT" ] + }, + "Oauth2": { + "Enabled": true, + "CallbackPath": "/signin-oauth2" } - } + } }, "Logging": { "LogLevel": { From ca1bcae483a203ef513c298c2fb89b0de26fb94e Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Sun, 24 Mar 2024 15:31:58 -0400 Subject: [PATCH 57/79] add openid scope to allow for access to user info endpoint --- LinkAdmin.BFF/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 0b20a26ba..44886ccbb 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -98,6 +98,7 @@ static void RegisterServices(WebApplicationBuilder builder) options.SaveTokens = false; options.Scope.Add("email"); options.Scope.Add("profile"); + options.Scope.Add("openid"); options.ClaimActions.MapJsonKey("sub", "sub"); options.ClaimActions.MapJsonKey("email", "email"); From 7204dcc138cc818ba589d55d42b8e8c3d6239aee Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Sun, 24 Mar 2024 18:46:08 -0400 Subject: [PATCH 58/79] Authentication using cookie and OAuth2 schemas now working. --- .../Presentation/Endpoints/AuthEndpoints.cs | 30 ++++++++++++------- .../Endpoints/IntegrationTestingEndpoints.cs | 3 ++ LinkAdmin.BFF/Program.cs | 27 ++++------------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index 42b656d53..af1ae80d7 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -1,16 +1,18 @@ - -using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.OpenApi.Models; namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints { public class AuthEndpoints : IApi { - private readonly ILogger _logger; + private readonly ILogger _logger; public AuthEndpoints(ILogger logger) { - _logger = logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public void RegisterEndpoints(WebApplication app) @@ -44,7 +46,7 @@ public void RegisterEndpoints(WebApplication app) authEndpoints.MapGet("/logout", Logout) .RequireAuthorization() - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .ProducesProblem(StatusCodes.Status500InternalServerError) .WithOpenApi(x => new OpenApiOperation(x) @@ -60,17 +62,23 @@ public void RegisterEndpoints(WebApplication app) public IResult Login() { //TODO: DI authentication schema options from settings - return Results.Challenge(authenticationSchemes: ["link_oauth2"]); + return Results.Challenge( + properties: new AuthenticationProperties { + RedirectUri = "/" + }, + authenticationSchemes: [LinkAdminConstants.AuthenticationSchemes.Oauth2]); } - public async Task GetUser() + public IResult GetUser(HttpContext context) { - return Results.Ok(new { Message = "Get User" }); + var user = context.User; + return Results.Ok(user.Claims.Select(x => new { x.Type, x.Value }).ToList()); } - public async Task Logout() - { - return Results.Ok(new { Message = "Logout" }); + public IResult Logout(HttpContext context) + { + context.SignOutAsync(LinkAdminConstants.AuthenticationSchemes.Cookie); + return Results.Ok(new { Message = "Successfully logged out of Link Admin!" }); } diff --git a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs index bdad9dc48..13ff130c1 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/IntegrationTestingEndpoints.cs @@ -32,6 +32,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/patient-event", CreatePatientEvent) + .RequireAuthorization("AuthenticatedUser") .AddEndpointFilter>() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) @@ -44,6 +45,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/report-scheduled", CreateReportScheduled) + .RequireAuthorization("AuthenticatedUser") .AddEndpointFilter>() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) @@ -56,6 +58,7 @@ public void RegisterEndpoints(WebApplication app) }); integrationEndpoints.MapPost("/data-acquisition-requested", CreateDataAcquisitionRequested) + .RequireAuthorization("AuthenticatedUser") .AddEndpointFilter>() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 44886ccbb..716d8d629 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -18,7 +18,6 @@ using Serilog.Settings.Configuration; using System.Reflection; using System.Net.Http.Headers; -using Newtonsoft.Json.Linq; using System.Text.Json; using Microsoft.AspNetCore.Authentication; @@ -72,7 +71,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddSingleton, KafkaProducerFactory>(); // Add Authentication - List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie, LinkAdminConstants.AuthenticationSchemes.Oauth2]; + List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie]; var authBuilder = builder.Services.AddAuthentication(options => { options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; options.DefaultChallengeScheme = LinkAdminConstants.AuthenticationSchemes.Oauth2; @@ -118,9 +117,9 @@ static void RegisterServices(WebApplicationBuilder builder) var user = await response.Content.ReadFromJsonAsync(); //TODO: Store token in bff associated with the user + //var db = context.HttpContext.RequestServices.GetRequiredService(); - //TODO: add application specific claims - + //TODO: add application specific claims context.RunClaimActions(user); }; @@ -149,23 +148,7 @@ static void RegisterServices(WebApplicationBuilder builder) options.NameClaimType = builder.Configuration.GetValue("Authentication:Schemas:Jwt:NameClaimType"); options.RoleClaimType = builder.Configuration.GetValue("Authentication:Schemas:Jwt:RoleClaimType"); - }); - //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - //authBuilder.Services.AddAuthentication() - // .AddJwtBearer(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken, options => - // { - // options.Authority = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Authority"); - // options.Audience = builder.Configuration.GetValue("Authentication:Schemes:Jwt:Audience"); - // options.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); - - // options.TokenValidationParameters = new() - // { - // NameClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:NameClaimType"), - // RoleClaimType = builder.Configuration.GetValue("Authentication:Schemes:Jwt:RoleClaimType"), - // //avoid jwt confustion attacks (ie: circumvent token signature checking) - // ValidTypes = builder.Configuration.GetValue("Authentication:Schemes:Jwt:ValidTypes") - // }; - // }); + }); } // Add Authorization @@ -298,6 +281,8 @@ static void SetupMiddleware(WebApplication app) app.UseAuthorization(); // Register endpoints + app.MapGet("/", (HttpContext ctx) => Results.Ok($"Welcome to {ServiceActivitySource.Instance.Name} version {ServiceActivitySource.Instance.Version}!")); + var apis = app.Services.GetServices(); foreach (var api in apis) { From 88df3779f0ba0df69465b313b4efab7b37273a2e Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 25 Mar 2024 08:39:02 -0400 Subject: [PATCH 59/79] refactored remaining auth schemes into extensions, added AuthenticationSchemaConfig model. --- .../Models/AuthenticationSchemaConfig.cs | 18 ++++ .../Extensions/OAuthSchemeExtension.cs | 70 ++++++++++++ .../OpenIdConnectSchemeExtension.cs | 48 +++++++++ .../Presentation/Endpoints/AuthEndpoints.cs | 13 ++- LinkAdmin.BFF/Program.cs | 100 +++++++++--------- LinkAdmin.BFF/appsettings.Development.json | 14 ++- 6 files changed, 203 insertions(+), 60 deletions(-) create mode 100644 LinkAdmin.BFF/Application/Models/AuthenticationSchemaConfig.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/OAuthSchemeExtension.cs create mode 100644 LinkAdmin.BFF/Infrastructure/Extensions/OpenIdConnectSchemeExtension.cs diff --git a/LinkAdmin.BFF/Application/Models/AuthenticationSchemaConfig.cs b/LinkAdmin.BFF/Application/Models/AuthenticationSchemaConfig.cs new file mode 100644 index 000000000..c2849405d --- /dev/null +++ b/LinkAdmin.BFF/Application/Models/AuthenticationSchemaConfig.cs @@ -0,0 +1,18 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models +{ + /// + /// Authentication schema configuration + /// + public class AuthenticationSchemaConfig + { + /// + /// The default authentication scheme + /// + public string DefaultScheme { get; set; } = null!; + + /// + /// The default authentication challenge scheme + /// + public string DefaultChallengeScheme { get; set; } = null!; + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/OAuthSchemeExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/OAuthSchemeExtension.cs new file mode 100644 index 000000000..bf2ac1f15 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/OAuthSchemeExtension.cs @@ -0,0 +1,70 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Authentication; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class OAuthSchemeExtension + { + public static AuthenticationBuilder AddOAuthAuthentication(this AuthenticationBuilder builder, Action? options) + { + var oauthOptions = new OAuthOptions(); + options?.Invoke(oauthOptions); + + builder.AddOAuth(LinkAdminConstants.AuthenticationSchemes.Oauth2, options => + { + options.AuthorizationEndpoint = oauthOptions.AuthorizationEndpoint; + options.TokenEndpoint = oauthOptions.TokenEndpoint; + options.UserInformationEndpoint = oauthOptions.UserInformationEndpoint; + options.ClientId = oauthOptions.ClientId; + options.ClientSecret = oauthOptions.ClientSecret; + options.CallbackPath = oauthOptions.CallbackPath; + options.SaveTokens = false; + options.Scope.Add("email"); + options.Scope.Add("profile"); + options.Scope.Add("openid"); + + options.ClaimActions.MapJsonKey("sub", "sub"); + options.ClaimActions.MapJsonKey("email", "email"); + options.ClaimActions.MapJsonKey("name", "name"); + options.ClaimActions.MapJsonKey("given_name", "given_name"); + options.ClaimActions.MapJsonKey("family_name", "family_name"); + + options.Events.OnCreatingTicket = async context => + { + var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); + + var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); + response.EnsureSuccessStatusCode(); + + var user = await response.Content.ReadFromJsonAsync(); + + //TODO: Store token in bff associated with the user + //var db = context.HttpContext.RequestServices.GetRequiredService(); + + //TODO: add application specific claims + + context.RunClaimActions(user); + }; + + }); + + return builder; + } + + public class OAuthOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public string ClientId { get; set; } = null!; + public string ClientSecret { get; set; } = null!; + public string AuthorizationEndpoint { get; set; } = null!; + public string TokenEndpoint { get; set; } = null!; + public string UserInformationEndpoint { get; set; } = null!; + public string? CallbackPath { get; set; } = "/signin-oauth2"; + + } + } +} diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/OpenIdConnectSchemeExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/OpenIdConnectSchemeExtension.cs new file mode 100644 index 000000000..151335185 --- /dev/null +++ b/LinkAdmin.BFF/Infrastructure/Extensions/OpenIdConnectSchemeExtension.cs @@ -0,0 +1,48 @@ +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using Microsoft.AspNetCore.Authentication; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions +{ + public static class OpenIdConnectSchemeExtension + { + public static AuthenticationBuilder AddOpenIdConnectAuthentication(this AuthenticationBuilder builder, Action? options) + { + var openIdConnectOptions = new OpenIdConnectOptions(); + options?.Invoke(openIdConnectOptions); + + builder.AddOpenIdConnect(LinkAdminConstants.AuthenticationSchemes.OpenIdConnect, options => + { + options.Authority = openIdConnectOptions.Authority; + options.ClientId = openIdConnectOptions.ClientId; + options.ClientSecret = openIdConnectOptions.ClientSecret; + options.RequireHttpsMetadata = !openIdConnectOptions.Environment.IsDevelopment(); + options.Scope.Add("email"); // openId and profile scopes are included by default + options.SaveTokens = false; + options.ResponseType = "code"; + options.CallbackPath = openIdConnectOptions.CallbackPath; + + options.TokenValidationParameters = new() + { + NameClaimType = openIdConnectOptions.NameClaimType, + RoleClaimType = openIdConnectOptions.RoleClaimType + }; + }); + + return builder; + + } + + public class OpenIdConnectOptions + { + public IWebHostEnvironment Environment { get; set; } = null!; + public string Authority { get; set; } = null!; + public string ClientId { get; set; } = null!; + public string ClientSecret { get; set; } = null!; + public string? NameClaimType { get; set; } = null!; + public string? RoleClaimType { get; set; } = null!; + public string CallbackPath { get; set; } = "/signin-oidc"; + + + } + } +} diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index af1ae80d7..3cd8d72c6 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -1,18 +1,23 @@ -using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; +using Hl7.FhirPath.Sprache; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; namespace LantanaGroup.Link.LinkAdmin.BFF.Presentation.Endpoints { public class AuthEndpoints : IApi { - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly IOptions _authSchemaOptions; - public AuthEndpoints(ILogger logger) + public AuthEndpoints(ILogger logger, IOptions oauthOptions) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _authSchemaOptions = oauthOptions ?? throw new ArgumentNullException(nameof(oauthOptions)); } public void RegisterEndpoints(WebApplication app) @@ -66,7 +71,7 @@ public IResult Login() properties: new AuthenticationProperties { RedirectUri = "/" }, - authenticationSchemes: [LinkAdminConstants.AuthenticationSchemes.Oauth2]); + authenticationSchemes: [ _authSchemaOptions.Value.DefaultChallengeScheme ]); } public IResult GetUser(HttpContext context) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 716d8d629..e63dcf9fe 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -74,8 +74,19 @@ static void RegisterServices(WebApplicationBuilder builder) List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie]; var authBuilder = builder.Services.AddAuthentication(options => { options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; - options.DefaultChallengeScheme = LinkAdminConstants.AuthenticationSchemes.Oauth2; - }); + options.DefaultChallengeScheme = builder.Configuration.GetValue("Authentication:DefaultChallengeScheme"); + }); + + var defaultChallengeScheme = builder.Configuration.GetValue("Authentication:DefaultChallengeScheme"); + builder.Services.Configure(options => + { + options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; + + if (string.IsNullOrEmpty(defaultChallengeScheme)) + throw new NullReferenceException("DefaultChallengeScheme is required."); + + options.DefaultChallengeScheme = defaultChallengeScheme; + }); authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => { @@ -83,62 +94,47 @@ static void RegisterServices(WebApplicationBuilder builder) options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); }); - - authBuilder.AddOAuth(LinkAdminConstants.AuthenticationSchemes.Oauth2, options => - { - options.SignInScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; - - options.AuthorizationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Authorization")!; - options.TokenEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Token")!; - options.UserInformationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:UserInformation")!; - options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientId")!; - options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientSecret")!; - options.CallbackPath = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:CallbackPath"); - options.SaveTokens = false; - options.Scope.Add("email"); - options.Scope.Add("profile"); - options.Scope.Add("openid"); - - options.ClaimActions.MapJsonKey("sub", "sub"); - options.ClaimActions.MapJsonKey("email", "email"); - options.ClaimActions.MapJsonKey("name", "name"); - options.ClaimActions.MapJsonKey("given_name", "given_name"); - options.ClaimActions.MapJsonKey("family_name", "family_name"); - - options.Events.OnCreatingTicket = async context => - { - var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); - - var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); - response.EnsureSuccessStatusCode(); - - var user = await response.Content.ReadFromJsonAsync(); - //TODO: Store token in bff associated with the user - //var db = context.HttpContext.RequestServices.GetRequiredService(); - - //TODO: add application specific claims - - context.RunClaimActions(user); - }; - }); + //Add Oauth authorization scheme if enabled + if(builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Enabled")) + { + if(!LinkAdminConstants.AuthenticationSchemes.Oauth2.Equals(defaultChallengeScheme)) + authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.Oauth2); - //authBuilder.AddOpenIdConnect(LinkAdminConstants.AuthenticationSchemes.OpenIdConnect, options => - //{ - // options.Authority = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:Authority"); + authBuilder.AddOAuthAuthentication(options => + { + options.Environment = builder.Environment; + options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientId")!; + options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:ClientSecret")!; + options.AuthorizationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Authorization")!; + options.TokenEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Token")!; + options.UserInformationEndpoint = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:UserInformation")!; + options.CallbackPath = builder.Configuration.GetValue("Authentication:Schemas:Oauth2:CallbackPath"); + }); + } + + // Add OpenIdConnect authorization scheme if enabled + if (builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:Enabled")) + { + if(!LinkAdminConstants.AuthenticationSchemes.OpenIdConnect.Equals(defaultChallengeScheme)) + authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.OpenIdConnect); - // options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientId"); - // options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientSecret"); - // options.Scope.Add("email"); // openId and profile scopes are included by default - // options.SaveTokens = false; - // options.ResponseType = "code"; - //}); + authBuilder.AddOpenIdConnectAuthentication(options => + { + options.Environment = builder.Environment; + options.Authority = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:Authority")!; + options.ClientId = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientId")!; + options.ClientSecret = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:ClientSecret")!; + options.NameClaimType = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:NameClaimType"); + options.RoleClaimType = builder.Configuration.GetValue("Authentication:Schemas:OpenIdConnect:RoleClaimType"); + }); + } + // Add JWT authorization scheme if enabled if (builder.Configuration.GetValue("Authentication:Schemas:Jwt:Enabled")) { - authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); + if(!LinkAdminConstants.AuthenticationSchemes.JwtBearerToken.Equals(defaultChallengeScheme)) + authSchemas.Add(LinkAdminConstants.AuthenticationSchemes.JwtBearerToken); authBuilder.AddJwTBearerAuthentication(options => { diff --git a/LinkAdmin.BFF/appsettings.Development.json b/LinkAdmin.BFF/appsettings.Development.json index a38779482..93776382f 100644 --- a/LinkAdmin.BFF/appsettings.Development.json +++ b/LinkAdmin.BFF/appsettings.Development.json @@ -10,9 +10,8 @@ "EnableSwagger": true, "EnableIntegrationFeature": true, "Authentication": { - "DefaultScheme": "", - "DefaultChallengeScheme": "", - "DefaultAuthenticateScheme": "", + "DefaultScheme": "link_cookie", + "DefaultChallengeScheme": "link_oauth2", "Schemas": { "Jwt": { "Enabled": true, @@ -24,7 +23,14 @@ }, "Oauth2": { "Enabled": true, - "CallbackPath": "/signin-oauth2" + "CallbackPath": "/signin-oauth2" + }, + "OpenIdConnect": { + "Enabled": false, + "Authority": "", + "CallbackPath": "/signin-oidc", + "NameClaimType": "email", + "RoleClaimType": "roles" } } }, From c16777156a16d932785fa2e3e68a2f2c16a08967 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 25 Mar 2024 08:43:19 -0400 Subject: [PATCH 60/79] code cleanup --- .../Presentation/Endpoints/AuthEndpoints.cs | 1 - LinkAdmin.BFF/Program.cs | 23 +++++++------------ LinkAdmin.BFF/appsettings.json | 12 +++++++--- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index 3cd8d72c6..f4f224113 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -3,7 +3,6 @@ using LantanaGroup.Link.LinkAdmin.BFF.Application.Models; using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index e63dcf9fe..b9154a1cf 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -17,10 +17,6 @@ using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; -using System.Net.Http.Headers; -using System.Text.Json; -using Microsoft.AspNetCore.Authentication; - var builder = WebApplication.CreateBuilder(args); @@ -72,22 +68,23 @@ static void RegisterServices(WebApplicationBuilder builder) // Add Authentication List authSchemas = [ LinkAdminConstants.AuthenticationSchemes.Cookie]; - var authBuilder = builder.Services.AddAuthentication(options => { - options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; - options.DefaultChallengeScheme = builder.Configuration.GetValue("Authentication:DefaultChallengeScheme"); - }); var defaultChallengeScheme = builder.Configuration.GetValue("Authentication:DefaultChallengeScheme"); builder.Services.Configure(options => { options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; - - if (string.IsNullOrEmpty(defaultChallengeScheme)) + + if (string.IsNullOrEmpty(defaultChallengeScheme)) throw new NullReferenceException("DefaultChallengeScheme is required."); options.DefaultChallengeScheme = defaultChallengeScheme; }); + var authBuilder = builder.Services.AddAuthentication(options => { + options.DefaultScheme = LinkAdminConstants.AuthenticationSchemes.Cookie; + options.DefaultChallengeScheme = defaultChallengeScheme; + }); + authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => { options.Cookie.Name = LinkAdminConstants.AuthenticationSchemes.Cookie; @@ -156,7 +153,6 @@ static void RegisterServices(WebApplicationBuilder builder) }); }); - // Add Endpoints builder.Services.AddTransient(); if (builder.Configuration.GetValue("EnableIntegrationFeature")) @@ -267,8 +263,6 @@ static void SetupMiddleware(WebApplication app) }); } - - app.UseRouting(); var corsConfig = app.Configuration.GetSection(LinkAdminConstants.AppSettingsSectionNames.CORS).Get(); app.UseCors(corsConfig?.PolicyName ?? CorsConfig.DefaultCorsPolicyName); @@ -292,8 +286,7 @@ static void SetupMiddleware(WebApplication app) app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }).RequireCors("HealthCheckPolicy"); - + }).RequireCors("HealthCheckPolicy"); } #endregion diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 724b37b0c..e876e570f 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -22,9 +22,8 @@ "IncludeExceptionDetails": false }, "Authentication": { - "DefaultScheme": "", - "DefaultChallengeScheme": "", - "DefaultAuthenticateScheme": "", + "DefaultScheme": "link_cookie", + "DefaultChallengeScheme": "link_oauth2", "Schemas": { "Jwt": { "Enabled": true, @@ -37,6 +36,13 @@ "Oauth2": { "Enabled": true, "CallbackPath": "/signin-oauth2" + }, + "OpenIdConnect": { + "Enabled": false, + "Authority": "", + "CallbackPath": "/signin-oidc", + "NameClaimType": "email", + "RoleClaimType": "roles" } } }, From 0ba9ae3a10ef1ad221e4bc3eccef6b818cae6b8e Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 25 Mar 2024 08:48:32 -0400 Subject: [PATCH 61/79] Added authenticated user policy to the logout endpoint --- LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs index f4f224113..5f3136fa6 100644 --- a/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs +++ b/LinkAdmin.BFF/Presentation/Endpoints/AuthEndpoints.cs @@ -49,7 +49,7 @@ public void RegisterEndpoints(WebApplication app) }); authEndpoints.MapGet("/logout", Logout) - .RequireAuthorization() + .RequireAuthorization("AuthenticatedUser") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .ProducesProblem(StatusCodes.Status500InternalServerError) From 271e7b18187e903c0451e311ae6f11c283d9aa7c Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 25 Mar 2024 10:49:04 -0400 Subject: [PATCH 62/79] Added default AuthenticatedUser authorization policy to YARP. --- LinkAdmin.BFF/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index b9154a1cf..877dadea1 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -280,7 +280,7 @@ static void SetupMiddleware(WebApplication app) api.RegisterEndpoints(app); } - app.MapReverseProxy(); + app.MapReverseProxy().RequireAuthorization("AuthenticatedUser"); // Map health check middleware app.MapHealthChecks("/health", new HealthCheckOptions From 578280fea55de34dca9f790a7d0bec2880e5641f Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Mon, 25 Mar 2024 13:33:54 -0400 Subject: [PATCH 63/79] fixed typo in YARP for tenant service cluster --- LinkAdmin.BFF/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index e876e570f..17c0ae595 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -153,7 +153,7 @@ } }, "route11": { - "ClusterId": "SubmissionService", + "ClusterId": "TenantService", "Match": { "Path": "api/facility/{**catch-all}" } From 054f600cea6d4b2d628130c7105c428aa7cf21f1 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 25 Mar 2024 11:31:15 -0700 Subject: [PATCH 64/79] Docker build works Kafka works REST API works Auto-generated OpenAPI spec works --- Java/.dockerignore | 2 +- Java/.gitignore | 3 + Java/Dockerfile.validation | 22 ----- Java/README.md | 9 ++ Java/pom.xml | 98 ++----------------- Java/shared/README.md | 13 +++ Java/shared/pom.xml | 59 +++++++++-- .../main/resources/{empty.txt => EMPTY.md} | 0 Java/validation/Dockerfile | 18 ++++ Java/validation/README.md | 25 +++++ Java/validation/pom.xml | 89 ++++++++++++++++- .../validation/ValidationApplication.java | 22 ----- .../ValidationApplicationConfig.java | 42 ++++++++ .../consumers/PatientEvaluatedConsumer.java | 10 +- .../controllers/ValidationController.java | 21 ++++ .../validation/kafka/KafkaErrorHandler.java | 44 +++++++++ .../validation/kafka/KafkaProperties.java | 25 ----- .../model/PatientEvaluatedModel.java | 5 + .../serdes/PatientEvaluatedDeserializer.java | 33 ------- .../services/ValidationService.java | 10 ++ .../src/main/resources/application.yml | 14 ++- .../com/lantanagroup/link/validation/EMPTY.md | 0 22 files changed, 351 insertions(+), 213 deletions(-) delete mode 100644 Java/Dockerfile.validation create mode 100644 Java/README.md create mode 100644 Java/shared/README.md rename Java/shared/src/main/resources/{empty.txt => EMPTY.md} (100%) create mode 100644 Java/validation/Dockerfile create mode 100644 Java/validation/README.md create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplicationConfig.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/controllers/ValidationController.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaErrorHandler.java delete mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java delete mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationService.java create mode 100644 Java/validation/src/test/java/com/lantanagroup/link/validation/EMPTY.md diff --git a/Java/.dockerignore b/Java/.dockerignore index d03a94cdf..1323c8825 100644 --- a/Java/.dockerignore +++ b/Java/.dockerignore @@ -7,7 +7,7 @@ target/ .vscode/ # Ignore Docker-related files -Dockerfile.validation +validation/Dockerfile.validation docker-compose.yml # Ignore version control system files diff --git a/Java/.gitignore b/Java/.gitignore index 417aa7de6..bbc90f4ea 100644 --- a/Java/.gitignore +++ b/Java/.gitignore @@ -27,3 +27,6 @@ build/ ### VS Code ### .vscode/ + +### Maven Settings ### +.m2/ \ No newline at end of file diff --git a/Java/Dockerfile.validation b/Java/Dockerfile.validation deleted file mode 100644 index 2c8dcaa1a..000000000 --- a/Java/Dockerfile.validation +++ /dev/null @@ -1,22 +0,0 @@ -FROM maven:3.8.7-openjdk-18-slim AS deps -WORKDIR /app -COPY pom.xml /app -COPY shared/pom.xml /app/shared/pom.xml -COPY validation/pom.xml /app/validation/pom.xml -RUN mvn dependency:go-offline - -# Stage 1: Build the Spring Boot application -FROM maven:3.8.7-openjdk-18-slim AS build -WORKDIR /app -COPY --from=deps /root/.m2 /root/.m2 -COPY . /app -RUN mvn clean install -DskipTests -RUN mv validation/target/validation-*.jar validation/target/validation.jar - -# Stage 2: Run the Spring Boot application -FROM openjdk:17-jdk-alpine -WORKDIR /app -# Extract the version from the JAR filename using shell scripting -COPY --from=build /app/validation/target/validation.jar validation.jar -EXPOSE 8080 -CMD ["java", "-jar", "validation.jar"] \ No newline at end of file diff --git a/Java/README.md b/Java/README.md new file mode 100644 index 000000000..b9fceb906 --- /dev/null +++ b/Java/README.md @@ -0,0 +1,9 @@ +# Overview + +Each folder is a separate module. The modules are independently built (i.e. CD to module directory and +run `mvn clean install`). +The shared module is a dependency for the other modules, but is published to a private maven repository so that it is +available to the other modules during the build process. +To authenticate against the private maven repo, each service has a .m2/settings.xml file that contains parameterized ( +from env variables) credentials for the private maven repo. +In automated builds, this environment variable is set by the CI/CD pipeline. \ No newline at end of file diff --git a/Java/pom.xml b/Java/pom.xml index 4ca490946..6e6585797 100644 --- a/Java/pom.xml +++ b/Java/pom.xml @@ -1,18 +1,13 @@ - - + + 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.2.3 - - com.lantanagroup.link - link + main 0.0.1-SNAPSHOT + Link Java Modules + Project/folder of Java modules/services for Link pom @@ -20,85 +15,4 @@ validation - - UTF-8 - 17 - 17 - 3.2.3 - 12.6.1.jre11 - 1.18.30 - 3.7.0 - 3.1.2 - 4.13.2 - - - - - org.projectlombok - lombok - ${lombok} - - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - org.springframework.boot - spring-boot-starter-test - test - ${spring.boot} - - - com.microsoft.sqlserver - mssql-jdbc - runtime - ${mssql} - - - org.apache.kafka - kafka-streams - ${kafka.streams} - - - org.springframework.kafka - spring-kafka - ${spring.kafka} - - - ca.uhn.hapi.fhir - hapi-fhir-structures-r4 - 7.0.2 - - - org.springframework.kafka - spring-kafka-test - ${spring.kafka} - test - - - junit - junit - ${junit} - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - \ No newline at end of file diff --git a/Java/shared/README.md b/Java/shared/README.md new file mode 100644 index 000000000..897e06087 --- /dev/null +++ b/Java/shared/README.md @@ -0,0 +1,13 @@ +# Overview + +The shared module contains shared functionality across all java services +It is published to a separate private maven repository so that builds of service modules can find the shared module in +the private repository + +# Testing Changes + +To test changes to the shared library before deploying them to the private maven repo, run `mvn clean install` on the +shared +library, first. Then re-run `mvn clean install` on the dependent modules, and they will pick up the changes from the +local +.m2 repository where the shared library was installed. \ No newline at end of file diff --git a/Java/shared/pom.xml b/Java/shared/pom.xml index 06e2d7335..2da9320b7 100644 --- a/Java/shared/pom.xml +++ b/Java/shared/pom.xml @@ -3,12 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.lantanagroup.link - link - 0.0.1-SNAPSHOT - - + com.lantanagroup.link shared 0.0.1-SNAPSHOT Shared Library @@ -16,14 +11,64 @@ 17 + Shared_BOTW_Feed + https://pkgs.dev.azure.com/lantanagroup/nhsnlink/_packaging/Shared_BOTW_Feed/maven/v1 + 4.13.2 + + + ${repo.id} + ${repo.url} + + true + + + true + + + + + + + ${repo.id} + ${repo.url} + + true + + + true + + + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + 7.0.2 + + + junit + junit + ${junit} + test + + + - org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + repackage diff --git a/Java/shared/src/main/resources/empty.txt b/Java/shared/src/main/resources/EMPTY.md similarity index 100% rename from Java/shared/src/main/resources/empty.txt rename to Java/shared/src/main/resources/EMPTY.md diff --git a/Java/validation/Dockerfile b/Java/validation/Dockerfile new file mode 100644 index 000000000..c2964f25b --- /dev/null +++ b/Java/validation/Dockerfile @@ -0,0 +1,18 @@ +ARG SHARED_BOTW_FEED_TOKEN_ARG + +# Stage 1: Build the Spring Boot application +FROM maven:3.8.7-openjdk-18-slim AS build +ARG SHARED_BOTW_FEED_TOKEN_ARG +WORKDIR /app +COPY . /app +ENV SHARED_BOTW_FEED_TOKEN=$SHARED_BOTW_FEED_TOKEN_ARG +COPY .m2/settings.xml /root/.m2/settings.xml +RUN mvn clean install -DskipTests +RUN mv target/validation-*.jar target/validation.jar + +# Stage 2: Run the Spring Boot application +FROM openjdk:17-jdk-alpine +WORKDIR /app +COPY --from=build /app/target/validation.jar validation.jar +EXPOSE 8080 +CMD ["java", "-jar", "validation.jar"] \ No newline at end of file diff --git a/Java/validation/README.md b/Java/validation/README.md new file mode 100644 index 000000000..107d9164d --- /dev/null +++ b/Java/validation/README.md @@ -0,0 +1,25 @@ +# Overview + +## Key Functionality + +* Validates patient data, persists validation results +* Categorizes validation results +* Consumes Kafka events to determine when to validate patient data +* Produces Kafka events when validation is complete +* Provides a REST API to query validation results + +## Building + +### Manually + +Ensure the local Maven repository has been specified in Maven settings. + +```bash +mvn clean install +``` + +### Docker + +```bash +docker build -t link-validation . +``` \ No newline at end of file diff --git a/Java/validation/pom.xml b/Java/validation/pom.xml index c04e7b5ee..13d4c7856 100644 --- a/Java/validation/pom.xml +++ b/Java/validation/pom.xml @@ -4,11 +4,13 @@ 4.0.0 - com.lantanagroup.link - link - 0.0.1-SNAPSHOT + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + com.lantanagroup.link validation 0.0.1-SNAPSHOT Validation Service @@ -16,8 +18,32 @@ 17 + UTF-8 + 17 + 17 + 3.2.3 + 12.6.1.jre11 + 1.18.30 + 3.7.0 + 3.1.2 + 4.13.2 + Shared_BOTW_Feed + https://pkgs.dev.azure.com/lantanagroup/nhsnlink/_packaging/Shared_BOTW_Feed/maven/v1 + + + ${repo.id} + ${repo.url} + + true + + + true + + + + org.springframework.boot @@ -34,13 +60,68 @@ spring-security-test test + + org.projectlombok + lombok + ${lombok} + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.springframework.boot + spring-boot-starter-test + test + ${spring.boot} + + + com.microsoft.sqlserver + mssql-jdbc + runtime + ${mssql} + + + org.apache.kafka + kafka-streams + ${kafka.streams} + + + org.springframework.kafka + spring-kafka + ${spring.kafka} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + 7.0.2 + + + org.springframework.kafka + spring-kafka-test + ${spring.kafka} + test + + + junit + junit + ${junit} + test + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + com.lantanagroup.link shared 0.0.1-SNAPSHOT - test diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java index d437b5a53..d6db07ca9 100644 --- a/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplication.java @@ -1,38 +1,16 @@ package com.lantanagroup.link.validation; -import com.lantanagroup.link.validation.kafka.KafkaProperties; -import com.lantanagroup.link.validation.model.PatientEvaluatedModel; -import com.lantanagroup.link.validation.serdes.PatientEvaluatedDeserializer; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; - -import java.util.HashMap; -import java.util.Map; @SpringBootApplication @PropertySource("classpath:application.yml") public class ValidationApplication { - @Autowired - private KafkaProperties kafkaProperties; - public static void main(String[] args) { SpringApplication application = new SpringApplication(ValidationApplication.class); - application.setWebApplicationType(WebApplicationType.NONE); application.setBannerMode(Banner.Mode.OFF); application.run(args); } - - @Bean - public ConsumerFactory patientEvaluatedConsumerFactory() { - Map props = new HashMap<>(kafkaProperties.buildConsumerProperties()); - return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new PatientEvaluatedDeserializer()); - } } diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplicationConfig.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplicationConfig.java new file mode 100644 index 000000000..1e4e101d7 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/ValidationApplicationConfig.java @@ -0,0 +1,42 @@ +package com.lantanagroup.link.validation; + +import com.lantanagroup.link.validation.kafka.KafkaErrorHandler; +import com.lantanagroup.link.validation.model.PatientEvaluatedModel; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.listener.CommonErrorHandler; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class ValidationApplicationConfig { + + @Bean + CommonErrorHandler commonErrorHandler() { + return new KafkaErrorHandler(); + } + + @Bean + ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory( + ConsumerFactory consumerFactory, + CommonErrorHandler commonErrorHandler + ) { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + factory.setCommonErrorHandler(commonErrorHandler); + return factory; + } + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + // TODO: This needs to be updated to secure the application + http + .authorizeHttpRequests((authorizeRequests) -> + authorizeRequests + .anyRequest() + .anonymous()); + return http.build(); + } +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java index 55a3683ea..62b848fc7 100644 --- a/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/consumers/PatientEvaluatedConsumer.java @@ -2,14 +2,18 @@ import com.lantanagroup.link.validation.model.PatientEvaluatedModel; import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @Component public class PatientEvaluatedConsumer { - @KafkaListener(topics = "PatientEvaluated", containerFactory = "kafkaListenerContainerFactory") + private static final Logger log = LoggerFactory.getLogger(PatientEvaluatedConsumer.class); + + @KafkaListener(topics = "PatientEvaluated", properties = {"spring.json.value.default.type=com.lantanagroup.link.validation.model.PatientEvaluatedModel"}) public void listen(ConsumerRecord record) { - System.out.println("Received message key: " + record.key()); - // Process your message here + PatientEvaluatedModel value = record.value(); + log.info("Received PatientEvaluated:\n\tTenant ID: {}\n\tPatient ID: {}", value.getTenantId(), value.getPatientId()); } } \ No newline at end of file diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/controllers/ValidationController.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/controllers/ValidationController.java new file mode 100644 index 000000000..f0debdba3 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/controllers/ValidationController.java @@ -0,0 +1,21 @@ +package com.lantanagroup.link.validation.controllers; + +import com.lantanagroup.link.validation.services.ValidationService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class ValidationController { + private final ValidationService validationService; + + public ValidationController(final ValidationService validationService) { + this.validationService = validationService; + } + + @GetMapping + public String getValidationResults() { + return this.validationService.getValidationResults(); + } +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaErrorHandler.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaErrorHandler.java new file mode 100644 index 000000000..b7435441f --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaErrorHandler.java @@ -0,0 +1,44 @@ +package com.lantanagroup.link.validation.kafka; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.errors.RecordDeserializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.listener.DefaultErrorHandler; +import org.springframework.kafka.listener.MessageListenerContainer; +import org.springframework.util.backoff.FixedBackOff; + +public class KafkaErrorHandler extends DefaultErrorHandler { + private static final Logger log = LoggerFactory.getLogger(KafkaErrorHandler.class); + + public KafkaErrorHandler() { + super(new FixedBackOff(10000, 2)); + } + + @Override + public boolean seeksAfterHandling() { + return true; + } + + @Override + public boolean handleOne(Exception exception, ConsumerRecord record, Consumer consumer, MessageListenerContainer container) { + handle(exception, consumer); + return true; + } + + @Override + public void handleOtherException(Exception exception, Consumer consumer, MessageListenerContainer container, boolean batchListener) { + handle(exception, consumer); + } + + private void handle(Exception exception, Consumer consumer) { + if (exception instanceof RecordDeserializationException ex) { + log.error("Kafka deserialization exception not handled: {}", exception.getMessage()); + consumer.seek(ex.topicPartition(), ex.offset() + 1L); + consumer.commitSync(); + } else { + log.error("Exception not handled", exception); + } + } +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java deleted file mode 100644 index 428c433ce..000000000 --- a/Java/validation/src/main/java/com/lantanagroup/link/validation/kafka/KafkaProperties.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.lantanagroup.link.validation.kafka; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -@Component -public class KafkaProperties { - - @Value("${spring.kafka.bootstrap-servers}") - private String bootstrapServers; - - @Value("${spring.kafka.consumer.group-id}") - private String groupId; - - public Map buildConsumerProperties() { - Map properties = new HashMap<>(); - properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); - properties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); - return properties; - } -} \ No newline at end of file diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java index f176a6cf7..15ec318c2 100644 --- a/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/model/PatientEvaluatedModel.java @@ -3,7 +3,12 @@ import lombok.Getter; import lombok.Setter; +import java.util.Date; + @Getter @Setter public class PatientEvaluatedModel { + private String tenantId; + private String patientId; + private Date timestamp; } diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java deleted file mode 100644 index 180de22bf..000000000 --- a/Java/validation/src/main/java/com/lantanagroup/link/validation/serdes/PatientEvaluatedDeserializer.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.lantanagroup.link.validation.serdes; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.lantanagroup.link.validation.model.PatientEvaluatedModel; -import org.apache.kafka.common.errors.SerializationException; -import org.apache.kafka.common.serialization.Deserializer; - -import java.nio.charset.StandardCharsets; -import java.util.Map; - -public class PatientEvaluatedDeserializer implements Deserializer { - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public void configure(Map configs, boolean isKey) { - } - - @Override - public PatientEvaluatedModel deserialize(String topic, byte[] data) { - try { - if (data == null) { - return null; - } - return objectMapper.readValue(new String(data, StandardCharsets.UTF_8), PatientEvaluatedModel.class); - } catch (Exception e) { - throw new SerializationException("Error when deserializing byte[] to MessageDto"); - } - } - - @Override - public void close() { - } -} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationService.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationService.java new file mode 100644 index 000000000..2254f7e8b --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationService.java @@ -0,0 +1,10 @@ +package com.lantanagroup.link.validation.services; + +import org.springframework.stereotype.Service; + +@Service +public class ValidationService { + public String getValidationResults() { + return "Validation Results"; + } +} diff --git a/Java/validation/src/main/resources/application.yml b/Java/validation/src/main/resources/application.yml index b4f199531..733f5b2f1 100644 --- a/Java/validation/src/main/resources/application.yml +++ b/Java/validation/src/main/resources/application.yml @@ -1,3 +1,8 @@ +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html spring: application: name: ValidationService @@ -5,9 +10,10 @@ spring: bootstrap-servers: localhost:9092 consumer: group-id: validation + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer producer: client-id: validation-service - #properties: - # security.protocol: SASL_PLAINTEXT - # sasl.mechanism: PLAIN - # sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="your-username" password="your-password"; \ No newline at end of file + properties: + security.protocol: SASL_SSL + sasl.mechanism: PLAIN + #sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="$ConnectionString" password=""; \ No newline at end of file diff --git a/Java/validation/src/test/java/com/lantanagroup/link/validation/EMPTY.md b/Java/validation/src/test/java/com/lantanagroup/link/validation/EMPTY.md new file mode 100644 index 000000000..e69de29bb From 4eb375a85266f8c730b0739a6c8caa0b32643374 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 25 Mar 2024 15:04:17 -0700 Subject: [PATCH 65/79] Adding basic example of persistence to sql server with hibernate --- Java/validation/pom.xml | 18 ++++++---- .../validation/entities/ValidationResult.java | 33 +++++++++++++++++++ .../ValidationResultRepository.java | 13 ++++++++ .../services/ValidationResultService.java | 20 +++++++++++ .../src/main/resources/application.yml | 11 +++++++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/entities/ValidationResult.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/repositories/ValidationResultRepository.java create mode 100644 Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationResultService.java diff --git a/Java/validation/pom.xml b/Java/validation/pom.xml index 13d4c7856..332d1e81b 100644 --- a/Java/validation/pom.xml +++ b/Java/validation/pom.xml @@ -76,12 +76,6 @@ test ${spring.boot} - - com.microsoft.sqlserver - mssql-jdbc - runtime - ${mssql} - org.apache.kafka kafka-streams @@ -110,6 +104,18 @@ test + + + com.microsoft.sqlserver + mssql-jdbc + runtime + ${mssql} + + + org.springframework.boot + spring-boot-starter-data-jpa + + org.springdoc diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/entities/ValidationResult.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/entities/ValidationResult.java new file mode 100644 index 000000000..fa2d1014a --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/entities/ValidationResult.java @@ -0,0 +1,33 @@ +package com.lantanagroup.link.validation.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.Setter; +import org.hl7.fhir.r4.model.OperationOutcome; + +@Entity +@Getter +@Setter +public class ValidationResult { + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String tenantId; + + @Column(nullable = false) + private String reportId; + + @Column(nullable = false, columnDefinition = "varchar(max)") + private String message; + + @Column(nullable = false, length = 4096) + private String expression; + + @Column(nullable = false) + private OperationOutcome.IssueSeverity severity; +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/repositories/ValidationResultRepository.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/repositories/ValidationResultRepository.java new file mode 100644 index 000000000..e362869a2 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/repositories/ValidationResultRepository.java @@ -0,0 +1,13 @@ +package com.lantanagroup.link.validation.repositories; + +import com.lantanagroup.link.validation.entities.ValidationResult; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ValidationResultRepository extends JpaRepository { + /* + @Query("SELECT v FROM validationResult v WHERE v.tenantId = :tenantId AND v.reportId = :reportId") + List findByTenantIdAndReportId(String tenantId, String reportId); + */ +} diff --git a/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationResultService.java b/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationResultService.java new file mode 100644 index 000000000..b25c92546 --- /dev/null +++ b/Java/validation/src/main/java/com/lantanagroup/link/validation/services/ValidationResultService.java @@ -0,0 +1,20 @@ +package com.lantanagroup.link.validation.services; + +import com.lantanagroup.link.validation.repositories.ValidationResultRepository; +import org.springframework.stereotype.Service; + +@Service +public class ValidationResultService { + private final ValidationResultRepository repository; + + public ValidationResultService(ValidationResultRepository repository) { + this.repository = repository; + } + + /* + public List getValidationResults(String tenantId, String reportId) { + return this.repository.findByTenantIdAndReportId(tenantId, reportId); + } + + */ +} diff --git a/Java/validation/src/main/resources/application.yml b/Java/validation/src/main/resources/application.yml index 733f5b2f1..0f472303d 100644 --- a/Java/validation/src/main/resources/application.yml +++ b/Java/validation/src/main/resources/application.yml @@ -6,6 +6,17 @@ springdoc: spring: application: name: ValidationService + datasource: + url: jdbc:sqlserver://;serverName=localhost\SQLEXPRESS;databaseName=link-validation;encrypt=true;trustServerCertificate=true + username: link-user + password: link-password + jpa: + hibernate: + ddl-auto: update + properties: + show_sql: true + hibernate: + dialect: org.hibernate.dialect.SQLServerDialect kafka: bootstrap-servers: localhost:9092 consumer: From 0523a2f78d0d71ed714861483bdd98e61ae9a9f3 Mon Sep 17 00:00:00 2001 From: Steven Williams Date: Tue, 26 Mar 2024 13:33:58 -0400 Subject: [PATCH 66/79] Forward cancellation token to `Consume` --- Normalization/Listeners/PatientDataAcquiredListener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalization/Listeners/PatientDataAcquiredListener.cs b/Normalization/Listeners/PatientDataAcquiredListener.cs index 3da059416..36747a873 100644 --- a/Normalization/Listeners/PatientDataAcquiredListener.cs +++ b/Normalization/Listeners/PatientDataAcquiredListener.cs @@ -71,7 +71,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca ConsumeResult message; try { - message = kafkaConsumer.Consume(); + message = kafkaConsumer.Consume(cancellationToken); } catch (Exception ex) { From 4330826bd67a5a5ac3024e92e1ea34880d5cfada Mon Sep 17 00:00:00 2001 From: Steven Williams Date: Tue, 26 Mar 2024 15:10:12 -0400 Subject: [PATCH 67/79] Fix service name --- Normalization/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalization/appsettings.json b/Normalization/appsettings.json index 5b7857df3..edb21d2e3 100644 --- a/Normalization/appsettings.json +++ b/Normalization/appsettings.json @@ -44,7 +44,7 @@ ] }, "ServiceInformation": { - "Name": "Link Audit Service", + "Name": "Link Normalization Service", "Version": "1.1.0-beta" }, "TelemetryConfig": { From c5cb05334d7ec047e3f0572111e2f5233404c00d Mon Sep 17 00:00:00 2001 From: Steven Williams Date: Tue, 26 Mar 2024 15:52:29 -0400 Subject: [PATCH 68/79] Add dead letter workflow for unrecoverable errors The following errors are considered unrecoverable: - Failure to consume message - Failure to extract facility ID or correlation ID - Failure to deserialize bundle --- .../Listeners/PatientDataAcquiredListener.cs | 40 +++++++++++++------ Normalization/Program.cs | 13 ++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Normalization/Listeners/PatientDataAcquiredListener.cs b/Normalization/Listeners/PatientDataAcquiredListener.cs index 36747a873..a66db9bcc 100644 --- a/Normalization/Listeners/PatientDataAcquiredListener.cs +++ b/Normalization/Listeners/PatientDataAcquiredListener.cs @@ -8,6 +8,7 @@ using LantanaGroup.Link.Normalization.Application.Models.Messages; using LantanaGroup.Link.Normalization.Application.Settings; using LantanaGroup.Link.Normalization.Domain.Entities; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Interfaces; using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Kafka; @@ -27,18 +28,21 @@ public class PatientDataAcquiredListener : BackgroundService private readonly IMediator _mediator; private readonly IKafkaConsumerFactory _consumerFactory; private readonly IKafkaProducerFactory _producerFactory; + private readonly IDeadLetterExceptionHandler _deadLetterExceptionHandler; private bool _cancelled = false; public PatientDataAcquiredListener( ILogger logger, IMediator mediator, IKafkaConsumerFactory consumerFactory, - IKafkaProducerFactory producerFactory) + IKafkaProducerFactory producerFactory, + IDeadLetterExceptionHandler deadLetterExceptionHandler) { this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); _consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory)); _producerFactory = producerFactory ?? throw new ArgumentNullException(nameof(producerFactory)); + _deadLetterExceptionHandler = deadLetterExceptionHandler ?? throw new ArgumentNullException(nameof(deadLetterExceptionHandler)); } ~PatientDataAcquiredListener() @@ -73,16 +77,34 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca { message = kafkaConsumer.Consume(cancellationToken); } - catch (Exception ex) + catch (OperationCanceledException) { - _logger.LogError(ex.Message, ex); + continue; + } + catch (ConsumeException ex) + { + // TODO: DLEH correctly handles these nulls despite declaring the corresponding parameters as non-nullable + // Update the signature of HandleException to reflect the fact that we anticipate (and explicitly check for) potentially null values? + // For now, use the null-forgiving operator (here and below) to suppress compiler warnings + _deadLetterExceptionHandler.HandleException(null!, ex, AuditEventType.Create, null!); + kafkaConsumer.Commit(); continue; } if (message == null) continue; - (string facilityId, string correlationId) messageMetaData = ExtractFacilityIdAndCorrelationIdFromMessage(message.Message); + (string facilityId, string correlationId) messageMetaData; + try + { + messageMetaData = ExtractFacilityIdAndCorrelationIdFromMessage(message.Message); + } + catch (Exception ex) + { + _deadLetterExceptionHandler.HandleException(message, ex, AuditEventType.Create, null!); + kafkaConsumer.Commit(message); + continue; + } NormalizationConfigEntity? config = null; try @@ -116,14 +138,8 @@ await _mediator.Send(new TriggerAuditEventCommand } catch(Exception ex) { - _logger.LogError(ex.Message,ex); - await _mediator.Send(new TriggerAuditEventCommand - { - Notes = $"{ex.Message}\n{ex.StackTrace}", - CorrelationId = messageMetaData.correlationId, - FacilityId = messageMetaData.facilityId, - patientDataAcquiredMessage = message.Value, - }); + _deadLetterExceptionHandler.HandleException(message, ex, AuditEventType.Create, messageMetaData.facilityId); + kafkaConsumer.Commit(message); continue; } diff --git a/Normalization/Program.cs b/Normalization/Program.cs index 0fd2722b0..c7115249e 100644 --- a/Normalization/Program.cs +++ b/Normalization/Program.cs @@ -6,8 +6,11 @@ using LantanaGroup.Link.Normalization.Application.Services; using LantanaGroup.Link.Normalization.Application.Settings; using LantanaGroup.Link.Normalization.Listeners; +using LantanaGroup.Link.Shared.Application.Error.Handlers; +using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Factories; using LantanaGroup.Link.Shared.Application.Interfaces; +using LantanaGroup.Link.Shared.Application.Models; using LantanaGroup.Link.Shared.Application.Models.Configs; using LantanaGroup.Link.Shared.Application.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; @@ -56,6 +59,16 @@ static void RegisterServices(WebApplicationBuilder builder) KafkaProducerFactory >(); builder.Services.AddTransient, KafkaConsumerFactory>(); + builder.Services.AddTransient(serviceProvider => + { + IKafkaProducerFactory producerFactory = + ActivatorUtilities.CreateInstance>(serviceProvider); + IDeadLetterExceptionHandler handler = + ActivatorUtilities.CreateInstance>(serviceProvider, producerFactory); + handler.ServiceName = serviceInformation.Name; + handler.Topic = $"{nameof(KafkaTopic.PatientAcquired)}-Error"; + return handler; + }); builder.Services.AddTransient(); From a38891aa91932f5eb3bde0685d5f0309daa9ddb0 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Wed, 27 Mar 2024 11:04:17 -0400 Subject: [PATCH 69/79] Fixed CORS policy origin issue, code clean up --- .../Extensions/ExternalConfigurationExtension.cs | 7 ++++++- .../Extensions/ProblemDetailsExtension.cs | 5 +++-- LinkAdmin.BFF/Program.cs | 12 ++++++------ LinkAdmin.BFF/Settings/LinkAdminConstants.cs | 2 ++ LinkAdmin.BFF/appsettings.json | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs index 5c9f0aecd..f0f7d322d 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/ExternalConfigurationExtension.cs @@ -1,4 +1,5 @@ using Azure.Identity; +using LantanaGroup.Link.LinkAdmin.BFF.Settings; using Microsoft.Extensions.Configuration.AzureAppConfiguration; namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions @@ -18,8 +19,12 @@ public static WebApplicationBuilder AddExternalConfiguration(this WebApplication builder.Configuration.AddAzureAppConfiguration(options => { options.Connect(externalConfigurationOptions.ExternalConfigurationConnectionString) + // Load configuration values with no label .Select("*", LabelFilter.Null) - .Select("*", "Link:AdminBFF:" + externalConfigurationOptions.Environment.EnvironmentName); + // Load configuration values for service name + .Select("*", LinkAdminConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", LinkAdminConstants.ServiceName + ":" + externalConfigurationOptions.Environment.EnvironmentName); options.ConfigureKeyVault(kv => { diff --git a/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs b/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs index 0e0fe4964..634072ba2 100644 --- a/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs +++ b/LinkAdmin.BFF/Infrastructure/Extensions/ProblemDetailsExtension.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using LantanaGroup.Link.LinkAdmin.BFF.Settings; +using System.Diagnostics; namespace LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Extensions { @@ -21,7 +22,7 @@ public static IServiceCollection AddProblemDetailsService(this IServiceCollectio if (problemDetailsOptions.Environment.IsDevelopment() || problemDetailsOptions.IncludeExceptionDetails) { - ctx.ProblemDetails.Extensions.Add("API", "Link Administration"); + ctx.ProblemDetails.Extensions.Add("API", LinkAdminConstants.ServiceName); } else { diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index 877dadea1..e9b5f1519 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -86,11 +86,11 @@ static void RegisterServices(WebApplicationBuilder builder) }); authBuilder.AddCookie(LinkAdminConstants.AuthenticationSchemes.Cookie, options => - { - options.Cookie.Name = LinkAdminConstants.AuthenticationSchemes.Cookie; - options.Cookie.SameSite = SameSiteMode.Strict; - options.ExpireTimeSpan = TimeSpan.FromMinutes(30); - }); + { + options.Cookie.Name = LinkAdminConstants.AuthenticationSchemes.Cookie; + options.Cookie.SameSite = SameSiteMode.Strict; + options.ExpireTimeSpan = TimeSpan.FromMinutes(30); + }); //Add Oauth authorization scheme if enabled if(builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Enabled")) @@ -276,7 +276,7 @@ static void SetupMiddleware(WebApplication app) var apis = app.Services.GetServices(); foreach (var api in apis) { - if(api is null) throw new InvalidProgramException("No Endpoints were not found."); + if(api is null) throw new InvalidProgramException("No Endpoints were registered."); api.RegisterEndpoints(app); } diff --git a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs index 62f98bc5d..de03ef736 100644 --- a/LinkAdmin.BFF/Settings/LinkAdminConstants.cs +++ b/LinkAdmin.BFF/Settings/LinkAdminConstants.cs @@ -2,6 +2,8 @@ { public class LinkAdminConstants { + public const string ServiceName = "LinkAdminBFF"; + public static class AppSettingsSectionNames { public const string ExternalConfigurationSource = "ExternalConfigurationSource"; diff --git a/LinkAdmin.BFF/appsettings.json b/LinkAdmin.BFF/appsettings.json index 17c0ae595..eb69d7ad5 100644 --- a/LinkAdmin.BFF/appsettings.json +++ b/LinkAdmin.BFF/appsettings.json @@ -9,7 +9,7 @@ }, "CORS": { "PolicyName": "LinkAdminCorsPolicy", - "AllowedOrigins": [ "https://localhost:7007", "http://localhost:5005" ], + "AllowedOrigins": [ "http://localhost:5218" ], "AllowedMethods": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ], "AllowedHeaders": [ "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", "X-Requested-With" ], "AllowedExposedHeaders": [ "X-Pagination" ], From 2c8c06dc4a9722c8e3b28a7d6b78cb173a6478b7 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Wed, 27 Mar 2024 11:57:34 -0400 Subject: [PATCH 70/79] Added authorization to swagger --- LinkAdmin.BFF/Program.cs | 76 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/LinkAdmin.BFF/Program.cs b/LinkAdmin.BFF/Program.cs index e9b5f1519..c76fa247c 100644 --- a/LinkAdmin.BFF/Program.cs +++ b/LinkAdmin.BFF/Program.cs @@ -17,6 +17,8 @@ using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; var builder = WebApplication.CreateBuilder(args); @@ -198,9 +200,81 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { + + if (builder.Configuration.GetValue("Authentication:Schemas:Jwt:Enabled")) + { + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = $"Authorization using JWT", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + In = ParameterLocation.Header, + Scheme = JwtBearerDefaults.AuthenticationScheme + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }, + new List() + } + }); + } + + c.AddSecurityDefinition("OAuth", new OpenApiSecurityScheme + { + Description = $"Authorization using OAuth", + Name = "OAuth", + Type = SecuritySchemeType.OAuth2, + Scheme = LinkAdminConstants.AuthenticationSchemes.Oauth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Authorization")!), + TokenUrl = new Uri(builder.Configuration.GetValue("Authentication:Schemas:Oauth2:Endpoints:Token")!), + Scopes = new Dictionary + { + { "openid", "OpenId" }, + { "profile", "Profile" }, + { "email", "Email" } + } + } + } + + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "OAuth", + Type = ReferenceType.SecurityScheme + }, + Scheme = LinkAdminConstants.AuthenticationSchemes.Oauth2, + Name = "Oauth", + In = ParameterLocation.Header + + }, + new List() + } + }); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); + }); // Add logging redaction services @@ -271,7 +345,7 @@ static void SetupMiddleware(WebApplication app) app.UseAuthorization(); // Register endpoints - app.MapGet("/", (HttpContext ctx) => Results.Ok($"Welcome to {ServiceActivitySource.Instance.Name} version {ServiceActivitySource.Instance.Version}!")); + app.MapGet("/", (HttpContext ctx) => Results.Ok($"Welcome to {ServiceActivitySource.Instance.Name} version {ServiceActivitySource.Instance.Version}!")).AllowAnonymous(); var apis = app.Services.GetServices(); foreach (var api in apis) From 2d8d7e14a19eea7b67409ba28ddda25429b0ea0d Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 28 Mar 2024 10:46:04 -0700 Subject: [PATCH 71/79] Removing Kestrel configuration from default app config to get deployment in ACA working --- Tenant/appsettings.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Tenant/appsettings.json b/Tenant/appsettings.json index 53b41cac5..c848e21b4 100644 --- a/Tenant/appsettings.json +++ b/Tenant/appsettings.json @@ -26,21 +26,6 @@ "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7331" - }, - "https": { - "Url": "https://localhost:7332" - }, - "Grpc": { - "Url": "http://localhost:7333", - "Protocols": "Http2" - } - } - }, - "MeasureServiceRegistry": { "MeasureServiceApiUrl": "" }, From 65e204d4e3827c3a7ff196b4581f1060626f2930 Mon Sep 17 00:00:00 2001 From: Adam Phillips Date: Thu, 28 Mar 2024 13:54:49 -0400 Subject: [PATCH 72/79] Fixed notification service kestrel --- Notification/appsettings.Development.json | 14 ++++++++++++++ Notification/appsettings.json | 16 +--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Notification/appsettings.Development.json b/Notification/appsettings.Development.json index 7dd8c4dff..374341c97 100644 --- a/Notification/appsettings.Development.json +++ b/Notification/appsettings.Development.json @@ -31,6 +31,20 @@ "MetricsEndpoint": "http://localhost:9101", "TelemetryCollectorEndpoint": "http://localhost:55690" }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7434" + }, + "https": { + "Url": "https://localhost:7435" + }, + "Grpc": { + "Url": "http://localhost:7436", + "Protocols": "Http2" + } + } + }, "Logging": { "LogLevel": { "Default": "Warning", diff --git a/Notification/appsettings.json b/Notification/appsettings.json index 303fe7739..381bc02ad 100644 --- a/Notification/appsettings.json +++ b/Notification/appsettings.json @@ -47,21 +47,7 @@ "System": "Warning" } }, - "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7434" - }, - "https": { - "Url": "https://localhost:7435" - }, - "Grpc": { - "Url": "http://localhost:7436", - "Protocols": "Http2" - } - } - }, + "AllowedHosts": "*", "Link:Notification:Logging:Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Grafana.Loki" ], "MinimumLevel": { From d53c3731cc927de17c990ac39e8dab00be0448ce Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 28 Mar 2024 11:58:02 -0700 Subject: [PATCH 73/79] Moving Kestrel from default `appsettings.json` to `appsettings.Development.json` Adding Azure AppConfig dependency, and registering Azure AppConfig at startup --- Account/Account.csproj | 2 + Account/Program.cs | 30 ++++++++++++++ Account/Settings/AccountConstants.cs | 1 + Account/appsettings.Development.json | 14 +++++++ Account/appsettings.json | 14 ------- Census/Census.csproj | 2 + Census/Program.cs | 32 ++++++++++++++- Census/Settings/CensusConstants.cs | 1 + .../DataAcqTenantConfigMongoRepo.cs | 5 +-- .../Settings/DataAcquisitionConstants.cs | 16 ++++++++ DataAcquisition/DataAcquisition.csproj | 6 +++ DataAcquisition/Program.cs | 30 ++++++++++++++ .../Settings/DataAcquisitionConstants.cs | 20 --------- DemoApiGateway/appsettings.Development.json | 10 +++++ DemoApiGateway/appsettings.json | 10 ----- MeasureEval/MeasureEval.csproj | 2 + MeasureEval/Program.cs | 30 ++++++++++++++ MeasureEval/Settings/MeasureEvalConstants.cs | 1 + .../Settings/NormalizationConstants.cs | 2 + Normalization/Normalization.csproj | 2 + Normalization/Program.cs | 30 ++++++++++++++ Normalization/appsettings.Development.json | 14 +++++++ PatientsToQuery/PatientsToQuery.csproj | 2 + PatientsToQuery/Program.cs | 31 ++++++++++++++ .../Settings/PatientsToQueryConstants.cs | 7 ++++ .../Settings/QueryDispatchConstants.cs | 1 + QueryDispatch/Program.cs | 30 ++++++++++++++ QueryDispatch/QueryDispatch.csproj | 2 + QueryDispatch/appsettings.Development.json | 14 +++++++ QueryDispatch/appsettings.json | 14 ------- .../Application/Settings/ReportConstants.cs | 1 + Report/Program.cs | 30 ++++++++++++++ Report/Report.csproj | 2 + Report/appsettings.Development.json | 14 +++++++ Report/appsettings.json | 14 ------- Submission/Program.cs | 30 ++++++++++++++ Submission/Settings/SubmissionConstants.cs | 1 + Submission/Submission.csproj | 2 + Submission/appsettings.Development.json | 10 +++++ Submission/appsettings.json | 10 ----- Tenant/Program.cs | 41 ++++++++----------- Tenant/appsettings.Development.json | 1 - 42 files changed, 421 insertions(+), 110 deletions(-) delete mode 100644 DataAcquisition/Settings/DataAcquisitionConstants.cs diff --git a/Account/Account.csproj b/Account/Account.csproj index 4045d913d..814c888a9 100644 --- a/Account/Account.csproj +++ b/Account/Account.csproj @@ -21,6 +21,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/Account/Program.cs b/Account/Program.cs index b8c7fccfb..fa47331bf 100644 --- a/Account/Program.cs +++ b/Account/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Confluent.Kafka.Extensions.OpenTelemetry; using HealthChecks.UI.Client; using LantanaGroup.Link.Account.Domain.Entities; @@ -8,6 +9,7 @@ using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -32,6 +34,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(AccountConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", AccountConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", AccountConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + var serviceInformation = builder.Configuration.GetRequiredSection(AccountConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/Account/Settings/AccountConstants.cs b/Account/Settings/AccountConstants.cs index 60458881b..a19f72af0 100644 --- a/Account/Settings/AccountConstants.cs +++ b/Account/Settings/AccountConstants.cs @@ -11,6 +11,7 @@ public static class AppSettingsSectionNames public const string Postgres = "Postgres"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } } } diff --git a/Account/appsettings.Development.json b/Account/appsettings.Development.json index 72a455eac..c9f7f9d30 100644 --- a/Account/appsettings.Development.json +++ b/Account/appsettings.Development.json @@ -13,6 +13,20 @@ "Microsoft.AspNetCore": "Warning" } }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7221" + }, + "https": { + "Url": "https://localhost:7222" + }, + "Grpc": { + "Url": "http://localhost:7223", + "Protocols": "Http2" + } + } + }, "TenantApiSettings": { "TenantServiceBaseEndpoint": "https://localhost:7332/api/", "CheckIfTenantExists": true, diff --git a/Account/appsettings.json b/Account/appsettings.json index 088b400ae..fc799bf9f 100644 --- a/Account/appsettings.json +++ b/Account/appsettings.json @@ -25,20 +25,6 @@ "TelemetryCollectorEndpoint": "http://localhost:4317" }, "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7221" - }, - "https": { - "Url": "https://localhost:7222" - }, - "Grpc": { - "Url": "http://localhost:7223", - "Protocols": "Http2" - } - } - }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/Census/Census.csproj b/Census/Census.csproj index 955c51878..bbe5017a1 100644 --- a/Census/Census.csproj +++ b/Census/Census.csproj @@ -24,12 +24,14 @@ + + diff --git a/Census/Program.cs b/Census/Program.cs index ffa3aa44c..f46791eba 100644 --- a/Census/Program.cs +++ b/Census/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Census.Jobs; using Census.Repositories; using Census.Services; @@ -17,6 +18,7 @@ using LantanaGroup.Link.Shared.Application.Models.Configs; using LantanaGroup.Link.Shared.Application.Models.Kafka; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; @@ -37,8 +39,36 @@ app.Run(); -static void RegisterServices(WebApplicationBuilder builder) +static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(CensusConstants.AppSettings.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", CensusConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", CensusConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + var serviceInformation = builder.Configuration.GetRequiredSection(CensusConstants.AppSettings.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/Census/Settings/CensusConstants.cs b/Census/Settings/CensusConstants.cs index e41dd40f0..832f99b2f 100644 --- a/Census/Settings/CensusConstants.cs +++ b/Census/Settings/CensusConstants.cs @@ -13,6 +13,7 @@ public static class AppSettings public const string Mongo = "MongoDB"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } public static class Scheduler diff --git a/DataAcquisition/Application/Repositories/DataAcqTenantConfigMongoRepo.cs b/DataAcquisition/Application/Repositories/DataAcqTenantConfigMongoRepo.cs index db3067f82..5c1985f89 100644 --- a/DataAcquisition/Application/Repositories/DataAcqTenantConfigMongoRepo.cs +++ b/DataAcquisition/Application/Repositories/DataAcqTenantConfigMongoRepo.cs @@ -1,11 +1,10 @@ -using LantanaGroup.Link.DataAcquisition.Domain.Entities; +using LantanaGroup.Link.DataAcquisition.Application.Settings; using LantanaGroup.Link.DataAcquisition.Entities; using LantanaGroup.Link.Shared.Application.Models.Configs; using LantanaGroup.Link.Shared.Application.Repositories.Implementations; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; -using static LantanaGroup.Link.DataAcquisition.Settings.DataAcquisitionConstants; namespace LantanaGroup.Link.DataAcquisition.Application.Repositories; @@ -100,7 +99,7 @@ public async Task HealthCheck() } catch (Exception ex) { - _logger.LogError(new EventId(LoggingIds.HealthCheck, "Data Acquisition Service - Database Health Check"), ex, "Health check failed for database connection."); + _logger.LogError(new EventId(DataAcquisitionConstants.LoggingIds.HealthCheck, "Data Acquisition Service - Database Health Check"), ex, "Health check failed for database connection."); return false; } diff --git a/DataAcquisition/Application/Settings/DataAcquisitionConstants.cs b/DataAcquisition/Application/Settings/DataAcquisitionConstants.cs index 83c5f4b51..314b3c387 100644 --- a/DataAcquisition/Application/Settings/DataAcquisitionConstants.cs +++ b/DataAcquisition/Application/Settings/DataAcquisitionConstants.cs @@ -14,6 +14,7 @@ public static class AppSettingsSectionNames public const string ServiceInformation = "ServiceInformation"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } public static class ValidationErrorMessages @@ -48,4 +49,19 @@ public static class Extension { public const string CdcUri = "http://www.cdc.gov/nhsn/fhirportal/dqm/ig/StructureDefinition/link-received-date-extension"; } + + public static class LoggingIds + { + public const int GenerateItems = 1000; + public const int ListItems = 1001; + public const int GetItem = 1002; + public const int InsertItem = 1003; + public const int UpdateItem = 1004; + public const int DeleteItem = 1005; + public const int GetItemNotFound = 1006; + public const int UpdateItemNotFound = 1007; + public const int KafkaConsumer = 10008; + public const int KafkaProducer = 10009; + public const int HealthCheck = 10010; + } } diff --git a/DataAcquisition/DataAcquisition.csproj b/DataAcquisition/DataAcquisition.csproj index 96d339d87..bd0246b99 100644 --- a/DataAcquisition/DataAcquisition.csproj +++ b/DataAcquisition/DataAcquisition.csproj @@ -23,6 +23,7 @@ + @@ -34,6 +35,7 @@ + @@ -60,4 +62,8 @@ + + + + diff --git a/DataAcquisition/Program.cs b/DataAcquisition/Program.cs index 1b5c5f7ee..9dfd383d6 100644 --- a/DataAcquisition/Program.cs +++ b/DataAcquisition/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Confluent.Kafka; using Confluent.Kafka.Extensions.OpenTelemetry; using HealthChecks.UI.Client; @@ -20,6 +21,7 @@ using LantanaGroup.Link.Shared.Application.Models.Kafka; using LantanaGroup.Link.Shared.Application.Wrappers; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -42,6 +44,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(DataAcquisitionConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", DataAcquisitionConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", DataAcquisitionConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + var serviceInformation = builder.Configuration.GetSection(DataAcquisitionConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) diff --git a/DataAcquisition/Settings/DataAcquisitionConstants.cs b/DataAcquisition/Settings/DataAcquisitionConstants.cs deleted file mode 100644 index 2c3a0bae4..000000000 --- a/DataAcquisition/Settings/DataAcquisitionConstants.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace LantanaGroup.Link.DataAcquisition.Settings -{ - public class DataAcquisitionConstants - { - public static class LoggingIds - { - public const int GenerateItems = 1000; - public const int ListItems = 1001; - public const int GetItem = 1002; - public const int InsertItem = 1003; - public const int UpdateItem = 1004; - public const int DeleteItem = 1005; - public const int GetItemNotFound = 1006; - public const int UpdateItemNotFound = 1007; - public const int KafkaConsumer = 10008; - public const int KafkaProducer = 10009; - public const int HealthCheck = 10010; - } - } -} diff --git a/DemoApiGateway/appsettings.Development.json b/DemoApiGateway/appsettings.Development.json index 19d60d8ab..cb959a2e5 100644 --- a/DemoApiGateway/appsettings.Development.json +++ b/DemoApiGateway/appsettings.Development.json @@ -24,6 +24,16 @@ } } }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7777" + }, + "https": { + "Url": "https://localhost:7778" + } + } + }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/DemoApiGateway/appsettings.json b/DemoApiGateway/appsettings.json index c3e89aada..c701b3580 100644 --- a/DemoApiGateway/appsettings.json +++ b/DemoApiGateway/appsettings.json @@ -33,16 +33,6 @@ }, "AllowedHosts": "*", "EnableSwagger": false, - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7777" - }, - "https": { - "Url": "https://localhost:7778" - } - } - }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/MeasureEval/MeasureEval.csproj b/MeasureEval/MeasureEval.csproj index 6eeaf70cc..48ef9781b 100644 --- a/MeasureEval/MeasureEval.csproj +++ b/MeasureEval/MeasureEval.csproj @@ -27,12 +27,14 @@ + + diff --git a/MeasureEval/Program.cs b/MeasureEval/Program.cs index 63b35e176..97896d6b8 100644 --- a/MeasureEval/Program.cs +++ b/MeasureEval/Program.cs @@ -16,6 +16,8 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Confluent.Kafka.Extensions.OpenTelemetry; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Azure.Identity; var builder = WebApplication.CreateBuilder(args); @@ -29,6 +31,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(MeasureEvalConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", MeasureEvalConstants.AppSettingsSectionNames.ServiceName) + // Load configuration values for service name and environment + .Select("*", MeasureEvalConstants.AppSettingsSectionNames.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 diff --git a/MeasureEval/Settings/MeasureEvalConstants.cs b/MeasureEval/Settings/MeasureEvalConstants.cs index 8784efd0a..0e6c9888b 100644 --- a/MeasureEval/Settings/MeasureEvalConstants.cs +++ b/MeasureEval/Settings/MeasureEvalConstants.cs @@ -7,6 +7,7 @@ public static class AppSettingsSectionNames public const string ServiceInformation = "ServiceInformation"; public const string Telemetry = "TelemetryConfig"; public const string ServiceName = "MeasureEval"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } public static class MeasureEvalLoggingIds diff --git a/Normalization/Application/Settings/NormalizationConstants.cs b/Normalization/Application/Settings/NormalizationConstants.cs index 78ec631a2..353f757d5 100644 --- a/Normalization/Application/Settings/NormalizationConstants.cs +++ b/Normalization/Application/Settings/NormalizationConstants.cs @@ -11,6 +11,8 @@ public static class AppSettingsSectionNames public const string Redis = "Redis"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; + public const string ServiceName = "Normalization"; } public static class FixResourceIDCommand diff --git a/Normalization/Normalization.csproj b/Normalization/Normalization.csproj index 310b87ccf..6758d52ab 100644 --- a/Normalization/Normalization.csproj +++ b/Normalization/Normalization.csproj @@ -21,6 +21,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/Normalization/Program.cs b/Normalization/Program.cs index c7115249e..27e96ea89 100644 --- a/Normalization/Program.cs +++ b/Normalization/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Confluent.Kafka.Extensions.OpenTelemetry; using HealthChecks.UI.Client; using Hellang.Middleware.ProblemDetails; @@ -14,6 +15,7 @@ using LantanaGroup.Link.Shared.Application.Models.Configs; using LantanaGroup.Link.Shared.Application.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -34,6 +36,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(NormalizationConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", NormalizationConstants.AppSettingsSectionNames.ServiceName) + // Load configuration values for service name and environment + .Select("*", NormalizationConstants.AppSettingsSectionNames.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + var serviceInformation = builder.Configuration.GetRequiredSection(NormalizationConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/Normalization/appsettings.Development.json b/Normalization/appsettings.Development.json index 2ce4728d2..ea259cf61 100644 --- a/Normalization/appsettings.Development.json +++ b/Normalization/appsettings.Development.json @@ -8,6 +8,20 @@ "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "botw-normalization" }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7434" + }, + "https": { + "Url": "https://localhost:7435" + }, + "Grpc": { + "Url": "http://localhost:7436", + "Protocols": "Http2" + } + } + }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/PatientsToQuery/PatientsToQuery.csproj b/PatientsToQuery/PatientsToQuery.csproj index dfc42c30b..32ad35a1b 100644 --- a/PatientsToQuery/PatientsToQuery.csproj +++ b/PatientsToQuery/PatientsToQuery.csproj @@ -10,9 +10,11 @@ + + diff --git a/PatientsToQuery/Program.cs b/PatientsToQuery/Program.cs index 3af7f49a1..c21563586 100644 --- a/PatientsToQuery/Program.cs +++ b/PatientsToQuery/Program.cs @@ -11,9 +11,40 @@ using PatientsToQuery.Application.Settings; using System.Text.Json.Serialization; using Serilog; +using PatientsToQuery.Settings; +using Azure.Identity; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; var builder = WebApplication.CreateBuilder(args); +//load external configuration source if specified +var externalConfigurationSource = builder.Configuration.GetSection(PatientsToQueryConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + +if (!string.IsNullOrEmpty(externalConfigurationSource)) +{ + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", PatientsToQueryConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", PatientsToQueryConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } +} + var serviceInformation = builder.Configuration.GetRequiredSection(PatientToQueryConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/PatientsToQuery/Settings/PatientsToQueryConstants.cs b/PatientsToQuery/Settings/PatientsToQueryConstants.cs index c5e4c31df..0b58a3e8f 100644 --- a/PatientsToQuery/Settings/PatientsToQueryConstants.cs +++ b/PatientsToQuery/Settings/PatientsToQueryConstants.cs @@ -2,6 +2,13 @@ { public class PatientsToQueryConstants { + public static string ServiceName = "PatientsToQuery"; + + public static class AppSettingsSectionNames + { + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; + } + public static class LoggingIds { public const int GenerateItems = 1000; diff --git a/QueryDispatch/Application/Settings/QueryDispatchConstants.cs b/QueryDispatch/Application/Settings/QueryDispatchConstants.cs index a01c44590..59d6f9c45 100644 --- a/QueryDispatch/Application/Settings/QueryDispatchConstants.cs +++ b/QueryDispatch/Application/Settings/QueryDispatchConstants.cs @@ -28,6 +28,7 @@ public static class AppSettingsSectionNames public const string ServiceInformation = "ServiceInformation"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } public static class QueryDispatchLoggingIds diff --git a/QueryDispatch/Program.cs b/QueryDispatch/Program.cs index d30519201..462d2a1bb 100644 --- a/QueryDispatch/Program.cs +++ b/QueryDispatch/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Confluent.Kafka.Extensions.OpenTelemetry; using HealthChecks.UI.Client; using LanatanGroup.Link.QueryDispatch.Jobs; @@ -20,6 +21,7 @@ using LantanaGroup.Link.Shared.Application.Models.Kafka; using LantanaGroup.Link.Shared.Application.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -36,6 +38,34 @@ var builder = WebApplication.CreateBuilder(args); +//load external configuration source if specified +var externalConfigurationSource = builder.Configuration.GetSection(QueryDispatchConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + +if (!string.IsNullOrEmpty(externalConfigurationSource)) +{ + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", QueryDispatchConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", QueryDispatchConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } +} + var serviceInformation = builder.Configuration.GetRequiredSection(QueryDispatchConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/QueryDispatch/QueryDispatch.csproj b/QueryDispatch/QueryDispatch.csproj index 7bb9d13ed..ca9baf8d2 100644 --- a/QueryDispatch/QueryDispatch.csproj +++ b/QueryDispatch/QueryDispatch.csproj @@ -19,11 +19,13 @@ + + diff --git a/QueryDispatch/appsettings.Development.json b/QueryDispatch/appsettings.Development.json index aaa4a97b8..2b98637b4 100644 --- a/QueryDispatch/appsettings.Development.json +++ b/QueryDispatch/appsettings.Development.json @@ -9,5 +9,19 @@ "TenantServiceBaseEndpoint": "https://localhost:7332/api/", "CheckIfTenantExists": false, "GetTenantRelativeEndpoint": "facility/" + }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7334" + }, + "https": { + "Url": "https://localhost:7335" + }, + "Grpc": { + "Url": "http://localhost:7336", + "Protocols": "Http2" + } + } } } diff --git a/QueryDispatch/appsettings.json b/QueryDispatch/appsettings.json index 16618ba0b..9a91b031b 100644 --- a/QueryDispatch/appsettings.json +++ b/QueryDispatch/appsettings.json @@ -18,20 +18,6 @@ } }, "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7334" - }, - "https": { - "Url": "https://localhost:7335" - }, - "Grpc": { - "Url": "http://localhost:7336", - "Protocols": "Http2" - } - } - }, "ServiceInformation": { "Name": "Link QueryDispatch Service", "Version": "1.1.0-beta" diff --git a/Report/Application/Settings/ReportConstants.cs b/Report/Application/Settings/ReportConstants.cs index f496cb7f2..dc4487dbc 100644 --- a/Report/Application/Settings/ReportConstants.cs +++ b/Report/Application/Settings/ReportConstants.cs @@ -14,6 +14,7 @@ public static class AppSettingsSectionNames public const string ServiceInformation = "ServiceInformation"; public const string Telemetry = "TelemetryConfig"; public const string TenantApiSettings = "TenantApiSettings"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } public static class Bundle diff --git a/Report/Program.cs b/Report/Program.cs index 7f31b0c7c..01ffbd6c7 100644 --- a/Report/Program.cs +++ b/Report/Program.cs @@ -1,3 +1,4 @@ +using Azure.Identity; using Confluent.Kafka.Extensions.OpenTelemetry; using HealthChecks.UI.Client; using LantanaGroup.Link.Report.Application.Factory; @@ -16,6 +17,7 @@ using LantanaGroup.Link.Shared.Application.Models.Kafka; using LantanaGroup.Link.Shared.Application.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -40,6 +42,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(ReportConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", ReportConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", ReportConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + var serviceInformation = builder.Configuration.GetRequiredSection(ReportConstants.AppSettingsSectionNames.ServiceInformation).Get(); if (serviceInformation != null) { diff --git a/Report/Report.csproj b/Report/Report.csproj index 8789c9f79..7e96e3572 100644 --- a/Report/Report.csproj +++ b/Report/Report.csproj @@ -22,6 +22,7 @@ + @@ -30,6 +31,7 @@ + diff --git a/Report/appsettings.Development.json b/Report/appsettings.Development.json index 0e13c745b..917305a31 100644 --- a/Report/appsettings.Development.json +++ b/Report/appsettings.Development.json @@ -28,5 +28,19 @@ "TenantServiceBaseEndpoint": "https://localhost:7332/api/", "CheckIfTenantExists": true, "GetTenantRelativeEndpoint": "facility/" + }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:7110" + }, + "https": { + "Url": "https://localhost:7111" + }, + "Grpc": { + "Url": "http://localhost:7112", + "Protocols": "Http2" + } + } } } diff --git a/Report/appsettings.json b/Report/appsettings.json index ffaeec740..33d946d45 100644 --- a/Report/appsettings.json +++ b/Report/appsettings.json @@ -26,20 +26,6 @@ }, "AllowReflection": true, "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:7110" - }, - "https": { - "Url": "https://localhost:7111" - }, - "Grpc": { - "Url": "http://localhost:7112", - "Protocols": "Http2" - } - } - }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/Submission/Program.cs b/Submission/Program.cs index a17c3a2e4..3213c9d90 100644 --- a/Submission/Program.cs +++ b/Submission/Program.cs @@ -19,6 +19,8 @@ using LantanaGroup.Link.Shared.Application.Error.Handlers; using LantanaGroup.Link.Shared.Application.Error.Interfaces; using LantanaGroup.Link.Shared.Application.Models.Kafka; +using Azure.Identity; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +34,34 @@ static void RegisterServices(WebApplicationBuilder builder) { + //load external configuration source if specified + var externalConfigurationSource = builder.Configuration.GetSection(SubmissionConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); + + if (!string.IsNullOrEmpty(externalConfigurationSource)) + { + switch (externalConfigurationSource) + { + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", SubmissionConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", SubmissionConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => + { + kv.SetCredential(new DefaultAzureCredential()); + }); + + }); + break; + } + } + //Add problem details builder.Services.AddProblemDetails(opts => { opts.IncludeExceptionDetails = (ctx, ex) => false; diff --git a/Submission/Settings/SubmissionConstants.cs b/Submission/Settings/SubmissionConstants.cs index c7f0e58fa..5de9dd7c7 100644 --- a/Submission/Settings/SubmissionConstants.cs +++ b/Submission/Settings/SubmissionConstants.cs @@ -11,6 +11,7 @@ public static class AppSettingsSectionNames { public const string Kafka = "KafkaConnection"; public const string Mongo = "MongoDB"; + public const string ExternalConfigurationSource = "ExternalConfigurationSource"; } } diff --git a/Submission/Submission.csproj b/Submission/Submission.csproj index bfac4d3c8..0c3ac087e 100644 --- a/Submission/Submission.csproj +++ b/Submission/Submission.csproj @@ -26,6 +26,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/Submission/appsettings.Development.json b/Submission/appsettings.Development.json index 3aef1ece2..d9ece612e 100644 --- a/Submission/appsettings.Development.json +++ b/Submission/appsettings.Development.json @@ -22,5 +22,15 @@ } } ] + }, + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:64501" + }, + "https": { + "Url": "https://localhost:64502" + } + } } } diff --git a/Submission/appsettings.json b/Submission/appsettings.json index 20e3aac45..64749527b 100644 --- a/Submission/appsettings.json +++ b/Submission/appsettings.json @@ -23,16 +23,6 @@ }, "AllowReflection": true, "EnableSwagger": true, - "Kestrel": { - "Endpoints": { - "http": { - "Url": "http://localhost:64501" - }, - "https": { - "Url": "https://localhost:64502" - } - } - }, "Serilog": { "Using": [ "Serilog.Sinks.Grafana.Loki" diff --git a/Tenant/Program.cs b/Tenant/Program.cs index 311bb06d7..a556ad218 100644 --- a/Tenant/Program.cs +++ b/Tenant/Program.cs @@ -51,36 +51,31 @@ public static void Main(string[] args) static void RegisterServices(WebApplicationBuilder builder) { - //load external configuration source if specified var externalConfigurationSource = builder.Configuration.GetSection(TenantConstants.AppSettingsSectionNames.ExternalConfigurationSource).Get(); - if ("Development" != builder.Environment.EnvironmentName) + if (!string.IsNullOrEmpty(externalConfigurationSource)) { - - if (!string.IsNullOrEmpty(externalConfigurationSource)) + switch (externalConfigurationSource) { - switch (externalConfigurationSource) - { - case ("AzureAppConfiguration"): - builder.Configuration.AddAzureAppConfiguration(options => + case ("AzureAppConfiguration"): + builder.Configuration.AddAzureAppConfiguration(options => + { + options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) + // Load configuration values with no label + .Select("*", LabelFilter.Null) + // Load configuration values for service name + .Select("*", TenantConstants.ServiceName) + // Load configuration values for service name and environment + .Select("*", TenantConstants.ServiceName + ":" + builder.Environment.EnvironmentName); + + options.ConfigureKeyVault(kv => { - options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration")) - // Load configuration values with no label - .Select("*", LabelFilter.Null) - // Load configuration values for service name - .Select("*", TenantConstants.ServiceName) - // Load configuration values for service name and environment - .Select("*", TenantConstants.ServiceName + ":" + builder.Environment.EnvironmentName); - - options.ConfigureKeyVault(kv => - { - kv.SetCredential(new DefaultAzureCredential()); - }); - + kv.SetCredential(new DefaultAzureCredential()); }); - break; - } + + }); + break; } } diff --git a/Tenant/appsettings.Development.json b/Tenant/appsettings.Development.json index 41ecb8547..d65190b91 100644 --- a/Tenant/appsettings.Development.json +++ b/Tenant/appsettings.Development.json @@ -43,7 +43,6 @@ "MetricsEndpoint": "http://localhost:9101", "TelemetryCollectorEndpoint": "http://localhost:4317" }, - "Kestrel": { "Endpoints": { "http": { From 20e6a6ee9a2a3bc53658bec199f6c2129fcf561e Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 28 Mar 2024 16:26:57 -0700 Subject: [PATCH 74/79] Updating Kafka factories in Shared to use Sasl properties --- .../Application/Factories/KafkaConsumerFactory.cs | 13 +++++++++++++ .../Application/Factories/KafkaProducerFactory.cs | 12 ++++++++++++ Shared/Shared.csproj | 3 +-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Shared/Application/Factories/KafkaConsumerFactory.cs b/Shared/Application/Factories/KafkaConsumerFactory.cs index f213a4134..8342ee107 100644 --- a/Shared/Application/Factories/KafkaConsumerFactory.cs +++ b/Shared/Application/Factories/KafkaConsumerFactory.cs @@ -5,6 +5,7 @@ using LantanaGroup.Link.Shared.Application.SerDes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using ZstdSharp.Unsafe; namespace LantanaGroup.Link.Shared.Application.Factories; public class KafkaConsumerFactory : IKafkaConsumerFactory @@ -28,6 +29,18 @@ public IConsumer CreateConsumer(ConsumerConfig con } config.BootstrapServers = string.Join(", ", _kafkaConnection.Value.BootstrapServers); + config.ReceiveMessageMaxBytes = _kafkaConnection.Value.ReceiveMessageMaxBytes; + config.ClientId = _kafkaConnection.Value.ClientId; + config.GroupId = _kafkaConnection.Value.GroupId; + + if (_kafkaConnection.Value.SaslProtocolEnabled) + { + config.SecurityProtocol = SecurityProtocol.SaslSsl; + config.SaslMechanism = SaslMechanism.Plain; + config.SaslUsername = _kafkaConnection.Value.SaslUsername; + config.SaslPassword = _kafkaConnection.Value.SaslPassword; + config.ApiVersionRequest = _kafkaConnection.Value.ApiVersionRequest; + } var consumerBuilder = new ConsumerBuilder(config); diff --git a/Shared/Application/Factories/KafkaProducerFactory.cs b/Shared/Application/Factories/KafkaProducerFactory.cs index 7da3188ae..284d85f83 100644 --- a/Shared/Application/Factories/KafkaProducerFactory.cs +++ b/Shared/Application/Factories/KafkaProducerFactory.cs @@ -6,6 +6,7 @@ using LantanaGroup.Link.Shared.Application.SerDes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Linq; namespace LantanaGroup.Link.Shared.Application.Factories; public class KafkaProducerFactory : IKafkaProducerFactory @@ -38,6 +39,17 @@ public IProducer CreateProducer(ProducerConfig con try { config.BootstrapServers = string.Join(", ", _kafkaConnection.Value.BootstrapServers); + config.ReceiveMessageMaxBytes = _kafkaConnection.Value.ReceiveMessageMaxBytes; + config.ClientId = _kafkaConnection.Value.ClientId; + + if (_kafkaConnection.Value.SaslProtocolEnabled) + { + config.SecurityProtocol = SecurityProtocol.SaslSsl; + config.SaslMechanism = SaslMechanism.Plain; + config.SaslUsername = _kafkaConnection.Value.SaslUsername; + config.SaslPassword = _kafkaConnection.Value.SaslPassword; + config.ApiVersionRequest = _kafkaConnection.Value.ApiVersionRequest; + } var producerBuilder = new ProducerBuilder(config); diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 9e6ab4dc2..53edc9d6b 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -13,8 +13,7 @@ Lantana Consulting Group Shared library for Lantana Link NHSN Link Shared Library - - 2.1.1 + 2.1.2 com.lantanagroup.link.Shared From 13dddeff414acc4bcafaef4bcdc5a8bfa915ce0f Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 28 Mar 2024 16:43:25 -0700 Subject: [PATCH 75/79] Updates to data acquisition to use the latest version of Shared (2.1.2) Refactored appsettings kafka propeties Removed unused measure eval config variable --- DataAcquisition/DataAcquisition.csproj | 2 +- DataAcquisition/Listeners/QueryListener.cs | 14 +++++++------- DataAcquisition/appsettings.Development.json | 4 ++++ DataAcquisition/appsettings.json | 3 +-- MeasureEval/Models/MeasureEvalConfig.cs | 1 - MeasureEval/appsettings.Development.json | 1 - MeasureEvalTests/MeasureEvalServiceTests.cs | 3 +-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/DataAcquisition/DataAcquisition.csproj b/DataAcquisition/DataAcquisition.csproj index bd0246b99..9d9f7490c 100644 --- a/DataAcquisition/DataAcquisition.csproj +++ b/DataAcquisition/DataAcquisition.csproj @@ -24,7 +24,7 @@ - + diff --git a/DataAcquisition/Listeners/QueryListener.cs b/DataAcquisition/Listeners/QueryListener.cs index af396b5be..0cae89131 100644 --- a/DataAcquisition/Listeners/QueryListener.cs +++ b/DataAcquisition/Listeners/QueryListener.cs @@ -79,7 +79,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Query, ""); continue; } @@ -98,7 +98,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca _logger.LogError(ex, "Error deserializing message: {1}", ex.Message); _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Query, ""); continue; } @@ -112,7 +112,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca { _logger.LogError(ex, "Error extracting facility id and correlation id from message"); _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, ex, ""); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Query, ""); continue; } @@ -120,7 +120,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca { var errorMessage = "No Facility ID provided. Unable to process message: {1}"; _logger.LogWarning(errorMessage, message); - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), ""); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception($"No Facility ID provided. Unable to process message: {message}"), AuditEventType.Query, ""); continue; } @@ -144,7 +144,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Query, messageMetaData.facilityId); _logger.LogError(ex, "Error producing message: {1}", ex.Message); responseMessages = null; continue; @@ -202,7 +202,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca catch (Exception ex) { _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, ex, messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, ex, AuditEventType.Query, messageMetaData.facilityId); _logger.LogError(ex, "Failed to produce message"); continue; } @@ -223,7 +223,7 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca _logger.LogWarning("Message with topic: {1} meets no condition for processing. full message: {2}", rawmessage.Topic, rawmessage.Message); _deadLetterConsumerHandler.Topic = rawmessage?.Topic + "-Error"; - _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), messageMetaData.facilityId); + _deadLetterConsumerHandler.HandleException(rawmessage, new Exception("Message meets no condition for processing"), AuditEventType.Query, messageMetaData.facilityId); } } } diff --git a/DataAcquisition/appsettings.Development.json b/DataAcquisition/appsettings.Development.json index 3d8aca181..db941f52b 100644 --- a/DataAcquisition/appsettings.Development.json +++ b/DataAcquisition/appsettings.Development.json @@ -13,5 +13,9 @@ } } ] + }, + "KafkaConnection": { + "BootstrapServers": [ "localhost:9092" ], + "GroupId": "default" } } diff --git a/DataAcquisition/appsettings.json b/DataAcquisition/appsettings.json index ab87fb4b2..e7ee31f0d 100644 --- a/DataAcquisition/appsettings.json +++ b/DataAcquisition/appsettings.json @@ -1,7 +1,6 @@ { "KafkaConnection": { - "BootstrapServers": [ "localhost:9092" ], - "ClientId": "", + "BootstrapServers": [], "GroupId": "DataAcquisition" }, "ServiceInformation": { diff --git a/MeasureEval/Models/MeasureEvalConfig.cs b/MeasureEval/Models/MeasureEvalConfig.cs index 3bbd35006..cc302b5f0 100644 --- a/MeasureEval/Models/MeasureEvalConfig.cs +++ b/MeasureEval/Models/MeasureEvalConfig.cs @@ -2,7 +2,6 @@ { public class MeasureEvalConfig { - public string DataStoreServiceUrl { get; set; } = null!; // TODO: Replace with MongoDB? public string TerminologyServiceUrl { get; set; } = null!; public string EvaluationServiceUrl { get; set; } = null!; public int MaxRetry { get; set; } = 10; diff --git a/MeasureEval/appsettings.Development.json b/MeasureEval/appsettings.Development.json index f5dd4ce94..215561ae6 100644 --- a/MeasureEval/appsettings.Development.json +++ b/MeasureEval/appsettings.Development.json @@ -18,7 +18,6 @@ "CollectionName": "MeasureDefinitions" }, "MeasureEvalConfig": { - "DataStoreServiceUrl": "https://dev-fhir.nhsnlink.org/fhir", "TerminologyServiceUrl": "https://cqf-ruler.nhsnlink.org/fhir", "EvaluationServiceUrl": "https://cqf-ruler.nhsnlink.org/fhir" } diff --git a/MeasureEvalTests/MeasureEvalServiceTests.cs b/MeasureEvalTests/MeasureEvalServiceTests.cs index 9f6b1d5eb..1f907e805 100644 --- a/MeasureEvalTests/MeasureEvalServiceTests.cs +++ b/MeasureEvalTests/MeasureEvalServiceTests.cs @@ -32,8 +32,7 @@ public async Task UpdateMeasure() var measureEvalConfig = new MeasureEvalConfig { - EvaluationServiceUrl = "https://www.testcqfruler.com/fhir", - DataStoreServiceUrl = "https://ehr-test.com/fhir" + EvaluationServiceUrl = "https://www.testcqfruler.com/fhir" }; var httpClient = CreateHttpClient(HttpMethod.Post, measureEvalConfig.EvaluationServiceUrl, HttpStatusCode.OK, cqfrResponse); From 45b445a8c363837e42967587a55bfe6f1442eb41 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 28 Mar 2024 20:08:04 -0700 Subject: [PATCH 76/79] Upgrading normalization service to use latest shared library Removing `Commit()` from dead letter handling; otherwise it throws an exception --- Normalization/Listeners/PatientDataAcquiredListener.cs | 1 - Normalization/Normalization.csproj | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Normalization/Listeners/PatientDataAcquiredListener.cs b/Normalization/Listeners/PatientDataAcquiredListener.cs index a66db9bcc..c178b69c7 100644 --- a/Normalization/Listeners/PatientDataAcquiredListener.cs +++ b/Normalization/Listeners/PatientDataAcquiredListener.cs @@ -87,7 +87,6 @@ private async System.Threading.Tasks.Task StartConsumerLoop(CancellationToken ca // Update the signature of HandleException to reflect the fact that we anticipate (and explicitly check for) potentially null values? // For now, use the null-forgiving operator (here and below) to suppress compiler warnings _deadLetterExceptionHandler.HandleException(null!, ex, AuditEventType.Create, null!); - kafkaConsumer.Commit(); continue; } diff --git a/Normalization/Normalization.csproj b/Normalization/Normalization.csproj index 6758d52ab..aadb21f08 100644 --- a/Normalization/Normalization.csproj +++ b/Normalization/Normalization.csproj @@ -22,7 +22,7 @@ - + From 4b43dc8248baa673ed30ddafe10befa22dab8dec Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 1 Apr 2024 09:21:41 -0700 Subject: [PATCH 77/79] Changing account's constant to "Account" instead of "Account Service" for consistency --- Account/Settings/AccountConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Account/Settings/AccountConstants.cs b/Account/Settings/AccountConstants.cs index a19f72af0..54950c4f9 100644 --- a/Account/Settings/AccountConstants.cs +++ b/Account/Settings/AccountConstants.cs @@ -2,7 +2,7 @@ { public static class AccountConstants { - public const string ServiceName = "Account Service"; + public const string ServiceName = "Account"; public static class AppSettingsSectionNames { From 8136f2d7aed1c69c9db1c1fa0e92c3b7310e7d6f Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 1 Apr 2024 09:25:56 -0700 Subject: [PATCH 78/79] Update azure-pipelines.account.cd.yaml for Azure Pipelines --- Azure_Pipelines/azure-pipelines.account.cd.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Azure_Pipelines/azure-pipelines.account.cd.yaml b/Azure_Pipelines/azure-pipelines.account.cd.yaml index db9bd0c19..aa2ade50f 100644 --- a/Azure_Pipelines/azure-pipelines.account.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.account.cd.yaml @@ -18,6 +18,8 @@ variables: value: 'AccountUnitTests/AccountUnitTests.csproj' - name: nugetPath value: 'Account/nuget.config' +- name: serviceName + value: 'Account' - name: tags1 value: 'latest' - name: tags2 From 59033f9509de93f78e1789493616cf366fe2b0eb Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 1 Apr 2024 10:37:32 -0700 Subject: [PATCH 79/79] Updating CD pipelines to use new container registry --- .../azure-pipelines.account.cd.yaml | 5 +- Azure_Pipelines/azure-pipelines.audit.cd.yaml | 4 +- .../azure-pipelines.census.cd.yaml | 4 +- .../azure-pipelines.dataacquisition.cd.yaml | 4 +- .../azure-pipelines.gateway.cd.yaml | 3 +- .../azure-pipelines.measureeval.cd.yaml | 4 +- .../azure-pipelines.normalization.cd.yaml | 4 +- .../azure-pipelines.notification.cd.yaml | 4 +- ...-pipelines.patientbundlepermeasure.cd.yaml | 72 ------------------- ...-pipelines.patientbundlepermeasure.ci.yaml | 37 ---------- .../azure-pipelines.patientlist.cd.yaml | 4 +- .../azure-pipelines.patientstoquery.cd.yaml | 4 +- .../azure-pipelines.querydispatch.cd.yaml | 4 +- .../azure-pipelines.report.cd.yaml | 5 +- .../azure-pipelines.submission.cd.yaml | 4 +- .../azure-pipelines.tenant.cd.yaml | 5 +- .../azure-pipelines.validation.cd.yaml | 4 +- 17 files changed, 47 insertions(+), 124 deletions(-) delete mode 100644 Azure_Pipelines/azure-pipelines.patientbundlepermeasure.cd.yaml delete mode 100644 Azure_Pipelines/azure-pipelines.patientbundlepermeasure.ci.yaml diff --git a/Azure_Pipelines/azure-pipelines.account.cd.yaml b/Azure_Pipelines/azure-pipelines.account.cd.yaml index aa2ade50f..51fe16c0c 100644 --- a/Azure_Pipelines/azure-pipelines.account.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.account.cd.yaml @@ -28,6 +28,9 @@ variables: value: 'link-account' - name: dockerPath value: '**/Account/Dockerfile' +- name: containerRegistry + value: 'NHSNLink ACR Premium' + steps: - template: ../azure-nuget-package-source.yml - task: DotNetCoreCLI@2 @@ -55,7 +58,7 @@ steps: displayName: "Build & Push Account Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.audit.cd.yaml b/Azure_Pipelines/azure-pipelines.audit.cd.yaml index eaa1b64f7..9cf5c7397 100644 --- a/Azure_Pipelines/azure-pipelines.audit.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.audit.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Audit/Dockerfile' - name: serviceName value: 'Audit' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Audit Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.census.cd.yaml b/Azure_Pipelines/azure-pipelines.census.cd.yaml index de3a68938..abd46530d 100644 --- a/Azure_Pipelines/azure-pipelines.census.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.census.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Census/Dockerfile' - name: serviceName value: 'Census' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Census Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.dataacquisition.cd.yaml b/Azure_Pipelines/azure-pipelines.dataacquisition.cd.yaml index c0cf50645..e675eb1ea 100644 --- a/Azure_Pipelines/azure-pipelines.dataacquisition.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.dataacquisition.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/DataAcquisition/Dockerfile' - name: serviceName value: 'DataAcquisition' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push DataAcquisition Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.gateway.cd.yaml b/Azure_Pipelines/azure-pipelines.gateway.cd.yaml index 708d53996..7efc67b2c 100644 --- a/Azure_Pipelines/azure-pipelines.gateway.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.gateway.cd.yaml @@ -17,6 +17,7 @@ variables: $(Build.BuildId) registry-repo-Name: 'link-api-gw' dockerPath: '**/DemoApiGateway/Dockerfile' + containerRegistry: 'NHSNLink ACR Premium' steps: - task: PowerShell@2 @@ -73,7 +74,7 @@ steps: displayName: "Build & Push DemoApiGateway Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml b/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml index f8719e427..a2edf50b7 100644 --- a/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/MeasureEval/Dockerfile' - name: serviceName value: 'MeasureEval' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push MeasureEval Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.normalization.cd.yaml b/Azure_Pipelines/azure-pipelines.normalization.cd.yaml index 8edb636b0..8714ff53f 100644 --- a/Azure_Pipelines/azure-pipelines.normalization.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.normalization.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Normalization/Dockerfile' - name: serviceName value: 'Normalization' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Normalization Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.notification.cd.yaml b/Azure_Pipelines/azure-pipelines.notification.cd.yaml index 197be68a9..7c1c744f2 100644 --- a/Azure_Pipelines/azure-pipelines.notification.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.notification.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Notification/Dockerfile' - name: serviceName value: 'Notification' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Notification Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.cd.yaml b/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.cd.yaml deleted file mode 100644 index ce604463f..000000000 --- a/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.cd.yaml +++ /dev/null @@ -1,72 +0,0 @@ -trigger: - branches: - include: - - master - paths: - include: - - PatientBundlePerMeasure/* -pr: none - -pool: - vmImage: 'ubuntu-latest' - -variables: -- group: Link_BOTW_Build_Package_Variables -- name: project - value: 'PatientBundlePerMeasure/PatientBundlePerMeasure.csproj' -- name: testProject - value: 'PatientBundlePerMeasureUnitTests/PatientBundlePerMeasureUnitTests.csproj' -- name: nugetPath - value: 'PatientBundlePerMeasure/nuget.config' -- name: tags1 - value: 'latest' -- name: tags2 - value: '$(Build.BuildId)' -- name: registry-repo-Name - value: 'link-patientbundle' -- name: dockerPath - value: '**/PatientBundlePerMeasure/Dockerfile' -- name: serviceName - value: 'PatientBundlePerMeasure' - -steps: - - template: ../azure-nuget-package-source.yml - - - task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: restore - projects: '$(project)' - nugetConfigPath: '$(nugetPath)' - feedsToUse: config - - - task: DotNetCoreCLI@2 - displayName: Build - inputs: - command: build - projects: '$(project)' - - - task: DotNetCoreCLI@2 - inputs: - command: 'test' - projects: $(testProject)' - displayName: 'Run Tests' - - - - task: Docker@2 - displayName: "Build & Push Account Docker Image" - condition: always() - inputs: - containerRegistry: 'Link ACR' - repository: $(registry-repo-name) - command: 'buildAndPush' - Dockerfile: $(dockerPath) - tags: | - $(tags1) - $(tags2) - buildContext: '$(Build.Repository.LocalPath)' - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)' - artifact: 'manifest' diff --git a/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.ci.yaml b/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.ci.yaml deleted file mode 100644 index dbf4636b1..000000000 --- a/Azure_Pipelines/azure-pipelines.patientbundlepermeasure.ci.yaml +++ /dev/null @@ -1,37 +0,0 @@ -trigger: - paths: - include: - - PatientBundlePerMeasure/* - -pool: - vmImage: 'ubuntu-latest' - -variables: - project: 'PatientBundlePerMeasure/PatientBundlePerMeasure.csproj' - testProject: 'PatientBundlePerMeasureUnitTests/PatientBundlePerMeasureUnitTests.csproj' - nugetPath: 'PatientBundlePerMeasure/nuget.config' - serviceName: 'PatientBundlePerMeasure' - -steps: - - template: ../azure-nuget-package-source.yml - - - task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: restore - projects: '$(project)' - nugetConfigPath: '$(nugetPath)' - feedsToUse: config - - - task: DotNetCoreCLI@2 - displayName: Build - inputs: - command: build - projects: '$(project)' - - - task: DotNetCoreCLI@2 - inputs: - command: 'test' - projects: $(testProject) - displayName: 'Run Tests' - diff --git a/Azure_Pipelines/azure-pipelines.patientlist.cd.yaml b/Azure_Pipelines/azure-pipelines.patientlist.cd.yaml index c6f18ae8e..82e2b3011 100644 --- a/Azure_Pipelines/azure-pipelines.patientlist.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.patientlist.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/PatientList/Dockerfile' - name: serviceName value: 'PatientList' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push PatientList Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.patientstoquery.cd.yaml b/Azure_Pipelines/azure-pipelines.patientstoquery.cd.yaml index 325810438..d6166910d 100644 --- a/Azure_Pipelines/azure-pipelines.patientstoquery.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.patientstoquery.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/PatientsToQuery/Dockerfile' - name: serviceName value: 'PatientsToQuery' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Account Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.querydispatch.cd.yaml b/Azure_Pipelines/azure-pipelines.querydispatch.cd.yaml index 2fa3ae485..42c0721d3 100644 --- a/Azure_Pipelines/azure-pipelines.querydispatch.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.querydispatch.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/QueryDispatch/Dockerfile' - name: serviceName value: 'QueryDispatch' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push QueryDispatch Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.report.cd.yaml b/Azure_Pipelines/azure-pipelines.report.cd.yaml index e227986c3..b70c5f408 100644 --- a/Azure_Pipelines/azure-pipelines.report.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.report.cd.yaml @@ -28,6 +28,9 @@ variables: value: '**/Report/Dockerfile' - name: serviceName value: 'Report' +- name: containerRegistry + value: 'NHSNLink ACR Premium' + steps: - template: ../azure-nuget-package-source.yml @@ -56,7 +59,7 @@ steps: displayName: "Build & Push Report Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.submission.cd.yaml b/Azure_Pipelines/azure-pipelines.submission.cd.yaml index eed8c3850..1bf498d72 100644 --- a/Azure_Pipelines/azure-pipelines.submission.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.submission.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Submission/Dockerfile' - name: serviceName value: 'Submission' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -56,7 +58,7 @@ steps: displayName: "Build & Push Account Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.tenant.cd.yaml b/Azure_Pipelines/azure-pipelines.tenant.cd.yaml index ba2d256c0..586556bb1 100644 --- a/Azure_Pipelines/azure-pipelines.tenant.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.tenant.cd.yaml @@ -28,6 +28,9 @@ variables: value: '**/Tenant/Dockerfile' - name: serviceName value: 'Tenant' +- name: containerRegistry + value: 'NHSNLink ACR Premium' + steps: - template: ../azure-nuget-package-source.yml @@ -56,7 +59,7 @@ steps: displayName: "Build & Push Tenant Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath) diff --git a/Azure_Pipelines/azure-pipelines.validation.cd.yaml b/Azure_Pipelines/azure-pipelines.validation.cd.yaml index 533dab9fe..c95e31b1e 100644 --- a/Azure_Pipelines/azure-pipelines.validation.cd.yaml +++ b/Azure_Pipelines/azure-pipelines.validation.cd.yaml @@ -28,6 +28,8 @@ variables: value: '**/Validation/Dockerfile' - name: serviceName value: 'Validation' +- name: containerRegistry + value: 'NHSNLink ACR Premium' steps: - template: ../azure-nuget-package-source.yml @@ -57,7 +59,7 @@ steps: displayName: "Build & Push Validation Docker Image" condition: always() inputs: - containerRegistry: 'Link ACR' + containerRegistry: $(containerRegistry) repository: $(registry-repo-name) command: 'buildAndPush' Dockerfile: $(dockerPath)