Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TECH_DEBT: Measure debuggability #572

Merged
merged 27 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0b00be0
* Adding config property for "cql-debug" that can be set for the serv…
seanmcilvenna Dec 12, 2024
d99bafb
Improving documentation on measure eval env variables
seanmcilvenna Dec 12, 2024
d9b109c
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 12, 2024
8859980
Completing operation for GET $cql
seanmcilvenna Dec 16, 2024
6b92647
Moving common logic for cql extract to CqlUtils
seanmcilvenna Dec 16, 2024
bc52c9f
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 16, 2024
35e7b8a
Additional REST documentation about the $evaluate and $cql operations
seanmcilvenna Dec 16, 2024
0401207
Cleaning up service spec documents about measure eval to be more accu…
seanmcilvenna Dec 16, 2024
fc9b868
Fix code scanning alert no. 140: Overly permissive regular expression…
seanmcilvenna Dec 16, 2024
8472f71
Grouping the output resources from logging by resource to keep the lo…
seanmcilvenna Dec 16, 2024
2d34022
Error handling results of regex for getting CQL range
seanmcilvenna Dec 16, 2024
4c1e5e7
Error handling for LinkInMemoryFhirRepository class
seanmcilvenna Dec 16, 2024
0409008
Adding AzurePipelines to a solution folder.
seanmcilvenna Dec 16, 2024
9ef99ac
Updating measure eval CD to build and publish the CLI JAR
seanmcilvenna Dec 16, 2024
c03e69c
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 18, 2024
1ea48eb
Adding class comment on `LinkInMemoryFhirRepository`
seanmcilvenna Dec 23, 2024
e7ec14e
Correctly arguments in logger.trace() call
seanmcilvenna Dec 23, 2024
9c695a9
Merge remote-tracking branch 'origin/measure_debuggability' into meas…
seanmcilvenna Dec 23, 2024
11fa779
Simplifying `if` logic in `CqlLogAppender`
seanmcilvenna Dec 23, 2024
e6f993d
Fixing build error from merge due to adding parameter to `compile()` …
seanmcilvenna Dec 23, 2024
12a209a
Fixing build error from merge due to adding parameter to `compile()` …
seanmcilvenna Dec 23, 2024
950e5fe
Changing default log level to INFO and making link's classes log leve…
seanmcilvenna Dec 23, 2024
bb22634
Fresh install of Docker Desktop and pulled latest images of Kafka... …
seanmcilvenna Dec 26, 2024
c2b57a9
Rework CQL debug logging
smailliwcs Dec 29, 2024
45eb1d4
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 30, 2024
c0a5941
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 30, 2024
74b1a3a
Merge branch 'dev' into measure_debuggability
seanmcilvenna Dec 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Azure_Pipelines/azure-pipelines.measureeval.cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'
1 change: 0 additions & 1 deletion Java/measureeval/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@
<id>cli</id>
<properties>
<mainClass>com.lantanagroup.link.measureeval.FileSystemInvocation</mainClass>
<logback.configurationFile>src/main/resources/logback-cli.xml</logback.configurationFile>
</properties>
<build>
<finalName>measureeval-cli</finalName>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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));
}
seanmcilvenna marked this conversation as resolved.
Show resolved Hide resolved

