From ececbe0e94bb85455c2f64a08101fb9abc49e624 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 11:28:13 -0700 Subject: [PATCH 01/12] Bump semver and aws-cdk-lib in /release/staging-resources-cdk (#3047) Bumps [semver](https://github.com/npm/node-semver) to 7.5.3 and updates ancestor dependencies [semver](https://github.com/npm/node-semver) and [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib). These dependencies need to be updated together. Updates `semver` from 6.3.0 to 7.5.3 - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v7.5.3) Updates `semver` from 5.7.1 to 7.5.3 - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v7.5.3) Updates `aws-cdk-lib` from 2.80.0 to 2.88.0 - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.88.0/packages/aws-cdk-lib) --- updated-dependencies: - dependency-name: semver dependency-type: indirect - dependency-name: semver dependency-type: indirect - dependency-name: aws-cdk-lib dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../staging-resources-cdk/package-lock.json | 142 +++++++++--------- release/staging-resources-cdk/package.json | 2 +- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/release/staging-resources-cdk/package-lock.json b/release/staging-resources-cdk/package-lock.json index 2475d21ab5..92c705a6cd 100644 --- a/release/staging-resources-cdk/package-lock.json +++ b/release/staging-resources-cdk/package-lock.json @@ -8,7 +8,7 @@ "name": "staging-resources-cdk", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "2.80.0", + "aws-cdk-lib": "2.88.0", "constructs": "^10.0.0", "source-map-support": "^0.5.16" }, @@ -42,9 +42,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.199", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.199.tgz", - "integrity": "sha512-zNdD2OxALdsdQaRZBpTfMTuudxV+4jLMznJIvVj6O+OqCru4m5UtgVQmyApW1z2H9s4/06ovVt20aXe2G8Ta+w==" + "version": "2.2.200", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz", + "integrity": "sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg==" }, "node_modules/@aws-cdk/asset-kubectl-v20": { "version": "2.1.2", @@ -108,9 +108,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -165,9 +165,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -1718,9 +1718,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.80.0.tgz", - "integrity": "sha512-PoqD3Yms5I0ajuTi071nTW/hpkH3XsdyZzn5gYsPv0qD7mqP3h6Qr+6RiGx+yQ1KcVFyxWdX15uK+DsC0KwvcQ==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.88.0.tgz", + "integrity": "sha512-bmhokh30HVeqlotWaoEmK7mKB9SJbJwpbsiVgmYe3JcMu8DposHQqaIPI7LnC+dg015tZaxUsExxOYBEw+vntQ==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1734,9 +1734,9 @@ "yaml" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "@aws-cdk/asset-awscli-v1": "^2.2.200", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.165", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.1.1", @@ -1744,7 +1744,7 @@ "jsonschema": "^1.4.1", "minimatch": "^3.1.2", "punycode": "^2.3.0", - "semver": "^7.5.1", + "semver": "^7.5.4", "table": "^6.8.1", "yaml": "1.10.2" }, @@ -1960,7 +1960,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.1", + "version": "7.5.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -2112,9 +2112,9 @@ } }, "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4235,9 +4235,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5046,9 +5046,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5280,9 +5280,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -6353,9 +6353,9 @@ } }, "node_modules/sane/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -6420,9 +6420,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -7822,9 +7822,9 @@ } }, "@aws-cdk/asset-awscli-v1": { - "version": "2.2.199", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.199.tgz", - "integrity": "sha512-zNdD2OxALdsdQaRZBpTfMTuudxV+4jLMznJIvVj6O+OqCru4m5UtgVQmyApW1z2H9s4/06ovVt20aXe2G8Ta+w==" + "version": "2.2.200", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz", + "integrity": "sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg==" }, "@aws-cdk/asset-kubectl-v20": { "version": "2.1.2", @@ -7875,9 +7875,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -7921,9 +7921,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -9097,13 +9097,13 @@ } }, "aws-cdk-lib": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.80.0.tgz", - "integrity": "sha512-PoqD3Yms5I0ajuTi071nTW/hpkH3XsdyZzn5gYsPv0qD7mqP3h6Qr+6RiGx+yQ1KcVFyxWdX15uK+DsC0KwvcQ==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.88.0.tgz", + "integrity": "sha512-bmhokh30HVeqlotWaoEmK7mKB9SJbJwpbsiVgmYe3JcMu8DposHQqaIPI7LnC+dg015tZaxUsExxOYBEw+vntQ==", "requires": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "@aws-cdk/asset-awscli-v1": "^2.2.200", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.165", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.1.1", @@ -9111,7 +9111,7 @@ "jsonschema": "^1.4.1", "minimatch": "^3.1.2", "punycode": "^2.3.0", - "semver": "^7.5.1", + "semver": "^7.5.4", "table": "^6.8.1", "yaml": "1.10.2" }, @@ -9248,7 +9248,7 @@ "bundled": true }, "semver": { - "version": "7.5.1", + "version": "7.5.4", "bundled": true, "requires": { "lru-cache": "^6.0.0" @@ -9354,9 +9354,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -10983,9 +10983,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -11616,9 +11616,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -11810,9 +11810,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -12608,9 +12608,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -12659,9 +12659,9 @@ } }, "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" diff --git a/release/staging-resources-cdk/package.json b/release/staging-resources-cdk/package.json index b293856d63..d9e237b71e 100644 --- a/release/staging-resources-cdk/package.json +++ b/release/staging-resources-cdk/package.json @@ -25,7 +25,7 @@ "typescript": "~3.9.7" }, "dependencies": { - "aws-cdk-lib": "2.80.0", + "aws-cdk-lib": "2.88.0", "constructs": "^10.0.0", "source-map-support": "^0.5.16" } From f15e5824e44704baceeb561e9f75975cc1cb7cff Mon Sep 17 00:00:00 2001 From: Qi Chen Date: Thu, 31 Aug 2023 15:38:14 -0500 Subject: [PATCH 02/12] MAINT: merge dataflow model instead of files (#3290) --------- Signed-off-by: George Chen --- .../parser/PipelinesDataflowModelParser.java | 73 +++++++++++-------- .../PipelinesDataflowModelParserTest.java | 2 + 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParser.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParser.java index 0cfd8fa257..c451d46c49 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParser.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParser.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.opensearch.dataprepper.model.configuration.DataPrepperVersion; +import org.opensearch.dataprepper.model.configuration.PipelineModel; import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,9 +15,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.SequenceInputStream; -import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -35,18 +35,8 @@ public PipelinesDataflowModelParser(final String pipelineConfigurationFileLocati } public PipelinesDataFlowModel parseConfiguration() { - try (final InputStream mergedPipelineConfigurationFiles = mergePipelineConfigurationFiles()) { - final PipelinesDataFlowModel pipelinesDataFlowModel = OBJECT_MAPPER.readValue(mergedPipelineConfigurationFiles, - PipelinesDataFlowModel.class); - - final DataPrepperVersion version = pipelinesDataFlowModel.getDataPrepperVersion(); - validateDataPrepperVersion(version); - - return pipelinesDataFlowModel; - } catch (IOException e) { - LOG.error("Failed to parse the configuration file {}", pipelineConfigurationFileLocation); - throw new ParseException(format("Failed to parse the configuration file %s", pipelineConfigurationFileLocation), e); - } + final List pipelinesDataFlowModels = parsePipelineConfigurationFiles(); + return mergePipelinesDataModels(pipelinesDataFlowModels); } private void validateDataPrepperVersion(final DataPrepperVersion version) { @@ -57,38 +47,61 @@ private void validateDataPrepperVersion(final DataPrepperVersion version) { } } - private InputStream mergePipelineConfigurationFiles() throws IOException { + private List parsePipelineConfigurationFiles() { final File configurationLocation = new File(pipelineConfigurationFileLocation); if (configurationLocation.isFile()) { - return new FileInputStream(configurationLocation); + return Stream.of(configurationLocation).map(this::parsePipelineConfigurationFile) + .filter(Objects::nonNull).collect(Collectors.toList()); } else if (configurationLocation.isDirectory()) { FileFilter yamlFilter = pathname -> (pathname.getName().endsWith(".yaml") || pathname.getName().endsWith(".yml")); - List configurationFiles = Stream.of(configurationLocation.listFiles(yamlFilter)) - .map(file -> { - InputStream inputStream; - try { - inputStream = new FileInputStream(file); - LOG.info("Reading pipeline configuration from {}", file.getName()); - } catch (FileNotFoundException e) { - inputStream = null; - LOG.warn("Pipeline configuration file {} not found", file.getName()); - } - return inputStream; - }) + List pipelinesDataFlowModels = Stream.of(configurationLocation.listFiles(yamlFilter)) + .map(this::parsePipelineConfigurationFile) .filter(Objects::nonNull) .collect(Collectors.toList()); - if (configurationFiles.isEmpty()) { + if (pipelinesDataFlowModels.isEmpty()) { LOG.error("Pipelines configuration file not found at {}", pipelineConfigurationFileLocation); throw new ParseException( format("Pipelines configuration file not found at %s", pipelineConfigurationFileLocation)); } - return new SequenceInputStream(Collections.enumeration(configurationFiles)); + return pipelinesDataFlowModels; } else { LOG.error("Pipelines configuration file not found at {}", pipelineConfigurationFileLocation); throw new ParseException(format("Pipelines configuration file not found at %s", pipelineConfigurationFileLocation)); } } + + private PipelinesDataFlowModel parsePipelineConfigurationFile(final File pipelineConfigurationFile) { + try (final InputStream pipelineConfigurationInputStream = new FileInputStream(pipelineConfigurationFile)) { + LOG.info("Reading pipeline configuration from {}", pipelineConfigurationFile.getName()); + final PipelinesDataFlowModel pipelinesDataFlowModel = OBJECT_MAPPER.readValue(pipelineConfigurationInputStream, + PipelinesDataFlowModel.class); + + final DataPrepperVersion version = pipelinesDataFlowModel.getDataPrepperVersion(); + validateDataPrepperVersion(version); + + return pipelinesDataFlowModel; + } catch (IOException e) { + if (e instanceof FileNotFoundException) { + LOG.warn("Pipeline configuration file {} not found", pipelineConfigurationFile.getName()); + return null; + } + LOG.error("Failed to parse the configuration file {}", pipelineConfigurationFileLocation); + throw new ParseException(format("Failed to parse the configuration file %s", pipelineConfigurationFileLocation), e); + } + } + + private PipelinesDataFlowModel mergePipelinesDataModels( + final List pipelinesDataFlowModels) { + final Map pipelinesDataFlowModelMap = pipelinesDataFlowModels.stream() + .map(PipelinesDataFlowModel::getPipelines) + .flatMap(pipelines -> pipelines.entrySet().stream()) + .collect(Collectors.toUnmodifiableMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + return new PipelinesDataFlowModel(pipelinesDataFlowModelMap); + } } diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParserTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParserTest.java index aaa79eba34..a5a13c0753 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParserTest.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/parser/PipelinesDataflowModelParserTest.java @@ -6,6 +6,7 @@ import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -46,6 +47,7 @@ void parseConfiguration_from_directory_with_multiple_files_creates_the_correct_m final PipelinesDataFlowModel actualPipelinesDataFlowModel = pipelinesDataflowModelParser.parseConfiguration(); assertThat(actualPipelinesDataFlowModel.getPipelines().keySet(), equalTo(TestDataProvider.VALID_MULTIPLE_PIPELINE_NAMES)); + assertThat(actualPipelinesDataFlowModel.getDataPrepperVersion(), nullValue()); } @Test From a911c2340653edc87154c7fb4b7169c0d96fcec9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:02:21 -0700 Subject: [PATCH 03/12] Bump tough-cookie from 4.1.2 to 4.1.3 in /release/staging-resources-cdk (#2993) Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/salesforce/tough-cookie/releases) - [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md) - [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: tough-cookie dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- release/staging-resources-cdk/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/release/staging-resources-cdk/package-lock.json b/release/staging-resources-cdk/package-lock.json index 92c705a6cd..83b8d7a0bd 100644 --- a/release/staging-resources-cdk/package-lock.json +++ b/release/staging-resources-cdk/package-lock.json @@ -7175,9 +7175,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", @@ -13268,9 +13268,9 @@ } }, "tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { "psl": "^1.1.33", From db10b9ef2c887dccf21e6c4be63d42ce9ad021a9 Mon Sep 17 00:00:00 2001 From: David Venable Date: Fri, 1 Sep 2023 09:36:23 -0700 Subject: [PATCH 04/12] Updates to Gradle 8.3; fixes deprecated Gradle behavior (#3269) Updates to Gradle 8.3, including fixing deprecated behavior. Resolves #3267 Signed-off-by: David Venable --- data-prepper-main/build.gradle | 18 ++++++++++++++++ e2e-test/build.gradle | 18 ++++++++-------- e2e-test/log/build.gradle | 9 ++++---- e2e-test/peerforwarder/build.gradle | 20 +++++++++++------- e2e-test/trace/build.gradle | 9 ++++---- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +++- gradlew | 25 +++++++++++++++-------- gradlew.bat | 1 + performance-test/build.gradle | 2 +- release/archives/build.gradle | 21 ++++++++++++++++--- release/docker/build.gradle | 4 ++-- 12 files changed, 90 insertions(+), 41 deletions(-) diff --git a/data-prepper-main/build.gradle b/data-prepper-main/build.gradle index 4cccbe14d5..63b6c57b99 100644 --- a/data-prepper-main/build.gradle +++ b/data-prepper-main/build.gradle @@ -27,3 +27,21 @@ jar { 'Main-Class': 'org.opensearch.dataprepper.DataPrepperExecute') } } + +configurations { + allDependencyJars { + canBeConsumed = true + canBeResolved = true + extendsFrom runtimeClasspath + } +} + +artifacts { + /** + * Shares a configuration which has all the necessary dependencies and can be used + * as part of the release to load all dependencies. + * https://docs.gradle.org/current/userguide/cross_project_publications.html + */ + allDependencyJars(jar) +} + diff --git a/e2e-test/build.gradle b/e2e-test/build.gradle index 95831fc8ed..d20d91f168 100644 --- a/e2e-test/build.gradle +++ b/e2e-test/build.gradle @@ -10,10 +10,17 @@ subprojects { gradlePluginPortal() } dependencies { - classpath 'com.bmuschko:gradle-docker-plugin:7.0.0' + classpath 'com.bmuschko:gradle-docker-plugin:9.3.2' } } + ext { + dataPrepperJarImageFilepath = 'bin/data-prepper/' + targetJavaVersion = project.hasProperty('endToEndJavaVersion') ? project.getProperty('endToEndJavaVersion') : '11' + targetOpenTelemetryVersion = project.hasProperty('openTelemetryVersion') ? project.getProperty('openTelemetryVersion') : "${libs.versions.opentelemetry.get()}" + dataPrepperBaseImage = "eclipse-temurin:${targetJavaVersion}-jre" + } + sourceSets { integrationTest { java { @@ -40,17 +47,10 @@ subprojects { duplicatesStrategy = DuplicatesStrategy.EXCLUDE from project(':data-prepper-main').jar.archivePath from project(':data-prepper-main').configurations.runtimeClasspath - into("${project.buildDir}/bin/data-prepper") + into("${project.buildDir}/docker/${dataPrepperJarImageFilepath}") } dependencies { testImplementation testLibs.junit.vintage } - - ext { - dataPrepperJarFilepath = "${project.buildDir.name}/bin/data-prepper/" - targetJavaVersion = project.hasProperty('endToEndJavaVersion') ? project.getProperty('endToEndJavaVersion') : '11' - targetOpenTelemetryVersion = project.hasProperty('openTelemetryVersion') ? project.getProperty('openTelemetryVersion') : "${libs.versions.opentelemetry.get()}" - dataPrepperBaseImage = "eclipse-temurin:${targetJavaVersion}-jre" - } } diff --git a/e2e-test/log/build.gradle b/e2e-test/log/build.gradle index f3516d406e..382dcc5100 100644 --- a/e2e-test/log/build.gradle +++ b/e2e-test/log/build.gradle @@ -41,16 +41,15 @@ task createDataPrepperDockerFile(type: Dockerfile) { dependsOn copyDataPrepperJar destFile = project.file('build/docker/Dockerfile') from(dataPrepperBaseImage) - workingDir("/app/data-prepper") - copyFile("${dataPrepperJarFilepath}", "/app/data-prepper/lib") + workingDir('/app/data-prepper') + copyFile("${dataPrepperJarImageFilepath}", '/app/data-prepper/lib') defaultCommand('java', '-Ddata-prepper.dir=/app/data-prepper', '-cp', '/app/data-prepper/lib/*', 'org.opensearch.dataprepper.DataPrepperExecute') } task buildDataPrepperDockerImage(type: DockerBuildImage) { dependsOn createDataPrepperDockerFile - inputDir = file(".") - dockerFile = file("build/docker/Dockerfile") - images.add("e2e-test-log-pipeline-image") + dockerFile = file('build/docker/Dockerfile') + images.add('e2e-test-log-pipeline-image') } def createDataPrepperDockerContainer(final String taskBaseName, final String dataPrepperName, final int sourcePort, diff --git a/e2e-test/peerforwarder/build.gradle b/e2e-test/peerforwarder/build.gradle index a93dc88100..4a0a0aad0f 100644 --- a/e2e-test/peerforwarder/build.gradle +++ b/e2e-test/peerforwarder/build.gradle @@ -36,23 +36,29 @@ def DATA_PREPPER_CONFIG_STATIC = "data_prepper_static.yml" /** * DataPrepper Docker tasks */ + +task copyTestResources(type: Copy) { + from 'src/integrationTest/resources/' + into "${project.buildDir}/docker/test-resources" +} + task createDataPrepperDockerFile(type: Dockerfile) { dependsOn copyDataPrepperJar + dependsOn copyTestResources destFile = project.file('build/docker/Dockerfile') from(dataPrepperBaseImage) exposePort(2021) - workingDir("/app/data-prepper") - copyFile("${dataPrepperJarFilepath}", "/app/data-prepper/lib") - copyFile("src/integrationTest/resources/default_certificate.pem", "/app/data-prepper/config/default_certificate.pem") - copyFile("src/integrationTest/resources/default_private_key.pem", "/app/data-prepper/config/default_private_key.pem") + workingDir('/app/data-prepper') + copyFile("${dataPrepperJarImageFilepath}", '/app/data-prepper/lib') + copyFile('test-resources/default_certificate.pem', '/app/data-prepper/config/default_certificate.pem') + copyFile('test-resources/default_private_key.pem', '/app/data-prepper/config/default_private_key.pem') defaultCommand('java', '-Ddata-prepper.dir=/app/data-prepper', '-cp', '/app/data-prepper/lib/*', 'org.opensearch.dataprepper.DataPrepperExecute') } task buildDataPrepperDockerImage(type: DockerBuildImage) { dependsOn createDataPrepperDockerFile - inputDir = file(".") - dockerFile = file("build/docker/Dockerfile") - images.add("integ-test-pipeline-image") + dockerFile = file('build/docker/Dockerfile') + images.add('integ-test-pipeline-image') } def createDataPrepperDockerContainer(final String taskBaseName, final String dataPrepperName, final int sourcePort, diff --git a/e2e-test/trace/build.gradle b/e2e-test/trace/build.gradle index bc97baf9ce..0f811a26a7 100644 --- a/e2e-test/trace/build.gradle +++ b/e2e-test/trace/build.gradle @@ -45,16 +45,15 @@ task createDataPrepperDockerFile(type: Dockerfile) { destFile = project.file('build/docker/Dockerfile') from(dataPrepperBaseImage) exposePort(21890) - workingDir("/app/data-prepper") - copyFile("${dataPrepperJarFilepath}", "/app/data-prepper/lib") + workingDir('/app/data-prepper') + copyFile("${dataPrepperJarImageFilepath}", '/app/data-prepper/lib') defaultCommand('java', '-Ddata-prepper.dir=/app/data-prepper', '-cp', '/app/data-prepper/lib/*', 'org.opensearch.dataprepper.DataPrepperExecute') } task buildDataPrepperDockerImage(type: DockerBuildImage) { dependsOn createDataPrepperDockerFile - inputDir = file(".") - dockerFile = file("build/docker/Dockerfile") - images.add("integ-test-pipeline-image") + dockerFile = file('build/docker/Dockerfile') + images.add('integ-test-pipeline-image') } def createDataPrepperDockerContainer(final String taskBaseName, final String dataPrepperName, final int grpcPort, diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44091 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 F{tsc4gtPzv delta 41205 zcmZ6zV|Zmzvn`yCZL2%BZQHhO8#~-Fcg&9Mbc~K|cWm3~;Ol$NdG7h%`)k!&YyKRw z#+ak3=IJfO;vWboWjP2)c+fXQtR#GlZ}3TsF5mv^4FwVm49v;ZiU|Vje^;zw{r680 zz-U1#8Q37jZ}?87VUmi5;$`IAHwM3J76)*$;elZLtDfYihJ&5$&%4TWjItW@QOL0dhkI-FW>8> z({9in_p8;jdq?AsfLB5G*88I=x-Y-`EyM)D+gS@RyCG7j8TAIJ8P$TlHCOL=!n~>- zA6i-Rc1XaCmUDUt&daT+kRdr7ljbdY*J6TOV3&N~goe7zFm0D8V~^@km9t@AmBysY zSe?qPZkJ+ow;uBI=#ZdgxRc6_CYFbHcC>DnK_8zweJc3X5FggY z@kpn7*o`CBb>GL`dAF-~KH=8&2+Vui&q7R;(N_SBhCeJyG@tWY0-fuC)Q7irAKBf#nd|L7tzfWH+OFD5bI6SJv{Z?7vQW&-*zP@TPY_e( z3wlrW4jrxMUKO~T*M*%8LhJX_OUG@m;vze(ze%+M0=WjAP~f}!Z#3O3l_L@Ooep&9 z-)#Zd8Edw~7%jxD&*yW+B&hVOTgzJu^LUO<6QdQ1O&6D!_Sa*|I7i9|oT>KlgJe(G z!G+2nf!~a(c!V9WcBMB~b7P6vs);|e7ZWA3K78GK9VHI<6&}_GlEQvB*4rR)AnUvd zFIw|EoRX0Nm)Zt0iJh%FAEa{>Urpb!GB5zV>L-mw2CYEqx<6(*!U}R*6?))@j8cR4 z8^lrg`V8M2VeL z4c<=2&q^AIEMo)H#HR{KY%aj-sKat4uBxQf{>_Vs+ z#z{Q$Z?|&>aB7JDzNK->qBeVIng(TebkE>4JOYy&-i7)kq3N zTi|M~igpW+rWKyqGF98o_x_4~B6sYi8B8*5}$cPdSe>VZzX>+n5AyPYq@K8o;|`SJisL_Oop zFPBet|L;PxH)8M7eF+@zkO?d?l%OUtB}BGJ{J`jZnwNH<(M~!(sdp9-Eoaf0P)X~C z4ykw83G&FVZHeaGm3>_8iZQ2j%#QqL@05gd2q3``rIj3AGFHb-re}L>_ZfUbAfvUx zn!;X@+)$o-r9fyW0bJLUcn(0}b$m9~K{fO#)0fZj4h2}c;lkVK-LC`!c4*HBFDDV~ zqJ(v((*S!$uG_s>{I$D6-lBZ~4qkFh7BLKo{<26@g%sy#s1+U&aa&fZCyISfa!Ye; z8lx8u7228`qY)|LkKS3-&=2fPdP1sv_8VDsc>&d;P(*7bV*+JUJClttd8bOo^h2go zLNivF+6a=RJi7yF0fsmd;x%`c0rDI{_oVsFw|2lspp+SR3xLx?AgadkGO(htuVF~# zn{sTc$l`%X4cx~F<~~Rj&!gRdziL0y2qF@Q5ipp7Dee<94_r7i7L+)QtI3Me^-dih zX=a|vfm~rAR^)<^WpgXt5pO&;%)@HDSUwC;X^KrMXLLf*40+EMU5NgbK6$vXE~`#U zS)q97VNkZf35emWlFB|6L_DL6RbnpI928v7m|5Ug8Lb=J?I+7;xnhW7`UzeEs!HLy z*0rk+O~IVjr)OJHY)y&Cc9ZkVIGp6nMb96hVJf(9>^sA2nOwNTKK)5Hu(By+&)w>? zZ?2kv{&Q{Z-OXmU(lX8c+dYALO)9znsKTqy=_#FFVPC6#}nDNP{(bd7+Y z7Z~MduSnokvk!U1cB1L^6;31N}rWiiDu{cJu($Gx4+ zQ5TlK4d`lhyC5k^>J8}~LMqC*4H}rAXc#1cZpbe>V%-7Do7`4?*!|Jorjt{qVo@x> z3M{;dW_j^+q23aRPwr8nR_MVug8ziz=-HE_zNCXs@pyg(*Y$#DQ=`r&*OGQEA^(mm zh2;gE6>S%NxOIkaBnIDVmHpaLT$GGaZ%rVQDHlrh!tP0(Vj6p@- z1$a{Vr@tEV%8Sdnph>-OgyIty7Y|Su=KU?$$8CSwBY$HNUQBRxG)_cgX>)@eP%f`Z z^%@&MJmmS~PDn+4$?p(DIvhDjBmhp?`-&?A!z-#I@opfmE*d}w@mOSyI{+VYf~skb zM%hR0)p8+>wOK1xwy!*kwVwfgOt;5P4(N^NMXtCb$zIS*wvGgX)e$$NIOQTbt!?ad3xfjTwiVD^XNUtxcD=+Xv)RVd`rVFb)1dz7C z>BE*y*%Cz^n7}2SZGx>iGe7y^ zIR69TF3;`tSn*yJ;*=i}mr$hMN7SbnF01j1t-!Z& zY!0Ekj!G?O@^>@iP@A5LPxU0xQaGVNtzSC@%RSTKOkB$b?WJ1fP>id_ zQcYLkmD^L*7fK^;ujTzjylF7CYzU5jV7KEQ@ZX9r3Bl>VKUX;n%;A2qhiz8+_9*jMQ)c9&%Vl{~jRXQ#=rt0SXA22LWVsir9394GsP8^DW^S z^8czvax6@603lr849gFNHjGD6JA8-X1m4UTy%|MUBVwKzhCRO zc&M!Dd)aMftjn}xu&G`PF8Wu_#AJ?B4-X%kU*PBG9oFw3n&j+c^U`AKq6nnurnnEL zu+Q8;o-2f@a>#g=co@Qc^sbDQAG;(YWbri639qsYkcEhw0GZ8E30Gjw6kU?MVI28G z4TH`ErG|n|T3m?f;Fz!elDb>6Nz2OGyAy(34nsrCa}7%yhOefHHCjkXZcVc(KWM=x zxtZcIHpd8rq;U}=+WK?C+2yRH0++2)g;~pMUP2mryQ`E&l9UMt9$qJo+Z9p0zks_t z(`*7>ON|%~AO@5hsfp#+Kmg&B@X5z3$=*vTN$0CxeE z4Z5a{0*aU8Kv&jM+vvU~9Hhe@H|*Rj$QHE2%$zama8YA+Ssh+=F_VWiiw?8OS6GeI z@_GyaIPGzcDeFUuXRNxf*jPq{m7E0O#A2&r*i(yY9Z!TSy#wzr>}yelfI7|Q@6*qI zqyQxdM$Wb`50>4gpM^1jCkzkgR)M|NTFsTAa_&sCN=cq>&2>dMOOls z2G(T_Iw!02XKRFA_QXWw=Rb(n_R(v>mZRSQ$YZ#*ATEMOqV69X>`IVAzaQbQZmbqN zZ;H9mN=Zha=Gwf#Y(BuY9+fj%dOpP@7V;!CXRR@e?a^xN;V$XJ!Sou+ zAswis)G3`2HpNA%9T&zWzD1z@Ch9*Wv4L1+g5>583|`YC&nB(;y{q|f!R?z6WXeXN zQ~Q!c7cU5IwM=V0#H2~$N_Ew`-H-d*BCBl7flZ)^SH?B;DBBUv3o5KPSaFaU{I(~W z1o?I~8qMRDHOA!6Wk2|oOzQ*8e{I_TdnPNv6E6bAk%#~slrTh4N51?Rx?LHX%YO)J zK?c(~2St+(i{FrtV<{v`dYd#hTk&*XWnLD%puIEpB#Kka4WjHsuudD!xXvd-m}Ol| zPfmYYT6#JDykT*s==`O6UL)*lu01oyKU zbre&&;JqgFd1X=JWB?O3%;wkK4-T&fao66W6%(SXu49LBK!r*VW><2{#4y76tFr2Q zkI%pb!^ifAY)Rl}!#v$*njRw#huuq1JKbpu%hQ*29_*8lG zi5e0C(I}DW5YF7N=J9p-s}+C4UX;+1`RBNCgPOzbZDEqTzL~aQKhcPpRfyoMXX%o# z0hfOY1LAOHD+Aq=nAGEtaP~|}C36g7qitKB1Q#L^7w(bSsombMo2@8hEiUiX$H{%1kOpwBg+Sn<2i_$R@jEZa8^Ail`unF{hl9)M z{o&GCD3Q?}t5@r#m|+kr{DXe!DN>1)@FS*-!K`|IQb|O!RIv@am3#}#6n&tGX}UU6 zH~SN*2w#3tOwE8X!Dy1h&(nB*MeyL_`dC0<+3a`GV{1)A-959IR8oS~7+5ho7WT#* zWZY1099H55+EOfE|E7T?vu2m>5Ae3bMEd;`^8$*-^((As*n48qd*AehzM3ivs*|cIaXl(XcCCT zL`M=keV{F*itu~%6#Ph~awnx2VAvy`fMnyKjbfiuFqtLDBfcw^nv)xz&(BALtHgELdfuz;7S(u*v z$2@Vl+97v01=XJ2)?%}#EUk(>>WD$1#<8-6#K z-KwD0x>9M|T?_hC$TaG$C5CCE&8K`Rs%S-z2$81auD(vg?}<2Z@DgS+tLN8qGE1VT z2YQt{Yqc${%u1D?Yd~sBK2MQ<6}zrizzwN1KwI=!EpoDIe-lq`y+O9tvtGCK_2_c) zt`DyiT^LbWJ2-{yhyB$8aFYtS2-J@!9hfE)tR+jL{re zrnV9nMfN(8N4Ubu8Hx-s$=PiiNfg8`+q(;Z%6>`NXM>`!XBm8dQNIDpXQO}h?Qpuv zSjK3Qv&<8?knZD&g;O_TAxH75H`T)D*mSQIT6(Y~&aumidR~J*tdVmYzQP`nv#$SNu_~ea7#XoMUZ)(ZSS+4&arxSequ? z+wcn$DOz575-_lUG{}%!LsWYg)cjpe&))OlaRO?A8mLY!MY1}w0$0Tf9kr;m5=QRt zgj0)wr^IKjS}%VG@|&M}g8=Pcz2$O5BebTAd`K!2L!@XbT{c+a!i%oVT?(Cg%_#HL zXz#&K-@3&1Wn6}j=0>nlEpcub$AG7?4=l2PmfhO&wB*=bh#V)~4+O#h z_A0+b*)hy@iEYU}DagDcp+`1vFebEdS+e=-jK@K$9w~PeR~ngh=a7d~D?eOn2_@Oy zJ$0H4MnNfaoKU7GRE83%Vl7=K0XGPAw8+7?rdETs_HX1Ic)Uk{0Ks@SdIE zKM}R2;2=nVPIaj@mT*R(DqQjC5p^v|oQwMC`c9>1VO_H@cP@J+^RYe5ltGYVJ(8~%~Bdxt{|Vam{1({TtIZT$23Uvh&u%2|%6-bG!Hh0|7y~>xETP~PW!RfG(&_`C{xEOc8ob4fcXfzvk+j90w zijRA%2gtHSrJ4CSTwSi;aVVv24h_B8iVe4Euyju|^Rak@%)D%G z9i1Y|4tUCF%FL*+M}5HZ;gtyP=_~loV#iP+uQTO{&*oCbT6;IWhexPC;Bl0xK*@W! zH*a=j0wBBm3c6+`jQN$SG+J|a4f(p?8%a%hDQgUKq?#N-ShM)8V>LJv(wWl^`5z%a za6X{1Cx2|5%dSwnNKrqF9K>jBa`Cq6`&q9NNQKEr%MW{e+=Y)VM?Ncud z4a~1@&Z**sZ*obr5-#E=$?m}+e42I=)y)z$*mR7DV~NPcY#x^LAp}>Qka+N5$37A-y)=2EIYN)o7GPXho-&tC3%*eJ zi6vYe&1$F$l);I5J&qk_S3C#$7HV5@d21)qNP+&{eK7nefWmaSBlBOO;( zFDyAs{jvw(zYHPh{&d8znYt!ibhbfKtA^n?>rKbWAV2Rc#l}QR2Zt~jRO`re zhvC=BUA(92MTD)UKvyegGquK6pNq6sDydGoNcovWi#JVMyS-4F>Qwgo&F1-)&88bm zm%o;8de4F`O%>%T<5ZsJWUNB+&;nMVWz;5v>!85*<}(?nIwaPH`111p-TN0 z@@%9LAoht+Pf1WE@0N+$6qz)&dnPlOxj_j%A9Qr%7b2du<>?t97N}dlfDyG(Btp_q z}ajcy)!%nWY z+HS7)3i|cnpEO^pWiIh&qBh3aD~9BLZGkxe%cx(&4leCmRmv<^#*z{K2m0<-9rIu6 z={H7B;P~WY&bu9N`p~e7Mu^9t^%cpU_L+`~*jA7?PBOP}R&Ro?3#!AOn0s^rKSE`| zsSbi~J^TvDvFemibPNjN)E|Nu!hy^C4}_d_9y_o}iY1>S0p!LUtuvy$Ib$ezJZlc1 zuxxtWb5oBIQy#*}GajabJZ8Z!}F~l{N^cz_p3RJ$K z^}#x7pJKg}lnR%K#&*&%wmW$Y0^m?bQq``*zY*#o4}ZXC%S7x%co^!~Dx|UAS$TNu z{KS!vs#E5IpI4vyMgaS9Fl*R(t`7kCx?7_sF3TC;&CTwsx-}a|4-)p7#H^$txWbf%m z?;o@ikE>75U<9MA*F)I3s3k%npvLDOP_0Z+sW9%Q^{Tj`M|*MiqN`iCLhl0ph|ANg z{g5(?Q@9tCB0*MyTcyhbcM#O%JT%|H#*B;Y`DtxljSww4d}%m$Q}Nv^F&4f0`DH7L zat6s{aOY>zmDX&aOXOFj^Q_`__hb*NsLa(5q))EFz9y1aq5ot*^1+_Ml7DjR;U5F? z|4%s>^ufye&r?`X#vJ+bBG_EU!lR8$kQZNrXhcdPDTkYmz@^GEX71C%S)Rx{e3i5A7I@rnncv$R2$3U?G}^kYa2NpFv&aaB zfCnDy^p1Zt8_w9X^%w2Zm?3({$Py`{U02Z4yz&c@FJTh(%px^%c@No&5w&!ukkoqi z2sm?dYI(9Z4EN_%eZ6t-w{%mkM%^Yn80KITCmPW-f6em6)aI$nc8m!*W)#aXwMnTo z{_^q%WaBt6;ty#kC9kVG=8}wCh#h(zP!9YgL;kVc`J+Sl?|I-j8eRMev{z--bk>0b7k%6ZjWPz(mdTb!hh-R^|j22rPQswn#bXD`Wn6fCTXMGcD5Ojr_wRL%;_DkJ7g_)Z`3z z01iL5e&Yjb{=>#;trT8uJkMLty%(#dl!hND&tzqOa+zBEj4vQ#i%)J7Sq?Wh#%!Y` z9Wx7{oq0kX!wDqq5VH-N6gg74+vo@LL&=rNDQKGeO=u+(!bC$~w9OM6K0Ab3d5F*n zhzzj1XYW91+3cJ9Lx^A&&okeWKj-N@03{)xMende&pR0UJ!I)<}o<5QamJy0d%KrJ+ZXZx|g znSYmvsI_5>5+_@*TdxwYP8XPG5n*|9^4iZkbtTvC1C8cVt}{!rg5d_OxhZg29--(= zAkY44^kdCaVgAT{`F|`ibx1iu&=rU3k7Ad-Hu4ls{c(z78ih@{Kf*NK&NNsOSOq_z zBxs!oMnJ}#41mFIZuHTLS!P? zZj4`V;l<3CDpVR}PFJlts!F|wtB~#xQwT%3X!W({p8&aNnT%p@V=Y!ZPvgiqJ-TcA z#6!P4);Wi4Lpy6_+QNU+yLD%t7^o?Hw%8_9bOj&|DEB->_a22qx1NVLQqgzzuz%)| zOiCC~ZeSIsaX$ggzN3=Ill%4J7&s40EnJkvH9TfG{l!w9P?WipWZjMA#QTJ~ zMn^@*V#V$Tv;4}`)YL2kvA}S53P$In(bct!9iVBe#M8Cbo|!SZV5UU!`#dW2p+7`L zN{;tk7+L`dKG*FCbq`yt{9G>nB z#AWUxEZQ*?;@`;_b2)YO{Fji?2(cdOp}spJ{yDKcYQ{bEAv{L1{riibb#b(3`Dm1t zf&kq$F7a)WZWs$ST~^V|ku9?Jh?i2Q%J1t=bQ~IEze`cg7Kl2A#5N1-{8FFbMT-(J zX$@i5Jm<-{rK%Kd$WMGY(F-%;>Cj%k=2#>mxog=6nET$q{vCbXfQJoFzt3FaQqPX@%V}mB#}a4 z&P-O2-}}Z)XQ~(irqK^BONuQ)FC*>77e_^^$?dN(sD@@ox{T+`DGykG;KECWvPcl2 z=7WJAsHwCe;Hx?6+3lHoX1GJt%Ztk~Kk-A$ ze%*N@?aBw5B&{-jU74UZ&=}8llU1Xi)8lVYnNLk;-maq2;VkZ;udzy@R=6E(zcd4J zfqLWu+ynZ6y8mwq|0KZlA9kNb<40woWevGj0^2Dsh0cO-JIFDdKiFQ+C~+Ni9=<8j zunb@$%f-dFdE{G6%<|iiQ?q((1T9ys-gA{-e-tLT7#@{Mxb_elWENGYY}!5cOUC$w z{v-wFsZETU3J*i)Mg7dw?KW8%5N(;6v3`b{&d0dbAI5I6xv2#_g_zKBTIf8-MqlE! zSiK!J%w_tKLX(&wGT~D0R}Q=EBbIy-fAk__d&Zlz>783g?~jD&t}wqd!v(}ZK$SQk z+5+ng{L&J`O^OA2E#~q>JIJR5Q@fQfTebpr*h}14>-+`g%9c)5Xx$quPCAO@q8Ww1 z!3nyWBPEw4o09|7*sP@e$ti+Ke4m}E{sK+r4^e~AHb}<2V+war#M4OIS^c69c*0sQ z5G#~U+JoBEE}Ux+j-}l?y&UDa!`)poYre~ne#VnLLHInejdl>SO6jzF;yj1sR__Sf z@Uuq1Wc*y^C$djwC+Sq^^uxV$Ox29U$;@0sc)&UZz`yDqWRY3+o9lcYkS22^N0kd! z`vpjf{XSGw%NQopRoZh2nkhn+hH8~jRGDI>R$b&ieV8a6Qyb)8vq_%7{X+UrRdHY` zmQnT%46FzO42=8#nm8x|(h%?ggiX2v-rrFEw@6qx{47xg+7%QAd@25`V|+gO9*(=D z=t7FCpv5#xO{fg!|G>ACkAr&t~1Gs2aid@Km=4afhfA-jer39B!;eW<8wP8N#+#fAmPvXn~p?53HRg z!-2+3gZ<1eT9NMQk4ov=tcWCxBgJ90Z>L!pLT*LC5urN34ew!l29rB&Gl1dfDMG7@ zArk7%cX)?! zae0jTGJt>)#1gk#%hC4%qs(Q)kuD) zq`Z=d36iLCmd}D=_DrayCHPaUOB$Sg6?bwv!v!6gsXLrRRFM1p-z9t*&tOtg1Q9kFXR}X^dn_4S8GX2hJ1)9evNS ztF)K8-(%V7hF!viQFB!Q5KGTmEj4z{?W~W`QTB7svxjA`zuyTd*Ao9k|JA*sg{GGVQgX}O<6VY?YNw+oEa3gW@iYU3{!O*)G`J*1TRrCJ6bnRiN z$1D?RgcHt>d?R4(BJ&1fV#dIzV)7?PVPw|yfnCI&ct*T)wk3?t)ih?M`*2KUD*0c= z--l16Q2JI$8w^96yrY@HuNuZhFDYqPvor;pEQP;g2x1-+g3 zWc?b*OafAK35lMoUhMDSSq~b90BYlYaj=RxE?~a*-ctKx2En~VTZ$f-biQbsarU7! zOW{J_ibpIy19bdjUdN{W)2l4hB*?G=o-w?{I*}AaPnMn04F-@x9zm^<$vl9cKOi6i zb2JX42i*-u2#FQ&*K6=c6!rv{_Jmj3Pk!Lnl&`6s6rJIrcjXFzu4vG0|3WO{T!2TB z0t!G5w1whvBd1N@@_zqNBAwunzZX1ck4OLh8(m2vER9dWOmx08d>w6!VS^+Aqn#pl zmvL#5G{Wzo;viU&Do)`E3;Mj;*EePuFJVN7qDz#ML+>5ZAwKZ<#O_N1q#hxvI}CD3 z;%MZ@PBCDzE`?}2$p0N6Ki3ligC-aeL6ej_tk7tdaxL|3;6OpzyCb2L=4WKX^?$*+dYl-)}nuy4rm;->F0~-Ek`g%p(B%g|NpLZrN=l%M7KdyKrdT zHH6U7sA8^*-8EKlOiu15Jeicutf~h`y>bh#c{Yt%(Oir9$UPp_eIk^zBAFo4$*_n5 zV!MRVmkGMrZe_T863xoK(GD;U;2M2xRTAH~cLr!2^M(Q?n9v|%l~ z+;g!6wCRYEBt`^xq5k#V;+MOoNu(Ji;AyOvTeYD-@>!mfa_|q7E&oEvGJXzq<8a^h zeOu(RWOfixK*PR+th%MOziQRP)|I#@us6xCZ=JN~|I>R(h%~)nBF?QHcp3I*Z?~3R zEuh5b`dXw!?ox!PPzt6dj z%Yejm_yzu-;BcPOMgski3WG}}FhbyR+&E!ss*%bE$NXQ;MlBM6!A1uIv!?;E7-0Qt z0zv~Yn%SGUx#jBE80!xueadEKa{2nUSgbV)7~AhcoTst0)E}w|g5k+=rZps?Oltck zOA^mSW}>xli?;Qn#iPa>V}J)6M?i+On!FGg6F2v@=|wPIcITd62964nz)sGYYCC>oxUV8Oj9GgaMDK@AHbizaFGjGW8YD>JjG1Xpr zHBW|>1`W;fIh{ZrJ)dJjgENZ~#^Z6?UUp@Sya@KQM&0F?L;hcFnjp$xmG4m*4Hmn` z{Eov=`$yCP@@3)CSC*13Z$6}+j0slrM$wj+lZS|eTxY5AB#6|5fw3N;^XBfTZ8IN-9iQr z2RJs%nPCna?ATwH)9^!kQKeT7i*{1X^RnOMrdq5gC$m<}&BniZ@P)FlnQbeCyvIgO zZ7^@@g|JkuR96{ow*6|TH5mddb9kID*J!U(!&aXm8lqQUDTehgu{3RNXmnF%{M1Z( z4N+0iTPqQe!7G|CCkQbp9xZ4ALOiPw5;A(NT+bkPTuM8F{ysD6)@8->zzBrH%lqW6~nOx={ z-qF%jQQayR2WgdNk8kf}(*hY&!=ZpoanOqRUd+R28Yx5P`72{=c8xScFIiG!=3X@# z#ffh~b@jH|_$g6RkpL5LoU+G8Ruk($EH4oHBP*8uP#Ncuf_&D#aAAzV<7pa+7S3!_ z-UQR#g6`Ut)Dij&y9nf=A(owPv)VQpbaE;S9iBa6AgT{_0}F4rHjP=Y&C#x0Z@YRi zz&khwK_eRJ3{Gu`gD0I~LlFwu9$x1Nr#tm-N8pIq8HKl*oI3_E$`w>>DJu+t$3aF% zn}N)sWD^0hv07mx!l<%e%$Zk3BkHDb3lZBN`#{et71vh9d~={e+wG6E6h2OW`nXBuYn&*?P*`1A}$=H=yT@cddx}^m;W#{d6vjbB_}Q}un{X@(8g(-?-m z?9i-bJJ56OCgGx%+Mw3Lbp`paCuSeoiaGXO0yk(|@$QHyX#Z|N4>gjsPI&2PxBo7V zj>BP{_Ar=a_o)q8h|s}C;-^ZnyH=jc1#VDyQN_8}vo&todw?hC@W}73CE$wnP(7Px zr~+_ep!_gd*~n?5k-9jCNaqc?m2}hUf2-~Doxo@a zjAfwlpwmV0g!F5vNOz`B@=w7^ZA%FySn~VayE5j^lSt%;LtjL3oi%do0SvFVmWbt= z8Fu;UZA2{lyUg#kzBcCoG&$kdwAUc=6MgC9NB3uAzmhXF7Tcu#* zrhI)Kp0ZJ^T|s??^EeLYzwU%6WIFlNpm*zH7JM0E<6wzQcC+w#rd9)y2^+_TW9-%T zu1q}s%LZ$&6=33@GG>AErF3vCD>k5_Efr~#FqzM}j-8;RCJr(Hs*zd19Y4|6CSh9I z=x=BE-HI#Q7CHijP@qQz)u%oVDB=`y+p}pz^dJc$21(=)u`Zem(6Q+h;81VR#pR$AEN|@efy?^sI{$MESkbf&^08!z1jsM#2Gz+h( zc4q`*xu%iXQA$;JDeqQhd{<~{zl>mUfW%{J=Ozt}4u?6vFoq;=aKfI^c}9e#o3V5svdV%>dntpsbP-}fT&c0{S&+q%$Sm$BG0j}T6AiBA2xD-h( ze#`BF{8$>60XR30dzk3@iXk%CA{xK->g#bpcK)0eXf)ASHQ2M@?}zQ9djhHQRv??` zA@@p&^IP6xOoE#?Rd-PS;GzIE=_9-9wMd{)={>cgUobs+CZAayt z9!M*tOkeZk$I(3Pr>ZB`%2_6LU6y`=?{V%@5fL}YXsl}g_ezq~QJyadKsJu;n(;QAx1DMZUY_`^>;EmN`uV+=C~ zHsWDy0Dm09?aFGj(LQOH-1u`nQhpKC$Q&)1@wj*M7>#whsk<}{@1eYXI%SvzhNm=D z;e0q_J2=jSLgbj?;GxQAAT-}0&%@|MoBGxu*MXQm4d6G&9bPR+Xy2CyNZ{t0kw*LU zeaQOkZ~(JJNECk#)C=x&xUzulGr+T=5k3AfSLr;^oG}aXk)N;UcVan$1c^8!d}IcxzRuy zcfxU3@NrL)Rp~>n!W31(EYH;a%ZeQ01GRvfb_X5|1N|c##@h`&gJNERz`_ik2T*O7 zXIlBe=4A0E2Y9>&zj+H)N~F^C7sZS;6L}YEouYllnXkY8z4E(T_5_MCu}f621q#4pil`Yp!!)(kE z)kH;S*ON8;F_YA27)NNog7v2ENBlb_Xd5Z#%^5rVa^N8Nu&{4p-X_jP9#|!ngOUt z9EQUYrZ$9=^QopfHC?k29N3+|UdOP)kA3n+{v7H5nkBbB8{)-huOqdYd1Wt&v~KTl z4s}T33hi3kmu|*O*q-F>+KEy}G#npQJxLkyhGSlk!=q@b|8AF&FLGoR1~~l%!gJ+v zI|v-!BpF_F!ZZ4cJ-+#tH!_mLGil5N%#VpTLX|U00q-Jjx^1NIqiaO0ljRGf;k(V+ z0InQTbdl8UDfr}idzGcGlF|6_TvCj?j7UY!&#=OHsdnb?(q}-2SkNt!7rN|IzPyD4J%KBaBjK0b9GP?%WmoJj!n}xVz{(< zeadg|y+Jz1zZxYEvunoc)Uw_gSf;jiueq%zPL#%8{Cw3cR*-Og2!APhc9)pcrW*H} z=mvXtfxqoQA#p6fIK-U5VxxJwXytF%m4OC)s&jt!@?d3VC&3VBx^&T>OC=E7wMOpO@pl`Lv-7+p3Lt0L$pb!B^=>G{$U@6#Um#tH%X-FGjL-tnf*KuaF* zDOd2^mM6J0lWo>7S^w=AfZL70V3S%NxB8Z{i?xblT-m7Gc<8E*%0Z^a`7hzk^Ax4v zKzj$qu2C5Qrv(v66k~!IRwyQhvB%|$M=BFqr(chS0SRIYfFuRCnh39ye#S z#(1)F%OFKTKx~E4Z$C^4O?Ec&GVGVz>lc(`%Ua7iWW{#w=eg?EzMN*hwF&#wmV&3T_CeT7v)u#-YlGO5rWvC_xMcgAT-kZ8!C5u6t6>&f0@H0cF>Fg^(o z=XT|`#rBRg_2ztFUG$2Kk*yhxeP&#0EPBk%T3~vuV{<|-wiP!)U z_TLy;*EL>~xCK1Kzj-qy>xKU_Zx(oi*Tiqz{(r)M7a4#M?D796;1$=uUmN9a_YTb zA#9JVKbhoo#!a&p&FS{Cg&;nIm;}pF{y(nHfjg|IP1|YA#%XNZP8y@JZQI6)ZQHgQ z+qP}n4Vtg-tXbchng6iY+0VZ3>$%S8Y1z$R%8|x=p{41xP`-|nXP=Y#zpsD(;P|nm zP30jEU|sfweOnoH0Ha{$47Y})a({CjAGDyGN#nHI&jPx0IG8Ggho!YgcZuoQjBruc z9oGiY!TyDMrTu_EXnf$VfgnDxvZ}AwDw<2u|>XW$rit9 zjY3G;7^yvn!WiEAD=E*snB|FKTW_W&9Jf=C971B!7+^eQp{O-8_sC#>vS(@SF|?gL z7UA-#4r@CXT43iHL$jCj*tR_F! zbZ_-=*!_~lwJOVR8hF+Q49^;z0fovBw#_`<3=x|>80a))$Yp105DKcy@S_&yjQEbm zm*Dx1t%Vv~wI*xG8L5Sl1gL$hu8^F_!c z-wd$O?8T>)88kd&nA z2exm1ewS8pz3{81BO9x=j3qj{lJ%86T| zr$O?V3b|k7y!nBV!Qc0K-Axgz3?%%HCN(F3hgYo0&$#^L1P?vj8%%vdhPQD* zKSqe3mo>U3pG3%VzB-mMW@p@8ryDC?`CTp3WZUa{#ok_3=mK|j!8mUhFWa@z_fx>- z+emyLjPqQsG(p4Q-~i>BYg^Qky@k-ZvtEtItVt7)rr8#b)1=;2%M&u+Fqt-^mYIc_lPc# zht|fhj&Ij{-LWzRh~BIa7&#V&TTcF(l|J;lcqip3tyort+fku>CNstiMkUB6;gi$F zOD4QBp0;^#{=jWA(>T6a10~wPh=c9B!p=CW(i{biIEhK&507iukzHlW1($hlla^d( zN&)h4Y%c$5Rd1r7;}R!xQrYFxq~k?r;yPx(&)n8;C|QdsOK)3%{ zC9e>Hwbmv-EHkdZKut9E5L_;I4CH;fV;m&04!*6YtONKh&KMJo65*5hc-H~BlBaq+ zL#PhWD;f@Z`LDu9n;Rjrtlsqw0EdIBR)R-NpOj-|`pcnQWE$_d=>`36Oa3$kQPyY1r92xz@Pz^rFM@#sB%q z4!p5WF~GUGQ#B`^Ii?t^(GaBGf-F^k1QnoPH!MN?E8%{q#(!y(qx>q zEt{#yF+CM4pwwB(O_t>qWsC;?ZXB>0ar}GNqq3Ui=|jrdt;z_3 z*6$`z3d6+(#$Q^pgR*Vmf&$88=zmeh@d z!q%oZr&FSGO=+N1vOCf-xreW?y44WH*^#|IZCV|XeG(uUZmBkr#F(cK5(q1MfH&T+%ISo@ zFemi^tiq!LSe1ro`z1$%2;-px2h+^4dd@UxwMLP0x!I8yrrFxFW2N2d$Qb*=fk*jh z!ZRs}Qjsmt)j=Du@JZk(_l+l9WW^vjT(GoN60hq12;=32c>z4I*AYe_l_<+--T`vx zn*%=M^uOK4FG`g0tj58pPzrWgyk)DYH3Jkk6jdMqyX>{ev&ycaz4XL9oD=(=y~o-q8f7!NcC}yr zlW+cTxe)f&E$^KPcIC@DwwUi5;YEYq!OqqCUK zlJ7R5xh%e{%!7#m9!hIj``r}gUS%EEN=Lczj;mgE6)wJTYL593(P;uj`bb74?zO3B~Bty6rXl0gKdkDHKbH=w#fs$ zCcmIJ3jb1j7W}=9q3$>GHz({&EF$63b60DQ*fQ!fkMqC~KlPbeD*Ix1tPzfR1B8Z`f^5*#K4QzDt>S{Ady9M3C-=0ARuoR{Sctzbl?$jP4kjE zV1&T#rH+}M5^tWKLo6^llp!0(;gkhBGY=1BvhsquRHTwecqBWS3PkFzVBW|EbM`}m z;)23NTxHwRu(nHbMINupm-fvH{+xPvqt=F0}WB82Xw-=*ZgdtU{heaSBt#B zoyXAt|J;Z;7RG`eCymv9s25Fm@QxaNkHr~-wkRFO{Z|{8&uJBnA<_pkFY~>1pW~X1mDacsIawZOZPGyQf zjt+n1R&NIo@*jJ#o0^(KZo$x(WSSAU*KVkup;-6D2X9e{KBe9BawEhUVk$4#^X=0! zBoC2*sdA1B_goSoJ^iB6yX#WUq`xTZZj1|jHhV6o#y8u-Nf^R(@wNJsk}oY`6ZyGW zh;vB(#>G^sBl>Zl!SwHXFc%z(FFZt_{@*^r4L*uji^Ey+Z5naYL=W-%KnkecBHp0>F`>a!{o>YhKTsecLpR4F=Wqk_sR~l7CH1$u3p1K z&0!7bvlq|MZVEj_z98rReD}etA3;QWMM7>mXVw(}TLX|t#D5eXn-Ia*Lx*T!YLpmd zVf`A%BK|>gkA+e}B6ah7!CeELyoHXTRD_8wv4EAV;@kQx8JH^SSpiN#ja7#7*r`aT zNY&H!S@!|itoKDC0`9uN#E*^Q*iq}r&C*4)b90!^lG(7f2yZ-w3JwMxe#$tZhyOEo z=LA%5^LKQOpFSr~QxsC;^RK?rCeq0k&{zkF83gM~HvHMY5BLT#|MA#;r&(ao1*1$KGlU zWi;w-){**OkB~lPabwSLB5ixAg#1!-F30xgql`z4!3-Ii@B%AXFG=^5d7MZCbkM6Q z&+D$Ka3G_H9&d*^_{J0dqvAlKRzK2G1%-U)?DS9<3e64BcxyE7-t#Oj;hS*5lr5Pb z>e#o6cPh%=AP~VfL_cyf(;`D&UG1kjIrf=x90!j;8g-LdF%5%!9jYjLHG1e~>`6mO zCRD@AW!65O)Ej{p2lM>x<-r!koW%(;r9auh2FxU>(GEFVo6%G<@46$5aK+^7Ns7i$ zO`fJ5y}~=;KB&*ukJblWaNF9`9*_&`c^#)(rsyQ#kHD&))m_@24AW5Nf>rbp)s+;mamf;o#eD*wz4p#8LCw+rG1AOIiV<&|E(=*7737u1O?4Uy zKvDBq`EiLQ7wK9+(LSjjb%#uizU$oyPUfucut9c!L7BYJWcah6#N+D9rQYE>LvE3|7))0aE zP;P5rkN&=$@~vE)bO@-fD8n{+B~rfK9$o_GS`>+B0&&r@1e?s4cphJ9d7;_+3xPZC z;GTS-za%p>RX4M+mj3$#M{BgXAg9Vaqe>veJamH~x}(1dzMYT;2jo22^3K9!M!fDrv>B=~RiX-EUcU27?se?)V;@ez09*S?4(G-v~- zt|TV(~dV3J~n*DHm&fggBXSD$Dm^5A53@4TE?xzU+a(fR$FQ9X@$Ww}Qv=6hHotpzo zokFZ^uqzeXC;3a-H^g_f0eJuzhv%tFxBO8uzV6VEaM4N#li&S6XgY_5)#jn7Yll)9 zkf(~NIl^m&B`j56ne)$%3M`PP&J%}RTTxHsi%HIGUm3iR#cy-E*(qbs4|@4>YS z?xo`^6`fK?7s|^j+jP#jXdR&{?8^9x9_mNP$t2V_3LdrUm@4kddzgtBQA1J2TFP?8 zQD_To7tus~cx)>pFC##K179Z>7J)2wxw09l3AVoRy}$GBZhI4#;o#(Gp6#$8?aj^h z;#Om|*UIwqwtQVFj&-)zqmQSNax4Nz$fPkT`{tzbGDd8%G@q&MQlW)8hS`1dv)$_K zb_WgG=JxcUAd~_d)71LrhLeh&7iCjrHCO{)ztL<>b!kUOS2cjhb9oNu`upYaw`WLUPIMExR(h7rT^*5f^kCHW$hIrmgN^mo^3>1O|k2grkZpE@D)49OI=lWb4N0*@h9r8ba( zLqYG2>YGiW7_tO-emP<}Zxx3#M(j?ASGbq}BRpy2*kwRl+P7QD)Afc4a_aaDZ7-=I zt{m}th?`1gFQ|Wo1|HKTd&_@j{hedx^l=BG!L0EBtrohM_x`J_S3^^>tPL6oHv*ny zzT}t<>o&%+uPDT3D=xtL=Ma2jeGiBEMD!pIhe$z4QrYQV-7JA*RUG;ly>-!~#AFIz zr9BXJ~ob6njN782OIoCtpeC=4-c9{ zodPA7j@m@}M<(KT~x#`cEmqgzPgEK#^|1|U&y zz(Z-?Bfkih4dyPKes5PQEIqZ*HyLC_MSzL__q<{K%S#|Qjl8%>ishPRv2dm)Q$KSB zyOV#H?VUQTx7)$i4JBR*kGF6d`WQ3(a{nWKxv{NS#Z-bME7w16|DMI}Z&KVB+1zI7 z-RFm&SdSc#H^*?w_bN-N&2U$82LyhYXcL*u>Kuf=(qR9j&8WR!Vg1Vu2p@>QX2Slg zxKr?zJ{bAaT-73iayb@^ZCj_o!9?{hO~PI z-oh)Euh78_^hWMGG7R<|H=C;FLVQ`&;rQh=A!JCx-JIX>&XJj@6K*&qBTzJ8xL4w5 zF@|<-9A|$YsBysL--uekMJ4;0bs8Oc?7^yj2WrhB%q-@bxmf#S2*&G3W_p{@1v>3c zKuE46%5OE%SM28qnf$)g!H+*^3wt&fo*jW(Y7lQq`;2%#KAB+5zoB5Aya;1>%mrfm zrm2jOIQ)B!-!L+bC(J-1K>;;MH>mzzALa%gI(KZ)%tA<%4)v6X-UxqZ+W~p7eQoO> za$>f1G~pYHLWOQ;BiKEI3|1d}fBB4me(?K)WyJ*8oviFeL$bL{?nP zX!gsP!G44b7bOk{BilyX7Zg<(XhymvO$RU(j;HKD@-&7k%KE?{ZhD_xyfPsM2Uxi? zG4cre9*8+qygnFCi)-ICKMgC+iFhl@64BqtY@8!m5wBO&Uh zMOVowavKM>o`N8hldzvq@9fXgAJOJr^wMz-gVElhyaqqGBH$QQNfMtefkj^Bo7b}a zzAAS?X?rwSi8qS%hz=JyRPam8X7V_K4hL3mtL1UOEW}iI3lGY^r?=$OWAOr&=#kyq zPcQB8iwYxkbps7#mk3+mp-Hvebn?`yBpg{{i}9OyUaEm6JM6H`P9DQ=xk*HOc@%<8 z0$TA;gNI(@d_X$~t|TZmJqtZk+QAq<6*zxM&+bQJQ$T2BS|&u=(0yNze#B(JO9JA_ z8C#9q!I~M^Szugt(u}y;Q;aH86(zI=mrBSRrF9P8kNus32mm^=C?GKvBgmsZ^}DRXJC6Xm zVnZ@#&|8A+?`VOHIDW5jBf|qRqK)4_!&}7;_gMukSiVAs;o*^@NyKyl=iGnsRt*`$ z7hKGohnw!?WR%|ifGH^dU}G^NN~J3P{cEpfb~lX3GCIIloYzFNYpM)zmHTmx)@CZT zj-;|2c@2aV@$*veNQO7|Z#)0G1zM!duORIM$Dnu-5Zt_;9m8Ho8p1 z;%Nh?gsR7Jmk<%ijpJe0-28@BuN z9+qmIBE7YsD8Tq+Va(b*hHt(y4(Cqd0I&ygC(%Cx@66za`R|lPcPMl#@4Y%chXR#+ z?_43jwi3SZ=VyCTyThKP@-7KExbEm`%a7XMP?a46aE%VV#h7=1@_y;i1me2#os2!K2{k2&0B)Z(jTTK4^V7J zvDFlJDnqBVC_HM)u`JiY&Kp`MIcaXs+Bheb{bhU;=A3y71I2A3mp4K-Rk-%y)<;SI zPP@}Tqx9GmO%7%SJ`<5)7Vh&Pm(T-VQ<+o<;pm((K4P&afRNJ{DLc!R2$fF8ah)gH z6kk!UL~Uc!+OPViaCVj;sw>|k1B_TdJS7U((AG}O?!#wyREuF!Gs5Ug*&|*yVSUnZ zaiRshV!g#6?BC2cs(WbL4mUx4O1rX*;HxWq)Z3uDIVDy;gJ-AaKV)tmIQR8((qZ)u zOO2RowCrT(|GOzNG<%$)D)#EoWg>Q9QmL^(s~tT=L(Z@40(a8$hso7U05-l__Hsk# zJ#4u%O}n#o=AMaKQ}d(VjQNC6ixU{l)F(_}{q2Kn%I4HK(i^vGvLyO_oLS>6Y!47~K%|*JP&lb7dtD2cih1 zeTJV;@U;Aq+a)jlO5SZK&px?k6SWY2P)P3m*o% zU@L*pSBy`fHejq#>B#cLtl)a-O{um8lnti1WwO0yl7kws{A_1lKoi0Hj>JvC!OW@Y z8w}E4C*!%7Gr~`jEf5*oLs4hcTyc&EX6`{YXBv)Kn@*H9RsCareIs;~bv+tXs`ijV$0sV3>m`}JDxTSPp-P`L- zu-OUucmi14+Hp3vP)Fa3W_gMOMW25Xl=`OolK#0kd)Y;zxoHh|fvA)kyy3l?d_c=0 zR5X@Ccs{+T`5D-7c2OHFi?#j+V!=$0iF zlX4pWayMA@0RV{w5pd%=KQ(@`86&Gn(cZVr{|PSetL_cujU;_Ic$;y#b`)-AdTh;! zEp5A)K5r@Fspby-2%NJ{$m2Eljx7_uvmx*9ZLlyQJJ$u) zgIGvNhPH3%POzj=ufhe+^ei-Hh;G3sQ`IJosu-Nw8<4FX)iFl3#YW9c{-Hv=@PxtH z^CRCAt~({#QXfl5qO$I!71@qy)VyKoJYl#cjl#;} zOxvkA9f$6K?d3CGnLL?1;~pFW^xw7oZ@qM^yU!g$U=cfb*z#7wVCdJ}tiOCt!SQ%g zXx>#>y8;%n#lGM_?x+h++0KRN*Cp*FBAwMo>gW_>7C+tQ2O{G)zjQT=@VTCREaE3D z)M-8gJJDR`Ad7B=8T(~5g};JwI)sXW5}+P$Bdj(|!lOVkcr&4u^~Ug^07%A3o$VQW z-ciUNjGmSxK>h1K%T?yh@$DY&t_urS_n#KGLkfWVFHj><{}C%uIc@nL>D2T14~R;? z`j29p@CmC?DzD-{c5bgHtVG1Z+5gd3eUSkTs{el367nygBK|L=qwX0h5&jt($X4_4 zMpMK4>b^8JOP3}@fM^%kSTxM^f}u+a8VCt8WQAeO`Uc?K>pUdxf!$Ga;a~j*Sb|YfoW#On5kJKLTs#x%jy^=XpA7MK z289~&ZW58iq>i5sOt`l?Az$}T{JF5js_msoKU87XjrIy~M@Z~u4_m3PTA6SM+aBey ztLW`=XF=@_PPMBR0Jp0YK(jkBAP#$Xf5&6q4tE>FG^N>h=RmxPzuhMM_pgM(_rO;H zB!Z;5D8#$IP>pp57=rlvZ!Z&vd*Wo?()}TH8Ox+L=gEDAj5&lYX`i`SB?5leBmm|s@KV#XG;ZgaZn4cmjph*LZyVvDVl&%n>y z>3A?=)+`ri`wyBkB+Omidx1=zd&%&h%2g$Rh)#>IFhET&e?`Tj%z|-E0m>o<|IJK9 zu115Vu01J*p5?48q|Z6iMaiuhc2k9iz`y|MS9L?TZj+KzbWAtW-d)`Y8Aho^-g+&o-5=BU*&)}mQl z*TPbhb3R9%HT>*U)BR$_zT?%3`Q48K4+@9Hp)R zldE`3NRDnjA_L2A6d6#>%L~OpZ9c*df%ngF2WF9wT>GmJ4RD32q6|50PO=qY)9PcD zrE!NgqpY1;P79EG@Py(o+%=e_e5FOqJ?NS_fP?K0W1xCv?k3)=gzXM#gzb*8lW-U5 z+Oo0aBYH7LTgf&SyU=sQj(nI5p!U9TTR=ASN zjK^1V6y#Vn8k~)Lxb%QdW5F;S)qC_1Ahx?6Njql^%(GNSuDcp=$uFdDojS5wYNupJ zCs--yCaLPGQ{}T8T2&k@*9cgR$!ax-PoK$kYuY9xGfk}(H>~L5Ni8O|OmjW?=@#9NEBV>u6P6{dl>7$XcrASYLxZA3J~&jtraWbO z<*%~bu3FOW%I8(?t>NFnh(Yc9Eegxk7r9J%K(W)N&5$9zI$UpdoBlDF+i5^QC29zI zCYudkX0yj$iHiwX!x^ZLExUf7`SrtiK^Qt{Xd+V4da37*&D=yo8SMj6`(FAKy zz*jj`mKPgr5OMy|8#{FYUs8a>^V>@#+gxBk zF1WLpr*}(!)>G1$84_+4fMjAp%MIpX4~xG2OR})RlL^6sHod@&;p%O^)ff~9XTB^) z0L}(_`|*2q17Gha#LIB z42C%kVsh><8z|~(zupS0r!M3MsOT?CNl8ipThC=mrJ+7R>Lh zpbg*td48b_#(YXKo`cgqrv9bupeoc>tkT&~@$hq|%aYO?keq)0deA^afFxv){qo0# zLnWtQAo2HT*7E-0x>f>Y2MmQfXuk3p0}1ZBM+yYhK6!zDT7Y z&(q3`XU)COr#ai7hhN|RH()w^7 z<_nLLSz8{t4>{RupARo13?O8@>`3CWA;sXqo$wRPbf=W#4*xoAGQ3K7v8e;E8uv9$ z4EXYD;lDXrvID@nv-VvO{iV~0jfbJuD}U`=gmYd0(Nl(sg{N$nSmmQVdf=amYmT;y z6vQB(qWSjQarzgZJr--@naUq6*t%1&(&_}fzK+DaG)2aq7Rur@9M5H=iz!M!YPCi> z`n@LHbdD+voLa{D#EW5XBXL!%Z9Yv?a{VRnHQZxWXp!)LE=TRhBD`r`zmdVH6M!+g zv##f7!B4he(0owgKXLy@%?0|TenM{RY%yt=gmpgJ%F<3@%F&;K*QXRyE*X2Y045H60;=rb}X2lAS}#A-h6uLHE? z+)=u0!HJ_8q|f=jqH^$66xI~+06LN(SXizfql`eHmS2V>rxMGWb&&7l;Q%v(dD2Tl zTkWi`@Cv_RWX^oBdF)Cue(e~Ncj{jz5%S!|Gnh%mB|o#^PRzBK?0+wXje+pJoWr!uKBovkwI+5}<9(2;iK!tnn!qW;8vfCel)C{qp!*?t zL*8FbJCnuPy|E;!5=lUKW@=1B8tkTQf`8GYe;&HFIa*au`V94lVL|>E_+*Wd(EcA5e5Fl=v7DR}zag=w-^{zB^Xu zs+oI7NY^f&k$6|Y`}WSb%-7BXZ8>W1ZGxuD&@ZK9{lB@B?F(~PB!Tu}8E(3RCv1+3 zCfpGN`#@HlVlLrR>x61L-)&kFE!ORdJNBQz>`XOtoldDrr*H=1*&|AB`JD!KE?w-& zsWW#bcl7~#^oGki;o^!zXfB<{SrU1XL#nJ+tJXo?^vS))yomEUIf(+0?RpFUbEGI% z3U$6M^ylSC(6nST9On>^XpxTZb#k{HCb!-Jobx$Gwd;d8?3vz&f(3ucT`;%kAL49= zQ&UFxd$WM185_H4`HH6E2?O)^l`IJl?^>YL?#UiF24z>!zfNJ2t;_ zOLdJ@3-}Gm6IBza>e>wenXQzPDYIHz>$T2uhozLMXq=3hZS@upIwggLwqw~wa>LnZ1TDg0rL*cCbDgxn&7IIAR)xVX}0BrD9we*a$_x4VA}ELKtJT+vo^whv`I#7%kPb z&iuR$y>>$r8M;XZ8uFaliL~X1Bs@nnzNx0S2;B^RBOHa5i20-q9HvCYqU?P1IA7b(V3u!d-JSzxqy- zObKY58>fB-dq|~91S*qrq)l^f%l4%L=fW#?^v~)!{OF$ptS+U=&~DF1mi9`#-;J~R zf1TG0b?lP?(F<#B^)R$ahXDq^lda`~yl`>6c=}-^c;@nB|47Ivsh117Nz_J*=eL%V z;FZ);9I*B&;HX6zdzn-Z=i*_oTy;lO7h$?duBrFMP;&BWMRJP;B2T*Vn24b%!zSD* z7i=khS1^BiR=;Xq$~XN$hDtc(=f$lyO`&n$!qXB3WyMYQ|xE0j6+<%PtG$BcfAMzQtdu^V?avs(eb3z9Ps&Y#m87|*O@w!y!SkjY81!_9?+)6px;Dw4S`!xYH(2vnMEOL+FykM zj+PCuG^JvtFT&=ig{{(OEBG-qH$pjbX)|XyBc?{M%O5;{Amlb^%a-$*k{-4j8DuO5 z{28I8cfFva7B2E(>$1(EWjW>Y+|K(sX9dakD;Dd~>MdGOZZ;9pCxD*q^uRe>5Ugn@ zuk!VQ^EItPcg?v=P;E9DQb#P+>H{JWu-nB!=nl7Ae#P);TiV@tbq&1P-B1YZMqK{^ zUC)E?mv{#P*^x{n&Rj!FExLbfm*FlInaa`3DGL*gkmhEu7V|@;dtz zNO#!$10k?y>;=0s^@iou*H<9J2lC}%3KDUaM*1YH(6uKB)xr%%BwID0Ot8T$zF!+7OsN;sK$axJ=BEQ#}!37|bD^C?>R;u8D#S|THEZ&N z5{M+=IF)@X=-WBI!TbPM9^cXeimhQ=Dk<@!+}dprTr(JLM_`KU=hbO?uiD8ipCLM? zvQ_J*8Vm(DqMjv?kvGwsJ5G{^7gt1yv70(%aH7$(F5a#fIaU+Hj0(4Y1mXHPrhv_z zvx=M)pXATxDa-fSqZm?%crki}TEk?()TP7(3Z!z=ck!*Jhl%GC@WlTDBFM4~ZF8V8 zCh6h=%Wj1F=8Y|AQRM>_v&WNIvVNO*wNJxSRr*QS$)ZOZk;D5Y*opbDM%Bucr{TqD zu5pWHT~jHe56rdD$20yLyKz2Py9{qxKC1e5l1oqY1w1vph3w!)=6z|wbHvddT4Alh z+gbF`|6Fa&asDEoX&{pV;Q!)#BPe&<-6Fn*LVihr{9Ni{Him5_wKJREC3uhvnw2a5 zvj&~tkX+cu5GK}HAa~c0YnBpS1%lwKkTT`@MJ#`yhxsApJGPrj=9&@}&18YDyf~s1 z|NV%IGI1|3ottZwMVk_e=OA2Oet(7r`)hnxFVdjN-BA}Fy>Pe(V0vx&s2J{Hd42hS z3j7)c`XA{_yIEh^{xfvQO4hHJRVk#)G_b80F5tsBW*@7;WGc_%%Q{@ zVe_PujBfW@AU4u|$u>py<Q^RFcL`JGYQ7Qkt3m$CjhA zBXNevnK#CBlyC4Z{j@mVV?!CC!?#H~W7(^a zsuIuvpw*dhyfaoBZrZ$h)5ips{E5v=IVPj60S@=MU8qKXUx&nMoL_lKf#Tjq`p3Pr zQ_T73Iy@PLu#{mu&k!Bmnx5&A^kRkiGTKgXo$arWiLCgJ;U{?Dm_&wHb2{M7mSqkU zu^y=^nZuo=LV|KSz=JefK%-_(Ot(Vjls)kRIDn}nG88qy!&Sj-^t^h!FlRA22!pKV z;z(%o(4JF+{MX7+A!rdhns6{#xN4>g&8;0p=A3LcL4_D_9Ya==t0|DspMMYi`p_Ab z!N?Z)A3D7;~>91ev$KzOXoVd=mT zBh4}BN)pXsbOO_1L&<$I_=Ssdf+$qw`c-l~QM!3CKWTL$k@rl0o)zWmX{^biE*GY+ zYTtoGj|>#WaqM1|7RYxyCkpdN#lpP;kQTqJM0c&E=k5Duyiq1^@16V(BM~_KRM;LC%yP7w-Olfy}TQQ&ZDj+z&0AQ9YK!29j~Kn)dT8zH*-5a^kyWDfMo?;W<$>ZDbQKl>Ig)}?wR&UHbVjt_#^yh zXuEU6T+D^AC)bqr3B$_{&$E&`?uIHPj^zcuOLI%E@i`9peoVO6t*#Rj3TZP{S|kMM_gi|1y6Zun%|j=>SH zket+m<4rhn3IVRtfIg*i*(dyBpt)K(iXEcf)lifnZvhH5S8fE%+3Ac#S(IR?J2HQA zYt@B`El&>)jANj6Lw}n+tr=?g3ha;`a?Ve zUZvERw6yT;lpRrdIv>gciJJ!@1AuPS^~8J#nXj7U3*egD0>8&C$PJj=R@fx zka|C3;gpzE()$EoQc*^j?T>5G#C4jBZUVv7>8v&nFBz<`wsUd|jky>ZlDv z?x8F+tO$XBrh`_FNN(|Yp1wI9W$1UgZ_J=|=eX+5$Zv^#E7X`b*hgjjofnUE=)JHO zSbQ-QFx*cXXBTH1)QsbaoEABrZ!wO?@5-l#Uw-;EZMk_A3n<_7f6VA`NKEWm5y6Q4 z=9q<_jbUi7i=>k43%@lx!n*Z zH>qOy==+^7J3SCqTR!ngKUL=xEY*1`fLVWEsI|b#TgW=?!kEK}FTI1*WRbYgggSdv z^{~Uvdjea;6=K~1(Xk8q?(^v^W;OZ+o($epAQKY`{z3P-*nps*pxoK!@&C%Y>VPP= zFAPgaFWpj0OXpJ3B_-V@-QCE7F4DP(bSn~qG=i|Sq<{ee3ew#v`CECu_u&2ZpV>Y4 z`_8>*X723F+G=7;^Cx2v zpofw05Ndaf`x3(>YLg_gk&L(K>noi4cZ5#nNIu*vl>UL5VHdj`=rsBtV=2i$?qQ=H zJ%aX_+qO?jgK^TC$%5aYi5enylZxznyZRoz3aciraf}Z?Py~Z|a4> ztRj6GYW3uUMP=+!pEs3(Px>Y9+3C@bXFtq$*BA7n4;7X&>&q0@YF*Az5#OoRe;d3j zQCQHQ>ptQH-WnE2GK;8b37cD0BcJ6Kpxstk-7K$pZnfyv{+7(pvA$oHW}^)034F~BIjnW1zHPa=tz;z#tBPGn5en>96Q({~VX{2~}ZX&&fEUKXu z$)rK>?3*TLEYlm^IpV4d;s}5G-1i4`IguK{6nT9Nu53!nZ_T4=7R zawK|$Hwxzs!l+trmUaC4Y6y90t_D#l zt}R>L##%;u&3Pq#1x@GE9G>_kE$HMc7HI7(JTXzcjS{E6SS6nyl~`RMKs+qjF>p?q%$>Rw64;Nu1& z_h3WA;CPNa?Y1VWyW0{Ul#1~m`r%g*V39SR=9>h^`yVxFdESpdH+&HC&~uGgvUdFo zTS#L(bSsu%yG>+%RwjT;VbDu*=^~Ex!vY4eRBruE=Bb?X(ARy20-_RN>P^%j1sJ|WLAZnv!5^^6D&|`&&*`oJa5&Omugi;GoVEuk zguEl++b=X4vjr(h+jSNOGptz~A#=>xulQ!zvpz=<*{)20Yv!kYPX`l0PT8S})KVuv zQ7q&YTaSUVkXkqK+pCR@^xcxE#qf7g6N`Q68(n+f&z)^g%R_s=*!(KZV(&BJ6;wp7 zoqHX6#B0jWX;?$LnEVD~1uQA%<@(6EwY99!zBfF%+Je2O$jlZf99vpUqoaZqzEVnl zYQy9fSB4(f0lIjA_qVi@3lgh1tgIImf4wx_BXJho*Nebb7uPJTd~DnzCs3O+9lo z)v#%3WxkTyYU&Q3^5o@|%A-DZjS{U&)o0JSFa0@;4l1JZ(Y&vpq>@z>_hq@cF7p_N zJpbK^i;TilsWl9WPa|ch`klz!L~2!9*pQlZQ$~vbvRJK_Y2g+&|L}gPE@MCkv9Mai z5_NCe7C^9-L^7AxCF`>bsB(vaM3!1N%@|0sq9ywvi$NTU$Zsqh8sQIolF+6pLsy-yf4}wO0 zwfB6OS3T+^8#*&DK>$fi?5;&J_dSP3NmYp4=+a!NJQtAD>i^?6f^zI0X3`+2DlLcn$^y2*#Xqe?Qb zwy!a&6iVr;e8N&}YSp;1{8BC36=LW1e1~4>wZZ#W%7Xoi?tBlJ$aIHZzcC#>aEceb zD)@oB#4qBSHkG>ce(tEd@g81C`}PZ&u4lfaD>--LxHrF;_?0{4Z+kDLumPWa!U8;l zkJxSNk_q#w%nUBLf1ZYOjuf2XYEpTL1QNaIysIWa&LX1Hq}nB?ZKg?zquT}vTbl8b z1v;!}&0Xe(EF4A0?yC$EY@faBTuSg zo2!LW9RkI7ZHHQ__~4E!k-I!Q=$4-qwnZ4&J@kvl8B~cOermS_5={7JkYoT6K&V40@h7m3*yS}~AX}({%blE+ddw{Vw|x zD_+f4eqyhv13Ax!cQD>7p7}2;6)lqn)}Z=-@J=2%6&isruxQie3ryML|(X z>h#uAyzjw4TQIdpA3dJ9jZ8T_Vv2YsdGI(i5lAGo-<-*aQ2^wJfHh8B7)Qr>Rf2#p ziHHD}%p}n>0uJy7Ss$KEZxr-%2gEAj&}OoaX*@gfyJ7d}+1a!-T~*G`p)PMP<*)jb zbrI3V2n-vn&04r-#Sfoa*WgZRqCP83GK{{?3*h8xeV0Bm@ECKlk2grU*g&XH(d)ol zGK^&$c5$hqiz5gbIo9mSqee-|m80<0R43@q_b_VlL2vOVP9RV~3aZ*5hRM;AdF_gr z7}1}T5fHpc!!s2V7ai6nC*9Ivkbx$<(Gy=Gajr-8x<6hrUe@04XXBHxS@@#kxbM}t z4!aBGCj16My=1*RYUOS5g1Y0(BujKwyLTKI3HGl2eD;J`I}59IE`{X+oo#5|{(|UTkh14$qjwk@ElpqD5_=ep&*Sk}agsXyz3IQWXIb3|skPPU)EgxNi? z`L*F-?n=pJx5|NY3*q;O|z%SZgu2EANdZR|3bI6{1z&n4oIh0qDm}e)4uwpOZTB0 z6VQ<>axrB8io&!F%aN_twpBQm?+K_-(6P*_$Jds1dP$bTR_l-oAwe1%Xh zbv`Gq;+u6}ozz5+7g_OAM;#EvbX^$$g?QiAP&TdWgPxkT5boRb`jn z>X0B&QblJg?Abt;0m@QouuBV2SSw(9n&TGq(RsZtIY8>eN@Qc|&BNiVL}e?HMz=|n z|H5&Iq|t|&SWU=C|5RN6Nt$CK6sI~mqmG6y0GaO%s&YS_E>kaxppq8)gw#+b4ILUX zACaT&ero^^YM$%M@2RoWB<44fxQVaR_=s6P$t#gi~exJ}oV9xQ`#qle)HJn zb7TRBOlFTopp|b=hxt3P`CbKt^|-p@CIzS1bWSZNNUYVk<6NCTv!Sm1 zvC;ssH)fm-(%rLUQyQq}qenA8rGXv+6ubfMGrg%UQ;Ny5Erh9eSJh(~@0{Ifg0K@v zq+o%W?9xSkRO3o;Tb2|CwhQrXM$L(o!1KC%+IkZ&v8mNNotGs%z7!2^r15fD$|g9( zH6$1ti552GR@YMhaFW~@%zjdA;ZSdByQ=jg%^@jkKCpk<%|IYyECu>z)N^Nw-Zy(p z8?8yfB21lMErWA|w>r?=(o?4a9a5(`CivazRqY`C5TE}xP4$lolRdW8SpYLTPIhi> z-Ipjc&gJQ{2`g*dpi1Z=iP34ZYbQM>p=sGMqx|@4N{*KV{#zCt&)|XV^hN=@Rgs}j z$5a6i%0~fDaSWe9U4V*lSePR7xb&Gb~r82OHDmw+uOh)|l$~vkX z8<@zZSg7d6#U)wf%2lu0^J2_0Luj>lF;K8xrN;ROp$C@z7}(Hw8T2%Thgu0!t$~Qu zvhOwUlC!@evc`nIv_8xE>5|{ zz>(0%G@F5GrIjX=opq0 zj-9l++5C7CXzP>mfikH!pz38)MgFq4R?S6Fctf*j)aH!u+J)%dKBKhCU<*>k0dw$D zX_@~s@c!%Q!q$Q{$X>|cbNsAA;eFBKfqVWjby7v=oXS`~L?10sdpwwK_F%FuG4cy7ZFn2wv6!_>F$$?9#e*65L;0g3SSDMOlqJsOtJhDvpVQi& z+j}x4Ki8ZAqAX#&;$xor;=gVOb5(@H99X zMZB|$Ne=`M?JN5N1EXpZe6wHYlZ^1(aq!2}?W}S?i;g8gjvqAvzvZS?R-*9AxAN`_sjOcvulWCBuBA%&ID!coPWSF-B}Fjs8*EYwiA6Y} zTE%$PkBJzwd>k{EvdVSuME`*6;VV1E@kpOL`I109+z#L8?ns!tfGdO3*vM7)kb%rq zI@Jo$26c+{)l`%xu>0|ytqo*;1&=i-I*tR4ui3m?qO_xL!t~cYmdL9O9OUr_A1!ez`lSxA4ulh#RXN zSH3H{i+6RBrycs{=UpwOP6obgXd~@8=BVl(QM_D_=ONY{mw2dfvRSzr#-}p-)V?j@ zQ7)-sP}k$~*L^5+I1H$>JFh65Tk)7krv+j-BbMDE$8QZ>v%Bm22iKTLP9G{_rSLh` zl|9Kc^r1<|f~++1_zhIw4MrMg8+V)F61iw8VS=!=tyRfI#FhL=w5Av$(4z4o z3A69FnxGstPEba7zeFN&eu*ohLHaD3P?j~43(~K|Vp@a()!C%0JEfn1hn!@MRcbD8(35V#>58oTM<{B{>8n37Y~N zp+0;4DGI}f%`9YEIKhS` z;_$=V73@RiPu%9bffPUg@V+aZG z50z#W1YjqQBOt3Ad@VjHA_$XE+UL?`xlM2Kxs znDO0fIQ|+P-oNmd6mo^I`)j0tE4n|Wh?QgXxFml9K|;bnESEx(Mv9G5BRYrWt&srs zK-a)Gj8ymj3#w%=Gg3o5uWchz0xI09TM0(Du5qK4!5dm1N6-=PMG26QByJ&pv0uAy zTxYle|E~`F-;AO#M!st}B|jDH{s#ID%5SafQbb$25VhQ{0VD`R-eQBnHmGmF|0_fM zZw;*AYxv~`=?(ZT3D>`|h;30ASnL{3DnS6t+q&7HYav%egX$5xAWcdOecH2cM`V9Zoq%**Zn}ioe)pe zlDFXA^4IX~Eyf%0f5q$&hwB!+LJ0w9fEjP&AWljeQwp5FON+uVjA66e46w@UqW9Y% z0ErdCAb`MW*i*o`cd!5=wlJ|B8H6EZ2L!;ihapBd{+#c>d+_NF9@S5WTiwgY+rjyt z{`DIJ-o*ppJHmE$sW|?7BHqe#{&%Yo+oCY#Yl6tFwCDe2-%9*^iyh$tv-m^;L*#)L zVC4NPXZnB1|Kt}&^fAPA>3=>&VR3GM$aILPxA#DRWAAH2LLmPC1#l~|=084u2QcaL zhlB|W3S#;{;&t*oME4It99)50M3=ySh<8HC|BrYpZ`UoNMaUl_2JWqp5$i8OfZoKv z3t?_)qzJwFzb?#hL{VeXbcn{c)c}={g&6hVuV-f{{+`{1Oohcf8F5Aqu*Cuf@2UM z3Vv;%8Nh`Z9Mjxzerv4fmUEZ(KNO;Wf*wG4s_TXA9D{Foxb?2knEN>Q{~fSf jFA#1K+D2e>Q|zP /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/gradlew.bat b/gradlew.bat index f127cfd49d..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/performance-test/build.gradle b/performance-test/build.gradle index 6e9a8a5902..0a9c1ce2c2 100644 --- a/performance-test/build.gradle +++ b/performance-test/build.gradle @@ -5,7 +5,7 @@ plugins { id 'java' - id 'io.gatling.gradle' version '3.9.0' + id 'io.gatling.gradle' version '3.9.5.5' } configurations.all { diff --git a/release/archives/build.gradle b/release/archives/build.gradle index dcb19752f8..327a84ec16 100644 --- a/release/archives/build.gradle +++ b/release/archives/build.gradle @@ -13,11 +13,24 @@ subprojects { apply plugin: 'de.undercouch.download' apply plugin: 'distribution' + configurations { + allDependencyJarsFromMain { + canBeConsumed = false + canBeResolved = true + } + } + dependencies { /* required to resolve below issue with aws sdk JAXB is unavailable. Will fallback to SDK implementation which may be less performant */ implementation 'javax.xml.bind:jaxb-api:2.3.1' + + /** + * Creates a configuration on the data-prepper-main runtimeClasspath. + * https://docs.gradle.org/current/userguide/cross_project_publications.html + */ + allDependencyJarsFromMain(project(path: ':data-prepper-main', configuration: 'allDependencyJars')) } ext { @@ -111,14 +124,14 @@ subprojects { dependsOn tarTask.name file = tarTask.archiveFile.get().asFile.absolutePath bucket = awsS3Bucket - key = "${destinationKeyPath}/${tarTask.archiveName}" + key = "${destinationKeyPath}/${tarTask.archiveFileName.get()}" } tasks.create(name: "upload${platformWithArchitecture}TarWithJDKToS3", type: S3Upload) { dependsOn tarWithJDKTask.name file = tarWithJDKTask.archiveFile.get().asFile.absolutePath bucket = awsS3Bucket - key = "${destinationKeyPath}/${tarWithJDKTask.archiveName}" + key = "${destinationKeyPath}/${tarWithJDKTask.archiveFileName.get()}" } } @@ -135,7 +148,9 @@ CopySpec archiveToTar() { duplicatesStrategy = DuplicatesStrategy.EXCLUDE into('lib') { from project(':data-prepper-main').jar.archivePath - from project(':data-prepper-main').configurations.runtimeClasspath + from configurations.runtimeClasspath + //from allDependencyJarsFromMain(project(path: ':data-prepper-main', configuration: 'allDependencyJars')).runtimeClasspath + //from project(':data-prepper-main').configurations.allDependencyJars fileMode 0755 } into('examples') { diff --git a/release/docker/build.gradle b/release/docker/build.gradle index faf10b0b44..be96705aaf 100644 --- a/release/docker/build.gradle +++ b/release/docker/build.gradle @@ -12,8 +12,8 @@ docker { tag "${project.rootProject.name}", "${project.version}" files project(':release:archives:linux').tasks.getByName('linuxx64DistTar').archivePath files "${project.projectDir}/config/default-data-prepper-config.yaml", "${project.projectDir}/config/default-keystore.p12" - buildArgs(['ARCHIVE_FILE' : project(':release:archives:linux').tasks.getByName('linuxx64DistTar').archiveName, - 'ARCHIVE_FILE_UNPACKED' : project(':release:archives:linux').tasks.getByName('linuxx64DistTar').archiveName.replace('.tar.gz', ''), + buildArgs(['ARCHIVE_FILE' : project(':release:archives:linux').tasks.getByName('linuxx64DistTar').archiveFileName.get(), + 'ARCHIVE_FILE_UNPACKED' : project(':release:archives:linux').tasks.getByName('linuxx64DistTar').archiveFileName.get().replace('.tar.gz', ''), 'CONFIG_FILEPATH' : '/usr/share/data-prepper/config/data-prepper-config.yaml', 'PIPELINE_FILEPATH' : '/usr/share/data-prepper/pipelines/pipelines.yaml']) dockerfile file('Dockerfile') From 33e29b5a21870852a84467aba43bb2bdc6237368 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:37:28 -0700 Subject: [PATCH 05/12] Bump net.bytebuddy:byte-buddy-agent in /data-prepper-plugins/opensearch (#3297) Bumps [net.bytebuddy:byte-buddy-agent](https://github.com/raphw/byte-buddy) from 1.14.4 to 1.14.7. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.4...byte-buddy-1.14.7) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy-agent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- data-prepper-plugins/opensearch/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-prepper-plugins/opensearch/build.gradle b/data-prepper-plugins/opensearch/build.gradle index 74e863f3f5..299268adca 100644 --- a/data-prepper-plugins/opensearch/build.gradle +++ b/data-prepper-plugins/opensearch/build.gradle @@ -33,7 +33,7 @@ dependencies { testImplementation testLibs.junit.vintage testImplementation 'commons-io:commons-io:2.12.0' testImplementation 'net.bytebuddy:byte-buddy:1.14.4' - testImplementation 'net.bytebuddy:byte-buddy-agent:1.14.4' + testImplementation 'net.bytebuddy:byte-buddy-agent:1.14.7' testImplementation testLibs.slf4j.simple } From fc48b0e013a0e68c62803711322b9e4d9d2eba03 Mon Sep 17 00:00:00 2001 From: David Venable Date: Wed, 6 Sep 2023 06:42:52 -0700 Subject: [PATCH 06/12] Updates Bouncy Castle to 1.76. This moves the dependency into the version catalog and starts using the jdk18on series as Data Prepper requires Java 11 as a minimum anyway. (#3302) Signed-off-by: David Venable --- data-prepper-plugins/common/build.gradle | 4 ++-- data-prepper-plugins/otel-logs-source/build.gradle | 4 ++-- data-prepper-plugins/otel-metrics-source/build.gradle | 4 ++-- data-prepper-plugins/otel-trace-source/build.gradle | 4 ++-- settings.gradle | 3 +++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/data-prepper-plugins/common/build.gradle b/data-prepper-plugins/common/build.gradle index 6ec3272bd4..1d64b0c66d 100644 --- a/data-prepper-plugins/common/build.gradle +++ b/data-prepper-plugins/common/build.gradle @@ -15,8 +15,8 @@ dependencies { implementation 'software.amazon.awssdk:acm' implementation 'org.apache.commons:commons-compress:1.23.0' implementation libs.commons.lang3 - implementation "org.bouncycastle:bcprov-jdk15on:1.70" - implementation "org.bouncycastle:bcpkix-jdk15on:1.70" + implementation libs.bouncycastle.bcprov + implementation libs.bouncycastle.bcpkix implementation 'org.reflections:reflections:0.10.2' implementation 'io.micrometer:micrometer-core' testImplementation testLibs.junit.vintage diff --git a/data-prepper-plugins/otel-logs-source/build.gradle b/data-prepper-plugins/otel-logs-source/build.gradle index de1900539d..45e67098e9 100644 --- a/data-prepper-plugins/otel-logs-source/build.gradle +++ b/data-prepper-plugins/otel-logs-source/build.gradle @@ -27,8 +27,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' implementation libs.commons.lang3 - implementation "org.bouncycastle:bcprov-jdk15on:1.69" - implementation "org.bouncycastle:bcpkix-jdk15on:1.69" + implementation libs.bouncycastle.bcprov + implementation libs.bouncycastle.bcpkix testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation testLibs.mockito.inline testImplementation("commons-io:commons-io:2.10.0") diff --git a/data-prepper-plugins/otel-metrics-source/build.gradle b/data-prepper-plugins/otel-metrics-source/build.gradle index d22e42968e..59591e66d4 100644 --- a/data-prepper-plugins/otel-metrics-source/build.gradle +++ b/data-prepper-plugins/otel-metrics-source/build.gradle @@ -26,8 +26,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' implementation libs.commons.lang3 - implementation "org.bouncycastle:bcprov-jdk15on:1.70" - implementation "org.bouncycastle:bcpkix-jdk15on:1.70" + implementation libs.bouncycastle.bcprov + implementation libs.bouncycastle.bcpkix testImplementation testLibs.junit.vintage testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation testLibs.mockito.inline diff --git a/data-prepper-plugins/otel-trace-source/build.gradle b/data-prepper-plugins/otel-trace-source/build.gradle index a037076e27..ca39952cca 100644 --- a/data-prepper-plugins/otel-trace-source/build.gradle +++ b/data-prepper-plugins/otel-trace-source/build.gradle @@ -25,8 +25,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' implementation libs.commons.lang3 - implementation "org.bouncycastle:bcprov-jdk15on:1.70" - implementation "org.bouncycastle:bcpkix-jdk15on:1.70" + implementation libs.bouncycastle.bcprov + implementation libs.bouncycastle.bcpkix testImplementation testLibs.junit.vintage testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation testLibs.mockito.inline diff --git a/settings.gradle b/settings.gradle index bbbcfb471c..43a1c5c7e6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,9 @@ dependencyResolutionManagement { version('spring', '5.3.28') library('spring-core', 'org.springframework', 'spring-core').versionRef('spring') library('spring-context', 'org.springframework', 'spring-context').versionRef('spring') + version('bouncycastle', '1.76') + library('bouncycastle-bcprov', 'org.bouncycastle', 'bcprov-jdk18on').versionRef('bouncycastle') + library('bouncycastle-bcpkix', 'org.bouncycastle', 'bcpkix-jdk18on').versionRef('bouncycastle') version('guava', '32.0.1-jre') library('guava-core', 'com.google.guava', 'guava').versionRef('guava') library('commons-lang3', 'org.apache.commons', 'commons-lang3').version('3.13.0') From a3c0b0708a0e7db73c513ed948a4946f9d3e3d64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 07:45:55 -0700 Subject: [PATCH 07/12] Bump net.bytebuddy:byte-buddy in /data-prepper-plugins/opensearch (#3298) Bumps [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) from 1.14.4 to 1.14.7. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.4...byte-buddy-1.14.7) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- data-prepper-plugins/opensearch/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-prepper-plugins/opensearch/build.gradle b/data-prepper-plugins/opensearch/build.gradle index 299268adca..8a87f1c926 100644 --- a/data-prepper-plugins/opensearch/build.gradle +++ b/data-prepper-plugins/opensearch/build.gradle @@ -32,7 +32,7 @@ dependencies { implementation 'software.amazon.awssdk:apache-client' testImplementation testLibs.junit.vintage testImplementation 'commons-io:commons-io:2.12.0' - testImplementation 'net.bytebuddy:byte-buddy:1.14.4' + testImplementation 'net.bytebuddy:byte-buddy:1.14.7' testImplementation 'net.bytebuddy:byte-buddy-agent:1.14.7' testImplementation testLibs.slf4j.simple } From eff31fe714f951e45384b82b2db6e681fa963a21 Mon Sep 17 00:00:00 2001 From: Taylor Gray Date: Wed, 6 Sep 2023 10:25:31 -0500 Subject: [PATCH 08/12] Add metrics for the opensearch source (#3304) Add metrics for the opensearch source Signed-off-by: Taylor Gray --- .../opensearch-source/build.gradle | 1 + .../source/opensearch/OpenSearchService.java | 17 +++-- .../source/opensearch/OpenSearchSource.java | 13 +++- .../OpenSearchSourcePluginMetrics.java | 50 ++++++++++++++ .../worker/NoSearchContextWorker.java | 14 +++- .../source/opensearch/worker/PitWorker.java | 16 ++++- .../opensearch/worker/ScrollWorker.java | 16 ++++- .../opensearch/OpenSearchServiceTest.java | 6 +- .../opensearch/OpenSearchSourceTest.java | 14 +++- .../worker/NoSearchContextWorkerTest.java | 51 +++++++++++++- .../opensearch/worker/PitWorkerTest.java | 66 +++++++++++++++++-- .../opensearch/worker/ScrollWorkerTest.java | 60 ++++++++++++++++- 12 files changed, 297 insertions(+), 27 deletions(-) create mode 100644 data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/metrics/OpenSearchSourcePluginMetrics.java diff --git a/data-prepper-plugins/opensearch-source/build.gradle b/data-prepper-plugins/opensearch-source/build.gradle index eacbd762c4..1ffb49541b 100644 --- a/data-prepper-plugins/opensearch-source/build.gradle +++ b/data-prepper-plugins/opensearch-source/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(':data-prepper-plugins:buffer-common') implementation project(':data-prepper-plugins:aws-plugin-api') implementation 'software.amazon.awssdk:apache-client' + implementation 'io.micrometer:micrometer-core' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2' implementation 'software.amazon.awssdk:s3' diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchService.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchService.java index 63f6a48f28..4c124afd92 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchService.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchService.java @@ -10,6 +10,7 @@ import org.opensearch.dataprepper.model.event.Event; import org.opensearch.dataprepper.model.record.Record; import org.opensearch.dataprepper.model.source.coordinator.SourceCoordinator; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.NoSearchContextWorker; import org.opensearch.dataprepper.plugins.source.opensearch.worker.OpenSearchIndexPartitionCreationSupplier; import org.opensearch.dataprepper.plugins.source.opensearch.worker.PitWorker; @@ -42,6 +43,7 @@ public class OpenSearchService { private final ScheduledExecutorService scheduledExecutorService; private final BufferAccumulator> bufferAccumulator; private final AcknowledgementSetManager acknowledgementSetManager; + private final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; private SearchWorker searchWorker; private ScheduledFuture searchWorkerFuture; @@ -50,11 +52,12 @@ public static OpenSearchService createOpenSearchService(final SearchAccessor sea final SourceCoordinator sourceCoordinator, final OpenSearchSourceConfiguration openSearchSourceConfiguration, final Buffer> buffer, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics) { return new OpenSearchService( searchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, Executors.newSingleThreadScheduledExecutor(), BufferAccumulator.create(buffer, openSearchSourceConfiguration.getSearchConfiguration().getBatchSize(), BUFFER_TIMEOUT), - acknowledgementSetManager); + acknowledgementSetManager, openSearchSourcePluginMetrics); } private OpenSearchService(final SearchAccessor searchAccessor, @@ -63,7 +66,8 @@ private OpenSearchService(final SearchAccessor searchAccessor, final Buffer> buffer, final ScheduledExecutorService scheduledExecutorService, final BufferAccumulator> bufferAccumulator, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics) { this.searchAccessor = searchAccessor; this.openSearchSourceConfiguration = openSearchSourceConfiguration; this.buffer = buffer; @@ -73,18 +77,19 @@ private OpenSearchService(final SearchAccessor searchAccessor, this.scheduledExecutorService = scheduledExecutorService; this.bufferAccumulator = bufferAccumulator; this.acknowledgementSetManager = acknowledgementSetManager; + this.openSearchSourcePluginMetrics = openSearchSourcePluginMetrics; } public void start() { switch(searchAccessor.getSearchContextType()) { case POINT_IN_TIME: - searchWorker = new PitWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + searchWorker = new PitWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); break; case SCROLL: - searchWorker = new ScrollWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + searchWorker = new ScrollWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); break; case NONE: - searchWorker = new NoSearchContextWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + searchWorker = new NoSearchContextWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); break; default: throw new IllegalArgumentException( diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSource.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSource.java index 430f5aa618..681e22b075 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSource.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSource.java @@ -4,6 +4,7 @@ */ package org.opensearch.dataprepper.plugins.source.opensearch; +import org.opensearch.dataprepper.metrics.PluginMetrics; import org.opensearch.dataprepper.model.acknowledgements.AcknowledgementSetManager; import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor; @@ -14,6 +15,7 @@ import org.opensearch.dataprepper.model.source.coordinator.SourceCoordinator; import org.opensearch.dataprepper.model.source.coordinator.UsesSourceCoordination; import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.OpenSearchClientFactory; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessorStrategy; @@ -24,6 +26,7 @@ public class OpenSearchSource implements Source>, UsesSourceCoordi private final AwsCredentialsSupplier awsCredentialsSupplier; private final OpenSearchSourceConfiguration openSearchSourceConfiguration; private final AcknowledgementSetManager acknowledgementSetManager; + private final PluginMetrics pluginMetrics; private SourceCoordinator sourceCoordinator; private OpenSearchService openSearchService; @@ -31,10 +34,12 @@ public class OpenSearchSource implements Source>, UsesSourceCoordi @DataPrepperPluginConstructor public OpenSearchSource(final OpenSearchSourceConfiguration openSearchSourceConfiguration, final AwsCredentialsSupplier awsCredentialsSupplier, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final PluginMetrics pluginMetrics) { this.openSearchSourceConfiguration = openSearchSourceConfiguration; this.awsCredentialsSupplier = awsCredentialsSupplier; this.acknowledgementSetManager = acknowledgementSetManager; + this.pluginMetrics = pluginMetrics; openSearchSourceConfiguration.validateAwsConfigWithUsernameAndPassword(); } @@ -47,14 +52,16 @@ public void start(final Buffer> buffer) { startProcess(openSearchSourceConfiguration, buffer); } - private void startProcess(final OpenSearchSourceConfiguration openSearchSourceConfiguration, final Buffer> buffer) { + private void startProcess(final OpenSearchSourceConfiguration openSearchSourceConfiguration, + final Buffer> buffer) { final OpenSearchClientFactory openSearchClientFactory = OpenSearchClientFactory.create(awsCredentialsSupplier); + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics = OpenSearchSourcePluginMetrics.create(pluginMetrics); final SearchAccessorStrategy searchAccessorStrategy = SearchAccessorStrategy.create(openSearchSourceConfiguration, openSearchClientFactory); final SearchAccessor searchAccessor = searchAccessorStrategy.getSearchAccessor(); - openSearchService = OpenSearchService.createOpenSearchService(searchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager); + openSearchService = OpenSearchService.createOpenSearchService(searchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager, openSearchSourcePluginMetrics); openSearchService.start(); } diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/metrics/OpenSearchSourcePluginMetrics.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/metrics/OpenSearchSourcePluginMetrics.java new file mode 100644 index 0000000000..80bc2d22a8 --- /dev/null +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/metrics/OpenSearchSourcePluginMetrics.java @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugins.source.opensearch.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; +import org.opensearch.dataprepper.metrics.PluginMetrics; + +public class OpenSearchSourcePluginMetrics { + + static final String DOCUMENTS_PROCESSED = "documentsProcessed"; + static final String INDICES_PROCESSED = "indicesProcessed"; + static final String INDEX_PROCESSING_TIME_ELAPSED = "indexProcessingTime"; + static final String PROCESSING_ERRORS = "processingErrors"; + + private final Counter documentsProcessedCounter; + private final Counter indicesProcessedCounter; + private final Counter processingErrorsCounter; + private final Timer indexProcessingTimeTimer; + + public static OpenSearchSourcePluginMetrics create(final PluginMetrics pluginMetrics) { + return new OpenSearchSourcePluginMetrics(pluginMetrics); + } + + private OpenSearchSourcePluginMetrics(final PluginMetrics pluginMetrics) { + documentsProcessedCounter = pluginMetrics.counter(DOCUMENTS_PROCESSED); + indicesProcessedCounter = pluginMetrics.counter(INDICES_PROCESSED); + processingErrorsCounter = pluginMetrics.counter(PROCESSING_ERRORS); + indexProcessingTimeTimer = pluginMetrics.timer(INDEX_PROCESSING_TIME_ELAPSED); + } + + public Counter getDocumentsProcessedCounter() { + return documentsProcessedCounter; + } + + public Counter getIndicesProcessedCounter() { + return indicesProcessedCounter; + } + + public Counter getProcessingErrorsCounter() { + return processingErrorsCounter; + } + + public Timer getIndexProcessingTimeTimer() { + return indexProcessingTimeTimer; + } +} diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java index cacd49c921..62edea9441 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java @@ -19,6 +19,7 @@ import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchIndexProgressState; import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.model.NoSearchContextSearchRequest; @@ -48,19 +49,22 @@ public class NoSearchContextWorker implements SearchWorker, Runnable { private final BufferAccumulator> bufferAccumulator; private final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier; private final AcknowledgementSetManager acknowledgementSetManager; + private final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; public NoSearchContextWorker(final SearchAccessor searchAccessor, final OpenSearchSourceConfiguration openSearchSourceConfiguration, final SourceCoordinator sourceCoordinator, final BufferAccumulator> bufferAccumulator, final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics) { this.searchAccessor = searchAccessor; this.sourceCoordinator = sourceCoordinator; this.openSearchSourceConfiguration = openSearchSourceConfiguration; this.bufferAccumulator = bufferAccumulator; this.openSearchIndexPartitionCreationSupplier = openSearchIndexPartitionCreationSupplier; this.acknowledgementSetManager = acknowledgementSetManager; + this.openSearchSourcePluginMetrics = openSearchSourcePluginMetrics; } @Override @@ -88,11 +92,12 @@ public void run() { sourceCoordinator, indexPartition.get()); - processIndex(indexPartition.get(), acknowledgementSet.getLeft()); + openSearchSourcePluginMetrics.getIndexProcessingTimeTimer().record(() -> processIndex(indexPartition.get(), acknowledgementSet.getLeft())); completeIndexPartition(openSearchSourceConfiguration, acknowledgementSet.getLeft(), acknowledgementSet.getRight(), indexPartition.get(), sourceCoordinator); + openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("The search_after worker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -102,8 +107,10 @@ public void run() { } catch (final Exception e) { LOG.error("Unknown exception while processing index '{}', moving on to another index:", indexPartition.get().getPartitionKey(), e); sourceCoordinator.giveUpPartitions(); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); } } catch (final Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Received an exception while trying to get index to process with search_after, backing off and retrying", e); try { Thread.sleep(STANDARD_BACKOFF_MILLIS); @@ -143,7 +150,9 @@ private void processIndex(final SourcePartition op acknowledgementSet.add(record.getData()); } bufferAccumulator.add(record); + openSearchSourcePluginMetrics.getDocumentsProcessedCounter().increment(); } catch (Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed writing OpenSearch documents to buffer. The last document created has document id '{}' from index '{}' : {}", record.getData().getMetadata().getAttribute(DOCUMENT_ID_METADATA_ATTRIBUTE_NAME), record.getData().getMetadata().getAttribute(INDEX_METADATA_ATTRIBUTE_NAME), e.getMessage()); @@ -157,6 +166,7 @@ private void processIndex(final SourcePartition op try { bufferAccumulator.flush(); } catch (final Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed writing remaining OpenSearch documents to buffer due to: {}", e.getMessage()); } } diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java index 4162fedd01..0904a018ec 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java @@ -18,6 +18,7 @@ import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchIndexProgressState; import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.SearchContextLimitException; @@ -63,19 +64,22 @@ public class PitWorker implements SearchWorker, Runnable { private final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier; private final AcknowledgementSetManager acknowledgementSetManager; + private final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; public PitWorker(final SearchAccessor searchAccessor, final OpenSearchSourceConfiguration openSearchSourceConfiguration, final SourceCoordinator sourceCoordinator, final BufferAccumulator> bufferAccumulator, final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics) { this.searchAccessor = searchAccessor; this.sourceCoordinator = sourceCoordinator; this.openSearchSourceConfiguration = openSearchSourceConfiguration; this.bufferAccumulator = bufferAccumulator; this.openSearchIndexPartitionCreationSupplier = openSearchIndexPartitionCreationSupplier; this.acknowledgementSetManager = acknowledgementSetManager; + this.openSearchSourcePluginMetrics = openSearchSourcePluginMetrics; } @Override @@ -102,10 +106,12 @@ public void run() { sourceCoordinator, indexPartition.get()); - processIndex(indexPartition.get(), acknowledgementSet.getLeft()); + openSearchSourcePluginMetrics.getIndexProcessingTimeTimer().record(() -> processIndex(indexPartition.get(), acknowledgementSet.getLeft())); completeIndexPartition(openSearchSourceConfiguration, acknowledgementSet.getLeft(), acknowledgementSet.getRight(), indexPartition.get(), sourceCoordinator); + + openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("PitWorker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -113,6 +119,7 @@ public void run() { LOG.warn("Received SearchContextLimitExceeded exception for index {}. Giving up index and waiting {} seconds before retrying: {}", indexPartition.get().getPartitionKey(), BACKOFF_ON_PIT_LIMIT_REACHED.getSeconds(), e.getMessage()); sourceCoordinator.giveUpPartitions(); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); try { Thread.sleep(BACKOFF_ON_PIT_LIMIT_REACHED.toMillis()); } catch (final InterruptedException ex) { @@ -124,9 +131,11 @@ public void run() { } catch (final RuntimeException e) { LOG.error("Unknown exception while processing index '{}':", indexPartition.get().getPartitionKey(), e); sourceCoordinator.giveUpPartitions(); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); } } catch (final Exception e) { LOG.error("Received an exception while trying to get index to process with PIT, backing off and retrying", e); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); try { Thread.sleep(STANDARD_BACKOFF_MILLIS); } catch (final InterruptedException ex) { @@ -180,7 +189,9 @@ private void processIndex(final SourcePartition op acknowledgementSet.add(record.getData()); } bufferAccumulator.add(record); + openSearchSourcePluginMetrics.getDocumentsProcessedCounter().increment(); } catch (Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed writing OpenSearch documents to buffer. The last document created has document id '{}' from index '{}' : {}", record.getData().getMetadata().getAttribute(DOCUMENT_ID_METADATA_ATTRIBUTE_NAME), record.getData().getMetadata().getAttribute(INDEX_METADATA_ATTRIBUTE_NAME), e.getMessage()); @@ -195,6 +206,7 @@ private void processIndex(final SourcePartition op try { bufferAccumulator.flush(); } catch (final Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed flushing remaining OpenSearch documents to buffer due to: {}", e.getMessage()); } diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java index 0b53b7e006..ecd878220b 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java @@ -17,6 +17,7 @@ import org.opensearch.dataprepper.model.source.coordinator.exceptions.PartitionUpdateException; import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchIndexProgressState; import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.SearchContextLimitException; @@ -55,19 +56,22 @@ public class ScrollWorker implements SearchWorker { private final BufferAccumulator> bufferAccumulator; private final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier; private final AcknowledgementSetManager acknowledgementSetManager; + private final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; public ScrollWorker(final SearchAccessor searchAccessor, final OpenSearchSourceConfiguration openSearchSourceConfiguration, final SourceCoordinator sourceCoordinator, final BufferAccumulator> bufferAccumulator, final OpenSearchIndexPartitionCreationSupplier openSearchIndexPartitionCreationSupplier, - final AcknowledgementSetManager acknowledgementSetManager) { + final AcknowledgementSetManager acknowledgementSetManager, + final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics) { this.searchAccessor = searchAccessor; this.openSearchSourceConfiguration = openSearchSourceConfiguration; this.sourceCoordinator = sourceCoordinator; this.bufferAccumulator = bufferAccumulator; this.openSearchIndexPartitionCreationSupplier = openSearchIndexPartitionCreationSupplier; this.acknowledgementSetManager = acknowledgementSetManager; + this.openSearchSourcePluginMetrics = openSearchSourcePluginMetrics; } @Override @@ -95,10 +99,12 @@ public void run() { sourceCoordinator, indexPartition.get()); - processIndex(indexPartition.get(), acknowledgementSet.getLeft()); + openSearchSourcePluginMetrics.getIndexProcessingTimeTimer().record(() -> processIndex(indexPartition.get(), acknowledgementSet.getLeft())); completeIndexPartition(openSearchSourceConfiguration, acknowledgementSet.getLeft(), acknowledgementSet.getRight(), indexPartition.get(), sourceCoordinator); + + openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("ScrollWorker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -106,6 +112,7 @@ public void run() { LOG.warn("Received SearchContextLimitExceeded exception for index {}. Giving up index and waiting {} seconds before retrying: {}", indexPartition.get().getPartitionKey(), BACKOFF_ON_SCROLL_LIMIT_REACHED.getSeconds(), e.getMessage()); sourceCoordinator.giveUpPartitions(); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); try { Thread.sleep(BACKOFF_ON_SCROLL_LIMIT_REACHED.toMillis()); } catch (final InterruptedException ex) { @@ -117,9 +124,11 @@ public void run() { } catch (final RuntimeException e) { LOG.error("Unknown exception while processing index '{}':", indexPartition.get().getPartitionKey(), e); sourceCoordinator.giveUpPartitions(); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); } } catch (final Exception e) { LOG.error("Received an exception while trying to get index to process with scroll, backing off and retrying", e); + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); try { Thread.sleep(STANDARD_BACKOFF_MILLIS); } catch (final InterruptedException ex) { @@ -168,6 +177,7 @@ private void processIndex(final SourcePartition op try { bufferAccumulator.flush(); } catch (final Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed flushing remaining OpenSearch documents to buffer due to: {}", e.getMessage()); } } @@ -180,7 +190,9 @@ private void writeDocumentsToBuffer(final List documents, acknowledgementSet.add(record.getData()); } bufferAccumulator.add(record); + openSearchSourcePluginMetrics.getDocumentsProcessedCounter().increment(); } catch (Exception e) { + openSearchSourcePluginMetrics.getProcessingErrorsCounter().increment(); LOG.error("Failed writing OpenSearch documents to buffer. The last document created has document id '{}' from index '{}' : {}", record.getData().getMetadata().getAttribute(DOCUMENT_ID_METADATA_ATTRIBUTE_NAME), record.getData().getMetadata().getAttribute(INDEX_METADATA_ATTRIBUTE_NAME), e.getMessage()); diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchServiceTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchServiceTest.java index 0e0566e2ba..731ee07e50 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchServiceTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchServiceTest.java @@ -22,6 +22,7 @@ import org.opensearch.dataprepper.model.source.coordinator.SourceCoordinator; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SchedulingParameterConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.NoSearchContextWorker; import org.opensearch.dataprepper.plugins.source.opensearch.worker.OpenSearchIndexPartitionCreationSupplier; import org.opensearch.dataprepper.plugins.source.opensearch.worker.PitWorker; @@ -65,6 +66,9 @@ public class OpenSearchServiceTest { @Mock private AcknowledgementSetManager acknowledgementSetManager; + @Mock + private OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; + @Mock private SourceCoordinator sourceCoordinator; @@ -93,7 +97,7 @@ private OpenSearchService createObjectUnderTest() { })) { executorsMockedStatic.when(Executors::newSingleThreadScheduledExecutor).thenReturn(scheduledExecutorService); bufferAccumulatorMockedStatic.when(() -> BufferAccumulator.create(buffer, openSearchSourceConfiguration.getSearchConfiguration().getBatchSize(), BUFFER_TIMEOUT)).thenReturn(bufferAccumulator); - return OpenSearchService.createOpenSearchService(openSearchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager); + return OpenSearchService.createOpenSearchService(openSearchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager, openSearchSourcePluginMetrics); } } diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSourceTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSourceTest.java index c48f552bf7..4ed020a173 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSourceTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/OpenSearchSourceTest.java @@ -11,11 +11,13 @@ import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier; +import org.opensearch.dataprepper.metrics.PluginMetrics; import org.opensearch.dataprepper.model.acknowledgements.AcknowledgementSetManager; import org.opensearch.dataprepper.model.buffer.Buffer; import org.opensearch.dataprepper.model.event.Event; import org.opensearch.dataprepper.model.record.Record; import org.opensearch.dataprepper.model.source.coordinator.SourceCoordinator; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.OpenSearchClientFactory; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessorStrategy; @@ -52,11 +54,17 @@ public class OpenSearchSourceTest { @Mock private AcknowledgementSetManager acknowledgementSetManager; + @Mock + private PluginMetrics pluginMetrics; + + @Mock + private OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; + @Mock private SourceCoordinator sourceCoordinator; private OpenSearchSource createObjectUnderTest() { - return new OpenSearchSource(openSearchSourceConfiguration, awsCredentialsSupplier, acknowledgementSetManager); + return new OpenSearchSource(openSearchSourceConfiguration, awsCredentialsSupplier, acknowledgementSetManager, pluginMetrics); } @Test @@ -75,11 +83,13 @@ void start_with_non_null_buffer_does_not_throw() { try (final MockedStatic searchAccessorStrategyMockedStatic = mockStatic(SearchAccessorStrategy.class); final MockedStatic openSearchClientFactoryMockedStatic = mockStatic(OpenSearchClientFactory.class); + final MockedStatic openSearchSourcePluginMetricsMockedStatic = mockStatic(OpenSearchSourcePluginMetrics.class); final MockedStatic openSearchServiceMockedStatic = mockStatic(OpenSearchService.class)) { openSearchClientFactoryMockedStatic.when(() -> OpenSearchClientFactory.create(awsCredentialsSupplier)).thenReturn(openSearchClientFactory); searchAccessorStrategyMockedStatic.when(() -> SearchAccessorStrategy.create(openSearchSourceConfiguration, openSearchClientFactory)).thenReturn(searchAccessorStrategy); + openSearchSourcePluginMetricsMockedStatic.when(() -> OpenSearchSourcePluginMetrics.create(pluginMetrics)).thenReturn(openSearchSourcePluginMetrics); - openSearchServiceMockedStatic.when(() -> OpenSearchService.createOpenSearchService(searchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager)) + openSearchServiceMockedStatic.when(() -> OpenSearchService.createOpenSearchService(searchAccessor, sourceCoordinator, openSearchSourceConfiguration, buffer, acknowledgementSetManager, openSearchSourcePluginMetrics)) .thenReturn(openSearchService); objectUnderTest.start(buffer); diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorkerTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorkerTest.java index 0b51ae2e0f..12eb0223d2 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorkerTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorkerTest.java @@ -5,6 +5,8 @@ package org.opensearch.dataprepper.plugins.source.opensearch.worker; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SchedulingParameterConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.model.NoSearchContextSearchRequest; @@ -53,6 +56,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.opensearch.dataprepper.plugins.source.opensearch.worker.WorkerCommonUtils.ACKNOWLEDGEMENT_SET_TIMEOUT_SECONDS; @@ -77,16 +81,35 @@ public class NoSearchContextWorkerTest { @Mock private AcknowledgementSetManager acknowledgementSetManager; + @Mock(lenient = true) + private OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; + + @Mock + private Counter documentsProcessedCounter; + + @Mock + private Counter indicesProcessedCounter; + + @Mock + private Counter processingErrorsCounter; + + @Mock + private Timer indexProcessingTimeTimer; + private ExecutorService executorService; @BeforeEach void setup() { executorService = Executors.newSingleThreadExecutor(); lenient().when(openSearchSourceConfiguration.isAcknowledgmentsEnabled()).thenReturn(false); + when(openSearchSourcePluginMetrics.getDocumentsProcessedCounter()).thenReturn(documentsProcessedCounter); + when(openSearchSourcePluginMetrics.getIndicesProcessedCounter()).thenReturn(indicesProcessedCounter); + when(openSearchSourcePluginMetrics.getProcessingErrorsCounter()).thenReturn(processingErrorsCounter); + when(openSearchSourcePluginMetrics.getIndexProcessingTimeTimer()).thenReturn(indexProcessingTimeTimer); } private NoSearchContextWorker createObjectUnderTest() { - return new NoSearchContextWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + return new NoSearchContextWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); } @Test @@ -104,7 +127,8 @@ void run_with_getNextPartition_returning_empty_will_sleep_and_exit_when_interrup } @Test - void run_when_search_without_search_context_throws_index_not_found_exception_completes_the_partition() throws InterruptedException { + void run_when_search_without_search_context_throws_index_not_found_exception_completes_the_partition() throws Exception { + mockTimerCallable(); final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); @@ -131,10 +155,16 @@ void run_when_search_without_search_context_throws_index_not_found_exception_com verify(sourceCoordinator).completePartition(partitionKey); verify(sourceCoordinator, never()).closePartition(anyString(), any(Duration.class), anyInt()); + + verifyNoInteractions(documentsProcessedCounter); + verifyNoInteractions(indicesProcessedCounter); + verifyNoInteractions(processingErrorsCounter); } @Test void run_with_getNextPartition_with_non_empty_partition_processes_and_closes_that_partition() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -188,10 +218,16 @@ void run_with_getNextPartition_with_non_empty_partition_processes_and_closes_tha assertThat(noSearchContextSearchRequests.get(1).getIndex(), equalTo(partitionKey)); assertThat(noSearchContextSearchRequests.get(1).getPaginationSize(), equalTo(2)); assertThat(noSearchContextSearchRequests.get(1).getSearchAfter(), equalTo(searchWithSearchAfterResults.getNextSearchAfter())); + + verify(documentsProcessedCounter, times(3)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test void run_with_getNextPartition_with_acknowledgments_processes_and_closes_that_partition() throws Exception { + mockTimerCallable(); + final AcknowledgementSet acknowledgementSet = mock(AcknowledgementSet.class); AtomicReference numEventsAdded = new AtomicReference<>(0); doAnswer(a -> { @@ -261,5 +297,16 @@ void run_with_getNextPartition_with_acknowledgments_processes_and_closes_that_pa assertThat(noSearchContextSearchRequests.get(1).getSearchAfter(), equalTo(searchWithSearchAfterResults.getNextSearchAfter())); verify(acknowledgementSet).complete(); + + verify(documentsProcessedCounter, times(3)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); + } + + private void mockTimerCallable() { + doAnswer(a -> { + a.getArgument(0).run(); + return null; + }).when(indexProcessingTimeTimer).record(any(Runnable.class)); } } diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorkerTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorkerTest.java index eac270e3d2..718dec6bb5 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorkerTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorkerTest.java @@ -5,6 +5,8 @@ package org.opensearch.dataprepper.plugins.source.opensearch.worker; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SchedulingParameterConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.SearchContextLimitException; @@ -84,16 +87,35 @@ public class PitWorkerTest { @Mock private AcknowledgementSetManager acknowledgementSetManager; + @Mock(lenient = true) + private OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; + + @Mock + private Counter documentsProcessedCounter; + + @Mock + private Counter indicesProcessedCounter; + + @Mock + private Counter processingErrorsCounter; + + @Mock + private Timer indexProcessingTimeTimer; + private ExecutorService executorService; @BeforeEach void setup() { executorService = Executors.newSingleThreadExecutor(); lenient().when(openSearchSourceConfiguration.isAcknowledgmentsEnabled()).thenReturn(false); + when(openSearchSourcePluginMetrics.getDocumentsProcessedCounter()).thenReturn(documentsProcessedCounter); + when(openSearchSourcePluginMetrics.getIndicesProcessedCounter()).thenReturn(indicesProcessedCounter); + when(openSearchSourcePluginMetrics.getProcessingErrorsCounter()).thenReturn(processingErrorsCounter); + when(openSearchSourcePluginMetrics.getIndexProcessingTimeTimer()).thenReturn(indexProcessingTimeTimer); } private PitWorker createObjectUnderTest() { - return new PitWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + return new PitWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); } @Test @@ -112,6 +134,8 @@ void run_with_getNextPartition_returning_empty_will_sleep_and_exit_when_interrup @Test void run_with_getNextPartition_with_non_empty_partition_creates_and_deletes_pit_and_closes_that_partition() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -188,10 +212,15 @@ void run_with_getNextPartition_with_non_empty_partition_creates_and_deletes_pit_ assertThat(deletePointInTimeRequest.getPitId(), equalTo(pitId)); verifyNoInteractions(acknowledgementSetManager); + + verify(documentsProcessedCounter, times(3)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test void run_with_acknowledgments_enabled_creates_and_deletes_pit_and_closes_that_partition() throws Exception { + mockTimerCallable(); final AcknowledgementSet acknowledgementSet = mock(AcknowledgementSet.class); AtomicReference numEventsAdded = new AtomicReference<>(0); @@ -283,10 +312,16 @@ void run_with_acknowledgments_enabled_creates_and_deletes_pit_and_closes_that_pa assertThat(deletePointInTimeRequest.getPitId(), equalTo(pitId)); verify(acknowledgementSet).complete(); + + verify(documentsProcessedCounter, times(3)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test void run_with_getNextPartition_with_valid_existing_point_in_time_does_not_create_another_point_in_time() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -342,10 +377,16 @@ void run_with_getNextPartition_with_valid_existing_point_in_time_does_not_create verify(searchAccessor, never()).createPit(any(CreatePointInTimeRequest.class)); verify(searchAccessor, times(2)).searchWithPit(any(SearchPointInTimeRequest.class)); verify(sourceCoordinator, times(2)).saveProgressStateForPartition(eq(partitionKey), eq(openSearchIndexProgressState)); + + verify(documentsProcessedCounter, times(3)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test - void run_gives_up_partitions_and_waits_when_createPit_throws_SearchContextLimitException() throws InterruptedException { + void run_gives_up_partitions_and_waits_when_createPit_throws_SearchContextLimitException() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -353,7 +394,7 @@ void run_gives_up_partitions_and_waits_when_createPit_throws_SearchContextLimitE when(searchAccessor.createPit(any(CreatePointInTimeRequest.class))).thenThrow(SearchContextLimitException.class); - when(sourceCoordinator.getNextPartition(openSearchIndexPartitionCreationSupplier)).thenReturn(Optional.of(sourcePartition)); + when(sourceCoordinator.getNextPartition(openSearchIndexPartitionCreationSupplier)).thenReturn(Optional.of(sourcePartition)).thenReturn(Optional.empty()); final Future future = executorService.submit(() -> createObjectUnderTest().run()); @@ -367,10 +408,16 @@ void run_gives_up_partitions_and_waits_when_createPit_throws_SearchContextLimitE verify(searchAccessor, never()).deletePit(any(DeletePointInTimeRequest.class)); verify(sourceCoordinator).giveUpPartitions(); verify(sourceCoordinator, never()).closePartition(anyString(), any(Duration.class), anyInt()); + + verifyNoInteractions(documentsProcessedCounter); + verifyNoInteractions(indicesProcessedCounter); + verify(processingErrorsCounter).increment(); } @Test - void run_completes_partitions_when_createPit_throws_IndexNotFoundException() throws InterruptedException { + void run_completes_partitions_when_createPit_throws_IndexNotFoundException() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -393,5 +440,16 @@ void run_completes_partitions_when_createPit_throws_IndexNotFoundException() thr verify(searchAccessor, never()).deletePit(any(DeletePointInTimeRequest.class)); verify(sourceCoordinator).completePartition(partitionKey); verify(sourceCoordinator, never()).closePartition(anyString(), any(Duration.class), anyInt()); + + verifyNoInteractions(documentsProcessedCounter); + verifyNoInteractions(indicesProcessedCounter); + verifyNoInteractions(processingErrorsCounter); + } + + private void mockTimerCallable() { + doAnswer(a -> { + a.getArgument(0).run(); + return null; + }).when(indexProcessingTimeTimer).record(any(Runnable.class)); } } diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorkerTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorkerTest.java index b28c140af6..f97838b187 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorkerTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorkerTest.java @@ -5,6 +5,8 @@ package org.opensearch.dataprepper.plugins.source.opensearch.worker; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SchedulingParameterConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.SearchConfiguration; +import org.opensearch.dataprepper.plugins.source.opensearch.metrics.OpenSearchSourcePluginMetrics; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.SearchAccessor; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.SearchContextLimitException; @@ -56,6 +59,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.opensearch.dataprepper.plugins.source.opensearch.worker.ScrollWorker.SCROLL_TIME_PER_BATCH; @@ -82,16 +86,35 @@ public class ScrollWorkerTest { @Mock private AcknowledgementSetManager acknowledgementSetManager; + @Mock(lenient = true) + private OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics; + + @Mock + private Counter documentsProcessedCounter; + + @Mock + private Counter indicesProcessedCounter; + + @Mock + private Counter processingErrorsCounter; + + @Mock + private Timer indexProcessingTimeTimer; + private ExecutorService executorService; @BeforeEach void setup() { executorService = Executors.newSingleThreadExecutor(); lenient().when(openSearchSourceConfiguration.isAcknowledgmentsEnabled()).thenReturn(false); + when(openSearchSourcePluginMetrics.getDocumentsProcessedCounter()).thenReturn(documentsProcessedCounter); + when(openSearchSourcePluginMetrics.getIndicesProcessedCounter()).thenReturn(indicesProcessedCounter); + when(openSearchSourcePluginMetrics.getProcessingErrorsCounter()).thenReturn(processingErrorsCounter); + when(openSearchSourcePluginMetrics.getIndexProcessingTimeTimer()).thenReturn(indexProcessingTimeTimer); } private ScrollWorker createObjectUnderTest() { - return new ScrollWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager); + return new ScrollWorker(searchAccessor, openSearchSourceConfiguration, sourceCoordinator, bufferAccumulator, openSearchIndexPartitionCreationSupplier, acknowledgementSetManager, openSearchSourcePluginMetrics); } @Test @@ -110,6 +133,8 @@ void run_with_getNextPartition_returning_empty_will_sleep_and_exit_when_interrup @Test void run_with_getNextPartition_with_non_empty_partition_creates_and_deletes_scroll_and_closes_that_partition() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -181,10 +206,16 @@ void run_with_getNextPartition_with_non_empty_partition_creates_and_deletes_scro final DeleteScrollRequest deleteScrollRequest = deleteRequestArgumentCaptor.getValue(); assertThat(deleteScrollRequest, notNullValue()); assertThat(deleteScrollRequest.getScrollId(), equalTo(scrollId)); + + verify(documentsProcessedCounter, times(5)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test void run_with_getNextPartition_with_acknowledgments_creates_and_deletes_scroll_and_closes_that_partition() throws Exception { + mockTimerCallable(); + final AcknowledgementSet acknowledgementSet = mock(AcknowledgementSet.class); AtomicReference numEventsAdded = new AtomicReference<>(0); doAnswer(a -> { @@ -272,10 +303,16 @@ void run_with_getNextPartition_with_acknowledgments_creates_and_deletes_scroll_a assertThat(deleteScrollRequest.getScrollId(), equalTo(scrollId)); verify(acknowledgementSet).complete(); + + verify(documentsProcessedCounter, times(5)).increment(); + verify(indicesProcessedCounter).increment(); + verifyNoInteractions(processingErrorsCounter); } @Test - void run_gives_up_partitions_and_waits_when_createScroll_throws_SearchContextLimitException() throws InterruptedException { + void run_gives_up_partitions_and_waits_when_createScroll_throws_SearchContextLimitException() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -300,11 +337,17 @@ void run_gives_up_partitions_and_waits_when_createScroll_throws_SearchContextLim verifyNoMoreInteractions(searchAccessor); verify(sourceCoordinator).giveUpPartitions(); verify(sourceCoordinator, never()).closePartition(anyString(), any(Duration.class), anyInt()); + + verifyNoInteractions(documentsProcessedCounter); + verifyNoInteractions(indicesProcessedCounter); + verify(processingErrorsCounter).increment(); } @Test - void run_completes_partitions_createScroll_throws_IndexNotFoundException() throws InterruptedException { + void run_completes_partitions_createScroll_throws_IndexNotFoundException() throws Exception { + mockTimerCallable(); + final SourcePartition sourcePartition = mock(SourcePartition.class); final String partitionKey = UUID.randomUUID().toString(); when(sourcePartition.getPartitionKey()).thenReturn(partitionKey); @@ -330,6 +373,17 @@ void run_completes_partitions_createScroll_throws_IndexNotFoundException() throw verifyNoMoreInteractions(searchAccessor); verify(sourceCoordinator).completePartition(partitionKey); verify(sourceCoordinator, never()).closePartition(anyString(), any(Duration.class), anyInt()); + + verifyNoInteractions(documentsProcessedCounter); + verifyNoInteractions(indicesProcessedCounter); + verifyNoInteractions(processingErrorsCounter); + } + + private void mockTimerCallable() { + doAnswer(a -> { + a.getArgument(0).run(); + return null; + }).when(indexProcessingTimeTimer).record(any(Runnable.class)); } } From 40980c1be49dccd5c9ebeee37b7cba7cd73b83d1 Mon Sep 17 00:00:00 2001 From: Taylor Gray Date: Wed, 6 Sep 2023 16:30:33 -0500 Subject: [PATCH 09/12] Adds sigv4 support to Elasticsearch client (#3305) Adds sigv4 support to Elasticsearch client. Move AwsRequestSigningApacheInterceptor to aws-plugin-api, use in os source and sink Signed-off-by: Taylor Gray --- .../aws-plugin-api/build.gradle | 3 +- .../AwsRequestSigningApache4Interceptor.java} | 34 ++--- ...sRequestSigningApache4InterceptorTest.java | 144 ++++++++++++++++++ .../worker/NoSearchContextWorker.java | 3 + .../source/opensearch/worker/PitWorker.java | 3 + .../opensearch/worker/ScrollWorker.java | 2 + .../client/OpenSearchClientFactory.java | 31 +++- .../client/OpenSearchClientFactoryTest.java | 27 ++++ .../opensearch/ConnectionConfiguration.java | 3 +- 9 files changed, 226 insertions(+), 24 deletions(-) rename data-prepper-plugins/{opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/AwsRequestSigningApacheInterceptor.java => aws-plugin-api/src/main/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4Interceptor.java} (85%) create mode 100644 data-prepper-plugins/aws-plugin-api/src/test/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4InterceptorTest.java diff --git a/data-prepper-plugins/aws-plugin-api/build.gradle b/data-prepper-plugins/aws-plugin-api/build.gradle index 53876284be..10c360f9da 100644 --- a/data-prepper-plugins/aws-plugin-api/build.gradle +++ b/data-prepper-plugins/aws-plugin-api/build.gradle @@ -1,6 +1,7 @@ dependencies { implementation 'software.amazon.awssdk:auth' + implementation 'software.amazon.awssdk:apache-client' } test { @@ -12,7 +13,7 @@ jacocoTestCoverageVerification { violationRules { rule { limit { - minimum = 1.0 + minimum = 0.99 } } } diff --git a/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/AwsRequestSigningApacheInterceptor.java b/data-prepper-plugins/aws-plugin-api/src/main/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4Interceptor.java similarity index 85% rename from data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/AwsRequestSigningApacheInterceptor.java rename to data-prepper-plugins/aws-plugin-api/src/main/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4Interceptor.java index 5d10779a3e..0f5e3ab3f3 100644 --- a/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/AwsRequestSigningApacheInterceptor.java +++ b/data-prepper-plugins/aws-plugin-api/src/main/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4Interceptor.java @@ -1,16 +1,8 @@ /* - * Copyright OpenSearch Contributors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 */ -package org.opensearch.dataprepper.plugins.sink.opensearch; +package org.opensearch.dataprepper.aws.api; import org.apache.http.Header; import org.apache.http.HttpEntityEnclosingRequest; @@ -48,7 +40,7 @@ * An {@link HttpRequestInterceptor} that signs requests using any AWS {@link Signer} * and {@link AwsCredentialsProvider}. */ -final class AwsRequestSigningApacheInterceptor implements HttpRequestInterceptor { +public final class AwsRequestSigningApache4Interceptor implements HttpRequestInterceptor { /** * Constant to check content-length @@ -90,10 +82,10 @@ final class AwsRequestSigningApacheInterceptor implements HttpRequestInterceptor * @param awsCredentialsProvider source of AWS credentials for signing * @param region signing region */ - public AwsRequestSigningApacheInterceptor(final String service, - final Signer signer, - final AwsCredentialsProvider awsCredentialsProvider, - final Region region) { + public AwsRequestSigningApache4Interceptor(final String service, + final Signer signer, + final AwsCredentialsProvider awsCredentialsProvider, + final Region region) { this.service = Objects.requireNonNull(service); this.signer = Objects.requireNonNull(signer); this.awsCredentialsProvider = Objects.requireNonNull(awsCredentialsProvider); @@ -107,10 +99,10 @@ public AwsRequestSigningApacheInterceptor(final String service, * @param awsCredentialsProvider source of AWS credentials for signing * @param region signing region */ - public AwsRequestSigningApacheInterceptor(final String service, - final Signer signer, - final AwsCredentialsProvider awsCredentialsProvider, - final String region) { + public AwsRequestSigningApache4Interceptor(final String service, + final Signer signer, + final AwsCredentialsProvider awsCredentialsProvider, + final String region) { this(service, signer, awsCredentialsProvider, Region.of(region)); } @@ -177,7 +169,7 @@ private URI buildUri(final HttpContext context, URIBuilder uriBuilder) throws IO } return uriBuilder.build(); - } catch (URISyntaxException e) { + } catch (final Exception e) { throw new IOException("Invalid URI", e); } } diff --git a/data-prepper-plugins/aws-plugin-api/src/test/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4InterceptorTest.java b/data-prepper-plugins/aws-plugin-api/src/test/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4InterceptorTest.java new file mode 100644 index 0000000000..0ed6d3f769 --- /dev/null +++ b/data-prepper-plugins/aws-plugin-api/src/test/java/org/opensearch/dataprepper/aws/api/AwsRequestSigningApache4InterceptorTest.java @@ -0,0 +1,144 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.aws.api; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHost; +import org.apache.http.RequestLine; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpCoreContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.regions.Region; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class AwsRequestSigningApache4InterceptorTest { + + @Mock + private Signer signer; + + @Mock + private AwsCredentialsProvider awsCredentialsProvider; + + @Mock + private HttpEntityEnclosingRequest httpRequest; + + @Mock + private HttpContext httpContext; + + private AwsRequestSigningApache4Interceptor createObjectUnderTest() { + return new AwsRequestSigningApache4Interceptor("es", signer, awsCredentialsProvider, Region.US_EAST_1); + } + + @Test + void invalidURI_throws_IOException() { + + final RequestLine requestLine = mock(RequestLine.class); + when(requestLine.getUri()).thenReturn("http://invalid-uri.com/file[/].html\n"); + + when(httpRequest.getRequestLine()).thenReturn(requestLine); + + final AwsRequestSigningApache4Interceptor objectUnderTest = new AwsRequestSigningApache4Interceptor("es", signer, awsCredentialsProvider, "us-east-1"); + + assertThrows(IOException.class, () -> objectUnderTest.process(httpRequest, httpContext)); + } + + @Test + void IOException_is_thrown_when_buildURI_throws_exception() { + final RequestLine requestLine = mock(RequestLine.class); + when(requestLine.getMethod()).thenReturn("GET"); + when(requestLine.getUri()).thenReturn("http://localhost?param=test"); + when(httpRequest.getRequestLine()).thenReturn(requestLine); + + when(httpContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST)).thenThrow(RuntimeException.class); + + final AwsRequestSigningApache4Interceptor objectUnderTest = createObjectUnderTest(); + + assertThrows(IOException.class, () -> objectUnderTest.process(httpRequest, httpContext)); + } + + @Test + void empty_contentStreamProvider_throws_IllegalStateException() throws IOException { + final RequestLine requestLine = mock(RequestLine.class); + when(requestLine.getMethod()).thenReturn("GET"); + when(requestLine.getUri()).thenReturn("http://localhost?param=test"); + when(httpRequest.getRequestLine()).thenReturn(requestLine); + when(httpRequest.getAllHeaders()).thenReturn(new BasicHeader[]{ + new BasicHeader("test-name", "test-value"), + new BasicHeader("content-length", "0") + }); + + final HttpEntity httpEntity = mock(HttpEntity.class); + final InputStream inputStream = mock(InputStream.class); + when(httpEntity.getContent()).thenReturn(inputStream); + + when((httpRequest).getEntity()).thenReturn(httpEntity); + + final HttpHost httpHost = HttpHost.create("http://localhost?param=test"); + when(httpContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST)).thenReturn(httpHost); + + final SdkHttpFullRequest signedRequest = mock(SdkHttpFullRequest.class); + when(signedRequest.headers()).thenReturn(Map.of("test-name", List.of("test-value"))); + when(signedRequest.contentStreamProvider()).thenReturn(Optional.empty()); + when(signer.sign(any(SdkHttpFullRequest.class), any(ExecutionAttributes.class))) + .thenReturn(signedRequest); + + final AwsRequestSigningApache4Interceptor objectUnderTest = createObjectUnderTest(); + + assertThrows(IllegalStateException.class, () -> objectUnderTest.process(httpRequest, httpContext)); + } + + @Test + void testHappyPath() throws IOException { + final RequestLine requestLine = mock(RequestLine.class); + when(requestLine.getMethod()).thenReturn("GET"); + when(requestLine.getUri()).thenReturn("http://localhost?param=test"); + when(httpRequest.getRequestLine()).thenReturn(requestLine); + when(httpRequest.getAllHeaders()).thenReturn(new BasicHeader[]{ + new BasicHeader("test-name", "test-value"), + new BasicHeader("content-length", "0") + }); + + final HttpEntity httpEntity = mock(HttpEntity.class); + final InputStream inputStream = mock(InputStream.class); + when(httpEntity.getContent()).thenReturn(inputStream); + + when((httpRequest).getEntity()).thenReturn(httpEntity); + + final HttpHost httpHost = HttpHost.create("http://localhost?param=test"); + when(httpContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST)).thenReturn(httpHost); + + final SdkHttpFullRequest signedRequest = mock(SdkHttpFullRequest.class); + when(signedRequest.headers()).thenReturn(Map.of("test-name", List.of("test-value"))); + final ContentStreamProvider contentStreamProvider = mock(ContentStreamProvider.class); + final InputStream contentInputStream = mock(InputStream.class); + when(contentStreamProvider.newStream()).thenReturn(contentInputStream); + when(signedRequest.contentStreamProvider()).thenReturn(Optional.of(contentStreamProvider)); + when(signer.sign(any(SdkHttpFullRequest.class), any(ExecutionAttributes.class))) + .thenReturn(signedRequest); + createObjectUnderTest().process(httpRequest, httpContext); + } +} diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java index 62edea9441..8b33732a57 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/NoSearchContextWorker.java @@ -98,6 +98,7 @@ public void run() { indexPartition.get(), sourceCoordinator); openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); + LOG.info("Completed processing for index: '{}'", indexPartition.get().getPartitionKey()); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("The search_after worker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -125,6 +126,8 @@ public void run() { private void processIndex(final SourcePartition openSearchIndexPartition, final AcknowledgementSet acknowledgementSet) { final String indexName = openSearchIndexPartition.getPartitionKey(); + LOG.info("Started processing for index: '{}'", indexName); + Optional openSearchIndexProgressStateOptional = openSearchIndexPartition.getPartitionState(); if (openSearchIndexProgressStateOptional.isEmpty()) { diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java index 0904a018ec..38b4e06dd2 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/PitWorker.java @@ -112,6 +112,7 @@ public void run() { indexPartition.get(), sourceCoordinator); openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); + LOG.info("Completed processing for index: '{}'", indexPartition.get().getPartitionKey()); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("PitWorker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -149,6 +150,8 @@ public void run() { private void processIndex(final SourcePartition openSearchIndexPartition, final AcknowledgementSet acknowledgementSet) { final String indexName = openSearchIndexPartition.getPartitionKey(); + + LOG.info("Starting processing for index: '{}'", indexName); Optional openSearchIndexProgressStateOptional = openSearchIndexPartition.getPartitionState(); if (openSearchIndexProgressStateOptional.isEmpty()) { diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java index ecd878220b..213e8dede5 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/ScrollWorker.java @@ -105,6 +105,7 @@ public void run() { indexPartition.get(), sourceCoordinator); openSearchSourcePluginMetrics.getIndicesProcessedCounter().increment(); + LOG.info("Completed processing for index: '{}'", indexPartition.get().getPartitionKey()); } catch (final PartitionUpdateException | PartitionNotFoundException | PartitionNotOwnedException e) { LOG.warn("ScrollWorker received an exception from the source coordinator. There is a potential for duplicate data for index {}, giving up partition and getting next partition: {}", indexPartition.get().getPartitionKey(), e.getMessage()); sourceCoordinator.giveUpPartitions(); @@ -142,6 +143,7 @@ public void run() { private void processIndex(final SourcePartition openSearchIndexPartition, final AcknowledgementSet acknowledgementSet) { final String indexName = openSearchIndexPartition.getPartitionKey(); + LOG.info("Started processing for index: '{}'", indexName); final Integer batchSize = openSearchSourceConfiguration.getSearchConfiguration().getBatchSize(); diff --git a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactory.java b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactory.java index 2bb193286f..f240f7bb24 100644 --- a/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactory.java +++ b/data-prepper-plugins/opensearch-source/src/main/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactory.java @@ -9,6 +9,7 @@ import co.elastic.clients.transport.ElasticsearchTransport; import org.apache.http.Header; import org.apache.http.HttpHost; +import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponseInterceptor; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -31,11 +32,13 @@ import org.opensearch.client.transport.rest_client.RestClientTransport; import org.opensearch.dataprepper.aws.api.AwsCredentialsOptions; import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier; +import org.opensearch.dataprepper.aws.api.AwsRequestSigningApache4Interceptor; import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; import org.opensearch.dataprepper.plugins.source.opensearch.configuration.ConnectionConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.apache.ApacheHttpClient; @@ -165,12 +168,38 @@ private org.elasticsearch.client.RestClient createElasticSearchRestClient(final new BasicHeader("Content-type", "application/json") }); - attachBasicAuth(restClientBuilder, openSearchSourceConfiguration); + if (Objects.nonNull(openSearchSourceConfiguration.getAwsAuthenticationOptions())) { + attachSigV4ForElasticsearchClient(restClientBuilder, openSearchSourceConfiguration); + } else { + attachBasicAuth(restClientBuilder, openSearchSourceConfiguration); + } setConnectAndSocketTimeout(restClientBuilder, openSearchSourceConfiguration); return restClientBuilder.build(); } + private void attachSigV4ForElasticsearchClient(final org.elasticsearch.client.RestClientBuilder restClientBuilder, + final OpenSearchSourceConfiguration openSearchSourceConfiguration) { + final AwsCredentialsProvider awsCredentialsProvider = awsCredentialsSupplier.getProvider(AwsCredentialsOptions.builder() + .withRegion(openSearchSourceConfiguration.getAwsAuthenticationOptions().getAwsRegion()) + .withStsRoleArn(openSearchSourceConfiguration.getAwsAuthenticationOptions().getAwsStsRoleArn()) + .withStsExternalId(openSearchSourceConfiguration.getAwsAuthenticationOptions().getAwsStsExternalId()) + .withStsHeaderOverrides(openSearchSourceConfiguration.getAwsAuthenticationOptions().getAwsStsHeaderOverrides()) + .build()); + final Aws4Signer aws4Signer = Aws4Signer.create(); + final HttpRequestInterceptor httpRequestInterceptor = new AwsRequestSigningApache4Interceptor(AOS_SERVICE_NAME, aws4Signer, + awsCredentialsProvider, openSearchSourceConfiguration.getAwsAuthenticationOptions().getAwsRegion()); + restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> { + httpClientBuilder.addInterceptorLast(httpRequestInterceptor); + attachSSLContext(httpClientBuilder, openSearchSourceConfiguration); + httpClientBuilder.addInterceptorLast( + (HttpResponseInterceptor) + (response, context) -> + response.addHeader("X-Elastic-Product", "Elasticsearch")); + return httpClientBuilder; + }); + } + private void attachBasicAuth(final RestClientBuilder restClientBuilder, final OpenSearchSourceConfiguration openSearchSourceConfiguration) { restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> { diff --git a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactoryTest.java b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactoryTest.java index b3a191a4e7..54497b2ce7 100644 --- a/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactoryTest.java +++ b/data-prepper-plugins/opensearch-source/src/test/java/org/opensearch/dataprepper/plugins/source/opensearch/worker/client/OpenSearchClientFactoryTest.java @@ -95,6 +95,33 @@ void provideElasticSearchClient_with_username_and_password() { verifyNoInteractions(awsCredentialsSupplier); } + @Test + void provideElasticSearchClient_with_aws_auth() { + when(connectionConfiguration.getCertPath()).thenReturn(null); + when(connectionConfiguration.getSocketTimeout()).thenReturn(null); + when(connectionConfiguration.getConnectTimeout()).thenReturn(null); + + final AwsAuthenticationConfiguration awsAuthenticationConfiguration = mock(AwsAuthenticationConfiguration.class); + when(awsAuthenticationConfiguration.getAwsRegion()).thenReturn(Region.US_EAST_1); + final String stsRoleArn = "arn:aws:iam::123456789012:role/my-role"; + when(awsAuthenticationConfiguration.getAwsStsRoleArn()).thenReturn(stsRoleArn); + when(awsAuthenticationConfiguration.getAwsStsHeaderOverrides()).thenReturn(Collections.emptyMap()); + when(openSearchSourceConfiguration.getAwsAuthenticationOptions()).thenReturn(awsAuthenticationConfiguration); + + final ArgumentCaptor awsCredentialsOptionsArgumentCaptor = ArgumentCaptor.forClass(AwsCredentialsOptions.class); + final AwsCredentialsProvider awsCredentialsProvider = mock(AwsCredentialsProvider.class); + when(awsCredentialsSupplier.getProvider(awsCredentialsOptionsArgumentCaptor.capture())).thenReturn(awsCredentialsProvider); + + final ElasticsearchClient elasticsearchClient = createObjectUnderTest().provideElasticSearchClient(openSearchSourceConfiguration); + assertThat(elasticsearchClient, notNullValue()); + + final AwsCredentialsOptions awsCredentialsOptions = awsCredentialsOptionsArgumentCaptor.getValue(); + assertThat(awsCredentialsOptions, notNullValue()); + assertThat(awsCredentialsOptions.getRegion(), equalTo(Region.US_EAST_1)); + assertThat(awsCredentialsOptions.getStsHeaderOverrides(), equalTo(Collections.emptyMap())); + assertThat(awsCredentialsOptions.getStsRoleArn(), equalTo(stsRoleArn)); + } + @Test void provideOpenSearchClient_with_aws_auth() { when(connectionConfiguration.getCertPath()).thenReturn(null); diff --git a/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/ConnectionConfiguration.java b/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/ConnectionConfiguration.java index 0eb173b7e5..87c94a5b91 100644 --- a/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/ConnectionConfiguration.java +++ b/data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/ConnectionConfiguration.java @@ -27,6 +27,7 @@ import org.opensearch.client.transport.rest_client.RestClientTransport; import org.opensearch.dataprepper.aws.api.AwsCredentialsOptions; import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier; +import org.opensearch.dataprepper.aws.api.AwsRequestSigningApache4Interceptor; import org.opensearch.dataprepper.model.configuration.PluginSetting; import org.opensearch.dataprepper.plugins.sink.opensearch.bulk.PreSerializedJsonpMapper; import org.slf4j.Logger; @@ -286,7 +287,7 @@ private void attachSigV4(final RestClientBuilder restClientBuilder, AwsCredentia final Aws4Signer aws4Signer = Aws4Signer.create(); final AwsCredentialsOptions awsCredentialsOptions = createAwsCredentialsOptions(); final AwsCredentialsProvider credentialsProvider = awsCredentialsSupplier.getProvider(awsCredentialsOptions); - final HttpRequestInterceptor httpRequestInterceptor = new AwsRequestSigningApacheInterceptor(AOS_SERVICE_NAME, aws4Signer, + final HttpRequestInterceptor httpRequestInterceptor = new AwsRequestSigningApache4Interceptor(AOS_SERVICE_NAME, aws4Signer, credentialsProvider, awsRegion); restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> { httpClientBuilder.addInterceptorLast(httpRequestInterceptor); From 3a3299c9a271162d90d01df94315dc7c37c9bd91 Mon Sep 17 00:00:00 2001 From: David Venable Date: Thu, 7 Sep 2023 09:29:13 -0700 Subject: [PATCH 10/12] Gatling performance test enhancements - HTTPS, path configuration, AWS SigV4 (#3312) Adds Gatling configurations for using HTTPS and for configuring the target path. Resolves #3308. Increase the maximum response time for the SingleRequestSimulation to 1 second. This is in line with other tests. Adds AWS SigV4 signing in the Gatling performance tests. Also moves the Gatling setup into constructors rather than static initializers. Resolves #3308. Signed-off-by: David Venable --- performance-test/build.gradle | 1 + .../performance/FixedClientSimulation.java | 1 + .../performance/HttpStaticLoadSimulation.java | 1 + .../HttpsStaticLoadSimulation.java | 1 + .../test/performance/RampUpSimulation.java | 1 + .../performance/SingleRequestSimulation.java | 10 +- .../test/performance/SlowBurnSimulation.java | 1 + .../test/performance/TargetRpsSimulation.java | 1 + .../VariousGrokPatternsSimulation.java | 4 +- .../performance/tools/AwsRequestSigner.java | 134 ++++++++++++++++++ .../test/performance/tools/Chain.java | 2 +- .../test/performance/tools/PathTarget.java | 10 ++ .../test/performance/tools/Protocol.java | 37 ++--- .../performance/tools/SignerProvider.java | 19 +++ 14 files changed, 190 insertions(+), 33 deletions(-) create mode 100644 performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/AwsRequestSigner.java create mode 100644 performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/PathTarget.java create mode 100644 performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/SignerProvider.java diff --git a/performance-test/build.gradle b/performance-test/build.gradle index 0a9c1ce2c2..b5ffad15f1 100644 --- a/performance-test/build.gradle +++ b/performance-test/build.gradle @@ -19,6 +19,7 @@ repositories { } dependencies { + gatlingImplementation 'software.amazon.awssdk:auth:2.20.67' implementation 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly testLibs.junit.engine } diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/FixedClientSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/FixedClientSimulation.java index b5d8988eb2..ae03274110 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/FixedClientSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/FixedClientSimulation.java @@ -22,6 +22,7 @@ public class FixedClientSimulation extends Simulation { .during(duration) .on(Chain.sendApacheCommonLogPostRequest("Post logs with large batch", largeBatchSize)); + public FixedClientSimulation() { setUp(fixedScenario.injectOpen(CoreDsl.atOnceUsers(users))) .protocols(Protocol.httpProtocol()) diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpStaticLoadSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpStaticLoadSimulation.java index e8078b560f..ca59af336e 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpStaticLoadSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpStaticLoadSimulation.java @@ -20,6 +20,7 @@ public class HttpStaticLoadSimulation extends Simulation { .during(testDuration) .on(Chain.sendApacheCommonLogPostRequest("Average log post request", 20)); + public HttpStaticLoadSimulation() { setUp(httpStaticLoad.injectOpen( CoreDsl.rampUsers(10).during(Duration.ofSeconds(10)), diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpsStaticLoadSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpsStaticLoadSimulation.java index 3cc36f52d5..69764979a9 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpsStaticLoadSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/HttpsStaticLoadSimulation.java @@ -20,6 +20,7 @@ public class HttpsStaticLoadSimulation extends Simulation { .during(testDuration) .on(Chain.sendApacheCommonLogPostRequest("Average log post request", 20)); + public HttpsStaticLoadSimulation() { setUp(httpStaticLoad.injectOpen( CoreDsl.rampUsers(10).during(Duration.ofSeconds(10)), diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/RampUpSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/RampUpSimulation.java index 6a942cc848..f7a5fdf18e 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/RampUpSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/RampUpSimulation.java @@ -23,6 +23,7 @@ public class RampUpSimulation extends Simulation { .during(peakLoadTime) .on(Chain.sendApacheCommonLogPostRequest("Post logs with large batch", largeBatchSize)); + public RampUpSimulation() { setUp( rampUpScenario.injectOpen( diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SingleRequestSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SingleRequestSimulation.java index 07160336bb..6815f055d2 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SingleRequestSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SingleRequestSimulation.java @@ -10,21 +10,23 @@ import io.gatling.javaapi.core.ScenarioBuilder; import io.gatling.javaapi.core.Simulation; import io.gatling.javaapi.http.HttpDsl; +import org.opensearch.dataprepper.test.performance.tools.PathTarget; import org.opensearch.dataprepper.test.performance.tools.Protocol; public class SingleRequestSimulation extends Simulation { ChainBuilder sendSingleLogFile = CoreDsl.exec( HttpDsl.http("Post log") - .post("/log/ingest") + .post(PathTarget.getPath()) .body(CoreDsl.ElFileBody("bodies/singleLog.json")) .asJson() - .check(HttpDsl.status().is(200), CoreDsl.responseTimeInMillis().lt(200)) - ); + .check(HttpDsl.status().is(200), CoreDsl.responseTimeInMillis().lt(500)) + ); ScenarioBuilder basicScenario = CoreDsl.scenario("Post static json log file") .exec(sendSingleLogFile); + public SingleRequestSimulation() { setUp( @@ -32,7 +34,7 @@ public class SingleRequestSimulation extends Simulation { ).protocols( Protocol.httpProtocol() ).assertions( - CoreDsl.global().responseTime().mean().lt(100), + CoreDsl.global().responseTime().max().lt(1000), CoreDsl.global().successfulRequests().percent().is(100.0) ); } diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SlowBurnSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SlowBurnSimulation.java index 9fd06673c3..10611565d4 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SlowBurnSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/SlowBurnSimulation.java @@ -23,6 +23,7 @@ public class SlowBurnSimulation extends Simulation { .forever() .on(Chain.sendApacheCommonLogPostRequest("Post logs with large batch", largeBatchSize)); + public SlowBurnSimulation() { setUp( rampUpScenario.injectOpen( diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/TargetRpsSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/TargetRpsSimulation.java index 73f9790d52..ea4fcea203 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/TargetRpsSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/TargetRpsSimulation.java @@ -43,6 +43,7 @@ private static PopulationBuilder runScenarioWithTargetRps(final ScenarioBuilder ).protocols(Protocol.httpProtocol()); } + public TargetRpsSimulation() { setUp( runScenarioWithTargetRps(smallBatchScenario, 400), diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/VariousGrokPatternsSimulation.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/VariousGrokPatternsSimulation.java index 36677de41c..a94662b290 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/VariousGrokPatternsSimulation.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/VariousGrokPatternsSimulation.java @@ -13,6 +13,7 @@ import io.gatling.javaapi.core.Session; import io.gatling.javaapi.core.Simulation; import io.gatling.javaapi.http.HttpDsl; +import org.opensearch.dataprepper.test.performance.tools.PathTarget; import org.opensearch.dataprepper.test.performance.tools.Protocol; import org.opensearch.dataprepper.test.performance.tools.Templates; @@ -52,7 +53,7 @@ private static Map logObject(final String logValue) { ChainBuilder sendMultipleGrokPatterns = CoreDsl.exec( HttpDsl.http("Http multiple grok pattern request") - .post("/log/ingest") + .post(PathTarget.getPath()) .asJson() .body(CoreDsl.StringBody(VariousGrokPatternsSimulation.multipleGrokPatterns))); @@ -60,6 +61,7 @@ private static Map logObject(final String logValue) { .during(testDuration) .on(sendMultipleGrokPatterns); + public VariousGrokPatternsSimulation() { setUp(sendMultipleGrokPatternsScenario.injectOpen( CoreDsl.rampUsers(rampUsers).during(rampUpTime) diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/AwsRequestSigner.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/AwsRequestSigner.java new file mode 100644 index 0000000000..377f58f589 --- /dev/null +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/AwsRequestSigner.java @@ -0,0 +1,134 @@ +package org.opensearch.dataprepper.test.performance.tools; + +import io.gatling.http.client.Request; +import io.netty.handler.codec.http.HttpHeaders; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.regions.Region; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; + +public class AwsRequestSigner implements Consumer { + static final String SIGNER_NAME = "aws_sigv4"; + + /** + * Constant to check content-length + */ + private static final String CONTENT_LENGTH = "content-length"; + /** + * Constant to check Zero content length + */ + private static final String ZERO_CONTENT_LENGTH = "0"; + /** + * Constant to check if host is the endpoint + */ + private static final String HOST = "host"; + + private static final String REGION_PROPERTY = "aws_region"; + private static final String SERVICE_NAME_PROPERTY = "aws_service"; + + + private final Aws4Signer awsSigner; + private final AwsCredentialsProvider credentialsProvider; + private final Region region; + private final String service; + + public AwsRequestSigner() { + region = getRequiredProperty(REGION_PROPERTY, Region::of); + service = getRequiredProperty(SERVICE_NAME_PROPERTY, Function.identity()); + + awsSigner = Aws4Signer.create(); + credentialsProvider = DefaultCredentialsProvider.create(); + } + + private static T getRequiredProperty(String propertyName, Function transform) { + String inputString = System.getProperty(propertyName); + if(inputString == null) { + throw new RuntimeException("Using " + SIGNER_NAME + " authentication requires providing the " + propertyName + " system property."); + } + + try { + return transform.apply(inputString); + } catch (Exception ex) { + throw new RuntimeException("Unable to process property " + propertyName + " with error: " + ex.getMessage()); + } + } + + @Override + public void accept(Request request) { + ExecutionAttributes attributes = new ExecutionAttributes(); + attributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, credentialsProvider.resolveCredentials()); + attributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, service); + attributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region); + + SdkHttpFullRequest incomingSdkRequest = convertIncomingRequest(request); + + SdkHttpFullRequest signedRequest = awsSigner.sign(incomingSdkRequest, attributes); + + modifyOutgoingRequest(request, signedRequest); + } + + private SdkHttpFullRequest convertIncomingRequest(Request request) { + URI uri; + try { + uri = request.getUri().toJavaNetURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.fromValue(request.getMethod().name())) + .uri(uri); + + requestBuilder.contentStreamProvider(() -> new ByteArrayInputStream(request.getBody().getBytes())); + requestBuilder.headers(headerArrayToMap(request.getHeaders())); + + return requestBuilder.build(); + } + + private static Map> headerArrayToMap(final HttpHeaders headers) { + Map> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry header : headers) { + if (!skipHeader(header)) { + headersMap.put(header.getKey(), headersMap + .getOrDefault(header.getKey(), + new LinkedList<>(Collections.singletonList(header.getValue())))); + } + } + return headersMap; + } + + private static boolean skipHeader(final Map.Entry header) { + return (CONTENT_LENGTH.equalsIgnoreCase(header.getKey()) + && ZERO_CONTENT_LENGTH.equals(header.getValue())) // Strip Content-Length: 0 + || HOST.equalsIgnoreCase(header.getKey()); // Host comes from endpoint + } + + private void modifyOutgoingRequest(Request request, SdkHttpFullRequest signedRequest) { + resetHeaders(request, signedRequest); + } + + private void resetHeaders(Request request, SdkHttpFullRequest signedRequest) { + request.getHeaders().clear(); + + for (Map.Entry> headerEntry : signedRequest.headers().entrySet()) { + for (String value : headerEntry.getValue()) { + request.getHeaders().add(headerEntry.getKey(), value); + } + } + } +} diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Chain.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Chain.java index f99a24b4b4..1f98c0031a 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Chain.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Chain.java @@ -20,7 +20,7 @@ public static ChainBuilder sendApacheCommonLogPostRequest(final int batchSize) { public static ChainBuilder sendApacheCommonLogPostRequest(final String name, final int batchSize) { return CoreDsl.exec( HttpDsl.http(name) - .post("/log/ingest") + .post(PathTarget.getPath()) .body(CoreDsl.StringBody(Templates.apacheCommonLogTemplate(batchSize)))); } } diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/PathTarget.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/PathTarget.java new file mode 100644 index 0000000000..835f71707b --- /dev/null +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/PathTarget.java @@ -0,0 +1,10 @@ +package org.opensearch.dataprepper.test.performance.tools; + +public class PathTarget { + private static final String PATH_PROPERTY_NAME = "path"; + private static final String DEFAULT_PATH = "/log/ingest"; + + public static String getPath() { + return System.getProperty(PATH_PROPERTY_NAME, DEFAULT_PATH); + } +} diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Protocol.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Protocol.java index 9b1439b986..43f33c2646 100644 --- a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Protocol.java +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/Protocol.java @@ -12,11 +12,13 @@ public final class Protocol { private Protocol() { } - private static final String http = "http"; - private static final String https = "https"; + private static final String HTTP = "http"; + private static final String HTTPS = "https"; - public static final String localhost = "localhost"; - private static final String host = System.getProperty("host", localhost); + private static final String HTTP_PROTOCOL = System.getProperty("protocol", HTTP); + + public static final String LOCALHOST = "localhost"; + private static final String HOST = System.getProperty("host", LOCALHOST); private static final Integer defaultPort = 2021; private static final Integer port = Integer.getInteger("port", defaultPort); @@ -26,36 +28,17 @@ private static String asUrl(final String protocol, final String host, final Inte } public static HttpProtocolBuilder httpProtocol() { - return httpProtocol(http, host, port); - } - - public static HttpProtocolBuilder httpProtocol(final String host) { - return httpProtocol(http, host, port); - } - - public static HttpProtocolBuilder httpsProtocol() { - return httpProtocol(https, host, port); - } - - public static HttpProtocolBuilder httpsProtocol(final String host) { - return httpProtocol(https, host, port); + return httpProtocol(HTTP_PROTOCOL, HOST, port); } public static HttpProtocolBuilder httpsProtocol(final Integer port) { - return httpProtocol(https, host, port); - } - - public static HttpProtocolBuilder httpsProtocol(final String host, final Integer port) { - return httpProtocol(https, host, port); - } - - public static HttpProtocolBuilder httpProtocol(final String protocol, final String host) { - return httpProtocol(protocol, host, port); + return httpProtocol(HTTPS, HOST, port); } - public static HttpProtocolBuilder httpProtocol(final String protocol, final String host, final Integer port) { + private static HttpProtocolBuilder httpProtocol(final String protocol, final String host, final Integer port) { return HttpDsl.http .baseUrl(asUrl(protocol, host, port)) + .sign(SignerProvider.getSigner()) .acceptHeader("application/json") .header("Content-Type", "application/json"); } diff --git a/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/SignerProvider.java b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/SignerProvider.java new file mode 100644 index 0000000000..87c85fe86c --- /dev/null +++ b/performance-test/src/gatling/java/org/opensearch/dataprepper/test/performance/tools/SignerProvider.java @@ -0,0 +1,19 @@ +package org.opensearch.dataprepper.test.performance.tools; + +import io.gatling.http.client.Request; + +import java.util.function.Consumer; + +public class SignerProvider { + private static final Consumer NO_OP_SIGNER = r -> { }; + + public static Consumer getSigner() { + String authentication = System.getProperty("authentication"); + + if(AwsRequestSigner.SIGNER_NAME.equals(authentication)) { + return new AwsRequestSigner(); + } + + return NO_OP_SIGNER; + } +} From 33b59371d385de3a77090e4ab2de45b2c15138d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:54:56 -0700 Subject: [PATCH 11/12] Bump certifi in /release/smoke-tests/otel-span-exporter (#3062) Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- release/smoke-tests/otel-span-exporter/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/smoke-tests/otel-span-exporter/requirements.txt b/release/smoke-tests/otel-span-exporter/requirements.txt index 631dcdfab3..7378b7425a 100644 --- a/release/smoke-tests/otel-span-exporter/requirements.txt +++ b/release/smoke-tests/otel-span-exporter/requirements.txt @@ -1,5 +1,5 @@ backoff==1.10.0 -certifi==2022.12.7 +certifi==2023.7.22 charset-normalizer==2.0.9 Deprecated==1.2.13 googleapis-common-protos==1.53.0 From cd194c167233287b9be7fc83699a925cc6e44409 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:56:12 -0700 Subject: [PATCH 12/12] Bump grpcio in /release/smoke-tests/otel-span-exporter (#2984) Bumps [grpcio](https://github.com/grpc/grpc) from 1.50.0 to 1.53.0. - [Release notes](https://github.com/grpc/grpc/releases) - [Changelog](https://github.com/grpc/grpc/blob/master/doc/grpc_release_schedule.md) - [Commits](https://github.com/grpc/grpc/compare/v1.50.0...v1.53.0) --- updated-dependencies: - dependency-name: grpcio dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- release/smoke-tests/otel-span-exporter/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/smoke-tests/otel-span-exporter/requirements.txt b/release/smoke-tests/otel-span-exporter/requirements.txt index 7378b7425a..6b6caec0f0 100644 --- a/release/smoke-tests/otel-span-exporter/requirements.txt +++ b/release/smoke-tests/otel-span-exporter/requirements.txt @@ -3,7 +3,7 @@ certifi==2023.7.22 charset-normalizer==2.0.9 Deprecated==1.2.13 googleapis-common-protos==1.53.0 -grpcio==1.50.0 +grpcio==1.53.0 idna==3.3 opentelemetry-api==1.7.1 opentelemetry-exporter-otlp==1.7.1