diff --git a/.gitignore b/.gitignore
index 91d7c24..56276da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea
.gradle
-build/*
\ No newline at end of file
+build/*
+*/build/*
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..922169d
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,96 @@
+pipeline {
+ environment {
+ ARTIFACTORY_URL = credentials('ARTIFACTORY_URL')
+ }
+
+ agent {
+ label 'build'
+ }
+
+ tools {
+ gradle "gradle-7.2"
+ jdk 'adoptjdk13'
+ }
+
+ stages {
+
+ stage ('Build Lambdas') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
+ sh './gradlew -b build.gradle '
+ }
+ }
+ }
+ stage ('Test Lambdas') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
+ sh './gradlew clean test --info -b build.gradle'
+ }
+ }
+ }
+
+ stage ('Build Jars') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
+ sh './gradlew buildZip --info -b build.gradle'
+ }
+ }
+ }
+ // stage('SonarQube Analysis') {
+ // steps {
+ // withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
+ // // Automatically saves the an id for the SonarQube build
+ // withSonarQubeEnv('CMSSonar') {
+ // sh './gradlew sonarqube -Dsonar.projectKey=ab2d-lambdas-project -Dsonar.host.url=https://sonarqube.cloud.cms.gov'
+ // }
+ // }
+ // }
+ //}
+ // stage("Quality Gate") {
+ // options {
+ // timeout(time: 10, unit: 'MINUTES')
+ // }
+ // steps {
+ // Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails
+ // true = set pipeline to UNSTABLE, false = don't
+ // waitForQualityGate abortPipeline: true
+ // }
+ // }*/
+
+ stage ('Publish Lambdas') {
+ //when {
+ // branch 'main'
+ // }
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
+ script {
+ def deployScript = '';
+
+ //Calls a gradle task to check if the version of each build has already been deployed
+ //Each build is broken up by '''.
+ def versionPublishedList = sh(
+ script: './gradlew -q lookForArtifacts',
+ returnStdout: true
+ ).trim().split("'''")
+
+ //First value represents the build name and the second value is if the version is already deployed.
+ for (int i = 1; i < versionPublishedList.size(); i++) {
+ def artifactoryInfo = versionPublishedList[i].split(":")
+ if (artifactoryInfo[1] == 'false') {
+ echo "Deploying ${artifactoryInfo[0]}"
+ deployScript += "${artifactoryInfo[0]}:artifactoryPublish "
+ }
+ }
+
+ //deployScript represents what we are publishing. Insert it into the gradle command to do the publishing.
+ //ex. ab2d-fhir:artifactoryPublish"
+ //If nothing is there, skip publishing
+ if(deployScript != '') {
+ sh "./gradlew ${deployScript} -b build.gradle"
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 4899770..f20434d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,8 +14,17 @@ ext {
: System.getenv()['ARTIFACTORY_USER']
artifactory_password = project.hasProperty('artifactory_password') ? project.artifactory_password
: System.getenv()['ARTIFACTORY_PASSWORD']
+
+ metricsVersion = '1.0.0'
+ fetcherVersion = '1.0.0'
+
+
+ sourcesRepo = 'ab2d-maven-repo'
+ deployerRepo = 'ab2d-main'
+ resolverRepo = 'ab2d-main'
}
+
repositories {
mavenCentral()
maven {
@@ -27,33 +36,112 @@ repositories {
}
}
-dependencies {
- compileOnly 'org.projectlombok:lombok:1.18.24'
- implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
- implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
- implementation 'org.springframework:spring-context:5.3.20'
- implementation 'com.newrelic.agent.java:newrelic-api:7.6.0'
- implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-dstu3:5.7.2'
- implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.7.2'
- implementation 'gov.cms.ab2d:ab2d-aggregator:1.2.1'
- implementation 'gov.cms.ab2d:ab2d-bfd:1.3.1'
- implementation 'gov.cms.ab2d:ab2d-events-client:1.3.1'
- implementation 'gov.cms.ab2d:ab2d-fhir:1.1.1'
- implementation 'gov.cms.ab2d:ab2d-filters:1.6.1'
- implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3'
- annotationProcessor "org.projectlombok:lombok:1.18.24"
-}
-
test {
useJUnitPlatform()
}
-task buildZip(type: Zip) {
- from compileJava
- from processResources
- into('lib') {
- from configurations.runtimeClasspath
+subprojects {
+ apply plugin: 'com.jfrog.artifactory'
+ apply plugin: 'maven-publish'
+ publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ artifact file("build/distributions/${project.name}.zip")
+ }
+ }
+ }
+
+ sourceCompatibility = 11
+ targetCompatibility = 11
+
+ group 'gov.cms.ab2d'
+
+
+ artifactory {
+ contextUrl = project.artifactoryLoc
+
+ publish {
+ repository {
+ repoKey = "${deployerRepo}"
+ username = project.artifactory_user
+ password = project.artifactory_password
+ maven = true
+ }
+ defaults {
+ publications('mavenJava')
+ publishArtifacts = true
+ publishBuildInfo = false
+ }
+ }
+ resolve {
+ repository {
+ repoKey = "${resolverRepo}"
+ username = project.artifactory_user
+ password = project.artifactory_password
+ maven = true
+ }
+ }
+ }
+}
+
+allprojects {
+
+ task lookForArtifacts {
+ doLast {
+ //This is the parent build where nothing gets published. Skip it.
+ if (project.name != 'ab2d-lambdas')
+ //Set path to repository that might exist if previous published on this version.
+ //Sets project name and if it exists in repository. Print out so jenkins can take it.
+ System.out.print("'''" + project.name + ":" + urlExists("${artifactoryLoc}/${deployerRepo}/gov/cms/ab2d/${project.name}/${project.version}/${project.name}-${project.version}.zip"))
+ }
}
+
+ repositories {
+ maven {
+ url = "${artifactoryLoc}/${sourcesRepo}"
+ credentials {
+ username = project.artifactory_user
+ password = project.artifactory_password
+ }
+ }
+ mavenCentral()
+ }
+
+
+}
+
+def getBase64EncodedCredentials() {
+ def s = "$artifactory_user" + ":" + "$artifactory_password"
+ return s.bytes.encodeBase64().toString()
+}
+
+//Check if url exist in the repository
+def urlExists(repositoryUrl) {
+ try {
+ def connection = (HttpURLConnection) new URL(repositoryUrl).openConnection()
+
+ connection.setRequestProperty("Authorization", "Basic " + getBase64EncodedCredentials())
+ connection.setConnectTimeout(10000)
+ connection.setReadTimeout(10000)
+ connection.setRequestMethod("HEAD")
+
+ def responseCode = connection.getResponseCode()
+
+ if (responseCode == 401) {
+ throw new RuntimeException("Unauthorized MavenUser user. Please provide valid username and password.")
+ }
+
+ return (200 == responseCode)
+
+ } catch (IOException ignored) {
+ println(ignored)
+ return false
+ }
+}
+
+task buildZip(type: Zip) {
+ dependsOn ':metrics-lambda:buildZip'
+ dependsOn ':eob-fetcher:buildZip'
}
java {
@@ -62,3 +150,5 @@ java {
}
build.dependsOn buildZip
+
+
diff --git a/eob-fetcher/build.gradle b/eob-fetcher/build.gradle
new file mode 100644
index 0000000..db63577
--- /dev/null
+++ b/eob-fetcher/build.gradle
@@ -0,0 +1,69 @@
+plugins {
+ id 'java'
+}
+
+configurations {
+ all.collect { configuration ->
+ configuration.exclude group: 'org.springframework.boot'
+ configuration.exclude group: 'com.slack.api'
+ configuration.exclude group: 'org.jetbrains.kotlin'
+ configuration.exclude group: 'org.postgresql'
+ configuration.exclude group: 'com.squareup.okhttp3'
+ configuration.exclude group: 'commons-codec'
+ configuration.exclude group: 'com.squareup.okio'
+ configuration.exclude group: 'org.springframework.cloud'
+ configuration.exclude group: 'org.mock-server'
+ configuration.exclude group: 'software.amazon.awssdk:netty-nio-client'
+ configuration.exclude group: 'io.netty'
+ configuration.exclude group: 'org.testcontainers:testcontainers'
+
+ }
+
+}
+
+version "$fetcherVersion"
+
+dependencies {
+ compileOnly 'org.projectlombok:lombok:1.18.24'
+ implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
+ implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
+ implementation 'org.springframework:spring-context:5.3.23'
+ implementation 'com.newrelic.agent.java:newrelic-api:7.10.0'
+ implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-dstu3:6.1.1'
+ implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:6.1.1'
+ implementation 'gov.cms.ab2d:ab2d-aggregator:1.2.1'
+ implementation 'gov.cms.ab2d:ab2d-bfd:1.3.1'
+ implementation 'gov.cms.ab2d:ab2d-events-client:1.3.1'
+ implementation 'gov.cms.ab2d:ab2d-fhir:1.1.1'
+ implementation 'gov.cms.ab2d:ab2d-filters:1.6.1'
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3'
+ annotationProcessor "org.projectlombok:lombok:1.18.24"
+}
+
+test {
+ useJUnitPlatform()
+}
+
+task buildZip(type: Zip) {
+ from compileJava
+ from processResources
+ into('lib') {
+ from configurations.runtimeClasspath
+ }
+ archiveFileName = 'eob-fetcher.zip'
+}
+
+task wrapper(type: Wrapper){
+ gradleVersion = '7.5'
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+repositories {
+ mavenCentral()
+}
+
+build.dependsOn buildZip
diff --git a/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java
diff --git a/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java
diff --git a/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java
diff --git a/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java
diff --git a/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java
diff --git a/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java
diff --git a/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java
diff --git a/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java
similarity index 100%
rename from src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java
rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java
diff --git a/src/main/resources/fetcher.properties b/eob-fetcher/src/main/resources/fetcher.properties
similarity index 100%
rename from src/main/resources/fetcher.properties
rename to eob-fetcher/src/main/resources/fetcher.properties
diff --git a/src/main/resources/logback.xml b/eob-fetcher/src/main/resources/logback.xml
similarity index 100%
rename from src/main/resources/logback.xml
rename to eob-fetcher/src/main/resources/logback.xml
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 69a9715..ae04661 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/gradlew.bat b/gradlew.bat
index 107acd3..ac1b06f 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,89 +1,89 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/logback.xml b/logback.xml
new file mode 100644
index 0000000..e67af38
--- /dev/null
+++ b/logback.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/metrics-lambda/README.md b/metrics-lambda/README.md
new file mode 100644
index 0000000..c5eb67b
--- /dev/null
+++ b/metrics-lambda/README.md
@@ -0,0 +1,12 @@
+A simple lambda that converts Cloudwatch events to messages the event service can accept.
+
+## Build
+
+AWS lambdas need to be zipped. The follow command will build the code and zip the resulting jar.
+```
+gradle buildZip
+```
+
+## Deploy
+
+For the time being this process is manual. Log into AWS, find the metric lambda, and upload the zip from the build process.
\ No newline at end of file
diff --git a/metrics-lambda/build.gradle b/metrics-lambda/build.gradle
new file mode 100644
index 0000000..d9ecc16
--- /dev/null
+++ b/metrics-lambda/build.gradle
@@ -0,0 +1,70 @@
+plugins {
+ id 'java'
+}
+
+version "$metricsVersion"
+
+configurations {
+ all.collect { configuration ->
+ configuration.exclude group: 'org.springframework'
+ configuration.exclude group: 'org.springframework.boot'
+ configuration.exclude group: 'com.slack.api'
+ configuration.exclude group: 'org.jetbrains.kotlin'
+ configuration.exclude group: 'org.postgresql'
+ configuration.exclude group: 'com.squareup.okhttp3'
+ configuration.exclude group: 'commons-codec'
+ configuration.exclude group: 'com.squareup.okio'
+ configuration.exclude group: 'org.springframework.cloud'
+ configuration.exclude group: 'org.mock-server'
+ configuration.exclude group: 'software.amazon.awssdk:netty-nio-client'
+ configuration.exclude group: 'io.netty'
+ configuration.exclude group: 'org.testcontainers:testcontainers'
+ // Lombok is amazing but its jar is 2 MBs which is 20% of the total fat jar.
+ // Even when set as compile and annotationProcessor only gradle still adds it to the fat jar
+ configuration.exclude group: 'org.projectlombok'
+ }
+
+}
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+dependencies {
+ implementation 'gov.cms.ab2d:ab2d-events-client:1.6'
+ implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
+ implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
+ implementation 'software.amazon.awssdk:cloudwatch:2.17.290'
+ implementation 'com.amazonaws:aws-java-sdk-sqs:1.12.319'
+ implementation 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.4'
+ implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
+ testImplementation 'org.mockito:mockito-core:4.8.0'
+}
+
+test {
+ useJUnitPlatform()
+}
+
+task buildZip(type: Zip) {
+ from compileJava
+ from processResources
+ into('lib') {
+ from configurations.runtimeClasspath
+ }
+ archiveFileName = 'metrics-lambda.zip'
+}
+
+task wrapper(type: Wrapper){
+ gradleVersion = '7.5'
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+build.dependsOn buildZip
+
diff --git a/metrics-lambda/docker-compose.yml b/metrics-lambda/docker-compose.yml
new file mode 100644
index 0000000..c06c4be
--- /dev/null
+++ b/metrics-lambda/docker-compose.yml
@@ -0,0 +1,16 @@
+version: '3'
+
+services:
+ localstack:
+ image: localstack/localstack:1.1.0
+ entrypoint: [ "/tmp/setup/localstack-setup.sh" ,"docker-entrypoint.sh" ]
+ environment:
+ - AWS_DEFAULT_REGION=us-east-1
+ - EDGE_PORT=4566
+ - SERVICES=sqs,sns,lambda
+ - LS_LOG=error
+ - DATA_DIR="/tmp/localstack"
+ ports:
+ - '4566:4566'
+ volumes:
+ - ./:/tmp/setup
\ No newline at end of file
diff --git a/metrics-lambda/localstack-setup.sh b/metrics-lambda/localstack-setup.sh
new file mode 100755
index 0000000..8427633
--- /dev/null
+++ b/metrics-lambda/localstack-setup.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+function setup(){
+ until grep -q '^Ready.' /tmp/localstack_infra.log >/dev/null 2>&1 ; do
+ echo "Waiting for all LocalStack services to be ready"
+ sleep 7
+ done
+
+ awslocal lambda create-function --function-name CloudwatchEventHandler --zip-file fileb:///tmp/setup/build/distributions/ab2d-metrics.zip --handler gov.cms.ab2d.metrics.CloudwatchEventHandler --environment="Variables={com.amazonaws.sdk.disableCertChecking=true,IS_LOCALSTACK=true}" --runtime java11 --timeout 900 --role=""
+ awslocal sqs create-queue --queue-name ab2d-events
+}
+
+setup &
+
+$@
\ No newline at end of file
diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java
new file mode 100644
index 0000000..7a4ddf1
--- /dev/null
+++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java
@@ -0,0 +1,135 @@
+package gov.cms.ab2d.metrics;
+
+import com.amazonaws.SDKGlobalConfiguration;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.LambdaLogger;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.amazonaws.util.StringUtils;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import gov.cms.ab2d.eventclient.events.MetricsEvent;
+import gov.cms.ab2d.eventclient.messages.GeneralSQSMessage;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import static gov.cms.ab2d.eventclient.events.MetricsEvent.State.END;
+import static gov.cms.ab2d.eventclient.events.MetricsEvent.State.START;
+import static software.amazon.awssdk.services.cloudwatch.model.StateValue.ALARM;
+import static software.amazon.awssdk.services.cloudwatch.model.StateValue.OK;
+
+
+// Catches cloudwatch alerts, extracts what we care about, then send an event to the ab2d-event sqs queue
+public class CloudwatchEventHandler implements RequestHandler {
+ private static AmazonSQS amazonSQS;
+
+ private final String environment = Optional.ofNullable(System.getenv("PARENT_ENV"))
+ .orElse("local") + "-";
+
+ // AWS sends an object that's not wrapped with type info. The event service expects the wrapper.
+ // Since there's not an easy way to enable/disable type wrapper just have 2 mappers.
+ private final ObjectMapper inputMapper = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .registerModule(new JodaModule())
+ .registerModule(new JavaTimeModule());
+
+ private final ObjectMapper outputMapper = new ObjectMapper()
+ .activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY)
+ .registerModule(new JodaModule())
+ .registerModule(new JavaTimeModule());
+
+ static {
+ amazonSQS = setup();
+ }
+
+ private static AmazonSQS setup() {
+ if (!StringUtils.isNullOrEmpty(System.getenv("IS_LOCALSTACK"))) {
+ System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY, "true");
+ return AmazonSQSAsyncClientBuilder.standard()
+ .withEndpointConfiguration(new AwsClientBuilder
+ .EndpointConfiguration("https://localhost:4566",
+ Regions.US_EAST_1.getName()))
+ .build();
+ } else {
+ return AmazonSQSAsyncClientBuilder.standard()
+ .build();
+ }
+ }
+
+ @Override
+ public String handleRequest(SNSEvent snsEvent, Context context) {
+ final LambdaLogger log = context.getLogger();
+ snsEvent.getRecords()
+ .forEach(snsRecord -> sendMetric(snsRecord, log));
+ return "OK";
+ }
+
+ private void sendMetric(SNSEvent.SNSRecord snsRecord, LambdaLogger log) {
+ final SendMessageRequest request = new SendMessageRequest();
+ final String queue = environment + "events-sqs";
+ final String service;
+ try {
+ final MetricAlarm alarm = inputMapper.readValue(Optional.ofNullable(snsRecord.getSNS())
+ .orElse(new SNSEvent.SNS())
+ .getMessage(), MetricAlarm.class);
+ OffsetDateTime time = alarm.getStateChangeTime() != null
+ ? OffsetDateTime.parse(fixDate(alarm.getStateChangeTime()))
+ : OffsetDateTime.now();
+ request.setQueueUrl(amazonSQS.getQueueUrl(queue)
+ .getQueueUrl());
+ service = Optional.ofNullable(alarm.getAlarmName())
+ .orElseThrow(() -> new EventDataException("AlarmName is null"))
+ .replace(environment, "");
+ request.setMessageBody(outputMapper.writeValueAsString(new GeneralSQSMessage(MetricsEvent.builder()
+ .service(service)
+ .eventDescription(alarm.getAlarmDescription())
+ .timeOfEvent(time)
+ //This might need more work later if AWS is sending unknown states regularly
+ .stateType(from(alarm.getNewStateValue()))
+ .build())));
+ } catch (Exception e) {
+ log.log(String.format("Handling lambda failed %s", exceptionToString(e)));
+ return;
+ }
+ log.log(String.format("Sending %s to %s", service, queue));
+ amazonSQS.sendMessage(request);
+ }
+
+ private String exceptionToString(Exception exception) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exception.printStackTrace(pw);
+ return sw.toString();
+ }
+
+
+ private String fixDate(String date) {
+ StringBuilder builder = new StringBuilder(date);
+ return builder.insert(builder.length() - 2, ":")
+ .toString();
+ }
+
+
+ private MetricsEvent.State from(String stateValue) {
+ return Stream.of(stateValue)
+ .filter(value1 -> List.of(OK.toString(), ALARM.toString())
+ .contains(value1))
+ .map(state -> stateValue.equals(OK.toString()) ? END : START)
+ .findFirst()
+ .orElseThrow(() -> new EventDataException(String.format("AWS provided Unknown State %s", stateValue)));
+ }
+}
diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java
new file mode 100644
index 0000000..a302adb
--- /dev/null
+++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java
@@ -0,0 +1,31 @@
+package gov.cms.ab2d.metrics;
+
+public class Dimensions {
+ private String value;
+ private String name;
+
+ public Dimensions(String value, String name) {
+ this.value = value;
+ this.name = name;
+ }
+
+ public Dimensions() {
+ //default constructor for Jackson
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java
new file mode 100644
index 0000000..2198e23
--- /dev/null
+++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java
@@ -0,0 +1,7 @@
+package gov.cms.ab2d.metrics;
+
+public class EventDataException extends RuntimeException {
+ public EventDataException(String error) {
+ super(error);
+ }
+}
diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java
new file mode 100644
index 0000000..249d761
--- /dev/null
+++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java
@@ -0,0 +1,160 @@
+package gov.cms.ab2d.metrics;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+import java.util.Optional;
+
+public class MetricAlarm {
+ @JsonProperty("AlarmName")
+ private String alarmName;
+ @JsonProperty("AlarmDescription")
+ private String alarmDescription;
+ @JsonProperty("AWSAccountId")
+ private String awsAccountId;
+ @JsonProperty("AlarmConfigurationUpdatedTimestamp")
+ private String alarmConfigurationUpdatedTimestamp;
+ @JsonProperty("NewStateValue")
+ private String newStateValue;
+ @JsonProperty("NewStateReason")
+ private String newStateReason;
+ @JsonProperty("StateChangeTime")
+ private String stateChangeTime;
+ @JsonProperty("Region")
+ private String region;
+ @JsonProperty("AlarmArn")
+ private String alarmArn;
+ @JsonProperty("OldStateValue")
+ private String oldStateValue;
+ @JsonProperty("OKActions")
+ private List okActions;
+ @JsonProperty("AlarmActions")
+ private List alarmActions;
+ @JsonProperty("InsufficientDataActions")
+ private List