private static Bundle getBundle(String measureBundlePath) throws IOException {
logger.info("Loading measure bundle from: {}", measureBundlePath);

Expand Down Expand Up @@ -138,7 +152,7 @@ private static Patient findPatient(Bundle bundle) {
.orElseThrow(() -> new IllegalArgumentException("Patient resource not found in bundle"));
}

private static void evaluatePatientBundle(String patientBundlePath, Bundle patientBundle, String start, String end, MeasureEvaluator evaluator) {
private static void evaluatePatientBundle(Bundle patientBundle, String start, String end, MeasureEvaluator evaluator, boolean isDebug) {
Patient patient = findPatient(patientBundle);
var report = evaluator.evaluate(
new DateTimeType(start),
Expand All @@ -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);
Expand All @@ -171,11 +186,11 @@ public static void main(String[] args) {

for (Bundle patientBundle : patientBundles) {
logger.info("\n===================================================");
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} else {
Bundle patientBundle = getBundle(patientBundlePath);
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} catch (Exception e) {
System.err.println("Error occurred while evaluating measure: " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
public class LinkConfig {
private String reportabilityPredicate;

private boolean cqlDebug = false;
smailliwcs marked this conversation as resolved.
Show resolved Hide resolved

@Bean
@SuppressWarnings("unchecked")
public Predicate<MeasureReport> reportabilityPredicate() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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;
import com.lantanagroup.link.measureeval.serdes.Views;
import com.lantanagroup.link.measureeval.services.MeasureDefinitionBundleValidator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.shared.auth.PrincipalUser;
import io.opentelemetry.api.trace.Span;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import javassist.NotFoundException;
import org.apache.commons.text.StringEscapeUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.MeasureReport;
Expand Down Expand Up @@ -98,21 +102,50 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV
return entity;
}

@GetMapping("/{id}/{library-id}/$cql")
@PreAuthorize("hasAuthority('IsLinkAdmin')")
@Operation(summary = "Get the CQL for a measure definition's library", tags = {"Measure Definitions"})
@Parameter(name = "id", description = "The ID of the measure definition", required = true)
@Parameter(name = "library-id", description = "The ID of the library in the measure definition", required = true)
@Parameter(name = "range", description = "The range of the CQL to return (e.g. 37:1-38:22)", required = false)
public String getMeasureLibraryCQL(
@PathVariable("id") String measureId,
@PathVariable("library-id") String libraryId,
@RequestParam(value = "range", required = false) String range) {

// Test that the range format is correct (i.e. "37:1-38:22")
if (range != null && !range.matches("\\d+:\\d+-\\d+:\\d+")) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid range format");
}

// Get the measure definition from the repo by ID
MeasureDefinition measureDefinition = repository.findById(measureId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

try {
return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range);
} catch (NotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage(), e);
}
}

@PostMapping("/{id}/$evaluate")
@PreAuthorize("hasAuthority('IsLinkAdmin')")
@Operation(summary = "Evaluate a measure against data in request body", tags = {"Measure Definitions"})
public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters) {
@Parameter(name = "id", description = "The ID of the measure definition", required = true)
@Parameter(name = "parameters", description = "The parameters to use in the evaluation", required = true)
@Parameter(name = "debug", description = "Whether to log CQL debugging information during evaluation", required = false)
public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters, @RequestParam(required = false, defaultValue = "false") boolean debug) {

if (user != null){
Span currentSpan = Span.current();
currentSpan.setAttribute("user", user.getEmailAddress());
}
MeasureEvaluator evaluator = evaluatorCache.get(id);
if (evaluator == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}

try {
return evaluator.evaluate(parameters);
// Ensure that a measure evaluator is cached (so that CQL logging can use it)
MeasureEvaluator evaluator = evaluatorCache.get(id);
// But recompile the bundle every time because the debug flag may not match what's in the cache
return MeasureEvaluator.compileAndEvaluate(FhirContext.forR4(), evaluator.getBundle(), parameters, debug);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
}
Expand Down
smailliwcs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.lantanagroup.link.measureeval.repositories;

import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Bundle;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
* This class extends the InMemoryFhirRepository to provide a transaction method that will update the resources in the repository.
* This implementation is primarily used to avoid the exception stack trace that is thrown when the InMemoryFhirRepository.transaction method is called by measure eval, but is not implemented
* in the default InMemoryFhirRepository.
*/
public class LinkInMemoryFhirRepository extends InMemoryFhirRepository {
private static final Logger logger = LoggerFactory.getLogger(LinkInMemoryFhirRepository.class);

public LinkInMemoryFhirRepository(FhirContext context) {
super(context);
}

public LinkInMemoryFhirRepository(FhirContext context, IBaseBundle bundle) {
super(context, bundle);
}

@Override
public <B extends IBaseBundle> B transaction(B transaction, Map<String, String> headers) {
if (!(transaction instanceof Bundle)) {
return transaction;
}

Bundle bundle = (Bundle) transaction;

if (bundle.getEntry() == null || bundle.getEntry().size() == 0) {
return transaction;
}

for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
if (entry != null && entry.hasResource()) {
try {
// Ensure each resource has an ID, or create a GUID for them
if (!entry.getResource().hasId()) {
entry.getResource().setId(java.util.UUID.randomUUID().toString());
}

this.update(entry.getResource());
} catch (Exception ex) {
logger.warn("Failed to process resource in transaction", ex);
}
}
}

return transaction;
}
seanmcilvenna marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.lantanagroup.link.measureeval.services;

import org.hl7.fhir.r4.model.Library;

public interface LibraryResolver {
Library resolve(String libraryId);
}
Loading
Loading