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 15 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'
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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());
Expand Down
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,18 @@
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 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 +101,44 @@ 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));
return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range);
}

@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);
// 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);
}
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,52 @@
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;

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
Expand Up @@ -3,8 +3,10 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import com.lantanagroup.link.measureeval.repositories.LinkInMemoryFhirRepository;
import com.lantanagroup.link.measureeval.utils.ParametersUtils;
import com.lantanagroup.link.measureeval.utils.StreamUtils;
import lombok.Getter;
import org.cqframework.cql.cql2elm.LibraryBuilder;
import org.hl7.fhir.r4.model.*;
import org.opencds.cqf.fhir.api.Repository;
Expand All @@ -14,7 +16,6 @@
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureService;
import org.opencds.cqf.fhir.utility.monad.Eithers;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -28,6 +29,7 @@ public class MeasureEvaluator {

private final FhirContext fhirContext;
private final MeasureEvaluationOptions options;
@Getter
private final Bundle bundle;
private final Measure measure;

Expand Down Expand Up @@ -74,10 +76,6 @@ private MeasureEvaluator(FhirContext fhirContext, Bundle bundle, boolean isDebug
}
}

public static MeasureEvaluator compile(FhirContext fhirContext, Bundle bundle) {
return compile(fhirContext, bundle, false);
}

public static MeasureEvaluator compile(FhirContext fhirContext, Bundle bundle, boolean isDebug) {
MeasureEvaluator instance = new MeasureEvaluator(fhirContext, bundle, isDebug);
instance.compile();
Expand All @@ -94,12 +92,21 @@ private void compile() {
doEvaluate(null, null, new StringType(subject), additionalData);
}

public static MeasureReport compileAndEvaluate(FhirContext fhirContext, Bundle bundle, Parameters parameters, boolean isDebug) {
MeasureEvaluator evaluator = new MeasureEvaluator(fhirContext, bundle, isDebug);
DateTimeType periodStart = ParametersUtils.getValue(parameters, "periodStart", DateTimeType.class);
DateTimeType periodEnd = ParametersUtils.getValue(parameters, "periodEnd", DateTimeType.class);
StringType subject = ParametersUtils.getValue(parameters, "subject", StringType.class);
Bundle additionalData = ParametersUtils.getResource(parameters, "additionalData", Bundle.class);
return evaluator.doEvaluate(periodStart, periodEnd, subject, additionalData);
}

private MeasureReport doEvaluate(
DateTimeType periodStart,
DateTimeType periodEnd,
StringType subject,
Bundle additionalData) {
Repository repository = new InMemoryFhirRepository(fhirContext, bundle);
Repository repository = new LinkInMemoryFhirRepository(fhirContext, bundle);
R4MeasureService measureService = new R4MeasureService(repository, options);
return measureService.evaluate(
Eithers.forRight3(measure),
Expand All @@ -117,38 +124,50 @@ 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<Bundle.BundleEntryComponent> 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());
seanmcilvenna marked this conversation as resolved.
Show resolved Hide resolved
}
}

seanmcilvenna marked this conversation as resolved.
Show resolved Hide resolved
// 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());
}
}
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,10 +14,12 @@ public class MeasureEvaluatorCache {
private final FhirContext fhirContext;
private final MeasureDefinitionRepository definitionRepository;
private final Map<String, MeasureEvaluator> 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) {
Expand All @@ -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());
seanmcilvenna marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading