Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into e2e/1452/map-local-an…
Browse files Browse the repository at this point in the history
…d-loinc-codes
  • Loading branch information
jbiskie committed Oct 31, 2024
2 parents 91faa6d + 21fedf5 commit da92294
Show file tree
Hide file tree
Showing 45 changed files with 1,156 additions and 756 deletions.
4 changes: 4 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"org.apache.httpcomponents.client5:httpclient5-fluent"
],
"allowedVersions": "!/^5\\.3$/"
},
{
"matchPackageNames": ["com.azure:azure-identity"],
"allowedVersions": "!/^1\\.14\\.0$/"
}],
"pinDigests": false
}
2 changes: 1 addition & 1 deletion .github/workflows/automated-staging-test-submit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Write private key to file
run: |
echo "${{ secrets.AUTOMATED_STAGING_RS_INTEGRATION_PRIVATE_KEY }}" > /tmp/staging_private_key.pem
echo "${{ secrets.SIMULATED_SENDER_STAGING_PRIVATE_KEY }}" > /tmp/staging_private_key.pem
chmod 600 /tmp/staging_private_key.pem
- name: Send HL7 sample messages to staging RS
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
VPN_CA_CERTIFICATE: ${{ secrets.VPN_CA_CERTIFICATE }}
VPN_GITHUB_CERTIFICATE: ${{ secrets.VPN_GITHUB_CERTIFICATE}}
VPN_GITHUB_SECRET_KEY: ${{ secrets.VPN_GITHUB_SECRET_KEY }}
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.NON_PROD_ALERT_SLACK_EMAIL }}"

staging-deploy:
name: Staging Application Deploy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
VPN_CA_CERTIFICATE: ${{ secrets.VPN_CA_CERTIFICATE }}
VPN_GITHUB_CERTIFICATE: ${{ secrets.VPN_GITHUB_CERTIFICATE}}
VPN_GITHUB_SECRET_KEY: ${{ secrets.VPN_GITHUB_SECRET_KEY }}
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.NON_PROD_ALERT_SLACK_EMAIL }}"

dev-deploy:
name: Dev Application Deploy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/internal-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.NON_PROD_ALERT_SLACK_EMAIL }}"

internal-deploy:
name: Internal Application Deploy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
VPN_CA_CERTIFICATE: ${{ secrets.VPN_CA_CERTIFICATE }}
VPN_GITHUB_CERTIFICATE: ${{ secrets.VPN_GITHUB_CERTIFICATE}}
VPN_GITHUB_SECRET_KEY: ${{ secrets.VPN_GITHUB_SECRET_KEY }}
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
TERRAFORM_APPLY_PARAMETERS: -var="alert_slack_email=${{ secrets.PROD_ALERT_SLACK_EMAIL }}"

prod-deploy:
name: Prod Application Deploy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/terraform-ci-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TERRAFORM_APPLY_PARAMETERS: -var="pr_number=${{ github.event.number }}" -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
TERRAFORM_APPLY_PARAMETERS: -var="pr_number=${{ github.event.number }}" -var="alert_slack_email=${{ secrets.NON_PROD_ALERT_SLACK_EMAIL }}"


terraform-deploy-skip: # runs when the PR doesn't have any changes that require the PR deploy; this ensures we get the appropriate required PR checks
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/terraform-ci-destroy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ jobs:
run: terraform init -backend-config="key=pr_${{ github.event.number }}.tfstate"

