diff --git a/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml b/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml
index 73ec8bdcf..d66583bc4 100644
--- a/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml
+++ b/Azure_Pipelines/azure-pipelines.measureeval.cd.yaml
@@ -73,7 +73,7 @@ steps:
Write-Host "Set MyTag to: $myTag1"
- task: Docker@2
- displayName: "Build & Push Audit Docker Image"
+ displayName: "Build & Push MeasureEval Docker Image"
condition: always()
inputs:
containerRegistry: $(containerRegistry)
@@ -89,3 +89,16 @@ steps:
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'manifest'
+
+ - task: Maven@2
+ displayName: Package CLI
+ inputs:
+ mavenPomFile: 'Java/pom.xml'
+ goals: 'clean package'
+ options: '-P cli -pl measureeval -am'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish MeasureEval CLI Jar'
+ inputs:
+ targetPath: 'Java/measureeval/target/measureeval-cli.jar'
+ artifact: 'measureeval-cli-jar'
\ No newline at end of file
diff --git a/Java/measureeval/pom.xml b/Java/measureeval/pom.xml
index eda847413..ed0b98ca3 100644
--- a/Java/measureeval/pom.xml
+++ b/Java/measureeval/pom.xml
@@ -190,7 +190,6 @@
cli
com.lantanagroup.link.measureeval.FileSystemInvocation
- src/main/resources/logback-cli.xml
measureeval-cli
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/FileSystemInvocation.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/FileSystemInvocation.java
index 6499cc275..5a8661135 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/FileSystemInvocation.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/FileSystemInvocation.java
@@ -1,7 +1,12 @@
package com.lantanagroup.link.measureeval;
import ca.uhn.fhir.context.FhirContext;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.core.joran.spi.JoranException;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
+import com.lantanagroup.link.measureeval.utils.CqlLogAppender;
+import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.measureeval.utils.StreamUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
@@ -28,6 +33,15 @@ public class FileSystemInvocation {
private static final FhirContext fhirContext = FhirContext.forR4Cached();
private static final Logger logger = LoggerFactory.getLogger(FileSystemInvocation.class);
+ private static void configureLogging(Bundle bundle) throws JoranException {
+ LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+ context.reset();
+ JoranConfigurator configurator = new JoranConfigurator();
+ configurator.setContext(context);
+ configurator.doConfigure(ClassLoader.getSystemResource("logback-cli.xml"));
+ CqlLogAppender.start(context, libraryId -> CqlUtils.getLibrary(bundle, libraryId));
+ }
+
private static Bundle getBundle(String measureBundlePath) throws IOException {
logger.info("Loading measure bundle from: {}", measureBundlePath);
@@ -138,7 +152,7 @@ private static Patient findPatient(Bundle bundle) {
.orElseThrow(() -> new IllegalArgumentException("Patient resource not found in bundle"));
}
- private static void evaluatePatientBundle(String patientBundlePath, Bundle patientBundle, String start, String end, MeasureEvaluator evaluator) {
+ private static void evaluatePatientBundle(Bundle patientBundle, String start, String end, MeasureEvaluator evaluator, boolean isDebug) {
Patient patient = findPatient(patientBundle);
var report = evaluator.evaluate(
new DateTimeType(start),
@@ -162,6 +176,7 @@ public static void main(String[] args) {
try {
Bundle measureBundle = getBundle(measureBundlePath);
+ configureLogging(measureBundle);
MeasureEvaluator evaluator = MeasureEvaluator.compile(fhirContext, measureBundle, true);
File patientBundleFile = new File(patientBundlePath);
@@ -171,11 +186,11 @@ public static void main(String[] args) {
for (Bundle patientBundle : patientBundles) {
logger.info("\n===================================================");
- evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
+ evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} else {
Bundle patientBundle = getBundle(patientBundlePath);
- evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
+ evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} catch (Exception e) {
System.err.println("Error occurred while evaluating measure: " + e.getMessage());
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/MeasureEvalApplication.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/MeasureEvalApplication.java
index ed06ee75b..76e847cc3 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/MeasureEvalApplication.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/MeasureEvalApplication.java
@@ -1,9 +1,14 @@
package com.lantanagroup.link.measureeval;
+import ch.qos.logback.classic.LoggerContext;
+import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
+import com.lantanagroup.link.measureeval.utils.CqlLogAppender;
+import org.slf4j.LoggerFactory;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
@@ -20,4 +25,10 @@ public static void main(String[] args) {
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}
+
+ @Bean
+ public CqlLogAppender cqlLogAppender(MeasureEvaluatorCache measureEvaluatorCache) {
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+ return CqlLogAppender.start(loggerContext, measureEvaluatorCache);
+ }
}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/configs/LinkConfig.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/configs/LinkConfig.java
index b7e5fe78d..f0ec6343b 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/configs/LinkConfig.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/configs/LinkConfig.java
@@ -16,6 +16,8 @@
public class LinkConfig {
private String reportabilityPredicate;
+ private boolean cqlDebug = false;
+
@Bean
@SuppressWarnings("unchecked")
public Predicate reportabilityPredicate() throws Exception {
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/controllers/MeasureDefinitionController.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/controllers/MeasureDefinitionController.java
index 7e8248664..383f51549 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/controllers/MeasureDefinitionController.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/controllers/MeasureDefinitionController.java
@@ -1,5 +1,6 @@
package com.lantanagroup.link.measureeval.controllers;
+import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.annotation.JsonView;
import com.lantanagroup.link.measureeval.entities.MeasureDefinition;
import com.lantanagroup.link.measureeval.repositories.MeasureDefinitionRepository;
@@ -7,9 +8,12 @@
import com.lantanagroup.link.measureeval.services.MeasureDefinitionBundleValidator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
+import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.shared.auth.PrincipalUser;
import io.opentelemetry.api.trace.Span;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import javassist.NotFoundException;
import org.apache.commons.text.StringEscapeUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.MeasureReport;
@@ -98,21 +102,50 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV
return entity;
}
+ @GetMapping("/{id}/{library-id}/$cql")
+ @PreAuthorize("hasAuthority('IsLinkAdmin')")
+ @Operation(summary = "Get the CQL for a measure definition's library", tags = {"Measure Definitions"})
+ @Parameter(name = "id", description = "The ID of the measure definition", required = true)
+ @Parameter(name = "library-id", description = "The ID of the library in the measure definition", required = true)
+ @Parameter(name = "range", description = "The range of the CQL to return (e.g. 37:1-38:22)", required = false)
+ public String getMeasureLibraryCQL(
+ @PathVariable("id") String measureId,
+ @PathVariable("library-id") String libraryId,
+ @RequestParam(value = "range", required = false) String range) {
+
+ // Test that the range format is correct (i.e. "37:1-38:22")
+ if (range != null && !range.matches("\\d+:\\d+-\\d+:\\d+")) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid range format");
+ }
+
+ // Get the measure definition from the repo by ID
+ MeasureDefinition measureDefinition = repository.findById(measureId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+
+ try {
+ return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range);
+ } catch (NotFoundException e) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage(), e);
+ }
+ }
+
@PostMapping("/{id}/$evaluate")
@PreAuthorize("hasAuthority('IsLinkAdmin')")
@Operation(summary = "Evaluate a measure against data in request body", tags = {"Measure Definitions"})
- public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters) {
+ @Parameter(name = "id", description = "The ID of the measure definition", required = true)
+ @Parameter(name = "parameters", description = "The parameters to use in the evaluation", required = true)
+ @Parameter(name = "debug", description = "Whether to log CQL debugging information during evaluation", required = false)
+ public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters, @RequestParam(required = false, defaultValue = "false") boolean debug) {
if (user != null){
Span currentSpan = Span.current();
currentSpan.setAttribute("user", user.getEmailAddress());
}
- MeasureEvaluator evaluator = evaluatorCache.get(id);
- if (evaluator == null) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND);
- }
+
try {
- return evaluator.evaluate(parameters);
+ // Ensure that a measure evaluator is cached (so that CQL logging can use it)
+ MeasureEvaluator evaluator = evaluatorCache.get(id);
+ // But recompile the bundle every time because the debug flag may not match what's in the cache
+ return MeasureEvaluator.compileAndEvaluate(FhirContext.forR4(), evaluator.getBundle(), parameters, debug);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/repositories/LinkInMemoryFhirRepository.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/repositories/LinkInMemoryFhirRepository.java
new file mode 100644
index 000000000..a17ca3892
--- /dev/null
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/repositories/LinkInMemoryFhirRepository.java
@@ -0,0 +1,57 @@
+package com.lantanagroup.link.measureeval.repositories;
+
+import ca.uhn.fhir.context.FhirContext;
+import org.hl7.fhir.instance.model.api.IBaseBundle;
+import org.hl7.fhir.r4.model.Bundle;
+import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+/**
+ * This class extends the InMemoryFhirRepository to provide a transaction method that will update the resources in the repository.
+ * This implementation is primarily used to avoid the exception stack trace that is thrown when the InMemoryFhirRepository.transaction method is called by measure eval, but is not implemented
+ * in the default InMemoryFhirRepository.
+ */
+public class LinkInMemoryFhirRepository extends InMemoryFhirRepository {
+ private static final Logger logger = LoggerFactory.getLogger(LinkInMemoryFhirRepository.class);
+
+ public LinkInMemoryFhirRepository(FhirContext context) {
+ super(context);
+ }
+
+ public LinkInMemoryFhirRepository(FhirContext context, IBaseBundle bundle) {
+ super(context, bundle);
+ }
+
+ @Override
+ public B transaction(B transaction, Map headers) {
+ if (!(transaction instanceof Bundle)) {
+ return transaction;
+ }
+
+ Bundle bundle = (Bundle) transaction;
+
+ if (bundle.getEntry() == null || bundle.getEntry().size() == 0) {
+ return transaction;
+ }
+
+ for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
+ if (entry != null && entry.hasResource()) {
+ try {
+ // Ensure each resource has an ID, or create a GUID for them
+ if (!entry.getResource().hasId()) {
+ entry.getResource().setId(java.util.UUID.randomUUID().toString());
+ }
+
+ this.update(entry.getResource());
+ } catch (Exception ex) {
+ logger.warn("Failed to process resource in transaction", ex);
+ }
+ }
+ }
+
+ return transaction;
+ }
+}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/LibraryResolver.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/LibraryResolver.java
new file mode 100644
index 000000000..04cdc5367
--- /dev/null
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/LibraryResolver.java
@@ -0,0 +1,7 @@
+package com.lantanagroup.link.measureeval.services;
+
+import org.hl7.fhir.r4.model.Library;
+
+public interface LibraryResolver {
+ Library resolve(String libraryId);
+}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluator.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluator.java
index d5aa7a3a5..60293027c 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluator.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluator.java
@@ -3,8 +3,10 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
+import com.lantanagroup.link.measureeval.repositories.LinkInMemoryFhirRepository;
import com.lantanagroup.link.measureeval.utils.ParametersUtils;
import com.lantanagroup.link.measureeval.utils.StreamUtils;
+import lombok.Getter;
import org.cqframework.cql.cql2elm.LibraryBuilder;
import org.hl7.fhir.r4.model.*;
import org.opencds.cqf.fhir.api.Repository;
@@ -14,7 +16,6 @@
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureService;
import org.opencds.cqf.fhir.utility.monad.Eithers;
-import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +29,7 @@ public class MeasureEvaluator {
private final FhirContext fhirContext;
private final MeasureEvaluationOptions options;
+ @Getter
private final Bundle bundle;
private final Measure measure;
@@ -74,10 +76,6 @@ private MeasureEvaluator(FhirContext fhirContext, Bundle bundle, boolean isDebug
}
}
- public static MeasureEvaluator compile(FhirContext fhirContext, Bundle bundle) {
- return compile(fhirContext, bundle, false);
- }
-
public static MeasureEvaluator compile(FhirContext fhirContext, Bundle bundle, boolean isDebug) {
MeasureEvaluator instance = new MeasureEvaluator(fhirContext, bundle, isDebug);
instance.compile();
@@ -94,12 +92,21 @@ private void compile() {
doEvaluate(null, null, new StringType(subject), additionalData);
}
+ public static MeasureReport compileAndEvaluate(FhirContext fhirContext, Bundle bundle, Parameters parameters, boolean isDebug) {
+ MeasureEvaluator evaluator = new MeasureEvaluator(fhirContext, bundle, isDebug);
+ DateTimeType periodStart = ParametersUtils.getValue(parameters, "periodStart", DateTimeType.class);
+ DateTimeType periodEnd = ParametersUtils.getValue(parameters, "periodEnd", DateTimeType.class);
+ StringType subject = ParametersUtils.getValue(parameters, "subject", StringType.class);
+ Bundle additionalData = ParametersUtils.getResource(parameters, "additionalData", Bundle.class);
+ return evaluator.doEvaluate(periodStart, periodEnd, subject, additionalData);
+ }
+
private MeasureReport doEvaluate(
DateTimeType periodStart,
DateTimeType periodEnd,
StringType subject,
Bundle additionalData) {
- Repository repository = new InMemoryFhirRepository(fhirContext, bundle);
+ Repository repository = new LinkInMemoryFhirRepository(fhirContext, bundle);
R4MeasureService measureService = new R4MeasureService(repository, options);
return measureService.evaluate(
Eithers.forRight3(measure),
@@ -117,16 +124,45 @@ private MeasureReport doEvaluate(
null);
}
+ public MeasureReport evaluate(Date periodStart, Date periodEnd, String patientId, Bundle additionalData) {
+ TimeZone utc = TimeZone.getTimeZone(ZoneOffset.UTC);
+ return evaluate(
+ new DateTimeType(periodStart, TemporalPrecisionEnum.MILLI, utc),
+ new DateTimeType(periodEnd, TemporalPrecisionEnum.MILLI, utc),
+ new StringType(new IdType(ResourceType.Patient.name(), patientId).getValue()),
+ additionalData);
+ }
+
+ public MeasureReport evaluate(Parameters parameters) {
+ DateTimeType periodStart = ParametersUtils.getValue(parameters, "periodStart", DateTimeType.class);
+ DateTimeType periodEnd = ParametersUtils.getValue(parameters, "periodEnd", DateTimeType.class);
+ StringType subject = ParametersUtils.getValue(parameters, "subject", StringType.class);
+ Bundle additionalData = ParametersUtils.getResource(parameters, "additionalData", Bundle.class);
+ return evaluate(periodStart, periodEnd, subject, additionalData);
+ }
+
public MeasureReport evaluate(
DateTimeType periodStart,
DateTimeType periodEnd,
StringType subject,
Bundle additionalData) {
List entries = additionalData.getEntry();
+
logger.debug(
"Evaluating measure: MEASURE=[{}] START=[{}] END=[{}] SUBJECT=[{}] RESOURCES=[{}]",
measure.getUrl(), periodStart.asStringValue(), periodEnd.asStringValue(), subject, entries.size());
+
+ // Output debug/trace information about the results of the evaluation
if (logger.isTraceEnabled()) {
+ // Output the group/population counts
+ for (MeasureReport.MeasureReportGroupComponent group : doEvaluate(periodStart, periodEnd, subject, additionalData).getGroup()) {
+ logger.trace("Group {}: {}", group.getId(), group.getPopulation().size());
+ for (MeasureReport.MeasureReportGroupPopulationComponent population : group.getPopulation()) {
+ logger.trace("Population {}: {}", population.getCode().getCodingFirstRep().getDisplay(), population.getCount());
+ }
+ }
+
+ // Output each resource in the bundle
for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
Resource resource = entries.get(entryIndex).getResource();
logger.trace("Resource {}: {}/{}", entryIndex, resource.getResourceType(), resource.getIdPart());
@@ -134,21 +170,4 @@ public MeasureReport evaluate(
}
return doEvaluate(periodStart, periodEnd, subject, additionalData);
}
-
- public MeasureReport evaluate(Date periodStart, Date periodEnd, String patientId, Bundle additionalData) {
- TimeZone utc = TimeZone.getTimeZone(ZoneOffset.UTC);
- return evaluate(
- new DateTimeType(periodStart, TemporalPrecisionEnum.MILLI, utc),
- new DateTimeType(periodEnd, TemporalPrecisionEnum.MILLI, utc),
- new StringType(new IdType(ResourceType.Patient.name(), patientId).getValue()),
- additionalData);
- }
-
- public MeasureReport evaluate(Parameters parameters) {
- DateTimeType periodStart = ParametersUtils.getValue(parameters, "periodStart", DateTimeType.class);
- DateTimeType periodEnd = ParametersUtils.getValue(parameters, "periodEnd", DateTimeType.class);
- StringType subject = ParametersUtils.getValue(parameters, "subject", StringType.class);
- Bundle additionalData = ParametersUtils.getResource(parameters, "additionalData", Bundle.class);
- return evaluate(periodStart, periodEnd, subject, additionalData);
- }
}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorCache.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorCache.java
index 84e833d48..a1f5d1fe6 100644
--- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorCache.java
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorCache.java
@@ -1,22 +1,27 @@
package com.lantanagroup.link.measureeval.services;
import ca.uhn.fhir.context.FhirContext;
+import com.lantanagroup.link.measureeval.configs.LinkConfig;
import com.lantanagroup.link.measureeval.entities.MeasureDefinition;
import com.lantanagroup.link.measureeval.repositories.MeasureDefinitionRepository;
+import com.lantanagroup.link.measureeval.utils.CqlUtils;
+import org.hl7.fhir.r4.model.Library;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
-public class MeasureEvaluatorCache {
+public class MeasureEvaluatorCache implements LibraryResolver {
private final FhirContext fhirContext;
private final MeasureDefinitionRepository definitionRepository;
private final Map instancesById = new ConcurrentHashMap<>();
+ private final LinkConfig linkConfig;
- public MeasureEvaluatorCache(FhirContext fhirContext, MeasureDefinitionRepository definitionRepository) {
+ public MeasureEvaluatorCache(FhirContext fhirContext, MeasureDefinitionRepository definitionRepository, LinkConfig linkConfig) {
this.fhirContext = fhirContext;
this.definitionRepository = definitionRepository;
+ this.linkConfig = linkConfig;
}
public MeasureEvaluator get(String id) {
@@ -25,11 +30,22 @@ public MeasureEvaluator get(String id) {
if (measureDefinition == null) {
return null;
}
- return MeasureEvaluator.compile(fhirContext, measureDefinition.getBundle());
+ return MeasureEvaluator.compile(fhirContext, measureDefinition.getBundle(), this.linkConfig.isCqlDebug());
});
}
public void remove(String id) {
instancesById.remove(id);
}
+
+ @Override
+ public Library resolve(String libraryId) {
+ for (MeasureEvaluator instance : instancesById.values()) {
+ Library library = CqlUtils.getLibrary(instance.getBundle(), libraryId);
+ if (library != null) {
+ return library;
+ }
+ }
+ return null;
+ }
}
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlLogAppender.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlLogAppender.java
new file mode 100644
index 000000000..eee2c1f2d
--- /dev/null
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlLogAppender.java
@@ -0,0 +1,117 @@
+package com.lantanagroup.link.measureeval.utils;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import com.lantanagroup.link.measureeval.services.LibraryResolver;
+import javassist.NotFoundException;
+import org.hl7.fhir.r4.model.Library;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CqlLogAppender extends AppenderBase {
+ private static final Logger logger = LoggerFactory.getLogger(CqlLogAppender.class);
+ private static final Pattern LOG_PATTERN = Pattern.compile("([\\w.]+)\\.(\\d+:\\d+-\\d+:\\d+)\\(\\d+\\):\\s*(\\{\\}|[^\\s]+)");
+
+ private final LibraryResolver libraryResolver;
+
+ public CqlLogAppender(LibraryResolver libraryResolver) {
+ this.libraryResolver = libraryResolver;
+ }
+
+ public static CqlLogAppender start(LoggerContext context, LibraryResolver libraryResolver) {
+ CqlLogAppender appender = new CqlLogAppender(libraryResolver);
+ appender.setContext(context);
+ appender.start();
+ ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("org.opencds.cqf.cql.engine.debug.DebugUtilities");
+ logger.setLevel(Level.DEBUG);
+ logger.setAdditive(false);
+ logger.addAppender(appender);
+ return appender;
+ }
+
+ @Override
+ protected void append(ILoggingEvent event) {
+ String message = event.getFormattedMessage();
+ Matcher matcher = LOG_PATTERN.matcher(message);
+
+ if (matcher.find()) {
+ String libraryId = matcher.group(1);
+ String range = matcher.group(2);
+ String output = matcher.group(3)
+ .replaceAll("org.hl7.fhir.r4.model.", "")
+ .replaceAll("@[0-9A-Fa-f]{6,8}", "");
+ String cql = null;
+
+ // Group the resources in the output
+ output = groupResources(output);
+
+ Library library = libraryResolver.resolve(libraryId);
+ if (library == null) {
+ logger.warn("Failed to resolve library: {}", libraryId);
+ } else {
+ try {
+ cql = CqlUtils.getCql(library, range);
+ } catch (NotFoundException e) {
+ logger.warn("Failed to get CQL for libraryId={}, range={}, output={}", libraryId, range, output);
+ }
+ }
+
+ // Custom processing with libraryId and range
+ processLogEntry(libraryId, range, output, cql);
+ }
+ }
+
+ private String groupResources(String output) {
+ if (output == null || output.isEmpty() || output.equals("{}") || !output.startsWith("{")) {
+ return output;
+ }
+
+ output = output.substring(1, output.length() - 1);
+
+ if (output.endsWith(",")) {
+ output = output.substring(0, output.length() - 1);
+ }
+
+ String[] resources = output.split(",");
+ Map resourceCount = new HashMap<>();
+
+ for (String resource : resources) {
+ resourceCount.put(resource, resourceCount.getOrDefault(resource, 0) + 1);
+ }
+
+ StringBuilder groupedOutput = new StringBuilder();
+ for (Map.Entry entry : resourceCount.entrySet()) {
+ if (groupedOutput.length() > 0) {
+ groupedOutput.append(",");
+ }
+ groupedOutput.append(entry.getKey());
+ if (entry.getValue() > 1) {
+ groupedOutput.append("(").append(entry.getValue()).append(")");
+ }
+ }
+
+ return groupedOutput.toString();
+ }
+
+ private void processLogEntry(String libraryId, String range, String output, String cql) {
+ if (cql != null) {
+ Pattern definePattern = Pattern.compile("^define \"([^\"]+)\"");
+ Matcher matcher = definePattern.matcher(cql);
+ if (matcher.find()) {
+ String definition = matcher.group(1);
+ logger.info("CQL DEBUG: libraryId={}, range={}, output={}, cql-definition={}", libraryId, range, output, definition);
+ } else {
+ logger.info("CQL DEBUG: libraryId={}, range={}, output={}, cql=\n{}", libraryId, range, output, cql);
+ }
+ } else {
+ logger.info("CQL DEBUG: libraryId={}, range={}, output={}", libraryId, range, output);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlUtils.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlUtils.java
new file mode 100644
index 000000000..2f8e49cc9
--- /dev/null
+++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlUtils.java
@@ -0,0 +1,84 @@
+package com.lantanagroup.link.measureeval.utils;
+
+import javassist.NotFoundException;
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.Library;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+public class CqlUtils {
+ public static Library getLibrary(Bundle bundle, String libraryId) {
+ return bundle.getEntry().stream()
+ .map(Bundle.BundleEntryComponent::getResource)
+ .filter(Library.class::isInstance)
+ .map(Library.class::cast)
+ .filter(l -> {
+ if (l.getUrl() == null) {
+ return false;
+ }
+
+ return l.getUrl().endsWith("/" + libraryId);
+ })
+ .findFirst()
+ .orElse(null);
+ }
+
+ public static String getCql(Bundle bundle, String libraryId, String range) throws NotFoundException {
+ Library library = getLibrary(bundle, libraryId);
+ if (library == null) {
+ throw new NotFoundException("Library not found in measure definition bundle");
+ }
+ return getCql(library, range);
+ }
+
+ public static String getCql(Library library, String range) throws NotFoundException {
+ // Get CQL from library's "content" and base64 decode it
+ String cql = library.getContent().stream()
+ .filter(content -> content.hasContentType() && content.getContentType().equals("text/cql"))
+ .findFirst()
+ .map(content -> new String(content.getData()))
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "CQL content not found in library"));
+
+ // Find range in CQL
+ if (range != null) {
+ // Split range into start and end line/column
+ return CqlUtils.getCqlRange(range, cql);
+ }
+
+ return cql;
+ }
+
+ private static String getCqlRange(String range, String cql) {
+ String[] rangeParts = range.split(":|-");
+
+ if (rangeParts.length != 4) {
+ return cql;
+ }
+
+ int startLine = Integer.parseInt(rangeParts[0]);
+ int startColumn = Integer.parseInt(rangeParts[1]);
+ int endLine = Integer.parseInt(rangeParts[2]);
+ int endColumn = Integer.parseInt(rangeParts[3]);
+
+ // Get the lines from the CQL
+ String[] lines = cql.split("\n");
+
+ // Get the lines in the range
+ StringBuilder rangeCql = new StringBuilder();
+ for (int i = startLine - 1; i < endLine; i++) {
+
+ if (i == startLine - 1) {
+ rangeCql.append(lines[i].substring(startColumn - 1));
+ } else if (i == endLine - 1) {
+ rangeCql.append(lines[i].substring(0, endColumn));
+ } else {
+ rangeCql.append(lines[i]);
+ }
+ if (i != endLine - 1) {
+ rangeCql.append("\n");
+ }
+ }
+
+ return rangeCql.toString();
+ }
+}
diff --git a/Java/measureeval/src/main/resources/application.yml b/Java/measureeval/src/main/resources/application.yml
index 2b67e2a20..fdd098a1f 100644
--- a/Java/measureeval/src/main/resources/application.yml
+++ b/Java/measureeval/src/main/resources/application.yml
@@ -51,6 +51,7 @@ spring:
link:
reportability-predicate: com.lantanagroup.link.measureeval.reportability.IsInInitialPopulation
+ cql-debug: false
secret-management:
key-vault-uri: ''
diff --git a/Java/measureeval/src/main/resources/logback-cli.xml b/Java/measureeval/src/main/resources/logback-cli.xml
index 0af8429e6..717f2d3c5 100644
--- a/Java/measureeval/src/main/resources/logback-cli.xml
+++ b/Java/measureeval/src/main/resources/logback-cli.xml
@@ -8,15 +8,9 @@
-
-
-
-
-
-
-
+
-
+
diff --git a/Java/measureeval/src/main/resources/logback-spring.xml b/Java/measureeval/src/main/resources/logback-spring.xml
index 6f52eedf7..75ef8a4ac 100644
--- a/Java/measureeval/src/main/resources/logback-spring.xml
+++ b/Java/measureeval/src/main/resources/logback-spring.xml
@@ -41,18 +41,18 @@
+
+
-
-
+
-
-
+
diff --git a/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorEvaluationTests.java b/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorEvaluationTests.java
index 3d1235805..275f0feaa 100644
--- a/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorEvaluationTests.java
+++ b/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorEvaluationTests.java
@@ -45,7 +45,7 @@ class MeasureEvaluatorEvaluationTests {
void simpleCohortMeasureTrueTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleCohortMeasureTrue.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientOnlyBundle());
@@ -66,7 +66,7 @@ void simpleCohortMeasureTrueTest() {
void simpleCohortMeasureFalseTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleCohortMeasureFalse.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientOnlyBundle());
@@ -88,7 +88,7 @@ void simpleCohortMeasureFalseTest() {
void cohortMeasureWithValueSetTrueTest() {
var measurePackage = KnowledgeArtifactBuilder.CohortMeasureWithValueSetTrue.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientAndEncounterBundle());
@@ -117,7 +117,7 @@ void cohortMeasureWithValueSetTrueTest() {
void cohortMeasureWithValueSetFalseTest() {
var measurePackage = KnowledgeArtifactBuilder.CohortMeasureWithValueSetFalse.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientAndEncounterBundle());
@@ -141,7 +141,7 @@ void cohortMeasureWithValueSetFalseTest() {
void cohortMeasureWithSDETest() {
var measurePackage = KnowledgeArtifactBuilder.CohortMeasureWithSDE.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientEncounterAndConditionBundle());
@@ -184,7 +184,7 @@ void cohortMeasureWithSDETest() {
void simpleProportionMeasureAllTrueNoExclusionTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleProportionMeasureAllTrueNoExclusion.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientOnlyBundle());
@@ -215,7 +215,7 @@ void simpleProportionMeasureAllTrueNoExclusionTest() {
void simpleProportionMeasureAllFalseTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleProportionMeasureAllFalse.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientOnlyBundle());
@@ -246,7 +246,7 @@ void simpleProportionMeasureAllFalseTest() {
void simpleRatioMeasureTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleRatioMeasure.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage);
+ var evaluator = MeasureEvaluator.compile(fhirContext, measurePackage, false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientAndEncounterBundle());
@@ -281,7 +281,7 @@ void simpleRatioMeasureTest() {
void simpleContinuousVariableMeasureTest() {
var measurePackage = KnowledgeArtifactBuilder.SimpleContinuousVariableMeasure.bundle();
validateMeasurePackage(measurePackage);
- var evaluator = MeasureEvaluator.compile(fhirContext, KnowledgeArtifactBuilder.SimpleContinuousVariableMeasure.bundle());
+ var evaluator = MeasureEvaluator.compile(fhirContext, KnowledgeArtifactBuilder.SimpleContinuousVariableMeasure.bundle(), false);
var report = evaluator.evaluate(new DateTimeType("2024-01-01"), new DateTimeType("2024-12-31"),
new StringType("Patient/simple-patient"), PatientDataBuilder.simplePatientAndEncounterBundle());
diff --git a/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorInstantiationTests.java b/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorInstantiationTests.java
index e2a29215c..067468eaa 100644
--- a/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorInstantiationTests.java
+++ b/Java/measureeval/src/test/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorInstantiationTests.java
@@ -29,7 +29,7 @@ void newInstanceWithR5FhirContextTest() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(new Measure());
Assertions.assertThrows(IllegalArgumentException.class,
- () -> MeasureEvaluator.compile(r5FhirContext, bundle),
+ () -> MeasureEvaluator.compile(r5FhirContext, bundle, false),
"Unsupported FHIR version!");
}
@@ -40,7 +40,7 @@ void newInstanceWithR5FhirContextTest() {
void newInstanceEmptyBundleTest() {
Bundle emptyBundle = new Bundle();
Assertions.assertThrows(IllegalArgumentException.class,
- () -> MeasureEvaluator.compile(fhirContext, emptyBundle),
+ () -> MeasureEvaluator.compile(fhirContext, emptyBundle, false),
"Please provide the necessary artifacts (e.g. Measure and Library resources) in the Bundle entry!");
}
@@ -52,7 +52,7 @@ void newInstanceEmptyBundleTest() {
void newInstanceMeasureWithoutPrimaryLibraryReference() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(createMeasure(false));
- Assertions.assertThrows(IllegalArgumentException.class, () -> MeasureEvaluator.compile(fhirContext, bundle),
+ Assertions.assertThrows(IllegalArgumentException.class, () -> MeasureEvaluator.compile(fhirContext, bundle, false),
"Measure null does not have a primary library specified");
}
@@ -64,7 +64,7 @@ void newInstanceMeasureWithoutPrimaryLibraryReference() {
void newInstanceMeasureWithMissingPrimaryLibraryReference() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(createMeasure(true));
- Assertions.assertThrows(ResourceNotFoundException.class, () -> MeasureEvaluator.compile(fhirContext, bundle),
+ Assertions.assertThrows(ResourceNotFoundException.class, () -> MeasureEvaluator.compile(fhirContext, bundle, false),
"Unable to find Library with url: https://example.com/Library/Nonexistent");
}
@@ -77,7 +77,7 @@ void newInstanceMeasureWithPrimaryLibraryReferenceWithoutCqlContent() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(createMeasure(true));
bundle.addEntry().setResource(createLibrary(false));
- Assertions.assertThrows(IllegalStateException.class, () -> MeasureEvaluator.compile(fhirContext, bundle),
+ Assertions.assertThrows(IllegalStateException.class, () -> MeasureEvaluator.compile(fhirContext, bundle, false),
"Unable to load CQL/ELM for library: Nonexistent. Verify that the Library resource is available in your environment and has CQL/ELM content embedded.");
}
@@ -89,7 +89,7 @@ void newInstanceMeasureWithPrimaryLibraryReferenceWithCqlContent() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(createMeasure(true));
bundle.addEntry().setResource(createLibrary(true));
- Assertions.assertDoesNotThrow(() -> MeasureEvaluator.compile(fhirContext, bundle));
+ Assertions.assertDoesNotThrow(() -> MeasureEvaluator.compile(fhirContext, bundle, false));
}
/**
diff --git a/docker-compose.yml b/docker-compose.yml
index dd22a123c..14facb73f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -190,7 +190,7 @@ services:
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=broker,controller
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093
- - KAFKA_CFG_LISTENERS=SASL_PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
+ - KAFKA_CFG_LISTENERS=SASL_PLAINTEXT://127.0.0.1:9092,CONTROLLER://127.0.0.1:9093,EXTERNAL://127.0.0.1:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:SASL_PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,EXTERNAL:SASL_PLAINTEXT
- KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://127.0.0.1:9092,EXTERNAL://kafka_b:9094
- KAFKA_CLIENT_USERS=${KAFKA_SASL_CLIENT_USER}
diff --git a/docs/README.md b/docs/README.md
index 95ad0dd62..a3f801bfb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -45,4 +45,22 @@ When deployed, each service provides a Swagger UI for exploring its API. The Swa
* JSON: `/swagger/v1/swagger.json`
* Java Services
* UI: `/swagger-ui.html`
- * JSON: `/v3/api-docs`
\ No newline at end of file
+ * JSON: `/v3/api-docs`
+
+## Java
+
+### Kafka Authentication
+
+If Kafka requires authentication (such as SASL_PLAINTEXT) the Java services use the following (example) properties:
+
+| Property Name | Value |
+|-------------------------------------------|-----------------------------------------------------------------------------------------------------|
+| spring.kafka.properties.sasl.jaas.config | org.apache.kafka.common.security.plain.PlainLoginModule required username=\"XXX\" password=\"XXX\"; |
+| spring.kafka.properties.sasl.mechanism | PLAIN |
+| spring.kafka.properties.security.protocol | SASL_PLAINTEXT |
+
+These properties can be applied when running/debugging the services locally by passing them as VM arguments, such as `-Dspring.kafka.properties.sasl.mechanism=PLAIN`.
+
+### Azure App Config
+
+Note: If a Java service is configured to use Azure App Config, keys in ACA take precedence over Java VM args _and_ Environment Variables.
\ No newline at end of file
diff --git a/docs/service_specs/measure_eval.md b/docs/service_specs/measure_eval.md
index 7ea0caaaa..6335004f2 100644
--- a/docs/service_specs/measure_eval.md
+++ b/docs/service_specs/measure_eval.md
@@ -55,9 +55,10 @@ The **Measure Evaluation Service** provides REST endpoints to manage measure def
- **PUT /api/measure-definition/{id}**: Creates or updates a measure definition with the specified ID.
- **GET /api/measure-definition**: Retrieves a list of all measure definitions.
-### Measure Evaluation
+### Measure Evaluation & Testing
-- **POST /api/measure-definition/{id}/$evaluate**: Evaluates a measure against clinical data provided in the request body.
+- **POST /api/measure-definition/{id}/$evaluate**: Evaluates a measure against clinical data provided in the request body. May include a `debug` flag that indicates to create cql debug logs on the service during evaluation.
+- **GET /api/measure-definition/{id}/{library-id}/$cql**: Retrieves the CQL for a specific measure definition and library. May include a `range` parameter that represents the range of CQL that is reported via debug logs.
### Health Check
diff --git a/link-cloud.sln b/link-cloud.sln
index e176c57aa..ee0c42eae 100644
--- a/link-cloud.sln
+++ b/link-cloud.sln
@@ -69,6 +69,47 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportTests", "DotNet\Repor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SubmissionTests", "DotNet\SubmissionTests\SubmissionTests.csproj", "{8EA05322-2C58-4781-A0C2-6F1FC58FD2FB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzurePipelines", "AzurePipelines", "{D88CD83D-09AC-4632-8066-4D7C36817749}"
+ ProjectSection(SolutionItems) = preProject
+ Azure_Pipelines\azure-pipelines.account.cd.yaml = Azure_Pipelines\azure-pipelines.account.cd.yaml
+ Azure_Pipelines\azure-pipelines.account.ci.yaml = Azure_Pipelines\azure-pipelines.account.ci.yaml
+ Azure_Pipelines\azure-pipelines.audit.cd.yaml = Azure_Pipelines\azure-pipelines.audit.cd.yaml
+ Azure_Pipelines\azure-pipelines.audit.ci.yaml = Azure_Pipelines\azure-pipelines.audit.ci.yaml
+ Azure_Pipelines\azure-pipelines.bff.cd.yaml = Azure_Pipelines\azure-pipelines.bff.cd.yaml
+ Azure_Pipelines\azure-pipelines.bff.ci.yaml = Azure_Pipelines\azure-pipelines.bff.ci.yaml
+ Azure_Pipelines\azure-pipelines.census.cd.yaml = Azure_Pipelines\azure-pipelines.census.cd.yaml
+ Azure_Pipelines\azure-pipelines.census.ci.yaml = Azure_Pipelines\azure-pipelines.census.ci.yaml
+ Azure_Pipelines\azure-pipelines.dataacquisition.cd.yaml = Azure_Pipelines\azure-pipelines.dataacquisition.cd.yaml
+ Azure_Pipelines\azure-pipelines.dataacquisition.ci.yaml = Azure_Pipelines\azure-pipelines.dataacquisition.ci.yaml
+ Azure_Pipelines\azure-pipelines.db-schema-creation.yaml = Azure_Pipelines\azure-pipelines.db-schema-creation.yaml
+ Azure_Pipelines\azure-pipelines.demo.cd.yaml = Azure_Pipelines\azure-pipelines.demo.cd.yaml
+ Azure_Pipelines\azure-pipelines.demo.ci.yaml = Azure_Pipelines\azure-pipelines.demo.ci.yaml
+ Azure_Pipelines\azure-pipelines.gateway.cd.yaml = Azure_Pipelines\azure-pipelines.gateway.cd.yaml
+ Azure_Pipelines\azure-pipelines.gateway.ci.yaml = Azure_Pipelines\azure-pipelines.gateway.ci.yaml
+ Azure_Pipelines\azure-pipelines.measureeval.cd.yaml = Azure_Pipelines\azure-pipelines.measureeval.cd.yaml
+ Azure_Pipelines\azure-pipelines.measureeval.ci.yaml = Azure_Pipelines\azure-pipelines.measureeval.ci.yaml
+ Azure_Pipelines\azure-pipelines.normalization.cd.yaml = Azure_Pipelines\azure-pipelines.normalization.cd.yaml
+ Azure_Pipelines\azure-pipelines.normalization.ci.yaml = Azure_Pipelines\azure-pipelines.normalization.ci.yaml
+ Azure_Pipelines\azure-pipelines.notification.cd.yaml = Azure_Pipelines\azure-pipelines.notification.cd.yaml
+ Azure_Pipelines\azure-pipelines.notification.ci.yaml = Azure_Pipelines\azure-pipelines.notification.ci.yaml
+ Azure_Pipelines\azure-pipelines.patientlist.cd.yaml = Azure_Pipelines\azure-pipelines.patientlist.cd.yaml
+ Azure_Pipelines\azure-pipelines.patientstoquery.cd.yaml = Azure_Pipelines\azure-pipelines.patientstoquery.cd.yaml
+ Azure_Pipelines\azure-pipelines.querydispatch.cd.yaml = Azure_Pipelines\azure-pipelines.querydispatch.cd.yaml
+ Azure_Pipelines\azure-pipelines.querydispatch.ci.yaml = Azure_Pipelines\azure-pipelines.querydispatch.ci.yaml
+ Azure_Pipelines\azure-pipelines.report.cd.yaml = Azure_Pipelines\azure-pipelines.report.cd.yaml
+ Azure_Pipelines\azure-pipelines.report.ci.yaml = Azure_Pipelines\azure-pipelines.report.ci.yaml
+ Azure_Pipelines\azure-pipelines.shared.cd.yml = Azure_Pipelines\azure-pipelines.shared.cd.yml
+ Azure_Pipelines\azure-pipelines.submission.cd.yaml = Azure_Pipelines\azure-pipelines.submission.cd.yaml
+ Azure_Pipelines\azure-pipelines.submission.ci.yaml = Azure_Pipelines\azure-pipelines.submission.ci.yaml
+ Azure_Pipelines\azure-pipelines.tenant.cd.yaml = Azure_Pipelines\azure-pipelines.tenant.cd.yaml
+ Azure_Pipelines\azure-pipelines.tenant.ci.yaml = Azure_Pipelines\azure-pipelines.tenant.ci.yaml
+ Azure_Pipelines\azure-pipelines.validation.cd.yaml = Azure_Pipelines\azure-pipelines.validation.cd.yaml
+ Azure_Pipelines\azure-pipelines.validation.ci.yaml = Azure_Pipelines\azure-pipelines.validation.ci.yaml
+ Azure_Pipelines\azure-pipelines.web.cd.yaml = Azure_Pipelines\azure-pipelines.web.cd.yaml
+ Azure_Pipelines\deploy_all_services.yaml = Azure_Pipelines\deploy_all_services.yaml
+ Azure_Pipelines\deploy_tags_all.yml = Azure_Pipelines\deploy_tags_all.yml
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU