From 0b00be0fa507f7349fa59b3367a7946bb998910a Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 12 Dec 2024 15:10:06 -0800 Subject: [PATCH 01/20] * Adding config property for "cql-debug" that can be set for the service as a whole which compiles the measures in the cache with "debug" logging enabled * Modified the $evaluate operation to accept a "debug" parameter and always compile the measure for every request so that the debug logging flag is always respected * Updating logback config so that it is configured to always use DEBUG logging for the DebugUtilities class. This class only outputs debug logs when the compiled measure is configured for debug logging. * Added stub for getting a specific line of CQL from the measure, so that the debug logging output from cqframework can be more easily traced to specific lines of CQL that are loaded in the measure eval system. * FileSystemInvocation always has cql debug logging enabled * Measures compiled into cache use whatever cql-debugging the service is configured for * Measure evaluation using the REST $evaluate operation always compiles the measure and uses whatever is passed in (or default of "false") in the operation request * Created a custom extension of InMemoryFhirRepository called LinkInMemoryFhirRepository that overrides the `transaction()` operation with a basic implementation of the operation so that logs aren't littered with NotImplementedExceptions() --- .../measureeval/FileSystemInvocation.java | 6 +- .../link/measureeval/configs/LinkConfig.java | 2 + .../MeasureDefinitionController.java | 33 ++++++++-- .../LinkInMemoryFhirRepository.java | 36 +++++++++++ .../services/MeasureEvaluator.java | 63 ++++++++++++------- .../services/MeasureEvaluatorCache.java | 7 ++- .../src/main/resources/application.yml | 1 + .../src/main/resources/logback-cli.xml | 11 +--- .../src/main/resources/logback-spring.xml | 5 +- 9 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/repositories/LinkInMemoryFhirRepository.java 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..c2cd4c346 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 @@ -138,7 +138,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), @@ -171,11 +171,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/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..b27c18404 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; @@ -98,21 +99,41 @@ 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"}) + public String getMeasureLibraryCQL( + @PathVariable("id") String measureId, + @PathVariable("library-id") String libraryId, + @RequestParam(value = "range", required = false) String range) { + + // TODO: Test the range format + + // TODO: Get library + + // TODO: Get CQL from library + + // TODO: Find range in CQL + + // TODO: Respond with CQL + + return null; + } + @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) { + 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); + // Compile the measure every time because the debug flag may have changed from what's in the cache + MeasureDefinition measureDefinition = repository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return MeasureEvaluator.compileAndEvaluate(FhirContext.forR4(), measureDefinition.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..dd3ae0b3f --- /dev/null +++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/repositories/LinkInMemoryFhirRepository.java @@ -0,0 +1,36 @@ +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 java.util.Map; + +public class LinkInMemoryFhirRepository extends InMemoryFhirRepository { + public LinkInMemoryFhirRepository(FhirContext context) { + super(context); + } + + public LinkInMemoryFhirRepository(FhirContext context, IBaseBundle bundle) { + super(context, bundle); + } + + @Override + public B transaction(B transaction, Map headers) { + Bundle bundle = (Bundle) transaction; + + for (Bundle.BundleEntryComponent entry : bundle.getEntry()) { + if (entry.hasResource()) { + // 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()); + } + } + + return transaction; + } +} 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 167da7b24..e5c79fe57 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 @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; 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 org.hl7.fhir.r4.model.*; @@ -12,7 +13,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; @@ -57,10 +57,6 @@ private MeasureEvaluator(FhirContext fhirContext, Bundle bundle, boolean isDebug .orElseThrow(); } - 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(); @@ -77,12 +73,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), @@ -100,16 +105,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()); @@ -117,21 +151,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..4c4ae015e 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,6 +1,7 @@ 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 org.springframework.stereotype.Service; @@ -13,10 +14,12 @@ public class MeasureEvaluatorCache { 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,7 +28,7 @@ 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()); }); } diff --git a/Java/measureeval/src/main/resources/application.yml b/Java/measureeval/src/main/resources/application.yml index 85a432d01..d77010ecb 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..7d4fcecc8 100644 --- a/Java/measureeval/src/main/resources/logback-cli.xml +++ b/Java/measureeval/src/main/resources/logback-cli.xml @@ -8,15 +8,10 @@ - - - - - - - + + - + diff --git a/Java/measureeval/src/main/resources/logback-spring.xml b/Java/measureeval/src/main/resources/logback-spring.xml index 6f52eedf7..e526007b2 100644 --- a/Java/measureeval/src/main/resources/logback-spring.xml +++ b/Java/measureeval/src/main/resources/logback-spring.xml @@ -41,9 +41,11 @@ + + + - @@ -51,7 +53,6 @@ - From d99bafb0b58fc627776471852e0e6f84af3954f2 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 12 Dec 2024 15:31:32 -0800 Subject: [PATCH 02/20] Improving documentation on measure eval env variables Adding note in readme about Java Kafka Auth and Azure App Config settings --- docs/README.md | 20 +++++++++++++++++++- docs/service_specs/measure_eval.md | 12 ++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 50768258a..8199cea98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,4 +41,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 96100dd14..d8da731aa 100644 --- a/docs/service_specs/measure_eval.md +++ b/docs/service_specs/measure_eval.md @@ -11,10 +11,14 @@ The Measure Eval service is a Java based application that is primarily responsib ## Environment Variables -| Name | Value | Secret? | -|---------------------------------------------|-------------------------------|---------| -| Link__Audit__ExternalConfigurationSource | AzureAppConfiguration | No | -| ConnectionStrings__AzureAppConfiguration | `` | Yes | +| Name | Secret? | Description | +|---------------------------------------------|---------|---------------------------------------------------------------| +| SPRING_CLOUD_AZURE_APPCONFIGURATION_ENABLED | No | Boolean value to enable or disable use of Azure App Config | +| AZURE_APP_CONFIG_ENDPOINT | No | If App Config enabled, the URI to the ACA instance. | +| AZURE_CLIENT_ID | No | The client id to use for authentication for ACA. | +| AZURE_CLIENT_SECRET | Yes | The secret/password to use for ACA authentication. | +| AZURE_TENANT_ID | No | The tenant id that the configured ACA instance is located in. | +| LOKI_URL | No | The URL to Loki where logs should persisted. | ## App Settings From 8859980d1316312c8d43653728ffec7be8cb0db6 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 10:55:24 -0800 Subject: [PATCH 03/20] Completing operation for GET $cql Adding log appender that parses the log message from CQFramework and makes it more readable --- .../MeasureDefinitionController.java | 80 ++++++++++++++++--- .../measureeval/utils/CqlLogAppender.java | 36 +++++++++ .../src/main/resources/logback-cli.xml | 5 +- .../src/main/resources/logback-spring.xml | 5 +- 4 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlLogAppender.java 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 b27c18404..b4bc07525 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 @@ -12,9 +12,7 @@ import io.opentelemetry.api.trace.Span; import io.swagger.v3.oas.annotations.Operation; import org.apache.commons.text.StringEscapeUtils; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.MeasureReport; -import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -25,6 +23,7 @@ import org.springframework.web.server.ResponseStatusException; import java.util.List; +import java.util.Optional; @RestController @RequestMapping("/api/measure-definition") @@ -99,6 +98,34 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV return entity; } + private static StringBuilder getRangeCql(String range, String cql) { + String[] rangeParts = range.split(":|-"); + 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; + } + @GetMapping("/{id}/{library-id}/$cql") @PreAuthorize("hasAuthority('IsLinkAdmin')") @Operation(summary = "Get the CQL for a measure definition's library", tags = {"Measure Definitions"}) @@ -107,17 +134,52 @@ public String getMeasureLibraryCQL( @PathVariable("library-id") String libraryId, @RequestParam(value = "range", required = false) String range) { - // TODO: Test the range format + // 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)); + + // Get library from the measure definition bundle based on the libraryId + Optional library = measureDefinition.getBundle().getEntry().stream() + .filter(entry -> { + if (!entry.hasResource() || entry.getResource().getResourceType() != ResourceType.Library) { + return false; + } - // TODO: Get library + Library l = (Library) entry.getResource(); - // TODO: Get CQL from library + if (l.getUrl() == null) { + return false; + } - // TODO: Find range in CQL + return l.getUrl().endsWith("/" + libraryId); + }) + .findFirst() + .map(entry -> (Library) entry.getResource()); - // TODO: Respond with CQL + if (library.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Library not found in measure definition bundle"); + } + + // Get CQL from library's "content" and base64 decode it + String cql = library.get().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 + StringBuilder rangeCql = getRangeCql(range, cql); + + return rangeCql.toString(); + } - return null; + return cql; } @PostMapping("/{id}/$evaluate") 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..48de3c9de --- /dev/null +++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlLogAppender.java @@ -0,0 +1,36 @@ +package com.lantanagroup.link.measureeval.utils; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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]+)"); + + @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-z]{8}", ""); + + // Custom processing with libraryId and range + processLogEntry(libraryId, range, output); + } + } + + private void processLogEntry(String libraryId, String range, String value) { + // Implement your custom processing logic here + logger.info("CQL DEBUG: libraryId={}, range={}, output={}", libraryId, range, value); + } +} \ No newline at end of file diff --git a/Java/measureeval/src/main/resources/logback-cli.xml b/Java/measureeval/src/main/resources/logback-cli.xml index 7d4fcecc8..5e82effaf 100644 --- a/Java/measureeval/src/main/resources/logback-cli.xml +++ b/Java/measureeval/src/main/resources/logback-cli.xml @@ -7,9 +7,12 @@ + - + + + diff --git a/Java/measureeval/src/main/resources/logback-spring.xml b/Java/measureeval/src/main/resources/logback-spring.xml index e526007b2..0f5431951 100644 --- a/Java/measureeval/src/main/resources/logback-spring.xml +++ b/Java/measureeval/src/main/resources/logback-spring.xml @@ -10,6 +10,7 @@ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + @@ -41,7 +42,9 @@ - + + + From 6b9264710ad85f2609036dbddac6e60b154026d0 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 11:20:53 -0800 Subject: [PATCH 04/20] Moving common logic for cql extract to CqlUtils Updating MeasureEvaluator to allow getting the measure definition bundle used by the evaluator Updating log appender to try and get the cql for the log statement out of the cached evaluator based on a matching the measure on library id (since that's all we have) --- .../MeasureDefinitionController.java | 74 ++--------------- .../services/MeasureEvaluator.java | 2 + .../utils/ApplicationContextProvider.java | 19 +++++ .../measureeval/utils/CqlLogAppender.java | 28 ++++++- .../link/measureeval/utils/CqlUtils.java | 80 +++++++++++++++++++ 5 files changed, 130 insertions(+), 73 deletions(-) create mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java create mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlUtils.java 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 b4bc07525..e1aebe116 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 @@ -8,11 +8,14 @@ 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 org.apache.commons.text.StringEscapeUtils; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -23,7 +26,6 @@ import org.springframework.web.server.ResponseStatusException; import java.util.List; -import java.util.Optional; @RestController @RequestMapping("/api/measure-definition") @@ -98,34 +100,6 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV return entity; } - private static StringBuilder getRangeCql(String range, String cql) { - String[] rangeParts = range.split(":|-"); - 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; - } - @GetMapping("/{id}/{library-id}/$cql") @PreAuthorize("hasAuthority('IsLinkAdmin')") @Operation(summary = "Get the CQL for a measure definition's library", tags = {"Measure Definitions"}) @@ -141,45 +115,7 @@ public String getMeasureLibraryCQL( // Get the measure definition from the repo by ID MeasureDefinition measureDefinition = repository.findById(measureId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - - // Get library from the measure definition bundle based on the libraryId - Optional library = measureDefinition.getBundle().getEntry().stream() - .filter(entry -> { - if (!entry.hasResource() || entry.getResource().getResourceType() != ResourceType.Library) { - return false; - } - - Library l = (Library) entry.getResource(); - - if (l.getUrl() == null) { - return false; - } - - return l.getUrl().endsWith("/" + libraryId); - }) - .findFirst() - .map(entry -> (Library) entry.getResource()); - - if (library.isEmpty()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Library not found in measure definition bundle"); - } - - // Get CQL from library's "content" and base64 decode it - String cql = library.get().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 - StringBuilder rangeCql = getRangeCql(range, cql); - - return rangeCql.toString(); - } - - return cql; + return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range); } @PostMapping("/{id}/$evaluate") 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 e5c79fe57..dc6a3403a 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 @@ -5,6 +5,7 @@ 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.hl7.fhir.r4.model.*; import org.opencds.cqf.fhir.api.Repository; import org.opencds.cqf.fhir.cql.EvaluationSettings; @@ -26,6 +27,7 @@ public class MeasureEvaluator { private final FhirContext fhirContext; private final MeasureEvaluationOptions options; + @Getter private final Bundle bundle; private final Measure measure; diff --git a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java new file mode 100644 index 000000000..8a9c17e72 --- /dev/null +++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java @@ -0,0 +1,19 @@ +package com.lantanagroup.link.measureeval.utils; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationContextProvider implements ApplicationContextAware { + private static ApplicationContext context; + + public static ApplicationContext getApplicationContext() { + return context; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + context = applicationContext; + } +} \ No newline at end of file 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 index 48de3c9de..3434f868d 100644 --- 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 @@ -2,6 +2,8 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; +import com.lantanagroup.link.measureeval.services.MeasureEvaluator; +import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,14 +25,32 @@ protected void append(ILoggingEvent event) { String output = matcher.group(3) .replaceAll("org.hl7.fhir.r4.model.", "") .replaceAll("@[0-9A-z]{8}", ""); + String cql = null; + + MeasureEvaluatorCache measureEvalCache = ApplicationContextProvider.getApplicationContext().getBean(MeasureEvaluatorCache.class); + MeasureEvaluator evaluator = measureEvalCache.get(libraryId); // Assume the measure id is the same as the library id since the log entry doesn't output the measure url/id + + if (evaluator != null) { + cql = CqlUtils.getCql(evaluator.getBundle(), libraryId, range); + } // Custom processing with libraryId and range - processLogEntry(libraryId, range, output); + processLogEntry(libraryId, range, output, cql); } } - private void processLogEntry(String libraryId, String range, String value) { - // Implement your custom processing logic here - logger.info("CQL DEBUG: libraryId={}, range={}, output={}", libraryId, range, value); + private void processLogEntry(String libraryId, String range, String output, String cql) { + if (cql != null) { + Pattern definePattern = Pattern.compile("^define \"([^\"]+)\""); + Matcher matcher = cql != null ? definePattern.matcher(cql) : null; + if (matcher != null && 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..5612e58ee --- /dev/null +++ b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/CqlUtils.java @@ -0,0 +1,80 @@ +package com.lantanagroup.link.measureeval.utils; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.ResourceType; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +public class CqlUtils { + public static String getCql(Bundle bundle, String libraryId, String range) { + // Get library from the measure definition bundle based on the libraryId + Optional library = bundle.getEntry().stream() + .filter(entry -> { + if (!entry.hasResource() || entry.getResource().getResourceType() != ResourceType.Library) { + return false; + } + + Library l = (Library) entry.getResource(); + + if (l.getUrl() == null) { + return false; + } + + return l.getUrl().endsWith("/" + libraryId); + }) + .findFirst() + .map(entry -> (Library) entry.getResource()); + + if (library.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Library not found in measure definition bundle"); + } + + // Get CQL from library's "content" and base64 decode it + String cql = library.get().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 + StringBuilder rangeCql = CqlUtils.getCqlRange(range, cql); + + return rangeCql.toString(); + } + + return cql; + } + + private static StringBuilder getCqlRange(String range, String cql) { + String[] rangeParts = range.split(":|-"); + 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; + } +} From 35e7b8ae8fa6100b865f4f3c172fd2d908366cdc Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 11:28:35 -0800 Subject: [PATCH 05/20] Additional REST documentation about the $evaluate and $cql operations --- .../controllers/MeasureDefinitionController.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 e1aebe116..d14d4f953 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 @@ -12,6 +12,7 @@ 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 org.apache.commons.text.StringEscapeUtils; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.MeasureReport; @@ -103,6 +104,9 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV @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, @@ -121,6 +125,9 @@ public String getMeasureLibraryCQL( @PostMapping("/{id}/$evaluate") @PreAuthorize("hasAuthority('IsLinkAdmin')") @Operation(summary = "Evaluate a measure against data in request body", tags = {"Measure Definitions"}) + @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){ From 0401207d8e6e18d2eb4c8efbe6bbc8f5e611a71d Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 11:44:03 -0800 Subject: [PATCH 06/20] Cleaning up service spec documents about measure eval to be more accurate, and adding more detail about the REST endpoints (and the new one) --- docs/service_specs/measure_eval.md | 59 +++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/service_specs/measure_eval.md b/docs/service_specs/measure_eval.md index d8da731aa..dc1edb647 100644 --- a/docs/service_specs/measure_eval.md +++ b/docs/service_specs/measure_eval.md @@ -24,17 +24,55 @@ The Measure Eval service is a Java based application that is primarily responsib ### Kafka Connection -| Name | Value | Secret? | -|------------------------------------------|---------------------------|---------| -| KafkaConnection__BootstrapServers__0 | `` | No | -| KafkaConnection__GroupId | measure-events | No | +| Name | Value | Secret? | +|-------------------------------------|-------------|---------| +| spring.kafka.bootstrap_servers | | No | +| spring.kafka.retry.max-attempts | 3 | No | +| spring.kafka.retry.retry-backoff-ms | 3000 | No | +| spring.kafka.consumer.group-id | measureeval | No | +| spring.kafka.producer.group-id | measureeval | No | ### Measure Evaluation Config -| Name | Value | Secret? | -|--------------------------------------------|-------------------------------------------------|---------| -| MeasureEvalConfig__TerminologyServiceUrl | `https://cqf-ruler.nhsnlink.org/fhir` | No | -| MeasureEvalConfig__EvaluationServiceUrl | `https://cqf-ruler.nhsnlink.org/fhir` | No | +| Name | Value | Secret? | +|------------------------------|-----------------------------------------------------------------------|---------| +| link.reportability-predicate | com.lantanagroup.link.measureeval.reportability.IsInInitialPopulation | No | +| link.cql_debug | false | No | + +### Database (Mongo) + +| Name | Value | Secret? | +|----------------------------------|-------------|---------| +| spring.data.mongodb.host | | No | +| spring.data.mongodb.port | | No | +| spring.data.mongodb.database | measureeval | No | +| TODO: Add authentication details | | No | + + +### Authentication & Secrets + +| Name | Value | Secret? | +|---------------------------------|------------------------|---------| +| authentication.anonymous | true | No | +| authentication.authority | https://localhost:7004 | No | +| secret-management.key-vault-uri | | No | + +### Logging & Telemetry + +| Name | Value | Secret? | +|-----------------------------|------------------------|---------| +| loki.enabled | true | No | +| loki.url | | No | +| loki.app | link-dev | No | +| telemetry.exporter-endpoint | http://localhost:55690 | No | + +### Swagger + +| Name | Value | Secret? | +|------------------------------|-------|---------| +| springdoc.api-docs.enabled | false | No | +| springdoc.swagger-ui.enabled | false | No | + ## Kafka Events/Topics @@ -57,9 +95,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 From fc9b8687233a3ec8d47d011565643d29cc3dad1a Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 11:45:19 -0800 Subject: [PATCH 07/20] Fix code scanning alert no. 140: Overly permissive regular expression range Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../com/lantanagroup/link/measureeval/utils/CqlLogAppender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3434f868d..6b91eb756 100644 --- 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 @@ -24,7 +24,7 @@ protected void append(ILoggingEvent event) { String range = matcher.group(2); String output = matcher.group(3) .replaceAll("org.hl7.fhir.r4.model.", "") - .replaceAll("@[0-9A-z]{8}", ""); + .replaceAll("@[0-9A-Fa-f]{8}", ""); String cql = null; MeasureEvaluatorCache measureEvalCache = ApplicationContextProvider.getApplicationContext().getBean(MeasureEvaluatorCache.class); From 8472f71a64b75ca4fb20d1d8f88a1e051d517587 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 12:13:51 -0800 Subject: [PATCH 08/20] Grouping the output resources from logging by resource to keep the logs shorter. --- .../measureeval/utils/CqlLogAppender.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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 index 6b91eb756..33e1decb6 100644 --- 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 @@ -7,6 +7,8 @@ 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; @@ -24,9 +26,12 @@ protected void append(ILoggingEvent event) { String range = matcher.group(2); String output = matcher.group(3) .replaceAll("org.hl7.fhir.r4.model.", "") - .replaceAll("@[0-9A-Fa-f]{8}", ""); + .replaceAll("@[0-9A-Fa-f]{6,8}", ""); String cql = null; + // Group the resources in the output + output = groupResources(output); + MeasureEvaluatorCache measureEvalCache = ApplicationContextProvider.getApplicationContext().getBean(MeasureEvaluatorCache.class); MeasureEvaluator evaluator = measureEvalCache.get(libraryId); // Assume the measure id is the same as the library id since the log entry doesn't output the measure url/id @@ -39,6 +44,38 @@ protected void append(ILoggingEvent event) { } } + 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 \"([^\"]+)\""); From 2d340223b0e3da9f6e1b11ef725a12be7639e620 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 12:16:26 -0800 Subject: [PATCH 09/20] Error handling results of regex for getting CQL range --- .../link/measureeval/utils/CqlUtils.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 index 5612e58ee..6cba6ddff 100644 --- 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 @@ -42,16 +42,19 @@ public static String getCql(Bundle bundle, String libraryId, String range) { // Find range in CQL if (range != null) { // Split range into start and end line/column - StringBuilder rangeCql = CqlUtils.getCqlRange(range, cql); - - return rangeCql.toString(); + return CqlUtils.getCqlRange(range, cql); } return cql; } - private static StringBuilder getCqlRange(String range, String 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]); @@ -75,6 +78,7 @@ private static StringBuilder getCqlRange(String range, String cql) { rangeCql.append("\n"); } } - return rangeCql; + + return rangeCql.toString(); } } From 4c1e5e72ef6efd133d9c2c8a9c0bcd139492b61a Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 12:20:57 -0800 Subject: [PATCH 10/20] Error handling for LinkInMemoryFhirRepository class --- .../LinkInMemoryFhirRepository.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) 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 index dd3ae0b3f..d1b745d8b 100644 --- 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 @@ -4,10 +4,14 @@ 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; public class LinkInMemoryFhirRepository extends InMemoryFhirRepository { + private static final Logger logger = LoggerFactory.getLogger(LinkInMemoryFhirRepository.class); + public LinkInMemoryFhirRepository(FhirContext context) { super(context); } @@ -18,16 +22,28 @@ public LinkInMemoryFhirRepository(FhirContext context, IBaseBundle 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.hasResource()) { - // Ensure each resource has an ID, or create a GUID for them - if (!entry.getResource().hasId()) { - entry.getResource().setId(java.util.UUID.randomUUID().toString()); + 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); } - - this.update(entry.getResource()); } } From 04090084ae1a1d1d233fa33fe1ed796d5ade39f7 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 15:39:27 -0800 Subject: [PATCH 11/20] Adding AzurePipelines to a solution folder. --- link-cloud.sln | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) 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 From 9ef99ac8b2dabacc364518927dc86936f2278ca1 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 16 Dec 2024 15:39:55 -0800 Subject: [PATCH 12/20] Updating measure eval CD to build and publish the CLI JAR --- .../azure-pipelines.measureeval.cd.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 From 1ea48eb42219b6c8594d14f67de8fbf6f6062e7c Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:25:27 -0800 Subject: [PATCH 13/20] Adding class comment on `LinkInMemoryFhirRepository` --- .../measureeval/repositories/LinkInMemoryFhirRepository.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index d1b745d8b..a17ca3892 100644 --- 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 @@ -9,6 +9,11 @@ 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); From e7ec14e5b28cf48e256b74d89559a4b14984db3e Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:28:51 -0800 Subject: [PATCH 14/20] Correctly arguments in logger.trace() call --- .../link/measureeval/services/MeasureEvaluator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dc6a3403a..8a0955504 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 @@ -141,7 +141,7 @@ public MeasureReport evaluate( 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()); + logger.trace("Population {}: {}", population.getCode().getCodingFirstRep().getDisplay(), population.getCount()); } } From 11fa779292c88e7d0cae87b3dc856c2a927fafa9 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:41:25 -0800 Subject: [PATCH 15/20] Simplifying `if` logic in `CqlLogAppender` --- .../lantanagroup/link/measureeval/utils/CqlLogAppender.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 33e1decb6..2c10c7bd5 100644 --- 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 @@ -79,8 +79,8 @@ private String groupResources(String output) { private void processLogEntry(String libraryId, String range, String output, String cql) { if (cql != null) { Pattern definePattern = Pattern.compile("^define \"([^\"]+)\""); - Matcher matcher = cql != null ? definePattern.matcher(cql) : null; - if (matcher != null && matcher.find()) { + 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 { From e6f993d1051a40106934e33a380435a9a8a9689b Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:46:40 -0800 Subject: [PATCH 16/20] Fixing build error from merge due to adding parameter to `compile()` method. Using `NotFoundException` instead of `ResponseStatusException` from `CqlUtils` --- .../controllers/MeasureDefinitionController.java | 8 +++++++- .../link/measureeval/utils/CqlLogAppender.java | 7 ++++++- .../com/lantanagroup/link/measureeval/utils/CqlUtils.java | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) 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 d14d4f953..48208d653 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 @@ -13,6 +13,7 @@ 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; @@ -119,7 +120,12 @@ public String getMeasureLibraryCQL( // Get the measure definition from the repo by ID MeasureDefinition measureDefinition = repository.findById(measureId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range); + + try { + return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range); + } catch (NotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage(), e); + } } @PostMapping("/{id}/$evaluate") 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 index 2c10c7bd5..87974bc22 100644 --- 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 @@ -4,6 +4,7 @@ import ch.qos.logback.core.AppenderBase; import com.lantanagroup.link.measureeval.services.MeasureEvaluator; import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache; +import javassist.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +37,11 @@ protected void append(ILoggingEvent event) { MeasureEvaluator evaluator = measureEvalCache.get(libraryId); // Assume the measure id is the same as the library id since the log entry doesn't output the measure url/id if (evaluator != null) { - cql = CqlUtils.getCql(evaluator.getBundle(), libraryId, range); + try { + cql = CqlUtils.getCql(evaluator.getBundle(), libraryId, range); + } catch (NotFoundException e) { + logger.warn("Failed to get CQL for libraryId={}, range={}, output={}", libraryId, range, output); + } } // Custom processing with libraryId and range 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 index 6cba6ddff..a66b80257 100644 --- 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 @@ -1,5 +1,6 @@ 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.hl7.fhir.r4.model.ResourceType; @@ -9,7 +10,7 @@ import java.util.Optional; public class CqlUtils { - public static String getCql(Bundle bundle, String libraryId, String range) { + public static String getCql(Bundle bundle, String libraryId, String range) throws NotFoundException { // Get library from the measure definition bundle based on the libraryId Optional library = bundle.getEntry().stream() .filter(entry -> { @@ -29,7 +30,7 @@ public static String getCql(Bundle bundle, String libraryId, String range) { .map(entry -> (Library) entry.getResource()); if (library.isEmpty()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Library not found in measure definition bundle"); + throw new NotFoundException("Library not found in measure definition bundle"); } // Get CQL from library's "content" and base64 decode it From 12a209a3a02958e19d70234e5638eb102ba9125c Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:57:08 -0800 Subject: [PATCH 17/20] Fixing build error from merge due to adding parameter to `compile()` method. --- .../MeasureEvaluatorEvaluationTests.java | 18 +++++++++--------- .../MeasureEvaluatorInstantiationTests.java | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) 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)); } /** From 950e5feced1dcb0a85d0ddf9b8b1ce8e0970ae11 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 23 Dec 2024 09:57:53 -0800 Subject: [PATCH 18/20] Changing default log level to INFO and making link's classes log level WARN --- Java/measureeval/src/main/resources/logback-cli.xml | 2 +- Java/measureeval/src/main/resources/logback-spring.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Java/measureeval/src/main/resources/logback-cli.xml b/Java/measureeval/src/main/resources/logback-cli.xml index 5e82effaf..d9c68590e 100644 --- a/Java/measureeval/src/main/resources/logback-cli.xml +++ b/Java/measureeval/src/main/resources/logback-cli.xml @@ -9,7 +9,7 @@ - + diff --git a/Java/measureeval/src/main/resources/logback-spring.xml b/Java/measureeval/src/main/resources/logback-spring.xml index 0f5431951..131770448 100644 --- a/Java/measureeval/src/main/resources/logback-spring.xml +++ b/Java/measureeval/src/main/resources/logback-spring.xml @@ -45,18 +45,18 @@ - + - + - + From bb22634e1a71feb371c35ee56d1fc1299db39b8b Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 26 Dec 2024 09:47:47 -0800 Subject: [PATCH 19/20] Fresh install of Docker Desktop and pulled latest images of Kafka... Now get errors when starting Kafka unless the 127.0.0.1 IP address is specified in the listeners config. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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} From c2b57a999e124dfd6f965d76ec6554024dd00466 Mon Sep 17 00:00:00 2001 From: Steven Williams Date: Sat, 28 Dec 2024 21:47:39 -0500 Subject: [PATCH 20/20] Rework CQL debug logging - Point CLI to logback-cli.xml. - Introduce explicit dependency for library resolution. --- Java/measureeval/pom.xml | 1 - .../measureeval/FileSystemInvocation.java | 15 +++++++++ .../measureeval/MeasureEvalApplication.java | 11 +++++++ .../MeasureDefinitionController.java | 7 ++-- .../measureeval/services/LibraryResolver.java | 7 ++++ .../services/MeasureEvaluatorCache.java | 15 ++++++++- .../utils/ApplicationContextProvider.java | 19 ----------- .../measureeval/utils/CqlLogAppender.java | 33 +++++++++++++++---- .../link/measureeval/utils/CqlUtils.java | 31 +++++++++-------- .../src/main/resources/logback-cli.xml | 6 +--- .../src/main/resources/logback-spring.xml | 6 +--- 11 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/LibraryResolver.java delete mode 100644 Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java 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 c2cd4c346..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); @@ -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); 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/controllers/MeasureDefinitionController.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/controllers/MeasureDefinitionController.java index 48208d653..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 @@ -142,9 +142,10 @@ public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @Path } try { - // Compile the measure every time because the debug flag may have changed from what's in the cache - MeasureDefinition measureDefinition = repository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - return MeasureEvaluator.compileAndEvaluate(FhirContext.forR4(), measureDefinition.getBundle(), parameters, debug); + // 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/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/MeasureEvaluatorCache.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/services/MeasureEvaluatorCache.java index 4c4ae015e..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 @@ -4,13 +4,15 @@ 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<>(); @@ -35,4 +37,15 @@ public MeasureEvaluator get(String id) { 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/ApplicationContextProvider.java b/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java deleted file mode 100644 index 8a9c17e72..000000000 --- a/Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/utils/ApplicationContextProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.lantanagroup.link.measureeval.utils; - -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.stereotype.Component; - -@Component -public class ApplicationContextProvider implements ApplicationContextAware { - private static ApplicationContext context; - - public static ApplicationContext getApplicationContext() { - return context; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - context = applicationContext; - } -} \ No newline at end of file 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 index 87974bc22..eee2c1f2d 100644 --- 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 @@ -1,10 +1,12 @@ 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.MeasureEvaluator; -import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache; +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; @@ -17,6 +19,23 @@ 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(); @@ -33,12 +52,12 @@ protected void append(ILoggingEvent event) { // Group the resources in the output output = groupResources(output); - MeasureEvaluatorCache measureEvalCache = ApplicationContextProvider.getApplicationContext().getBean(MeasureEvaluatorCache.class); - MeasureEvaluator evaluator = measureEvalCache.get(libraryId); // Assume the measure id is the same as the library id since the log entry doesn't output the measure url/id - - if (evaluator != null) { + Library library = libraryResolver.resolve(libraryId); + if (library == null) { + logger.warn("Failed to resolve library: {}", libraryId); + } else { try { - cql = CqlUtils.getCql(evaluator.getBundle(), libraryId, range); + cql = CqlUtils.getCql(library, range); } catch (NotFoundException e) { logger.warn("Failed to get CQL for libraryId={}, range={}, output={}", libraryId, range, output); } 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 index a66b80257..2f8e49cc9 100644 --- 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 @@ -3,23 +3,16 @@ import javassist.NotFoundException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.ResourceType; import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; -import java.util.Optional; - public class CqlUtils { - public static String getCql(Bundle bundle, String libraryId, String range) throws NotFoundException { - // Get library from the measure definition bundle based on the libraryId - Optional library = bundle.getEntry().stream() - .filter(entry -> { - if (!entry.hasResource() || entry.getResource().getResourceType() != ResourceType.Library) { - return false; - } - - Library l = (Library) entry.getResource(); - + 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; } @@ -27,14 +20,20 @@ public static String getCql(Bundle bundle, String libraryId, String range) throw return l.getUrl().endsWith("/" + libraryId); }) .findFirst() - .map(entry -> (Library) entry.getResource()); + .orElse(null); + } - if (library.isEmpty()) { + 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.get().getContent().stream() + String cql = library.getContent().stream() .filter(content -> content.hasContentType() && content.getContentType().equals("text/cql")) .findFirst() .map(content -> new String(content.getData())) diff --git a/Java/measureeval/src/main/resources/logback-cli.xml b/Java/measureeval/src/main/resources/logback-cli.xml index d9c68590e..717f2d3c5 100644 --- a/Java/measureeval/src/main/resources/logback-cli.xml +++ b/Java/measureeval/src/main/resources/logback-cli.xml @@ -7,12 +7,8 @@ - - - - - + diff --git a/Java/measureeval/src/main/resources/logback-spring.xml b/Java/measureeval/src/main/resources/logback-spring.xml index 131770448..75ef8a4ac 100644 --- a/Java/measureeval/src/main/resources/logback-spring.xml +++ b/Java/measureeval/src/main/resources/logback-spring.xml @@ -10,7 +10,6 @@ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - @@ -42,10 +41,7 @@ - - - - +