- name: Terraform Destroy
run: terraform destroy -auto-approve -input=false -var="pr_number=${{ github.event.number }}" -var="alert_slack_email=${{ secrets.ALERT_SLACK_EMAIL }}"
run: terraform destroy -auto-approve -input=false -var="pr_number=${{ github.event.number }}" -var="alert_slack_email=${{ secrets.NON_PROD_ALERT_SLACK_EMAIL }}"
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,10 @@ with this option enabled.
4. Run the `./cleanslate` script. For more information you can refer to the [ReportStream docs](https://github.com/CDCgov/prime-reportstream/blob/master/prime-router/docs/docs-deprecated/getting-started/getting-started.md#building-the-baseline)
5. If attempting to access the metadata endpoint in ReportStream add the variable `ETOR_TI_baseurl="http://host.docker.internal:8080"` to `.prime-router/.vault/env/.env.local` file before building the container
6. Run RS with `docker compose up --build -d`
7. Edit `/settings/STLTs/Flexion/flexion.yml` to comment the lines related to staging settings, and uncomment the ones for local settings:
- `authTokenUrl`, `reportUrl`, `authHeaders.host` under REST `transport` in `receivers`
- `type` and `credentialName` under SFTP `transport` in `receivers`
8. Run the `./reset.sh` script to reset the database
9. Run the `./load-etor-org-settings.sh` to apply the ETOR organization settings
10. Run the `./setup-local-vault.sh` script to set up the local vault secrets
7. Run the `reset.sh` script to reset the database
8. Run the `update_org_yaml.sh` script to update the RS organization settings
9. Run the `load-etor-org-settings.sh` to apply the ETOR organization settings
10. Run the `setup-local-vault.sh` script to set up the local vault secrets
- You can verify that the script created the secrets successfully by going to `http://localhost:8200/` in your browser, use the token in `prime-router/.vault/env/.env.local` to authenticate, and then go to `Secrets engines` > `secret/` to check the available secrets

#### Submit request to ReportStream
Expand Down
31 changes: 18 additions & 13 deletions adr/020-azure-alerts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ Accepted.
## Context

As part of our CI/CD infrastructure, we need notifications when failures occur.
We chose Azure for alerting because it's built into the infrastructure we're already using,
which gives us easy access to metrics. We're not currently using an external
log aggregation system, so Azure alerts were a much lower lift to implement than
any of the other potential options.

To ensure rapid response to application failures within our CI/CD infrastructure, we require real-time notifications for critical issues. The current alert setup focuses on:
Alerts are configured in [alert.tf](../operations/template/alert.tf). To reduce
unhelpful notifications, we have alerts turned off in the PR environments, so they must
either be tested in `internal` or `dev`, or developers may temporarily turn alerts back on in
their branch's PR environment.

- **Type:** [Azure Log Search Alerts](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-types#log-alerts) for HikariCP connection failures.


- **Trigger:** Any logged failures with database connections.


- **Configuration:** Alerts are stateful (auto-mitigation); set to `fired` status to reduce noise from frequent or duplicate alerts.


- **Notification:** Alerts sent to a Slack channel via email until PagerDuty is operational.
Alerts are sent to email addresses that forward to Slack channels. As of October 2024,
production alerts go to `#production-alerts-cdc-trusted-intermediary` and non-prod alerts
go to `#non-prod-alerts-cdc-trusted-intermediary`.

## Impact

Expand All @@ -35,11 +35,16 @@ To ensure rapid response to application failures within our CI/CD infrastructure

### Negative

- Possible alert fatigue if not fine-tuned
- Azure's built-in alert options are less robust than some other services - for instance,
they don't have an option for p50/90/99 latency alert. This means we're more limited in
what kinds of alerts we can have
- Navigating from the Azure Slack alerts to the actual logs where issues are occurring
is unintuitive and requires multiple clicks. Even once you find the right logs,
Azure logs lack syntax highlighting and can be hard to read.

### Risks

- None
- Possible alert fatigue if not fine-tuned

## Related Issues

Expand Down
8 changes: 4 additions & 4 deletions e2e/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ java {

dependencies {
//client
implementation 'org.apache.httpcomponents.client5:httpclient5:5.4'
implementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.4'
implementation 'org.apache.httpcomponents.client5:httpclient5:5.4.1'
implementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.4.1'

//jackson
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.1'

//fhir
implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:7.4.5'
Expand Down
2 changes: 1 addition & 1 deletion etor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies {
testImplementation testFixtures(project(':shared'))

implementation 'com.azure:azure-storage-blob:12.28.1'
implementation 'com.azure:azure-identity:1.14.0'
implementation 'com.azure:azure-identity:1.13.3'

testImplementation 'org.apache.groovy:groovy:4.0.23'
testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,61 @@
public class MapLocalObservationCodes implements CustomFhirTransformation {
protected final Logger logger = ApplicationContext.getImplementation(Logger.class);

private HashMap<String, IdentifierCode> codingMap;

public MapLocalObservationCodes() {
initMap();
}

@Override
public void transform(HealthData<?> resource, Map<String, Object> args) {
var codingMap = getMapFromArgs(args);

var bundle = (Bundle) resource.getUnderlyingData();
var msh41Identifier =
HapiHelper.getMSH4_1Identifier(bundle) != null
? HapiHelper.getMSH4_1Identifier(bundle).getValue()
: null;
var messageId = HapiHelper.getMessageControlId(bundle);
var observations = HapiHelper.resourcesInBundle(bundle, Observation.class);

for (Observation obv : observations.toList()) {
var codingList = obv.getCode().getCoding();

if (codingList.size() != 1) {
continue;
}

var coding = codingList.get(0);
if (!HapiHelper.hasDefinedCoding(
coding, HapiHelper.EXTENSION_ALT_CODING, HapiHelper.LOCAL_CODE)) {
continue;
}
observations
.filter(this::hasValidCoding)
.forEach(
observation ->
processCoding(observation, codingMap, msh41Identifier, messageId));
}

var identifier = codingMap.get(coding.getCode());
if (identifier == null) {
logUnmappedLocalCode(bundle, coding);
continue;
}
private boolean hasValidCoding(Observation observation) {
var codingList = observation.getCode().getCoding();
return codingList.size() == 1 && isLocalCode(codingList.get(0));
}

var mappedCoding = getMappedCoding(identifier);
private boolean isLocalCode(Coding coding) {
return HapiHelper.hasDefinedCoding(
coding, HapiHelper.EXTENSION_ALT_CODING, HapiHelper.LOCAL_CODE);
}

// Add the mapped code as the first in the list, ahead of the existing alternate code
codingList.add(0, mappedCoding);
private void processCoding(
Observation observation,
Map<String, IdentifierCode> codingMap,
String msh41Identifier,
String messageId) {
var originalCoding = observation.getCode().getCoding().get(0);
IdentifierCode identifier = codingMap.get(originalCoding.getCode());

if (identifier == null) {
logger.logWarning(
"Unmapped local code detected: '{}', from sender: '{}', message Id: '{}'",
originalCoding.getCode(),
msh41Identifier,
messageId);
return;
}
}

private void logUnmappedLocalCode(Bundle bundle, Coding coding) {
var msh41Identifier = HapiHelper.getMSH4_1Identifier(bundle);
var msh41Value = msh41Identifier != null ? msh41Identifier.getValue() : null;
var mappedCoding = getMappedCoding(identifier);
observation.getCode().getCoding().add(0, mappedCoding);
}

logger.logWarning(
"Unmapped local code detected: '{}', from sender: '{}', message Id: '{}'",
coding.getCode(),
msh41Value,
HapiHelper.getMessageControlId(bundle));
private String validateField(String field, String fieldName) {
if (field == null || field.isBlank()) {
throw new IllegalArgumentException("missing or empty required field " + fieldName);
}
return field;
}

private Coding getMappedCoding(IdentifierCode identifierCode) {
Expand All @@ -85,92 +94,30 @@ private Coding getMappedCoding(IdentifierCode identifierCode) {
return mappedCoding;
}

/**
* Initializes the local-to-LOINC/PLT hash map, customized for CDPH and UCSD. Currently, the
* mapping is hardcoded for simplicity. If expanded to support additional entities, the
* implementation may be updated to allow dynamic configuration via
* transformation_definitions.json or a database-driven mapping.
*/
private void initMap() {
this.codingMap = new HashMap<>();
// ALD
codingMap.put(
"99717-32",
new IdentifierCode(
"85269-9",
"X-linked Adrenoleukodystrophy (X- ALD) newborn screen interpretation",
HapiHelper.LOINC_CODE));
codingMap.put(
"99717-33",
new IdentifierCode(
"85268-1",
"X-linked Adrenoleukodystrophy (X- ALD) newborn screening comment-discussion",
HapiHelper.LOINC_CODE));
codingMap.put(
"99717-34",
new IdentifierCode(
"PLT325",
"ABCD1 gene mutation found [Identifier] in DBS by Sequencing",
HapiHelper.PLT_CODE));
// CAH
codingMap.put(
"99717-6",
new IdentifierCode(
"53340-6",
"17-Hydroxyprogesterone [Moles/volume] in DBS",
HapiHelper.LOINC_CODE));
// CF
codingMap.put(
"99717-35",
new IdentifierCode(
"PLT3289",
"CFTR gene mutation found [Interpretation] in DBS by Sequencing",
HapiHelper.PLT_CODE));
codingMap.put(
"99717-36",
new IdentifierCode(
"PLT3290",
"CFTR gene variant found [Identifier] in DBS by Sequencing comments/discussion",
HapiHelper.PLT_CODE));
// MPS I
codingMap.put(
"99717-48",
new IdentifierCode(
"PLT3258",
"IDUA gene mutations found [Identifier] in DBS by Sequencing",
HapiHelper.PLT_CODE));
codingMap.put(
"99717-44",
new IdentifierCode(
"PLT3291",
"IDUA gene variant analysis in DBS by Sequencing comments/discussion",
HapiHelper.PLT_CODE));
// MPS II
codingMap.put(
"99717-50",
new IdentifierCode(
"PLT3294",
"IDS gene mutations found [Identifier] in Dried Bloodspot by Molecular genetics method",
HapiHelper.PLT_CODE));
// Pompe
codingMap.put(
"99717-47",
new IdentifierCode(
"PLT3252",
"GAA gene mutation found [Identifier] in DBS by Sequencing",
HapiHelper.PLT_CODE));
codingMap.put(
"99717-46",
new IdentifierCode(
"PLT3292",
"GAA gene variant analysis in DBS by Sequencing comments/discussion",
HapiHelper.PLT_CODE));
// SMA
codingMap.put(
"99717-60",
new IdentifierCode(
"PLT3293",
"SMN1 exon 7 deletion analysis in DBS by Sequencing",
HapiHelper.PLT_CODE));
private Map<String, IdentifierCode> getMapFromArgs(Map<String, Object> args) {
var codingMap = new HashMap<String, IdentifierCode>();

// Suppressing the unchecked cast warning. The assignment below will throw a
// ClassCastException if it fails.
@SuppressWarnings("unchecked")
var argsCodingMap = (Map<String, Map<String, String>>) args.get("codingMap");

for (Map.Entry<String, Map<String, String>> entry : argsCodingMap.entrySet()) {
var localCode = entry.getKey();
var mappedCode = getIdentifierCode(entry);

codingMap.put(localCode, mappedCode);
}

return codingMap;
}

private IdentifierCode getIdentifierCode(Map.Entry<String, Map<String, String>> entry) {
var value = entry.getValue();
var code = validateField(value.get("code"), "code");
var display = validateField(value.get("display"), "display");
var codingSystem = validateField(value.get("codingSystem"), "codingSystem");

return new IdentifierCode(code, display, codingSystem);
}
}
Loading

0 comments on commit da92294

Please sign in to comment.