diff --git a/CHANGELOG.md b/CHANGELOG.md index 9824752..0beacfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ -## [project-title] Changelog +# Observability Accelerators Changelog - -# x.y.z (yyyy-mm-dd) +## 1.0 (2023-05-19) -*Features* -* ... +### Features -*Bug Fixes* -* ... +- Initial public release -*Breaking Changes* -* ... +### Bug Fixes + +- Not applicable + +### Breaking Changes + +- Not applicable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9115cf..1fe1f0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to [project-title] +# Contributing to Observability Accelerators -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. @@ -12,61 +12,67 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - - [Code of Conduct](#coc) - - [Issues and Bugs](#issue) - - [Feature Requests](#feature) - - [Submission Guidelines](#submit) +- [Code of Conduct](#coc) +- [Issues and Bugs](#issue) +- [Feature Requests](#feature) +- [Submission Guidelines](#submit) ## Code of Conduct + Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). ## Found an Issue? + If you find a bug in the source code or a mistake in the documentation, you can help us by [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can [submit a Pull Request](#submit-pr) with a fix. ## Want a Feature? -You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub -Repository. If you would like to *implement* a new feature, please submit an issue with + +You can _request_ a new feature by [submitting an issue](#submit-issue) to the GitHub +Repository. If you would like to _implement_ a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it. -* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). +- **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). ## Submission Guidelines ### Submitting an Issue + Before you submit an issue, search the archive, maybe your question was already answered. If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new -features, by not reporting duplicate issues. Providing the following information will increase the +features, by not reporting duplicate issues. Providing the following information will increase the chances of your issue being dealt with quickly: -* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps -* **Version** - what version is affected (e.g. 0.1.2) -* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you -* **Browsers and Operating System** - is this a problem with all browsers? -* **Reproduce the Error** - provide a live example or a unambiguous set of steps -* **Related Issues** - has a similar issue been reported before? -* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be +- **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps +- **Version** - what version is affected (e.g. 0.1.2) +- **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you +- **Browsers and Operating System** - is this a problem with all browsers? +- **Reproduce the Error** - provide a live example or a unambiguous set of steps +- **Related Issues** - has a similar issue been reported before? +- **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. ### Submitting a Pull Request (PR) + Before you submit your Pull Request (PR) consider the following guidelines: -* Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR +- Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate effort. -* Make your changes in a new git fork: +- Make your changes in a new git fork: + +- Commit your changes using a descriptive commit message +- Push your fork to GitHub: +- In GitHub, create a pull request +- If we suggest changes then: -* Commit your changes using a descriptive commit message -* Push your fork to GitHub: -* In GitHub, create a pull request -* If we suggest changes then: - * Make the required updates. - * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): + - Make the required updates. + - Rebase your fork and force push to your GitHub repository (this will update your Pull Request): ```shell git rebase master -i diff --git a/README.md b/README.md index 364f052..c3d8fa3 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,17 @@ -# Project Name +# Observability Accelerators -(short, 1-3 sentenced, description of the project) +This repository contains multiple samples that are meant to accelerate development in the Observability and Monitoring space on Azure. -## Features +Each accelerator focuses on a different application architecture. They contain all source code and infrastructure as code necessary to deploy the application, as well as in-depth documentation that details important O&M concepts. -This project framework provides the following features: +Navigate to one of the accelerators in the list below. The README will include instructions on how to get started with that application. -* Feature 1 -* Feature 2 -* ... +## Accelerator Index -## Getting Started +| Accelerator | +| -------------------------------------------------------------------------------------------------------------------------- | +| [Azure Monitor in a Message-Based Distributed Application on AKS](./accelerators/aks-sb-azmonitor-microservices/README.md) | -### Prerequisites +## Trademarks -(ideally very short, if any) - -- OS -- Library version -- ... - -### Installation - -(ideally very short) - -- npm install [package name] -- mvn install -- ... - -### Quickstart -(Add steps to get up and running quickly) - -1. git clone [repository clone url] -2. cd [repository name] -3. ... - - -## Demo - -A demo app is included to show how to use the project. - -To run the demo, follow these steps: - -(Add steps to start up the demo) - -1. -2. -3. - -## Resources - -(Any additional resources or related projects) - -- Link to supporting information -- Link to similar sample -- ... +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft’s Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. diff --git a/accelerators/aks-sb-azmonitor-microservices/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/Dockerfile new file mode 100644 index 0000000..a6bd0b3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ubuntu/.devcontainer/base.Dockerfile + +# [Choice] Ubuntu version (use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon): ubuntu-22.04, ubuntu-20.04, ubuntu-18.04 +ARG VARIANT="jammy" +FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends figlet + + +ARG USERNAME=vscode +USER $USERNAME + +COPY kubelogin.sh /tmp/kubelogin.sh +RUN mkdir -p "/home/$USERNAME/.local/bin" && \ + /tmp/kubelogin.sh +ENV PATH="/home/vscode/.local/bin:${PATH}" \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/devcontainer.json new file mode 100644 index 0000000..fb28795 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/devcontainer.json @@ -0,0 +1,46 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ubuntu +{ + "name": "aks-sb-azmonitor-microservices", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04 + // Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon. + "args": { + "VARIANT": "ubuntu-22.04" + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/terraform:1": { + "version": "1.3" + }, + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/stuartleeks/dev-container-features/azure-cli-persistence:0": {}, + "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, + "ghcr.io/devcontainers/features/docker-from-docker:1": {}, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { + "helm": "3.10.1" + } + }, + "runArgs": [ + // Attach dev container to host network so allow accessing services on the host + // when running via docker-compose + "--network", "host" + ], + "customizations": { + "vscode": { + "extensions": [ + "timonwong.shellcheck", + "hashicorp.terraform", + "ms-azuretools.vscode-bicep", + "humao.rest-client" + ] + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/.devcontainer/kubelogin.sh b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/kubelogin.sh new file mode 100644 index 0000000..49d7f5c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.devcontainer/kubelogin.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +wget -O /tmp/kubelogin-linux-amd64.zip \ + https://github.com/Azure/kubelogin/releases/download/v0.0.24/kubelogin-linux-amd64.zip + +unzip /tmp/kubelogin-linux-amd64.zip -d /tmp/kubelogin + +cp /tmp/kubelogin/bin/linux_amd64/kubelogin "/home/$USERNAME/.local/bin/kubelogin" diff --git a/accelerators/aks-sb-azmonitor-microservices/.env.sample b/accelerators/aks-sb-azmonitor-microservices/.env.sample new file mode 100644 index 0000000..01e996c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.env.sample @@ -0,0 +1,8 @@ +# Unique name to assign in all deployed services, your high school hotmail alias is a great idea! +USERNAME= + +# Email address for alert notifications +EMAIL_ADDRESS= + +# Uncomment the following line to change the deployment location +# LOCATION=westeurope \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/.gitattributes b/accelerators/aks-sb-azmonitor-microservices/.gitattributes new file mode 100644 index 0000000..c91154c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.gitattributes @@ -0,0 +1,6 @@ +# Ensure that all shell scripts are checked out with LF line endings +# on Windows. This is necessary because Git for Windows defaults to +# CRLF line endings, which breaks the shell scripts. +# NOTE: for best results on Windows, clone the code in a in a file system +# under Windows Subsystem for Linux (WSL) - see https://www.docker.com/blog/docker-desktop-wsl-2-best-practices/ +*.sh text eol=lf diff --git a/accelerators/aks-sb-azmonitor-microservices/.gitignore b/accelerators/aks-sb-azmonitor-microservices/.gitignore new file mode 100644 index 0000000..0f84b33 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/.gitignore @@ -0,0 +1,7 @@ +plan.out +terraform.tfvars +azuredeploy.parameters.json +.env + +output.json +env.yaml diff --git a/accelerators/aks-sb-azmonitor-microservices/README.md b/accelerators/aks-sb-azmonitor-microservices/README.md new file mode 100644 index 0000000..61e41c0 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/README.md @@ -0,0 +1,34 @@ +# Azure Monitor in a Message Based Distributed Application + +Using Azure Monitor to observe a distributed application comes with unique challenges and considerations. The ability to generate and view traces, ensure service availability, use custom telemetry to track business critical indicators, etc. are all more complex in a distributed environment. The sample is designed to demonstrate how to automatically and manually instrument data in variety of languages within a distributed application, as well as provide similar examples for visualization and alerts based on this incoming data. + +The sample contains a conceptual cargo processing application to demonstrate these points. The microservice-based solution is deployed to Azure Kubernetes Service and employs multiple communication protocols, including HTTP and message-based interactions, to enable seamless communication between its services. The services cover a wide variety of programming languages and instrumentation libraries - the Java services utilize OpenTelemetry exporters, while the Node, .NET, and Python services use the Application Insights SDKs for instrumentation purposes. + +The sample contains all code and documentation necessary to deploy and monitor the application. Source code for the microservices can be found in the [/src](./src/) folder, while Bicep and Terraform versions (identical output) of the supporting infrastructure can be found in the [/infrastructure/bicep](./infrastructure/bicep/) and [/infrastructure/terraform](./infrastructure/bicep/) folders, respectively. + +## Use Case + +![Architecture Diagram](./assets/sb-microservice-accelerator-arch-diagram.drawio.png) + +A `cargo-processing-api` service (Java) receives a PUT request with an object in the request body containing ports, products, and other cargo related information. The api validates the request schema and places a message containing the cargo object on an Azure Service Bus queue. A `cargo-processing-validator` service (Typescript) validates the internal cargo properties to ensure it can be successfully shipped before placing the cargo object with boolean validation result on a Service Bus topic. Finally, two services (.NET and Python) with subscriptions to the topic receive the final message, filtering for `valid = True` or `valid = False` flags, respectively, before storing the message in a dedicated Cosmos DB container for further processing. + +A fifth, `operations-api` service (Java) implements the [async request-reply](https://learn.microsoft.com/azure/architecture/patterns/async-request-reply) pattern, adding a level of resiliency to the long running operation. + +Each microservice sends telemetry data to Application Insights, while AKS, Key Vault, Cosmos DB, and Service Bus are each configured to export telemetry data directly to the Log Analytics Workspace associated with the Application Insights resource. + +## Docs + +Getting started instructions and documentation on observability and monitoring topics within the application can be found in the following pages: + +| Topic | Content | +| --------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Getting Started | [getting-started.md](./docs/getting-started.md) | +| Auto vs Manually Instrumented Telemetry | [auto-vs-manually-instrumented-telemetry.md](./docs/auto-vs-manually-instrumented-telemetry.md) | +| Distributed Tracing | [distributed-tracing.md](./docs/distributed-tracing.md) | +| Health Checks | [health-checks.md](./docs/health-checks.md) | +| Custom Dimensions | [custom-dimensions.md](./docs/custom-dimensions.md) | +| Custom Metrics | [custom-metrics.md](./docs/custom-metrics.md) | +| Workbooks | [workbooks.md](./docs/workbooks.md) | +| Alerts | [alerts.md](./docs/alerts.md) | +| Introducing Chaos | [introducing-chaos.md](./docs/introducing-chaos.md) | +| Reducing Telemetry Volume | [reducing-telemetry-volume.md](./docs/reducing-telemetry-volume.md) | diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/Dockerfile new file mode 100644 index 0000000..d64dd2c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster +ARG VARIANT=16-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install an additional version of node using nvm +# ARG EXTRA_NODE_VERSION=10 +# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" + +RUN su node -c "npm install -g @cadl-lang/compiler" +RUN su node -c "npm install -g cadl-vscode" diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/base.Dockerfile b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/base.Dockerfile new file mode 100644 index 0000000..35b6654 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/base.Dockerfile @@ -0,0 +1,17 @@ +# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster +ARG VARIANT=16-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} + +# Install tslint, typescript. eslint is installed by javascript image +ARG NODE_MODULES="tslint-to-eslint-config typescript" +COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers +RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \ + && npm cache clean --force > /dev/null 2>&1 + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install an additional version of node using nvm +# ARG EXTRA_NODE_VERSION=10 +# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bbc0729 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/typescript-node +{ + "name": "Node.js, TypeScript & CADL", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick a Node version: 18, 16, 14. + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "args": { + "VARIANT": "16-bullseye" + } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "dbaeumer.vscode-eslint", + "/usr/local/share/npm-global/lib/node_modules/cadl-vscode/cadl-vscode-0.16.0.vsix" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "node" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/.gitignore b/accelerators/aks-sb-azmonitor-microservices/api-spec/.gitignore new file mode 100644 index 0000000..9794b20 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/.gitignore @@ -0,0 +1,2 @@ +node_modules +cadl-output \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/cadl-project.yaml b/accelerators/aks-sb-azmonitor-microservices/api-spec/cadl-project.yaml new file mode 100644 index 0000000..43afbf8 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/cadl-project.yaml @@ -0,0 +1,2 @@ +emitters: + "@cadl-lang/openapi3": true \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/main.cadl b/accelerators/aks-sb-azmonitor-microservices/api-spec/main.cadl new file mode 100644 index 0000000..d8ee9c4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/main.cadl @@ -0,0 +1,26 @@ +import "@cadl-lang/rest"; +import "./models.cadl"; + +@serviceTitle("CargoProcessingService") +namespace CargoProcessingService; + +using Cadl.Http; +using Cadl.Rest; +using ServiceModels; + +@route("/operations") +interface OperationsService { + @put + @createsOrUpdatesResource(Operation) + putOperation(@path id: string): Operation | Error; + @get + getOperation(@path id: string): Operation | Error; +} + +@route("/cargo") +interface CargoService { + @put + updateCargo(@path id: string, @header("operation-id") operationId?: string, @body body: Cargo): CargoHydrated | Error; + @post + createCargo(@body body: Cargo): CargoHydrated | Error; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/models.cadl b/accelerators/aks-sb-azmonitor-microservices/api-spec/models.cadl new file mode 100644 index 0000000..e976cc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/models.cadl @@ -0,0 +1,65 @@ +import "@cadl-lang/rest"; + +namespace ServiceModels; +using Cadl.Http; +using Cadl.Rest; + +@error +model Error { + code: int32; + message: string; + target: string; +} + +model Product { + name: string; + quantity: int32; +} + +model Port { + source: string; + destination: string; +} + +model DemandDates { + start: plainDate; + end: plainDate; +} + +model Cargo { + product: Product; + port: Port; + demandDates: DemandDates; + @header + operationId: string; +} + +model CargoHydrated { + ...Cargo; + @visibility("read") + @key + id: string; + @visibility("read") + timestamp: zonedDateTime; + @header + waitTime: int32 +} + +model CargoValidated { + ...Cargo; + @visibility("read") + @key + id: string; + @visibility("read") + timestamp: zonedDateTime; + valid: boolean; + error: string; +} + +model Operation { + id: string; + state: string; + result?: CargoValidated; + error?: string; + updatedAt: zonedDateTime; +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/package-lock.json b/accelerators/aks-sb-azmonitor-microservices/api-spec/package-lock.json new file mode 100644 index 0000000..df98e86 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/package-lock.json @@ -0,0 +1,1756 @@ +{ + "name": "api-spec", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "api-spec", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@cadl-lang/compiler": "0.35.0", + "@cadl-lang/openapi3": "0.15.0", + "@cadl-lang/rest": "0.17.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cadl-lang/compiler": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/compiler/-/compiler-0.35.0.tgz", + "integrity": "sha512-0hztF32Qev2K6NAenVx6at8zYGwaWrIVRIFdqyp3/6ZDJ3q8yffH9eERP0ddq2E5TOtKlWF52MgvuIOWY9qyEQ==", + "dependencies": { + "@babel/code-frame": "~7.16.7", + "ajv": "~8.9.0", + "change-case": "~4.1.2", + "globby": "~13.1.1", + "js-yaml": "~4.1.0", + "mkdirp": "~1.0.4", + "mustache": "~4.2.0", + "node-fetch": "3.2.8", + "node-watch": "~0.7.1", + "picocolors": "~1.0.0", + "prettier": "~2.7.1", + "prompts": "~2.4.1", + "vscode-languageserver": "~7.0.0", + "vscode-languageserver-textdocument": "~1.0.1", + "yargs": "~17.3.1" + }, + "bin": { + "cadl": "cmd/cadl.js", + "cadl-server": "cmd/cadl-server.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@cadl-lang/openapi": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/openapi/-/openapi-0.12.0.tgz", + "integrity": "sha512-yoP/gO03oZ09e3n0oW6XgAIcVqBcUmPLQEPvrYqo0/UsZx/ibGZG8oKhhf/C3Kqrp0Vr/qcr6y7SV3NCEHE8bw==", + "peer": true, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@cadl-lang/compiler": "~0.35.0", + "@cadl-lang/rest": "~0.17.0" + } + }, + "node_modules/@cadl-lang/openapi3": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/openapi3/-/openapi3-0.15.0.tgz", + "integrity": "sha512-Ee0muF6/S1eLDDQ9m2/R0N/PeXNNM7J3Q+JHWNE0SepJb/LTlihyN5n/0MAAsaT0mPXoQwSe5Lt8lZ3KaDULqQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@cadl-lang/compiler": "~0.35.0", + "@cadl-lang/openapi": "~0.12.0", + "@cadl-lang/rest": "~0.17.0", + "@cadl-lang/versioning": "~0.8.0" + } + }, + "node_modules/@cadl-lang/rest": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/rest/-/rest-0.17.0.tgz", + "integrity": "sha512-Q5UhVXWXW3XAuri/cAYLw3NJleCXzmqu9TDh6mc+YWbRThvfWx2GYKRbp+7WWCWI1e0zAQt4D49WkYwr/4OJRA==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@cadl-lang/compiler": "~0.35.0" + } + }, + "node_modules/@cadl-lang/versioning": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/versioning/-/versioning-0.8.0.tgz", + "integrity": "sha512-TF5iWtJEaQBKmo4RN/yvzdllWwwCWVTbQnEHHAefVRoq4/ThwO5mGKZI8/RG9zeHcJOGHlvGKyu7n1xY4SlqUw==", + "peer": true, + "dependencies": { + "@cadl-lang/compiler": "~0.35.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.8.tgz", + "integrity": "sha512-KtpD1YhGszhntMpBDyp5lyagk8KIMopC1LEb7cQUAh7zcosaX5uK8HnbNb2i3NTQK3sIawCItS0uFC3QzcLHdg==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-watch": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz", + "integrity": "sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz", + "integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@cadl-lang/compiler": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/compiler/-/compiler-0.35.0.tgz", + "integrity": "sha512-0hztF32Qev2K6NAenVx6at8zYGwaWrIVRIFdqyp3/6ZDJ3q8yffH9eERP0ddq2E5TOtKlWF52MgvuIOWY9qyEQ==", + "requires": { + "@babel/code-frame": "~7.16.7", + "ajv": "~8.9.0", + "change-case": "~4.1.2", + "globby": "~13.1.1", + "js-yaml": "~4.1.0", + "mkdirp": "~1.0.4", + "mustache": "~4.2.0", + "node-fetch": "3.2.8", + "node-watch": "~0.7.1", + "picocolors": "~1.0.0", + "prettier": "~2.7.1", + "prompts": "~2.4.1", + "vscode-languageserver": "~7.0.0", + "vscode-languageserver-textdocument": "~1.0.1", + "yargs": "~17.3.1" + } + }, + "@cadl-lang/openapi": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/openapi/-/openapi-0.12.0.tgz", + "integrity": "sha512-yoP/gO03oZ09e3n0oW6XgAIcVqBcUmPLQEPvrYqo0/UsZx/ibGZG8oKhhf/C3Kqrp0Vr/qcr6y7SV3NCEHE8bw==", + "peer": true, + "requires": {} + }, + "@cadl-lang/openapi3": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/openapi3/-/openapi3-0.15.0.tgz", + "integrity": "sha512-Ee0muF6/S1eLDDQ9m2/R0N/PeXNNM7J3Q+JHWNE0SepJb/LTlihyN5n/0MAAsaT0mPXoQwSe5Lt8lZ3KaDULqQ==", + "requires": {} + }, + "@cadl-lang/rest": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/rest/-/rest-0.17.0.tgz", + "integrity": "sha512-Q5UhVXWXW3XAuri/cAYLw3NJleCXzmqu9TDh6mc+YWbRThvfWx2GYKRbp+7WWCWI1e0zAQt4D49WkYwr/4OJRA==", + "requires": {} + }, + "@cadl-lang/versioning": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cadl-lang/versioning/-/versioning-0.8.0.tgz", + "integrity": "sha512-TF5iWtJEaQBKmo4RN/yvzdllWwwCWVTbQnEHHAefVRoq4/ThwO5mGKZI8/RG9zeHcJOGHlvGKyu7n1xY4SlqUw==", + "peer": true, + "requires": { + "@cadl-lang/compiler": "~0.35.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "requires": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "requires": { + "reusify": "^1.0.4" + } + }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "requires": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.8.tgz", + "integrity": "sha512-KtpD1YhGszhntMpBDyp5lyagk8KIMopC1LEb7cQUAh7zcosaX5uK8HnbNb2i3NTQK3sIawCItS0uFC3QzcLHdg==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "node-watch": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz", + "integrity": "sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==" + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==" + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" + }, + "vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "requires": { + "vscode-languageserver-protocol": "3.16.0" + } + }, + "vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "requires": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz", + "integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg==" + }, + "vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" + }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/api-spec/package.json b/accelerators/aks-sb-azmonitor-microservices/api-spec/package.json new file mode 100644 index 0000000..4bcd4ff --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/api-spec/package.json @@ -0,0 +1,17 @@ +{ + "name": "api-spec", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@cadl-lang/compiler": "0.35.0", + "@cadl-lang/openapi3": "0.15.0", + "@cadl-lang/rest": "0.17.0" + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-alert.png b/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-alert.png new file mode 100644 index 0000000..f078cb5 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-alert.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-dimensions.png b/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-dimensions.png new file mode 100644 index 0000000..e694ae6 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/custom-metric-dimensions.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-rg-list.png b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-rg-list.png new file mode 100644 index 0000000..1365948 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-rg-list.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-initial.png b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-initial.png new file mode 100644 index 0000000..62dfdf8 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-initial.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow1.png b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow1.png new file mode 100644 index 0000000..bb3b53d Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow1.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow2.png b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow2.png new file mode 100644 index 0000000..642b000 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/dimensions-workbook-slow2.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-invalid.png b/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-invalid.png new file mode 100644 index 0000000..93a1a9c Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-invalid.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-valid.png b/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-valid.png new file mode 100644 index 0000000..fc4d2fe Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/full-trace-valid.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/health-check-logs.png b/accelerators/aks-sb-azmonitor-microservices/assets/health-check-logs.png new file mode 100644 index 0000000..78789be Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/health-check-logs.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/log-auto.png b/accelerators/aks-sb-azmonitor-microservices/assets/log-auto.png new file mode 100644 index 0000000..4d7a016 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/log-auto.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/log-manual.png b/accelerators/aks-sb-azmonitor-microservices/assets/log-manual.png new file mode 100644 index 0000000..958e3f5 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/log-manual.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto-rus.png b/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto-rus.png new file mode 100644 index 0000000..356991d Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto-rus.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto.png b/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto.png new file mode 100644 index 0000000..daa31b2 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/metric-auto.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/metric-manual.png b/accelerators/aks-sb-azmonitor-microservices/assets/metric-manual.png new file mode 100644 index 0000000..e4690f1 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/metric-manual.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/sb-microservice-accelerator-arch-diagram.drawio.png b/accelerators/aks-sb-azmonitor-microservices/assets/sb-microservice-accelerator-arch-diagram.drawio.png new file mode 100644 index 0000000..cc7e73c Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/sb-microservice-accelerator-arch-diagram.drawio.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/span-auto.png b/accelerators/aks-sb-azmonitor-microservices/assets/span-auto.png new file mode 100644 index 0000000..08d2065 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/span-auto.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/span-manual.png b/accelerators/aks-sb-azmonitor-microservices/assets/span-manual.png new file mode 100644 index 0000000..c208f2c Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/span-manual.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/verify-invalid-cargo.png b/accelerators/aks-sb-azmonitor-microservices/assets/verify-invalid-cargo.png new file mode 100644 index 0000000..65a8f98 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/verify-invalid-cargo.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/verify-valid-cargo.png b/accelerators/aks-sb-azmonitor-microservices/assets/verify-valid-cargo.png new file mode 100644 index 0000000..3caa3b6 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/verify-valid-cargo.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/workbook-aks-metric.png b/accelerators/aks-sb-azmonitor-microservices/assets/workbook-aks-metric.png new file mode 100644 index 0000000..25fefc0 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/workbook-aks-metric.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/assets/workbook-key-vault-metric.png b/accelerators/aks-sb-azmonitor-microservices/assets/workbook-key-vault-metric.png new file mode 100644 index 0000000..21c6ba9 Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/assets/workbook-key-vault-metric.png differ diff --git a/accelerators/aks-sb-azmonitor-microservices/deploy-bicep.sh b/accelerators/aks-sb-azmonitor-microservices/deploy-bicep.sh new file mode 100644 index 0000000..bc987c9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/deploy-bicep.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -e + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +function help() { + echo + echo "deploy-bicep.sh" + echo + echo "Deploy sample via Bicep" + echo + echo -e "\t--skip-helm-deploy\t(Optional)Skip Helm deployment of services to AKS" + echo -e "\t--aks-aad-auth\t(Optional)Enable AAD authentication for AKS" + echo +} + + +# Set default values here +SKIP_HELM_DEPLOY=false +AKS_AAD_AUTH=false + + +# Process switches: +SHORT=h +LONG=skip-helm-deploy,aks-aad-auth,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +while : +do + case "$1" in + --skip-helm-deploy) + SKIP_HELM_DEPLOY=true + shift 1 + ;; + --aks-aad-auth ) + AKS_AAD_AUTH=true + shift 1 + ;; + -h | --help) + help + exit 0 + ;; + --) + shift; + break + ;; + *) + echo "Unexpected '$1'" + help + exit 1 + ;; + esac +done + +if [[ -z $IN_CD ]]; then # skip loading env vars if running in CD (as they are already set) + if [[ ! -f "$script_dir/.env" ]]; then + echo "Please create a .env file (using .env.sample as a starter)" 1>&2 + exit 1 + fi + source "$script_dir/.env" +fi + +if [[ -z "$USERNAME" ]]; then + echo 'USERNAME not set - ensure you have specifed a value for it in your .env file' 1>&2 + exit 6 +fi + +if [[ -z "$EMAIL_ADDRESS" ]]; then + echo 'EMAIL_ADDRESS not set - ensure you have specifed a value for it in your .env file' 1>&2 + exit 6 +fi + +deploy_args=() +if [[ "$AKS_AAD_AUTH" == "true" ]]; then + deploy_args+=(--aks-aad-auth) +fi + +# Set default values +LOCATION=${LOCATION:-eastus} + +figlet infra +echo "Starting Bicep deployment to $LOCATION" +echo "${deploy_args[@]}" | xargs "$script_dir/infrastructure/scripts/deploy-bicep-infrastructure.sh" --username "$USERNAME" --email-address "$EMAIL_ADDRESS" --location "$LOCATION" +echo "Bicep deployment completed" + +figlet images +echo "Building and pushing service images" +ACR_NAME=$(jq -r '.acr_name' < "$script_dir/output.json") +if [[ ${#ACR_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value acr_name' 1>&2 + exit 6 +fi +"$script_dir/infrastructure/scripts/build-and-push-images.sh" --acr-name "$ACR_NAME" --image-tag latest + +figlet env +echo "Creating env files" +"$script_dir/infrastructure/scripts/create-env-files-from-output.sh" + +if [[ "$SKIP_HELM_DEPLOY" == "true" ]]; then + echo "Skipping Helm deployment" +else + figlet services + echo "Deploying services" + echo "${deploy_args[@]}" | xargs "$script_dir/infrastructure/scripts/deploy-helm-charts.sh" +fi + +echo "Deployment completed" diff --git a/accelerators/aks-sb-azmonitor-microservices/deploy-terraform.sh b/accelerators/aks-sb-azmonitor-microservices/deploy-terraform.sh new file mode 100644 index 0000000..2ad5756 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/deploy-terraform.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -e + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +function help() { + echo + echo "deploy-terraform.sh" + echo + echo "Deploy sample via Terraform" + echo + echo -e "\t--skip-helm-deploy\t(Optional)Skip Helm deployment of services to AKS" + echo -e "\t--aks-aad-auth\t(Optional)Enable AAD authentication for AKS" + echo +} + + +# Set default values here +SKIP_HELM_DEPLOY=false +AKS_AAD_AUTH=false + + +# Process switches: +SHORT=h +LONG=skip-helm-deploy,aks-aad-auth,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +while : +do + case "$1" in + --skip-helm-deploy) + SKIP_HELM_DEPLOY=true + shift 1 + ;; + --aks-aad-auth ) + AKS_AAD_AUTH=true + shift 1 + ;; + -h | --help) + help + exit 0 + ;; + --) + shift; + break + ;; + *) + echo "Unexpected '$1'" + help + exit 1 + ;; + esac +done + +if [[ -z $IN_CD ]]; then # skip loading env vars if running in CD (as they are already set) + if [[ ! -f "$script_dir/.env" ]]; then + echo "Please create a .env file (using .env.sample as a starter)" 1>&2 + exit 1 + fi + source "$script_dir/.env" +fi + +if [[ -z "$USERNAME" ]]; then + echo 'USERNAME not set - ensure you have specifed a value for it in your .env file' 1>&2 + exit 6 +fi + +if [[ -z "$EMAIL_ADDRESS" ]]; then + echo 'EMAIL_ADDRESS not set - ensure you have specifed a value for it in your .env file' 1>&2 + exit 6 +fi + +deploy_args=() +if [[ "$AKS_AAD_AUTH" == "true" ]]; then + deploy_args+=(--aks-aad-auth) +fi + +# Set default values +LOCATION=${LOCATION:-eastus} + +figlet infra +echo "Starting Terraform deployment to $LOCATION" +echo "${deploy_args[@]}" | xargs "$script_dir/infrastructure/scripts/deploy-terraform-infrastructure.sh" --username "$USERNAME" --email-address "$EMAIL_ADDRESS" --location "$LOCATION" +echo "Terraform deployment completed" + +figlet images +echo "Building and pushing service images" +ACR_NAME=$(jq -r '.acr_name' < "$script_dir/output.json") +if [[ ${#ACR_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value acr_name' 1>&2 + exit 6 +fi +"$script_dir/infrastructure/scripts/build-and-push-images.sh" --acr-name "$ACR_NAME" --image-tag latest + +figlet env +echo "Creating env files" +"$script_dir/infrastructure/scripts/create-env-files-from-output.sh" + +if [[ "$SKIP_HELM_DEPLOY" == "true" ]]; then + echo "Skipping Helm deployment" +else + figlet services + echo "Deploying services" + echo "${deploy_args[@]}" | xargs "$script_dir/infrastructure/scripts/deploy-helm-charts.sh" +fi + +echo "Deployment completed" diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/alerts.md b/accelerators/aks-sb-azmonitor-microservices/docs/alerts.md new file mode 100644 index 0000000..c74c122 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/alerts.md @@ -0,0 +1,57 @@ +# Alerts + +[Alerts](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-overview) proactively notify application administrators when the data ingested by Azure Monitor suggests the application is experiencing problems, or will in the near future. Visualization tooling like Workbooks highlight important indicators from the application and can illuminate issues, but require active, manual watching by administrators. Alerts can take those same indicators one step further by taking automated, prescriptive action when certain conditions are met. Rather than requiring active watching of a dashboard, alerts let application admins understand and resolve issues with the application _before_ they become problematic for most downstream users of the system. + +Azure alert rules are scoped to a specific resource. These resources emit different telemetry signals, defined by the resource type. Service Bus namespaces emit a numeric `DeadletteredMessages` metric, for instance, while AKS emits `node_cpu_usage_percentage`, among other metrics. The application relies on a number of metric alerts that utilize these signals. It also utilizes several log alerts that use KQL queries to pull the data evaluated in alert conditions. The `cargoProcessingAPIHealthCheckFailure` alert, for example, uses the following KQL query to pull failed health checks for the `cargo-processing-api` service: + +```sql +requests +| where cloud_RoleName == "cargo-processing-api" and name == "GET /actuator/health" and success == "False" +``` + +Alert conditions combine the signal and some numeric threshold that may be met over a defined window of time. If a signal exceeds some threshold over a time window defined in an alert rule, the alert fires and triggers an action group. Severity levels dictate the relative importance of the alert and mitigation steps. Certain alerts suggest with high likelihood that the application is already experiencing issues, like the microservice exceptions alert (`microserviceExceptions`). Immediate attention should be paid to uncover the underlying issue and resolve the alert. Others, like the Key Vault saturation rate (`keyVaultSaturation`) or number of invalid cargo objects saved (`cosmosInvalidCargo`), don't necessarily require immediate action but suggest that an administrator should take a closer look. + +We elected to create alert rules for signals that suggested issues with the underlying infrastructure or the service code deployed to AKS that utilizes it. Each of the microservices has average duration, health check failure, and health check not reporting alerts. A single microservice exceptions alert is split across the 5 services and alerts when any microservice throws a certain number of exceptions. The combination of these alerts proactively notifies when a service has experienced failure or become less performant. Service Bus exposes many message count metrics, like dead-lettered and abandoned messages, that are also important indicators of application issues and are used in rules. Deadlettered messages, for example, may suggest that the initial `cargo-processing-api` service is not properly validating the cargo object structure before sending the message to the `ingest-cargo` queue. The AKS and Log Analytics alerts include the pre-defined, [recommended alert rules](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-overview#recommended-alert-rules) that suggest impending failure for those resource types. The full list of alerts deployed alongside the application is as follows: + +| Alert Name | Description | Entity | Alert Type | Severity | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------- | ----------------- | ---------- | -------- | +| cosmosRUs | Alert when RUs exceed 400. | Cosmos DB | Metric | 1 | +| cosmosInvalidCargo | Alert when more than 10 documents have been saved to the invalid-cargo container. | Cosmos DB | Metric | 3 | +| serviceBusAbandonedMessages | Alert when a Service Bus entity has abandoned more than 10 messages. | Service Bus | Metric | 2 | +| serviceBusDeadLetteredMessages | Alert when a Service Bus entity has dead-lettered more than 10 messages. | Service Bus | Metric | 2 | +| serviceBusThrottledRequests | Alert when a Service Bus entity has throttled more than 10 requests. | Service Bus | Metric | 2 | +| aksCPUPercentage | Alert when Node CPU percentage exceeds 80. | AKS | Metric | 2 | +| aksMemoryPercentage | Alert when Node memory working set percentage exceeds 80. | AKS | Metric | 2 | +| aksPodRestarts | Alert when a microservice restarts more than once. | AKS | Log | 1 | +| keyVaultSaturation | Alert when Key Vault saturation falls outside the range of a dynamic threshold. | Key Vault | Metric | 3 | +| logAnalyticsDataIngestionDailyCap | Alert when the Log Analytics data ingestion daily cap has been reached. | Log Analytics | Log | 2 | +| logAnalyticsDataIngestionRate | Alert when the Log Analytics max data ingestion rate has been reached. | Log Analytics | Log | 2 | +| logAnalyticsOperationalIssues | Alert when the Log Analytics workspace has an operational issue. | Log Analytics | Log | 3 | +| microserviceExceptions | Alert when a microservice throws more than 5 exceptions. | App Insights/Code | Log | 1 | +| productQtyScheduledForDestinationPort | Alert when a single port/destination receives more than quantity 1000 of a given product. | App Insights/Code | Metric | 3 | +| e2eAverageDuration | Alert when the end to end average request duration exceeds 5 seconds. | App Insights/Code | Log | 1 | +| cargoProcessingAPIRequests | Alert when the cargo-processing-api microservice is not receiving any requests. | App Insights/Code | Log | 3 | +| cargoProcessingAPIAverageDuration | Alert when the cargo-processing-api microservice average request duration exceeds 2 seconds. | App Insights/Code | Log | 1 | +| cargoProcessingAPIHealthCheckFailure | Alert when a cargo-processing-api microservice health check fails. | App Insights/Code | Log | 1 | +| cargoProcessingAPIHealthCheckNotReporting | Alert when the cargo-processing-api microservice health check is not reporting. | App Insights/Code | Log | 1 | +| cargoProcessingValidatorAverageDuration | Alert when the cargo-processing-validator microservice average request duration exceeds 2 seconds. | App Insights/Code | Log | 1 | +| validCargoManagerAverageDuration | Alert when the valid-cargo-manager microservice average request duration exceeds 2 seconds. | App Insights/Code | Log | 1 | +| validCargoManagerHealthCheckFailure | Alert when a valid-cargo-manager microservice health check fails. | App Insights/Code | Log | 1 | +| validCargoManagerHealthCheckNotReporting | Alert when the valid-cargo-manager microservice health check is not reporting. | App Insights/Code | Log | 1 | +| invalidCargoManagerAverageDuration | Alert when the invalid-cargo-manager microservice average request duration exceeds 2 seconds. | App Insights/Code | Log | 1 | +| invalidCargoManagerHealthCheckFailure | Alert when an invalid-cargo-manager microservice health check fails. | App Insights/Code | Log | 1 | +| invalidCargoManagerHealthCheckNotReporting | Alert when the invalid-cargo-manager microservice health check is not reporting. | App Insights/Code | Log | 1 | +| operationsAPIAverageDuration | Alert when the operations-api microservice average request duration exceeds 1 second. | App Insights/Code | Log | 1 | +| operationsAPIHealthCheckFailure | Alert when an operations-api microservice health check fails. | App Insights/Code | Log | 1 | +| operationsAPIHealthCheckNotReporting | Alert when the operations-api microservice health check is not reporting. | App Insights/Code | Log | 1 | + +All alerts in the cargo processing application are [_stateful_](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-overview#alerts-and-state), meaning that they will fire when the condition is met, but _will not_ fire again until the condition is resolved. They all utilize the same action group, which notifies an administrator via email. [Action groups](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) _can_ contain additional actions, like triggering webhooks, Logic Apps, Azure Functions, and more. The notification email address is set in the initial `.env`: + +```yaml +# Email address for alert notifications +EMAIL_ADDRESS=youremail@organization.com +``` + +Most alerts use static thresholds to evaluate the telemetry signals emitted from the application. These alert rules use specific threshold values for a signal pre-defined by the application team. The Cosmos DB RUs alert, for instance, defines a static threshold of 400 RUs that will trigger an alert when exceeded. The Key Vault saturation rate alert, however, uses a [dynamic threshold](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-dynamic-thresholds) that uses a machine learning algorithm to define it. The algorithm uses 10 days of recent data to evaluate patterns and calculate the correct threshold for the signal. The thresholds and windows defined in the alert conditions are easily configurable via [Bicep](../infrastructure/bicep/modules/alerts.bicep) or [Terraform](../infrastructure/terraform/modules/alerts/main.tf). + +No [alert processing rules](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-processing-rules?tabs=portal) are used, but could be easily added to modify or suppress certain alerts before they fire. diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/auto-vs-manually-instrumented-telemetry.md b/accelerators/aks-sb-azmonitor-microservices/docs/auto-vs-manually-instrumented-telemetry.md new file mode 100644 index 0000000..759ecdc --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/auto-vs-manually-instrumented-telemetry.md @@ -0,0 +1,26 @@ +# Auto vs Manually Instrumented Telemetry + +The telemetry data generated by the application can be separated into two distinct groups - automatically and manually instrumented data. + +Automatically instrumented logs, metrics, and traces are produced by the application without any addition of custom code. Each exporter or SDK auto-instruments a unique set of telemetry data. The Java-based API services that utilize OpenTelemetry exporters for Azure Monitor, for instance, instrument a significant amount of telemetry data, by default, while the Typescript-based `cargo-processing-validator` service uses Application Insights SDK setup methods to [define the level of auto-instrumentation](../src/cargo-processing-validator/src/index.ts). Each SDK/exporter defines its own set of [auto-collected items](https://opentelemetry.io/docs/instrumentation/java/automatic/) for review. + +Much of the telemetry we depend on for visualization or alert functionalities is generated out-of-the-box by the services in the application. Distributed tracing, for instance, depends on a number of spans produced automatically by these services. The Azure resources that support the application like Cosmos DB, Service Bus, Key Vault, etc. automatically export additional data that are used in Workbooks and Alert rules, like the number of dead-lettered messages in each queue and topic subscription. + +Manually instrumented data refers to the data generated via custom code added to one of the microservices. The exporters and SDKs expose various methods to produce telemetry data in order to augment the initial, automatically instrumented set. It fills in the gaps that auto-instrumented data fails to provide. The set of auto-instrumented data generated by the application was first examined before determinations were made about what additional data was required to support the proposed Workbooks tiles and Alert rules. We elected to manually instrument data that enabled distributed traces, additional logging for debugging purposes, health checks, tracking of specific business rules, and more. + +The following examples display automatically and manually instrumented logs, metrics, and trace data in Azure Monitor that was exported by the application: + +The `cargo-processing-api` service automatically instruments a log related to sending a batch of messages, while the "Validating cargo schema" log results from a `logger.info()` call within its [CargoController](../src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoController.java) class: +![Auto-Instrumented Log](../assets/log-auto.png) +![Manually-Instrumented Log](../assets/log-manual.png) + +The `jvm_memory_used` metric is automatically instrumented by the `cargo-processing-api` service, while the `port_product_qty` custom metric is [manually instrumented within](../src/valid-cargo-manager/Services/SubscriptionReceiver.cs) the `valid-cargo-manager` service: +![Auto-Instrumented Metric](../assets/metric-auto.png) +![Manually-Instrumented Metric](../assets/metric-manual.png) + +The `TotalRequestUnits` metric is automatically instrumented by the Cosmos DB resource: +![Auto-Instrumented Metric](../assets/metric-auto-rus.png) + +The span that represents the initial POST request to the `cargo-processing-api` is automatically instrumented. The message send dependency to the `validated-cargo` Service Bus topic is represented by a manually instrumented span [generated within](../src/cargo-processing-validator/src/services/ServiceBusWithTelemetry.ts) the `cargo-processing-validator` service: +![Auto-Instrumented Span](../assets/span-auto.png) +![Manually-Instrumented Span](../assets/span-manual.png) diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/custom-dimensions.md b/accelerators/aks-sb-azmonitor-microservices/docs/custom-dimensions.md new file mode 100644 index 0000000..65bb259 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/custom-dimensions.md @@ -0,0 +1,148 @@ +# Custom Dimensions + +When examining the behavior of a system, we often find aspects that we want to explore more deeply. For example, if we see that some requests in the system are taking longer than expected we may want to know more about the specific requests that are slow. Are all requests slow or just some of them? Are there common features to the slow requests? Do the requests all come from a particular system? In a multi-tenant system, are the slow requests distributed across all tenants or just a subset? + +As we explore these questions, we may find that we need to filter our telemetry data by additional properties. For example, we may want to filter our telemetry data by the request path, or by the tenant ID. Some of these properties will be available in the telemetry data by default, but others will not. For the properties that aren't part of the default data collected, we can use custom dimensions to add the additional information to our telemetry data. + +One place where we add custom dimensions in this project is in the `cargo-processing-validator` service. In the next couple of sections we will see what the custom dimensions look like from the monitoring dashboard and explore the implementation in code. + +## Custom Dimensions in Action + +In this section we will generate some test load on the system and then explore the telemetry dashboard to see the custom dimensions in action. + +To generate the test load, we will use the code in `src/cargo-test-scripts`. That folder contains a dev container for use with Visual Studio Code which makes it easy to run the scripts. If you are using Visual Studio Code, you can open the folder in a dev container by selecting the `Open Folder in Container` option from the `File` menu. The folder also contains a [README.md](../src/cargo-test-scripts/README.md) file with instructions for running the scripts from the command line. + +From the terminal in the dev container, run the following command to generate some test load: + +```bash +cat << EOF | node index.js -c - +{ + "tests": [ + { + "name": "Send cargo to cargo processing api", + "target": "cargo-processing-api", + "volume": 500, + "validateResults": false, + "delayBetweenCargoInMilliseconds": 1500, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0 + + } + } + ] +} +EOF +``` + +This command will generate 500 cargo messages and send them to the `cargo-processing-api` service. The cargo messages will be sent at a rate of one message every 1.5 seconds. The cargo messages will not be validated, so all of them will be sent to the `valid-cargo-manager` service. + +Now that we are sending load, we can open the Once you have this running, open the [Azure portal](https://portal.azure.com) and navigate to the resource group you deployed to. Next, select the `Service Processing` Workbook as shown below: + +![Resource Group List Showing Service Processing Workbook](../assets/dimensions-rg-list.png) + +Next, click the `Open Workbook` button. You should see a screen similar to the following (if you don't see any telemetry then ): + +![Service Processing Workbook With Initial Telemetry](../assets/dimensions-workbook-initial.png) + +The top chart in the diagram above shows the end-to-end processing time for a cargo message and the chart below it shows the number of requests. + +Now that we have some baseline load through the system, kill the previous load command by pressing `Ctrl+C`, and run the following command instead: + +```bash +cat << EOF | node index.js -c - +{ + "tests": [ + { + "name": "Send cargo to cargo processing api with 50% chance of slow port", + "target": "cargo-processing-api", + "volume": 500, + "validateResults": false, + "delayBetweenCargoInMilliseconds": 1500, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0, + "chaosSettings": [ + { + "target": "cargo-processing-api", + "type": "slow-port", + "chanceToCauseChaos": 2, + "isEnabled": true + } + ] + } + } + ] +} +EOF +``` + +This command will generate and send cargo messages as before, but this time with a 50% chance that the destination port will be set to `slow-port`. The code in the `cargo-processing-validator` service has code that simulates making a call to a service at the destination port. When the the port is `slow-port` the simulation adds an extra delay. + +Now that we have some load that includes the `slow-port`, we can go back to the `Service Processing` Workbook and refresh the data. You should see a screen similar to the following: + +![Service Processing Workbook With slow-port Telemetry (Overview)](../assets/dimensions-workbook-slow1.png) + +In the top chart we can see a slight increase in the overall processing time. The chart below shows that the number of requests hasn't increased. Continuing down the charts, the bottom chart shows that the increase in processing time is due to the `cargo-processing-validator` service. + +Further down the workbook we have the "Service Breakdown" section which allows us to drill into telemetry for each of the services. From the `cloud_RoleName` dropdown, select `cargo-processing-validator` and you should see a screen similar to the following: + +![Service Processing Workbook With slow-port Telemetry (Service Breakdown)](../assets/dimensions-workbook-slow2.png) + +The top chart in the "Service Breakdown" section shows the request breakdown for the selected service (mean, median, max and 95%th centile durations) and this confirms that the increase in processing time is due to the `cargo-processing-validator` service. The chart below shows the dependency breakdown for the selected service. Looking at this chart we can see that the dependency for the simulated call to the destination port service looks normal for all ports apart from the `slow-port`. +The final chart shows the end-to-end processing time broken out by destination port, and this also highlights the increase in processing time for the `slow-port`. + +## Implementing Custom Dimensions in cargo-processing-validator + +In this section we will look at the code in the `cargo-processing-validator` service to see how the custom dimensions are implemented. The code for the `cargo-processing-validator` service is in the `src/cargo-processing-validator/src` folder. + +When a message is received from Service Bus, a `request` telemetry item is started. The code that does this is unaware of the content of the messages, so it only attaches standard fields on the telemetry item. There are two steps to adding custom dimensions to the telemetry item. First, we obtain the telemetry correlation context and set the custom properties. Secondly, we use a telemetry processor to modify the telemetry items before they are sent to Application Insights. + +The code below (from `services/ServiceBusProcessingService.ts`) shows how we obtain the telemetry correlation context and set the custom properties when processing a message from Service Bus: + +```typescript +// get the correlation context +const correlationContext = appInsights.getCorrelationContext(); +// strip commas from the destination port value as they are not allowed +const destination = validatedCargo.port.destination.replaceAll(',', ';'); +// set the custom property +correlationContext.customProperties.setProperty( + CUSTOM_PROPERTY_CARGO_DESTINATION, + destination +); +``` + +Once the correlation context is updated, we need a telemetry processor that will modify the telemetry items before they are sent to Application Insights. The code below (from `index.ts`) shows how we add a telemetry processor to the Application Insights client and use it to update the telemetry items based on the values in the correlation context: + +```typescript +const client = appInsights.defaultClient; +client.addTelemetryProcessor((envelope, contextObjects) => { + // envelope is the telemetry item being processed + // Here we set a variable to point to the properties of the telemetry item for convenience + const envelopeProperties = envelope.data?.baseData?.properties; + + // Check whether we have the destination property set on contextObjects.correlationContext + // which is the correlation context associated with the telemetry item being processed (if set). + if ( + envelopeProperties && + customProperties?.getProperty(CUSTOM_PROPERTY_CARGO_DESTINATION) + ) { + // Assign the custom dimension value on the telemetry item + envelopeProperties['cargo-destination'] = customProperties.getProperty( + CUSTOM_PROPERTY_CARGO_DESTINATION + ); + } + + // return true to allow the telemetry item through (we could return false to discard it) + return true; +}); +``` + +With these pieces in place, we can now see the custom dimension in the telemetry items that are sent to Application Insights. For example, the following query will show the request telemetry items for the `cargo-processing-validator` service and add a `destinationPort` field using the value of the `cargo-destination` custom dimension: + +```kusto +requests +| where cloud_RoleName == "cargo-processing-validator" +| extend destinationPort = customDimensions["cargo-destination"] +| order by timestamp desc +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/custom-metrics.md b/accelerators/aks-sb-azmonitor-microservices/docs/custom-metrics.md new file mode 100644 index 0000000..23cc1a1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/custom-metrics.md @@ -0,0 +1,37 @@ +# Custom Metrics + +Metrics track key indicators over time and provide a neat, numeric value that can be displayed in a time series, used in alerts, and more. The application tracks a multitude of metrics automatically, like the `jvm_memory_used` metric that measures the number of bytes used by the Java based API services. + +Organizations often have additional indicators related to specific business rules or industry-wide ones that are meaningful to track and necessary to understand system health. Custom metrics enable generation of data points over time against these metrics that aren't tracked by default. The application tracks an additional metric, `port_product_qty`, that captures the total quantity of specific products scheduled for shipment to specific ports. Ports do not have unlimited capacity to accept shipping containers. Administrators need to be able to retrieve data on an ad-hoc basis that illuminates product velocity on each port and rely on alerts that proactively notify them when the total shipping container quantity of a given product scheduled for a specific destination port exceeds some value defined by the business. + +The `valid-cargo-manager` generates the custom metric as it is the last service to interact with a valid cargo object destined for shipment to a port (invalid cargo objects are simply stored for later processing). It generates a multi-dimensional custom metric, tracking the product quantity, while passing in `product`, `source`, and `destination` dimensions taken from the cargo. + +```c# +private void TrackMultiDimensionalMetrics(ValidCargo cargo) +{ + var metric = _telemetryClient.GetMetric("port_product_qty", "product", "source", "destination", _customMetricConfiguration); + + metric.TrackValue(cargo.Product.Quantity, + cargo.Product.Name, + cargo.Port.Source, + cargo.Port.Destination); +} +``` + +Importantly, the `GetMetric` and `TrackValue` methods pre-aggregate the metric before sending the values every minute. `TrackMetric`, also exposed by the SDK, sends a separate telemetry item every time the method is called and is no longer the preffered approach for generating custom metrics. Rather than generate a new record with a specific value every time the metric is tracked, the service exports an aggregated metric record every minute that includes properties like **value**, **valueCount**, **valueSum**, **valueMin**, and **valueMax**. **valueCount** defines the number of times the metric was tracked over that minute, **valueSum** is the total sum of each of the values, etc. + +The custom metric is exported each minute for every specific custom dimension combination. All metric data tracked that includes the same `product`, `source`, and `destination` within the same minute will be grouped together in Application Insights records. If `TrackValue` is called twice within the same minute with `product-Cars, source-New York City, destination-Miami` then they will be grouped together. If, in that same minute `TrackValue` is called with `product-Cars, source-Seattle, destination-Tacoma` then that metric data is exported separately: + +![Custom Metric Dimensions](../assets/custom-metric-dimensions.png) + +The custom metric is exported to Application Insights as both a [log-based and pre-aggregated](https://learn.microsoft.com/en-us/azure/azure-monitor/app/pre-aggregated-metrics-log-metrics) metric. The pre-aggregated version is optimized for time series and enables faster, more performant queries. It _only_ maintains certain dimensions and other specific properties, in contrast with the log-based version that includes all relevant information attached to the record. To ensure that the pre-aggregated metric version has the dimensions we rely on, they must be [enabled via the App Insights resource in the Portal](https://learn.microsoft.com/en-us/azure/azure-monitor/app/pre-aggregated-metrics-log-metrics#custom-metrics-dimensions-and-pre-aggregation) after deployment (currently in Preview and unsupported in ARM). + +The alert we employ relies on the `product` and `destination` dimensions within the custom metric, alerting when the total quantity of a given `product` exceeds 1000 for a given `destination` port over a single minute interval. The alert rule maintains different time series for each `product`/`destination` combination and alerts on each separately: + +![Custom Metric Alert](../assets/custom-metric-alert.png) + +The `source` port is irrelevant. Cars sent to Miami from New York and cars sent to Miami from Boston will roll up together and the total product quantity across both will be used. If `source` was added as a dimension to the alert, for instance, these would be split into two different time series and alerted on separately. The number of ports and products used could quickly inflate the number of time series Azure Monitor maintains, resulting in throttling, reduced system performance, increased cost, etc. By default, Azure Monitor limits metrics to 1000 total time series and 100 unique values per dimension. These values can be customized and set by the TelemetryClient that originally exports the metrics. The `valid-cargo-manager` that instruments the `port_product_qty` custom metric sets series count and values per dimension limits to 100 and 40 respectively, to guard against potential scale issues. The configuration allows for 40 unique destination ports and products, with no more than 100 time series maintained: + +```c# + _customMetricConfiguration = new MetricConfiguration(seriesCountLimit: 100, valuesPerDimensionLimit: 40, new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false)); +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/distributed-tracing.md b/accelerators/aks-sb-azmonitor-microservices/docs/distributed-tracing.md new file mode 100644 index 0000000..89a6ac3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/distributed-tracing.md @@ -0,0 +1,42 @@ +# Distributed Tracing + +Distributed tracing depends on careful stitching together of auto and manually instrumented spans from both OpenTelemetry and Application Insights based tooling destined for export to Azure. + +## Azure Monitor and OpenTelemetry Data Models + +Azure Monitor splits the concept of a generic [OpenTelemetry span](https://opentelemetry.io/docs/concepts/signals/traces/#spans-in-opentelemetry) into a number of specific telemetry items like Requests and Dependencies. Rather than refer to these items as "spans", the term "operation" is heavily used in documentation and tooling. A trace is a distributed logical operation comprised of smaller sub-operations - the Requests, Dependencies, PageViews, etc. In Application Insights, all operations in a distributed trace will share the same `operation_Id` value, while ordering within the trace is defined by `operation_ParentId` values. An operation's `operation_ParentId` will point to the `Id` of another operation in the trace. + +OpenTelemetry-based tooling like the OpenTelemetry exporters for Java and the Application Insights SDK for Python (which relies on OpenCensus) use OpenTelemetry span terminology in exposed methods and classes. Spans in these tools encompass all telemetry types and generally expose a [SpanKind](https://opentelemetry.io/docs/concepts/signals/traces/#span-kind) property that dictates the type of item that surfaces in Application Insights. `SpanKind.SERVER` and `SpanKind.CLIENT` spans created in [`invalid-cargo-manager` instrumentation methods](../src/invalid-cargo-manager/src/service/message_receiver.py), for instance, result in export of Request and Dependency items in Application Insights. The `SpanId`, parent `SpanId`, and `TraceId` values in these OpenTelemetry-based libraries surface in Application Insights as `Id`, `operation_ParentId`, and `operation_Id`, respectively. + +## Instrumenting the Distributed Trace + +### Concepts + +The instrumentation process requires generation of operations (spans) with proper attachment of the `operation_Id` and `operation_ParentId` values to ensure they are connected to one another in the same trace, in the correct order. + +Each SDK/exporter exposes different methods that allow for creation of operations. The Application Insights SDK for Node [tracks specific operations](https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs#telemetryclient-api) using methods like `trackDependency()` and `trackRequest()` on its `TelemetryClient` class. The .NET SDK uses `Activity` classes and [`StartOperation()` calls](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-end-to-end-tracing?tabs=net-standard-sdk-2#trace-message-processing) exposed by its own `TelemetryClient` class to do the same. The OpenCensus based Python OpenTelemetry exporter, on the other hand, enables [creation of spans](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable?tabs=python#instrument-with-opentelemetry) via a Tracer. No spans are manually instrumented in either of the Java-based APIs, but the libraries do [expose the functionality](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable?tabs=java#add-custom-spans) to generate them, if necessary. + +In order for one service's operations to be properly tied to an operation from an upstream service, a trace context must be passed between them. OpenTelemetry based tooling uses the widely recognized [W3C Trace Context](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format) as a means to pass the required values and Application Insights is transitioning to use the same. W3C Trace Context defines a `traceparent` string that contains the Id values necessary to set a telemetry item's `operation_Id` and `operation_ParentId`. It uses the following syntax: + +`---` + +The `` value is uniquely generated by the first service in the distributed trace and becomes the `operation_Id` in Application Insights. The `` value refers to the `Id` of the most recent operation in the trace and becomes the `operation_ParentId` property for the next operation. When an upstream service makes a request or sends a message to a downstream service, it attaches the `traceparent` string in the manner dictated by the [communication protocol](https://www.w3.org/TR/trace-context-protocols-registry/#registry). Services that communicate via HTTP, like the inter-service communication between the `cargo-processing-api` and `operations-api` pass the string in the request headers, while services that communicate via message brokers, like all other inter-service communications in the application, pass the value in the message's application properties. + +### Implementation + +A distributed trace begins when a POST request is made to the `cargo-processing-api` service. The initial request is automatically instrumented and generates the `operation_Id` that will be attached to all subsequent telemetry items. The service makes a PUT request to the `operations-api` and automatically attaches the `traceparent` value in the headers, passing in the `operation_Id` and the `Id` of the last instrumented item. The last instrumented item in this case is the Dependency generated by the `cargo-processing-api` which refers to the PUT request. The `operations-api` similarly auto-instruments its own subsequent span data that is tied into the trace. It breaks open the `traceparent` string and uses the values to set trace context for the spans it will instrument. The initial span auto-instrumented by the `operations-api` when the PUT request is made surfaces as a Request, while other spans that refer to Cosmos DB interactions become Dependencies in Application Insights. + +When the `cargo-processing-api` receives a response back from the `operations-api`, it sends a message to the `ingest-cargo` Service Bus queue. The `traceparent` string is automatically passed by the service in the message's application properties and is received by the `cargo-processing-validator`. The `` value passed in the `traceparent` that becomes the `operation_ParentId` now refers to the message send Dependency item generated by the `cargo-processing-api`. The first operation produced by the `cargo-processing-validator` must be parented to this value. The service pulls the necessary `operation_Id` and `operation_ParentId` from the `traceparent` and uses the values in [pre-processor functionality](../src/cargo-processing-validator/src/index.ts) to attach the proper `operation_ParentId` to telemetry items prior to export. After manually instrumenting a request and a number of dependencies related to Service Bus operations and custom business logic, the `cargo-processing-validator` service sends a message to the `validated-cargo` Service Bus Topic. The `traceparent` string is again passed in the message's application properties. While the Java API services automatically attach the `traceparent` string, the `cargo-processing-validator` attaches the value [manually](../src/cargo-processing-validator/src/services/ServiceBusWithTelemetry.ts) in a `Diagnostic-Id` property. + +The `valid-cargo-manager` and `invalid-cargo-manager` are both prepared to pull the `operation_Id` and `operation_ParentId` values from the `Diagnostic-Id`. The `valid-cargo-manager` uses the values to [manually instrument a request](../src/valid-cargo-manager/Services/SubscriptionReceiver.cs), then begins automatically instrumenting Cosmos DB and Service Bus operations. The `invalid-cargo-manager` does the same to manually instrument a request, but follows with a [series of manually instrumented dependencies](../src/invalid-cargo-manager/src/service/message_receiver.py) that refer to the same Cosmos DB and Service Bus operations. + +## Visualization and Analysis + +The generated distributed traces through the valid and invalid flows are easily viewable in the Application Insights [Transaction Diagnostics window](https://learn.microsoft.com/en-us/azure/azure-monitor/app/transaction-diagnostics#transaction-diagnostics-experience): + +![Distributed Trace - Valid Flow](../assets/full-trace-valid.png) +![Distributed Trace - Invalid Flow](../assets/full-trace-invalid.png) + +Transaction Diagnostics displays a distributed trace's individual components with their timing and success properties. It is a visual representation of a KQL query that pulls all operation data associated with a specific `operation_Id`. The interface quickly reveals where issues arose within a specific trace. Inspecting individual traces is a helpful debugging tool, especially combined with correlated logs that provide some additional level of detail about why an operation may have failed. + +Aggregated trace data allows for construction of an application topology, visible within the [Application Map](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-map?tabs=net), and supports a number of monitoring functionalities that the application relies on. Using operation timing and failure properties, performance data can be quickly retrieved and filtered by service, helping to identify which components in the application may be experiencing failure or performance issues. Combined, they enable retrieval of end to end transaction duration. KQL queries that pull end to end and per-service failure and performance data are heavily used in Workbooks and automated Alert rules. diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/getting-started.md b/accelerators/aks-sb-azmonitor-microservices/docs/getting-started.md new file mode 100644 index 0000000..97c8cf7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/getting-started.md @@ -0,0 +1,108 @@ +# Getting Started + +## Prerequisites + +Visual Studio Code and dev containers are used to automatically install the required packages necessary to deploy and run the application. To get started, you will need to have the following installed: + +- Docker ([link](https://docs.docker.com/get-docker/)) +- Visual Studio Code ([link](https://code.visualstudio.com/download)) + - Dev Containers extension ([link](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)) + +Alternatively, you can deploy and run the application from your local machine but will need to have the following additionally installed: + +- Azure CLI ([link](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)) +- Azure Kubelogin ([link](https://github.com/Azure/Kubelogin)) +- Kubectl ([link](https://kubernetes.io/docs/reference/kubectl/)) +- Helm ([link](https://helm.sh/)) +- Various command line tools ([figlet](http://www.figlet.org/), [jq](https://stedolan.github.io/jq/)) + +## Running the Application + +Open the repository in Visual Studio Code. If you have the DevContainer extension installed, you will be prompted to "Reopen in Container" to work using the DevContainer. + +Copy the `.env.sample` file to `.env` and fill in the required values. + +The sample uses either Bicep or Terraform to provision the required infrastructure. Run `./deploy-bicep.sh` to deploy the application to Azure using Bicep, or `./deploy-terraform.sh` to do so using Terraform. The scripts will create the required resources in Azure, build the docker images, push them to Azure Container Registry and deploy the containers to Azure Kubernetes Service (AKS). + +> **_NOTE:_** By default, the AKS cluster is deployed without AAD integration. To enable AAD integration, pass the `--aks-aad-auth` switch to the deployment script. This will configure authentication for the current `az` user. To configure for a service principal, set the `ARM_CLIENT_ID` value to the client ID for the service principal. + +## Sending Requests + +After deploying the application, you can use the [`cargo-processing-api.http`](../http/cargo-processing-api.http) file to send requests to it. + +The file contains a number of requests that can be sent to the cargo-processing-api service. It uses an `.env` file generated by the deployment script that contains the IP address of the AKS NGINX ingress controller. + +Use the "Send Request" options in the file to send `POST`/`PUT` requests to the cargo-processing-api and see the responses. + +> **_NOTE:_** By default, the `cargo-processing-api.http` file is configured to use services deployed to AKS. If you are running the services locally, uncomment the lines that set the service address to `localhost`. + +## Verifying Successful Deployment + +A cargo object sent to the `cargo-processing-api` service can take one of two paths depending on the validation result from the `cargo-processing-validator` service. The first path, when the cargo is valid, incorporates the `cargo-processing-api`, `operations-api`, `cargo-processing-validator` , and `valid-cargo-manager` services and results in a record being stored in the `valid-cargo` Cosmos DB container. An invalid piece of cargo reaches the `invalid-cargo-manager` rather than the `valid-cargo-manager` service and is stored in the `invalid-cargo` Cosmos DB container. End to end functionality can be verified by sending a request through both flows and ensuring that the cargo objects are stored in the proper Cosmos DB containers. + +The [`cargo-processing-api.http`](../http/cargo-processing-api.http) file contains `createRequest` and `createRequest_invalid` requests that are used to send a valid and invalid cargo object to the `cargo-processing-api` service, respectively. Use the "Send Request" option on `createRequest` to send a valid request and note the ID returned in the right-hand window (`080f393d-893c-3d80-a267-350c6abef090` in the below example). + +```json +HTTP/1.1 202 +Date: Tue, 25 Apr 2023 21:46:07 GMT +Content-Type: application/json +Transfer-Encoding: chunked +Connection: close +operation-id: 49d8f01c-a284-44b4-8c97-605d224016af + +{ + "id": "080f393d-893c-3d80-a267-350c6abef090", + "timestamp": "2023-04-25T21:46:06.310Z", + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2023-05-05T09:45:52.548Z", + "end": "2023-05-10T09:45:52.548Z" + } +} +``` + +The subsequent request in the `.http` file can be used to retrieve the status of that request. Next, use the "Send Request" option on `createRequest_invalid` to send a invalid request and note the ID returned in the right-hand window (`8438307f-8303-3d9c-b958-9caf08f610b4` in the below example). + +```json +HTTP/1.1 202 +Date: Tue, 25 Apr 2023 21:48:16 GMT +Content-Type: application/json +Transfer-Encoding: chunked +Connection: close +operation-id: 9d3bdc2f-a4aa-45e5-8965-d9e53716c1e7 + +{ + "id": "8438307f-8303-3d9c-b958-9caf08f610b4", + "timestamp": "2023-04-25T21:48:16.438Z", + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2023-07-04T09:48:20.816Z", + "end": "2023-07-09T09:48:20.816Z" + } +} +``` + +Finally, navigate to the Cosmos DB instance's Data Explorer window and verify that a new record has been added to both the `valid-cargo` and `invalid-cargo` containers with IDs and other properties that match the ones copied earlier. + +![Valid cargo verification](../assets/verify-valid-cargo.png) +![Invalid cargo verification](../assets/verify-invalid-cargo.png) + +## Local Development + +To run the services locally, you still need to deploy the supporting infrastructure in Azure. You can run the deployment scripts described in the [Running the Application](#running-the-application) section, but pass the `--skip-helm-deploy` switch to skip the Helm deployment of services to AKS. This will ensure that the services you run locally will be the only services retrieving messages from the Service Bus queues etc. + +After the infrastructure deployment completes, run `run-local.sh` to start all of the services locally via `docker compose`. To run a service individually, open it in its dev container and follow the instructions provided in the service's README. diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/health-checks.md b/accelerators/aks-sb-azmonitor-microservices/docs/health-checks.md new file mode 100644 index 0000000..6db9391 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/health-checks.md @@ -0,0 +1,66 @@ +# Health Checks + +Monitoring and quickly responding to changes in service health is crucial for distributed applications deployed to an AKS environment. Health checks report the internal status of a microservice at regular intervals and are used by orchestrators, like Kubernetes, to determine if each service is functioning properly. Health checks should examine connections to databases and other dependencies and can report health based on memory usage, CPU utilization, network connectivity, or any other key performance indicators that are critical to the functioning of the microservice. Essentially, a health check should verify that the microservice is able to perform its intended function and that it is not experiencing any critical errors or failures. AKS automatically triggers these health checks and acts upon pods that report back unhealthy. + +Health check functionality is often exposed via HTTP endpoints, but Kubernetes supports consumption of TCP and gRPC endpoints as well and is also capable of running `exec` commands exposed by pods. Kubernetes consumes the endpoints or commands via [3 types of probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) - startup, readiness, and liveness probes. Startup probes run after deployment and make the kubelet agent aware that the containers in the pod have started. Kubernetes will not start readiness and liveness probes until the startup probe reports success. Readiness probes alert Kubernetes that the pod is ready to accept traffic and liveness probes are subsequently used to regularly check that the pod is healthy. Pods that fail liveness probes are automatically restarted by AKS to fix ephemeral issues. While different endpoints or commands can be used for each probe type, we elected to reuse the same health check endpoints in our services, declared via the helm charts that deploy the services to AKS. + +Services like the `cargo-processing-api` and `operations-api`, which are Spring Boot apps that already expose HTTP endpoints, are easy candidates to expose health checks via HTTP endpoint. `spring-boot-starter-actuator` used in these projects is capable of [exposing a `/health` endpoint](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints) that reports internal application health using indicators like [dependency connections and disk space](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.health.auto-configured-health-indicators). The endpoint is configured via the [application.properties](../src/cargo-processing-api/src/main/resources/application.properties) file: + +```java +management.endpoints.web.exposure.include=health,info +endpoints.health.sensitive=false +management.endpoint.health.show-details=always +``` + +The `/actuator/health` endpoint that Spring Boot spins up is declared within the helm charts for those services: + +```yaml +livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 10 +``` + +The `cargo-processing-validator`, `valid-cargo-manager`, and `invalid-cargo-manager` are background worker services that do not already expose HTTP endpoints. The `cargo-processing-validator` and `invalid-cargo-manager` do not include explicit health checks. Instead, they are designed to [self-destruct](../src/cargo-processing-validator/src/index.ts) when errors occur. These services restart via error when failed dependency connections arise, rather than failed liveness probes that would result from those same connections. In contrast, we elected to demonstrate TCP health check functionality on the `valid-cargo-manager`. A [HealthCheckController](../src/valid-cargo-manager/Controllers/HealthCheckController.cs) that starts a TCP server is added to the list of [services configured during startup](../src/valid-cargo-manager/Program.cs). The controller uses [CosmosDBHealthChecker](../src/valid-cargo-manager/HealthCheck/CosmosDbHealthChecker.cs) and [ServiceBusHealthChecker](../src/valid-cargo-manager/HealthCheck/ServiceBusHealthChecker.cs) classes to report status of connection to those dependent services. The exposed TCP port and other configuration details are set via the [appsettings.json file](../src/valid-cargo-manager/appsettings.sample.json): + +```json +"HealthCheck": { + "TcpServer": { + "Port": 3030 + }, + "CosmosDB": { + "MaxDurationMs": 200 + }, + "ServiceBus": { + "MaxDurationMs": 200 + } +} +``` + +The TCP socket that the service exposes is then declared within its helm chart: + +```yaml +livenessProbe: + tcpSocket: + port: 3030 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 10 +``` + +Kubernetes automatically consumes these endpoints and will take action on a pod if a probe fails, like a pod restart if a liveness probe fails. The calls to these endpoints can be viewed in the Logs window, via the `requests` table: + +```sql +requests +| where cloud_RoleName == "cargo-processing-api" and url contains "/health" +``` + +![Health check logs](../assets/health-check-logs.png) + +While Kubernetes will automatically respond to these events, the application additionally includes alerts that proactively notifies admins about issues related to health checks so they can take additional action to debug, if necessary. Each microservice has a health check failure and health check not reporting alert that consumes the same logs used above, as well as a pod restart alert triggered when a service pod restarts more than once within 5 minutes. +Health checks often fail due to ephemeral issues that can be resolved by automatic Kubernetes actions, like a pod restart, but other underlying issues may require human intervention. Alerts offer an additional monitoring layer that serves to reduce the downtime to fix those more major issues surfaced by health check issues. diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/introducing-chaos.md b/accelerators/aks-sb-azmonitor-microservices/docs/introducing-chaos.md new file mode 100644 index 0000000..5c5333b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/introducing-chaos.md @@ -0,0 +1,25 @@ +# Introducing Chaos + +Chaos engineering involves intentionally introducing failures to assess resilience and identify potential weaknesses in an application. Controlled experiments are conducted to understand how the application behaves in unexpected situations. Development teams can identify proper mitigation techniques for real-world scenarios _before_ they occur in production. Chaos engineering is closely tied with the concepts of observability and monitoring - system behavior must be accurately measured over time to understand how it responds to various failure scenarios. Introduction of chaos into the cargo processing application allows us to test the alerting and visualization functionality included in the project, as well as use those same tools to determine best case mitigation techniques for a set of fault scenarios that the team expects the application to handle gracefully. + +Azure offers [Azure Chaos Studio](https://learn.microsoft.com/en-us/azure/chaos-studio/chaos-studio-overview) as a tool to inject common fault scenarios into the application, like CPU/memory pressure or downed nodes in a cluster. Rather than use Chaos Studio, we elected add chaos into the application code directly, in both the `cargo-processing-api` and `cargo-processing-validator` services, with built in integration with our existing load test scripts. + +The [cargo-test-scripts](../src/cargo-test-scripts/) folder includes a JavaScript based application used to send requests to the `cargo-processing-api` ingress endpoint or to downstream services directly. Tests are supplied via [json based test run configurations](../src/cargo-test-scripts/testConfigurations/valid_tests.json) that send a configurable number of requests to specific target services. Importantly, `properties.chaosSettings` is available on tests that target the `cargo-processing-api` and `cargo-processing-validator` services, with a set of available `type` options that cause specific fault scenarios in those services: + +| Target | Type | Description | +| -------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| cargo-processing-api | operations-api-failure | Will cause a chaos exception to occur when the cargo-processing-api attempts to call the operations-api. | +| cargo-processing-api | process-ending | Will cause the cargo-processing-api to shut down. | +| cargo-processing-api | service-bus-failure | Will cause the service to close the service-bus connection right before it attempts to use it. | +| cargo-processing-api | invalid-schema | Will cause the test script to modify the cargo object being sent in a way that causes the cargo-processing-api to throw an invalid json schema exception. | +| cargo-processing-validator | service-bus-failure | Will cause the service to close the service-bus connection right before it attempts to use it. | +| cargo-processing-validator | process-killing | Will cause the cargo-processing-validator to shut down. | +| cargo-processing-validator | invalid-schema | Sends a message that is missing its demandDates directly to the ingest-cargo queue. | + +The test scripts use a [raiseChaos utility function](../src/cargo-test-scripts/dataBuilderUtils.js) that sets a cargo object's `source` port to the `target` and `destination` port to the `type` specified above in a chaos test. The services themselves are configured to execute fault scenarios when the source and destination ports match these known strings. The `cargo-processing-validator` service contains a [ChaosMonkey](../src/cargo-processing-validator/src/chaos/ChaosMonkey.ts) class that determines whether to cause chaos based on the source and destination ports. It includes [ProcessEnding](../src/cargo-processing-validator/src/chaos/ProcessEndingMonkey.ts) and [ServiceBusKilling](../src/cargo-processing-validator/src/chaos/ServiceBusKillingMonkey.ts) classes that exit the running process or close the existing service bus connection, respectively. If the `source` port for a cargo object is set to `cargo-processing-validator` and `destination` port is set to `process-killing`, the `ProcessEndingMonkey` will initialize and exit the current process, for example. The `cargo-processing-api` service has similar ChaosMonkey implementations. + +The chaos tests should cause internal exceptions, restarts, and health check issues that should immediately surface in alerts (detailed below). Workbook tiles (detailed below) should illuminate how the application performed over the test run, displaying increases in request duration, dead-lettered messages, and other indicators of application health. To run a chaos test, open the [cargo-test-scripts](../src/cargo-test-scripts/) folder in its dedicated dev container. The folder contains a number of [pre-defined test configurations](../src/cargo-test-scripts/testConfigurations/) that includes a [`cargo_processing_api_chaos_tests.json`](../src/cargo-test-scripts/testConfigurations/cargo_processing_api_chaos_tests.json) configuration. From the terminal in the dev container, run the following command to trigger each of the types of fault scenarios listed above: + +```bash +node ./index.js -c ./testConfigurations/cargo_processing_api_chaos_tests.json +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/reducing-telemetry-volume.md b/accelerators/aks-sb-azmonitor-microservices/docs/reducing-telemetry-volume.md new file mode 100644 index 0000000..69576e2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/reducing-telemetry-volume.md @@ -0,0 +1,11 @@ +# Reducing Telemetry Volume + +The application does not have high scale requirements, though there are a variety of techniques that were used, or could be used, to mitigate storage and cost concerns. + +The `valid-cargo-manager` and both Java APIs [implement adaptive sampling by default](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling?tabs=net-core-new#configuring-adaptive-sampling-for-aspnet-applications), limiting the number of requests sent to Application Insights. Both services target a specific number of items to export per minute - the actual sampling rate can vary depending on the number of requests the services handle. Given the low scale requirements for the application, sampling does not actually kick in for either of these services with the current test scripts provided. Implementing coordinated fixed-rate sampling across the service architecture would result in reduced storage costs and alleviate retention and rotation concerns. The Java services can implement explicit [fixed rate sampling and sampling overrides](https://learn.microsoft.com/en-us/azure/azure-monitor/app/java-standalone-sampling-overrides#getting-started) via the [`applicationinsights.json`](../src/cargo-processing-api/applicationinsights.json) file, or by supplying specific environment variables that overwrite those properties. The `cargo-processing-validator` can do the same by [providing a percentage](https://github.com/microsoft/ApplicationInsights-node.js/blob/dd7c195f481acdaf39c4abc271424fb750aac81f/README.md#sampling) to the `applicationInsights.defaultClient` in the [`index.ts`](../src/cargo-processing-validator/src/index.ts) file. The `valid-cargo-manager` can [add a sampling rate](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling?tabs=net-core-new#configuring-fixed-rate-sampling-for-aspnet-applications) to the [`CreateHostBuilder`](../src/valid-cargo-manager/Program.cs), while the `invalid-cargo-manager` could do so via a `ProbabilitySampler` that can be [passed to tracer classes](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling?tabs=net-core-new#configuring-fixed-rate-sampling-for-opencensus-python-applications), rather than the `AlwaysOnSampler` that is [currently used](../src/invalid-cargo-manager/src/service/telemetry_publisher.py). + +Especially noisy libraries or specific telemetry items can be targeted with pre-processing functionality that serves to suppress or transform those items. The `valid-cargo-manager` suppresses items from the `Microsoft` namespace that do not meet or exceed the `Warning` severity level, for instance, via the [appsettings.json file](../src/valid-cargo-manager/appsettings.sample.json). The `cargo-processing-validator` includes a [code based pre-processor](../src/cargo-processing-validator/src/index.ts) used to transform outgoing telemetry items before export, as does the [`invalid-cargo-manager`](../src/invalid-cargo-manager/src/service/telemetry_publisher.py). The existing functionalities could be extended to suppress or remove unnecessary properties from additional items, and similar pre-processing functionality could be added to the [Java](https://learn.microsoft.com/en-us/azure/azure-monitor/app/java-standalone-telemetry-processors#getting-started) services via configuration file. + +[Log levels](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-7.0) are used to accommodate the fact that certain services automatically instrument telemetry data that is not necessary to capture by default. The `cargo-processing-validator` and `invalid-cargo-manager` services do not emit nearly as much data as the other three services and are [configured to capture all logs](../src/invalid-cargo-manager/src/service/logging_config.py) using DEBUG. The Java based APIs, on the other hand, are configured to [capture all logs of level INFO and above](../src/cargo-processing-api/applicationinsights.json), automatically suppressing debug statements, by default. As mentioned, the `valid-cargo-manager` uses the appsettings.json file to [capture WARNING and above](../src/valid-cargo-manager/appsettings.sample.json) for logs from the Microsoft namespace and INFO for others. These configuration based log levels can be easily reduced to DEBUG to capture additional logs in a debugging scenario. The application uses the default retention policy for all Azure Monitor tables - generally 90 days but certain tables have a 30 day default retention policy, though these could be [set on the Log Analytics resource](https://learn.microsoft.com/en-us/azure/templates/microsoft.operationalinsights/workspaces?pivots=deployment-language-bicep#workspaceproperties) in Bicep or Terraform. + +Visit Azure documentation for additional information on [sampling](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling?tabs=net-core-new), [pre-processing](https://learn.microsoft.com/en-us/azure/azure-monitor/app/api-filtering-sampling), log levels and [retention](https://learn.microsoft.com/en-us/azure/azure-monitor/app/data-retention-privacy), and more. diff --git a/accelerators/aks-sb-azmonitor-microservices/docs/workbooks.md b/accelerators/aks-sb-azmonitor-microservices/docs/workbooks.md new file mode 100644 index 0000000..0b8772a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/docs/workbooks.md @@ -0,0 +1,74 @@ +# Workbooks + +The application utilizes Azure Workbooks to visualize/analyze the extensive telemetry data that has been captured by the centralized Azure Monitor backend. Workbooks allow you to seamlessly display and track all relevant configured data within the Azure Portal, without the need to navigate away. + +Workbooks can be deployed using infrastructure as code tools, similar to other Azure services. In this scenario, the deployment creates three distinct workbooks, each focusing on specific categories that feature the following charts: + +| Workbook | Chart | Type | Description | +| ----------------- | -------------------------------- | --------- | ------------------------------------------------------------------------------------------------- | +| Index | servicesExceptionsQuery | KQL Query | Displays exceptions that ocurred while working with the system. | +| Index | servicesMonitoringQuery | KQL Query | Displays the big picture of resources per service. | +| Index | workbooksLinksText | Text | Includes the links to access to remaining workbooks. | +| Infrastructure | serviceBusCompletedTimesQuery | KQL Query | Displays statistics of service bus completed operations. | +| Infrastructure | serviceBusMessagingMetric | Metric | Displays the count of active, delivered and dead-lettered messages in a Queue or Topic. | +| Infrastructure | serviceBusThrottledMetric | Metric | Displays the number of throttled requests in Service Bus. | +| Infrastructure | cosmosDbLatencyOfReadsQuery | KQL Query | Displays the average time per read requests from Cosmos DB. | +| Infrastructure | cosmosDbOperationsQuery | KQL Query | Displays the number of valid, invalid, and operations writes into CosmosDB. | +| Infrastructure | keyVaultSaturationMetric | Metric | Displays the KeyVault saturation percentage. | +| Infrastructure | keyVaultLatencyMetric | Metric | Displays the latency when executing an operation to KeyVault. | +| Infrastructure | keyVaultResultsMetric | Metric | Displays the count of Key Vault API Results. | +| Infrastructure | aksCpuMetric | Metric | Displays the max count of CPU percentage of the cluster. | +| Infrastructure | aksRequestsMetric | Metric | Displays the average inflight requests to the cluster. | +| System processing | endpointsRequestsStatisticsQuery | KQL Query | Displays different measures for time per requests. | +| System processing | endpointsRequestsQuery | KQL Query | Extracts the last column from previous chart in order to gain more focus. | +| System processing | lastOperationsQuery | KQL Query | Shows the last 100 operations executed and their associated operation ID. | +| System processing | transactionSearchBladeText | Text | Link to a transaction search blade. | +| System processing | additionalTelemetryText | Text | Link to get more telemetry in sections like Application Map, Availability, Failures, Performance. | +| System processing | operationsParameters | KQL Query | Parameters designed to get more details in the following charts. | +| System processing | endToEndProcessingQuery | KQL Query | Displays the end to end processing time. | +| System processing | requestsCountQuery | KQL Query | Displays the request count. | +| System processing | servicesProcessingTimeQuery | KQL Query | Displays the processing time in the services. | +| System processing | serviceDependencyQuery | KQL Query | Displays the service dependency duration. | +| System processing | destinationPortBreakdownQuery | KQL Query | Displays the end to end processing time by destination port. | +| System processing | podRestartQuery | KQL Query | Displays the number of times each service pod has restarted. | + +No matter what infrastructure deployment tool is used, workbooks content is supplied via the same set of **json** templates, found in the [workbooks](../infrastructure/workbooks/) folder. The templates demonstrate proper workbook structure/syntax and include a variety of types of visualization items [available for use in workbooks](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-visualizations), like text and charts. The templates also illustrate how to pass required parameters from Bicep and Terraform to the workbooks json content, like the IDs of the source resources for log query and metric visualizations. In the following snippet, a metric chart receives the ID of the AKS cluster and uses it in the **resourcesIds** field. + +```json +{ + "type": 10, + "content": { + "chartId": "workbook171b383f-5043-41dd-9154-a1fa92367891", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "pink", + "resourceType": "microsoft.containerservice/managedclusters", + "metricScope": 0, + "resourceIds": ["${aks_id}"], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.containerservice/managedclusters", + "metric": "microsoft.containerservice/managedclusters-Nodes (PREVIEW)-node_cpu_usage_percentage", + "aggregation": 3, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "aksCpuMetric" +} +``` + +Development teams can adapt the workbook presentation according to how they want to visualize data. Chart colors, for instance, can be used to visually separate the tools they are monitoring, allowing for easy identification what resource and signal is being observed: + +![Workbooks Key Vault metric](../assets/workbook-key-vault-metric.png) +![Workbooks AKS metric](../assets/workbook-aks-metric.png) + +Azure Workbooks can provide a dynamic presentation that captures all relevant data in one single visualization tool, enabling creation of a single pane of glass for application administrators. Not all projects will look for the same telemetry, as each solution will focus on different metrics according to their specific needs. diff --git a/accelerators/aks-sb-azmonitor-microservices/http/.env.sample b/accelerators/aks-sb-azmonitor-microservices/http/.env.sample new file mode 100644 index 0000000..28a43c7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/http/.env.sample @@ -0,0 +1,6 @@ +# Copy this file to .env and fill in the values. + + +# Run the following command to get the SERVICE_IP value: +# kubectl get svc --namespace default cargo-processing-api --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}" +SERVICE_IP= \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/http/cargo-processing-api.http b/accelerators/aks-sb-azmonitor-microservices/http/cargo-processing-api.http new file mode 100644 index 0000000..2b88fe4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/http/cargo-processing-api.http @@ -0,0 +1,122 @@ +# This file shows how to make requests against the deployed API +# The following lines load the IP address for the deployed services from a .env file +# This file is created for you when you deploy the services +@cargo_service=http://{{$dotenv SERVICE_IP}} +@operations_service=http://{{$dotenv SERVICE_IP}} + +# Uncomment the following lines to use locally running services +# @cargo_service=http://localhost:8080 +# @operations_service=http://localhost:8081 + + +# +# issue a POST request to create a valid cargo request +# +# @name createRequest +POST {{cargo_service}}/cargo/ +Content-Type: application/json +operation-id: {{$guid}} + +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 10 d}}Z", + "end": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 15 d}}Z" + } +} + +### +# issue a PUT request to update the previous cargo request +# + +PUT {{cargo_service}}/cargo/{{createRequest.response.body.id}} +Content-Type: application/json + +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Seattle" + }, + "demandDates": { + "start": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 10 d}}Z", + "end": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 15 d}}Z" + } +} + + +### +# issue a GET request to retrieve the status of the previous cargo request +# +GET {{operations_service}}/operations/{{createRequest.response.headers.operation-id}} + +############################################################### + +# +# issue a POST request to create a valid cargo request (start date cannot be more than 60 days in the future) +# + +# @name createRequest_invalid +POST {{cargo_service}}/cargo/ +Content-Type: application/json +operation-id: {{$guid}} + +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 70 d}}Z", + "end": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 75 d}}Z" + } +} + + + +### +# issue a GET request to retrieve the status of the previous cargo request +# + +GET {{operations_service}}/operations/{{createRequest_invalid.response.headers.operation-id}} + + +############################################################### +# Test degraded behaviour: + +### +# issue a POST request to create a cargo request with processing delays +# (destination port slow-port) +# +POST {{cargo_service}}/cargo/ +Content-Type: application/json +operation-id: {{$guid}} + +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "slow-port" + }, + "demandDates": { + "start": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 10 d}}Z", + "end": "{{$localDatetime "YYYY-MM-DDThh:mm:ss.ms" 15 d}}Z" + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/abbreviations.json b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/abbreviations.json new file mode 100644 index 0000000..a4fc9df --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/azuredeploy.parameters.sample.json b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/azuredeploy.parameters.sample.json new file mode 100644 index 0000000..269e46c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/azuredeploy.parameters.sample.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "value": "eastus" + }, + "uniqueUserName": { + "value": "myusername" + }, + "cosmosDatabaseName": { + "value": "cargo" + }, + "cosmosContainer1Name": { + "value": "valid-cargo" + }, + "cosmosContainer2Name": { + "value": "invalid-cargo" + }, + "cosmosContainer3Name": { + "value": "operations" + }, + "serviceBusQueue1Name": { + "value": "ingest-cargo" + }, + "serviceBusQueue2Name": { + "value": "operation-state" + }, + "serviceBusTopicName": { + "value": "validated-cargo" + }, + "serviceBusSubscription1Name": { + "value": "valid-cargo" + }, + "serviceBusSubscription2Name": { + "value": "invalid-cargo" + }, + "serviceBusTopicRule1Name": { + "value": "valid" + }, + "serviceBusTopicRule2Name": { + "value": "invalid" + }, + "notificationEmailAddress": { + "value": "alias@microsoft.com" + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/main.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/main.bicep new file mode 100644 index 0000000..3b2cb59 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/main.bicep @@ -0,0 +1,219 @@ +targetScope = 'subscription' + +//parameters section +@description('Specifies the supported Azure location (region) where the resources will be deployed') +@minLength(1) +param location string + +@description('This value will explain who is the author of specific resources and will be reflected in every deployed tool') +@minLength(1) +param uniqueUserName string + +@description('Name for the Cosmos DB SQL database') +@minLength(1) +param cosmosDatabaseName string + +@description('Name for the first Cosmos DB SQL container') +@minLength(1) +param cosmosContainer1Name string + +@description('Name for the second Cosmos DB SQL container') +@minLength(1) +param cosmosContainer2Name string + +@description('Name for the third Cosmos DB SQL container') +@minLength(1) +param cosmosContainer3Name string + +@description('Name for the first Service Bus Queue') +@minLength(1) +param serviceBusQueue1Name string + +@description('Name for the second Service Bus Queue') +@minLength(1) +param serviceBusQueue2Name string + +@description('Name for the Service Bus Topic') +@minLength(1) +param serviceBusTopicName string + +@description('Name for the first Service Bus Subscription') +@minLength(1) +param serviceBusSubscription1Name string + +@description('Name for the second Service Bus Subscription') +@minLength(1) +param serviceBusSubscription2Name string + +@description('Name for the first Service Bus Subscriptions filter rule') +@minLength(1) +param serviceBusTopicRule1Name string + +@description('Name for the second Service Bus Subscriptions filter rule') +@minLength(1) +param serviceBusTopicRule2Name string + +@description('Tenant Id for the service principal that will be in charge of KeyVault access') +@minLength(1) +param kvTenantId string = tenant().tenantId + +@description('Definition Id for AcrPull role') +@minLength(1) +param roleAcrPull string = 'b24988ac-6180-42a0-ab88-20f7382dd24c' + +@description('Configure Azure Active Directory authentication for Kubernetes cluster') +param aksAadAuth bool = false + +@description('The object ID of the Azure Active Directory user to make cluster admin (only valid if aksAadAuth is true)') +param aksAadAdminUserObjectId string = '' + +@description('Email address for alert notifications') +@minLength(1) +param notificationEmailAddress string + +//load abbreviations for Azure features +var abbrs = loadJsonContent('abbreviations.json') + +//variables section +var toolName = 'bicep' +var resourceGroupName = '${abbrs.resourcesResourceGroups}${toolName}-${uniqueUserName}' +var acrName = '${abbrs.containerRegistryRegistries}${toolName}${uniqueUserName}' +var kvName = '${abbrs.keyVaultVaults}${toolName}-${uniqueUserName}' +var appInsightsName = '${abbrs.insightsComponents}${uniqueUserName}' +var logAnalyticsName = '${abbrs.operationalInsightsWorkspaces}${toolName}-${uniqueUserName}' +var aksName = '${abbrs.containerServiceManagedClusters}${toolName}-${uniqueUserName}' +var cosmosDBName = '${abbrs.documentDBDatabaseAccounts}${toolName}-${uniqueUserName}' +var serviceBusName = '${abbrs.serviceBusNamespaces}${toolName}-${uniqueUserName}' + +//resourceGroup section +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: location +} + +resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: subscription() + name: roleAcrPull +} + +//modules section +module acr 'modules/acr.bicep' = { + name: 'acrDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + acrName: acrName + aksPrincipalId: aks.outputs.clusterPrincipalID + roleDefinitionId: contributorRoleDefinition.id + } +} + +module kv 'modules/key-vault.bicep' = { + name: 'keyVaultDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + kvName: kvName + kvTenantId: kvTenantId + serviceBusNamespaceName: serviceBus.outputs.serviceBusNamespaceName + appInsightsConnectionString: appInsights.outputs.connectionString + logAnalyticsWorkspaceId: appInsights.outputs.workspaceId + clusterKeyVaultSecretProviderObjectId: aks.outputs.clusterKeyVaultSecretProviderObjectId + cosmosDBEndpoint: cosmos.outputs.cosmosDBEndpoint + cosmosDBAccountName: cosmos.outputs.cosmosDBAccountName + } +} + +module appInsights 'modules/app-insights.bicep' = { + name: 'appInsightsDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + appInsightsName: appInsightsName + logAnalyticsName: logAnalyticsName + } +} + +module workbook 'modules/workbooks.bicep' = { + name: 'workbookDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + workspaceId: appInsights.outputs.workspaceId + uniqueUserName: uniqueUserName + serviceBusNamespaceId: serviceBus.outputs.serviceBusNamespaceId + appInsightsId: appInsights.outputs.appInsightsId + keyVaultId: kv.outputs.kvId + aksId: aks.outputs.clusterId + } +} + +module aks 'modules/aks.bicep' = { + name: 'kubernetesDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + aksName: aksName + logAnalyticsWorkspaceId: appInsights.outputs.workspaceId + aksAadAuth: aksAadAuth + aksAadAdminUserObjectId: aksAadAdminUserObjectId + } +} + +module cosmos 'modules/cosmos.bicep' = { + name: 'cosmosDBDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + accountName: cosmosDBName + databaseName: cosmosDatabaseName + container1Name: cosmosContainer1Name + container2Name: cosmosContainer2Name + container3Name: cosmosContainer3Name + logAnalyticsWorkspaceId: appInsights.outputs.workspaceId + } +} + +module serviceBus 'modules/service-bus.bicep' = { + name: 'serviceBusDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + serviceBusName: serviceBusName + serviceBusQueue1Name: serviceBusQueue1Name + serviceBusQueue2Name: serviceBusQueue2Name + serviceBusTopicName: serviceBusTopicName + serviceBusSubscription1Name: serviceBusSubscription1Name + serviceBusSubscription2Name: serviceBusSubscription2Name + serviceBusTopicRule1Name: serviceBusTopicRule1Name + serviceBusTopicRule2Name: serviceBusTopicRule2Name + logAnalyticsWorkspaceId: appInsights.outputs.workspaceId + } +} + +module alerts 'modules/alerts.bicep' = { + name: 'alertsDeploy' + scope: resourceGroup + params: { + location: resourceGroup.location + actionGroupName: 'default-actiongroup' + notificationEmailAddress: notificationEmailAddress + cosmosDBId: cosmos.outputs.cosmosDBId + keyVaultId: kv.outputs.kvId + serviceBusNamespaceId: serviceBus.outputs.serviceBusNamespaceId + aksClusterId: aks.outputs.clusterId + appInsightsId: appInsights.outputs.appInsightsId + logAnalyticsWorkspaceId: appInsights.outputs.workspaceId + } +} + +//output section +output rg_name string = resourceGroup.name +output insights_name string = appInsights.outputs.insightsName +output sb_namespace_name string = serviceBus.outputs.serviceBusNamespaceName +output cosmosdb_name string = cosmos.outputs.cosmosDBAccountName +output kv_name string = kv.outputs.kvName +output acr_name string = acr.outputs.acrName +output aks_name string = aks.outputs.clusterName +output aks_key_vault_secret_provider_client_id string = aks.outputs.clusterKeyVaultSecretProviderClientId +output tenant_id string = subscription().tenantId diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/acr.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/acr.bicep new file mode 100644 index 0000000..a5af59b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/acr.bicep @@ -0,0 +1,45 @@ +@description('Default value obtained from resource group, it can be overwritten') +@minLength(1) +param location string = resourceGroup().location + +@description('Name for the ACR') +@minLength(1) +param acrName string + +@description('The principal ID of the AKS cluster') +@minLength(1) +param aksPrincipalId string + +@description('Built-in role for role assignment') +@minLength(1) +param roleDefinitionId string + +@description('Expected ACR sku') +@allowed([ + 'Basic' + 'Classic' + 'Premium' + 'Standard' +]) +param acrSku string = 'Standard' + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { + name: acrName + location: location + sku: { + name: acrSku + } +} + +resource assignAcrPullToAks 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(resourceGroup().id, acrName, aksPrincipalId, 'AssignAcrPullToAks') + scope: containerRegistry + properties: { + description: 'Assign AcrPull role to AKS' + principalId: aksPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: roleDefinitionId + } +} + +output acrName string = containerRegistry.name diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/aks.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/aks.bicep new file mode 100644 index 0000000..f4cb5cf --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/aks.bicep @@ -0,0 +1,100 @@ +@description('Default value obtained from resource group, it can be overwritten') +param location string = resourceGroup().location + +@description('The name of the AKS resource') +@minLength(1) +param aksName string + +@description('Disk size (in GB) to provision for each of the agent pool nodes. Specifying 0 will apply the default disk size for that agentVMSize') +@minValue(0) +@maxValue(1023) +param aksDiskSizeGB int = 30 + +@description('The number of nodes for the cluster') +@minValue(1) +@maxValue(50) +param aksNodeCount int = 3 + +@description('The size of the Virtual Machine') +param aksVMSize string = 'Standard_D2s_v3' + +@description('The name of the Log Analytics workspace linked to AKS') +@minLength(1) +param logAnalyticsWorkspaceId string + +@description('Configure Azure Active Directory authentication for Kubernetes cluster') +param aksAadAuth bool + +@description('The object ID of the Azure Active Directory user to make cluster admin (only valid if aksAadAuth is true)') +param aksAadAdminUserObjectId string = '' + +var aksAadProfile = { + managed: true + enableAzureRBAC: true + tenantId: subscription().tenantId +} + +resource aks 'Microsoft.ContainerService/managedClusters@2020-09-01' = { + name: aksName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + dnsPrefix: 'aks' + aadProfile: aksAadAuth ? aksAadProfile : null + agentPoolProfiles: [ + { + name: 'agentpool' + osDiskSizeGB: aksDiskSizeGB + count: aksNodeCount + minCount: 1 + maxCount: aksNodeCount + vmSize: aksVMSize + osType: 'Linux' + mode: 'System' + enableAutoScaling: true + } + ] + addonProfiles: { + omsAgent: { + enabled: true + config: { + logAnalyticsWorkspaceResourceID: logAnalyticsWorkspaceId + } + } + azureKeyvaultSecretsProvider: { + enabled: true + config: { + enableSecretRotation: 'true' + rotationPollInterval: '2m' + } + } + } + } +} + +resource adminRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (aksAadAuth) { + name: guid(subscription().id, resourceGroup().id, 'aks-admin-${aksAadAdminUserObjectId}') + scope: aks + properties: { + // Azure Kubernetes Service Cluster Admin Role + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8') + principalId: aksAadAdminUserObjectId + } +} +resource userRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (aksAadAuth) { + name: guid(subscription().id, resourceGroup().id, 'aks-user-${aksAadAdminUserObjectId}') + scope: aks + properties: { + // Azure Kubernetes Service Cluster User Role + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '4abbcc35-e782-43d8-92c5-2d3f1bd2253f') + principalId: aksAadAdminUserObjectId + } +} + +output clusterName string = aks.name +output clusterId string = aks.id +output clusterPrincipalID string = aks.properties.identityProfile.kubeletidentity.objectId +output clusterKeyVaultSecretProviderClientId string = aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.clientId +output clusterKeyVaultSecretProviderObjectId string = aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/alerts.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/alerts.bicep new file mode 100644 index 0000000..b614152 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/alerts.bicep @@ -0,0 +1,953 @@ +@description('Default value obtained from resource group, it can be overwritten') +param location string = resourceGroup().location + +@description('Name for the default action group') +@minLength(1) +param actionGroupName string + +@description('Email address for alert notifications') +@minLength(1) +param notificationEmailAddress string + +@description('Cosmos DB resource id') +param cosmosDBId string + +@description('Service Bus namespace resource id') +param serviceBusNamespaceId string + +@description('AKS cluster resource id') +param aksClusterId string + +@description('Key Vault resource id') +param keyVaultId string + +@description('Application Insights resource id') +param appInsightsId string + +@description('Log Analytics workspace resource id') +param logAnalyticsWorkspaceId string + +var defaultMetricAlertActions = [ + { + actionGroupId: defaultActionGroup.id + } +] + +var defaultLogAlertActions = { + actionGroups: [ + defaultActionGroup.id + ] +} + +var serviceBusSplitByEntityDimensions = [ + { + name: 'EntityName' + operator: 'Include' + values: [ + '*' + ] + } +] + +resource defaultActionGroup 'Microsoft.Insights/actionGroups@2022-06-01' = { + name: actionGroupName + location: 'global' + properties: { + enabled: false + groupShortName: length(actionGroupName) <= 12 ? actionGroupName : substring(actionGroupName, 0, 12) + emailReceivers: [ + { + name: 'email-receiver' + emailAddress: notificationEmailAddress + useCommonAlertSchema: false + } + ] + } +} + +resource cosmosRusAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'cosmosRUs' + location: 'global' + properties: { + description: 'Alert when RUs exceed 400.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'TotalRequestUnits' + metricNamespace: 'Microsoft.DocumentDB/databaseAccounts' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Total' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 400 + } + ] + } + scopes: [ cosmosDBId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource cosmosInvalidCargoAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'cosmosInvalidCargo' + location: 'global' + properties: { + description: 'Alert when more than 10 documents have been saved to the invalid-cargo container.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'DocumentCount' + metricNamespace: 'Microsoft.DocumentDB/databaseAccounts' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Total' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 10 + dimensions: [ + { + name: 'CollectionName' + operator: 'Include' + values: [ + 'invalid-cargo' + ] + } + ] + } + ] + } + scopes: [ cosmosDBId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT5M' + severity: 3 + enabled: false + } +} + +resource serviceBusAbandonedMessagesAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'serviceBusAbandonedMessages' + location: 'global' + properties: { + description: 'Alert when a Service Bus entity has abandoned more than 10 messages.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'AbandonMessage' + metricNamespace: 'Microsoft.ServiceBus/namespaces' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Total' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 10 + dimensions: serviceBusSplitByEntityDimensions + } + ] + } + scopes: [ serviceBusNamespaceId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource serviceBusDeadLetteredMessagesAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'serviceBusDeadLetteredMessages' + location: 'global' + properties: { + description: 'Alert when a Service Bus entity has dead-lettered more than 10 messages.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'DeadletteredMessages' + metricNamespace: 'Microsoft.ServiceBus/namespaces' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Average' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 10 + dimensions: serviceBusSplitByEntityDimensions + } + ] + } + scopes: [ serviceBusNamespaceId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource serviceBusThrottledRequestsAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'serviceBusThrottledRequests' + location: 'global' + properties: { + description: 'Alert when a Service Bus entity has throttled more than 10 requests.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'ThrottledRequests' + metricNamespace: 'Microsoft.ServiceBus/namespaces' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Total' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 10 + dimensions: serviceBusSplitByEntityDimensions + } + ] + } + scopes: [ serviceBusNamespaceId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource aksCPUPercentageAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'aksCPUPercentage' + location: 'global' + properties: { + description: 'Alert when Node CPU percentage exceeds 80.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'node_cpu_usage_percentage' + metricNamespace: 'Microsoft.ContainerService/managedClusters' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Average' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 80 + } + ] + } + scopes: [ aksClusterId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource aksMemoryPercentageAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'aksMemoryPercentage' + location: 'global' + properties: { + description: 'Alert when Node memory working set percentage exceeds 80.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'node_memory_working_set_percentage' + metricNamespace: 'Microsoft.ContainerService/managedClusters' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Average' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 80 + } + ] + } + scopes: [ aksClusterId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource keyVaultSaturationAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'keyVaultSaturation' + location: 'global' + properties: { + description: 'Alert when Key Vault saturation falls outside the range of a dynamic threshold.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'SaturationShoebox' + metricNamespace: 'Microsoft.KeyVault/vaults' + name: 'Metric1' + skipMetricValidation: false + timeAggregation: 'Average' + criterionType: 'DynamicThresholdCriterion' + operator: 'GreaterOrLessThan' + alertSensitivity: 'Medium' + failingPeriods: { + minFailingPeriodsToAlert: 4 + numberOfEvaluationPeriods: 4 + } + } + ] + } + scopes: [ keyVaultId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 3 + enabled: false + } +} + +// Tenant specific issues prevent deployment of custom metric alert +// +resource productQtyScheduledForDestinationPortAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'productQtyScheduledForDestinationPort' + location: 'global' + properties: { + description: 'Alert when a single port/destination receives more than quantity 1000 of a given product.' + criteria: { + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + allOf: [ + { + metricName: 'port_product_qty' + metricNamespace: 'azure.applicationinsights' + name: 'Metric1' + skipMetricValidation: true + timeAggregation: 'Total' + criterionType: 'StaticThresholdCriterion' + operator: 'GreaterThan' + threshold: 1000 + dimensions: [ + { + name: 'destination' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'product' + operator: 'Include' + values: [ + '*' + ] + } + ] + } + ] + } + scopes: [ appInsightsId ] + actions: defaultMetricAlertActions + evaluationFrequency: 'PT1M' + windowSize: 'PT1M' + severity: 3 + enabled: false + } +} + +resource microserviceExceptionsAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'microserviceExceptions' + location: location + properties: { + description: 'Alert when a microservice throws more than 5 exceptions.' + criteria: { + allOf: [ + { + query: 'exceptions\n' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 5 + dimensions: [ + { + name: 'cloud_RoleName' + operator: 'Include' + values: [ + '*' + ] + } + ] + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource cargoProcessingAPIRequestsAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'cargoProcessingAPIRequests' + location: location + properties: { + description: 'Alert when the cargo-processing-api microservice is not receiving any requests.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}")' + timeAggregation: 'Count' + operator: 'Equal' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 3 + enabled: false + } +} + +resource e2eAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'e2eAverageDuration' + location: location + properties: { + description: 'Alert when the end to end average request duration exceeds 5 seconds.' + criteria: { + allOf: [ + { + query: 'let cargo_processing_api = requests\r\n| where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}")\r\n| project-rename ingest_timestamp = timestamp\r\n| project ingest_timestamp, operation_Id;\r\nlet operation_api_succeeded = requests\r\n| where cloud_RoleName == "operations-api" and name == "ServiceBus.process" and customDimensions["operation-state"] == "Succeeded"\r\n| extend operation_api_completed = timestamp + (duration*1ms)\r\n| project operation_Id, operation_api_completed;\r\ncargo_processing_api\r\n| join kind=inner operation_api_succeeded on $left.operation_Id == $right.operation_Id\r\n| extend end_to_end_Duration_ms = (operation_api_completed - ingest_timestamp) /1ms\r\n| summarize avg(end_to_end_Duration_ms)' + metricMeasureColumn: 'avg_end_to_end_Duration_ms' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 5000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource cargoProcessingAPIAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'cargoProcessingAPIAverageDuration' + location: location + properties: { + description: 'Alert when the cargo-processing-api microservice average request duration exceeds 2 seconds.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}")\r\n| summarize avg(duration)' + metricMeasureColumn: 'avg_duration' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 2000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource cargoProcessingValidatorAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'cargoProcessingValidatorAverageDuration' + location: location + properties: { + description: 'Alert when the cargo-processing-validator microservice average request duration exceeds 2 seconds.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "cargo-processing-validator" and (name == "ServiceBus.ProcessMessage" or name == "ServiceBusQueue.ProcessMessage")\r\n| summarize avg(duration)' + metricMeasureColumn: 'avg_duration' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 2000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource validCargoManagerAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'validCargoManagerAverageDuration' + location: location + properties: { + description: 'Alert when the valid-cargo-manager microservice average request duration exceeds 2 seconds.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "valid-cargo-manager" and name == "ServiceBusTopic.ProcessMessage"\r\n| summarize avg(duration)' + metricMeasureColumn: 'avg_duration' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 2000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource invalidCargoManagerAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'invalidCargoManagerAverageDuration' + location: location + properties: { + description: 'Alert when the invalid-cargo-manager microservice average request duration exceeds 2 seconds.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "invalid-cargo-manager" and name == "ServiceBusTopic.ProcessMessage"\r\n| summarize avg(duration)' + metricMeasureColumn: 'avg_duration' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 2000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource operationsAPIAverageDurationAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'operationsAPIAverageDuration' + location: location + properties: { + description: 'Alert when the operations-api microservice average request duration exceeds 1 second.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "operations-api" and name == "ServiceBus.process"\r\n| summarize avg(duration)' + metricMeasureColumn: 'avg_duration' + timeAggregation: 'Average' + operator: 'GreaterThan' + threshold: 1000 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource logAnalyticsDataIngestionDailyCapAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'logAnalyticsDataIngestionDailyCap' + location: location + properties: { + description: 'Alert when the Log Analytics data ingestion daily cap has been reached.' + criteria: { + allOf: [ + { + query: '_LogOperation | where Category == "Ingestion" | where Operation has "Data collection"' + resourceIdColumn: '_ResourceId' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ logAnalyticsWorkspaceId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource logAnalyticsDataIngestionRateAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'logAnalyticsDataIngestionRate' + location: location + properties: { + description: 'Alert when the Log Analytics max data ingestion rate has been reached.' + criteria: { + allOf: [ + { + query: '_LogOperation | where Category == "Ingestion" | where Operation has "Ingestion rate"' + resourceIdColumn: '_ResourceId' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ logAnalyticsWorkspaceId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 2 + enabled: false + } +} + +resource logAnalyticsOperationalIssuesAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'logAnalyticsOperationalIssues' + location: location + properties: { + description: 'Alert when the Log Analytics workspace has an operational issue.' + criteria: { + allOf: [ + { + query: '_LogOperation | where Level == "Warning"' + resourceIdColumn: '_ResourceId' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ logAnalyticsWorkspaceId ] + actions: defaultLogAlertActions + evaluationFrequency: 'P1D' + windowSize: 'P1D' + severity: 3 + enabled: false + } +} + +resource cargoProcessingAPIHealthCheckFailureAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'cargoProcessingAPIHealthCheckFailure' + location: location + properties: { + description: 'Alert when a cargo-processing-api microservice health check fails.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "cargo-processing-api" and name == "GET /actuator/health" and success == "False"' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource cargoProcessingAPIHealthCheckNotReportingAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'cargoProcessingAPIHealthCheckNotReporting' + location: location + properties: { + description: 'Alert when the cargo-processing-api microservice health check is not reporting.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "cargo-processing-api" and name == "GET /actuator/health"' + timeAggregation: 'Count' + operator: 'Equal' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource validCargoManagerHealthCheckFailureAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'validCargoManagerHealthCheckFailureAlert' + location: location + properties: { + description: 'Alert when a valid-cargo-manager microservice health check fails.' + criteria: { + allOf: [ + { + query: 'customMetrics\r\n| where cloud_RoleName == "valid-cargo-manager" and name == "HeartbeatState" and value != 2' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT30M' + windowSize: 'PT30M' + severity: 1 + enabled: false + } +} + +resource validCargoManagerHealthCheckNotReportingAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'validCargoManagerHealthCheckNotReporting' + location: location + properties: { + description: 'Alert when the valid-cargo-manager microservice health check is not reporting.' + criteria: { + allOf: [ + { + query: 'customMetrics\r\n| where cloud_RoleName == "valid-cargo-manager" and name == "HeartbeatState"' + timeAggregation: 'Count' + operator: 'Equal' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT30M' + windowSize: 'PT30M' + severity: 1 + enabled: false + } +} + +resource invalidCargoManagerHealthCheckFailureAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'invalidCargoManagerHealthCheckFailure' + location: location + properties: { + description: 'Alert when an invalid-cargo-manager microservice health check fails.' + criteria: { + allOf: [ + { + query: 'traces\r\n| where cloud_RoleName == "invalid-cargo-manager" and message contains "peeked at messages for over"' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource invalidCargoManagerHealthCheckNotReportingAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'invalidCargoManagerHealthCheckNotReporting' + location: location + properties: { + description: 'Alert when the invalid-cargo-manager microservice health check is not reporting.' + criteria: { + allOf: [ + { + query: 'traces\r\n| where cloud_RoleName == "invalid-cargo-manager" and (message contains "since last peek" or message contains "peeked at messages for over")' + timeAggregation: 'Count' + operator: 'Equal' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource operationsAPIHealthCheckFailureAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'operationsAPIHealthCheckFailure' + location: location + properties: { + description: 'Alert when an operations-api microservice health check fails.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "operations-api" and name == "GET /actuator/health" and success == "False"' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource operationsAPIHealthCheckNotReportingAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'operationsAPIHealthCheckNotReporting' + location: location + properties: { + description: 'Alert when the operations-api microservice health check is not reporting.' + criteria: { + allOf: [ + { + query: 'requests\r\n| where cloud_RoleName == "operations-api" and name == "GET /actuator/health"' + timeAggregation: 'Count' + operator: 'Equal' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + scopes: [ appInsightsId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} + +resource aksPodRestartsAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'aksPodRestarts' + location: location + properties: { + description: 'Alert when a microservice restarts more than once.' + criteria: { + allOf: [ + { + query: 'KubePodInventory\r\n| summarize numRestarts = sum(PodRestartCount) by ServiceName' + metricMeasureColumn: 'numRestarts' + timeAggregation: 'Total' + operator: 'GreaterThan' + threshold: 1 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + dimensions: [ + { + name: 'ServiceName' + operator: 'Include' + values: [ + 'cargo-processing-api' + 'cargo-processing-validator' + 'invalid-cargo-manager' + 'operations-api' + 'valid-cargo-manager' + ] + } + ] + } + ] + } + scopes: [ logAnalyticsWorkspaceId ] + actions: defaultLogAlertActions + evaluationFrequency: 'PT5M' + windowSize: 'PT5M' + severity: 1 + enabled: false + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/app-insights.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/app-insights.bicep new file mode 100644 index 0000000..ac1b57a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/app-insights.bicep @@ -0,0 +1,33 @@ +@description('Default value obtained from resource group, it can be overwritten') +param location string = resourceGroup().location + +@description('Name of the Application Insights instance') +param appInsightsName string + +@description('Name of the Log Analytics instance') +param logAnalyticsName string + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output workspaceId string = logAnalyticsWorkspace.id +output insightsName string = applicationInsights.name +output appInsightsId string = applicationInsights.id diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/cosmos.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/cosmos.bicep new file mode 100644 index 0000000..dff8b70 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/cosmos.bicep @@ -0,0 +1,161 @@ +@description('Default value obtained from resource group, it can be overwriten') +param location string = resourceGroup().location + +@description('Cosmos DB account name, max length 44 characters, lowercase') +@minLength(1) +@maxLength(44) +param accountName string = 'sql-${uniqueString(resourceGroup().id)}' + +@description('The default consistency level of the Cosmos DB account.') +@allowed([ + 'Eventual' + 'ConsistentPrefix' + 'Session' + 'BoundedStaleness' + 'Strong' +]) +param defaultConsistencyLevel string = 'Session' + +@description('Enable automatic failover for regions') +param automaticFailover bool = true + +@description('The name for the database') +@minLength(1) +param databaseName string + +@description('The name for the container 1') +@minLength(1) +param container1Name string + +@description('The name for the container 2') +@minLength(1) +param container2Name string + +@description('The name for the container 3') +@minLength(1) +param container3Name string + +@description('Name for diagnostic settings') +@minLength(1) +param diagnosticSettingsName string = 'cosmosDbDiagnostics' + +@description('Log analytics workspace id') +@minLength(1) +param logAnalyticsWorkspaceId string + +var accountNameVar = toLower(accountName) +var locations = [ + { + locationName: location + failoverPriority: 0 + isZoneRedundant: false + } +] + +resource accountNameResource 'Microsoft.DocumentDB/databaseAccounts@2021-01-15' = { + name: accountNameVar + kind: 'GlobalDocumentDB' + location: location + properties: { + consistencyPolicy: { + defaultConsistencyLevel: defaultConsistencyLevel + } + locations: locations + databaseAccountOfferType: 'Standard' + enableAutomaticFailover: automaticFailover + } + + resource database 'sqlDatabases' = { + name: databaseName + properties: { + resource: { + id: databaseName + } + } + + resource container1 'containers' = { + name: container1Name + properties: { + resource: { + id: container1Name + partitionKey: { + paths: [ + '/id' + ] + kind: 'Hash' + } + } + } + } + + resource container2 'containers' = { + name: container2Name + properties: { + resource: { + id: container2Name + partitionKey: { + paths: [ + '/id' + ] + kind: 'Hash' + } + } + } + } + + resource container3 'containers' = { + name: container3Name + properties: { + resource: { + id: container3Name + partitionKey: { + paths: [ + '/id' + ] + kind: 'Hash' + } + } + } + } + } +} + +resource cosmosDbDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: diagnosticSettingsName + scope: accountNameResource + properties: { + logs: [ + { + category: 'DataPlaneRequests' + enabled: true + } + { + category: 'QueryRuntimeStatistics' + enabled: true + } + { + category: 'PartitionKeyStatistics' + enabled: true + } + { + category: 'PartitionKeyRUConsumption' + enabled: true + } + { + category: 'ControlPlaneRequests' + enabled: true + } + ] + metrics: [ + { + category: 'Requests' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspaceId + } +} + +output cosmosDBId string = accountNameResource.id +output cosmosDBEndpoint string = accountNameResource.properties.documentEndpoint +output cosmosDBAccountName string = accountNameResource.name diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/key-vault.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/key-vault.bicep new file mode 100644 index 0000000..58e4194 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/key-vault.bicep @@ -0,0 +1,125 @@ +@description('Location obtained from resource group') +param location string = resourceGroup().location + +@description('KeyVault name') +@minLength(1) +param kvName string + +@description('Expected KeyVault sku') +@allowed([ + 'premium' + 'standard' +]) +param kvSku string = 'standard' + +@description('Tenant Id for the service principal that will be in charge of KeyVault access') +@minLength(1) +param kvTenantId string = tenant().tenantId + +//secrets stored in KeyVault +@description('Service Bus Namespace name') +@minLength(1) +param serviceBusNamespaceName string + +@description('App Insights Connection String') +@minLength(1) +@secure() +param appInsightsConnectionString string + +@description('Cosmos DB endpoint') +@minLength(1) +param cosmosDBEndpoint string + +@description('Cosmos DB account name') +@minLength(1) +param cosmosDBAccountName string + +@description('Name for diagnostic settings') +@minLength(1) +param diagnosticSettingsName string = 'keyVaultDiagnostics' + +@description('Log analytics workspace id') +@minLength(1) +param logAnalyticsWorkspaceId string + +@description('The Object ID of the user-defined Managed Identity used by the AKS Secret Provider') +@minLength(1) +@secure() +param clusterKeyVaultSecretProviderObjectId string + +resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = { + name: kvName + location: location + properties: { + tenantId: kvTenantId + sku: { + family: 'A' + name: kvSku + } + createMode: 'default' + publicNetworkAccess: 'Enabled' + accessPolicies: [ + { + objectId: clusterKeyVaultSecretProviderObjectId + permissions: { + secrets: [ + 'get' + ] + } + tenantId: subscription().tenantId + } + ] + enabledForTemplateDeployment: true + } + + resource appInsightsStringSecret 'secrets' = { + name: 'AppInsightsConnectionString' + properties: { + value: appInsightsConnectionString + } + } + + resource serviceBusSecret 'secrets' = { + name: 'ServiceBusConnectionString' + properties: { + value: listKeys(resourceId('Microsoft.ServiceBus/namespaces/AuthorizationRules', serviceBusNamespaceName, 'RootManageSharedAccessKey'), '2022-01-01-preview').primaryConnectionString + } + } + + resource cosmosDBEndpointSecret 'secrets' = { + name: 'CosmosDBEndpoint' + properties: { + value: cosmosDBEndpoint + } + } + + resource cosmosDBKeySecret 'secrets' = { + name: 'CosmosDBKey' + properties: { + value: listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', cosmosDBAccountName), '2022-05-15').primaryMasterKey + } + } +} + +resource keyVaultDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: diagnosticSettingsName + scope: keyVault + properties: { + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspaceId + } +} + +output kvName string = keyVault.name +output kvId string = keyVault.id diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/service-bus.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/service-bus.bicep new file mode 100644 index 0000000..0ff68a5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/service-bus.bicep @@ -0,0 +1,130 @@ +@description('Name for the Service Bus Namespace') +@minLength(1) +param serviceBusName string + +@description('Default value obtained from resource group, it can be overwritten') +@minLength(1) +param location string = resourceGroup().location + +@description('Name for the first Service Bus Queue') +@minLength(1) +param serviceBusQueue1Name string + +@description('Name for the second Service Bus Queue') +@minLength(1) +param serviceBusQueue2Name string + +@description('Name for the Service Bus Topic') +@minLength(1) +param serviceBusTopicName string + +@description('Name for the first Service Bus Subscription') +@minLength(1) +param serviceBusSubscription1Name string + +@description('Name for the second Service Bus Subscription') +@minLength(1) +param serviceBusSubscription2Name string + +@description('Name for the first Service Bus Subscriptions filter rule') +@minLength(1) +param serviceBusTopicRule1Name string + +@description('Name for the second Service Bus Subscriptions filter rule') +@minLength(1) +param serviceBusTopicRule2Name string + +@description('Name for diagnostic settings') +@minLength(1) +param diagnosticSettingsName string = 'serviceBusDiagnostics' + +@description('Log analytics workspace id') +@minLength(1) +param logAnalyticsWorkspaceId string + +resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-01-01-preview' = { + name: serviceBusName + location: location + sku: { + capacity: 1 + name: 'Standard' + tier: 'Standard' + } + + properties: { + publicNetworkAccess: 'Enabled' + } + + resource serviceBusQueue 'queues' = { + name: serviceBusQueue1Name + } + + resource serviceBusQueue2 'queues' = { + name: serviceBusQueue2Name + } +} + +resource serviceBusTopic 'Microsoft.ServiceBus/namespaces/topics@2022-01-01-preview' = { + name: serviceBusTopicName + parent: serviceBusNamespace + properties: { + supportOrdering: true + } + + resource serviceBusSubscription1 'subscriptions' = { + name: serviceBusSubscription1Name + properties: { + maxDeliveryCount: 1 + } + + resource serviceBusTopicRule 'rules' = { + name: serviceBusTopicRule1Name + properties: { + filterType: 'SqlFilter' + sqlFilter: { + sqlExpression: 'valid = True' + } + } + } + } + + resource serviceBusSubscription2 'subscriptions' = { + name: serviceBusSubscription2Name + properties: { + maxDeliveryCount: 1 + } + + resource serviceBusTopicRule 'rules' = { + name: serviceBusTopicRule2Name + properties: { + filterType: 'SqlFilter' + sqlFilter: { + sqlExpression: 'valid = False' + } + } + } + } +} + +resource serviceBusDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: diagnosticSettingsName + scope: serviceBusNamespace + properties: { + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspaceId + } +} + +output serviceBusNamespaceName string = serviceBusNamespace.name +output serviceBusNamespaceId string = serviceBusNamespace.id diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/workbooks.bicep b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/workbooks.bicep new file mode 100644 index 0000000..5447e41 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/bicep/modules/workbooks.bicep @@ -0,0 +1,81 @@ +@description('Default value obtained from resource group, it can be overwritten') +param location string = resourceGroup().location + +@description('This value will explain who is the author of specific resources and will be reflected in every deployed tool') +@minLength(1) +param uniqueUserName string + +@description('Linked resource for Workook') +@minLength(1) +param workspaceId string + +@description('Id for monitored Service Bus Namespace') +@minLength(1) +param serviceBusNamespaceId string + +@description('Id for monitored Key Vault resource') +@minLength(1) +param keyVaultId string + +@description('Id for App Insights resource') +@minLength(1) +param appInsightsId string + +@description('Id for monitored AKS resource') +@minLength(1) +param aksId string + +var indexWorkbookName = guid(subscription().subscriptionId, resourceGroup().name, uniqueUserName, 'index') +var baseIndexWorkbookContent = loadTextContent('../../workbooks/index.json') +var indexInsightsWorkbookContent = replace(baseIndexWorkbookContent, '\${app_insights_id}', appInsightsId) +var indexWorkspaceWorkbookContent =replace(indexInsightsWorkbookContent, '\${logs_workspace_id}', uriComponent(workspaceId)) +var indexInfrastructureWorkbookContent = replace(indexWorkspaceWorkbookContent, '\${infrastructure_workbook_id}', uriComponent(infrastructureWorkbook.id)) +var indexFinalWorkbookContent = replace(indexInfrastructureWorkbookContent, '\${system_workbook_id}', uriComponent(serviceProcessingWorkbook.id)) +resource observabilityWorkbook 'Microsoft.Insights/workbooks@2022-04-01' = { + name: indexWorkbookName + location: location + kind: 'shared' + properties: { + category: 'workbook' + displayName: 'Index' + serializedData: string(indexFinalWorkbookContent) + version: '0.01' + sourceId: workspaceId + } +} + +var infrastructureWorkbookName = guid(subscription().subscriptionId, resourceGroup().name, uniqueUserName, 'infrastructure') +var baseInfrastructureWorkbookContent = loadTextContent('../../workbooks/infrastructure.json') +var baseInfrastructureSeviceBusWorkbookContent = replace(baseInfrastructureWorkbookContent, '\${servicebus_namespace_id}', serviceBusNamespaceId) +var baseInfrastructureKeyVaultWorkbookContent = replace(baseInfrastructureSeviceBusWorkbookContent, '\${key_vault_id}', keyVaultId) +var infrastructureUrlWorkbookContent =replace(baseInfrastructureKeyVaultWorkbookContent, '\${app_insights_id_url}', uriComponent(appInsightsId)) +var baseInfrastructureAksWorkbookContent = replace(infrastructureUrlWorkbookContent, '\${aks_id}', aksId) +var infrastructureFinalWorkbookContent = replace(baseInfrastructureAksWorkbookContent, '\${app_insights_id}', appInsightsId) +resource infrastructureWorkbook 'Microsoft.Insights/workbooks@2022-04-01' = { + name: infrastructureWorkbookName + location: location + kind: 'shared' + properties: { + category: 'workbook' + displayName: 'Infrastructure' + serializedData: string(infrastructureFinalWorkbookContent) + version: '0.01' + sourceId: workspaceId + } +} + +var serviceProcessingWorkbookName = guid(subscription().subscriptionId, resourceGroup().name, uniqueUserName, 'service-processing') +var baseServiceProcessingWorkbookContent = loadTextContent('../../workbooks/system-processing.json') +var serviceProcessingWorkbookContent = replace(baseServiceProcessingWorkbookContent, '\${app_insights_id}', appInsightsId) +resource serviceProcessingWorkbook 'Microsoft.Insights/workbooks@2022-04-01' = { + name: serviceProcessingWorkbookName + location: location + kind: 'shared' + properties: { + category: 'workbook' + displayName: 'System Processing' + serializedData: string(serviceProcessingWorkbookContent) + version: '0.01' + sourceId: workspaceId + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/build-and-push-images.sh b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/build-and-push-images.sh new file mode 100644 index 0000000..9aa7a86 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/build-and-push-images.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -e + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +function help() { + echo + echo "build-images.sh" + echo + echo "Build images" + echo + echo -e "\t--acr-name\t(Optional)The name of the Azure Container Registry to push to. If not provided, the images will be built but not pushed." + echo -e "\t--image-tag\t(Optional)The tag to build the image with (defaults to 'latest')" + echo +} + + +# Set default values here +acr_name="" +image_tag="latest" + + +# Process switches: +SHORT=h +LONG=acr-name:,image-tag:,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +while : +do + case "$1" in + --acr-name) + acr_name=$2 + shift 2 + ;; + --image-tag) + image_tag=$2 + shift 2 + ;; + -h | --help) + help + ;; + --) + shift; + break + ;; + *) + echo "Unexpected '$1'" + help + exit 1 + ;; + esac +done + +image_base_name="" +if [[ -n $acr_name ]]; then + echo -e "**\n** Authenticating to container registry ($acr_name)...\n**" + az acr login --name "$acr_name" + + image_base_name="${acr_name}.azurecr.io/" +fi + + +services_to_build=("cargo-processing-api" "cargo-processing-validator" "invalid-cargo-manager" "operations-api" "valid-cargo-manager") +for service in "${services_to_build[@]}" +do + echo + echo "*******************************************************************************************************************" + echo -e "\n**\n** Building ${service}...\n**" + echo "*******************************************************************************************************************" + docker build --progress plain -t "${image_base_name}${service}:${image_tag}" "$script_dir/../../src/${service}" + + if [[ -n $acr_name ]]; then + echo -e "\n**\n** Pushing ${service}...\n**" + docker push "${image_base_name}${service}:${image_tag}" + fi +done diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/create-env-files-from-output.sh b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/create-env-files-from-output.sh new file mode 100644 index 0000000..a4b1053 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/create-env-files-from-output.sh @@ -0,0 +1,246 @@ +#!/bin/bash +set -e + +# +# This script expects to find an output.json in the project root with the values +# from the infrastructure deployment. +# It then creates the env files, settings files, and helm chart values files for each service +# + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +RESOURCE_GROUP=$(jq -r '.rg_name' < "$script_dir/../../output.json") +if [[ ${#RESOURCE_GROUP} -eq 0 ]]; then + echo 'ERROR: Missing output value rg_name' 1>&2 + exit 6 +fi + +APP_INSIGHTS=$(jq -r '.insights_name' < "$script_dir/../../output.json") +if [[ ${#APP_INSIGHTS} -eq 0 ]]; then + echo 'ERROR: Missing output value insights_name' 1>&2 + exit 6 +fi + +SERVICE_BUS_NAMESPACE=$(jq -r '.sb_namespace_name' < "$script_dir/../../output.json") +if [[ ${#SERVICE_BUS_NAMESPACE} -eq 0 ]]; then + echo 'ERROR: Missing output value sb_namespace_name' 1>&2 + exit 6 +fi + +COSMOSDB_NAME=$(jq -r '.cosmosdb_name' < "$script_dir/../../output.json") +if [[ ${#COSMOSDB_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value cosmosdb_name' 1>&2 + exit 6 +fi + +ACR_NAME=$(jq -r '.acr_name' < "$script_dir/../../output.json") +if [[ ${#ACR_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value acr_name' 1>&2 + exit 6 +fi + +KEYVAULT_NAME=$(jq -r '.kv_name' < "$script_dir/../../output.json") +if [[ ${#KEYVAULT_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value kv_name' 1>&2 + exit 6 +fi + +TENANT_ID=$(jq -r '.tenant_id' < "$script_dir/../../output.json") +if [[ ${#TENANT_ID} -eq 0 ]]; then + echo 'ERROR: Missing output value tenant_id' 1>&2 + exit 6 +fi + +AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID=$(jq -r '.aks_key_vault_secret_provider_client_id' < "$script_dir/../../output.json") +if [[ ${#AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID} -eq 0 ]]; then + echo 'ERROR: Missing output value aks_key_vault_secret_provider_client_id' 1>&2 + exit 6 +fi + +#get information from Application Insights +APP_INSIGHTS_KEY=$(az resource show -g "${RESOURCE_GROUP}" -n "${APP_INSIGHTS}" --resource-type "microsoft.insights/components" --query properties.ConnectionString --output tsv) + +#get information from Service Bus +SERVICE_BUS_CONNECTION_STRING=$(az servicebus namespace authorization-rule keys list --resource-group "${RESOURCE_GROUP}" --namespace-name "${SERVICE_BUS_NAMESPACE}" --name RootManageSharedAccessKey --query primaryConnectionString --output tsv) + +#get information from Cosmos DB +COSMOS_DB_ENDPOINT=$(az resource show -g "${RESOURCE_GROUP}" -n "${COSMOSDB_NAME}" --resource-type "microsoft.documentdb/databaseaccounts" --query properties.documentEndpoint --output tsv) +COSMOS_DB_KEY=$(az cosmosdb keys list -g "${RESOURCE_GROUP}" -n "${COSMOSDB_NAME}" --query primaryMasterKey --output tsv) + +#create env file for cargo-processing-api +cat << EOF > "$script_dir/../../src/cargo-processing-api/.env" +APPLICATIONINSIGHTS_CONNECTION_STRING=$APP_INSIGHTS_KEY +APPLICATIONINSIGHTS_VERSION=3.4.7 + +#Service Bus Information +servicebus_connection_string=$SERVICE_BUS_CONNECTION_STRING +accelerator_queue_name=ingest-cargo + +# Operation API +operations_api_url=http://operations-api:8081/ +EOF +echo "CREATED: env file for CARGO-PROCESSING-API" + +#create helm values file for cargo-processing-api +cat << EOF > "$script_dir/../../src/cargo-processing-api/helm/env.yaml" +image: + repository: $ACR_NAME.azurecr.io/cargo-processing-api + +keyVault: + name: $KEYVAULT_NAME + tenantId: $TENANT_ID + +aksKeyVaultSecretProviderIdentityId: $AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID +EOF +echo "CREATED: helm value file for CARGO-PROCESSING-API" + + +#create env file for cargo-processing-validator +cat < "$script_dir/../../src/cargo-processing-validator/.env" +APPLICATIONINSIGHTS_CONNECTION_STRING=$APP_INSIGHTS_KEY +SERVICE_BUS_CONNECTION_STRING=$SERVICE_BUS_CONNECTION_STRING +QUEUE_NAME="ingest-cargo" +TOPIC_NAME="validated-cargo" +MAX_WAIT_TIME_IN_MS=1000 +MAX_MESSAGE_DEQUEUE_COUNT=10 +OPERATION_QUEUE_NAME="operation-state" +EOF +echo "CREATED: env file for CARGO-PROCESSING-VALIDATOR" + +#create helm values file for cargo-processing-validator +cat << EOF > "$script_dir/../../src/cargo-processing-validator/helm/env.yaml" +image: + repository: $ACR_NAME.azurecr.io/cargo-processing-validator + +keyVault: + name: $KEYVAULT_NAME + tenantId: $TENANT_ID + +aksKeyVaultSecretProviderIdentityId: $AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID +EOF +echo "CREATED: helm value file for CARGO-PROCESSING-VALIDATOR" + + +#create env file for invalid-cargo-manager +cat << EOF > "$script_dir/../../src/invalid-cargo-manager/.env" +SERVICE_BUS_CONNECTION_STR=$SERVICE_BUS_CONNECTION_STRING +SERVICE_BUS_TOPIC_NAME=validated-cargo +SERVICE_BUS_SUBSCRIPTION_NAME=invalid-cargo +SERVICE_BUS_QUEUE_NAME=operation-state +SERVICE_BUS_MAX_MESSAGE_COUNT=1 +SERVICE_BUS_MAX_WAIT_TIME=30 + +COSMOS_DB_ENDPOINT=$COSMOS_DB_ENDPOINT +COSMOS_DB_KEY=$COSMOS_DB_KEY +COSMOS_DB_DATABASE_NAME=cargo +COSMOS_DB_CONTAINER_NAME=invalid-cargo + +APPLICATIONINSIGHTS_CONNECTION_STRING=$APP_INSIGHTS_KEY +CLOUD_LOGGING_LEVEL=INFO +CONSOLE_LOGGING_LEVEL=DEBUG + +HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS=30 +HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS=60 +EOF +echo "CREATED: env file for INVALID-CARGO-MANAGER" + +#create helm values file for invalid-cargo-manager +cat << EOF > "$script_dir/../../src/invalid-cargo-manager/helm/env.yaml" +image: + repository: $ACR_NAME.azurecr.io/invalid-cargo-manager + +keyVault: + name: $KEYVAULT_NAME + tenantId: $TENANT_ID + +aksKeyVaultSecretProviderIdentityId: $AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID +EOF +echo "CREATED: helm value file for INVALID-CARGO-MANAGER" + + +#create env file for operations-api +cat << EOF > "$script_dir/../../src/operations-api/.env" +APPLICATIONINSIGHTS_CONNECTION_STRING=$APP_INSIGHTS_KEY +APPLICATIONINSIGHTS_VERSION=3.4.7 + +# Service Bus Information +SERVICEBUS_CONNECTION_STRING=$SERVICE_BUS_CONNECTION_STRING +SERVICEBUS_PREFETCH_COUNT=10 +OPERATION_STATE_QUEUE_NAME=operation-state + +# Cosmos Db Information +COSMOS_DB_ENDPOINT=$COSMOS_DB_ENDPOINT +COSMOS_DB_KEY=$COSMOS_DB_KEY +COSMOS_DB_DATABASE_NAME=cargo +COSMOS_DB_CONTAINER_NAME=invalid-cargo +EOF +echo "CREATED: env file for OPERATIONS-API" + +#create helm values file for operations-api +cat << EOF > "$script_dir/../../src/operations-api/helm/env.yaml" +image: + repository: $ACR_NAME.azurecr.io/operations-api + +keyVault: + name: $KEYVAULT_NAME + tenantId: $TENANT_ID + +aksKeyVaultSecretProviderIdentityId: $AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID +EOF +echo "CREATED: helm value file for OPERATIONS-API" + + +#create appsettings.json file for valid-cargo-manager +cat < "$script_dir/../../src/valid-cargo-manager/appsettings.json" +{ + "ApplicationInsights": { + "ConnectionString": "$APP_INSIGHTS_KEY" + }, + "ServiceBus": { + "ConnectionString": "$SERVICE_BUS_CONNECTION_STRING", + "Topic": "validated-cargo", + "Queue": "operation-state", + "Subscription": "valid-cargo", + "PrefetchCount": 100, + "MaxConcurrentCalls": 10 + }, + "CosmosDB": { + "EndpointUri": "$COSMOS_DB_ENDPOINT", + "PrimaryKey": "$COSMOS_DB_KEY", + "Database": "cargo", + "Container": "valid-cargo" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "HealthCheck": { + "TcpServer": { + "Port": 3030 + }, + "CosmosDB": { + "MaxDurationMs": 200 + }, + "ServiceBus": { + "MaxDurationMs": 200 + } + } +} +EOF +echo "CREATED: appsettings.json file for VALID-CARGO-MANAGER" + +#create helm values file for valid-cargo-manager +cat << EOF > "$script_dir/../../src/valid-cargo-manager/helm/env.yaml" +image: + repository: $ACR_NAME.azurecr.io/valid-cargo-manager + +keyVault: + name: $KEYVAULT_NAME + tenantId: $TENANT_ID + +aksKeyVaultSecretProviderIdentityId: $AKS_KEY_VAULT_SECRET_PROVIDER_CLIENT_ID +EOF +echo "CREATED: helm value file for VALID-CARGO-MANAGER" diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-bicep-infrastructure.sh b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-bicep-infrastructure.sh new file mode 100644 index 0000000..9888ec9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-bicep-infrastructure.sh @@ -0,0 +1,169 @@ +#!/bin/bash +set -e + +# +# This script generates the bicep parameters file and then uses that to deploy the infrastructure +# An output.json file is generated in the project root containing the outputs from the deployment +# The output.json format is consistent between Terraform and Bicep deployments +# + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +help() +{ + echo "" + echo "" + echo "" + echo "Command" + echo " deploy-bicep-infrastructure.sh : Will deploy all required services services." + echo "" + echo "Arguments" + echo " --username, -u : REQUIRED: Unique name to assign in all deployed services, your high school hotmail alias is a great idea!" + echo " --email-address, -e : REQUIRED: Email address for alert notifications" + echo " --location, -l : REQUIRED: Azure region to deploy to" + echo " --aks-aad-auth : OPTIONAL Enable AAD authentication for AKS" + echo "" + exit 1 +} + +SHORT=u:,l:,h +LONG=username:,email-address:,location:,aks-aad-auth,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +USERNAME='' +LOCATION='' +EMAIL_ADDRESS='' +AKS_AAD_AUTH=false +while : +do + case "$1" in + -u | --username ) + USERNAME="$2" + shift 2 + ;; + -e | --email-address ) + EMAIL_ADDRESS="$2" + shift 2 + ;; + -l | --location ) + LOCATION="$2" + shift 2 + ;; + --aks-aad-auth ) + AKS_AAD_AUTH=true + shift 1 + ;; + -h | --help) + help + ;; + --) + shift; + break + ;; + *) + echo "Unexpected option: $1" + ;; + esac +done + +if [[ ${#USERNAME} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --username | -u' 1>&2 + exit 6 +fi + +if [[ ${#EMAIL_ADDRESS} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --email-address | -e' 1>&2 + exit 6 +fi + +if [[ ${#LOCATION} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --location | -l' 1>&2 + exit 6 +fi + + +if [[ "$AKS_AAD_AUTH" == true ]]; then + if [[ -z "$ARM_CLIENT_ID" ]]; then + # Get the ID of the currently signed in user + current_user_object_id=$(az ad signed-in-user show --query id -o tsv) + else + # Get the ID of the service principal for ARM_CLIENT_ID + current_user_object_id=$(az ad sp show --id "$ARM_CLIENT_ID" --query id -o tsv) + fi + echo "Enabling AKS AAD authentication (current user object ID: $current_user_object_id)" +fi + +cat << EOF > "$script_dir/../bicep/azuredeploy.parameters.json" +{ + "\$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "value": "${LOCATION}" + }, + "uniqueUserName": { + "value": "${USERNAME}" + }, + "cosmosDatabaseName": { + "value": "cargo" + }, + "cosmosContainer1Name": { + "value": "valid-cargo" + }, + "cosmosContainer2Name": { + "value": "invalid-cargo" + }, + "cosmosContainer3Name": { + "value": "operations" + }, + "serviceBusQueue1Name": { + "value": "ingest-cargo" + }, + "serviceBusQueue2Name": { + "value": "operation-state" + }, + "serviceBusTopicName": { + "value": "validated-cargo" + }, + "serviceBusSubscription1Name": { + "value": "valid-cargo" + }, + "serviceBusSubscription2Name": { + "value": "invalid-cargo" + }, + "serviceBusTopicRule1Name": { + "value": "valid" + }, + "serviceBusTopicRule2Name": { + "value": "invalid" + }, + "aksAadAuth": { + "value": $AKS_AAD_AUTH + }, + "aksAadAdminUserObjectId" : { + "value": "$current_user_object_id" + }, + "notificationEmailAddress": { + "value": "${EMAIL_ADDRESS}" + } + } +} +EOF + +echo "Bicep parameters file created" + +cd "$script_dir/../bicep/" + +deployment_name="deployment-${USERNAME}-${LOCATION}" +echo "Starting Bicep deployment ($deployment_name)" +az deployment sub create \ + --location "$LOCATION" \ + --template-file main.bicep \ + --name "$deployment_name" \ + --parameters azuredeploy.parameters.json \ + --output json \ + | jq "[.properties.outputs | to_entries | .[] | {key:.key, value: .value.value}] | from_entries" > "$script_dir/../../output.json" + +echo "Bicep deployment completed" diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-helm-charts.sh b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-helm-charts.sh new file mode 100644 index 0000000..528c54f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-helm-charts.sh @@ -0,0 +1,205 @@ +#!/bin/bash +set -e + +# +# This script expects to find an output.json in the project root with the values +# from the infrastructure deployment. +# It deploys helm charts for each service to the AKS cluster +# + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + + +function help() { + echo + echo "deploy-helm-charts.sh" + echo + echo "Deploy solution into AKS using Helm" + echo + echo -e "\t--aks-aad-auth\t(Optional)Enable AAD authentication for AKS" + echo +} + + +# Set default values here +AKS_AAD_AUTH=false + + +# Process switches: +SHORT=h +LONG=aks-aad-auth,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +while : +do + case "$1" in + --aks-aad-auth ) + AKS_AAD_AUTH=true + shift 1 + ;; + -h | --help) + help + exit 0 + ;; + --) + shift; + break + ;; + *) + echo "Unexpected '$1'" + help + exit 1 + ;; + esac +done + + +RESOURCE_GROUP=$(jq -r '.rg_name' < "$script_dir/../../output.json") +if [[ ${#RESOURCE_GROUP} -eq 0 ]]; then + echo 'ERROR: Missing output value rg_name' 1>&2 + exit 6 +fi + +AKS_NAME=$(jq -r '.aks_name' < "$script_dir/../../output.json") +if [[ ${#AKS_NAME} -eq 0 ]]; then + echo 'ERROR: Missing output value aks_name' 1>&2 + exit 6 +fi + + +if [[ "$AKS_AAD_AUTH" == "true" ]]; then + echo "Getting Admin AKS credentials" + # Temporarily get cluster admin credentials to set up user permisions for default namespace + + # Get kubeconfig for the AKS cluster + az aks get-credentials --resource-group "$RESOURCE_GROUP" --name "$AKS_NAME" --admin --overwrite-existing + # Update the kubeconfig to use https://github.com/azure/kubelogin + kubelogin convert-kubeconfig -l azurecli + + if [[ -z "$ARM_CLIENT_ID" ]]; then + # Get the UPN of the currently signed in user + current_user_object_id=$(az ad signed-in-user show --query id -o tsv) + else + # Get the ID of the service principal for ARM_CLIENT_ID + current_user_object_id=$(az ad sp show --id "$ARM_CLIENT_ID" --query id -o tsv) + fi + + echo "Adding user-full-access role & binding" +cat < "$script_dir/../../http/.env" +SERVICE_IP=$ingress_ip +EOF +echo "CREATED: env file for http docs" + + + +#get information from Service Bus +SERVICE_BUS_NAMESPACE=$(jq -r '.sb_namespace_name' < "$script_dir/../../output.json") +if [[ ${#SERVICE_BUS_NAMESPACE} -eq 0 ]]; then + echo 'ERROR: Missing output value sb_namespace_name' 1>&2 + exit 6 +fi +SERVICE_BUS_CONNECTION_STRING=$(az servicebus namespace authorization-rule keys list --resource-group "${RESOURCE_GROUP}" --namespace-name "${SERVICE_BUS_NAMESPACE}" --name RootManageSharedAccessKey --query primaryConnectionString --output tsv) + + + +#create env file for cargo-test-scripts +cat << EOF > "$script_dir/../../src/cargo-test-scripts/.env" +SERVICEBUS_CONNECTION_STRING=$SERVICE_BUS_CONNECTION_STRING +QUEUE_NAME=ingest-cargo +TOPIC_NAME=validated-cargo +CARGO_PROCESSING_API_URL=http://$ingress_ip/cargo +OPERATIONS_API_URL=http://$ingress_ip/cargo + +EOF +echo "CREATED: env file for CARGO-TEST-SCRIPTS" diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-terraform-infrastructure.sh b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-terraform-infrastructure.sh new file mode 100644 index 0000000..b608117 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/scripts/deploy-terraform-infrastructure.sh @@ -0,0 +1,152 @@ +#!/bin/bash +set -e + +# +# This script generates the terraform.tfvars file and then uses that to deploy the infrastructure +# An output.json file is generated in the project root containing the outputs from the deployment +# The output.json format is consistent between Terraform and Bicep deployments +# + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +help() +{ + echo "" + echo "" + echo "" + echo "Command" + echo " deploy-terraform-infrastructure.sh : Will deploy all required services." + echo "" + echo "Arguments" + echo " --username, -u : REQUIRED: Unique name to assign in all deployed services, your high school hotmail alias is a great idea!" + echo " --email-address, -e : REQUIRED: Email address for alert notifications" + echo " --location, -l : REQUIRED: Azure region to deploy to" + echo " --aks-aad-auth : OPTIONAL Enable AAD authentication for AKS" + echo "" + exit 1 +} + +SHORT=u:,l:,h +LONG=username:,email-address:,location:,aks-aad-auth,help +OPTS=$(getopt -a -n files --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" + +USERNAME='' +LOCATION='' +EMAIL_ADDRESS='' +AKS_AAD_AUTH=false +while : +do + case "$1" in + -u | --username ) + USERNAME="$2" + shift 2 + ;; + -e | --email-address ) + EMAIL_ADDRESS="$2" + shift 2 + ;; + -l | --location ) + LOCATION="$2" + shift 2 + ;; + --aks-aad-auth ) + AKS_AAD_AUTH=true + shift 1 + ;; + -h | --help) + help + ;; + --) + shift; + break + ;; + *) + echo "Unexpected option: $1" + ;; + esac +done + +if [[ ${#USERNAME} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --username | -u' 1>&2 + exit 6 +fi + +if [[ ${#EMAIL_ADDRESS} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --email-address | -e' 1>&2 + exit 6 +fi + +if [[ ${#LOCATION} -eq 0 ]]; then + echo 'ERROR: Missing required parameter --location | -l' 1>&2 + exit 6 +fi + +current_user_object_id="" +if [[ "$AKS_AAD_AUTH" == true ]]; then + if [[ -z "$ARM_CLIENT_ID" ]]; then + # Get the ID of the currently signed in user + current_user_object_id=$(az ad signed-in-user show --query id -o tsv) + else + # Get the ID of the service principal for ARM_CLIENT_ID + current_user_object_id=$(az ad sp show --id "$ARM_CLIENT_ID" --query id -o tsv) + fi + echo "Enabling AKS AAD authentication (current user object ID: $current_user_object_id)" +fi + +cat << EOF > "$script_dir/../terraform/terraform.tfvars" +location = "${LOCATION}" +prefix = "dev" +unique_username = "${USERNAME}" +cosmosdb_database_name = "cargo" +cosmosdb_container1_name = "valid-cargo" +cosmosdb_container2_name = "invalid-cargo" +cosmosdb_container3_name = "operations" +service_bus_queue1_name = "ingest-cargo" +service_bus_queue2_name = "operation-state" +service_bus_topic_name = "validated-cargo" +service_bus_subscription1_name = "valid-cargo" +service_bus_subscription2_name = "invalid-cargo" +service_bus_topic_rule1_name = "valid" +service_bus_topic_rule2_name = "invalid" +aks_aad_auth = ${AKS_AAD_AUTH} +aks_aad_admin_user_object_id = "${current_user_object_id}" +notification_email_address = "${EMAIL_ADDRESS}" +EOF + +echo -e "\n*** Terraform parameters file created" + +cd "$script_dir"/../terraform/ + +if [[ -n "$TERRAFORM_STATE_STORAGE_ACCOUNT_NAME" ]]; then + # init with Azure backend + echo -e "\n*** Initializing Terraform (with Azure backend: $TERRAFORM_STATE_STORAGE_ACCOUNT_NAME)" +cat > backend.tf << EOF +terraform { + backend "azurerm" {} +} +EOF + terraform init -upgrade \ + -backend-config "resource_group_name=${TERRAFORM_STATE_RESOURCE_GROUP_NAME}" \ + -backend-config "storage_account_name=${TERRAFORM_STATE_STORAGE_ACCOUNT_NAME}" \ + -backend-config "container_name=${TERRAFORM_STATE_CONTAINER_NAME}" \ + -backend-config "key=${TERRAFORM_STATE_KEY}" +else + # init with local backend + echo -e "\n*** Initializing Terraform (with local backend)" + rm -rf backend.tf + terraform init -upgrade +fi + +echo -e "\n*** Planning Terraform resources" + +terraform plan -var-file=terraform.tfvars -out=plan.out + +echo -e "\n*** Deploying Terraform resources" + +terraform apply "plan.out" + +echo -e "\n*** Gathering required outputs" + +terraform output -json | jq "[. | to_entries | .[] | {key:.key, value: .value.value}] | from_entries" > "${script_dir}/../../output.json" diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/.gitignore b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/.gitignore new file mode 100644 index 0000000..d4951f9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/.gitignore @@ -0,0 +1,5 @@ +.terraform/* +.terraform* +*.tfstate +*.tfstate.backup +backend.tf \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/main.tf new file mode 100644 index 0000000..33bb716 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/main.tf @@ -0,0 +1,171 @@ +data "azurerm_client_config" "current_config" {} + +resource "azurerm_resource_group" "rg" { + name = "rg-${var.prefix}-tf-${var.unique_username}" + location = var.location +} + +//Cosmos DB module +resource "azurecaf_name" "cosmosdb" { + name = "accl" + resource_type = "azurerm_cosmosdb_account" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "cosmosdb" { + source = "./modules/cosmos" + account_name = azurecaf_name.cosmosdb.result + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + cosmosdb_database_name = var.cosmosdb_database_name + cosmosdb_valid_container_name = var.cosmosdb_container1_name + cosmosdb_invalid_container_name = var.cosmosdb_container2_name + cosmosdb_operations_container_name = var.cosmosdb_container3_name + log_analytics_workspace_id = module.app_insights.log_analytics_workspace_id +} + +//ACR module +resource "azurecaf_name" "acr" { + name = "accl" + resource_type = "azurerm_container_registry" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "acr" { + source = "./modules/acr" + name = azurecaf_name.acr.result + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name +} + +//AKS module +resource "azurecaf_name" "aks" { + name = "accl" + resource_type = "azurerm_kubernetes_cluster" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "aks" { + source = "./modules/aks" + name = azurecaf_name.aks.result + prefix = var.prefix + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + acr_id = module.acr.acr_id + log_analytics_workspace_id = module.app_insights.log_analytics_workspace_id + aks_aad_auth = var.aks_aad_auth + aks_aad_admin_user_object_id = var.aks_aad_admin_user_object_id +} + +//Application Insights module +resource "azurecaf_name" "appi" { + name = "accl" + resource_type = "azurerm_application_insights" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +resource "azurecaf_name" "log" { + name = "accl" + resource_type = "azurerm_log_analytics_workspace" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "app_insights" { + source = "./modules/app_insights" + app_insights_name = azurecaf_name.appi.result + log_analytics_workspace_name = azurecaf_name.log.result + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name +} + +module "workbooks" { + source = "./modules/workbooks" + workspace_id = module.app_insights.log_analytics_workspace_id + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + servicebus_namespace_id = module.service_bus.servicebus_namespace_id + app_insights_id = module.app_insights.app_insights_id + key_vault_id = module.key_vault.kv_id + aks_id = module.aks.aks_id +} + +module "alerts" { + source = "./modules/alerts" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + notification_email_address = var.notification_email_address + action_group_name = "default-actiongroup" + cosmosdb_id = module.cosmosdb.cosmosdb_id + servicebus_namespace_id = module.service_bus.servicebus_namespace_id + aks_id = module.aks.aks_id + kv_id = module.key_vault.kv_id + app_insights_id = module.app_insights.app_insights_id + log_analytics_workspace_id = module.app_insights.log_analytics_workspace_id +} + +//Service Bus module +resource "azurecaf_name" "service_bus" { + name = "accl" + resource_type = "azurerm_servicebus_namespace" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "service_bus" { + source = "./modules/service_bus" + services_bus_namespace_name = azurecaf_name.service_bus.result + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + log_analytics_workspace_id = module.app_insights.log_analytics_workspace_id + service_bus_queue1_name = var.service_bus_queue1_name + service_bus_queue2_name = var.service_bus_queue2_name + service_bus_topic_name = var.service_bus_topic_name + service_bus_valid_subscription = var.service_bus_subscription1_name + service_bus_invalid_subscription = var.service_bus_subscription2_name + service_bus_valid_rule = var.service_bus_topic_rule1_name + service_bus_invalid_rule = var.service_bus_topic_rule2_name +} + +//Key Vault module +resource "azurecaf_name" "kv_compute" { + name = "accl" + resource_type = "azurerm_key_vault" + prefixes = [var.prefix] + suffixes = [azurerm_resource_group.rg.location] + random_length = 3 + clean_input = true +} + +module "key_vault" { + source = "./modules/keyvault" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kev_vault_name = azurecaf_name.kv_compute.result + log_analytics_workspace_id = module.app_insights.log_analytics_workspace_id + aks_key_vault_secret_provider_object_id = module.aks.aks_key_vault_secret_provider_object_id + key_vault_secrets = tomap( + { + "AppInsightsConnectionString" = module.app_insights.connection_string + "ServiceBusConnectionString" = module.service_bus.connection_string + "CosmosDBEndpoint" = module.cosmosdb.cosmosdb_endpoint + "CosmosDBKey" = module.cosmosdb.cosmosdb_key + } + ) +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/main.tf new file mode 100644 index 0000000..fbfd094 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/main.tf @@ -0,0 +1,7 @@ +resource "azurerm_container_registry" "acr" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + sku = "Standard" + admin_enabled = false +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/outputs.tf new file mode 100644 index 0000000..492dc93 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/outputs.tf @@ -0,0 +1,8 @@ +output "acr_id" { + value = azurerm_container_registry.acr.id + sensitive = true +} + +output "acr_name" { + value = azurerm_container_registry.acr.name +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/variables.tf new file mode 100644 index 0000000..c80de96 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/acr/variables.tf @@ -0,0 +1,14 @@ +variable "name" { + type = string + description = "resource name" +} + +variable "location" { + type = string + description = "The Azure region in which ACR should be provisioned" +} + +variable "resource_group_name" { + type = string + description = "The Azure Resource Group where the ACR should be provisioned" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/main.tf new file mode 100644 index 0000000..3949a23 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/main.tf @@ -0,0 +1,63 @@ +data "azurerm_client_config" "current_config" {} + +resource "azurerm_kubernetes_cluster" "aks" { + name = var.name + location = var.location + resource_group_name = var.resource_group_name + dns_prefix = var.kubernetes_dns_prefix + private_cluster_enabled = false + + + default_node_pool { + name = "agentpool" + min_count = 1 + max_count = var.kubernetes_node_count + enable_auto_scaling = true + type = "VirtualMachineScaleSets" + vm_size = var.kubernetes_vm_size + os_disk_size_gb = var.kubernetes_vm_disk_size + } + + // Use dynamic to conditionally set AAD auth block + dynamic "azure_active_directory_role_based_access_control" { + for_each = var.aks_aad_auth ? [1] : [] + content { + managed = true + tenant_id = data.azurerm_client_config.current_config.tenant_id + azure_rbac_enabled = true + } + } + + identity { + type = "SystemAssigned" + } + + key_vault_secrets_provider { + secret_rotation_enabled = true + secret_rotation_interval = "2m" + } + + oms_agent { + log_analytics_workspace_id = var.log_analytics_workspace_id + } +} + +resource "azurerm_role_assignment" "acrpull_role" { + scope = var.acr_id + role_definition_name = "AcrPull" + principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id + skip_service_principal_aad_check = true +} + +resource "azurerm_role_assignment" "aks_admin_role" { + count = var.aks_aad_auth ? 1 : 0 + scope = azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster Admin Role" + principal_id = var.aks_aad_admin_user_object_id +} +resource "azurerm_role_assignment" "aks_user_role" { + count = var.aks_aad_auth ? 1 : 0 + scope = azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster User Role" + principal_id = var.aks_aad_admin_user_object_id +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/outputs.tf new file mode 100644 index 0000000..594c2a4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/outputs.tf @@ -0,0 +1,17 @@ +output "aks_name" { + value = azurerm_kubernetes_cluster.aks.name +} + +output "aks_id" { + value = azurerm_kubernetes_cluster.aks.id +} + +output "aks_key_vault_secret_provider_client_id" { + value = azurerm_kubernetes_cluster.aks.key_vault_secrets_provider[0].secret_identity[0].client_id + sensitive = true +} + +output "aks_key_vault_secret_provider_object_id" { + value = azurerm_kubernetes_cluster.aks.key_vault_secrets_provider[0].secret_identity[0].object_id + sensitive = true +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/variables.tf new file mode 100644 index 0000000..15bf9af --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/aks/variables.tf @@ -0,0 +1,65 @@ +variable "name" { + type = string + description = "The AKS resource name" +} + +variable "location" { + type = string + description = "The Azure region in which AKS should be provisioned" +} + +variable "resource_group_name" { + type = string + description = "The Azure Resource Group where the AKS should be provisioned" +} + +variable "prefix" { + type = string + description = "Name prefix" +} + +variable "kubernetes_dns_prefix" { + type = string + description = "AKS DNS prefix" + default = "aks" +} + +variable "kubernetes_node_count" { + type = number + description = "The agent count" + default = 3 +} + +variable "kubernetes_vm_size" { + type = string + description = "Azure Kubernetes Cluster VM Size" + default = "Standard_D2s_v3" +} + +variable "kubernetes_vm_disk_size" { + type = string + description = "Azure Kubernetes Cluster VM Disk Size" + default = "30" +} + +variable "log_analytics_workspace_id" { + type = string + description = "The ID of the Log Analytics Workspace related to the cluster." +} + +variable "acr_id" { + type = string + description = "Id from ACR to get acrPull role assignment" +} + +variable "aks_aad_auth" { + type = bool + description = "Configure Azure Active Directory authentication for Kubernetes cluster" + default = false +} + +variable "aks_aad_admin_user_object_id" { + type = string + description = "Object ID of the AAD user to be added as an admin to the AKS cluster" + default = "" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/main.tf new file mode 100644 index 0000000..15003ae --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/main.tf @@ -0,0 +1,975 @@ +resource "azurerm_monitor_action_group" "default" { + name = var.action_group_name + resource_group_name = var.resource_group_name + short_name = length(var.action_group_name) <= 12 ? var.action_group_name : substr(var.action_group_name, 0, 12) + + email_receiver { + name = "email-receiver" + email_address = var.notification_email_address + use_common_alert_schema = false + } +} + +resource "azurerm_monitor_metric_alert" "cosmos_rus" { + name = "cosmos_rus" + resource_group_name = var.resource_group_name + scopes = [var.cosmosdb_id] + severity = 1 + description = "Alert when RUs exceed 400." + enabled = false + frequency = "PT1M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.DocumentDB/databaseAccounts" + metric_name = "TotalRequestUnits" + aggregation = "Total" + operator = "GreaterThan" + threshold = 400 + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "cosmos_invalid_cargo" { + name = "cosmos_invalid_cargo" + resource_group_name = var.resource_group_name + scopes = [var.cosmosdb_id] + severity = 3 + description = "Alert when more than 10 documents have been saved to the invalid-cargo container." + enabled = false + frequency = "PT1M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.DocumentDB/databaseAccounts" + metric_name = "DocumentCount" + aggregation = "Total" + operator = "GreaterThan" + threshold = 10 + dimension { + name = "CollectionName" + operator = "Include" + values = ["invalid_cargo"] + } + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "service_bus_abandoned_messages" { + name = "service_bus_abandoned_messages" + resource_group_name = var.resource_group_name + scopes = [var.servicebus_namespace_id] + severity = 2 + description = "Alert when a Service Bus entity has abandoned more than 10 messages." + enabled = false + frequency = "PT1M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.ServiceBus/namespaces" + metric_name = "AbandonMessage" + aggregation = "Total" + operator = "GreaterThan" + threshold = 10 + dimension { + name = "EntityName" + operator = "Include" + values = ["*"] + } + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "service_bus_dead_lettered_messages" { + name = "service_bus_dead_lettered_messages" + resource_group_name = var.resource_group_name + scopes = [var.servicebus_namespace_id] + severity = 2 + description = "Alert when a Service Bus entity has dead-lettered more than 10 messages." + enabled = false + frequency = "PT1M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.ServiceBus/namespaces" + metric_name = "DeadletteredMessages" + aggregation = "Average" + operator = "GreaterThan" + threshold = 10 + dimension { + name = "EntityName" + operator = "Include" + values = ["*"] + } + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "service_bus_throttled_requests" { + name = "service_bus_throttled_requests" + resource_group_name = var.resource_group_name + scopes = [var.servicebus_namespace_id] + severity = 2 + description = "Alert when a Service Bus entity has throttled more than 10 requests." + enabled = false + frequency = "PT1M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.ServiceBus/namespaces" + metric_name = "ThrottledRequests" + aggregation = "Total" + operator = "GreaterThan" + threshold = 10 + dimension { + name = "EntityName" + operator = "Include" + values = ["*"] + } + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "aks_cpu_percentage" { + name = "aks_cpu_percentage" + resource_group_name = var.resource_group_name + scopes = [var.aks_id] + severity = 2 + description = "Alert when Node CPU percentage exceeds 80." + enabled = false + frequency = "PT5M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.ContainerService/managedClusters" + metric_name = "node_cpu_usage_percentage" + aggregation = "Average" + operator = "GreaterThan" + threshold = 80 + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "aks_memory_percentage" { + name = "aks_memory_percentage" + resource_group_name = var.resource_group_name + scopes = [var.aks_id] + severity = 2 + description = "Alert when Node memory working set percentage exceeds 80." + enabled = false + frequency = "PT5M" + window_size = "PT5M" + + criteria { + metric_namespace = "Microsoft.ContainerService/managedClusters" + metric_name = "node_memory_working_set_percentage" + aggregation = "Average" + operator = "GreaterThan" + threshold = 80 + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +resource "azurerm_monitor_metric_alert" "key_vault_saturation_rate" { + name = "key_vault_saturation_rate" + resource_group_name = var.resource_group_name + scopes = [var.kv_id] + severity = 3 + description = "Alert when Key Vault saturation falls outside the range of a dynamic threshold." + enabled = false + frequency = "PT5M" + window_size = "PT5M" + + dynamic_criteria { + metric_namespace = "Microsoft.KeyVault/vaults" + metric_name = "SaturationShoebox" + aggregation = "Average" + operator = "GreaterOrLessThan" + alert_sensitivity = "Medium" + evaluation_total_count = 4 + evaluation_failure_count = 4 + } + + action { + action_group_id = azurerm_monitor_action_group.default.id + } +} + +# Tenant specific issues prevent deployment of custom metric alert +# +# resource "azurerm_monitor_metric_alert" "product_qty_scheduled_for_destination_port" { +# name = "product_qty_scheduled_for_destination_port" +# resource_group_name = var.resource_group_name +# scopes = [var.app_insights_id] +# severity = 3 +# description = "Alert when a single port/destination receives more than quantity 1000 of a given product." +# enabled = false +# frequency = "PT1M" +# window_size = "PT1M" + +# criteria { +# metric_namespace = "azure.applicationinsights" +# metric_name = "port_product_qty" +# aggregation = "Total" +# operator = "GreaterThan" +# threshold = 1000 +# skip_metric_validation = true + +# dimension { +# name = "destination" +# operator = "Include" +# values = ["*"] +# } + +# dimension { +# name = "product" +# operator = "Include" +# values = ["*"] +# } +# } + +# action { +# action_group_id = azurerm_monitor_action_group.default.id +# } +# } + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "microservice_exceptions" { + name = "microservice_exceptions" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when a microservice throws more than 5 exceptions." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + exceptions + QUERY + time_aggregation_method = "Count" + threshold = 5 + operator = "GreaterThan" + + dimension { + name = "cloud_RoleName" + operator = "Include" + values = ["*"] + } + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "cargo_processing_api_requests" { + name = "cargo_processing_api_requests" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 3 + description = "Alert when the cargo-processing-api microservice is not receiving any requests." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}") + QUERY + time_aggregation_method = "Count" + # usage of the "Equal" operator is currently blocked + # LessThan 1 should suffice as a workaround for Equal 0 until the bug is fixed is released in 3.36.0 + # please see discussion at https://github.com/hashicorp/terraform-provider-azurerm/issues/19581 + threshold = 1 + operator = "LessThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "e2e_average_duration" { + name = "e2e_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the end to end average request duration exceeds 5 seconds." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + let cargo_processing_api = requests + | where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}") + | project-rename ingest_timestamp = timestamp + | project ingest_timestamp, operation_Id; + let operation_api_succeeded = requests + | where cloud_RoleName == "operations-api" and name == "ServiceBus.process" and customDimensions["operation-state"] == "Succeeded" + | extend operation_api_completed = timestamp + (duration*1ms) + | project operation_Id, operation_api_completed; + cargo_processing_api + | join kind=inner operation_api_succeeded on $left.operation_Id == $right.operation_Id + | extend end_to_end_Duration_ms = (operation_api_completed - ingest_timestamp) /1ms + | summarize avg(end_to_end_Duration_ms) + QUERY + time_aggregation_method = "Average" + threshold = 5000 + operator = "GreaterThan" + metric_measure_column = "avg_end_to_end_Duration_ms" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "cargo_processing_api_average_duration" { + name = "cargo_processing_api_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the cargo-processing-api microservice average request duration exceeds 2 seconds." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "cargo-processing-api" and (name == "POST /cargo/" or name == "PUT /cargo/{cargoId}") + | summarize avg(duration) + QUERY + time_aggregation_method = "Average" + threshold = 2000 + operator = "GreaterThan" + metric_measure_column = "avg_duration" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "cargo_processing_validator_average_duration" { + name = "cargo_processing_validator_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the cargo-processing-validator microservice average request duration exceeds 2 seconds." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "cargo-processing-validator" and (name == "ServiceBus.ProcessMessage" or name == "ServiceBusQueue.ProcessMessage") + | summarize avg(duration) + QUERY + time_aggregation_method = "Average" + threshold = 2000 + operator = "GreaterThan" + metric_measure_column = "avg_duration" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "valid_cargo_manager_average_duration" { + name = "valid_cargo_manager_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the valid-cargo-manager microservice average request duration exceeds 2 seconds." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "valid-cargo-manager" and name == "ServiceBusTopic.ProcessMessage" + | summarize avg(duration) + QUERY + time_aggregation_method = "Average" + threshold = 2000 + operator = "GreaterThan" + metric_measure_column = "avg_duration" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "invalid_cargo_manager_average_duration" { + name = "invalid_cargo_manager_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the invalid-cargo-manager microservice average request duration exceeds 2 seconds." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "invalid-cargo-manager" and name == "ServiceBusTopic.ProcessMessage" + | summarize avg(duration) + QUERY + time_aggregation_method = "Average" + threshold = 2000 + operator = "GreaterThan" + metric_measure_column = "avg_duration" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "operations_api_average_duration" { + name = "operations_api_average_duration" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the operations-api microservice average request duration exceeds 1 second." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "operations-api" and name == "ServiceBus.process" + | summarize avg(duration) + QUERY + time_aggregation_method = "Average" + threshold = 1000 + operator = "GreaterThan" + metric_measure_column = "avg_duration" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "log_analytics_data_ingestion_daily_cap" { + name = "log_analytics_data_ingestion_daily_cap" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.log_analytics_workspace_id] + severity = 2 + description = "Alert when the Log Analytics data ingestion daily cap has been reached." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + _LogOperation + | where Category == "Ingestion" + | where Operation has "Data collection" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + resource_id_column = "_ResourceId" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "log_analytics_data_ingestion_rate" { + name = "log_analytics_data_ingestion_rate" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.log_analytics_workspace_id] + severity = 2 + description = "Alert when the Log Analytics max data ingestion rate has been reached." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + _LogOperation + | where Category == "Ingestion" + | where Operation has "Ingestion rate" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + resource_id_column = "_ResourceId" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "log_analytics_operational_issues" { + name = "log_analytics_operational_issues" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "P1D" + window_duration = "P1D" + scopes = [var.log_analytics_workspace_id] + severity = 3 + description = "Alert when the Log Analytics workspace has an operational issue." + enabled = false + # tf stateful rules can not run in a frequency greater than 12 hours, auto_mitigation_enabled must be false + auto_mitigation_enabled = false + + criteria { + query = <<-QUERY + _LogOperation + | where Level == "Warning" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + resource_id_column = "_ResourceId" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "cargo_processing_api_health_check_failure" { + name = "cargo_processing_api_health_check_failure" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when a cargo-processing-api microservice health check fails." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "cargo-processing-api" and name == "GET /actuator/health" and success == "False" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "cargo_processing_api_health_check_not_reporting" { + name = "cargo_processing_api_health_check_not_reporting" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the cargo-processing-api microservice health check is not reporting." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "cargo-processing-api" and name == "GET /actuator/health" + QUERY + time_aggregation_method = "Count" + # usage of the "Equal" operator is currently blocked + # LessThan 1 should suffice as a workaround for Equal 0 until the bug is fixed is released in 3.36.0 + # please see discussion at https://github.com/hashicorp/terraform-provider-azurerm/issues/19581 + threshold = 1 + operator = "LessThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "valid_cargo_manager_health_check_failure" { + name = "valid_cargo_manager_health_check_failure" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT30M" + window_duration = "PT30M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when a valid-cargo-manager microservice health check fails." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + customMetrics + | where cloud_RoleName == "valid-cargo-manager" and name == "HeartbeatState" and value != 2 + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "valid_cargo_manager_health_check_not_reporting" { + name = "valid_cargo_manager_health_check_not_reporting" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT30M" + window_duration = "PT30M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the valid-cargo-manager microservice health check is not reporting." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + customMetrics + | where cloud_RoleName == "valid-cargo-manager" and name == "HeartbeatState" + QUERY + time_aggregation_method = "Count" + # usage of the "Equal" operator is currently blocked + # LessThan 1 should suffice as a workaround for Equal 0 until the bug is fixed is released in 3.36.0 + # please see discussion at https://github.com/hashicorp/terraform-provider-azurerm/issues/19581 + threshold = 1 + operator = "LessThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "invalid_cargo_manager_health_check_failure" { + name = "invalid_cargo_manager_health_check_failure" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when an invalid-cargo-manager microservice health check fails." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + traces + | where cloud_RoleName == "invalid-cargo-manager" and message contains "peeked at messages for over" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "invalid_cargo_manager_health_check_not_reporting" { + name = "invalid_cargo_manager_health_check_not_reporting" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the invalid-cargo-manager microservice health check is not reporting." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + traces + | where cloud_RoleName == "invalid-cargo-manager" and (message contains "since last peek" or message contains "peeked at messages for over") + QUERY + time_aggregation_method = "Count" + # usage of the "Equal" operator is currently blocked + # LessThan 1 should suffice as a workaround for Equal 0 until the bug is fixed is released in 3.36.0 is released in 3.36.0 + # please see discussion at https://github.com/hashicorp/terraform-provider-azurerm/issues/19581 + threshold = 1 + operator = "LessThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "operations_api_health_check_failure" { + name = "operations_api_health_check_failure" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when an operations-api microservice health check fails." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "operations-api" and name == "GET /actuator/health" and success == "False" + QUERY + time_aggregation_method = "Count" + threshold = 0 + operator = "GreaterThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "operations_api_health_check_not_reporting" { + name = "operations_api_health_check_not_reporting" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.app_insights_id] + severity = 1 + description = "Alert when the operations-api microservice health check is not reporting." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + requests + | where cloud_RoleName == "operations-api" and name == "GET /actuator/health" + QUERY + time_aggregation_method = "Count" + # usage of the "Equal" operator is currently blocked + # LessThan 1 should suffice as a workaround for Equal 0 until the bug is fixed is released in 3.36.0 + # please see discussion at https://github.com/hashicorp/terraform-provider-azurerm/issues/19581 + threshold = 1 + operator = "LessThan" + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} + +resource "azurerm_monitor_scheduled_query_rules_alert_v2" "aks_pod_restarts" { + name = "aks_pod_restarts" + resource_group_name = var.resource_group_name + location = var.location + + evaluation_frequency = "PT5M" + window_duration = "PT5M" + scopes = [var.log_analytics_workspace_id] + severity = 1 + description = "Alert when a microservice restarts more than once." + enabled = false + auto_mitigation_enabled = true + + criteria { + query = <<-QUERY + KubePodInventory + | summarize numRestarts = sum(PodRestartCount) by ServiceName + QUERY + time_aggregation_method = "Total" + threshold = 1 + operator = "GreaterThan" + metric_measure_column = "numRestarts" + + dimension { + name = "ServiceName" + operator = "Include" + values = [ + "cargo-processing-api", + "cargo-processing-validator", + "invalid-cargo-manager", + "operations-api", + "valid-cargo-manager" + ] + } + + failing_periods { + minimum_failing_periods_to_trigger_alert = 1 + number_of_evaluation_periods = 1 + } + } + + action { + action_groups = [azurerm_monitor_action_group.default.id] + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/variables.tf new file mode 100644 index 0000000..9b5b153 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/alerts/variables.tf @@ -0,0 +1,49 @@ +variable "location" { + type = string + description = "Location for the Azure Workbook" +} + +variable "resource_group_name" { + type = string + description = "Resource group for the Azure Workbook" +} + +variable "action_group_name" { + type = string + description = "Name for the default action group" +} + +variable "notification_email_address" { + type = string + description = "Email address for alert notifications" +} + +variable "cosmosdb_id" { + type = string + description = "Id for monitored Cosmos DB" +} + +variable "servicebus_namespace_id" { + type = string + description = "Id for monitored Service Bus namespace" +} + +variable "aks_id" { + type = string + description = "Id for monitored AKS cluster" +} + +variable "kv_id" { + type = string + description = "Id for monitored Key Vault" +} + +variable "app_insights_id" { + type = string + description = "Id for monitored Application Insights" +} + +variable "log_analytics_workspace_id" { + type = string + description = "Id for monitored Log Analytics workspace" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/main.tf new file mode 100644 index 0000000..69b7e97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/main.tf @@ -0,0 +1,28 @@ +resource "azurerm_application_insights" "app_insights" { + name = var.app_insights_name + location = var.location + resource_group_name = var.resource_group_name + application_type = var.application_type + workspace_id = azurerm_log_analytics_workspace.log_analytics.id +} + +resource "azurerm_log_analytics_workspace" "log_analytics" { + name = var.log_analytics_workspace_name + location = var.location + resource_group_name = var.resource_group_name + sku = var.log_analytics_workspace_sku + retention_in_days = 31 +} + +resource "azurerm_log_analytics_solution" "log_solution" { + solution_name = "ContainerInsights" + location = azurerm_log_analytics_workspace.log_analytics.location + resource_group_name = azurerm_log_analytics_workspace.log_analytics.resource_group_name + workspace_resource_id = azurerm_log_analytics_workspace.log_analytics.id + workspace_name = azurerm_log_analytics_workspace.log_analytics.name + + plan { + publisher = "Microsoft" + product = "OMSGallery/ContainerInsights" + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/outputs.tf new file mode 100644 index 0000000..5516376 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/outputs.tf @@ -0,0 +1,16 @@ +output "name" { + value = azurerm_application_insights.app_insights.name +} + +output "connection_string" { + value = azurerm_application_insights.app_insights.connection_string + sensitive = true +} + +output "log_analytics_workspace_id" { + value = azurerm_log_analytics_workspace.log_analytics.id +} + +output "app_insights_id" { + value = azurerm_application_insights.app_insights.id +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/variables.tf new file mode 100644 index 0000000..47dfbdb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/app_insights/variables.tf @@ -0,0 +1,31 @@ +variable "app_insights_name" { + type = string + description = "The name of the Application Insights resource" +} + +variable "location" { + type = string + description = "The Azure region in which AppInsights should be provisioned" +} + +variable "resource_group_name" { + type = string + description = "The Azure Resource Group where the AppInsights should be provisioned" +} + +variable "application_type" { + type = string + description = "The kind of application that will be sending the telemetry" + default = "web" +} + +variable "log_analytics_workspace_name" { + type = string + description = "The resource name for log analytics" +} + +variable "log_analytics_workspace_sku" { + type = string + description = "Specifies the SKU of the Log Analytics Workspace." + default = "PerGB2018" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/main.tf new file mode 100644 index 0000000..a1be6ba --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/main.tf @@ -0,0 +1,151 @@ +resource "azurerm_cosmosdb_account" "account" { + name = var.account_name + location = var.location + resource_group_name = var.resource_group_name + offer_type = "Standard" + kind = "GlobalDocumentDB" + enable_automatic_failover = true + + + consistency_policy { + consistency_level = "Session" + max_interval_in_seconds = 400 + } + + geo_location { + location = var.location + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_sql_database" "db" { + name = var.cosmosdb_database_name + resource_group_name = azurerm_cosmosdb_account.account.resource_group_name + account_name = azurerm_cosmosdb_account.account.name +} + +resource "azurerm_cosmosdb_sql_container" "valid_container" { + name = var.cosmosdb_valid_container_name + resource_group_name = azurerm_cosmosdb_account.account.resource_group_name + account_name = azurerm_cosmosdb_account.account.name + database_name = azurerm_cosmosdb_sql_database.db.name + partition_key_path = "/id" +} + +resource "azurerm_cosmosdb_sql_container" "invalid_container" { + name = var.cosmosdb_invalid_container_name + resource_group_name = azurerm_cosmosdb_account.account.resource_group_name + account_name = azurerm_cosmosdb_account.account.name + database_name = azurerm_cosmosdb_sql_database.db.name + partition_key_path = "/id" +} + +resource "azurerm_cosmosdb_sql_container" "operations_container" { + name = var.cosmosdb_operations_container_name + resource_group_name = azurerm_cosmosdb_account.account.resource_group_name + account_name = azurerm_cosmosdb_account.account.name + database_name = azurerm_cosmosdb_sql_database.db.name + partition_key_path = "/id" +} + + + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_settings" { + name = var.cosmos_db_diagnostic_settings_name + target_resource_id = azurerm_cosmosdb_account.account.id + log_analytics_workspace_id = var.log_analytics_workspace_id + log_analytics_destination_type = "AzureDiagnostics" + + /* + category groups are still not allowed so we need to set all fields one by one + reference: https://github.com/hashicorp/terraform-provider-azurerm/issues/17349 + supported log categories per resource can be found here: + https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories + */ + + log { + category = "DataPlaneRequests" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "QueryRuntimeStatistics" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "PartitionKeyStatistics" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "PartitionKeyRUConsumption" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "ControlPlaneRequests" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "CassandraRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "GremlinRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "MongoRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "TableApiRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + metric { + category = "Requests" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/outputs.tf new file mode 100644 index 0000000..38b080c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/outputs.tf @@ -0,0 +1,17 @@ +output "name" { + value = azurerm_cosmosdb_account.account.name +} + +output "cosmosdb_id" { + value = azurerm_cosmosdb_account.account.id +} + +output "cosmosdb_endpoint" { + value = azurerm_cosmosdb_account.account.endpoint +} + +output "cosmosdb_key" { + value = azurerm_cosmosdb_account.account.primary_key + sensitive = true +} + diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/variables.tf new file mode 100644 index 0000000..3704ed2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/cosmos/variables.tf @@ -0,0 +1,41 @@ +variable "account_name" { + description = "CosmosDB account name" +} + +variable "location" { + type = string + description = "The Azure region in which CosmosDB should be provisioned" +} + +variable "resource_group_name" { + type = string + description = "The Azure Resource Group where the CosmosDB should be provisioned" +} + +variable "cosmosdb_database_name" { + type = string + description = "Name for the Cosmos DB SQL database" +} + +variable "cosmosdb_valid_container_name" { + description = "Name for the Cosmos DB SQL container that stores valid cargo" +} + +variable "cosmosdb_invalid_container_name" { + description = "Name for the Cosmos DB SQL container that stores invalid cargo" +} + +variable "cosmosdb_operations_container_name" { + description = "Name for the Cosmos DB SQL container that stores operations" +} + +variable "cosmos_db_diagnostic_settings_name" { + type = string + description = "Name for the diagnostic settings" + default = "cosmosDbDiagnostics" +} + +variable "log_analytics_workspace_id" { + type = string + description = "Id for the targeted log analytics workspace" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/main.tf new file mode 100644 index 0000000..44b3734 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/main.tf @@ -0,0 +1,97 @@ +data "azurerm_client_config" "current_config" {} + +resource "azurerm_key_vault" "akv" { + name = var.kev_vault_name + location = var.location + resource_group_name = var.resource_group_name + tenant_id = data.azurerm_client_config.current_config.tenant_id + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "admin" { + key_vault_id = azurerm_key_vault.akv.id + tenant_id = data.azurerm_client_config.current_config.tenant_id + object_id = data.azurerm_client_config.current_config.object_id + + key_permissions = [ + "Create", + "Get", + "List", + "Delete" + ] + + secret_permissions = [ + "List", + "Set", + "Get", + "Delete", + "Purge", + "Recover", + "Backup", + "Restore" + ] +} + +resource "azurerm_key_vault_access_policy" "aks" { + key_vault_id = azurerm_key_vault.akv.id + tenant_id = data.azurerm_client_config.current_config.tenant_id + object_id = var.aks_key_vault_secret_provider_object_id + + secret_permissions = [ + "Get" + ] +} + +resource "azurerm_key_vault_secret" "akvSecret" { + for_each = var.key_vault_secrets + + name = each.key + value = each.value + key_vault_id = azurerm_key_vault.akv.id + content_type = "text/plain" + expiration_date = var.secrets_expiration_date + + # explicitly depend on access policy so destroy works + depends_on = [ + azurerm_key_vault_access_policy.admin + ] +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_settings" { + name = var.key_vault_diagnostic_settings_name + target_resource_id = azurerm_key_vault.akv.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + /* + category groups are still not allowed so we need to set all fields one by one + reference: https://github.com/hashicorp/terraform-provider-azurerm/issues/17349 + supported log categories per resource can be found here: + https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories + */ + + log { + category = "AuditEvent" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "AzurePolicyEvaluationDetails" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + + metric { + category = "AllMetrics" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/outputs.tf new file mode 100644 index 0000000..c655101 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/outputs.tf @@ -0,0 +1,7 @@ +output "kv_name" { + value = azurerm_key_vault.akv.name +} + +output "kv_id" { + value = azurerm_key_vault.akv.id +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/variables.tf new file mode 100644 index 0000000..82d1e10 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/keyvault/variables.tf @@ -0,0 +1,41 @@ +variable "kev_vault_name" { + type = string + description = "Name of the Key Vault instance" +} + +variable "location" { + type = string + description = "The Azure region in which Key Vault should be provisioned" +} + +variable "resource_group_name" { + type = string + description = "The Azure Resource Group where the Key Vault should be provisioned" +} + +variable "key_vault_secrets" { + type = map(string) + description = "Map name/value of secrets for the AKV." +} + +variable "secrets_expiration_date" { + type = string + description = "Secrets expiration date." + default = "2022-12-30T20:00:00Z" +} + +variable "key_vault_diagnostic_settings_name" { + type = string + description = "Name for the diagnostic settings" + default = "keyVaultDiagnostics" +} + +variable "log_analytics_workspace_id" { + type = string + description = "Id for the targeted log analytics workspace" +} + +variable "aks_key_vault_secret_provider_object_id" { + type = string + description = "The Object ID of the user-defined Managed Identity used by the AKS Secret Provider" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/main.tf new file mode 100644 index 0000000..cab4c85 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/main.tf @@ -0,0 +1,103 @@ +resource "azurerm_servicebus_namespace" "bus_namespace" { + name = var.services_bus_namespace_name + location = var.location + resource_group_name = var.resource_group_name + capacity = var.service_bus_capacity + sku = var.service_bus_sku +} + +resource "azurerm_servicebus_queue" "bus_queue1" { + name = var.service_bus_queue1_name + namespace_id = azurerm_servicebus_namespace.bus_namespace.id +} + +resource "azurerm_servicebus_queue" "bus_queue2" { + name = var.service_bus_queue2_name + namespace_id = azurerm_servicebus_namespace.bus_namespace.id +} + +resource "azurerm_servicebus_topic" "validation_topic" { + name = var.service_bus_topic_name + namespace_id = azurerm_servicebus_namespace.bus_namespace.id +} + +resource "azurerm_servicebus_subscription" "valid_subscription" { + name = var.service_bus_valid_subscription + topic_id = azurerm_servicebus_topic.validation_topic.id + max_delivery_count = 1 +} + +resource "azurerm_servicebus_subscription" "invalid_subscription" { + name = var.service_bus_invalid_subscription + topic_id = azurerm_servicebus_topic.validation_topic.id + max_delivery_count = 1 +} + +resource "azurerm_servicebus_subscription_rule" "valid_rule" { + name = var.service_bus_valid_rule + subscription_id = azurerm_servicebus_subscription.valid_subscription.id + filter_type = "SqlFilter" + sql_filter = "valid = True" +} + +resource "azurerm_servicebus_subscription_rule" "invalid_rule" { + name = var.service_bus_invalid_rule + subscription_id = azurerm_servicebus_subscription.invalid_subscription.id + filter_type = "SqlFilter" + sql_filter = "valid = False" +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_settings" { + name = var.service_bus_diagnostic_settings_name + target_resource_id = azurerm_servicebus_namespace.bus_namespace.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + /* + category groups are still not allowed so we need to set all fields one by one + reference: https://github.com/hashicorp/terraform-provider-azurerm/issues/17349 + supported log categories per resource can be found here: + https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories + */ + + log { + category = "OperationalLogs" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "ApplicationMetricsLogs" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "RuntimeAuditLogs" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + log { + category = "VNetAndIPFilteringLogs" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } + + metric { + category = "AllMetrics" + enabled = true + retention_policy { + days = 0 + enabled = false + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/outputs.tf new file mode 100644 index 0000000..493e693 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/outputs.tf @@ -0,0 +1,12 @@ +output "name" { + value = azurerm_servicebus_namespace.bus_namespace.name +} + +output "connection_string" { + value = azurerm_servicebus_namespace.bus_namespace.default_primary_connection_string + sensitive = true +} + +output "servicebus_namespace_id" { + value = azurerm_servicebus_namespace.bus_namespace.id +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/variables.tf new file mode 100644 index 0000000..c74a99a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/service_bus/variables.tf @@ -0,0 +1,72 @@ +variable "services_bus_namespace_name" { + type = string + description = "Name for the service bus namespace" +} + +variable "location" { + type = string + description = "Location for the service bus namespace" +} + +variable "resource_group_name" { + type = string + description = "Resource group for the service bus namespace" +} + +variable "service_bus_capacity" { + type = number + description = "Capacity for the Service Bus namespace" + default = 0 +} + +variable "service_bus_sku" { + type = string + description = "Sku for the service bus namespace" + default = "Standard" +} + +variable "service_bus_queue1_name" { + type = string + description = "Name for the first service bus queue (ingest)" +} + +variable "service_bus_queue2_name" { + type = string + description = "Name for the second service bus queue (operations)" +} + +variable "service_bus_topic_name" { + type = string + description = "Name for the service bus topic" +} + +variable "service_bus_valid_subscription" { + type = string + description = "Name for the valid subscription" +} + +variable "service_bus_invalid_subscription" { + type = string + description = "Name for the valid subscription" +} + +variable "service_bus_valid_rule" { + type = string + description = "Name for the valid rule" +} + +variable "service_bus_invalid_rule" { + type = string + description = "Name for the invalid rule" +} + +variable "service_bus_diagnostic_settings_name" { + type = string + description = "Name for the diagnostic settings" + default = "serviceBusDiagnostics" +} + +variable "log_analytics_workspace_id" { + type = string + description = "Id for the targeted log analytics workspace" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/main.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/main.tf new file mode 100644 index 0000000..7c95160 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/main.tf @@ -0,0 +1,42 @@ +resource "random_uuid" "index_uuid" { +} +resource "random_uuid" "observability_uuid" { +} +resource "random_uuid" "service_processing_uuid" { +} + +resource "azurerm_application_insights_workbook" "index" { + name = random_uuid.index_uuid.result + resource_group_name = var.resource_group_name + location = var.location + display_name = "Index" + source_id = lower(var.workspace_id) + data_json = templatefile( + "${path.module}/../../../workbooks/index.json", + { app_insights_id = var.app_insights_id, logs_workspace_id = urlencode(var.workspace_id), infrastructure_workbook_id = urlencode(azurerm_application_insights_workbook.infrastructure.id), system_workbook_id = urlencode(azurerm_application_insights_workbook.system_processing.id)} + ) +} + +resource "azurerm_application_insights_workbook" "infrastructure" { + name = random_uuid.observability_uuid.result + resource_group_name = var.resource_group_name + location = var.location + display_name = "Infrastructure" + source_id = lower(var.workspace_id) + data_json = templatefile( + "${path.module}/../../../workbooks/infrastructure.json", + { servicebus_namespace_id = var.servicebus_namespace_id, key_vault_id = var.key_vault_id, app_insights_id = var.app_insights_id, app_insights_id_url = urlencode(var.app_insights_id), aks_id = var.aks_id } + ) +} + +resource "azurerm_application_insights_workbook" "system_processing" { + name = random_uuid.service_processing_uuid.result + resource_group_name = var.resource_group_name + location = var.location + display_name = "System Processing" + source_id = lower(var.workspace_id) + data_json = templatefile( + "${path.module}/../../../workbooks/system-processing.json", + { app_insights_id = var.app_insights_id, app_insights_id_url = urlencode(var.app_insights_id) } + ) +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/variables.tf new file mode 100644 index 0000000..e27467c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/modules/workbooks/variables.tf @@ -0,0 +1,34 @@ +variable "workspace_id" { + type = string + description = "Name for the Azure Workbook" +} + +variable "location" { + type = string + description = "Location for the Azure Workbook" +} + +variable "resource_group_name" { + type = string + description = "Resource group for the Azure Workbook" +} + +variable "servicebus_namespace_id" { + type = string + description = "Id for monitored Service Bus Namespace" +} + +variable "app_insights_id" { + type = string + description = "Id for Application Insights resource" +} + +variable "key_vault_id" { + type = string + description = "Id for Key Vault resource" +} + +variable "aks_id" { + type = string + description = "Id for AKS cluster resource" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/outputs.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/outputs.tf new file mode 100644 index 0000000..62d00e1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/outputs.tf @@ -0,0 +1,36 @@ +output "rg_name" { + value = azurerm_resource_group.rg.name +} + +output "insights_name" { + value = module.app_insights.name +} + +output "sb_namespace_name" { + value = module.service_bus.name +} + +output "cosmosdb_name" { + value = module.cosmosdb.name +} + +output "kv_name" { + value = module.key_vault.kv_name +} + +output "acr_name" { + value = module.acr.acr_name +} + +output "aks_name" { + value = module.aks.aks_name +} + +output "aks_key_vault_secret_provider_client_id" { + value = module.aks.aks_key_vault_secret_provider_client_id + sensitive = true +} + +output "tenant_id" { + value = data.azurerm_client_config.current_config.tenant_id +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/provider.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/provider.tf new file mode 100644 index 0000000..8632878 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/provider.tf @@ -0,0 +1,28 @@ +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} + +terraform { + required_providers { + azuread = { + source = "hashicorp/azuread" + version = "~> 2.0.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "3.31.0" + } + azurecaf = { + source = "aztfmod/azurecaf" + version = "~> 1.2.10" + } + azapi = { + source = "azure/azapi" + version = "1.0.0" + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/sample.tfvars b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/sample.tfvars new file mode 100644 index 0000000..c24e63b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/sample.tfvars @@ -0,0 +1,15 @@ +location = "eastus" +prefix = "dev" +unique_username = "myusername" +cosmosdb_database_name = "cargo" +cosmosdb_container1_name = "valid-cargo" +cosmosdb_container2_name = "invalid-cargo" +cosmosdb_container3_name = "operations" +service_bus_queue1_name = "ingest-cargo" +service_bus_queue2_name = "operation-state" +service_bus_topic_name = "validated-cargo" +service_bus_subscription1_name = "valid-cargo" +service_bus_subscription2_name = "invalid-cargo" +service_bus_topic_rule1_name = "valid" +service_bus_topic_rule2_name = "invalid" +notification_email_address = "alias@microsoft.com" \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/variables.tf b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/variables.tf new file mode 100644 index 0000000..320b41b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/terraform/variables.tf @@ -0,0 +1,85 @@ +variable "location" { + type = string + description = "Specifies the supported Azure location (region) where the resources will be deployed" +} + +variable "prefix" { + type = string + description = "Prefix for resource names" +} + +variable "unique_username" { + type = string + description = "This value will explain who is the author of specific resources and will be reflected in every deployed tool" +} + +variable "cosmosdb_database_name" { + type = string + description = "Name for the Cosmos DB SQL database" +} + +variable "cosmosdb_container1_name" { + type = string + description = "Name for the first Cosmos DB SQL container" +} + +variable "cosmosdb_container2_name" { + type = string + description = "Name for the second Cosmos DB SQL container" +} + +variable "cosmosdb_container3_name" { + description = "Name for the third Cosmos DB SQL container" +} + +variable "service_bus_queue1_name" { + type = string + description = "Name for the first service bus queue (ingest)" +} + +variable "service_bus_queue2_name" { + type = string + description = "Name for the second service bus queue (operations)" +} + +variable "service_bus_topic_name" { + type = string + description = "Name for the Service Bus Topic" +} + +variable "service_bus_subscription1_name" { + type = string + description = "Name for the first Service Bus Subscription" +} + +variable "service_bus_subscription2_name" { + type = string + description = "Name for the second Service Bus Subscription" +} + +variable "service_bus_topic_rule1_name" { + type = string + description = "Name for the first Service Bus Subscriptions filter rulee" +} + +variable "service_bus_topic_rule2_name" { + type = string + description = "Name for the second Service Bus Subscriptions filter rulee" +} + +variable "aks_aad_auth" { + type = bool + description = "Configure Azure Active Directory authentication for Kubernetes cluster" + default = false +} + +variable "aks_aad_admin_user_object_id" { + type = string + description = "Object ID of the AAD user to be added as an admin to the AKS cluster" + default = "" +} + +variable "notification_email_address" { + type = string + description = "Email address for alert notifications" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/index.json b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/index.json new file mode 100644 index 0000000..fb94988 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/index.json @@ -0,0 +1,218 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "# Observability and Monitoring Main Dashboard\nThis workbook has been created to provide a consolidated view of microservices observability\n\nIt contains two main sections. The first one displays the Exceptions made from any of the components involved in the whole system.\n\nSecond section can redirect you to two more workbooks that are more focused on Infrastructure or System's behaviour to get a deeper insight of data collected." + }, + "name": "mainTitleText" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Exceptions" + }, + "name": "exceptionsText" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "${app_insights_id}" + ], + "parameters": [ + { + "id": "899fa4be-a565-4534-b537-6070e46fd44e", + "version": "KqlParameterItem/1.0", + "name": "Show", + "type": 2, + "isRequired": true, + "query": "datatable(x:string, y:string)[\r\n\"['New Failure Rate (%)'], ['Existing Failure Rate (%)']\", 'New and Existing Failures',\r\n\"['New Failure Rate (%)']\", 'Only New Failures',\r\n\"['Existing Failure Rate (%)']\", 'Only Existing Failures',\r\n]", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "['New Failure Rate (%)']" + }, + { + "id": "38721383-ec13-430d-8229-997332f57352", + "version": "KqlParameterItem/1.0", + "name": "OverTimeRange", + "type": 4, + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 1800000 + }, + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + } + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": { + "durationMs": 43200000 + } + }, + { + "id": "8dc31735-b2c2-40a9-94a6-2b73f69a9303", + "version": "KqlParameterItem/1.0", + "name": "UseComparisonTimeRangeOf", + "type": 1, + "isRequired": true, + "query": "let t = {OverTimeRange:seconds};\r\nlet w = case(t <= 86400, '7d', t <= 259200, '14d', t <= 120960, '28d', '60d');\r\nrange i from 1 to 1 step 1\r\n| project x = w", + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + { + "id": "3d002cfd-8dca-4015-9f77-26b62fcc2564", + "version": "KqlParameterItem/1.0", + "name": "ProblemFilter", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "exceptions\r\n| where timestamp {OverTimeRange}\r\n| summarize Count = count() by problemId\r\n| order by Count desc\r\n| project v = problemId, t = problemId, s=false\r\n| union (datatable(v:string, t:string, s:boolean)[\r\n'*', 'All Exceptions', true\r\n])\r\n", + "crossComponentResources": [ + "${app_insights_id}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.insights/components" + }, + { + "id": "a4eb0f16-861b-4587-ad9a-774db54a0cc2", + "version": "KqlParameterItem/1.0", + "name": "Source", + "type": 2, + "isRequired": true, + "query": "datatable(x:string, y:string)[\r\n'1 == 1', 'Server and Client Exceptions',\r\n'client_Type <> \"Browser\"', 'Only Server Exceptions',\r\n'client_Type == \"Browser\"', 'Only Client Exceptions',\r\n]", + "crossComponentResources": [ + "${app_insights_id}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "value": "1 == 1" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.insights/components" + }, + "name": "displayExceptionsParameters" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let startTime = {OverTimeRange:start};\r\nlet grain = {OverTimeRange:grain};\r\nlet bigWindowTimeRange = {UseComparisonTimeRangeOf};\r\nlet bigWindow = exceptions\r\n| where timestamp >= ago(bigWindowTimeRange) and timestamp < bin(startTime, grain)\r\n| where {Source}\r\n| where problemId in ({ProblemFilter}) or '*' in ({ProblemFilter})\r\n| summarize makeset(problemId, 10000);\r\nexceptions\r\n| where timestamp {OverTimeRange}\r\n| where {Source}\r\n| summarize Count = count(), Users = dcount(user_Id) by problemId\r\n| where problemId in ({ProblemFilter}) or '*' in ({ProblemFilter})\r\n| extend IsNew = iff(problemId !in (bigWindow), true, false)\r\n| where \"{Show}\" == \"['New Failure Rate (%)'], ['Existing Failure Rate (%)']\" or IsNew\r\n| order by Users desc, Count desc, problemId asc\r\n| project Problem = iff(IsNew, strcat('🔸 ', problemId), strcat('🔹 ', problemId)), ['Exception Count'] = Count, ['Users Affected'] = Users", + "size": 0, + "showAnalytics": true, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Exception Count", + "formatter": 4, + "formatOptions": { + "min": 0, + "palette": "yellow" + } + }, + { + "columnMatch": "Users Affected", + "formatter": 4, + "formatOptions": { + "min": 0, + "palette": "green" + } + } + ] + } + }, + "name": "servicesExceptionsQuery" + } + ] + }, + "name": "exceptionsGroup" + }, + { + "type": 1, + "content": { + "json": "## Performance" + }, + "name": "performanceTitleText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let cpu = performanceCounters\r\n| where name == \"% Processor Time Normalized\"\r\n| summarize CPU=avg(value) by cloud_RoleName;\r\nlet ioRate = performanceCounters\r\n| where name == \"IO Data Bytes/sec\"\r\n| summarize ioRate=avg(value) by cloud_RoleName;\r\nlet memory = performanceCounters\r\n| where name == \"Available Bytes\"\r\n| summarize Memory=avg(value) by cloud_RoleName;\r\nlet requests = requests\r\n| summarize req_Duration=avg(duration), requestsCount = count() by cloud_RoleName;\r\nlet average = dependencies\r\n| summarize average = avg(duration), dependenciesCount = count() by cloud_RoleName;\r\naverage\r\n| join kind=fullouter requests on cloud_RoleName\r\n| join kind=fullouter memory on cloud_RoleName \r\n| join kind=fullouter ioRate on cloud_RoleName\r\n| join kind=fullouter cpu on cloud_RoleName\r\n| project Service_Name=cloud_RoleName, CPU=iff(isnull(CPU), \"N/A\", strcat(bin(CPU, 0.01), \" %\")), Memory=iff(isnull(Memory), \"N/A\", format_bytes(Memory, 2, \"GB\")), IO_Rate=iff(isnull(ioRate), \"N/A\", strcat(bin(ioRate, 0.01), \" B/s\")), Avg_Dependency=iff(isnull(average), \"N/A\", strcat(bin(average, 0.01), \" ms\")), Dependencies_Count=iff(isnull(dependenciesCount), \"N/A\", tostring(dependenciesCount)), Req_Duration=iff(isnull(req_Duration), \"N/A\", strcat(bin(req_Duration, 0.01), \" ms\")), Requests_Count=iff(isnull(requestsCount), \"N/A\", tostring(requestsCount))", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ] + }, + "name": "servicesMonitoringQuery" + }, + { + "type": 1, + "content": { + "json": "## Additional workbooks\r\n\r\nThere are two workbooks made to keep track of the entire system's information.\r\n\r\n|Workbooks|Descriiption|Link|\r\n|---------|------------|----|\r\n|Infrastructure|Data related to infrastructure|[Link](https://portal.azure.com/#blade/AppInsightsExtension/UsageNotebookBlade/ComponentId/${logs_workspace_id}/ConfigurationId/${infrastructure_workbook_id}/Type/workbook/WorkbookTemplateName/Infrastructure)|\r\n|System|Data related to system functionality|[Link](https://portal.azure.com/#blade/AppInsightsExtension/UsageNotebookBlade/ComponentId/${logs_workspace_id}/ConfigurationId/${system_workbook_id}/Type/workbook/WorkbookTemplateName/System%20Processing)|" + }, + "name": "workbooksLinksText" + } + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" + } \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/infrastructure.json b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/infrastructure.json new file mode 100644 index 0000000..0662f4a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/infrastructure.json @@ -0,0 +1,477 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "# Infrastructure Dashboard\nThis workbook has been created to provide a consolidated view of the system infrastructure" + }, + "name": "mainTitleText" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Service Bus Telemetry\r\n\r\nThis section displays telemetry obtained from Service Bus operations." + }, + "name": "serviceBusTitleText" + }, + { + "type": 1, + "content": { + "json": "### Service Bus completed operations\r\nThese tiles display the following:\r\n* The fastest time an operation takes to be completed.\r\n* The average time all operations take to be completed.\r\n* The slowest time an operation takes to be completed.\r\n\r\nAll data is being displayed in milliseconds." + }, + "name": "serviceBusDescriptionText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "dependencies\r\n| where name == \"ServiceBus.complete\"\r\n| summarize Result = avg(duration), Name = \"Average\"\r\n| union (dependencies\r\n| where name == \"ServiceBus.complete\"\r\n| top 1 by duration asc \r\n| summarize count() by Result = duration, Name = \"Fastest\")\r\n| union ( dependencies\r\n| where name == \"ServiceBus.complete\"\r\n| top 1 by duration desc \r\n| summarize count() by Result = duration, Name = \"Slowest\")", + "size": 0, + "showAnalytics": true, + "title": "Statistics of service bus completed operations (ms)", + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Name", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Result", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 2, + "maximumSignificantDigits": 3 + } + } + }, + "showBorder": false, + "sortOrderField": 1 + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "id", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "duration", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "nodeIdField": "duration", + "sourceIdField": "timestamp", + "targetIdField": "name", + "graphOrientation": 3, + "showOrientationToggles": false, + "nodeSize": null, + "staticNodeSize": 100, + "colorSettings": null, + "hivesMargin": 5 + } + }, + "customWidth": "50", + "name": "serviceBusCompletedTimesQuery", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 1, + "content": { + "json": "### Count of Messages\r\n\r\nThis chart displays:\r\n* The count of active messages in a Queue/Topic\r\n* The count of delivered messages in a Queue/Topic\r\n* The count of dead-lettered messages in a Queue/Topic" + }, + "name": "serviceBusMessageCountText" + }, + { + "type": 10, + "content": { + "chartId": "workbook0f9894a2-554d-406d-b03e-c87fe7b37293", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "resourceType": "microsoft.servicebus/namespaces", + "metricScope": 0, + "resourceIds": [ + "${servicebus_namespace_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.servicebus/namespaces", + "metric": "microsoft.servicebus/namespaces--ActiveMessages", + "aggregation": 4, + "splitBy": null + }, + { + "namespace": "microsoft.servicebus/namespaces", + "metric": "microsoft.servicebus/namespaces--Messages", + "aggregation": 4 + }, + { + "namespace": "microsoft.servicebus/namespaces", + "metric": "microsoft.servicebus/namespaces--DeadletteredMessages", + "aggregation": 4 + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "serviceBusMessagingMetric" + }, + { + "type": 1, + "content": { + "json": "### Throttled Requests\r\n\r\nThis chart displays the number of throttled requests in Service Bus." + }, + "name": "serviceBusThrottledText" + }, + { + "type": 10, + "content": { + "chartId": "workbooke8c22d13-3c2a-4fc8-8722-0180737c45f4", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "blueDark", + "resourceType": "microsoft.servicebus/namespaces", + "metricScope": 0, + "resourceIds": [ + "${servicebus_namespace_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.servicebus/namespaces", + "metric": "microsoft.servicebus/namespaces--ThrottledRequests", + "aggregation": 1, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "serviceBusThrottledMetric" + } + ] + }, + "name": "serviceBusTelemetryGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Cosmos DB Telemetry\r\n\r\nThis section displays telemetry obtained from Cosmos DB operations." + }, + "name": "cosmosDbTitleText" + }, + { + "type": 1, + "content": { + "json": "### Average time for reads from Cosmos DB\r\n\r\nThis chart displays the average time per read requests from Cosmos DB." + }, + "name": "cosmosDbDescriptionText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "dependencies \r\n| where target == \"readDatabase.cargo\" \r\n| summarize Average = avg(duration) by bin(timestamp, 10m)\r\n| render timechart", + "size": 0, + "showAnalytics": true, + "aggregation": 3, + "color": "green", + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "visualization": "areachart" + }, + "name": "latencyOfReadsCosmosDbQuery" + }, + { + "type": 1, + "content": { + "json": "### Number of valid, invalid and operations saved.\r\n\r\nThis chart displays the total number of valid, invalid and operations writes into Cosmos DB." + }, + "name": "cosmosDbOperationsText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "dependencies\r\n| summarize dependencies = count() by name\r\n| where name == \"upsertItem.operations\" or name == \"upsertItem.invalid-cargo\" or name == \"upsertItem.valid-cargo\"", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "visualization": "piechart" + }, + "name": "cosmosDbOperationsQuery" + } + ] + }, + "name": "cosmosDbTelemetryGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Key Vault\r\n\r\n### Key Vault Saturation\r\n\r\nThis metric displays the percentage of saturation Key Vault is having at the moment." + }, + "name": "keyVaultTitleText" + }, + { + "type": 10, + "content": { + "chartId": "workbook1dfaaa15-6964-4398-a9ab-4849c2e07653", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "turquoise", + "resourceType": "microsoft.keyvault/vaults", + "metricScope": 0, + "resourceIds": [ + "${key_vault_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.keyvault/vaults", + "metric": "microsoft.keyvault/vaults--SaturationShoebox", + "aggregation": 4, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "keyVaultSaturationMetric" + }, + { + "type": 1, + "content": { + "json": "### Key Vault Latency\r\n\r\nThis metric displays the latency when executing an operation to KeyVault. The metric displays an average time and it is being estimated in milliseconds." + }, + "name": "keyVaultLatencyText" + }, + { + "type": 10, + "content": { + "chartId": "workbook7000b67b-e89a-4481-99d3-779513f70214", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "turquoise", + "resourceType": "microsoft.keyvault/vaults", + "metricScope": 0, + "resourceIds": [ + "${key_vault_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.keyvault/vaults", + "metric": "microsoft.keyvault/vaults--ServiceApiLatency", + "aggregation": 4, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "keyVaultLatencyMetric" + }, + { + "type": 1, + "content": { + "json": "### Key Vault Results (Count)\r\n\r\nThis metric displays the count of Key Vault API Results." + }, + "name": "keyVaultResultsText" + }, + { + "type": 10, + "content": { + "chartId": "workbook93558986-b83b-4a80-8cbf-1d588fc01058", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "turquoise", + "resourceType": "microsoft.keyvault/vaults", + "metricScope": 0, + "resourceIds": [ + "${key_vault_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.keyvault/vaults", + "metric": "microsoft.keyvault/vaults--ServiceApiResult", + "aggregation": 7, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "keyVaultResultsMetric" + } + ] + }, + "name": "keyVaultTelemetryGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Kubernetes\r\n\r\n### CPU Percentage\r\n\r\nThis chart displays the max count of CPU percentage of the cluster." + }, + "name": "aksTitleText" + }, + { + "type": 10, + "content": { + "chartId": "workbook171b383f-5043-41dd-9154-a1fa92367891", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "pink", + "resourceType": "microsoft.containerservice/managedclusters", + "metricScope": 0, + "resourceIds": [ + "${aks_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.containerservice/managedclusters", + "metric": "microsoft.containerservice/managedclusters-Nodes (PREVIEW)-node_cpu_usage_percentage", + "aggregation": 3, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "aksCpuMetric" + }, + { + "type": 1, + "content": { + "json": "### Requests\r\n\r\nThis chart shows the average inflight requests to the cluster." + }, + "name": "aksRequestsText" + }, + { + "type": 10, + "content": { + "chartId": "workbook2e1c3664-7b39-433d-81b2-863ab1b9b307", + "version": "MetricsItem/2.0", + "size": 0, + "showAnalytics": true, + "chartType": 3, + "color": "pink", + "resourceType": "microsoft.containerservice/managedclusters", + "metricScope": 0, + "resourceIds": [ + "${aks_id}" + ], + "timeContext": { + "durationMs": 3600000 + }, + "metrics": [ + { + "namespace": "microsoft.containerservice/managedclusters", + "metric": "microsoft.containerservice/managedclusters-API Server (PREVIEW)-apiserver_current_inflight_requests", + "aggregation": 4, + "splitBy": null + } + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "name": "aksRequestsMetric" + } + ] + }, + "name": "aksTelemetryGroup" + } + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/system-processing.json b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/system-processing.json new file mode 100644 index 0000000..5990268 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/infrastructure/workbooks/system-processing.json @@ -0,0 +1,492 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "# System Processing Dashboard\n\nThis workbook shows data from system operation across services." + }, + "name": "mainTitleText" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Microservices" + }, + "name": "microservicesTitleText" + }, + { + "type": 1, + "content": { + "json": "### Statistics for endpoints requests\r\n\r\nThis chart displays different measures for time per requests. First measure is the mean per endpoint, second column goes for Median, columns 3, 4 ans 5 are for different percentiles ranges and finally last column displays the total amount of number requests per endpoint." + }, + "name": "endpointsStatisticsText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "requests\r\n| summarize Mean = avg(duration), (Median, p80, p95, p99) = percentiles(duration, 50, 80, 95, 99), Requests = count() by name\r\n| order by Requests desc", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Mean", + "formatter": 8, + "formatOptions": { + "palette": "orange" + } + }, + { + "columnMatch": "Median", + "formatter": 8, + "formatOptions": { + "palette": "yellow" + } + }, + { + "columnMatch": "p80", + "formatter": 8, + "formatOptions": { + "palette": "green" + } + }, + { + "columnMatch": "p95", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "p99", + "formatter": 8, + "formatOptions": { + "palette": "purple" + } + }, + { + "columnMatch": "Requests", + "formatter": 8, + "formatOptions": { + "palette": "pink" + } + } + ] + } + }, + "name": "endpointsRequestsStatisticsQuery" + }, + { + "type": 1, + "content": { + "json": "### Total request to endpoints\r\n\r\nThis chart extracts the last column from previous chart in order to gain more focus on this metric." + }, + "name": "endpointsRequestsText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let dataset=requests\r\n| where client_Type != \"Browser\";\r\n\r\ndataset\r\n| summarize\r\n Count=sum(itemCount),\r\n Average=sum(itemCount * duration) / sum(itemCount) \r\n| project operation_Name=\"Overall\", Count,Average\r\n| union(dataset\r\n | summarize\r\n Count=sum(itemCount),\r\n Average=sum(itemCount * duration) / sum(itemCount) \r\n by operation_Name\r\n | sort by Count desc, Average desc\r\n )", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Average", + "formatter": 8, + "formatOptions": { + "palette": "turquoise" + } + }, + { + "columnMatch": "Count", + "formatter": 8, + "formatOptions": { + "palette": "orange" + } + } + ] + } + }, + "name": "endpointsRequestsQuery" + }, + { + "type": 1, + "content": { + "json": "### Last 100 operations executed\r\n\r\nThis list shows the last 100 of operations executed and their asociated operation ID. You can use this value to request more information from the link after the list that will redirect you to a **Transaction Search** tool." + }, + "name": "operationsText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "requests\r\n| top 100 by timestamp\r\n| distinct name, operation_Id", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ] + }, + "name": "lastOperationsQuery" + }, + { + "type": 1, + "content": { + "json": "You can go and check the **Transaction Search** [here](https://portal.azure.com/#blade/AppInsightsExtension/BladeRedirect/BladeName/searchV1/ResourceId/%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2/BladeInputs/%7B%22tables%22%3A%5B%22availabilityResults%22%2C%22requests%22%2C%22exceptions%22%2C%22pageViews%22%2C%22traces%22%2C%22customEvents%22%2C%22dependencies%22%5D%7D). \r\n\r\nAnd using the list above of the last 100 operation IDs start looking for an specific operation." + }, + "name": "transactionSearchBladeText" + }, + { + "type": 1, + "content": { + "json": "### Additional telemetry\r\n\r\nYou can find in these sections more information that you can use or add to this workbook.\r\n\r\n|Application map|Availability|Failures|Performance|\r\n|---------------|------------|--------|-----------|\r\n|[Link](https://portal.azure.com/#blade/AppInsightsExtension/BladeRedirect/BladeName/applicationMap/ResourceId/%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2/BladeInputs/%7B%22MainResourceId%22%3A%22%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2%22%2C%22TimeContext%22%3A%7B%22durationMs%22%3A3600000%2C%22createdTime%22%3A%222023-03-07T15%3A39%3A08.000Z%22%2C%22isInitialTime%22%3Afalse%2C%22grain%22%3A1%2C%22useDashboardTimeRange%22%3Afalse%7D%2C%22DataModel%22%3A%7B%22exclude4xxError%22%3Atrue%2C%22timeContext%22%3A%7B%22durationMs%22%3A3600000%2C%22createdTime%22%3A%222023-03-07T15%3A39%3A08.000Z%22%2C%22isInitialTime%22%3Afalse%2C%22grain%22%3A1%2C%22useDashboardTimeRange%22%3Afalse%7D%2C%22layoutOption%22%3A%22Organic%22%2C%22nodeContentFilter%22%3A%22%22%7D%7D)|[Link](https://portal.azure.com/#blade/AppInsightsExtension/BladeRedirect/BladeName/availability/ResourceId/%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2/BladeInputs/%7B%22filters%22%3A%5B%5D%2C%22timeContext%22%3A%7B%22durationMs%22%3A86400000%2C%22createdTime%22%3A%222023-03-07T12%3A54%3A05.627Z%22%2C%22endTime%22%3A%222023-03-07T15%3A39%3A00.000Z%22%2C%22isInitialTime%22%3Afalse%2C%22grain%22%3A1%2C%22useDashboardTimeRange%22%3Afalse%7D%2C%22experience%22%3A5%2C%22roleSelectors%22%3A%5B%5D%7D)|[Link](https://portal.azure.com/#blade/AppInsightsExtension/BladeRedirect/BladeName/failures/ResourceId/%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2/BladeInputs/%7B%22filters%22%3A%5B%5D%2C%22timeContext%22%3A%7B%22durationMs%22%3A86400000%2C%22createdTime%22%3A%222023-03-07T12%3A54%3A05.627Z%22%2C%22endTime%22%3A%222023-03-07T12%3A58%3A00.000Z%22%2C%22isInitialTime%22%3Afalse%2C%22grain%22%3A1%2C%22useDashboardTimeRange%22%3Afalse%7D%2C%22selectedOperation%22%3Anull%2C%22experience%22%3A4%2C%22roleSelectors%22%3A%5B%5D%2C%22clientTypeMode%22%3A%22Server%22%7D)|[Link](https://portal.azure.com/#blade/AppInsightsExtension/BladeRedirect/BladeName/performance/ResourceId/%2Fsubscriptions%2F30a83aff-7a8b-4ca3-aa48-ab93268b5a8b%2FresourceGroups%2Frg-dev-tf-amines4%2Fproviders%2FMicrosoft.Insights%2Fcomponents%2Fdev-appi-accl-glc-eastus2/BladeInputs/%7B%22filters%22%3A%5B%5D%2C%22timeContext%22%3A%7B%22durationMs%22%3A86400000%2C%22createdTime%22%3A%222023-03-07T12%3A54%3A05.627Z%22%2C%22endTime%22%3A%222023-03-07T15%3A41%3A00.000Z%22%2C%22isInitialTime%22%3Afalse%2C%22grain%22%3A1%2C%22useDashboardTimeRange%22%3Afalse%7D%2C%22selectedOperation%22%3Anull%2C%22experience%22%3A1%2C%22roleSelectors%22%3A%5B%5D%2C%22clientTypeMode%22%3A%22Server%22%7D)|" + }, + "name": "aditionalTelemetryText" + } + ] + }, + "name": "microservicesTelemetryGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Microservices operations telemetry\r\n\r\nSelect from the following parameters the options to display desired results:\r\nFirst parameter is for a time range and second one is for the service you want to monitor." + }, + "name": "operationsTitleText" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "8f9405b8-1cc0-419f-a465-f35464bb15c0", + "version": "KqlParameterItem/1.0", + "name": "param_time_range", + "label": "Time Range", + "type": 4, + "description": "Select the time range for queries", + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 900000 + }, + { + "durationMs": 1800000 + }, + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + } + ], + "allowCustom": true + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": { + "durationMs": 1800000 + } + }, + { + "id": "5da2ece4-7e2b-4356-a8ce-795bf3e58bd2", + "version": "KqlParameterItem/1.0", + "name": "paramCloudRoleName", + "label": "Cloud Role", + "type": 2, + "query": "dependencies\r\n| distinct cloud_RoleName\r\n| order by cloud_RoleName asc", + "crossComponentResources": [ + "${app_insights_id}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components" + }, + { + "id": "0093df18-0e13-4eac-b50e-1afbc78a7b9c", + "version": "KqlParameterItem/1.0", + "name": "appinsights", + "type": 5, + "description": "Used as a single place to set the app insights resource to query", + "isHiddenWhenLocked": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\"/subscriptions/30a83aff-7a8b-4ca3-aa48-ab93268b5a8b/resourceGroups/rg-dev-tf-amines4/providers/Microsoft.Insights/components/dev-appi-accl-glc-eastus2\"]", + "value": "${app_insights_id}" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "operationsParameters" + }, + { + "type": 1, + "content": { + "json": "### End to end processing time\r\n\r\nThis chart displays the end to end processing time. This is measured in seconds and to be displayed requires the selection of parameters time range and cloud role." + }, + "name": "endToEndProcessingText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let put_name = strcat(\"PUT /cargo/{cargoId\", \"}\"); // TODO - determine how to escape curly braces!\r\nlet cargo_processing_api = requests\r\n| where cloud_RoleName == \"cargo-processing-api\" and (name == \"POST /cargo/\" or name == put_name) and timestamp {param_time_range}\r\n| project-rename ingest_timestamp = timestamp\r\n| project ingest_timestamp, operation_Id\r\n;\r\nlet operation_api_succeeded = requests\r\n| where cloud_RoleName == \"operations-api\" and name == \"ServiceBus.process\" and customDimensions[\"operation-state\"] == \"Succeeded\"\r\n| extend operation_api_completed = timestamp + (duration*1ms)\r\n| project operation_Id, operation_api_completed\r\n;\r\ncargo_processing_api\r\n| join kind=inner operation_api_succeeded on $left.operation_Id == $right.operation_Id\r\n| extend end_to_end_Duration_s = (operation_api_completed - ingest_timestamp) /1s\r\n| summarize avg(end_to_end_Duration_s), max(end_to_end_Duration_s) by bin(ingest_timestamp, {param_time_range:grain})\r\n| order by ingest_timestamp desc\r\n| project ingest_timestamp, avg_end_to_end_Duration_s, max_end_to_end_Duration_s\r\n| render timechart \r\n", + "size": 0, + "aggregation": 3, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "avg_end_to_end_Duration_s", + "label": "Avg duration (s)", + "color": "blue" + }, + { + "seriesName": "max_end_to_end_Duration_s", + "label": "Max duration (s)", + "color": "lightBlue" + } + ] + } + }, + "name": "endToEndProcessingQuery" + }, + { + "type": 1, + "content": { + "json": "### Request count\r\n\r\nThis chart displays the count of ingest of requests. It required the selection of parameters time range and cloud role." + }, + "name": "requestsCountText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let put_name = strcat(\"PUT /cargo/{cargoId\", \"}\"); // TODO - determine how to escape curly braces!\r\nrequests\r\n| where cloud_RoleName == \"cargo-processing-api\" and (name == \"POST /cargo/\" or name == put_name) and timestamp {param_time_range}\r\n| summarize request_count=count() by bin(timestamp, {param_time_range:grain})\r\n| project timestamp, request_count\r\n| render timechart \r\n", + "size": 1, + "showAnalytics": true, + "color": "gray", + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ] + }, + "name": "requestsCountQuery" + }, + { + "type": 1, + "content": { + "json": "### Services processing time \r\n\r\nThis chart displays the processing time in the services. This is measured in seconds and to be displayed requires the selection of parameters time range and cloud role." + }, + "name": "servicesProcessingTimeText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let put_name = strcat(\"PUT /cargo/{cargoId\", \"}\"); // TODO - determine how to escape curly braces!\r\nlet cargo_processing_api = requests\n | where cloud_RoleName == \"cargo-processing-api\" and (name == \"POST /cargo/\" or name == put_name) and timestamp {param_time_range}\n | project-rename durationMs=duration\n | extend duration=durationMs * 1ms\n | project timestamp, cloud_RoleName, cloud_RoleInstance, duration, operation_Id\n;\nlet cargo_processing_validator = requests\n | where cloud_RoleName == \"cargo-processing-validator\" and (name == \"ServiceBus.ProcessMessage\" or name == \"ServiceBusQueue.ProcessMessage\")\n | project-rename durationMs=duration\n | extend duration=durationMs * 1ms\n | project timestamp, cloud_RoleName, cloud_RoleInstance, duration, operation_Id\n;\nlet valid_cargo_manager = requests\n | where cloud_RoleName == \"valid-cargo-manager\" and name == \"ServiceBusTopic.ProcessMessage\"\n | project-rename durationMs=duration\n | extend duration=durationMs * 1ms\n | project timestamp, cloud_RoleName, cloud_RoleInstance, name, duration, operation_Id\n;\nlet invalid_cargo_manager = requests\n | where cloud_RoleName == \"invalid-cargo-manager\" and name == \"ServiceBusTopic.ProcessMessage\"\n | project-rename durationMs=duration\n | extend duration=durationMs * 1ms\n | project timestamp, cloud_RoleName, cloud_RoleInstance, name, duration, operation_Id\n;\ncargo_processing_api\n| join kind=leftouter cargo_processing_validator on $left.operation_Id == $right.operation_Id\n| join kind=leftouter valid_cargo_manager on $left.operation_Id == $right.operation_Id\n| join kind=leftouter invalid_cargo_manager on $left.operation_Id == $right.operation_Id\n| project-rename\n cpa_timestamp=timestamp, cpa_duration=duration, \n cpv_timestamp=timestamp1, cpv_duration=duration1,\n vcm_timestamp=timestamp2, vcm_duration=duration2,\n icm_timestamp=timestamp3, icm_duration=duration3\n| extend\n time_to_cpv=cpv_timestamp - cpa_timestamp,\n time_to_vcm=vcm_timestamp - cpv_timestamp,\n time_to_icm=icm_timestamp - cpv_timestamp\n| extend\n in_cpa_s = cpa_duration / 1s,\n in_cpv_s = cpv_duration / 1s,\n in_vcm_s = vcm_duration / 1s,\n in_icm_s = icm_duration / 1s\n| summarize \n avg(in_cpa_s),\n avg(in_cpv_s),\n avg(in_vcm_s),\n avg(in_icm_s)\n by bin (cpa_timestamp, {param_time_range:grain})\n| order by cpa_timestamp desc\n| render areachart with(kind=stacked)\n", + "size": 0, + "aggregation": 3, + "showAnalytics": true, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ], + "chartSettings": { + "xAxis": "cpa_timestamp", + "seriesLabelSettings": [ + { + "seriesName": "avg_to_cpv_s", + "label": "Average time to cargo-processing_validator", + "color": "redBright" + }, + { + "seriesName": "avg_to_vcm_s", + "color": "green" + }, + { + "seriesName": "avg_to_icm_s", + "color": "lightBlue" + }, + { + "seriesName": "avg_in_cpa_s", + "color": "yellow" + }, + { + "seriesName": "avg_in_cpv_s", + "color": "red" + }, + { + "seriesName": "avg_in_vcm_s", + "color": "greenDark" + }, + { + "seriesName": "avg_in_icm_s", + "color": "blue" + } + ] + } + }, + "name": "servicesProcessingTimeQuery" + }, + { + "type": 1, + "content": { + "json": "### Service dependency\r\n\r\nThis chart displays the service dependency duration. This is measured in seconds and to be displayed requires the selection of parameters time range and cloud role." + }, + "name": "serviceDependencyText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let replace_guid = '[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?';\r\ndependencies\r\n| where cloud_RoleName == \"{paramCloudRoleName}\" and timestamp {param_time_range}\r\n| extend name_pattern = replace_regex(name, replace_guid, \"\")\r\n| extend duration_s = duration /1000\r\n| summarize avg(duration_s) by name_pattern, bin(timestamp, {param_time_range:grain})\r\n| project-reorder timestamp, avg_duration_s , name_pattern\r\n| render areachart with(kind=stacked)", + "size": 0, + "aggregation": 3, + "showAnalytics": true, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ] + }, + "name": "serviceDependencyQuery" + }, + { + "type": 1, + "content": { + "json": "### Breakdown by destination port\r\n\r\nThis chart displays the end to end processing time by destination port. This is measured in seconds and to be displayed requires the selection of parameters time range and cloud role." + }, + "name": "destinationPortBreakdownText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let put_name = strcat(\"PUT /cargo/{cargoId\", \"}\"); // TODO - determine how to escape curly braces!\r\nlet portMap = requests\r\n| where cloud_RoleName == \"cargo-processing-validator\"\r\n| extend destinationPort = customDimensions[\"cargo-destination\"]\r\n| project operation_Id, destinationPort;\r\nlet cargo_processing_api = requests\r\n| where cloud_RoleName == \"cargo-processing-api\" and (name == \"POST /cargo/\" or name == put_name) and timestamp {param_time_range}\r\n| project-rename ingest_timestamp = timestamp\r\n| project ingest_timestamp, operation_Id\r\n;\r\nlet operation_api_succeeded = requests\r\n| where cloud_RoleName == \"operations-api\" and name == \"ServiceBus.process\" and customDimensions[\"operation-state\"] == \"Succeeded\"\r\n| extend operation_api_completed = timestamp + (duration*1ms)\r\n| project operation_Id, operation_api_completed\r\n;\r\ncargo_processing_api\r\n| join kind=inner operation_api_succeeded on $left.operation_Id == $right.operation_Id\r\n| join kind=leftouter portMap on $left.operation_Id == $right.operation_Id\r\n| extend end_to_end_Duration_s = (operation_api_completed - ingest_timestamp) /1s\r\n| extend destinationPort=iif(destinationPort ==\"\", \"\", destinationPort)\r\n| summarize avg(end_to_end_Duration_s) by destinationPort, bin(ingest_timestamp, {param_time_range:grain})\r\n| project ingest_timestamp, avg_end_to_end_Duration_s, destinationPort\r\n| render timechart ", + "size": 0, + "aggregation": 3, + "showAnalytics": true, + "queryType": 0, + "resourceType": "microsoft.insights/components", + "crossComponentResources": [ + "${app_insights_id}" + ] + }, + "name": "destinationPortBreakdownQuery" + }, + { + "type": 1, + "content": { + "json": "### Pod Restarts\r\n\r\nThis chart shows the number of times each service pod has restarted." + }, + "name": "podRestartText" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "KubePodInventory\r\n| where ServiceName == \"{paramCloudRoleName}\"\r\n| summarize numRestarts = sum(PodRestartCount) by ServiceName, bin(TimeGenerated, 1m)\r\n| render timechart", + "size": 0, + "showAnalytics": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "podRestartQuery" + } + ] + }, + "name": "operationsTelemetryGroup" + } + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/run-local.sh b/accelerators/aks-sb-azmonitor-microservices/run-local.sh new file mode 100644 index 0000000..7493e16 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/run-local.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +if [[ ! -f "$script_dir/.env" ]]; then + echo "Please create a .env file (using .env.sample as a starter)" 1>&2 + exit 1 +fi + +source "$script_dir/.env" + +if [[ -z "$USERNAME" ]]; then + echo 'USERNAME not set - ensure you have specifed a value for it in your .env file' 1>&2 + exit 6 +fi + +echo "Starting services locally (Ctrl+C to stop)" +cd "$script_dir/src" +docker compose up diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/Dockerfile new file mode 100644 index 0000000..32bfefa --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/java/.devcontainer/base.Dockerfile + +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster +ARG VARIANT="17-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="false" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/devcontainer.json new file mode 100644 index 0000000..583e6d2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/java +{ + "name": "Java", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Java version: 11, 17 + // Append -bullseye or -buster to pin to an OS version. + // Use the -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "17-bullseye", + // Options + "INSTALL_MAVEN": "true", + "INSTALL_GRADLE": "false", + "NODE_VERSION": "lts/*" + } + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.jdt.ls.lombokSupport.enabled": true + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "vscjava.vscode-java-pack", + "redhat.fabric8-analytics" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.dockerignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.dockerignore new file mode 100644 index 0000000..2ce5e1c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.dockerignore @@ -0,0 +1,2 @@ +.env +helm diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.env.sample b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.env.sample new file mode 100644 index 0000000..cb2518d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.env.sample @@ -0,0 +1,9 @@ +APPLICATIONINSIGHTS_CONNECTION_STRING= +APPLICATIONINSIGHTS_VERSION=3.4.7 + +# Service Bus Information +servicebus_connection_string= +accelerator_queue_name=ingest-cargo + +# Operation API +operations_api_url=http://operations-api:8081/ \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.gitignore new file mode 100644 index 0000000..8977a26 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.gitignore @@ -0,0 +1,3 @@ +target + +.env \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/launch.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/launch.json new file mode 100644 index 0000000..52a4a37 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Launch Current File", + "request": "launch", + "mainClass": "${file}", + "envFile": "${workspaceFolder}/.env" + }, + { + "type": "java", + "name": "Launch Application", + "request": "launch", + "mainClass": "com.microsoft.cse.cargoprocessing.api.Application", + "projectName": "cargoprocessing.api", + "vmArgs": "-javaagent:${workspaceFolder}/target/dependency/applicationinsights-agent-3.4.7.jar", + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/settings.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/tasks.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/tasks.json new file mode 100644 index 0000000..b681057 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "verify", + "type": "shell", + "command": "mvn -B verify", + "group": "build" + }, + { + "label": "test", + "type": "shell", + "command": "mvn -B test", + "group": "test" + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/Dockerfile new file mode 100644 index 0000000..1487559 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/Dockerfile @@ -0,0 +1,27 @@ +FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu as base + + +FROM maven:3.8.5-openjdk-17-slim as build +WORKDIR /src + +RUN mvn -version + +COPY pom.xml . +RUN mvn -B dependency:resolve-plugins dependency:resolve +# RUN mvn -B dependency:go-offline + +COPY . . +RUN mvn package + +RUN ls -al target +RUN ls -al target/dependency + +FROM base as final +COPY applicationinsights.json applicationinsights.json + +ARG JAR_FILE=/src/target/*.jar +ARG DEPENDENCY=/src/target/dependency +COPY --from=build ${DEPENDENCY}/applicationinsights-agent-3.4.7.jar applicationinsights-agent-3.4.7.jar +COPY --from=build ${JAR_FILE} app.jar + +ENTRYPOINT ["java", "-javaagent:applicationinsights-agent-3.4.7.jar" ,"-jar","/app.jar" ] diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/README.md b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/README.md new file mode 100644 index 0000000..f8ec5e6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/README.md @@ -0,0 +1,72 @@ +# Running the service + +## Pre-Requisites + +1. Service Bus [namespace](https://docs.microsoft.com/en-us/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create) with [queue](https://docs.microsoft.com/en-us/cli/azure/servicebus/queue?view=azure-cli-latest#az-servicebus-queue-create) +1. Application Insights [account](https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource#azure-cli-preview) + +## Debugging from VSCode Dev Container + +* Open the project in the dev container. + * Make sure to open in the devcontainer + * Ignore the alerts for Java on the initial load. The alerts move faster than the dev container builds. + * If you see an alert for Lombok asking to reload, please do reload. +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Build the Build task 2 options: + * From the command pallet `Tasks: Run Build Task` + * From the terminal `mvn -B verify` +* Configure debugger to use the "Launch Application" configuration. +* Run the Debugger. +* Post a message to ".../cargo/{GUID VALUE}" that conforms to the [Cargo API](../../api-spec/main.cadl) specification. + +## Docker Container + +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Run `docker compose up` to run the service. +* Post a message to ".../cargo/{GUID VALUE}" that conforms to the [Cargo API](../../api-spec/main.cadl) specification. + +## Samples + +Sample PUT request: + +``` bash +curl --request PUT \ + --url http://localhost:8080/cargo/2dfc711b-7335-4b17-aede-2d67fbf6866f \ + --header 'Content-Type: application/json' \ + --data '{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Seattle" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } +}' +``` + +Sample POST request: + +``` bash +curl --request POST \ + --url http://localhost:8080/cargo/ \ + --header 'Content-Type: application/json' \ + --data '{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } +}' +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/applicationinsights.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/applicationinsights.json new file mode 100644 index 0000000..8efec15 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/applicationinsights.json @@ -0,0 +1,17 @@ +{ + "role": { + "name": "cargo-processing-api" + }, + "instrumentation": { + "logging": { + "level": "INFO" + } + }, + "preview": { + "instrumentation": { + "springIntegration": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/docker-compose.yml new file mode 100644 index 0000000..47fdf30 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + cargo_processing_api: + env_file: + - .env + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/.helmignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/Chart.yaml new file mode 100644 index 0000000..83847e9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: cargo-processing-api +description: cargo-processing-api for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/cargo-processing-api.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/cargo-processing-api.yaml new file mode 100644 index 0000000..c5b1597 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/cargo-processing-api.yaml @@ -0,0 +1,36 @@ + +image: + pullPolicy: Always + tag: "latest" + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + + +# When running one of the deploy-*.sh scripts, an additional env.yaml +# values file is created containing values specific to the deployed environment +# with the following values: +# image: +# repository: + +# keyVault: +# name: +# tenantId: + +# aksKeyVaultSecretProviderIdentityId: diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/NOTES.txt new file mode 100644 index 0000000..0e7f6bf --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "aks-sb-azmonitor-microservices.fullname" . }}' +export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "aks-sb-azmonitor-microservices.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") +echo http://$SERVICE_IP:80 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/_helpers.tpl new file mode 100644 index 0000000..1e34b64 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/deployment.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/deployment.yaml new file mode 100644 index 0000000..feda6c3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/deployment.yaml @@ -0,0 +1,97 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 8 }} + spec: + serviceAccountName: default + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: APPLICATIONINSIGHTS_VERSION + value: 3.4.7 + - name: accelerator_queue_name + value: ingest-cargo + - name: operations_api_url + value: http://operations-api/ + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: cargo-processing-api-secrets + key: AppInsightsConnectionString + - name: servicebus_connection_string + valueFrom: + secretKeyRef: + name: cargo-processing-api-secrets + key: ServiceBusConnectionString + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 10 + + startupProbe: + httpGet: + path: /actuator/health + port: 8080 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "aks-sb-azmonitor-microservices.fullname" . }} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/hpa.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/hpa.yaml new file mode 100644 index 0000000..0a3ca97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/secretProviderClass.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/secretProviderClass.yaml new file mode 100644 index 0000000..983846c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/secretProviderClass.yaml @@ -0,0 +1,41 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: {{ .Values.aksKeyVaultSecretProviderIdentityId }} + keyvaultName: {{ .Values.keyVault.name }} + cloudName: "" + objects: | + array: + - | + objectName: AppInsightsConnectionString + objectType: secret + - | + objectName: ServiceBusConnectionString + objectType: secret + - | + objectName: CosmosDBEndpoint + objectType: secret + - | + objectName: CosmosDBKey + objectType: secret + tenantId: {{ .Values.keyVault.tenantId }} + secretObjects: + - data: + - key: AppInsightsConnectionString + objectName: AppInsightsConnectionString + - key: ServiceBusConnectionString + objectName: ServiceBusConnectionString + - key: CosmosDBEndpoint + objectName: CosmosDBEndpoint + - key: CosmosDBKey + objectName: CosmosDBKey + secretName: cargo-processing-api-secrets + type: Opaque \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/service.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/service.yaml new file mode 100644 index 0000000..af3f13a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 4 }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/tests/test-connection.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5eb4bc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "aks-sb-azmonitor-microservices.fullname" . }}-test-connection" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "aks-sb-azmonitor-microservices.fullname" . }}:80'] + restartPolicy: Never diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw.cmd b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/pom.xml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/pom.xml new file mode 100644 index 0000000..0563da7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/pom.xml @@ -0,0 +1,172 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.1 + + + com.microsoft.cse + cargoprocessing.api + 0.0.1-SNAPSHOT + cargoprocessing-api + Ingestion API for the Service Bus Messaging scenario + + + 17 + 3.12.0 + 5.8.2 + 3.4.7 + 3.4.7 + LATEST + 1.0.71 + 7.13.0 + 2.11.0 + 3.0.0-M5 + 3.3.0 + 4.4.0 + 2.7.3 + true + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-actuator + + + + com.azure + azure-messaging-servicebus + ${servicebus.version} + + + + org.apache.commons + commons-lang3 + ${commons.lang.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + com.networknt + json-schema-validator + ${json.schema.validation.version} + + + + commons-io + commons-io + ${commons.io.version} + test + + + + io.opentelemetry + opentelemetry-api + + + + com.microsoft.azure + applicationinsights-web + ${applicationinsights.web.version} + + + + com.microsoft.azure + applicationinsights-agent + ${applicationinsights.agent.version} + + + + + + + io.opentelemetry + opentelemetry-bom + 1.22.0 + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + com.azure.spring + spring-cloud-azure-dependencies + ${spring.cloud.azure.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin.version} + + + + copy + compile + + copy + + + + + com.microsoft.azure + applicationinsights-agent + ${applicationinsights.agent.version} + applicationinsights-agent-${applicationinsights.agent.version}.jar + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipITs} + + + + + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Application.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Application.java new file mode 100644 index 0000000..dd885dc --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Application.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Exceptions/JsonValidationException.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Exceptions/JsonValidationException.java new file mode 100644 index 0000000..c0074ee --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/Exceptions/JsonValidationException.java @@ -0,0 +1,33 @@ +package com.microsoft.cse.cargoprocessing.api.Exceptions; + +import java.util.Set; +import java.util.stream.Collectors; + +import com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling.ErrorCodes; +import com.networknt.schema.ValidationMessage; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper=false) +public class JsonValidationException extends RuntimeException { + private Set validationMessages; + private String failureCode; + + public JsonValidationException(Throwable cause) { + super(cause); + this.failureCode = ErrorCodes.FAILS_SERIALIZATION; + } + + public JsonValidationException(Set validationMessages) { + super(String.format("Json failed validation with the following errors:%n%n* %s", + validationMessages + .stream() + .map(v -> String.format("%s: {%s} %s", v.getCode(), v.getPath(), v.getMessage())) + .collect(Collectors.joining(String.format("%n* "))))); + + this.validationMessages = validationMessages; + this.failureCode = ErrorCodes.FAILS_SCHEMA_VALIDATION; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/ChaosMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/ChaosMonkey.java new file mode 100644 index 0000000..36c0151 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/ChaosMonkey.java @@ -0,0 +1,15 @@ +package com.microsoft.cse.cargoprocessing.api.chaos; + +import java.util.Map; + +import com.microsoft.cse.cargoprocessing.api.models.Cargo; + +public interface ChaosMonkey { + boolean CanWakeTheMonkey(Cargo cargo); + + void WakeTheMonkey(Map parameters); + + void RattleTheCage(Cargo cargo, Map parameters); + + void RattleTheCage(Cargo cargo); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/BaseMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/BaseMonkey.java new file mode 100644 index 0000000..baa8c87 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/BaseMonkey.java @@ -0,0 +1,41 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +import java.util.Map; + +import com.microsoft.cse.cargoprocessing.api.chaos.ChaosMonkey; +import com.microsoft.cse.cargoprocessing.api.models.Cargo; +import com.microsoft.cse.cargoprocessing.api.models.Port; + +abstract public class BaseMonkey implements ChaosMonkey { + private final String chaosTrigger; + private final String SERVICE_TRIGGER = "cargo-processing-api"; + + public BaseMonkey(String chaosTrigger) { + this.chaosTrigger = chaosTrigger; + } + + @Override + public boolean CanWakeTheMonkey(Cargo cargo) { + Port portInfo = cargo.getPort(); + return portInfo.getSource().equalsIgnoreCase(SERVICE_TRIGGER) && + portInfo.getDestination().equalsIgnoreCase(chaosTrigger); + } + + @SuppressWarnings("unchecked") + protected static T getParm(Map map, String key, T defaultValue) { + return (map.containsKey(key)) ? (T) map.get(key) : defaultValue; + } + + abstract public void WakeTheMonkey(Map parameters); + + @Override + public void RattleTheCage(Cargo cargo, Map parameters) { + if (CanWakeTheMonkey(cargo)) + WakeTheMonkey(parameters); + } + + @Override + public void RattleTheCage(Cargo cargo) { + RattleTheCage(cargo, null); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ChaosMonkeyException.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ChaosMonkeyException.java new file mode 100644 index 0000000..bdc02fa --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ChaosMonkeyException.java @@ -0,0 +1,7 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +public class ChaosMonkeyException extends RuntimeException { + public ChaosMonkeyException(String chaosType) { + super(String.format("%s Chaos Monkey reeking havoc.", chaosType)); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/DependantApiFailureMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/DependantApiFailureMonkey.java new file mode 100644 index 0000000..67d8b31 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/DependantApiFailureMonkey.java @@ -0,0 +1,17 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +import java.util.Map; + +import org.springframework.stereotype.Service; + +@Service +public class DependantApiFailureMonkey extends BaseMonkey { + public DependantApiFailureMonkey() { + super("operations-api-failure"); + } + + @Override + public void WakeTheMonkey(Map parameters) { + throw new ChaosMonkeyException("Dependant Api Failing"); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ProcessKillingMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ProcessKillingMonkey.java new file mode 100644 index 0000000..fa66bb8 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ProcessKillingMonkey.java @@ -0,0 +1,18 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +import java.util.Map; + +import org.springframework.stereotype.Service; + +@Service +public class ProcessKillingMonkey extends BaseMonkey { + public ProcessKillingMonkey() { + super("process-ending"); + } + + @Override + public void WakeTheMonkey(Map parameters) { + // Completely Kill the application + System.exit(-1); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusKillingMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusKillingMonkey.java new file mode 100644 index 0000000..b29517f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusKillingMonkey.java @@ -0,0 +1,22 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.azure.messaging.servicebus.ServiceBusSenderClient; + +@Service +public class ServiceBusKillingMonkey extends BaseMonkey { + public ServiceBusKillingMonkey() { + super("service-bus-failure"); + } + + @Override + public void WakeTheMonkey(Map parameters) { + // Oh, let's just close that sender before trying to use it, what could possibly + // go wrong? + ServiceBusSenderClient sender = getParm(parameters, "sender", null); + sender.close(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusThrollingMonkey.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusThrollingMonkey.java new file mode 100644 index 0000000..5f6ab0c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/chaos/impl/ServiceBusThrollingMonkey.java @@ -0,0 +1,50 @@ +package com.microsoft.cse.cargoprocessing.api.chaos.impl; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.azure.messaging.servicebus.ServiceBusClientBuilder; +import com.azure.messaging.servicebus.ServiceBusMessage; +import com.azure.messaging.servicebus.ServiceBusSenderClient; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@Service +public class ServiceBusThrollingMonkey extends BaseMonkey { + public ServiceBusThrollingMonkey() { + super("service-bus-throttling"); + } + + @Value("${accelerator.queue-name:defaultValue}") + private String queueName; + @Value("${servicebus.connection-string:defaultValue}") + private String connectionString; + + @Override + public void WakeTheMonkey(Map parameters) { + ServiceBusSenderClient sender = new ServiceBusClientBuilder() + .connectionString(connectionString) + .sender() + .queueName(queueName) + .buildClient(); + + ServiceBusMessage message = getParm(parameters, "message", null); + + // Let's slam the service bus with that message ALOT, what could go wrong with + // that? + // TODO: Not able to get this to actually cause the service bus to throttle the + // requests. Need to revisit before calling this done. + Flux.just(1) + .repeat(10000) + .flatMap(i -> Mono.fromCallable(() -> { + sender.sendMessage(message); + return i; + })) + .subscribeOn(Schedulers.boundedElastic(), true) + .subscribe(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoController.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoController.java new file mode 100644 index 0000000..f056ecc --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoController.java @@ -0,0 +1,143 @@ +package com.microsoft.cse.cargoprocessing.api.controllers; + +import org.springframework.web.bind.annotation.RestController; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.cse.cargoprocessing.api.Exceptions.JsonValidationException; +import com.microsoft.cse.cargoprocessing.api.chaos.impl.DependantApiFailureMonkey; +import com.microsoft.cse.cargoprocessing.api.chaos.impl.ProcessKillingMonkey; +import com.microsoft.cse.cargoprocessing.api.models.Cargo; +import com.microsoft.cse.cargoprocessing.api.models.MessageEnvelope; +import com.microsoft.cse.cargoprocessing.api.services.CargoPublisher; +import com.microsoft.cse.cargoprocessing.api.services.OperationPublisher; +import com.microsoft.cse.cargoprocessing.api.services.SchemaValidator; + +import lombok.SneakyThrows; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Timestamp; +import java.util.Map; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +@RequestMapping("cargo") +public class CargoController { + @Autowired + private CargoPublisher publisher; + @Autowired + private SchemaValidator validator; + @Autowired + private OperationPublisher operationPublisher; + @Autowired + private DependantApiFailureMonkey apiFailingMonkey; + @Autowired + private ProcessKillingMonkey processKillingMonkey; + + private static final Logger logger = LoggerFactory.getLogger(CargoController.class); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @PutMapping("/{cargoId}") + public ResponseEntity createCargo(@PathVariable String cargoId, @RequestBody String cargoBody, + @RequestHeader Map headers) { + Cargo cargo = getJsonCargo(cargoBody); + + // Let's see if we need to add a little chaos + processKillingMonkey.RattleTheCage(cargo); + + cargo.setId(cargoId); + logger.info("Cargo body loaded for cargo id: {}", cargoId); + + return processCargo(cargo, getOperationId(headers, cargo)); + } + + private String getOperationId(Map headers, Cargo cargo) { + String key = "operation-id"; + if (headers.containsKey(key)) { + return headers.get(key); + } + // If the client doesn't provide an operation-id, generate a + // deterministic UUID based on the cargo object provided + return generateId(cargo); + } + + @PostMapping("/") + public ResponseEntity createCargo(@RequestBody String cargoBody, @RequestHeader Map headers) { + Cargo cargo = getJsonCargo(cargoBody); + + // Let's see if we need to add a little chaos + processKillingMonkey.RattleTheCage(cargo); + + cargo.setId(generateId(cargo)); + logger.info("Cargo body loaded for cargo id: {}", cargo.getId()); + + // Take note that the cargo object's id has been set at this point, + // so the UUID that is generated for the operation id + // (when the client doesn't provide one) will be + // different then the UUID generated for the cargo object + return processCargo(cargo, getOperationId(headers, cargo)); + } + + @SneakyThrows + private String generateId(Cargo cargo) { + // Get a deterministic UUID based on the cargo object provided + String cargoString = objectMapper.writeValueAsString(cargo); + + return UUID.nameUUIDFromBytes(cargoString.getBytes()).toString(); + } + + private ResponseEntity processCargo(Cargo cargo, String operationId) { + // Let's see if we need to add a little chaos + apiFailingMonkey.RattleTheCage(cargo); + + Boolean isNewOperation = operationPublisher.isNewOperation(operationId).block(); + + // To ensure we don't have duplicate requests in play: + // If the operation was created in the previous call, then we haven't + // received this request before, so we will process it. + if (isNewOperation) { + logger.info("New Cargo request, processing cargo id: {}", cargo.getId()); + cargo.setTimestamp(new Timestamp(System.currentTimeMillis())); + publisher.publishCargo(new MessageEnvelope(cargo, operationId)); + + logger.info("Cargo id {} published", cargo.getId()); + } + + return ResponseEntity.accepted() + .headers(getHeaders(operationId)) + .body(cargo); + } + + private HttpHeaders getHeaders(String operationId) { + HttpHeaders headers = new HttpHeaders(); + headers.add("operation-id", operationId); + return headers; + } + + private Cargo getJsonCargo(String cargo) { + try { + logger.info("Validating cargo schema"); + JsonNode jsonCargo = objectMapper.readTree(cargo); + validator.validate("cargo", jsonCargo); + + return objectMapper.treeToValue(jsonCargo, Cargo.class); + + } catch (JsonProcessingException e) { + throw new JsonValidationException(e); + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/Error.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/Error.java new file mode 100644 index 0000000..49d5c81 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/Error.java @@ -0,0 +1,14 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Error implements Serializable { + private ErrorDetail error; + + public Error(String code, String message, String target, InnerError innerError){ + error = new ErrorDetail(code, message, target, innerError); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorCodes.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorCodes.java new file mode 100644 index 0000000..d6e21be --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorCodes.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +public class ErrorCodes { + private ErrorCodes() { throw new IllegalStateException("Utility class, should not be constructed"); } + + public static final String INVALID_JSON = "InvalidJson"; + public static final String FAILS_SCHEMA_VALIDATION = "InvalidJson-SchemaValidationFailure"; + public static final String FAILS_SERIALIZATION = "InvalidJson-UnableToSerialize"; + + public static final String INTERNAL_SERVER_ERROR = "InternalServerError"; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorDetail.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorDetail.java new file mode 100644 index 0000000..12cc31b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ErrorDetail.java @@ -0,0 +1,20 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class ErrorDetail implements Serializable { + private String code; + private String message; + private String target; + private InnerError innerError; + + public ErrorDetail(String code, String message, String target, InnerError innerError){ + this.code = code; + this.innerError = innerError; + this.target = target; + this.message = message; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ExceptionAdvisor.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ExceptionAdvisor.java new file mode 100644 index 0000000..5d2a2f3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ExceptionAdvisor.java @@ -0,0 +1,52 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import com.microsoft.cse.cargoprocessing.api.Exceptions.JsonValidationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ControllerAdvice +public class ExceptionAdvisor extends ResponseEntityExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvisor.class); + + @ExceptionHandler(JsonValidationException.class) + protected ResponseEntity handleJsonValidationException( + JsonValidationException ex, + WebRequest request) { + logger.error(ex.getMessage(), ex); + + InnerError innerError = ex.getFailureCode() == + ErrorCodes.FAILS_SCHEMA_VALIDATION + ? new ValidationError(ex.getFailureCode(), ex.getMessage(), ex.getValidationMessages()) + : new InnerError(ex.getFailureCode(), ex.getMessage()); + Error error = new Error( + ErrorCodes.INVALID_JSON, + "Invalid Json object, please see inner error for details", + request.getDescription(false), innerError); + + return new ResponseEntity<>(error, new HttpHeaders(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleDefaultExceptions( + Exception ex, + WebRequest request) { + logger.error(ex.getMessage(), ex); + + Error error = new Error(ErrorCodes.INTERNAL_SERVER_ERROR, + "Internal server error. Please see service logs for more information", + request.getDescription(false), null); + + return new ResponseEntity<>(error, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/InnerError.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/InnerError.java new file mode 100644 index 0000000..6459e90 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/InnerError.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class InnerError implements Serializable { + private String code; + private String message; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ValidationError.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ValidationError.java new file mode 100644 index 0000000..b718229 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/controllers/ExceptionHandling/ValidationError.java @@ -0,0 +1,19 @@ +package com.microsoft.cse.cargoprocessing.api.controllers.ExceptionHandling; + +import java.util.Set; + +import com.networknt.schema.ValidationMessage; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper=true) +public class ValidationError extends InnerError { + private Set validationMessages; + + public ValidationError (String code, String message, Set validationMessages) { + super(code, message); + this.validationMessages = validationMessages; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Cargo.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Cargo.java new file mode 100644 index 0000000..8141e7f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Cargo.java @@ -0,0 +1,18 @@ +package com.microsoft.cse.cargoprocessing.api.models; + +import java.io.Serializable; +import java.sql.Timestamp; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +@Data +public class Cargo implements Serializable { + private String id; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Timestamp timestamp; + private Product product; + private Port port; + private DemandDates demandDates; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/DemandDates.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/DemandDates.java new file mode 100644 index 0000000..9ccc1c5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/DemandDates.java @@ -0,0 +1,16 @@ +package com.microsoft.cse.cargoprocessing.api.models; + +import java.io.Serializable; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +@Data +public class DemandDates implements Serializable { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Date start; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Date end; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/MessageEnvelope.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/MessageEnvelope.java new file mode 100644 index 0000000..c0b5b8e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/MessageEnvelope.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.api.models; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class MessageEnvelope { + private Cargo data; + private String operationId; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Port.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Port.java new file mode 100644 index 0000000..6872497 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Port.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.api.models; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Port implements Serializable { + private String source; + private String destination; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Product.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Product.java new file mode 100644 index 0000000..1f851c7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/models/Product.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.api.models; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Product implements Serializable { + private String name; + private int quantity; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/CargoPublisher.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/CargoPublisher.java new file mode 100644 index 0000000..c7d9cfe --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/CargoPublisher.java @@ -0,0 +1,7 @@ +package com.microsoft.cse.cargoprocessing.api.services; + +import com.microsoft.cse.cargoprocessing.api.models.MessageEnvelope; + +public interface CargoPublisher { + void publishCargo(MessageEnvelope envelope); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/OperationPublisher.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/OperationPublisher.java new file mode 100644 index 0000000..f963fd1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/OperationPublisher.java @@ -0,0 +1,7 @@ +package com.microsoft.cse.cargoprocessing.api.services; + +import reactor.core.publisher.Mono; + +public interface OperationPublisher { + Mono isNewOperation(String operationId); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidator.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidator.java new file mode 100644 index 0000000..0e17153 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidator.java @@ -0,0 +1,7 @@ +package com.microsoft.cse.cargoprocessing.api.services; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaValidator { + void validate(String schemaName, JsonNode json); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/CargoPublisher.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/CargoPublisher.java new file mode 100644 index 0000000..9618c75 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/CargoPublisher.java @@ -0,0 +1,58 @@ +package com.microsoft.cse.cargoprocessing.api.services.impl; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.azure.messaging.servicebus.ServiceBusClientBuilder; +import com.azure.messaging.servicebus.ServiceBusMessage; +import com.azure.messaging.servicebus.ServiceBusSenderClient; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.microsoft.cse.cargoprocessing.api.chaos.impl.ServiceBusKillingMonkey; +import com.microsoft.cse.cargoprocessing.api.chaos.impl.ServiceBusThrollingMonkey; +import com.microsoft.cse.cargoprocessing.api.models.MessageEnvelope; + +import lombok.SneakyThrows; + +@Service +public class CargoPublisher implements com.microsoft.cse.cargoprocessing.api.services.CargoPublisher { + + @Autowired + private ServiceBusKillingMonkey killingMonkey; + @Autowired + private ServiceBusThrollingMonkey throttlingMonkey; + + @Value("${accelerator.queue-name:defaultValue}") + private String queueName; + @Value("${servicebus.connection-string:defaultValue}") + private String connectionString; + + private JsonMapper mapper = new JsonMapper(); + + private static final Logger logger = LoggerFactory.getLogger(CargoPublisher.class); + + @SneakyThrows + public void publishCargo(MessageEnvelope envelope) { + ServiceBusSenderClient sender = new ServiceBusClientBuilder() + .connectionString(connectionString) + .sender() + .queueName(queueName) + .buildClient(); + + logger.info("Cargo being published"); + ServiceBusMessage message = new ServiceBusMessage(mapper.writeValueAsBytes(envelope)); + message.addContext(connectionString, message); + + // Maybe add a little chaos + Map chaosParameters = Map.of("sender", sender, "message", message); + killingMonkey.RattleTheCage(envelope.getData(), chaosParameters); + throttlingMonkey.RattleTheCage(envelope.getData(), chaosParameters); + + sender.sendMessage(message); + sender.close(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/OperationPublisher.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/OperationPublisher.java new file mode 100644 index 0000000..344d21d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/OperationPublisher.java @@ -0,0 +1,37 @@ +package com.microsoft.cse.cargoprocessing.api.services.impl; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +@Service +public class OperationPublisher implements com.microsoft.cse.cargoprocessing.api.services.OperationPublisher { + private static final Logger logger = LoggerFactory.getLogger(OperationPublisher.class); + + @Value("${operations.api.url:defaultValue}") + private String operationApiUrl; + + @Override + public Mono isNewOperation(String operationId) { + logger.info("Starting operation {} to {}", operationId, operationApiUrl); + // Return a true when we're debugging the cargo processing only + if (operationApiUrl.equals("debug")) + return Mono.just(true); + return WebClient.create(operationApiUrl) + .put() + .uri("/operations/" + operationId) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toBodilessEntity() + .map(response -> { + return response.getStatusCode() == HttpStatus.CREATED; + }); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/SchemaValidator.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/SchemaValidator.java new file mode 100644 index 0000000..dca6983 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/java/com/microsoft/cse/cargoprocessing/api/services/impl/SchemaValidator.java @@ -0,0 +1,48 @@ +package com.microsoft.cse.cargoprocessing.api.services.impl; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.cse.cargoprocessing.api.Exceptions.JsonValidationException; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; + +@Service +public class SchemaValidator implements com.microsoft.cse.cargoprocessing.api.services.SchemaValidator { + private static final JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); + private static final String JSON_SCHEMAS_PATH = "static/json-schemas/"; + private static final Map schemas = new HashMap<>(); + + public void validate(String schemaName, JsonNode json) { + JsonSchema jsonSchema = getSchema(schemaName); + + Set results = jsonSchema.validate(json); + if (!results.isEmpty()) { + throw new JsonValidationException(results); + } + } + + private JsonSchema getSchema(String schemaName) { + if (schemas.containsKey(schemaName)) { + return schemas.get(schemaName); + } + + StringBuilder sb = new StringBuilder(JSON_SCHEMAS_PATH); + sb.append(schemaName); + sb.append("-schema.json"); + + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream schemaStream = loader.getResourceAsStream(sb.toString()); + JsonSchema schema = schemaFactory.getSchema(schemaStream); + + schemas.put(schemaName, schema); + return schema; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/application.properties b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/application.properties new file mode 100644 index 0000000..d41ac13 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/application.properties @@ -0,0 +1,16 @@ +APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING} +APPLICATIONINSIGHTS_VERSION=${APPLICATIONINSIGHTS_VERSION} + +# Service Bus Information +servicebus.connection-string=${servicebus_connection_string} +accelerator.queue-name=${accelerator_queue_name} + +operations.api.url=${operations_api_url} + +server.port=8080 + +management.endpoints.web.exposure.include=health,info +endpoints.health.sensitive=false +management.endpoint.health.show-details=always + +otel.logs.exporter=logging diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-sample.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-sample.json new file mode 100644 index 0000000..43b8ce6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-sample.json @@ -0,0 +1,14 @@ +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-schema.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-schema.json new file mode 100644 index 0000000..400ee6b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/main/resources/static/json-schemas/cargo-schema.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "http://example.com/example.json", + "type": "object", + "default": {}, + "title": "Cargo Schema", + "required": [ + "product", + "port", + "demandDates" + ], + "properties": { + "product": { + "type": "object", + "default": {}, + "title": "The product in the cargo", + "required": [ + "name", + "quantity" + ], + "properties": { + "name": { + "type": "string", + "default": "", + "title": "The name of the product", + "examples": [ + "Toys" + ] + }, + "quantity": { + "type": "integer", + "default": 0, + "title": "The quantity of the product to be shipped", + "examples": [ + 100 + ] + } + }, + "examples": [{ + "name": "Toys", + "quantity": 100 + }] + }, + "port": { + "type": "object", + "default": {}, + "title": "The ports the cargo will use", + "required": [ + "source", + "destination" + ], + "properties": { + "source": { + "type": "string", + "default": "", + "title": "The source port for the cargo", + "examples": [ + "New York City" + ] + }, + "destination": { + "type": "string", + "default": "", + "title": "The destination port for the cargo", + "examples": [ + "Tacoma" + ] + } + }, + "examples": [{ + "source": "New York City", + "destination": "Tacoma" + }] + }, + "demandDates": { + "type": "object", + "default": {}, + "title": "The the dates the cargo is expected to be transported", + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "type": "string", + "default": "", + "format": "date-time", + "title": "The start date", + "examples": [ + "2022-06-24T00:00:00.000Z" + ] + }, + "end": { + "type": "string", + "default": "", + "format": "date-time", + "title": "The end date", + "examples": [ + "2022-06-30T00:00:00.000Z" + ] + } + }, + "examples": [{ + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + }] + } + }, + "examples": [{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } + }] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/ApplicationTestsIT.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/ApplicationTestsIT.java new file mode 100644 index 0000000..8977bfa --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/ApplicationTestsIT.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTestsIT { + + @Test + void contextLoads() { + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerIT.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerIT.java new file mode 100644 index 0000000..5206588 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerIT.java @@ -0,0 +1,56 @@ +package com.microsoft.cse.cargoprocessing.api.controllers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.cse.cargoprocessing.api.services.CargoPublisher; + + +@SpringBootTest +@AutoConfigureMockMvc +public class CargoControllerIT { + @Autowired + private MockMvc mockMvc; + + @MockBean + private CargoPublisher publisher; + + @ParameterizedTest + @MethodSource("cargoToPublish") + void publishesCargo(String cargoSource, ResultMatcher matcher) throws JsonProcessingException, Exception{ + String cargo = IOUtils.toString( + this.getClass().getResourceAsStream(cargoSource), + "UTF-8"); + + String id = UUID.randomUUID().toString(); + + mockMvc.perform( + put("/cargo/{id}", id) + .contentType("application/json") + .content(cargo)) + .andExpect(matcher); + } + + private static Stream cargoToPublish() { + return Stream.of( + Arguments.of("/cargo-test-objects/basic-cargo.json", status().isOk()), + Arguments.of("/cargo-test-objects/invalid-cargo-object.json", status().isBadRequest()), + Arguments.of("/cargo-test-objects/invalid-syntax.json", status().isBadRequest()) + ); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerTest.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerTest.java new file mode 100644 index 0000000..d3ef805 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/controllers/CargoControllerTest.java @@ -0,0 +1,81 @@ +package com.microsoft.cse.cargoprocessing.api.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.UUID; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + + +import com.microsoft.cse.cargoprocessing.api.models.Cargo; +import com.microsoft.cse.cargoprocessing.api.services.CargoPublisher; +import com.microsoft.cse.cargoprocessing.api.services.SchemaValidator; + +import reactor.core.publisher.Mono; + +import com.microsoft.cse.cargoprocessing.api.services.OperationPublisher; + +@SpringBootTest +@TestInstance(Lifecycle.PER_CLASS) +public class CargoControllerTest { + @MockBean + private CargoPublisher publisher; + + @MockBean + private SchemaValidator validator; + + @MockBean + private OperationPublisher operationPublisher; + + @Autowired + private CargoController controller; + + @BeforeEach + void configureMocks() { + when(operationPublisher.isNewOperation(any())).thenReturn(Mono.just(true)); + } + + @Test + void PutValidCargoHydratesAdditionContent() throws IOException { + String cargo = IOUtils.toString( + this.getClass().getResourceAsStream("/cargo-test-objects/basic-cargo.json"), + "UTF-8"); + + String id = UUID.randomUUID().toString(); + + Map headers = new HashMap<>(); + + Cargo results = controller.createCargo(id, cargo, headers).getBody(); + + assertEquals(id, results.getId()); + assertNotNull(results.getTimestamp()); + } + + @Test + void PostValidCargoHydratesAdditionContent() throws IOException { + String cargo = IOUtils.toString( + this.getClass().getResourceAsStream("/cargo-test-objects/basic-cargo.json"), + "UTF-8"); + + Map headers = new HashMap<>(); + + Cargo results = controller.createCargo(cargo, headers).getBody(); + + assertFalse(results.getId().isBlank()); + assertNotNull(results.getTimestamp()); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidatorTest.java b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidatorTest.java new file mode 100644 index 0000000..6014b39 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/java/com/microsoft/cse/cargoprocessing/api/services/SchemaValidatorTest.java @@ -0,0 +1,44 @@ +package com.microsoft.cse.cargoprocessing.api.services; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.cse.cargoprocessing.api.Exceptions.JsonValidationException; + +@SpringBootTest +public class SchemaValidatorTest { + @Autowired + private SchemaValidator validator; + + @Test + void UsingValidJsonAndSchema() throws IOException { + String cargo = IOUtils.toString( + this.getClass().getResourceAsStream("/cargo-test-objects/basic-cargo.json"), + "UTF-8"); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonCargo = mapper.readTree(cargo); + assertDoesNotThrow(() -> validator.validate("cargo", jsonCargo)); + + } + + @Test + void UsingInvalidSchema() throws IOException { + String cargo = IOUtils.toString( + this.getClass().getResourceAsStream("/cargo-test-objects/invalid-cargo-object.json"), + "UTF-8"); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonCargo = mapper.readTree(cargo); + + assertThrows(JsonValidationException.class, () -> validator.validate("cargo", jsonCargo)); + + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/application.properties b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/application.properties new file mode 100644 index 0000000..1382467 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/application.properties @@ -0,0 +1,7 @@ +APPLICATIONINSIGHTS_CONNECTION_STRING=$APPLICATIONINSIGHTS_CONNECTION_STRING +APPLICATIONINSIGHTS_VERSION=$APPLICATIONINSIGHTS_VERSION + +# Service Bus Information +spring.jms.servicebus.connection-string=Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test-publisher;SharedAccessKey=not-a-real-value +spring.jms.servicebus.pricing-tier=basic +accelerator.queue-name=ingestor-queue \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/basic-cargo.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/basic-cargo.json new file mode 100644 index 0000000..43b8ce6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/basic-cargo.json @@ -0,0 +1,14 @@ +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-cargo-object.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-cargo-object.json new file mode 100644 index 0000000..074f012 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-cargo-object.json @@ -0,0 +1,14 @@ +{ + "product": { + "named": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-syntax.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-syntax.json new file mode 100644 index 0000000..02c07e9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-api/src/test/resources/cargo-test-objects/invalid-syntax.json @@ -0,0 +1,13 @@ +{ + "product": { + "name": "Toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000Z", + "end": "2022-06-30T00:00:00.000Z" + } diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/Dockerfile new file mode 100644 index 0000000..d821fc9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16-bullseye + +RUN apt-get update && apt-get install -y \ + lsb-release \ + curl \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e576cbc --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "name": "cargo-processing-validator", + "build": { + "dockerfile": "Dockerfile" + }, + "extensions": [ + "dbaeumer.vscode-eslint", + "ms-vscode.vscode-typescript-tslint-plugin" + ], + "postCreateCommand": "npm install", + "remoteUser": "node" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore new file mode 100644 index 0000000..b2d59d1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore copy b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore copy new file mode 100644 index 0000000..2ce5e1c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.dockerignore copy @@ -0,0 +1,2 @@ +.env +helm diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.env.sample b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.env.sample new file mode 100644 index 0000000..3456a5a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.env.sample @@ -0,0 +1,6 @@ +SERVICE_BUS_CONNECTION_STRING="" +QUEUE_NAME="" +TOPIC_NAME="" +OPERATION_QUEUE_NAME="" +MAX_MESSAGE_DEQUEUE_COUNT=10 +APPLICATIONINSIGHTS_CONNECTION_STRING="" diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.gitignore new file mode 100644 index 0000000..c7fb7a4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/dist +.env \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.vscode/launch.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.vscode/launch.json new file mode 100644 index 0000000..d25553b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/src/index.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/Dockerfile new file mode 100644 index 0000000..53072b4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/cbl-mariner/base/nodejs:16 + +WORKDIR /usr/src/app + +COPY package*.json ./ +COPY tsconfig.json ./ +COPY . . + +RUN npm install +RUN npm run build + +CMD ["npm","run","start"] \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/README.md b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/README.md new file mode 100644 index 0000000..2bf75f8 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/README.md @@ -0,0 +1,48 @@ +# Running the service + +## Pre-Requisites + +1. Service Bus [namespace](https://docs.microsoft.com/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create) with [queue](https://docs.microsoft.com/cli/azure/servicebus/queue?view=azure-cli-latest#az-servicebus-queue-create) and [topic](https://docs.microsoft.com/cli/azure/servicebus/topic?view=azure-cli-latest#az-servicebus-topic-create) +1. Application Insights [account](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource#azure-cli-preview) + +## Running from VSCode Dev Container + +* Open the project in the dev container. +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Transpile typescript - `npm run build`. If you want to have transpilation happen automatically when you save code changes, run `npm run watch-build` in a separate terminal window. +* Run javascript - `npm run start`. If you want to have the application restart on code changes, run `npm run watch` instead. +* Post a message to the the Service Bus Queue similar to the [sample message](#sample-message) + +## Docker Container + +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Run `docker compose up` to run the service. +* Post a message to the the Service Bus Queue similar to the [sample message](#sample-message) + +## Sample Message + +When posting a message to the service bus, also ensure a traceparent custom property has been set to a value that conforms pattern defined [by w3c](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format). + +For example a value like: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 + +```json +{ + "operationId": "56bb0b4c-5c8c-4361-9771-25f997cf651b", + "data": { + "timestamp": "2022-07-29T00:00:00.000Z", + "id": "f725da7e-af18-4bf2-85f9-610504cc3d40", + "product": { + "name": "minerals", + "quantity": 2 + }, + "port": { + "source": "Boston", + "destination": "Charlotte" + }, + "demandDates": { + "start": "2022-07-28T00:00:00.000Z", + "end": "2022-07-29T00:00:00.000Z" + } + } +} +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/docker-compose.yml new file mode 100644 index 0000000..e0c74ef --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.9" + +services: + cargo-processing-validator: + env_file: + - .env + build: + context: . + dockerfile: Dockerfile \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/.helmignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/Chart.yaml new file mode 100644 index 0000000..2051ee4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: cargo-processing-validator +description: cargo-processing-validator for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/cargo-processing-validator.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/cargo-processing-validator.yaml new file mode 100644 index 0000000..6389407 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/cargo-processing-validator.yaml @@ -0,0 +1,34 @@ +image: + pullPolicy: Always + tag: "latest" + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + +# When running one of the deploy-*.sh scripts, an additional env.yaml +# values file is created containing values specific to the deployed environment +# with the following values: +# image: +# repository: + +# keyVault: +# name: +# tenantId: + +# aksKeyVaultSecretProviderIdentityId: diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/NOTES.txt new file mode 100644 index 0000000..dd366ce --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aks-sb-azmonitor-microservices.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/_helpers.tpl new file mode 100644 index 0000000..0172cb3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aks-sb-azmonitor-microservices.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aks-sb-azmonitor-microservices.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/deployment.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/deployment.yaml new file mode 100644 index 0000000..0a2cb45 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/deployment.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: default + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: QUEUE_NAME + value: ingest-cargo + - name: TOPIC_NAME + value: validated-cargo + - name: OPERATION_QUEUE_NAME + value: operation-state + - name: MAX_MESSAGE_DEQUEUE_COUNT + value: "100" + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: cargo-processing-validator-secrets + key: AppInsightsConnectionString + - name: SERVICE_BUS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: cargo-processing-validator-secrets + key: ServiceBusConnectionString + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "aks-sb-azmonitor-microservices.fullname" . }} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/hpa.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/hpa.yaml new file mode 100644 index 0000000..0a3ca97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/secretProviderClass.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/secretProviderClass.yaml new file mode 100644 index 0000000..279d7c7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/secretProviderClass.yaml @@ -0,0 +1,31 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: {{ .Values.aksKeyVaultSecretProviderIdentityId }} + keyvaultName: {{ .Values.keyVault.name }} + cloudName: "" + objects: | + array: + - | + objectName: AppInsightsConnectionString + objectType: secret + - | + objectName: ServiceBusConnectionString + objectType: secret + tenantId: {{ .Values.keyVault.tenantId }} + secretObjects: + - data: + - key: AppInsightsConnectionString + objectName: AppInsightsConnectionString + - key: ServiceBusConnectionString + objectName: ServiceBusConnectionString + secretName: cargo-processing-validator-secrets + type: Opaque \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/service.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/service.yaml new file mode 100644 index 0000000..af3f13a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 4 }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/tests/test-connection.yaml b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5eb4bc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "aks-sb-azmonitor-microservices.fullname" . }}-test-connection" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "aks-sb-azmonitor-microservices.fullname" . }}:80'] + restartPolicy: Never diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package-lock.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package-lock.json new file mode 100644 index 0000000..1d4e41e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package-lock.json @@ -0,0 +1,4588 @@ +{ + "name": "cargo-processing-validator", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cargo-processing-validator", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@azure/service-bus": "^7.5.1", + "ajv": "^8.11.0", + "applicationinsights": "^2.3.3", + "dotenv": "^16.0.1", + "luxon": "^2.4.0", + "typescript": "^4.7.4" + }, + "devDependencies": { + "@types/chai": "^4.3.1", + "@types/luxon": "^2.3.2", + "@types/mocha": "^9.1.1", + "chai": "^4.3.6", + "mocha": "^10.0.0", + "nodemon": "^2.0.20", + "ts-node": "^10.8.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-amqp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-3.1.0.tgz", + "integrity": "sha512-TyI0WFNrVb0EkRg36UwdcqR/7n9YpcEw64O4xVrgzMAlXIciVZpabl05C/Q0iUvLkItmez2XSVDduncjr02oGw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/logger": "^1.0.0", + "buffer": "^6.0.0", + "events": "^3.0.0", + "jssha": "^3.1.0", + "process": "^0.11.10", + "rhea": "^2.0.3", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0", + "url": "^0.11.0", + "util": "^0.12.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-asynciterator-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", + "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz", + "integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-http": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.2.tgz", + "integrity": "sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.3.0.tgz", + "integrity": "sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz", + "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/service-bus": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.5.1.tgz", + "integrity": "sha512-fIbI5aJDzN2HctcS+3i7rXY3L5jWG7/SuXOq0cMpwy+aH5aHOZVoxX8eeSnUnr2jUpUflU1wJ9yiYZ/sXqZvqw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^3.1.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-http": "^2.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "@types/long": "^4.0.1", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^4.0.0", + "process": "^0.11.10", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz", + "integrity": "sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", + "integrity": "sha512-YveTnGNsFFixTKJz09Oi4zYkiLT5af3WpZDu4aIUM7xX+2bHAkOJayFTVQd6zB8kkWPpbua4Ha6Ql00grdLlJQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.8.0.tgz", + "integrity": "sha512-6SDjwBML4Am0AQmy7z1j6HGrWDgeK8awBRUvl1PGw6HayViMk4QpnUXvv4HTHisecgVBy43NE/cstWprm8tIfw==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.8.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.4.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.8.0.tgz", + "integrity": "sha512-KSyMH6Jvss/PFDy16z5qkCK0ERlpyqixb1xwb73wLMvVq+j7i89lobDjw3JkpCcd1Ws0J6jAI4fw28Zufj2ssg==", + "dependencies": { + "@opentelemetry/core": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.4.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.8.0.tgz", + "integrity": "sha512-iH41m0UTddnCKJzZx3M85vlhKzRcmT48pUeBbnzsGrq4nIay1oWVHKM5nhB5r8qRDGvd/n7f/YLCXClxwM0tvA==", + "dependencies": { + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.4.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.8.0.tgz", + "integrity": "sha512-TYh1MRcm4JnvpqtqOwT9WYaBYY4KERHdToxs/suDTLviGRsQkIjS5yYROTYTSJQUnYLOn/TuOh5GoMwfLSU+Ew==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", + "dev": true + }, + "node_modules/@types/is-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", + "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/luxon": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz", + "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", + "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/applicationinsights": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.3.3.tgz", + "integrity": "sha512-Q4o6gexNhzukgmzzWYzXLa2gdJ6DhM+c35tw0lRNNjc/qldWxGHVxV65DMRYrQIp4vetLdCK7Pyd/dmEsGO4qA==", + "dependencies": { + "@azure/core-http": "^2.2.3", + "@opentelemetry/api": "^1.0.4", + "@opentelemetry/core": "^1.0.1", + "@opentelemetry/sdk-trace-base": "^1.0.1", + "@opentelemetry/semantic-conventions": "^1.0.1", + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "1.1.0", + "diagnostic-channel-publishers": "1.0.5" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "applicationinsights-native-metrics": "*" + }, + "peerDependenciesMeta": { + "applicationinsights-native-metrics": { + "optional": true + } + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "dependencies": { + "stack-chain": "^1.3.7" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3" + } + }, + "node_modules/async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "dependencies": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "engines": { + "node": "<=0.11.8 || >0.11.10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "dependencies": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "dependencies": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diagnostic-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", + "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "dependencies": { + "semver": "^5.3.0" + } + }, + "node_modules/diagnostic-channel-publishers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", + "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "peerDependencies": { + "diagnostic-channel": "*" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "dependencies": { + "shimmer": "^1.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "engines": { + "node": "*" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/luxon": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz", + "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rhea": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-2.0.8.tgz", + "integrity": "sha512-IgwlP4D2lzinBSll5f35tAWa30dGCZhG9Ujd1DiaB7MUGegIjAaLzqATCw3ha+h9oq9mXcitqayBbNIXYdvtFg==", + "dependencies": { + "debug": "0.8.0 - 3.5.0" + } + }, + "node_modules/rhea-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-2.1.0.tgz", + "integrity": "sha512-CRMwdJ/o4oO/xKcvAwAsd0AHy5fVvSlqso7AadRmaaLGzAzc9LCoW7FOFnucI8THasVmOeCnv5c/fH/n7FcNaA==", + "dependencies": { + "debug": "^3.1.0", + "rhea": "^2.0.3", + "tslib": "^2.2.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-node": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-amqp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-3.1.0.tgz", + "integrity": "sha512-TyI0WFNrVb0EkRg36UwdcqR/7n9YpcEw64O4xVrgzMAlXIciVZpabl05C/Q0iUvLkItmez2XSVDduncjr02oGw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/logger": "^1.0.0", + "buffer": "^6.0.0", + "events": "^3.0.0", + "jssha": "^3.1.0", + "process": "^0.11.10", + "rhea": "^2.0.3", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0", + "url": "^0.11.0", + "util": "^0.12.1" + } + }, + "@azure/core-asynciterator-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", + "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==" + }, + "@azure/core-auth": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz", + "integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-http": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.2.tgz", + "integrity": "sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + } + }, + "@azure/core-paging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.3.0.tgz", + "integrity": "sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "requires": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + } + }, + "@azure/core-util": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz", + "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/logger": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/service-bus": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.5.1.tgz", + "integrity": "sha512-fIbI5aJDzN2HctcS+3i7rXY3L5jWG7/SuXOq0cMpwy+aH5aHOZVoxX8eeSnUnr2jUpUflU1wJ9yiYZ/sXqZvqw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^3.1.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-http": "^2.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "@types/long": "^4.0.1", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^4.0.0", + "process": "^0.11.10", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0" + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz", + "integrity": "sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@opentelemetry/api": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", + "integrity": "sha512-YveTnGNsFFixTKJz09Oi4zYkiLT5af3WpZDu4aIUM7xX+2bHAkOJayFTVQd6zB8kkWPpbua4Ha6Ql00grdLlJQ==" + }, + "@opentelemetry/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.8.0.tgz", + "integrity": "sha512-6SDjwBML4Am0AQmy7z1j6HGrWDgeK8awBRUvl1PGw6HayViMk4QpnUXvv4HTHisecgVBy43NE/cstWprm8tIfw==", + "requires": { + "@opentelemetry/semantic-conventions": "1.8.0" + } + }, + "@opentelemetry/resources": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.8.0.tgz", + "integrity": "sha512-KSyMH6Jvss/PFDy16z5qkCK0ERlpyqixb1xwb73wLMvVq+j7i89lobDjw3JkpCcd1Ws0J6jAI4fw28Zufj2ssg==", + "requires": { + "@opentelemetry/core": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.8.0.tgz", + "integrity": "sha512-iH41m0UTddnCKJzZx3M85vlhKzRcmT48pUeBbnzsGrq4nIay1oWVHKM5nhB5r8qRDGvd/n7f/YLCXClxwM0tvA==", + "requires": { + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.8.0.tgz", + "integrity": "sha512-TYh1MRcm4JnvpqtqOwT9WYaBYY4KERHdToxs/suDTLviGRsQkIjS5yYROTYTSJQUnYLOn/TuOh5GoMwfLSU+Ew==" + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@types/chai": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", + "dev": true + }, + "@types/is-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", + "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", + "requires": { + "@types/node": "*" + } + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/luxon": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz", + "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", + "dev": true + }, + "@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", + "dev": true + }, + "@types/node": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", + "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" + }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "requires": { + "@types/node": "*" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "applicationinsights": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.3.3.tgz", + "integrity": "sha512-Q4o6gexNhzukgmzzWYzXLa2gdJ6DhM+c35tw0lRNNjc/qldWxGHVxV65DMRYrQIp4vetLdCK7Pyd/dmEsGO4qA==", + "requires": { + "@azure/core-http": "^2.2.3", + "@opentelemetry/api": "^1.0.4", + "@opentelemetry/core": "^1.0.1", + "@opentelemetry/sdk-trace-base": "^1.0.1", + "@opentelemetry/semantic-conventions": "^1.0.1", + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "1.1.0", + "diagnostic-channel-publishers": "1.0.5" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "diagnostic-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", + "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "requires": { + "semver": "^5.3.0" + } + }, + "diagnostic-channel-publishers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", + "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "requires": {} + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==" + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "luxon": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz", + "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "rhea": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-2.0.8.tgz", + "integrity": "sha512-IgwlP4D2lzinBSll5f35tAWa30dGCZhG9Ujd1DiaB7MUGegIjAaLzqATCw3ha+h9oq9mXcitqayBbNIXYdvtFg==", + "requires": { + "debug": "0.8.0 - 3.5.0" + } + }, + "rhea-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-2.1.0.tgz", + "integrity": "sha512-CRMwdJ/o4oO/xKcvAwAsd0AHy5fVvSlqso7AadRmaaLGzAzc9LCoW7FOFnucI8THasVmOeCnv5c/fH/n7FcNaA==", + "requires": { + "debug": "^3.1.0", + "rhea": "^2.0.3", + "tslib": "^2.2.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "ts-node": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + } + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package.json new file mode 100644 index 0000000..ca40c2d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/package.json @@ -0,0 +1,32 @@ +{ + "name": "cargo-processing-validator", + "version": "1.0.0", + "description": "node based microservice", + "main": "dist/index.js", + "scripts": { + "build": "tsc -p .", + "start": "node ./dist/index.js", + "watch": "nodemon ./dist/index.js", + "watch-build": "tsc -w -p .", + "test": "mocha -r ts-node/register src/tests/**/*.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@azure/service-bus": "^7.5.1", + "ajv": "^8.11.0", + "applicationinsights": "^2.3.3", + "dotenv": "^16.0.1", + "luxon": "^2.4.0", + "typescript": "^4.7.4" + }, + "devDependencies": { + "@types/chai": "^4.3.1", + "@types/luxon": "^2.3.2", + "@types/mocha": "^9.1.1", + "chai": "^4.3.6", + "mocha": "^10.0.0", + "nodemon": "^2.0.20", + "ts-node": "^10.8.1" + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ChaosMonkey.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ChaosMonkey.ts new file mode 100644 index 0000000..963d90d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ChaosMonkey.ts @@ -0,0 +1,29 @@ +import { ServiceBusReceivedMessage } from '@azure/service-bus'; +import { Cargo } from '../models/Cargo'; +import { ServiceBusReceiverWithTelemetry } from '../services/ServiceBusWithTelemetry'; + +export abstract class ChaosMonkey { + chaosTrigger: string; + serviceTrigger: string = "cargo-processing-validator"; + private queueReceiver: ServiceBusReceiverWithTelemetry; + + constructor(chaosTrigger: string, queueReceiver: ServiceBusReceiverWithTelemetry) { + this.chaosTrigger = chaosTrigger; + this.queueReceiver = queueReceiver; + } + + canWakeTheMonkey(cargo: Cargo): boolean { + return cargo.port.source == this.serviceTrigger && cargo.port.destination == this.chaosTrigger; + } + + async rattleTheCage(message: ServiceBusReceivedMessage, cargo: Cargo, parameters?: Map): Promise { + if (this.canWakeTheMonkey(cargo)) { + // Need to make sure we complete the message, otherwise the chaos will not end until the + // message has been dequeued the maximum amount of times + await this.queueReceiver.completeMessage(message); + await this.wakeTheMonkey(parameters); + } + } + + abstract wakeTheMonkey(parameters?: Map): Promise; +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ProcessEndingMonkey.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ProcessEndingMonkey.ts new file mode 100644 index 0000000..c0bcc32 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ProcessEndingMonkey.ts @@ -0,0 +1,12 @@ +import { ServiceBusReceiverWithTelemetry } from "../services/ServiceBusWithTelemetry"; +import { ChaosMonkey } from "./ChaosMonkey"; + +export class ProcessEndingMonkey extends ChaosMonkey { + constructor(queueReceiver: ServiceBusReceiverWithTelemetry) { + super("process-ending", queueReceiver); + } + + wakeTheMonkey(parameters?: Map): Promise { + process.exit(); + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ServiceBusKillingMonkey.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ServiceBusKillingMonkey.ts new file mode 100644 index 0000000..5f0b646 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/chaos/ServiceBusKillingMonkey.ts @@ -0,0 +1,14 @@ +import { ServiceBusSender } from '@azure/service-bus'; +import { ServiceBusReceiverWithTelemetry } from '../services/ServiceBusWithTelemetry'; +import { ChaosMonkey } from "./ChaosMonkey"; + +export class ServiceBusKillingMonkey extends ChaosMonkey { + constructor(queueReceiver: ServiceBusReceiverWithTelemetry) { + super("service-bus-failure", queueReceiver); + } + + async wakeTheMonkey(parameters: Map): Promise { + const sender = parameters.get("sender") as ServiceBusSender; + await sender.close(); + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/index.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/index.ts new file mode 100644 index 0000000..075eb4e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/index.ts @@ -0,0 +1,98 @@ +import { config } from 'dotenv'; +import { CargoValidationService } from './services/CargoValidationService'; +import * as appInsights from 'applicationinsights'; +import { CUSTOM_PROPERTY_CARGO_DESTINATION, CUSTOM_PROPERTY_CARGO_ID, CUSTOM_PROPERTY_CARGO_OPERATION_ID, CUSTOM_PROPERTY_CARGO_VALID, ServiceBusProcessingService } from './services/ServiceBusProcessingService'; +import { CUSTOM_PROPERTY_REQUEST_PARENT_ID } from './services/ServiceBusWithTelemetry'; + +// load environment variables +config(); + + +// start application insights and allow console.log to export logs +// see configuration details at https://docs.microsoft.com/en-us/azure/azure-monitor/app/nodejs#sdk-configuration +appInsights + .setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) + .setAutoDependencyCorrelation(true) + .setAutoCollectRequests(true) + // .setAutoCollectPerformance(true, true) // uncomment to send performance metrics (note this will increase the amount of data sent to Azure Monitor) + // .setSendLiveMetrics(true) + .setAutoCollectExceptions(true) + .setAutoCollectDependencies(true) + .setAutoCollectConsole(true, true) + .setUseDiskRetryCaching(true) + .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C) + .start(); + + + +const client = appInsights.defaultClient; +client.context.tags[client.context.keys.cloudRole] = 'cargo-processing-validator'; + +// Automatically add tags for operationID/ParentID if set in correlation context +client.addTelemetryProcessor((envelope, contextObjects) => { + if (envelope?.data?.baseData?.properties) { + const azNamespace = envelope.data.baseData.properties["az.namespace"]; + if (azNamespace === "Microsoft.ServiceBus") { + // This is a telemetry item that was auto-generated by the Azure SDK + // Return false to suppress it as we are replacing with our own telemetry + // which contains additional metadata + return false; + // If you wish to suppress individual message types use conditions such as: + // envelope?.data?.baseData?.name === "Azure.ServiceBus.message" + } + } + + + // If we have the cargo-operation-id in the correlation context, add it to the telemetry + const customProperties = contextObjects?.correlationContext?.customProperties; + const operation = contextObjects?.correlationContext?.operation; + const envelopeProperties = envelope.data?.baseData?.properties; + const telemetryType = envelope.data?.baseType; + + if (envelopeProperties && customProperties?.getProperty(CUSTOM_PROPERTY_CARGO_ID)) { + envelopeProperties["cargo-id"] = customProperties.getProperty(CUSTOM_PROPERTY_CARGO_ID); + } + if (envelopeProperties && customProperties?.getProperty(CUSTOM_PROPERTY_CARGO_OPERATION_ID)) { + envelopeProperties["cargo-operation-id"] = customProperties.getProperty(CUSTOM_PROPERTY_CARGO_OPERATION_ID); + } + if (envelopeProperties && customProperties?.getProperty(CUSTOM_PROPERTY_CARGO_VALID)) { + envelopeProperties["cargo-valid"] = customProperties.getProperty(CUSTOM_PROPERTY_CARGO_VALID); + } + if (envelopeProperties && customProperties?.getProperty(CUSTOM_PROPERTY_CARGO_DESTINATION)) { + envelopeProperties["cargo-destination"] = customProperties.getProperty(CUSTOM_PROPERTY_CARGO_DESTINATION); + } + // ensure dependencies are children of the ServiceBus.ProcessMessage request + if (telemetryType && telemetryType == 'RemoteDependencyData' && operation && customProperties?.getProperty(CUSTOM_PROPERTY_REQUEST_PARENT_ID)) { + operation.parentId = customProperties?.getProperty(CUSTOM_PROPERTY_REQUEST_PARENT_ID); + envelope.tags['ai.operation.parentId'] = customProperties?.getProperty(CUSTOM_PROPERTY_REQUEST_PARENT_ID); + } + + return true; +}); + +const cargoValidationService: CargoValidationService = + new CargoValidationService(client); + +const serviceBusProcessingService: ServiceBusProcessingService = + new ServiceBusProcessingService( + process.env.SERVICE_BUS_CONNECTION_STRING as string, + process.env.QUEUE_NAME as string, + process.env.TOPIC_NAME as string, + process.env.OPERATION_QUEUE_NAME as string, + parseInt(process.env.MAX_MESSAGE_DEQUEUE_COUNT as string), + cargoValidationService, + client + ); + +(async function () { + try { + console.log("Queue processing starting..."); + await serviceBusProcessingService.startProcessingQueueMessages(); + console.log("Queue processing started"); + } catch (e: any) { + // throwing the error causes the container to self-destruct + // exceptions and other telemetry events from the original request are not logged to application insights + // code to log an explicit exception here would be: client.trackException({ exception: e, severity: SeverityLevel.Error }); + throw e; + } +})(); diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/Cargo.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/Cargo.ts new file mode 100644 index 0000000..fe3581f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/Cargo.ts @@ -0,0 +1,16 @@ +export interface Cargo { + timestamp: Date; + id: string; + product: { + name: string; + quantity: number; + }; + port: { + source: string; + destination: string; + }; + demandDates: { + start: Date; + end: Date; + }; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/MessageEnvelope.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/MessageEnvelope.ts new file mode 100644 index 0000000..398ac9d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/MessageEnvelope.ts @@ -0,0 +1,6 @@ +import { Cargo } from './Cargo'; + +export interface MessageEnvelope { + operationId: string; + data: Cargo; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/ValidatedCargo.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/ValidatedCargo.ts new file mode 100644 index 0000000..8faafea --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/models/ValidatedCargo.ts @@ -0,0 +1,6 @@ +import { Cargo } from './Cargo'; + +export interface ValidatedCargo extends Cargo { + valid: boolean; + errorMessage: string | null; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/schemas/cargo-envelope-schema.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/schemas/cargo-envelope-schema.json new file mode 100644 index 0000000..f42216a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/schemas/cargo-envelope-schema.json @@ -0,0 +1,186 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "messageEnvelope", + "required": [ + "operationId", + "data" + ], + "properties": { + "operationId": { + "title": "The operationId Schema", + "type": "string", + "default": "", + "examples": [ + "f725da7e-af18-4bf2-85f9-610504cc3d40" + ] + }, + "data": { + "title": "The data Schema", + "type": "object", + "default": {}, + "required": [ + "timestamp", + "id", + "product", + "port", + "demandDates" + ], + "properties": { + "timestamp": { + "title": "The timestamp Schema", + "type": "string", + "default": "", + "examples": [ + "2022-07-29T00:00:00.000Z" + ] + }, + "id": { + "title": "The id Schema", + "type": "string", + "default": "", + "examples": [ + "f725da7e-af18-4bf2-85f9-610504cc3d40" + ] + }, + "product": { + "title": "The product Schema", + "type": "object", + "default": {}, + "required": [ + "name", + "quantity" + ], + "properties": { + "name": { + "title": "The name Schema", + "type": "string", + "default": "", + "examples": [ + "minerals" + ] + }, + "quantity": { + "title": "The quantity Schema", + "type": "integer", + "default": 0, + "examples": [ + 2 + ] + } + }, + "examples": [ + { + "name": "minerals", + "quantity": 2 + } + ] + }, + "port": { + "title": "The port Schema", + "type": "object", + "default": {}, + "required": [ + "source", + "destination" + ], + "properties": { + "source": { + "title": "The source Schema", + "type": "string", + "default": "", + "examples": [ + "Boston" + ] + }, + "destination": { + "title": "The destination Schema", + "type": "string", + "default": "", + "examples": [ + "Charlotte" + ] + } + }, + "examples": [ + { + "source": "Boston", + "destination": "Charlotte" + } + ] + }, + "demandDates": { + "title": "The demandDates Schema", + "type": "object", + "default": {}, + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "title": "The start Schema", + "type": "string", + "default": "", + "examples": [ + "2022-07-28T00:00:00.000Z" + ] + }, + "end": { + "title": "The end Schema", + "type": "string", + "default": "", + "examples": [ + "2022-07-29T00:00:00.000Z" + ] + } + }, + "examples": [ + { + "start": "2022-07-28T00:00:00.000Z", + "end": "2022-07-29T00:00:00.000Z" + } + ] + } + }, + "examples": [ + { + "timestamp": "2022-07-29T00:00:00.000Z", + "id": "56bb0b4c-5c8c-4361-9771-25f997cf651b", + "product": { + "name": "minerals", + "quantity": 2 + }, + "port": { + "source": "Boston", + "destination": "Charlotte" + }, + "demandDates": { + "start": "2022-07-28T00:00:00.000Z", + "end": "2022-07-29T00:00:00.000Z" + } + } + ] + } + }, + "examples": [ + { + "operationId": "f725da7e-af18-4bf2-85f9-610504cc3d40", + "data": { + "timestamp": "2022-07-29T00:00:00.000Z", + "id": "f725da7e-af18-4bf2-85f9-610504cc3d40", + "product": { + "name": "minerals", + "quantity": 2 + }, + "port": { + "source": "Boston", + "destination": "Charlotte" + }, + "demandDates": { + "start": "2022-07-28T00:00:00.000Z", + "end": "2022-07-29T00:00:00.000Z" + } + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoSchemaValidation.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoSchemaValidation.ts new file mode 100644 index 0000000..8618d31 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoSchemaValidation.ts @@ -0,0 +1,25 @@ +import Ajv from 'ajv'; +import * as cargoEnvelopeSchema from '../schemas/cargo-envelope-schema.json'; + +export class CargoSchemaValidation { + private ajv: Ajv; + private validator; + + constructor() { + this.ajv = new Ajv(); + this.validator = this.ajv.compile(cargoEnvelopeSchema); + } + + validate(cargo: string) { + console.log('Validating cargo schema'); + const results = this.validator(cargo); + if (results) { + return { isValid: true }; + } else { + return { + isValid: false, + message: JSON.stringify(this.validator.errors), + }; + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoValidationService.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoValidationService.ts new file mode 100644 index 0000000..aed9a13 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/CargoValidationService.ts @@ -0,0 +1,149 @@ +import { Cargo } from '../models/Cargo'; +import { ValidatedCargo } from '../models/ValidatedCargo'; +import { DateTime, Duration } from 'luxon'; +import { TelemetryService } from './TelemetryService'; +import * as appInsights from 'applicationinsights'; + +export class CargoValidationService { + private telemetryClient: appInsights.TelemetryClient; + + constructor(telemetryClient : appInsights.TelemetryClient) { + this.telemetryClient = telemetryClient; + } + async validateCargo(cargo: Cargo): Promise { + console.log('Validating cargo properties'); + + let isValid: boolean = true; + let errorMessage: string | null = null; + + // start, end, and timestamp are epoch times on the incoming message + const start: DateTime = DateTime.fromJSDate( + new Date(cargo.demandDates.start) + ); + const end: DateTime = DateTime.fromJSDate(new Date(cargo.demandDates.end)); + const now: DateTime = DateTime.now(); + const diffBetweenStartAndNow: Duration = start.diff(now, 'days'); + const diffBetweenStartAndEnd: Duration = end.diff(start, 'days'); + + // validate start and end are in future + if (start <= now || end <= now) { + isValid = false; + errorMessage = 'Start and end dates must be in future.'; + } + + // validate start is not more than 60 days in future + if (diffBetweenStartAndNow.days > 60) { + isValid = false; + errorMessage = 'Start date cannot be more than 60 days in future.'; + } + + // validate range is not greater than 30 days + if (diffBetweenStartAndEnd.days > 30) { + isValid = false; + errorMessage = 'Range between start and end dates cannot exceed 30 days.'; + } + + // validate end date is after start date + if (diffBetweenStartAndEnd.days < 0) { + isValid = false; + errorMessage = 'End date must be after start date.'; + } + + // validate destination port + const destinationPortOk = await this.checkDestinationPort(cargo.port.destination); + if (!destinationPortOk) { + isValid = false; + errorMessage = 'Rejected by destination port.'; + } + + console.log(`Valid - ${isValid}, Error message - ${errorMessage}`); + const validatedMessageBody: ValidatedCargo = { + ...cargo, + valid: isValid, + errorMessage, + }; + return validatedMessageBody; + } + + private async executeDependency( + dependency: () => Promise, + dependencyTarget: string, + dependencyTypeName: string, + properties?: { [key: string]: any; } + ): Promise { + const dependencyId: string = TelemetryService.generateOpenTelemetryDependencyId(); + const dependencyStart: bigint = process.hrtime.bigint(); + + const dependencyName = `${dependencyTypeName} ${dependencyTarget}` + + try { + // Make the dependency call + const result = await (dependency()); + + // track dependencies in application insights, ensure they are properly parented + const dependencyEnd: bigint = process.hrtime.bigint(); + this.telemetryClient.trackDependency({ + target: dependencyTarget, + name: dependencyName, + data: '', + duration: TelemetryService.returnElapsedMillisecondsSinceStart( + dependencyStart, + dependencyEnd + ), + resultCode: 200, + success: true, + dependencyTypeName, + id: dependencyId, + properties, + }); + + return result; + } catch (error) { + const dependencyEnd: bigint = process.hrtime.bigint(); + + // track dependencies in application insights, ensure they are properly parented + this.telemetryClient.trackDependency({ + target: dependencyTarget, + name: dependencyName, + data: '', + duration: TelemetryService.returnElapsedMillisecondsSinceStart( + dependencyStart, + dependencyEnd + ), + resultCode: 500, + success: false, + dependencyTypeName, + id: dependencyId, + properties, + }); + + throw error; + } + } + + private async checkDestinationPort(name: string) : Promise { + const _internal = async (name : string) : Promise => { + // This method is used to mock out calling an HTTP service at the destination port + // The intent is to show how telemetry can be used to track variations in behavior + switch (name) { + case "slow-port": + // Simulate an issue with the port response times by adding a delay + await this.sleep(2000) + return true + + default: + await this.sleep(100) + return true; + } + } + return await this.executeDependency( + () => _internal(name), + name, + "destination-port-check", + ) + } + + private sleep(time: number) { + return new Promise(resolve => setTimeout(resolve, time)); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusProcessingService.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusProcessingService.ts new file mode 100644 index 0000000..5b1867d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusProcessingService.ts @@ -0,0 +1,192 @@ +import { Cargo } from '../models/Cargo'; +import { MessageEnvelope } from '../models/MessageEnvelope'; +import { ProcessEndingMonkey } from '../chaos/ProcessEndingMonkey'; +import { ServiceBusKillingMonkey } from '../chaos/ServiceBusKillingMonkey'; +import { + OperationOptionsBase, + ProcessErrorArgs, + ServiceBusClient, + ServiceBusMessage, + ServiceBusReceivedMessage, +} from '@azure/service-bus'; +import { CargoValidationService } from './CargoValidationService'; +import { CargoSchemaValidation } from './CargoSchemaValidation'; + +import { ServiceBusSenderWithTelemetry, ServiceBusReceiverWithTelemetry } from './ServiceBusWithTelemetry'; +import * as appInsights from 'applicationinsights'; + +export const CUSTOM_PROPERTY_CARGO_ID = 'cargo-id'; +export const CUSTOM_PROPERTY_CARGO_OPERATION_ID = 'cargo-operation-id'; +export const CUSTOM_PROPERTY_CARGO_VALID = 'cargo-valid'; +export const CUSTOM_PROPERTY_CARGO_DESTINATION = 'cargo-destination'; + +export class ServiceBusProcessingService { + private connectionString: string; + private queueName: string; + private topicName: string; + private operationQueueName: string; + private maxMessageDequeueCount: number; + private serviceBusClient: ServiceBusClient; + private cargoValidationService: CargoValidationService; + private telemetryClient: appInsights.TelemetryClient; + private cargoSchemaValidator: CargoSchemaValidation; + + private queueReceiver: ServiceBusReceiverWithTelemetry; + private operationStateSender: ServiceBusSenderWithTelemetry; + private topicSender: ServiceBusSenderWithTelemetry; + private processKillingMonkey: ProcessEndingMonkey; + private serviceBusKillingMonkey: ServiceBusKillingMonkey; + + constructor( + connectionString: string, + queueName: string, + topicName: string, + operationQueueName: string, + maxMessageDequeueCount: number, + cargoValidationService: CargoValidationService, + telemetryClient: appInsights.TelemetryClient + ) { + this.connectionString = connectionString; + this.queueName = queueName; + this.topicName = topicName; + this.operationQueueName = operationQueueName; + this.maxMessageDequeueCount = maxMessageDequeueCount; + this.cargoValidationService = cargoValidationService; + this.telemetryClient = telemetryClient; + this.serviceBusClient = new ServiceBusClient(this.connectionString); + + this.queueReceiver = new ServiceBusReceiverWithTelemetry( + this.serviceBusClient.createReceiver(this.queueName), + this.telemetryClient); + this.operationStateSender = new ServiceBusSenderWithTelemetry( + this.serviceBusClient.createSender(this.operationQueueName), + this.telemetryClient); + this.topicSender = new ServiceBusSenderWithTelemetry( + this.serviceBusClient.createSender(this.topicName), + this.telemetryClient); + + this.cargoSchemaValidator = new CargoSchemaValidation(); + + this.processKillingMonkey = new ProcessEndingMonkey(this.queueReceiver); + this.serviceBusKillingMonkey = new ServiceBusKillingMonkey(this.queueReceiver); + } + + startProcessingQueueMessages(): { close(): Promise } { + const response = this.queueReceiver.subscribe({ + processMessage: this.processMessageFromQueue.bind(this), + processError: async (args: ProcessErrorArgs) => { + console.log(args); // Write to console. The receiver already tracks the exception. + // exit the process and allow scheduler to restart + process.exit(1); + } + }, { + autoCompleteMessages: false, + maxConcurrentCalls: this.maxMessageDequeueCount, + }); + return response; + } + + private async processMessageFromQueue(message: ServiceBusReceivedMessage) { + // validate message schema + const validSchema = this.cargoSchemaValidator.validate( + message.body + ); + + if (!validSchema.isValid) { + console.log('Dead lettering message'); + await this.queueReceiver.deadLetterMessage( + message, + { + deadLetterReason: 'Invalid message structure', + deadLetterErrorDescription: validSchema.message!, + } + ); + // Can't update operation state if we can't be sure the message + // structure actually has an operationId, no try catch needed + } else { + await this.processValidCargoMessage(message); + } + } + + private async processValidCargoMessage(message: ServiceBusReceivedMessage) { + const messageEnvelope = message.body as MessageEnvelope; + const cargo: Cargo = messageEnvelope.data; + // Let's add a little chaos + await this.processKillingMonkey.rattleTheCage(message, cargo); + // Set the operation ID on the context for the telemetry processor to include in telemetry items + const correlationContext = appInsights.getCorrelationContext(); + if (correlationContext?.customProperties) { + correlationContext.customProperties.setProperty(CUSTOM_PROPERTY_CARGO_ID, cargo.id); + correlationContext.customProperties.setProperty(CUSTOM_PROPERTY_CARGO_OPERATION_ID, messageEnvelope.operationId); + } + const sendOptions: OperationOptionsBase = { + tracingOptions: { + spanOptions: { + attributes: { + [CUSTOM_PROPERTY_CARGO_ID]: cargo.id, + [CUSTOM_PROPERTY_CARGO_OPERATION_ID]: messageEnvelope.operationId + } + } + } + } + try { + // validate cargo object in message + const validatedCargo = await this.cargoValidationService.validateCargo(cargo); + const validatedMessage: ServiceBusMessage = { + body: { + operationId: messageEnvelope.operationId, + data: validatedCargo, + }, + }; + if (correlationContext?.customProperties) { + correlationContext.customProperties.setProperty(CUSTOM_PROPERTY_CARGO_VALID, validatedCargo.valid.toString()); + const destination = validatedCargo.port.destination.replaceAll(",", ";"); // can't have ',' in props + correlationContext.customProperties.setProperty(CUSTOM_PROPERTY_CARGO_DESTINATION, destination); + } + sendOptions.tracingOptions!.spanOptions!.attributes!["cargo-valid"] = validatedCargo.valid; + + // add valid property to message so it can be properly filtered + // add telemetry properties so the cargo manager services can tie child operations to dependency below + validatedMessage.applicationProperties = { + valid: validatedCargo.valid, + }; + + // send validated cargo with additional properties to service bus topic + console.log(`Sending message to ${this.topicName} topic (cargo ID: ${cargo.id}, opid:${messageEnvelope.operationId})`); + + // let's add a little chaos + await this.serviceBusKillingMonkey.rattleTheCage(message, validatedCargo, new Map([["sender", this.topicSender]])); + + await this.topicSender.sendMessages(validatedMessage, sendOptions); + + // send message to operations queue + console.log(`Sending message to ${this.operationQueueName} queue (cargo ID: ${cargo.id})`); + const operationStateMessage = { + body: { + operationId: messageEnvelope.operationId, + state: 'CargoValidated', + }, + }; + await this.operationStateSender.sendMessages(operationStateMessage, sendOptions); + + // complete original message + await this.queueReceiver.completeMessage(message); + } catch (e: any) { + // catching the exception to attempt to update the operation state to failed + const errorMessage = (e as Error).message; + + const operationStateMessage = { + body: { + operationId: messageEnvelope.operationId, + state: 'Failed', + error: errorMessage, + }, + }; + await this.operationStateSender.sendMessages(operationStateMessage, sendOptions); + + // make sure we still self destruct for the exception + throw e; + } + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusWithTelemetry.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusWithTelemetry.ts new file mode 100644 index 0000000..6169a02 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/ServiceBusWithTelemetry.ts @@ -0,0 +1,300 @@ +import { ServiceBusSender, ServiceBusMessage, OperationOptionsBase, DeadLetterOptions, GetMessageIteratorOptions, MessageHandlers, PeekMessagesOptions, ReceiveMessagesOptions, ServiceBusReceivedMessage, ServiceBusReceiver, SubscribeOptions } from '@azure/service-bus'; +import { TelemetryService } from './TelemetryService'; +import * as appInsights from 'applicationinsights'; + +export const CUSTOM_PROPERTY_REQUEST_PARENT_ID= 'parent-request-id'; + +function generateServiceBusDependency( + telemetryClient: appInsights.TelemetryClient, + dependencyId: string, + duration: number, + serviceBusEntity: string, + serviceBusAction: string, + success: boolean +): void { + telemetryClient.trackDependency({ + target: serviceBusEntity, + name: `${serviceBusEntity} ${serviceBusAction}`, + data: '', + duration: duration, + resultCode: 200, + success: success, + dependencyTypeName: `Azure Service Bus`, + id: dependencyId, + properties: { + "message_bus.destination": serviceBusEntity, + "ai.operation.name": serviceBusAction, + }, + }); +} +function retrieveParentContextOrGenerateNew(message: ServiceBusReceivedMessage): { operationId: string, operationParentId: string } { + return retrieveParentContext(message) ?? TelemetryService.generateNewContext(); +} +function retrieveParentContext(message: ServiceBusReceivedMessage): { operationId: string, operationParentId: string } | null { + // pull trace parent set automatically in cargo-processing-api + const traceParent: any = message.applicationProperties?.traceparent; + if (!traceParent) { + return null + } + // syntax is --- + const parts = traceParent.split('-'); + return { + operationId: parts[1], + operationParentId: parts[2], + }; +} + +export class ServiceBusSenderWithTelemetry { + // Wraps a ServiceBusSender and adds telemetry + // Note that this is a simple example and doesn't support all the features of ServiceBusSender: + // - it only supports the sendMessages function + // - it only supports ServiceBusMessage types (not AmqpAnnotatedMessage etc) + + private sender: ServiceBusSender; + private telemetryClient: appInsights.TelemetryClient; + constructor( + sender: ServiceBusSender, + telemetryClient: appInsights.TelemetryClient, + ) { + this.sender = sender; + this.telemetryClient = telemetryClient; + } + + public get entityPath(): string { + return this.sender.entityPath; + } + public get isClosed(): boolean { + return this.sender.isClosed; + } + async sendMessages(messages: ServiceBusMessage | ServiceBusMessage[], options?: OperationOptionsBase | undefined): Promise { + const correlationContext = appInsights.getCorrelationContext(); + const dependencyId: string = TelemetryService.generateOpenTelemetryDependencyId(); + const dependencyStart: bigint = process.hrtime.bigint(); + + if (!Array.isArray(messages)) { + messages = [messages]; + } + const messagesWithTelemetry: ServiceBusMessage[] = messages.map((message: ServiceBusMessage) => { + const messageForTopic: ServiceBusMessage = { + ...message, + applicationProperties: { + ...message.applicationProperties, + 'Diagnostic-Id': `00-${correlationContext.operation.id}-${dependencyId}-01` + } + }; + return messageForTopic; + }); + + await this.sender.sendMessages(messagesWithTelemetry, options); + + const dependencyEnd: bigint = process.hrtime.bigint(); + + // track dependencies in application insights, ensure they are properly parented + generateServiceBusDependency( + this.telemetryClient, + dependencyId, + TelemetryService.returnElapsedMillisecondsSinceStart( + dependencyStart, + dependencyEnd + ), + this.entityPath, + 'SendMessage', + true + ); + } + async close(): Promise { + await this.sender.close(); + } +} + +export class ServiceBusReceiverWithTelemetry implements ServiceBusReceiver { + // Wraps a ServiceBusReceiver and adds telemetry + // Note that this is a simple example and doesn't support all the features of ServiceBusReceiver: + + private receiver: ServiceBusReceiver; + private telemetryClient: appInsights.TelemetryClient; + constructor( + receiver: ServiceBusReceiver, + telemetryClient: appInsights.TelemetryClient, + ) { + this.receiver = receiver; + this.telemetryClient = telemetryClient; + } + + public get entityPath(): string { + return this.receiver.entityPath; + } + public get receiveMode(): 'peekLock' | 'receiveAndDelete' { + return this.receiver.receiveMode; + } + public get isClosed(): boolean { + return this.receiver.isClosed; + } + private wrapHandler(handler: MessageHandlers): MessageHandlers { + // wrap the user's handler so that we can add telemetry for message processing + return { + processMessage: async (message: ServiceBusReceivedMessage) => { + // track time so telemetry operation duration can be calculated + const requestStart: bigint = process.hrtime.bigint(); + + // pull trace and span ids from traceparent so that telemetry can be correlated back + // to the original API request + const { operationId, operationParentId } = retrieveParentContextOrGenerateNew(message); + const requestId: string = TelemetryService.generateOpenTelemetryRequestId(); + + // wrap the processing in a correlation context so that any telemetry is associated with it + // and can be updated in the telemetry processor + const spanContext = { + traceId: operationId, + spanId: operationParentId, + traceFlags: 1 + } + const correlationContext = appInsights.startOperation(spanContext, "ServiceBus.ProcessMessage") ?? undefined; + correlationContext?.customProperties.setProperty(CUSTOM_PROPERTY_REQUEST_PARENT_ID, requestId) + await appInsights.wrapWithCorrelationContext(async () => { + let success = false; + try { + // invoke handler's processMessage function + await handler.processMessage(message); + success = true; + } catch (error: any) { + // track exception + this.telemetryClient.trackException({ + exception: error, + properties: { + "message_bus.destination": this.receiver.entityPath, + "message_bus.delivery_count": (message.deliveryCount ?? -1).toString(), + }, + tagOverrides: { + "ai.operation.id": operationId, + "ai.operation.parentId": operationParentId, + } + }) + + // track failure request in application insights, ensure parent is set to dependency from inbound message + const requestEnd: bigint = process.hrtime.bigint(); + this.telemetryClient.trackRequest({ + name: 'ServiceBus.ProcessMessage', + url: `sb://${this.receiver.entityPath}`, + duration: TelemetryService.returnElapsedMillisecondsSinceStart( + requestStart, + requestEnd + ), + resultCode: 500, + // unsuccessful requests cause exceptions and self destruction, in which case the request isn't logged at all + // no need to handle sending unsuccessful requests + success: false, + id: requestId, + properties: { + "message_bus.destination": this.receiver.entityPath, + "message_bus.delivery_count": (message.deliveryCount ?? -1).toString(), + }, + tagOverrides: { + "ai.operation.id": operationId, + "ai.operation.parentId": operationParentId, + } + }); + + // rethrow so that Service Bus subscribe sees the exception and calls processError + throw error; + } + + // track successful request in application insights, ensure parent is set to dependency from inbound message + const requestEnd: bigint = process.hrtime.bigint(); + this.telemetryClient.trackRequest({ + name: 'ServiceBus.ProcessMessage', + url: `sb://${this.receiver.entityPath}`, + duration: TelemetryService.returnElapsedMillisecondsSinceStart( + requestStart, + requestEnd + ), + resultCode: 200, + // unsuccessful requests cause exceptions and self destruction, in which case the request isn't logged at all + // no need to handle sending unsuccessful requests + success: true, + id: requestId, + properties: { + "message_bus.destination": this.receiver.entityPath, + "message_bus.delivery_count": (message.deliveryCount ?? -1).toString(), + }, + tagOverrides: { + "ai.operation.id": operationId, + "ai.operation.parentId": operationParentId, + } + }); + }, correlationContext)(); + }, + processError: handler.processError, + }; + } + subscribe(handlers: MessageHandlers, options?: SubscribeOptions | undefined): { close(): Promise; } { + return this.receiver.subscribe(this.wrapHandler(handlers), options); + } + getMessageIterator(options?: GetMessageIteratorOptions | undefined): AsyncIterableIterator { + throw new Error('Method not implemented.'); + } + receiveMessages(maxMessageCount: number, options?: ReceiveMessagesOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + receiveDeferredMessages(sequenceNumbers: Long | Long[], options?: OperationOptionsBase | undefined): Promise { + throw new Error('Method not implemented.'); + } + peekMessages(maxMessageCount: number, options?: PeekMessagesOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + async close(): Promise { + await this.receiver.close(); + } + async completeMessage(message: ServiceBusReceivedMessage): Promise { + const queueCompleteDependencyId: string = TelemetryService.generateOpenTelemetryDependencyId(); + + const queueCompleteDependencyStart: bigint = process.hrtime.bigint(); + await this.receiver.completeMessage(message); + const queueCompleteDependencyEnd: bigint = process.hrtime.bigint(); + + generateServiceBusDependency( + this.telemetryClient, + queueCompleteDependencyId, + TelemetryService.returnElapsedMillisecondsSinceStart( + queueCompleteDependencyStart, + queueCompleteDependencyEnd + ), + this.entityPath, + 'CompleteMessage', + true + ); + } + abandonMessage(message: ServiceBusReceivedMessage, propertiesToModify?: { [key: string]: any; } | undefined): Promise { + throw new Error('Method not implemented.'); + } + deferMessage(message: ServiceBusReceivedMessage, propertiesToModify?: { [key: string]: any; } | undefined): Promise { + throw new Error('Method not implemented.'); + } + async deadLetterMessage(message: ServiceBusReceivedMessage, options?: (DeadLetterOptions & { [key: string]: any; }) | undefined): Promise { + // generate id for dependency + const dependencyId: string = TelemetryService.generateOpenTelemetryDependencyId(); + + // deadletter invalid message structures + const dependencyStart: bigint = process.hrtime.bigint(); + await this.receiver.deadLetterMessage(message, options); + const dependencyEnd: bigint = process.hrtime.bigint(); + + // track dependency in application insights, ensure it is properly parented + generateServiceBusDependency( + this.telemetryClient, + dependencyId, + TelemetryService.returnElapsedMillisecondsSinceStart( + dependencyStart, + dependencyEnd + ), + this.entityPath, + 'DeadLetterMessage', + true + ); + } + renewMessageLock(message: ServiceBusReceivedMessage): Promise { + throw new Error('Method not implemented.'); + } +} + + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/TelemetryService.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/TelemetryService.ts new file mode 100644 index 0000000..09162e5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/services/TelemetryService.ts @@ -0,0 +1,32 @@ +export class TelemetryService { + public static generateNewContext(): { operationId: string, operationParentId: string } { + return { + operationId: TelemetryService.generateOpenTelemetryRequestId(), + operationParentId: TelemetryService.generateOpenTelemetryDependencyId(), + }; + } + + public static generateOpenTelemetryId(length: number): string { + // must satisfy regex for ids - https://github.com/open-telemetry/opentelemetry-js/blob/0f178d1e2e9b3aed81789820944452c153543198/api/src/trace/spancontext-utils.ts#L22 + const chars: string = 'abcdef1234567890'; + const randomArray: string[] = Array.from( + { length: length }, + () => chars[Math.floor(Math.random() * chars.length)] + ); + return randomArray.join(''); + } + + public static generateOpenTelemetryRequestId(): string { + return TelemetryService.generateOpenTelemetryId(32); + } + + public static generateOpenTelemetryDependencyId(): string { + return TelemetryService.generateOpenTelemetryId(16); + } + + public static returnElapsedMillisecondsSinceStart(start: bigint, end: bigint): number { + const elapsedNanoSeconds: number = Number(end - start); + const elapsedMilliseconds = elapsedNanoSeconds / 1000000; + return elapsedMilliseconds; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/tests/CargoValidationServiceTests.ts b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/tests/CargoValidationServiceTests.ts new file mode 100644 index 0000000..6841e70 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/src/tests/CargoValidationServiceTests.ts @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import { CargoValidationService } from '../services/CargoValidationService'; +import { Cargo } from '../models/Cargo'; +import 'mocha'; +import { ValidatedCargo } from '../models/ValidatedCargo'; + +describe('validation tests', () => { + let cargo: Cargo; + + beforeEach(() => { + cargo = { + timestamp: new Date(), + id: '', + product: { + name: 'product', + quantity: 1, + }, + port: { + source: 'sourcePort', + destination: 'destinationPort', + }, + // initialize demand dates as today + demandDates: { + start: new Date(), + end: new Date(), + }, + }; + }); + + it('should return valid', () => { + // arrange + const cargoValidationService: CargoValidationService = + new CargoValidationService(); + + // ensure demand dates are valid + cargo.demandDates.start.setDate(cargo.demandDates.start.getDate() + 1); + cargo.demandDates.end.setDate(cargo.demandDates.end.getDate() + 2); + + // act + const result: ValidatedCargo = + cargoValidationService.validateCargo(cargo); + + // assert + expect(result.valid).to.be.true; + expect(result.errorMessage).to.be.null; + }); + + it('should return invalid - dates must be in future', () => { + // arrange + const cargoValidationService: CargoValidationService = + new CargoValidationService(); + + // ensure demand dates are valid + cargo.demandDates.start.setDate(cargo.demandDates.start.getDate() - 2); + cargo.demandDates.end.setDate(cargo.demandDates.end.getDate() - 1); + + // act + const result: ValidatedCargo = + cargoValidationService.validateCargo(cargo); + + // assert + expect(result.valid).to.be.false; + expect(result.errorMessage).to.equal( + 'Start and end dates must be in future.' + ); + }); + it('should return invalid - start date cannot be 60 days in future', () => { + // arrange + const cargoValidationService: CargoValidationService = + new CargoValidationService(); + + // ensure demand dates are invalid + cargo.demandDates.start.setDate(cargo.demandDates.start.getDate() + 65); + cargo.demandDates.end.setDate(cargo.demandDates.end.getDate() + 68); + + // act + const result: ValidatedCargo = + cargoValidationService.validateCargo(cargo); + + // assert + expect(result.valid).to.be.false; + expect(result.errorMessage).to.equal( + 'Start date cannot be more than 60 days in future.' + ); + }); + it('should return invalid - date range cannot exceed 30 days', () => { + // arrange + const cargoValidationService: CargoValidationService = + new CargoValidationService(); + + // ensure demand dates are invalid + cargo.demandDates.start.setDate(cargo.demandDates.start.getDate() + 30); + cargo.demandDates.end.setDate(cargo.demandDates.end.getDate() + 90); + + // act + const result: ValidatedCargo = + cargoValidationService.validateCargo(cargo); + + // assert + expect(result.valid).to.be.false; + expect(result.errorMessage).to.equal( + 'Range between start and end dates cannot exceed 30 days.' + ); + }); + it('should return invalid - end date must be after start date', () => { + // arrange + const cargoValidationService: CargoValidationService = + new CargoValidationService(); + + // ensure demand dates are invalid + cargo.demandDates.start.setDate(cargo.demandDates.start.getDate() + 2); + cargo.demandDates.end.setDate(cargo.demandDates.end.getDate() + 1); + + // act + const result: ValidatedCargo = + cargoValidationService.validateCargo(cargo); + + // assert + expect(result.valid).to.be.false; + expect(result.errorMessage).to.equal( + 'End date must be after start date.' + ); + }); +}); diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/tsconfig.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/tsconfig.json new file mode 100644 index 0000000..d200f45 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-processing-validator/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + "resolveJsonModule": true, + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "exclude": [ + "src/tests/**/*.ts" + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/Dockerfile new file mode 100644 index 0000000..828de1f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/Dockerfile @@ -0,0 +1,11 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ubuntu/.devcontainer/base.Dockerfile + +# [Choice] Ubuntu version (use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon): ubuntu-22.04, ubuntu-20.04, ubuntu-18.04 +ARG VARIANT="jammy" +FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b2d5886 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ubuntu +{ + "name": "cargo-test-scripts", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04 + // Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon. + "args": { + "VARIANT": "ubuntu-22.04" + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.js-debug", + "nwhatt.chai-snippets" + ], + "settings": { + "editor.formatOnType": true, + "editor.formatOnSave": true + } + } + }, + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "node": "lts", + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.env.sample b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.env.sample new file mode 100644 index 0000000..f05400f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.env.sample @@ -0,0 +1,5 @@ +SERVICE_BUS_CONNECTION_STRING="{Enter Connection String to the Service Bus}" +QUEUE_NAME="{Enter the name of the queue to feed for generated cargo items}" +TOPIC_NAME="{Enter the name of the topic to feed generated cargo items to}" +CARGO_PROCESSING_API_URL="http://localhost:8080/" +OPERATIONS_API_URL="http://localhost:8081/" \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.gitignore new file mode 100644 index 0000000..6ee4b04 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +testResults \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.vscode/launch.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.vscode/launch.json new file mode 100644 index 0000000..e8f8fd1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Default Test Scripts", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/index.js", + }, + { + "type": "node", + "request": "launch", + "name": "Chaos Testing", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/index.js", + "args": [ + "-c", + "./testConfigurations/cargo_processing_api_chaos_tests.json" + ] + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/README.md b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/README.md new file mode 100644 index 0000000..b694a3c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/README.md @@ -0,0 +1,224 @@ +# Cargo Processing Tests Scripts + +This set of scrips is used to exercise the different features of the Cargo Processing system. It has the following capabilities: + +* Generate realistic looking cargo records based on data configured within the ./seed.json file +* Make the cargo data that is generated invalid based on the different validation tests that exist within the cargo-processing-validator logic +* Send large volumes of generated data at the system, that can be configured to randomly invalidate the cargo +* Ensure that the cargo was processed correctly based on the data stored within the operations-api +* Target different entry points within the system (helpful when trying to isolate a specific service for testing of new functionality) +* Trigger chaos within the system TBD + * Configure the target for the chaos TBD + * Inject random chaos within a high load test TBD + * Configure the type of chaos to trigger based on what the target of the chaos is TBD + +## Targets + +There are 4 different targets for the test cases. As each target needs to have access to different environment specific settings, you may not need to configure all of the settings used by the test scripts. The following describes the different targets the test generators can hit, along with the relevant environment settings for them. + +* cargo-processing-api: The only end-to-end test target as it represents sending messages to the ingestion point of the system. As such it has the ability to perform validation tests. The environment settings required to run tests targetting this test generator are: + * CARGO_PROCESSING_API_URL: The host path for the cargo-processing-api, it is where cargo will be posted to. + * OPERATIONS_API_URL: The host path for the operation-api, it is where the validation tests will look for the operations detail. This is only required if you've configured the tests to validate the results. +* cargo-processing-validator: This target will post cargo object directly into the ingestion queue that the cargo-processing-validator will read from to process cargo. Based on configuration settings within the test run, this generator will randomly create invalid cargo along with valid cargo. The environment settings required to run tests targeting this test generator are: + * SERVICE_BUS_CONNECTION_STRING: The connection string to the service bus that the cargo-processing-validator service is listening to + * QUEUE_NAME: The name of the ingestion queue that the cargo-processing-validator service is listening to +* valid-cargo-manager: This will target just the valid-cargo-manager, bypassing both the cargo-processing-api and the cargo-processing-validator. The cargo objects generated from this step, would pass the validation tests performed by the cargo-processing-validator. The environment settings required to run tests targeting this test generator are: + * SERVICE_BUS_CONNECTION_STRING: The connection string to the service bus that the valid-cargo-manager service is listening to + * TOPIC_NAME: The name of the topic that the valid-cargo-manager service is listening to +* invalid-cargo-manager: This will target just the valid-cargo-manager, bypassing both the cargo-processing-api and the cargo-processing-validator. . The cargo objects generated from this step, would pass the validation tests performed by the cargo-processing-validator. The environment settings required to run tests targeting this test generator are: + * SERVICE_BUS_CONNECTION_STRING: The connection string to the service bus that the valid-cargo-manager service is listening to + * TOPIC_NAME: The name of the topic that the valid-cargo-manager service is listening to + +## Configuration + +There are quite a number of toggles that can be used to create the tests. Enough that providing via command lines args can be quite cumbersume. Instead, you have the ability to configure either a single test run, or a suite of test runs, within a single configuration file. The current set of configurations can be found within the ./testConfigurations directory. The details of the structure of the configuration are as follows: + +At the top level is a single object named "tests" which is an array of test objects to run. The test object structure is: + +* name(string): Used when writing to the console, and constructing the test report. +* target(string): Defines which of the above targets the test will run for +* volume(number): the number of cargo objects to generate for the tests +* validateResults(boolean): instructs the scripts to validate the results of the test (only available when targeting the cargo-processing-api) +* validationDelayInMilliseconds(number): the number of milliseconds the script will delay before validating the test results +* delayBetweenCargoInMilliseconds(number): the number of milliseconds the script will delay between sending each cargo object to the target. No delay is applied if this is set to `0` (the default) +* maxRetries(number): the number of times the scripts will retry a failed validation test +* startingRetryBufferInMilliseconds(number): the number of milliseconds the scripts will delay before retrying the validation. Each retry will double this value, providing a growing backoff period between retries. +* properties(object): key/value properties provided to the generators to assist with their processing. Each generator has the ability to have their own relevant properties. Current properties available are: + +| property name | datatype | targets using | implemented | description | +| ------------- | -------- | ------------- | ----------- | ----------- | +| chanceToInvalidate | number | cargo-processing-api, cargo-processing-validator | Yes | Indicates the chance that a generated cargo object will be made invalid. It acts as a 1 in N chance for the cargo to be made invalid. e.g. a value of 0, guarantees none of the cargo will be made invalid, a value of 1 guarantees that all of the cargo will be made invalid, a value of 50 means that there is a 1 in 50 chance that any single cargo object will be made invalid | +| chaosSettings | Array | None | No | Will house the configuration of how the tests will create chaos within the system | + +For example the default test that are run when no configuration file is provided when run is this: + +``` json +{ + "tests": [ + { + "name": "End to End Validation of valid cargo", + "target": "cargo-processing-api", + "volume": 5, + "validateResults": true, + "validationDelayInMilliseconds": 20000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0 + } + }, + { + "name": "End to End Validation of invalid cargo", + "target": "cargo-processing-api", + "volume": 5, + "validateResults": true, + "validationDelayInMilliseconds": 10000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 1 + } + } + ] +} +``` + +## Running the tests + +Once configure you can either run the default tests simply by running the + +``` bash +node ./index.js +``` + +from the command line. You can override this default behavior by by providing an alternative configuration file for your tests. Like so: + +``` bash +node ./index.js -c ./testConfigurations/scale.json +``` + +The console output will provide status of what the scripts are doing while they and provide a summary of each tests results for example: + +``` bash +Starting End to End Validation of invalid cargo test. +index.js:23 +5 cargo objects generated +generators/cargoProcessingApi.js:15 +Sending cargo to: http://20.106.116.247/cargo/a2e5fd80-6c0a-43ba-908c-ea6fad23bb24 +generators/cargoProcessingApi.js:41 +Sending cargo to: http://20.106.116.247/cargo/42e6c004-f9ad-4f48-97e0-962d9492303f +generators/cargoProcessingApi.js:41 +Sending cargo to: http://20.106.116.247/cargo/9863bff4-028c-4e90-84a9-c0e8b5bd5efb +generators/cargoProcessingApi.js:41 +Sending cargo to: http://20.106.116.247/cargo/67ee24ed-880a-43cb-ad6c-6f9c43df4465 +generators/cargoProcessingApi.js:41 +Sending cargo to: http://20.106.116.247/cargo/8732fda3-1f36-444f-bbd7-7ff5ce4c174a +generators/cargoProcessingApi.js:41 +2 +Giving system time to process the cargo before validating the results +index.js:45 +Getting operation details from: http://20.106.116.247/operations/fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 +cargoValidation.js:55 +Operation id: fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 failed validation. 5 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 +cargoValidation.js:55 +Operation id: fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 failed validation. 4 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 +cargoValidation.js:55 +Operation id: fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 failed validation. 3 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 +cargoValidation.js:55 +Operation id: fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 failed validation. 2 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/fc8dd636-d8b2-3fcf-88d5-77a1e34f77e2 +cargoValidation.js:55 +Getting operation details from: http://20.106.116.247/operations/f74b33e7-b7df-3519-97bf-37ae07d8a9db +cargoValidation.js:55 +Getting operation details from: http://20.106.116.247/operations/d80c3a68-dc69-3607-bc1d-4f5eb22106bf +cargoValidation.js:55 +Operation id: d80c3a68-dc69-3607-bc1d-4f5eb22106bf failed validation. 5 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/d80c3a68-dc69-3607-bc1d-4f5eb22106bf +cargoValidation.js:55 +Operation id: d80c3a68-dc69-3607-bc1d-4f5eb22106bf failed validation. 4 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/d80c3a68-dc69-3607-bc1d-4f5eb22106bf +cargoValidation.js:55 +Operation id: d80c3a68-dc69-3607-bc1d-4f5eb22106bf failed validation. 3 retries remaining +cargoValidation.js:44 +Getting operation details from: http://20.106.116.247/operations/d80c3a68-dc69-3607-bc1d-4f5eb22106bf +cargoValidation.js:55 +Getting operation details from: http://20.106.116.247/operations/0f6d7a74-563d-399d-bb56-49423a5655cf +cargoValidation.js:55 +Getting operation details from: http://20.106.116.247/operations/5774754b-7dcf-3503-9d6a-fda7d542c947 +cargoValidation.js:55 +********************************************************************************** +outputTestResults.js:23 +End to End Validation of invalid cargo results: 5 test; 0 failed; 5 succeeded; +outputTestResults.js:24 +********************************************************************************** +Detailed Test Results can be viewed at ./testResults/EndtoEndValidationofinvalidcargo-43bff096-5255-4b87-bb82-1f34ede460de.txt +``` + +For more detailed results of the tests, a test report is provided. The final output line from the test run will provide the file name for the test report. + +The test report will contain the same summary information provided in the console, but also what cargo objects were sent, the final operation state for the cargo sent, and a break down of which validation tests passed/failed. + +### Experimenting with test configuration + +As well as loading the test configuration from a file, you can also specify the test configuration via stdin. +This is useful when you want to experiment with different test configurations without having to modify the test configuration file. + +The command below shows an example of how to specify the test configuration via stdin: + +```bash +cat << EOF | node index.js -c - +{ + "tests": [ + { + "name": "Send cargo to cargo processing api", + "target": "cargo-processing-api", + "volume": 50, + "validateResults": false, + "delayBetweenCargoInMilliseconds": 5000, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0 + } + } + ] +} +EOF +``` + +## Creating Chaos + +Built into the tests is the ability to trigger chaos within the services. This is functionality intended to ensure our observability and monitoring solutions are capable of finding and potentially guarding against known failures that could occur. To create a chaos within a test run, you will need to add chaosSettings within the properties of the test. + +A chaos setting is made up of the following values: + +* Target: The service that will end up causing chaos. In the context of this solution space, the values map to the names of the different services. cargo-processing-api, cargo-processing-validator, invalid-cargo-manager, valid-cargo-manager and operations-api. +* Type: Each service has it's own types and variety of chaos that can be let lose on it. Below is a table describing what types of chaos are available. +* isEnabled: indicates if the chaosSetting will actually stand a chance of being triggered. +* chanceToCauseChaos: Works similarly to the chanceToInvalidate variable fo the test configuration. It acts as a 1 in N chance for the chaos to be triggered. e.g. a value of 0, guarantees none of the cargo will trigger chaos, a value of 1 guarantees that all of the cargo will trigger this type of chaos, a value of 50 means that there is a 1 in 50 chance that any single cargo object will trigger the chaos + +### Types of chaos + +The below table defines the different types of chaos that can be created within these services. + +| Target | Type | Description | Notes | +| -------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| cargo-processing-api | operations-api-failure | Will cause a chaos exception to occur when the cargo-processing-api attempts to call the operations-api. | This will cause the put/post request to receive a INTERNAL SERVER ERROR response, but the api should continue to function. | +| cargo-processing-api | process-ending | Will cause the cargo-processing-api to shut down | | +| cargo-processing-api | service-bus-failure | Will cause the service to close the service-bus connection right before it attempts to use it. | | +| cargo-processing-api | invalid-schema | Will cause the test script to modify the cargo object being sent in away that causes the cargo-processing-api to throw an invalid json schema exception. | | +| cargo-processing-validator | service-bus-failure | Will cause the service to close the service-bus connection right before it attempts to use it. | | +| cargo-processing-validator | process-killing | Will cause the cargo-processing-validator to shut down | | +| cargo-processing-validator | invalid-schema | Sends a message that is missing it's demandDates directly to the ingest-cargo queue | In order to bypass the APIs validation checks, the message must be injected directly into the queue to trigger the dead letter effect of an invalid schema being sent to the validator | + +### Sample Chaos + +The [cargo_processing_api_chaos_tests](./testConfigurations/cargo_processing_api_chaos_tests.json) file has settings that allow you to create specific chaos events. Each chaos type has been configured to always trigger, when enabled. By default all of the chaos settings have been disabled. Changing the isEnabled to true on one of them will test that specific chaos type. The launch.json has been configured with a debug options for running the test scripts with that specific configuration. diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/cargoValidation.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/cargoValidation.js new file mode 100644 index 0000000..f22d0a6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/cargoValidation.js @@ -0,0 +1,75 @@ +const dataBuilderUtil = require('./dataBuilderUtils.js'); +const config = require('./config.js'); +const axios = require('axios'); +const path = require('path'); +const URL = require('node:url').URL; + +class CargoValidation { + async validate(cargoSent, retries, buffer) { + const operation = await this.getOperationInformation(cargoSent.operationId); + const validationResults = { testDetails: { cargo: { ...cargoSent }, operation: { ...operation } } }; + + if (operation == undefined) { + validationResults.operationFound = false; + // Giving the services more time to populate the operation + return await this.retryValidation(cargoSent, validationResults, retries, buffer); + } + + validationResults.operationFound = true; + validationResults.stateCorrect = operation.state != undefined && operation.state == cargoSent.resultDetails.state; + + if (!validationResults.stateCorrect) { + // State isn't correct + // Giving the services more time to finish processing the operation + return await this.retryValidation(cargoSent, validationResults, retries, buffer); + } + + // State is what is expected, no more retries, services have processed to the expected state, + // Any incorrect values from here due to services not processing he cargo the way the tests expected them to + validationResults.resultPopulated = !(operation.result == null || operation.result == undefined); + validationResults.correctCargoId = cargoSent.cargo.id == operation.result.id; + validationResults.validFieldIsCorrect = cargoSent.resultDetails.isValid == operation.result.valid; + if (cargoSent.resultDetails.isValid == false) { + validationResults.correctErrorMessage = + operation.result.errorMessage == cargoSent.resultDetails.failureReason + } + + return validationResults; + } + + async retryValidation(cargoSent, validationResults, retries, buffer) { + if (retries <= 0) { + return validationResults; + } + console.log(`Operation id: ${cargoSent.operationId} failed validation. ${retries} retries remaining`); + + await dataBuilderUtil.delay(buffer); + + return await this.validate(cargoSent, retries - 1, buffer * 2); + } + + async getOperationInformation(id, retries = 3, backoff = 300) { + const retryCodes = [408, 500, 502, 503, 504, 522, 524] /* 2 */ + try { + const route = new URL(path.join('operations', id), config.operationsApiUrl).toString(); + console.log(`Getting operation details from: ${route}`); + const res = await axios.get(route); + const statusCode = res.status; + if (statusCode < 200 || statusCode > 299) { + if (retries > 0 && retryCodes.includes(statusCode)) { + //Non-blocking sleep + await dataBuilderUtil.delay(backoff); + return await this.getOperationInformation(id, retries - 1, backoff * 2); + } else { + throw (Error(res)); + } + } + return res.data; + + } catch (ex) { + console.error(ex); + } + } +} + +module.exports = { default: CargoValidation }; diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/config.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/config.js new file mode 100644 index 0000000..59bdba2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/config.js @@ -0,0 +1,11 @@ +require('dotenv').config(); + +serviceBusConnectionString = process.env.SERVICE_BUS_CONNECTION_STRING; +queueName = process.env.QUEUE_NAME; +topicName = process.env.TOPIC_NAME; +cargoProcessingApiUrl = process.env.CARGO_PROCESSING_API_URL; +operationsApiUrl = process.env.OPERATIONS_API_URL; + +module.exports = { + serviceBusConnectionString, queueName, topicName, cargoProcessingApiUrl, operationsApiUrl +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/dataBuilderUtils.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/dataBuilderUtils.js new file mode 100644 index 0000000..563f73b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/dataBuilderUtils.js @@ -0,0 +1,170 @@ +const seedData = require('./seed.json'); +const crypto = require('crypto'); +const addDays = require('date-fns/addDays'); + +generateBaseCargoObject = function () { + return { + id: crypto.randomUUID(), + product: getProduct(), + demandDates: getDemandDates(), + port: getPorts() + } +} + +getProduct = function () { + return { + name: getRandomValue(seedData.products), + quantity: getRandomNumber(10000) + 1 + }; +} + +getPorts = function () { + const source = getRandomValue(seedData.ports); + let destination = getRandomValue(seedData.ports); + while (destination == source) { + destination = getRandomValue(seedData.ports); + } + return { + source, destination + } +} + +getDemandDates = function () { + const today = new Date(); + + // Random day within the next 2 weeks + const startDaysFromToday = getRandomNumber(14) + 1; + // Random day, after the start date, within 60 days of today, and no more than 30 days from start + let endDaysFromStart = getRandomNumber(60 - startDaysFromToday) + 1; + endDaysFromStart = endDaysFromStart >= 30 ? 29 : endDaysFromStart; + + const start = addDays(today, startDaysFromToday); + const end = addDays(start, endDaysFromStart); + + return { start, end }; +} + +getRandomValue = function (from) { + return from[getRandomNumber(from.length)]; +} + +getRandomNumber = function (max) { + //Will return a random number between 0 and (max - 1) + return Math.floor(Math.random() * max); +} + +randomYesOrNo = function (chance) { + // No point in any of the rest of the processing if there is no chance of returning a true result + if (chance == 0) return false; + // No point in any of the rest of the processing if there is a guarentee in return a true + if (chance == 1) return true; + return (getRandomNumber(chance) + 1) % chance == 0; +} + +makeInvalid = function (cargo, includeValidationObject, failureReason) { + if (failureReason === undefined) { + failureReason = getRandomNumber(4) + 1; + } + let failureMessage = ''; + switch (failureReason) { + case 1: + // Make the dates occurr in the past + failureMessage = 'Start and end dates must be in future.'; + const reduceBy = -70; + cargo.demandDates.start = addDays(cargo.demandDates.start, reduceBy); + cargo.demandDates.end = addDays(cargo.demandDates.end, reduceBy); + if (includeValidationObject) { + cargo.valid = false; + cargo.errorMessage = failureMessage; + } + + break; + case 2: + // Make the dates occurr way in the future + failureMessage = 'Start date cannot be more than 60 days in future.'; + const increaseBy = 70; + cargo.demandDates.start = addDays(cargo.demandDates.start, increaseBy); + cargo.demandDates.end = addDays(cargo.demandDates.end, increaseBy); + if (includeValidationObject) { + cargo.valid = false; + cargo.errorMessage = failureMessage; + } + break; + case 3: + // Make the gap between the dates greater than 30 days + failureMessage = 'Range between start and end dates cannot exceed 30 days.'; + cargo.demandDates.end = addDays(cargo.demandDates.end, 30); + if (includeValidationObject) { + cargo.valid = false; + cargo.errorMessage = failureMessage; + } + break; + case 4: + // Flip the dates + failureMessage = 'End date must be after start date.'; + const tempDate = cargo.demandDates.start; + cargo.demandDates.start = cargo.demandDates.end; + cargo.demandDates.end = tempDate; + if (includeValidationObject) { + cargo.valid = false; + cargo.errorMessage = failureMessage; + } + break; + } + + return failureMessage; +} + +addToEnvelope = function (cargo) { + cargo.timestamp = new Date(); + return { + operationId: crypto.randomUUID(), + data: cargo + } +} + +toServiceBusMessage = function (envelope, isValid) { + const message = { + body: envelope, + applicationProperties: { + 'Diagnostic-Id': `00-${generateOpenTelemetryId(32)}-${generateOpenTelemetryId(16)}-01` + } + } + + if (isValid !== undefined) { + message.applicationProperties.valid = isValid; + } + return message; +} + +function generateOpenTelemetryId(length) { + // must satisfy regex for ids - https://github.com/open-telemetry/opentelemetry-js/blob/0f178d1e2e9b3aed81789820944452c153543198/api/src/trace/spancontext-utils.ts#L22 + const chars = 'abcdef1234567890'; + const randomArray = Array.from( + { length: length }, + () => chars[getRandomNumber(chars.length)] + ); + return randomArray.join(''); +} + +function raiseChaos(resultDetails, cargo, chaosSetting) { + if (resultDetails.chaosSetting !== undefined) { + // Chaos already set for this cargo, no need to get CRAZY here + return; + } + resultDetails.chaosSetting = chaosSetting; + cargo.port.source = chaosSetting.target; + cargo.port.destination = chaosSetting.type; + if (chaosSetting.type == "invalid-schema") { + cargo.demandDates = undefined; + } +} + +async function delay(milliseconds) { + await new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +module.exports = { + generateBaseCargoObject, makeInvalid, addToEnvelope, toServiceBusMessage, + getRandomNumber, getDemandDates, getPorts, randomYesOrNo, delay, raiseChaos +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingApi.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingApi.js new file mode 100644 index 0000000..3f40dd3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingApi.js @@ -0,0 +1,87 @@ +const dataBuilderUtil = require('../dataBuilderUtils.js'); +const config = require('../config.js'); +const axios = require('axios'); +const path = require('path'); +const URL = require('node:url').URL; + +class CargoProcessingApiGenerator { + constructor(properties) { + this.properties = properties; + this.canValidateResults = true; + } + + async run(count, delay) { + const cargo = this.generateCargoToSend(count); + console.log(`${cargo.length} cargo objects generated`); + if (delay === 0) { + return await Promise.all(cargo.map(this.putCargo)); + } else { + const result = [] + for (const cargoToSend of cargo) { + await this.putCargo(cargoToSend); + result.push(await dataBuilderUtil.delay(delay)); + } + return result; + } + } + + generateCargoToSend(count) { + return [...Array(count).keys()].map(() => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + const resultDetails = { isValid: true, state: "Succeeded" }; + this.randomlyMakeInvalid(resultDetails, cargo); + this.addSomeChaos(resultDetails, cargo) + + return { + cargo, + resultDetails + }; + }); + } + + randomlyMakeInvalid(resultDetails, cargo) { + if (dataBuilderUtil.randomYesOrNo(this.properties.chanceToInvalidate)) { + resultDetails.failureReason = dataBuilderUtil.makeInvalid(cargo, false); + resultDetails.isValid = false; + } + } + + addSomeChaos(resultDetails, cargo) { + if (this.properties.chaosSettings === undefined) return; + var activeChaos = this.properties.chaosSettings.filter(chaosSetting => chaosSetting.isEnabled); + activeChaos.forEach(chaosSetting => { + if (dataBuilderUtil.randomYesOrNo(chaosSetting.chanceToCauseChaos)) { + dataBuilderUtil.raiseChaos(resultDetails, cargo, chaosSetting); + } + }); + } + + async putCargo(cargoToSend, retries = 3, backoff = 300) { + const retryCodes = [408, 500, 502, 503, 504, 522, 524] /* 2 */ + const cargo = cargoToSend.cargo; + try { + const route = new URL(path.join('cargo', cargo.id), config.cargoProcessingApiUrl); + console.log(`Sending cargo to: ${route} (dest port: ${cargo.port.destination})`); + const res = await axios.put(route, cargo); + const statusCode = res.status; + if (statusCode < 200 || statusCode > 299) { + if (retries > 0 && retryCodes.includes(statusCode)) { + //Non-blocking sleep + await dataBuilderUtil.delay(backoff); + return await this.putCargo(cargoToSend, retries - 1, backoff * 2); + } else { + throw (Error(res)); + } + } + return { + operationId: res.headers.get('operation-id'), + cargo, + resultDetails: cargoToSend.resultDetails + } + } catch (ex) { + console.error(ex); + } + } +} + +module.exports = { default: CargoProcessingApiGenerator }; diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingValidator.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingValidator.js new file mode 100644 index 0000000..d7e0b45 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/cargoProcessingValidator.js @@ -0,0 +1,85 @@ +const dataBuilderUtil = require('../dataBuilderUtils.js'); +const ServiceBusClient = require('@azure/service-bus').ServiceBusClient; +const config = require('../config.js'); + +class CargoProcessingValidatorGenerator { + constructor(properties) { + this.properties = properties; + this.canValidateResults = false; + } + + async run(count) { + const cargoMessageBodies = this.generateCargoToSend(count); + console.log(`${cargoMessageBodies.length} cargo objects created`); + const sbClient = new ServiceBusClient(config.serviceBusConnectionString); + const sender = sbClient.createSender(config.queueName); + const cargoSent = []; + try { + let batch = await sender.createMessageBatch(); + for (let i = 0; i < cargoMessageBodies.length; i++) { + const cargoToSend = cargoMessageBodies[i]; + const message = { body: cargoToSend.cargo }; + // try to add the message to the batch + if (!batch.tryAddMessage(message)) { + // Couldn't add more to the batch, sending what we have, then starting a new batch + console.log(`Sending batch of ${batch.count} cargo objects`); + await sender.sendMessages(batch); + + // create a new batch + batch = await sender.createMessageBatch(); + + // now, add the message failed to be added to the previous batch to this batch + if (!batch.tryAddMessage(message)) { + // if it still can't be added to the batch, the message is to big + throw new Error("Message too big to fit in a batch"); + } + } + cargoSent.push({ + id: cargoToSend.cargo.id, + resultDetails: cargoToSend.resultDetails + }); + } + console.log(`Sending batch of ${batch.count} cargo objects`); + await sender.sendMessages(batch); + } + finally { + sender.close(); + sbClient.close(); + } + return cargoSent; + } + + generateCargoToSend(count) { + return [...Array(count).keys()].map(() => { + let cargo = dataBuilderUtil.generateBaseCargoObject(); + const resultDetails = { isValid: true }; + //Randomly make the cargo invalid + this.randomlyMakeInvalid(resultDetails, cargo); + this.addSomeChaos(resultDetails, cargo) + + return { + cargo: dataBuilderUtil.addToEnvelope(cargo), + resultDetails + }; + }); + } + + randomlyMakeInvalid(resultDetails, cargo) { + if (dataBuilderUtil.randomYesOrNo(this.properties.chanceToInvalidate)) { + resultDetails.failureReason = dataBuilderUtil.makeInvalid(cargo, false); + resultDetails.isValid = false; + } + } + + addSomeChaos(resultDetails, cargo) { + if (this.properties.chaosSettings === undefined) return; + var activeChaos = this.properties.chaosSettings.filter(chaosSetting => chaosSetting.isEnabled); + activeChaos.forEach(chaosSetting => { + if (dataBuilderUtil.randomYesOrNo(chaosSetting.chanceToCauseChaos)) { + dataBuilderUtil.raiseChaos(resultDetails, cargo, chaosSetting); + } + }); + } +} + +module.exports = { default: CargoProcessingValidatorGenerator }; diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/validatedCargoManagers.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/validatedCargoManagers.js new file mode 100644 index 0000000..dbd79e1 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/generators/validatedCargoManagers.js @@ -0,0 +1,73 @@ +const dataBuilderUtil = require('../dataBuilderUtils.js'); +const ServiceBusClient = require('@azure/service-bus').ServiceBusClient; +const config = require('../config.js'); + +class ValidatedCargoManagerGenerator { + constructor(isValid, properties) { + this.isValid = isValid; + this.properties = properties; + this.canValidateResults = false; + } + + async run(count) { + const cargoMessageBodies = this.generateCargoToSend(count); + console.log(`${cargoMessageBodies.length} cargo objects created`); + const sbClient = new ServiceBusClient(config.serviceBusConnectionString); + const sender = sbClient.createSender(config.topicName); + const cargoSent = []; + try { + let batch = await sender.createMessageBatch(); + for (let i = 0; i < cargoMessageBodies.length; i++) { + const cargoToSend = cargoMessageBodies[i]; + // try to add the message to the batch + if (!batch.tryAddMessage(cargoToSend.cargo)) { + // Couldn't add more to the batch, sending what we have, then starting a new batch + console.log(`Sending batch of ${batch.count} cargo objects`); + await sender.sendMessages(batch); + + // create a new batch + batch = await sender.createMessageBatch(); + + // now, add the message failed to be added to the previous batch to this batch + if (!batch.tryAddMessage(cargoToSend.cargo)) { + // if it still can't be added to the batch, the message is probably too big to fit in a batch + throw new Error("Message too big to fit in a batch"); + } + } + cargoSent.push({ + id: cargoToSend.cargo.id, + resultDetails: cargoToSend.resultDetails + }); + } + + console.log(`Sending batch of ${batch.count} cargo objects`); + await sender.sendMessages(batch); + } + finally { + sender.close(); + sbClient.close(); + } + return cargoSent; + } + + generateCargoToSend(count) { + return [...Array(count).keys()].map(() => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + const resultDetails = { + isValid + } + if (this.isValid) { + cargo.valid = true; + cargo.errorMessage = null; + } else { + resultDetails.failureReason = dataBuilderUtil.makeInvalid(cargo, true); + } + return { + cargo: dataBuilderUtil.toServiceBusMessage(addToEnvelope(cargo), this.isValid), + resultDetails + }; + }); + } +} + +module.exports = { default: ValidatedCargoManagerGenerator }; diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/index.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/index.js new file mode 100644 index 0000000..a58c85b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/index.js @@ -0,0 +1,68 @@ +const dataBuilderUtil = require('./dataBuilderUtils.js'); +const parseArgs = require('node:util').parseArgs; +const CargoProcessingApiGenerator = require('./generators/cargoProcessingApi.js').default; +const CargoProcessingValidatorGenerator = require('./generators/cargoProcessingValidator.js').default; +const ValidatedCargoManagerGenerator = require('./generators/validatedCargoManagers.js').default; +const CargoValidation = require('./cargoValidation.js').default +const displayTestResults = require('./outputTestResults.js').displayTestResults; +const { v4: uuidv4 } = require('uuid'); +const fs = require('node:fs'); +const runId = uuidv4(); + +const options = { + 'config': { type: 'string', short: 'c', default: "./testConfigurations/valid_tests.json" }, +}; + +const { values } = parseArgs({ options, tokens: false }); + +const configSource = values.config === "-" ? 0 : values.config; // Read from stdin if config is "-" +const config = JSON.parse(fs.readFileSync(configSource, 'utf8')); + +let testsFailed = false; + +for (const test of config.tests) { + console.log(`Starting ${test.name} test.`) + let generator = null; + switch (test.target) { + case 'cargo-processing-api': + generator = new CargoProcessingApiGenerator(test.properties); + break; + case 'cargo-processing-validator': + generator = new CargoProcessingValidatorGenerator(test.properties); + break; + case 'valid-cargo-manager': + generator = new ValidatedCargoManagerGenerator(true, test.properties); + break; + case 'invalid-cargo-manager': + generator = new ValidatedCargoManagerGenerator(false, test.properties); + break; + } + + (async function () { + const testVolume = parseInt(test.volume); + if (testVolume == 0) { + console.log("Volume configured for 0 tests, exiting test"); + return; + } + + const cargoSent = [...await generator.run(parseInt(test.volume), parseInt(test.delayBetweenCargoInMilliseconds ?? 0))]; + if (test.validateResults && generator.canValidateResults) { + const validationResults = []; + const cargoValidator = new CargoValidation(); + console.log('Giving system time to process the cargo before validating the results'); + + await dataBuilderUtil.delay(test.validationDelayInMilliseconds); + for (const cargo of cargoSent) { + validationResults.push( + await cargoValidator.validate(cargo, test.maxRetries, test.startingRetryBufferInMilliseconds) + ); + } + await displayTestResults(validationResults, test.name, runId); + } + + })(); +}; + +if (testsFailed) { + process.exit(1); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/outputTestResults.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/outputTestResults.js new file mode 100644 index 0000000..c743249 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/outputTestResults.js @@ -0,0 +1,65 @@ +const fs = require('fs/promises'); + +async function displayTestResults(validationResults, testName, runId) { + const successfulResults = []; + const failedResults = []; + for (const result of validationResults) { + var failed = false; + for (const key of Object.keys(result)) { + if (key == "testDetails") { + continue; + } + if (result[key] == false) { + failedResults.push(result); + failed = true; + continue; + } + } + if (!failed) { + successfulResults.push(result); + } + } + + console.log(`**********************************************************************************`); + console.log(`${testName} results: ${validationResults.length} test; \x1b[31m${failedResults.length} failed\x1b[0m; ${successfulResults.length} succeeded; `); + console.log(`**********************************************************************************`); + + await saveTestReport(testName, validationResults, failedResults, successfulResults, runId) +} + +async function saveTestReport(testName, validationResults, failedResults, successfulResults, runId) { + const testReportDirectory = "testResults"; + await createDirectory(testReportDirectory); + + const fileName = `./${testReportDirectory}/${testName.replaceAll(' ', '')}-${runId}.txt`; + await fs.writeFile(fileName, ""); + await fs.appendFile(fileName, `**********************************************************************************\n`); + await fs.appendFile(fileName, `${testName} results: ${validationResults.length} test; ${failedResults.length} failed; ${successfulResults.length} succeeded;\n`); + await fs.appendFile(fileName, `**********************************************************************************\n`); + + if (failedResults.length > 0) { + testsFailed = true; + await fs.appendFile(fileName, `\nFailed Results\n`); + await fs.appendFile(fileName, `----------------------------------------------------------------------------------\n`); + await fs.appendFile(fileName, `${JSON.stringify(failedResults, null, 2)}\n`); + } + if (successfulResults.length > 0) { + await fs.appendFile(fileName, `\nSuccessful Results:\n`); + await fs.appendFile(fileName, `----------------------------------------------------------------------------------\n`); + await fs.appendFile(fileName, JSON.stringify(successfulResults, null, 2)); + } + console.log(`Detailed Test Results can be viewed at ${fileName}`) +} + +async function createDirectory(path) { + try { + await fs.access(path); + } catch (error) { + console.log(error); + await fs.mkdir(path); + } +} + +module.exports = { + displayTestResults +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package-lock.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package-lock.json new file mode 100644 index 0000000..88ecc4e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package-lock.json @@ -0,0 +1,2943 @@ +{ + "name": "cargo-test-scripts", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cargo-test-scripts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@azure/service-bus": "^7.7.2", + "axios": "^1.1.3", + "date-fns": "^2.29.3", + "dotenv": "^16.0.3", + "uuid": "^9.0.0" + }, + "devDependencies": { + "chai": "^4.3.7", + "mocha": "^10.1.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-amqp": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-3.1.1.tgz", + "integrity": "sha512-mQp19Z7uw/OaDw6v4ASLCMCuy3CPs/o2F45n3yOImnTjS2KrfMxVhrvxD7UL/+fRqhGJZwBpDqCXnlAiKNanrA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-util": "^1.1.0", + "@azure/logger": "^1.0.0", + "buffer": "^6.0.0", + "events": "^3.0.0", + "jssha": "^3.1.0", + "process": "^0.11.10", + "rhea": "^2.0.3", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0", + "url": "^0.11.0", + "util": "^0.12.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", + "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.6.1.tgz", + "integrity": "sha512-mZ1MSKhZBYoV8GAWceA+PEJFWV2VpdNSpxxcj1wjIAOi00ykRuIQChT99xlQGZWLY3/NApWhSImlFwsmCEs4vA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.3.0.tgz", + "integrity": "sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.9.2.tgz", + "integrity": "sha512-8rXI6ircjenaLp+PkOFpo37tQ1PQfztZkfVj97BIF3RPxHAsoVSgkJtu3IK/bUEWcb7HzXSoyBe06M7ODRkRyw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.1.1.tgz", + "integrity": "sha512-A4TBYVQCtHOigFb2ETiiKFDocBoI1Zk2Ui1KpI42aJSIDexF7DHQFpnjonltXAIU/ceH+1fsZAWWgvX6/AKzog==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.3.0.tgz", + "integrity": "sha512-HYulCHr/3eMDxGubmbm+KIUxpOKPGtRxpaKBN6GpgPDQzREefdQ5bDlTuwHWhtqwyUG4RicKtZu8rhv5Sbg8jQ==", + "dependencies": { + "fast-xml-parser": "^4.0.8", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/service-bus": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.7.2.tgz", + "integrity": "sha512-acHI/ghFz6wRJgE1LucAZkdpWTEzfWt2moxD7RTo0/9nQoVsriE6GWI/lkyt56m+WiUbUh8Dms1srjF0G3AdyA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^3.1.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.1.1", + "@azure/core-xml": "^1.0.0", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^5.2.0", + "process": "^0.11.10", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/is-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", + "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", + "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", + "integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==", + "engines": { + "node": "*" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", + "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rhea": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-2.0.8.tgz", + "integrity": "sha512-IgwlP4D2lzinBSll5f35tAWa30dGCZhG9Ujd1DiaB7MUGegIjAaLzqATCw3ha+h9oq9mXcitqayBbNIXYdvtFg==", + "dependencies": { + "debug": "0.8.0 - 3.5.0" + } + }, + "node_modules/rhea-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-2.1.0.tgz", + "integrity": "sha512-CRMwdJ/o4oO/xKcvAwAsd0AHy5fVvSlqso7AadRmaaLGzAzc9LCoW7FOFnucI8THasVmOeCnv5c/fH/n7FcNaA==", + "dependencies": { + "debug": "^3.1.0", + "rhea": "^2.0.3", + "tslib": "^2.2.0" + } + }, + "node_modules/rhea-promise/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/rhea/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-amqp": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-3.1.1.tgz", + "integrity": "sha512-mQp19Z7uw/OaDw6v4ASLCMCuy3CPs/o2F45n3yOImnTjS2KrfMxVhrvxD7UL/+fRqhGJZwBpDqCXnlAiKNanrA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-util": "^1.1.0", + "@azure/logger": "^1.0.0", + "buffer": "^6.0.0", + "events": "^3.0.0", + "jssha": "^3.1.0", + "process": "^0.11.10", + "rhea": "^2.0.3", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0", + "url": "^0.11.0", + "util": "^0.12.1" + } + }, + "@azure/core-auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", + "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.6.1.tgz", + "integrity": "sha512-mZ1MSKhZBYoV8GAWceA+PEJFWV2VpdNSpxxcj1wjIAOi00ykRuIQChT99xlQGZWLY3/NApWhSImlFwsmCEs4vA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-paging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.3.0.tgz", + "integrity": "sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-rest-pipeline": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.9.2.tgz", + "integrity": "sha512-8rXI6ircjenaLp+PkOFpo37tQ1PQfztZkfVj97BIF3RPxHAsoVSgkJtu3IK/bUEWcb7HzXSoyBe06M7ODRkRyw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.1.1.tgz", + "integrity": "sha512-A4TBYVQCtHOigFb2ETiiKFDocBoI1Zk2Ui1KpI42aJSIDexF7DHQFpnjonltXAIU/ceH+1fsZAWWgvX6/AKzog==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-xml": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.3.0.tgz", + "integrity": "sha512-HYulCHr/3eMDxGubmbm+KIUxpOKPGtRxpaKBN6GpgPDQzREefdQ5bDlTuwHWhtqwyUG4RicKtZu8rhv5Sbg8jQ==", + "requires": { + "fast-xml-parser": "^4.0.8", + "tslib": "^2.2.0" + } + }, + "@azure/logger": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/service-bus": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.7.2.tgz", + "integrity": "sha512-acHI/ghFz6wRJgE1LucAZkdpWTEzfWt2moxD7RTo0/9nQoVsriE6GWI/lkyt56m+WiUbUh8Dms1srjF0G3AdyA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^3.1.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.1.1", + "@azure/core-xml": "^1.0.0", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^5.2.0", + "process": "^0.11.10", + "rhea-promise": "^2.1.0", + "tslib": "^2.2.0" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "@types/is-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", + "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "axios": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", + "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-xml-parser": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", + "integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==", + "requires": { + "strnum": "^1.0.5" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==" + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", + "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "rhea": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-2.0.8.tgz", + "integrity": "sha512-IgwlP4D2lzinBSll5f35tAWa30dGCZhG9Ujd1DiaB7MUGegIjAaLzqATCw3ha+h9oq9mXcitqayBbNIXYdvtFg==", + "requires": { + "debug": "0.8.0 - 3.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "rhea-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-2.1.0.tgz", + "integrity": "sha512-CRMwdJ/o4oO/xKcvAwAsd0AHy5fVvSlqso7AadRmaaLGzAzc9LCoW7FOFnucI8THasVmOeCnv5c/fH/n7FcNaA==", + "requires": { + "debug": "^3.1.0", + "rhea": "^2.0.3", + "tslib": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package.json new file mode 100644 index 0000000..ab77261 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/package.json @@ -0,0 +1,22 @@ +{ + "name": "cargo-test-scripts", + "version": "1.0.0", + "description": "Command utility to execute test scripts from", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@azure/service-bus": "^7.7.2", + "axios": "^1.1.3", + "date-fns": "^2.29.3", + "dotenv": "^16.0.3", + "uuid": "^9.0.0" + }, + "devDependencies": { + "chai": "^4.3.7", + "mocha": "^10.1.0" + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/seed.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/seed.json new file mode 100644 index 0000000..d3dbb98 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/seed.json @@ -0,0 +1,177 @@ +{ + "ports": [ + "Shanghai, China", + "Singapore", + "Ningbo-Zhoushan, China", + "Shenzhen, China", + "Guangzhou Harbor, China", + "Busan, South Korea", + "Qingdao, China", + "Hong Kong, S.A.R, China", + "Tianjin, China", + "Rotterdam, The Netherlands", + "Jebel Ali, Dubai, United Arab Emirates", + "Port Klang, Malaysia", + "Xiamen, China", + "Antwerp, Belgium", + "Kaohsiung, Taiwan, China", + "Dalian, China", + "Los Angeles, U.S.A", + "Hamburg, Germany", + "Tanjung Pelepas, Malaysia", + "Laem Chabang, Thailand", + "Keihin Ports, Japan", + "Long Beach, U.S.A.", + "Tanjung Priok, Jakarta, Indonesia", + "New York-New Jersey, U.S.A.", + "Colombo, Sri Lanka", + "Ho Chi Minh City, Vietnam", + "Suzhou, China", + "Piraeus, Greece", + "Yingkou, China", + "Valencia, Spain", + "Manila, Philippines", + "Taicang, China", + "Algeciras, Spain", + "Jawarharlal Nehru Port (Nhava Sheva), India", + "Bremen/Bremerhaven, Germany", + "Tanger Med, Morocco", + "Lianyungang, China", + "Mundra, India", + "Savannah, U.S.A", + "Tokyo, Japan", + "English Chinese Korean Japanese", + "Rizhao, China", + "English Chinese", + "Foshan, China", + "Jeddah, Saudi Arabia", + "Colon, Panama", + "Santos, Brazil", + "Salalah, Oman", + "Dongguan, China", + "Guangxi Beibu, China" + ], + "products": [ + "Tracksuits", + "Silk pajamas", + "Backpacks", + "Slides", + "Knitted dress", + "Flannel shirts", + "Leggings", + "Silicone rings", + "Boho clothes", + "Mom jeans", + "Loose jeans", + "Balaclava", + "Tote bags", + "Scrunchies", + "Couple unisex T-shirts", + "Essential short sleeve tees", + "Sandals", + "Floral-print kimonos", + "Quick-dry running shorts", + "Wirefree bras", + "Cargo shorts", + "Loafers", + "Tank tops", + "Smart locks", + "Cordless electric drills", + "Repair tool kit", + "Digital micrometer", + "Electric scissors", + "Electric crimping tools", + "Welding tips", + "Desoldering pumps", + "Ceramic coating", + "Socket wrenches", + "Digital tire-pressure gauge", + "Portable power stations", + "Car jump starters", + "Portable air compressor", + "Decorative lamp", + "Sheet metal tools", + "Tire repair & Installation tools", + "Code readers & Scan tools", + "Hand sanitizer", + "Bamboo toothbrushes", + "Sleep eye masks", + "Sleep gummies", + "Yoga mats", + "Massage guns", + "Vitamin C serum", + "Jade rollers", + "Acne patches", + "Electric toothbrushes", + "Lip masks", + "Blue light glasses", + "Posture corrector", + "Oral irrigators", + "Scalp massager", + "Hair growth oil", + "Yoga socks", + "Makeup remover facial wipes", + "Cruelty-free mascara", + "Pimple patches", + "Sunscreen", + "Skin care oil", + "Eyeliner pencil", + "Facial moisturizing lotion", + "Hydrating eye gel", + "Baby monitor", + "Baby carrier", + "Smart bassinet", + "Nipple breastmilk storage bags", + "Baby stroller & accessories", + "Toilet training", + "Car seats & accessories", + "Hypoallergenic baby diaper wipes", + "Water-based wipes", + "Diapers", + "Stroller fans", + "Blue light glasses", + "Pencil cases", + "Mechanical pencils", + "Podcast microphones", + "Mechanical keycaps", + "Wall clock", + "Drawing tablet", + "Laser acupuncture pen", + "Gaming monitors", + "Ergonomic chairs", + "Gaming mouse pads", + "Gaming headsets", + "Smart plugs", + "Smart lights", + "Soap dispensers", + "Smart locks", + "Wireless security cameras", + "Foldable picnic table", + "Air purifiers", + "Rice cooker", + "Rugs", + "Reusable silicone food covers", + "Vacuum packing machine", + "Thermos/Flask", + "Biometric locks", + "Portable blenders", + "Water shower filter", + "Electric kettles", + "Oat milk", + "Fasting tea", + "Wine fridges", + "Doormats", + "Air-purifying plants", + "Ceiling fans", + "Blackout curtains", + "Blankets", + "Orthopedic pillows", + "Duvet covers", + "Bedside lamps", + "Air fryers", + "Electric kettles", + "Portable blenders", + "Coffee pods", + "Latte mixers" + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/test/dataBuilderUtils.test.js b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/test/dataBuilderUtils.test.js new file mode 100644 index 0000000..a3b8e7f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/test/dataBuilderUtils.test.js @@ -0,0 +1,60 @@ +const expect = require('chai').expect; +const dataBuilderUtil = require('../dataBuilderUtils'); +const addDays = require('date-fns/addDays'); + +describe('Make Invalid Functionality', () => { + it('Should make the dates occur in the past', () => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + //Make sure the values are populated + dataBuilderUtil.makeInvalid(cargo, false, 1); + expect(cargo.demandDates.start).to.not.be.undefined; + expect(cargo.demandDates.end).to.not.be.undefined; + //Make sure the start date occurs before the end date + expect(cargo.demandDates.end).to.be.above(cargo.demandDates.start); + //Make sure the dates occur in the past + expect(new Date()).to.be.above(cargo.demandDates.end); + }); + + it('Should make the dates occur to far in the future', () => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + dataBuilderUtil.makeInvalid(cargo, false, 2); + //Make sure the values are populated + expect(cargo.demandDates.start).to.not.be.undefined; + expect(cargo.demandDates.end).to.not.be.undefined; + //Make sure the start date occurs before the end date + expect(cargo.demandDates.end).to.be.above(cargo.demandDates.start); + //Make sure the dates occur to far into the future + expect(addDays(new Date(), 60)).to.be.below(cargo.demandDates.start); + }); + + it('Should make the dates to far apart', () => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + dataBuilderUtil.makeInvalid(cargo, false, 3); + //Make sure the values are populated + expect(cargo.demandDates.start).to.not.be.undefined; + expect(cargo.demandDates.end).to.not.be.undefined; + //Make sure the start date occurs before the end date + expect(cargo.demandDates.end).to.be.above(cargo.demandDates.start); + //Make sure the dates occur to far apart + expect(cargo.demandDates.end - cargo.demandDates.start).to.be.above(30); + }); + + it('Should make the end date occur before the start date', () => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + dataBuilderUtil.makeInvalid(cargo, false, 4); + //Make sure the values are populated + expect(cargo.demandDates.start).to.not.be.undefined; + expect(cargo.demandDates.end).to.not.be.undefined; + //Make sure the start date occurs after the end date + expect(cargo.demandDates.end).to.be.below(cargo.demandDates.start); + }); + + it('Should populate the error details on the cargo object', () => { + const cargo = dataBuilderUtil.generateBaseCargoObject(); + dataBuilderUtil.makeInvalid(cargo, true, 1); + //Make sure the values are populated + expect(cargo.valid).to.be.false; + expect(cargo.errorMessage).to.not.be.undefined; + }); +}); + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/cargo_processing_api_chaos_tests.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/cargo_processing_api_chaos_tests.json new file mode 100644 index 0000000..53f30c4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/cargo_processing_api_chaos_tests.json @@ -0,0 +1,24 @@ +{ + "tests": [ + { + "name": "CHAOS: Cargo Processing Validator Starting", + "target": "cargo-processing-api", + "volume": 1, + "validateResults": false, + "validationDelayInMilliseconds": 10000, + "delayBetweenCargoInMilliseconds": 1000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0, + "chaosSettings": [ + { + "target": "cargo-processing-api", + "type": "operations-api-failure", + "chanceToCauseChaos": 1 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/scale.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/scale.json new file mode 100644 index 0000000..c99c2ef --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/scale.json @@ -0,0 +1,30 @@ +{ + "tests": [ + { + "name": "End to End Validation at scale", + "target": "cargo-processing-api", + "volume": 10000, + "validateResults": true, + "validationDelayInMilliseconds": 60000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 10, + "chaosSettings": [ + { + "target": "operations-api", + "type": "service-failure", + "chanceToCauseChaos": 4, + "isEnabled": false + }, + { + "target": "cargo-processing-api", + "type": "duplicate", + "chanceToCauseChaos": 4, + "isEnabled": false + } + ] + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-normal.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-normal.json new file mode 100644 index 0000000..7894a9c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-normal.json @@ -0,0 +1,15 @@ +{ + "tests": [ + { + "name": "End to End Validation of valid cargo with normal ports", + "target": "cargo-processing-api", + "volume": 50, + "validateResults": false, + "delayBetweenCargoInMilliseconds": 1000, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0 + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-slow-port.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-slow-port.json new file mode 100644 index 0000000..85ac953 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid-cargo-slow-port.json @@ -0,0 +1,23 @@ +{ + "tests": [ + { + "name": "End to End Validation of valid cargo with 50% chance of slow port", + "target": "cargo-processing-api", + "volume": 50, + "validateResults": false, + "delayBetweenCargoInMilliseconds": 1000, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0, + "chaosSettings": [ + { + "target": "cargo-processing-api", + "type": "slow-port", + "chanceToCauseChaos": 2, + "isEnabled": true + } + ] + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid_tests.json b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid_tests.json new file mode 100644 index 0000000..7e63c70 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/cargo-test-scripts/testConfigurations/valid_tests.json @@ -0,0 +1,28 @@ +{ + "tests": [ + { + "name": "End to End Validation of valid cargo", + "target": "cargo-processing-api", + "volume": 5, + "validateResults": true, + "validationDelayInMilliseconds": 10000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 0 + } + }, + { + "name": "End to End Validation of invalid cargo", + "target": "cargo-processing-api", + "volume": 5, + "validateResults": true, + "validationDelayInMilliseconds": 10000, + "maxRetries": 5, + "startingRetryBufferInMilliseconds": 300, + "properties": { + "chanceToInvalidate": 1 + } + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/docker-compose.yml new file mode 100644 index 0000000..5603371 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + operations-api: + env_file: + - ./operations-api/.env + build: + context: ./operations-api + dockerfile: Dockerfile + ports: + - "8081:8081" + cargo-processing-api: + env_file: + - ./cargo-processing-api/.env + build: + context: ./cargo-processing-api + dockerfile: Dockerfile + ports: + - "8080:8080" + cargo-processing-validator: + env_file: + - ./cargo-processing-validator/.env + build: + context: ./cargo-processing-validator + dockerfile: Dockerfile + valid-cargo-manager: + build: + context: ./valid-cargo-manager + dockerfile: Dockerfile + invalid-cargo-manager: + env_file: + - ./invalid-cargo-manager/.env + build: + context: ./invalid-cargo-manager + dockerfile: Dockerfile + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/Dockerfile new file mode 100644 index 0000000..f90d80d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT="3.10-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && python -m pip install --upgrade pip \ + && apt-get -y install --no-install-recommends cmake + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/devcontainer.json new file mode 100644 index 0000000..84a5fc5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.devcontainer/devcontainer.json @@ -0,0 +1,59 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3 +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.10-bullseye", + // Options + "NODE_VERSION": "lts/*" + } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", + "autoDocstring.docstringFormat": "numpy" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "njpwerner.autodocstring" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "git": "latest", + "github-cli": "latest", + "azure-cli": "latest" + }, + "postCreateCommand": "pip install -r requirements.txt" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.dockerignore b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.dockerignore new file mode 100644 index 0000000..2ce5e1c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.dockerignore @@ -0,0 +1,2 @@ +.env +helm diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.env.sample b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.env.sample new file mode 100644 index 0000000..04d04a5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.env.sample @@ -0,0 +1,18 @@ +SERVICE_BUS_CONNECTION_STR= +SERVICE_BUS_TOPIC_NAME= +SERVICE_BUS_SUBSCRIPTION_NAME= +SERVICE_BUS_QUEUE_NAME= +SERVICE_BUS_MAX_MESSAGE_COUNT= +SERVICE_BUS_MAX_WAIT_TIME= + +COSMOS_DB_ENDPOINT= +COSMOS_DB_KEY= +COSMOS_DB_DATABASE_NAME= +COSMOS_DB_CONTAINER_NAME= + +APPLICATIONINSIGHTS_CONNECTION_STRING= +CLOUD_LOGGING_LEVEL=INFO +CONSOLE_LOGGING_LEVEL=DEBUG + +HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS=30 +HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS=60 \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.gitignore new file mode 100644 index 0000000..972165c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +error.json \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.vscode/launch.json b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.vscode/launch.json new file mode 100644 index 0000000..7675091 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true, + "envFile": "${workspaceFolder}/.env" + }, + { + "name": "Python: Service", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/src/service/app.py", + "justMyCode": true, + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/Dockerfile new file mode 100644 index 0000000..5f41734 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/mirror/docker/library/python:3.10-buster +WORKDIR /code +COPY ./requirements.txt /code/requirements.txt + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && python -m pip install --upgrade pip \ + && apt-get -y install --no-install-recommends cmake + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./src/service /code/service + +CMD ["python", "/code/service/app.py", "--host", "0.0.0.0", "--port", "3100", "--proxy-headers"] diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/README.md b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/README.md new file mode 100644 index 0000000..413ccbe --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/README.md @@ -0,0 +1,45 @@ +# Running the service + +## Pre-Requisites + +1. Service Bus [namespace](https://docs.microsoft.com/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create) with [topic](https://docs.microsoft.com/cli/azure/servicebus/topic?view=azure-cli-latest#az-servicebus-topic-create) and [subscription](https://docs.microsoft.com/cli/azure/servicebus/topic/subscription?view=azure-cli-latest#az-servicebus-topic-subscription-create) +1. Application Insights [account](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource#azure-cli-preview) +1. Cosmos DB [account](https://docs.microsoft.com/cli/azure/cosmosdb?view=azure-cli-latest#az-cosmosdb-create) with [database](https://docs.microsoft.com/cli/azure/cosmosdb/sql/database?view=azure-cli-latest#az-cosmosdb-sql-database-create) and [container](https://docs.microsoft.com/cli/azure/cosmosdb/sql/container?view=azure-cli-latest) + +## Debugging from VSCode Dev Container + +* Open the project in the dev container. + * Make sure to open in the devcontainer +* Rename `.env.sample` to `.env` and add connection strings for Service Bus, Cosmos DB and Application Insights. +* Configure debugger to use the "Python: Service" configuration. +* Run the Debugger. +* Post a message to the the Service Bus Topic similar to the [sample message](#sample-message). + +## Docker Container + +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Run `docker compose up` to run the service. +* Post a message to the the Service Bus Topic similar to the [sample message](#sample-message). + +## Sample Message + +``` json +{ + "id": "08e222e4-5180-4f35-a8d6-e41b47b6447c", + "timestamp": "2022-06-24T17:10:28.000+00:00", + "product": { + "name": "toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-06-24T00:00:00.000+00:00", + "end": "2022-06-30T00:00:00.000+00:00" + }, + "valid": false, + "errorMessage": "Bad stuff happened when it was validated" +} +``` diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/docker-compose.yml new file mode 100644 index 0000000..4f3bd00 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.9" + +services: + invalid_cargo_manager: + env_file: + - .env + build: + context: . + dockerfile: Dockerfile diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/.helmignore b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/Chart.yaml new file mode 100644 index 0000000..1c01e1f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: invalid-cargo-manager +description: invalid-cargo-manager for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/invalid-cargo-manager.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/invalid-cargo-manager.yaml new file mode 100644 index 0000000..3d7bf17 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/invalid-cargo-manager.yaml @@ -0,0 +1,35 @@ +image: + pullPolicy: Always + tag: "latest" + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + + +# When running one of the deploy-*.sh scripts, an additional env.yaml +# values file is created containing values specific to the deployed environment +# with the following values: +# image: +# repository: + +# keyVault: +# name: +# tenantId: + +# aksKeyVaultSecretProviderIdentityId: diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/NOTES.txt new file mode 100644 index 0000000..dd366ce --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aks-sb-azmonitor-microservices.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/_helpers.tpl new file mode 100644 index 0000000..0172cb3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aks-sb-azmonitor-microservices.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aks-sb-azmonitor-microservices.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/deployment.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/deployment.yaml new file mode 100644 index 0000000..ea831b2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/deployment.yaml @@ -0,0 +1,115 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: default + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: SERVICE_BUS_TOPIC_NAME + value: validated-cargo + - name: SERVICE_BUS_SUBSCRIPTION_NAME + value: invalid-cargo + - name: SERVICE_BUS_QUEUE_NAME + value: operation-state + - name: SERVICE_BUS_MAX_MESSAGE_COUNT + value: "1" + - name: SERVICE_BUS_MAX_WAIT_TIME + value: "5" + - name: COSMOS_DB_DATABASE_NAME + value: cargo + - name: COSMOS_DB_CONTAINER_NAME + value: invalid-cargo + - name: CLOUD_LOGGING_LEVEL + value: INFO + - name: CONSOLE_LOGGING_LEVEL + value: DEBUG + - name: HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS + value: "30" + - name: HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS + value: "60" + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: invalid-cargo-manager-secrets + key: AppInsightsConnectionString + - name: SERVICE_BUS_CONNECTION_STR + valueFrom: + secretKeyRef: + name: invalid-cargo-manager-secrets + key: ServiceBusConnectionString + - name: COSMOS_DB_ENDPOINT + valueFrom: + secretKeyRef: + name: invalid-cargo-manager-secrets + key: CosmosDBEndpoint + - name: COSMOS_DB_KEY + valueFrom: + secretKeyRef: + name: invalid-cargo-manager-secrets + key: CosmosDBKey + ports: + - name: http + containerPort: 8080 + protocol: TCP + # livenessProbe: + # httpGet: + # path: / + # port: http + # readinessProbe: + # httpGet: + # path: / + # port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "aks-sb-azmonitor-microservices.fullname" . }} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/hpa.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/hpa.yaml new file mode 100644 index 0000000..0a3ca97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/secretProviderClass.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/secretProviderClass.yaml new file mode 100644 index 0000000..8edb248 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/secretProviderClass.yaml @@ -0,0 +1,41 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: {{ .Values.aksKeyVaultSecretProviderIdentityId }} + keyvaultName: {{ .Values.keyVault.name }} + cloudName: "" + objects: | + array: + - | + objectName: AppInsightsConnectionString + objectType: secret + - | + objectName: ServiceBusConnectionString + objectType: secret + - | + objectName: CosmosDBEndpoint + objectType: secret + - | + objectName: CosmosDBKey + objectType: secret + tenantId: {{ .Values.keyVault.tenantId }} + secretObjects: + - data: + - key: AppInsightsConnectionString + objectName: AppInsightsConnectionString + - key: ServiceBusConnectionString + objectName: ServiceBusConnectionString + - key: CosmosDBEndpoint + objectName: CosmosDBEndpoint + - key: CosmosDBKey + objectName: CosmosDBKey + secretName: invalid-cargo-manager-secrets + type: Opaque \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/service.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/service.yaml new file mode 100644 index 0000000..af3f13a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 4 }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/tests/test-connection.yaml b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5eb4bc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "aks-sb-azmonitor-microservices.fullname" . }}-test-connection" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "aks-sb-azmonitor-microservices.fullname" . }}:80'] + restartPolicy: Never diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/requirements.txt b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/requirements.txt new file mode 100644 index 0000000..c8374b4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/requirements.txt @@ -0,0 +1,6 @@ +azure-servicebus +azure-cosmos +asyncio +opencensus-ext-azure +jsons +py-healthcheck diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app.py new file mode 100644 index 0000000..09e1970 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +"""Defines the applications processing loop +""" + +import asyncio +from app_context import ApplicationContext + +if __name__ == "__main__": + application_context = ApplicationContext() + + loop = asyncio.get_event_loop() + loop.run_until_complete(application_context.start()) diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_config.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_config.py new file mode 100644 index 0000000..f3a6b6f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_config.py @@ -0,0 +1,25 @@ +"""Centralized location for application setting to be loaded +""" +import os + +COSMOS_ENDPOINT = os.environ['COSMOS_DB_ENDPOINT'] +COSMOS_KEY = os.environ['COSMOS_DB_KEY'] +COSMOS_DATABASE_NAME = os.environ['COSMOS_DB_DATABASE_NAME'] +COSMOS_CONTAINER_NAME = os.environ['COSMOS_DB_CONTAINER_NAME'] + +LOGGING_APP_NAME = 'invalid-cargo-manager' +LOGGING_CLOUD_LOGGING_LEVEL = os.environ['CLOUD_LOGGING_LEVEL'].upper() +LOGGING_CONSOLE_LOGGING_LEVEL = os.environ['CONSOLE_LOGGING_LEVEL'].upper() + +SERVICE_BUS_CONNECTION_STR = os.environ['SERVICE_BUS_CONNECTION_STR'] +SERVICE_BUS_TOPIC_NAME = os.environ["SERVICE_BUS_TOPIC_NAME"] +SERVICE_BUS_QUEUE_NAME = os.environ["SERVICE_BUS_QUEUE_NAME"] +SERVICE_BUS_SUBSCRIPTION_NAME = os.environ["SERVICE_BUS_SUBSCRIPTION_NAME"] +SERVICE_BUS_MAX_MESSAGE_COUNT = int( + os.environ["SERVICE_BUS_MAX_MESSAGE_COUNT"]) +SERVICE_BUS_MAX_WAIT_TIME = int(os.environ["SERVICE_BUS_MAX_WAIT_TIME"]) + +HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS = int( + os.environ["HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS"]) +HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS = int( + os.environ["HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS"]) diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_context.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_context.py new file mode 100644 index 0000000..68c2f78 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/app_context.py @@ -0,0 +1,36 @@ +"""Contains the primary objects that make up the applications context +""" +from logging_config import logger +from state_processor import send_operation_state +from message_receiver import MessageReceiver +from cargo_repo import CargoRepo +from healthcheck import EnvironmentDump +from telemetry_publisher import TelemetryPublisher + +#pylint: disable=too-few-public-methods + + +class ApplicationContext: + """Class defining the context of the application + """ + + def __init__(self): + self._telemetry_publisher: TelemetryPublisher = TelemetryPublisher() + self._cargo_repo: CargoRepo = CargoRepo() + self._message_receiver: MessageReceiver = MessageReceiver( + telemetry_publisher=self._telemetry_publisher) + self._environment_dump = EnvironmentDump() + + async def start(self): + """Entry point for the application + """ + env_dump = self._environment_dump.run()[0] + logger.info("Environment Dump: %s", env_dump) + logger.info("Entering listening loop") + try: + while True: + await self._message_receiver.listen( + self._cargo_repo.store_cargo, send_operation_state) + except BaseException as err: # pylint: disable=broad-except + # Want to ensure the exception is logged on our way out + logger.exception(err) diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/cargo_repo.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/cargo_repo.py new file mode 100644 index 0000000..ea18377 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/cargo_repo.py @@ -0,0 +1,31 @@ +"""Class used to communicate with the Cosmos Db +""" + +import jsons +from logging_config import logger +from azure.cosmos import CosmosClient +from models import InvalidCargo +from app_config import COSMOS_CONTAINER_NAME, COSMOS_DATABASE_NAME, \ + COSMOS_ENDPOINT, COSMOS_KEY + +class CargoRepo: #pylint: disable=too-few-public-methods + """Class used to communicate with the Cosmos Db + """ + def __init__(self): + client = CosmosClient(COSMOS_ENDPOINT, COSMOS_KEY) + database = client.get_database_client(database=COSMOS_DATABASE_NAME) + self.container = database.get_container_client( + container=COSMOS_CONTAINER_NAME + ) + + def store_cargo(self, invalid_cargo: InvalidCargo): + """Store the cargo object provided in the cosmos db + + Parameters + ---------- + invalid_cargo : InvalidCargo + cargo object to store + """ + logger.info("Storing invalid cargo in database") + self.container.upsert_item(body=jsons.dump(invalid_cargo)) + \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/logging_config.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/logging_config.py new file mode 100644 index 0000000..996e5e8 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/logging_config.py @@ -0,0 +1,31 @@ +"""Creates the logger for the application +""" + +import logging +import sys +from opencensus.ext.azure.log_exporter import AzureLogHandler +from app_config import LOGGING_APP_NAME, LOGGING_CLOUD_LOGGING_LEVEL, \ + LOGGING_CONSOLE_LOGGING_LEVEL + +logger = logging.getLogger(LOGGING_APP_NAME) + # Set the root level for logging, no handler will be able to report anything lower than this value +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + +consoleHandler = logging.StreamHandler() +consoleHandler.setLevel(LOGGING_CONSOLE_LOGGING_LEVEL) +consoleHandler.setFormatter(formatter) +consoleHandler.setStream(sys.stdout) + +logger.addHandler(consoleHandler) + +def callback_add_role_name(envelope): + """ Callback function for opencensus """ + envelope.tags["ai.cloud.role"] = LOGGING_APP_NAME + return True + +azureHandler = AzureLogHandler() +azureHandler.setLevel(LOGGING_CLOUD_LOGGING_LEVEL) +azureHandler.add_telemetry_processor(callback_add_role_name) + +logger.addHandler(azureHandler) diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/message_receiver.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/message_receiver.py new file mode 100644 index 0000000..1cb1712 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/message_receiver.py @@ -0,0 +1,129 @@ +"""Used to listen for messages from the service bus topic containing +invalid cargo messages +""" +import time +from typing import Awaitable, Callable +from logging_config import logger +import jsons +from azure.servicebus.aio import ServiceBusClient +from models import MessageEnvelope, OperationState +from app_config import SERVICE_BUS_CONNECTION_STR, SERVICE_BUS_MAX_MESSAGE_COUNT, \ + SERVICE_BUS_MAX_WAIT_TIME, SERVICE_BUS_SUBSCRIPTION_NAME, SERVICE_BUS_TOPIC_NAME, \ + HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS, \ + HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS +from telemetry_publisher import TelemetryPublisher +from opencensus.trace.status import Status + +#pylint: disable=too-many-instance-attributes + +class MessageReceiver: + """Class used to receive messages from the service bus topic + """ + + def __init__(self, telemetry_publisher: TelemetryPublisher): + self._telemetry_publisher = telemetry_publisher + self._configure_service_bus_receiver() + self._max_message_count = SERVICE_BUS_MAX_MESSAGE_COUNT + self._max_wait_time = SERVICE_BUS_MAX_WAIT_TIME + self._last_peeked = 0 + self._degraded_threshold = HEALTH_CHECK_SERVICE_BUS_DEGRADED_THRESHOLD_SECONDS + self._unhealthy_threshold = HEALTH_CHECK_SERVICE_BUS_UNHEALTHY_THRESHOLD_SECONDS + + def _configure_service_bus_receiver(self): + logger.info('Creating service bus client') + servicebus_client = ServiceBusClient.from_connection_string( + conn_str=SERVICE_BUS_CONNECTION_STR) + logger.info('Creating receiver') + self._servicebus_receiver = servicebus_client.get_subscription_receiver( + topic_name=SERVICE_BUS_TOPIC_NAME, + subscription_name=SERVICE_BUS_SUBSCRIPTION_NAME + ) + + def _retrieve_value_from_message_application_properties(self, msg, key): + # Keys and values are byte strings + byte_key = key.encode() + byte_value = msg.application_properties[byte_key] + return byte_value.decode() + + async def listen( + self, message_processor: Callable, + state_publisher: Callable[[OperationState, str, str], Awaitable[None]]): + """Listens for messages from the service bus topic + + Parameters + ---------- + message_processor : Callable + Function to call when a message is received + state_publisher : Callable[[OperationState, str, str], Awaitable[None]] + Awaitable function to call to update operation state + """ + + logger.info('Retrieving messages') + + if not self.is_healthy(): + # We want to fail hard if the health check returns a false + raise Exception("Service is not healthy") + self._last_peeked = time.time() + received_msgs = await self._servicebus_receiver.receive_messages( + max_message_count=self._max_message_count, + max_wait_time=self._max_wait_time) + + for msg in received_msgs: + logger.info('Processing message') + # Pull operation and operation parent id from application properties on incoming message + diagnostic_id = self._retrieve_value_from_message_application_properties(msg, "Diagnostic-Id") + telemetry_operation_id = diagnostic_id.split("-")[1] + telemetry_operation_parent_id = diagnostic_id.split("-")[2] + tracer = self._telemetry_publisher.create_tracer(telemetry_operation_id, telemetry_operation_parent_id) + # Create request in application insights with parent dependency from cargo-processing-validator + with self._telemetry_publisher.create_process_message_request_span(tracer, "ServiceBusTopic.ProcessMessage", SERVICE_BUS_SUBSCRIPTION_NAME) as process_message_request: + try: + message = jsons.loads(str(msg), MessageEnvelope) + with self._telemetry_publisher.create_cosmos_db_store_dependency_span(tracer, "upsertItem.invalid-cargo") as cosmos_db_store_dependency: + message_processor(message.data) + with self._telemetry_publisher.create_operations_queue_send_dependency_span(tracer, "operations send") as operations_queue_send_dependency: + await state_publisher(OperationState( + operationId=message.operationId, + state="Succeeded", + result=message.data), + telemetry_operation_id, operations_queue_send_dependency.span_id) + with self._telemetry_publisher.create_validated_cargo_topic_dependency_span(tracer, "validated-cargo complete") as validated_cargo_topic_complete_dependency: + await self._servicebus_receiver.complete_message(msg) + except jsons.DecodeError as err: + with self._telemetry_publisher.create_validated_cargo_topic_dependency_span(tracer, "validated-cargo deadletter") as validated_cargo_topic_deadletter_dependency: + logger.exception(err) + # set request success field to false using open census status code mapping - https://opencensus.io/tracing/span/status/ + process_message_request.set_status(Status(code=3)) + await self._servicebus_receiver.dead_letter_message(message=msg, reason=str(err)) + # Can't update operation state if we can't be sure the message structure actually has an operationId + + def is_healthy(self) -> bool: + """Performs tests to determine if the message receiver is healthy + + Returns + ------- + bool + indicates if the MessageReceiver is healthy + """ + if self._last_peeked == 0: + logger.info("First pass of the messaging loop. So far so good.") + return True + + time_since_last_peek = time.time() - self._last_peeked + seconds_since_last_peek = str(round(time_since_last_peek, 2)) + + if time_since_last_peek > self._unhealthy_threshold: + logger.critical( + "Service bus hasn't peeked at messages for over %s seconds", + seconds_since_last_peek) + return False + + if time_since_last_peek > self._degraded_threshold: + logger.warning( + "Performance degraded: Service bus hasn't peeked at messages for over %s seconds", + seconds_since_last_peek) + return True + + logger.info( + "Message receiver is healthy. %s seconds since last peek.", seconds_since_last_peek) + return True diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/models.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/models.py new file mode 100644 index 0000000..510d8c8 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/models.py @@ -0,0 +1,59 @@ +"""Module containing the models used by the service implementation +""" + +#pylint: disable=too-few-public-methods +#pylint: disable=invalid-name +class Product: + """Defines the structure for products + """ + name: str + quantity: int + +class Port: + """Defines the structure for which ports are defined for cargo + """ + source: str + destination: str + +class DemandDates: + """Defines the structure for the demand dates of the cargo + """ + start: str + end: str + +class Cargo: + """Defines the structure of a cargo object""" + id: str + timestamp: str + product: Product + port: Port + demandDates: DemandDates + +class InvalidCargo(Cargo): + """Extends the cargo base class with information about why the cargo is invalid + + Parameters + ---------- + Cargo : _type_ + Base class being extended + """ + valid: bool + errorMessage: str + +class OperationState: + """Defines the state for operational state messages""" + result: Cargo + state: str + operationId: str + error: str + def __init__(self, operationId: str, state: str, result: Cargo=None, error: str=""): + self.operationId = operationId + self.state = state + self.result = result + self.error = error + +class MessageEnvelope: + """Defines the structure of the messages received from + the service bus topic""" + operationId: str + data: InvalidCargo diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/state_processor.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/state_processor.py new file mode 100644 index 0000000..1c7e931 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/state_processor.py @@ -0,0 +1,29 @@ +"""Used to send messages to the Operation State Queue""" + +import jsons +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus import ServiceBusMessage +from app_config import SERVICE_BUS_CONNECTION_STR, SERVICE_BUS_QUEUE_NAME +from logging_config import logger + +from models import OperationState + + +async def send_operation_state(operation_state: OperationState, telemetry_operation_id: str, telemetry_operation_parent_id: str): + """Send OperationState to the service bus queue defined in the app settings + + Parameters + ---------- + operation_state : OperationState + The Operation State to send + telemetry_operation_id : str + The operation id for the trace + telemetry_operation_parent_id : str + The id of the current operation in the trace + """ + servicebus_client = ServiceBusClient.from_connection_string(conn_str=SERVICE_BUS_CONNECTION_STR) + logger.info("Sending operation state message to % s queue" % SERVICE_BUS_QUEUE_NAME) + async with servicebus_client: + sender = servicebus_client.get_queue_sender(queue_name=SERVICE_BUS_QUEUE_NAME) + async with sender: + await sender.send_messages(ServiceBusMessage(jsons.dumps(operation_state), application_properties={"Diagnostic-Id": "00-{}-{}-01".format(telemetry_operation_id, telemetry_operation_parent_id)})) diff --git a/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/telemetry_publisher.py b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/telemetry_publisher.py new file mode 100644 index 0000000..894f790 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/invalid-cargo-manager/src/service/telemetry_publisher.py @@ -0,0 +1,71 @@ +"""Class used to create and publish telemetry entities +""" + +from app_config import LOGGING_APP_NAME +from opencensus.ext.azure.trace_exporter import AzureExporter +from opencensus.trace.samplers import AlwaysOnSampler +from opencensus.trace.tracer import Tracer +from opencensus.trace.span import SpanKind +from opencensus.trace.propagation.text_format import TextFormatPropagator + + +class TelemetryPublisher: #pylint: disable=too-few-public-methods + """Class used to create and publish telemetry entities + """ + + def callback_function(self, envelope): + envelope.tags['ai.cloud.role'] = LOGGING_APP_NAME + + # Check to see if the private.name attribute is set + # And use it to set the name field of the telemetry item + base_data = envelope.data['baseData'] + private_name = base_data['properties'].get('private.name') + if private_name: + # Remove the callback.name property from the telemetry item + base_data['properties']['private.name']=None + if base_data.name == '': + base_data.name = private_name + return True + + def create_tracer(self, telemetry_operation_id, telemetry_operation_parent_id): + # Create tracer with parent set to the incoming item from cargo-processing-validator + extracted_context = TextFormatPropagator().from_carrier(carrier={"opencensus-trace-traceid": telemetry_operation_id, "opencensus-trace-spanid": telemetry_operation_parent_id}) + app_insights_exporter=AzureExporter() + tracer = Tracer(exporter=app_insights_exporter, sampler=AlwaysOnSampler(), span_context=extracted_context) + app_insights_exporter.add_telemetry_processor(self.callback_function) + return tracer + + def create_process_message_request_span(self, tracer, span_name, subscription_name): + request_span = tracer.start_span(name=span_name) + # Setting span kind to server causes the span to generate a request + request_span.span_kind = SpanKind.SERVER + request_span.add_attribute("http.url", "sb://{}".format(subscription_name)) + # AzureExporter doesn't only sets the name field for HTTP spans + # Pass the span name as an attribute so that the callback function can set the name field + request_span.add_attribute("private.name", span_name) + return request_span + + def create_dependency_span(self, tracer, span_name): + dependency_span = tracer.start_span(name=span_name) + # Setting span kind to client causes the span to generate a dependency + dependency_span.span_kind = SpanKind.CLIENT + return dependency_span + + def create_operations_queue_send_dependency_span(self, tracer, span_name): + dependency_span = self.create_dependency_span(tracer, span_name) + # Set dependency type property + dependency_span.add_attribute("component", "Queue Message | servicebus") + # Set dependency target property (xxx:///xx) + dependency_span.add_attribute("http.url", "sb://operations") + return dependency_span + + def create_validated_cargo_topic_dependency_span(self, tracer, span_name): + dependency_span = self.create_dependency_span(tracer, span_name) + dependency_span.add_attribute("component", "Azure Service Bus") + dependency_span.add_attribute("http.url", "sb://validated-cargo") + return dependency_span + + def create_cosmos_db_store_dependency_span(self, tracer, span_name): + dependency_span = self.create_dependency_span(tracer, span_name) + dependency_span.add_attribute("component", "Azure DocumentDB") + return dependency_span \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/Dockerfile new file mode 100644 index 0000000..32bfefa --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/java/.devcontainer/base.Dockerfile + +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster +ARG VARIANT="17-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="false" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/devcontainer.json new file mode 100644 index 0000000..583e6d2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/java +{ + "name": "Java", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Java version: 11, 17 + // Append -bullseye or -buster to pin to an OS version. + // Use the -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "17-bullseye", + // Options + "INSTALL_MAVEN": "true", + "INSTALL_GRADLE": "false", + "NODE_VERSION": "lts/*" + } + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.jdt.ls.lombokSupport.enabled": true + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "vscjava.vscode-java-pack", + "redhat.fabric8-analytics" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.dockerignore b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.dockerignore new file mode 100644 index 0000000..2ce5e1c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.dockerignore @@ -0,0 +1,2 @@ +.env +helm diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.env.sample b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.env.sample new file mode 100644 index 0000000..74909f6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.env.sample @@ -0,0 +1,13 @@ +APPLICATIONINSIGHTS_CONNECTION_STRING= +APPLICATIONINSIGHTS_VERSION=3.4.7 + +# Service Bus Information +SERVICEBUS_CONNECTION_STRING= +SERVICEBUS_PREFETCH_COUNT=10 +OPERATION_STATE_QUEUE_NAME=cargo-operations + +# Cosmos Db Information +COSMOS_DB_ENDPOINT= +COSMOS_DB_KEY= +COSMOS_DB_DATABASE_NAME= +COSMOS_DB_CONTAINER_NAME= diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.gitignore new file mode 100644 index 0000000..8977a26 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.gitignore @@ -0,0 +1,3 @@ +target + +.env \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.jar b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..c1dd12f Binary files /dev/null and b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.jar differ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.properties b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b74bf7f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/launch.json b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/launch.json new file mode 100644 index 0000000..d118949 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Launch Current File", + "request": "launch", + "mainClass": "${file}", + "envFile": "${workspaceFolder}/.env" + }, + { + "type": "java", + "name": "Launch Application", + "request": "launch", + "mainClass": "com.microsoft.cse.cargoprocessing.operations.api.Application", + "projectName": "operations.api", + "vmArgs": "-javaagent:${workspaceFolder}/target/dependency/applicationinsights-agent-3.4.7.jar", + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/settings.json b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/tasks.json b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/tasks.json new file mode 100644 index 0000000..b681057 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "verify", + "type": "shell", + "command": "mvn -B verify", + "group": "build" + }, + { + "label": "test", + "type": "shell", + "command": "mvn -B test", + "group": "test" + } + ] +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/Dockerfile new file mode 100644 index 0000000..1487559 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/Dockerfile @@ -0,0 +1,27 @@ +FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu as base + + +FROM maven:3.8.5-openjdk-17-slim as build +WORKDIR /src + +RUN mvn -version + +COPY pom.xml . +RUN mvn -B dependency:resolve-plugins dependency:resolve +# RUN mvn -B dependency:go-offline + +COPY . . +RUN mvn package + +RUN ls -al target +RUN ls -al target/dependency + +FROM base as final +COPY applicationinsights.json applicationinsights.json + +ARG JAR_FILE=/src/target/*.jar +ARG DEPENDENCY=/src/target/dependency +COPY --from=build ${DEPENDENCY}/applicationinsights-agent-3.4.7.jar applicationinsights-agent-3.4.7.jar +COPY --from=build ${JAR_FILE} app.jar + +ENTRYPOINT ["java", "-javaagent:applicationinsights-agent-3.4.7.jar" ,"-jar","/app.jar" ] diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/README.md b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/README.md new file mode 100644 index 0000000..37fde20 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/README.md @@ -0,0 +1,24 @@ +# Running the service + +## Pre-Requisites + +1. Service Bus [namespace](https://docs.microsoft.com/en-us/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create) with [queue](https://docs.microsoft.com/en-us/cli/azure/servicebus/queue?view=azure-cli-latest#az-servicebus-queue-create) +1. Application Insights [account](https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource#azure-cli-preview) + +## Debugging from VSCode Dev Container + +* Open the project in the dev container. + * Make sure to open in the devcontainer + * Ignore the alerts for Java on the initial load. The alerts move faster than the dev container builds. + * If you see an alert for Lombok asking to reload, please do reload. +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Build the Build task 2 options: + * From the command pallet `Tasks: Run Build Task` + * From the terminal `mvn -B verify` +* Configure debugger to use the "Launch Application" configuration. +* Run the Debugger. + +## Docker Container + +* Rename `.env.sample` to `.env` and add connection strings for Service Bus and Application Insights. +* Run `docker compose up` to run the service. \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/applicationinsights.json b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/applicationinsights.json new file mode 100644 index 0000000..35d4544 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/applicationinsights.json @@ -0,0 +1,17 @@ +{ + "role": { + "name": "operations-api" + }, + "instrumentation": { + "logging": { + "level": "INFO" + } + }, + "preview": { + "instrumentation": { + "springIntegration": { + "enabled": true + } + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/docker-compose.yml new file mode 100644 index 0000000..a973747 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + operations-api: + env_file: + - .env + build: + context: . + dockerfile: Dockerfile + ports: + - "8081:8081" diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/.helmignore b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/Chart.yaml new file mode 100644 index 0000000..b39af23 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 + +name: operations-api +description: operations-api for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/operations-api.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/operations-api.yaml new file mode 100644 index 0000000..6389407 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/operations-api.yaml @@ -0,0 +1,34 @@ +image: + pullPolicy: Always + tag: "latest" + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + +# When running one of the deploy-*.sh scripts, an additional env.yaml +# values file is created containing values specific to the deployed environment +# with the following values: +# image: +# repository: + +# keyVault: +# name: +# tenantId: + +# aksKeyVaultSecretProviderIdentityId: diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/NOTES.txt new file mode 100644 index 0000000..d9dd7b7 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: +export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aks-sb-azmonitor-microservices.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") +export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") +echo "Visit http://127.0.0.1:8080 to use your application" +kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/_helpers.tpl new file mode 100644 index 0000000..0172cb3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aks-sb-azmonitor-microservices.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aks-sb-azmonitor-microservices.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/deployment.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/deployment.yaml new file mode 100644 index 0000000..a9baa12 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/deployment.yaml @@ -0,0 +1,111 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: default + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: APPLICATIONINSIGHTS_VERSION + value: 3.4.7 + - name: OPERATION_STATE_QUEUE_NAME + value: operation-state + - name: COSMOS_DB_DATABASE_NAME + value: cargo + - name: COSMOS_DB_CONTAINER_NAME + value: operations + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: operations-api-secrets + key: AppInsightsConnectionString + - name: SERVICEBUS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: operations-api-secrets + key: ServiceBusConnectionString + - name: SERVICEBUS_PREFETCH_COUNT + value: "10" + - name: COSMOS_DB_ENDPOINT + valueFrom: + secretKeyRef: + name: operations-api-secrets + key: CosmosDBEndpoint + - name: COSMOS_DB_KEY + valueFrom: + secretKeyRef: + name: operations-api-secrets + key: CosmosDBKey + ports: + - name: http + containerPort: 8081 + protocol: TCP + livenessProbe: + httpGet: + path: /actuator/health + port: 8081 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 10 + + startupProbe: + httpGet: + path: /actuator/health + port: 8081 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "aks-sb-azmonitor-microservices.fullname" . }} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/hpa.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/hpa.yaml new file mode 100644 index 0000000..0a3ca97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/secretProviderClass.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/secretProviderClass.yaml new file mode 100644 index 0000000..0057293 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/secretProviderClass.yaml @@ -0,0 +1,41 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: {{ .Values.aksKeyVaultSecretProviderIdentityId }} + keyvaultName: {{ .Values.keyVault.name }} + cloudName: "" + objects: | + array: + - | + objectName: AppInsightsConnectionString + objectType: secret + - | + objectName: ServiceBusConnectionString + objectType: secret + - | + objectName: CosmosDBEndpoint + objectType: secret + - | + objectName: CosmosDBKey + objectType: secret + tenantId: {{ .Values.keyVault.tenantId }} + secretObjects: + - data: + - objectName: AppInsightsConnectionString + key: AppInsightsConnectionString + - objectName: ServiceBusConnectionString + key: ServiceBusConnectionString + - objectName: CosmosDBEndpoint + key: CosmosDBEndpoint + - objectName: CosmosDBKey + key: CosmosDBKey + secretName: operations-api-secrets + type: Opaque \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/service.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/service.yaml new file mode 100644 index 0000000..af3f13a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 4 }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/tests/test-connection.yaml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5eb4bc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "aks-sb-azmonitor-microservices.fullname" . }}-test-connection" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "aks-sb-azmonitor-microservices.fullname" . }}:80'] + restartPolicy: Never diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw.cmd b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/pom.xml b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/pom.xml new file mode 100644 index 0000000..50d2851 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.3 + + + com.microsoft.cse + operations.api + 0.0.1-SNAPSHOT + operations-api + API used to maintain the state of the cargo processing operations + + 17 + 3.12.0 + 2.7.3 + 3.4.7 + 3.4.7 + LATEST + 7.13.0 + 1.0.71 + 2.11.0 + 3.0.0-M5 + 3.3.0 + 4.4.0 + true + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.azure + azure-messaging-servicebus + ${servicebus.version} + + + + com.azure.spring + spring-cloud-azure-starter-actuator + + + + com.azure.spring + spring-cloud-azure-starter-data-cosmos + + + + org.apache.commons + commons-lang3 + ${commons.lang.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + com.networknt + json-schema-validator + ${json.schema.validation.version} + + + + commons-io + commons-io + ${commons.io.version} + test + + + + io.opentelemetry + opentelemetry-api + + + + com.microsoft.azure + applicationinsights-web + ${applicationinsights.web.version} + + + + com.microsoft.azure + applicationinsights-agent + ${applicationinsights.agent.version} + + + + + + + io.opentelemetry + opentelemetry-bom + 1.22.0 + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + com.azure.spring + spring-cloud-azure-dependencies + ${spring.cloud.azure.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin.version} + + + + copy + compile + + copy + + + + + com.microsoft.azure + applicationinsights-agent + ${applicationinsights.agent.version} + applicationinsights-agent-${applicationinsights.agent.version}.jar + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipITs} + + + + + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/Application.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/Application.java new file mode 100644 index 0000000..00f3bae --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/Application.java @@ -0,0 +1,29 @@ +package com.microsoft.cse.cargoprocessing.operations.api; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +import com.microsoft.cse.cargoprocessing.operations.api.services.StateProcessor; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @EventListener + @Async + public void StartListening(ApplicationReadyEvent event) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + StateProcessor runnable = event.getApplicationContext().getBean(StateProcessor.class); + executor.execute(runnable); + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/Error.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/Error.java new file mode 100644 index 0000000..0170250 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/Error.java @@ -0,0 +1,14 @@ +package com.microsoft.cse.cargoprocessing.operations.api.ExceptionHandling; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Error implements Serializable { + private ErrorDetail error; + + public Error(String code, String message, String target, InnerError innerError){ + error = new ErrorDetail(code, message, target, innerError); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorCodes.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorCodes.java new file mode 100644 index 0000000..dc516a9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorCodes.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.operations.api.ExceptionHandling; + +public class ErrorCodes { + private ErrorCodes() { throw new IllegalStateException("Utility class, should not be constructed"); } + + public static final String INVALID_JSON = "InvalidJson"; + public static final String FAILS_SCHEMA_VALIDATION = "InvalidJson-SchemaValidationFailure"; + public static final String FAILS_SERIALIZATION = "InvalidJson-UnableToSerialize"; + + public static final String INTERNAL_SERVER_ERROR = "InternalServerError"; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorDetail.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorDetail.java new file mode 100644 index 0000000..9f3da9c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ErrorDetail.java @@ -0,0 +1,20 @@ +package com.microsoft.cse.cargoprocessing.operations.api.ExceptionHandling; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class ErrorDetail implements Serializable { + private String code; + private String message; + private String target; + private InnerError innerError; + + public ErrorDetail(String code, String message, String target, InnerError innerError){ + this.code = code; + this.innerError = innerError; + this.target = target; + this.message = message; + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ExceptionAdvisor.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ExceptionAdvisor.java new file mode 100644 index 0000000..de24266 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/ExceptionAdvisor.java @@ -0,0 +1,32 @@ +package com.microsoft.cse.cargoprocessing.operations.api.ExceptionHandling; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ControllerAdvice +public class ExceptionAdvisor extends ResponseEntityExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvisor.class); + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleDefaultExceptions( + Exception ex, + WebRequest request) { + logger.error(ex.getMessage(), ex); + + Error error = new Error(ErrorCodes.INTERNAL_SERVER_ERROR, + "Internal server error. Please see service logs for more information", + request.getDescription(false), null); + + return new ResponseEntity<>(error, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/InnerError.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/InnerError.java new file mode 100644 index 0000000..cc613e5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/ExceptionHandling/InnerError.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.operations.api.ExceptionHandling; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class InnerError implements Serializable { + private String code; + private String message; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosConfiguration.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosConfiguration.java new file mode 100644 index 0000000..483f992 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosConfiguration.java @@ -0,0 +1,42 @@ +package com.microsoft.cse.cargoprocessing.operations.api.configuration; + +import com.azure.cosmos.CosmosClientBuilder; + +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.repository.config.EnableCosmosRepositories; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + + +@Configuration +@EnableConfigurationProperties(CosmosProperties.class) +@EnableCosmosRepositories(basePackages = "com.microsoft.cse.cargoprocessing.operations.api") +@PropertySource("classpath:application.properties") +public class CosmosConfiguration extends AbstractCosmosConfiguration { + @Autowired + private CosmosProperties properties; + + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + return new CosmosClientBuilder() + .endpoint(properties.getUri()) + .key(properties.getKey()); + } + + @Bean + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(true) + .build(); + } + + @Override + protected String getDatabaseName() { + return properties.getDbName(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosProperties.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosProperties.java new file mode 100644 index 0000000..379d43e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/CosmosProperties.java @@ -0,0 +1,19 @@ +package com.microsoft.cse.cargoprocessing.operations.api.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Data; + +@ConfigurationProperties +@Data +public class CosmosProperties { + @Value("${COSMOS_DB_ENDPOINT:defaultValue}") + private String uri; + + @Value("${COSMOS_DB_KEY:defaultValue}") + private String key; + + @Value("${COSMOS_DB_DATABASE_NAME:defaultValue}") + private String dbName; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/ServiceBusProperties.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/ServiceBusProperties.java new file mode 100644 index 0000000..6c8bbf5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/configuration/ServiceBusProperties.java @@ -0,0 +1,19 @@ +package com.microsoft.cse.cargoprocessing.operations.api.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Data; + +@Data +@ConfigurationProperties +@Component +public class ServiceBusProperties { + @Value("${accelerator.queue-name:defaultValue}") + private String queueName; + @Value("${servicebus.connection-string:defaultValue}") + private String connectionString; + @Value("${servicebus.prefetch-count:defaultValue}") + private int prefetchCount; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/OperationController.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/OperationController.java new file mode 100644 index 0000000..d3b5837 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/OperationController.java @@ -0,0 +1,10 @@ +package com.microsoft.cse.cargoprocessing.operations.api.controllers; + +import org.springframework.http.ResponseEntity; + +import com.microsoft.cse.cargoprocessing.operations.api.models.Operation; + +public interface OperationController { + ResponseEntity getOperation(String operationId); + ResponseEntity putOperation(String operationId); +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/impl/HttpOperationController.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/impl/HttpOperationController.java new file mode 100644 index 0000000..b5af998 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/controllers/impl/HttpOperationController.java @@ -0,0 +1,58 @@ +package com.microsoft.cse.cargoprocessing.operations.api.controllers.impl; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.azure.cosmos.models.PartitionKey; +import com.microsoft.cse.cargoprocessing.operations.api.controllers.OperationController; +import com.microsoft.cse.cargoprocessing.operations.api.models.Operation; +import com.microsoft.cse.cargoprocessing.operations.api.repositories.OperationRepo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RestController +@RequestMapping("operations") +public class HttpOperationController implements OperationController { + @Autowired + private OperationRepo repo; + + private final Logger logger = LoggerFactory.getLogger(HttpOperationController.class); + + @Override + @GetMapping("/{operationId}") + public ResponseEntity getOperation(@PathVariable String operationId) { + Optional operation = repo.findById(operationId, new PartitionKey(operationId)); + + if (operation.isPresent()) { + logger.info("Operation {} was found in database.", operationId); + return new ResponseEntity<>(operation.get(), HttpStatus.OK); + } + + logger.info("Operation {} was not found in database.", operationId); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @Override + @PutMapping("/{operationId}") + public ResponseEntity putOperation(@PathVariable String operationId) { + Operation operation = new Operation(operationId); + Optional stored = repo.findById(operation.getId(), new PartitionKey(operation.getId())); + + if (!stored.isPresent()) { + logger.info("Operation {} was not found in database. Saving now", operation.getId()); + repo.save(operation); + return new ResponseEntity<>(operation, HttpStatus.CREATED); + } + + return new ResponseEntity<>(stored.get(), HttpStatus.OK); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Cargo.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Cargo.java new file mode 100644 index 0000000..98a2626 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Cargo.java @@ -0,0 +1,20 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import java.io.Serializable; +import java.sql.Timestamp; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +@Data +public class Cargo implements Serializable { + private String id; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Timestamp timestamp; + private Product product; + private Port port; + private DemandDates demandDates; + private Boolean valid; + private String errorMessage; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/DemandDates.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/DemandDates.java new file mode 100644 index 0000000..1bbd91c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/DemandDates.java @@ -0,0 +1,16 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import java.io.Serializable; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +@Data +public class DemandDates implements Serializable { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Date start; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Date end; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Operation.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Operation.java new file mode 100644 index 0000000..c7a598a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Operation.java @@ -0,0 +1,33 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +import org.springframework.data.annotation.Id; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import com.fasterxml.jackson.annotation.JsonFormat; + +@Data +@Container(containerName = "operations", autoCreateContainer = false) +@NoArgsConstructor +public class Operation implements Serializable { + @PartitionKey + @Id + private String id; + private String state; + private Cargo result; + private String error; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") + private Date updatedAt; + + public Operation(String operationId) { + id = operationId; + state = "New"; + updatedAt = new Date(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/OperationState.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/OperationState.java new file mode 100644 index 0000000..d3fba9d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/OperationState.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class OperationState implements Serializable { + private String operationId; + private String state; + private Cargo result; + private String error; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Port.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Port.java new file mode 100644 index 0000000..215782b --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Port.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Port implements Serializable { + private String source; + private String destination; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Product.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Product.java new file mode 100644 index 0000000..48a5726 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/models/Product.java @@ -0,0 +1,11 @@ +package com.microsoft.cse.cargoprocessing.operations.api.models; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class Product implements Serializable { + private String name; + private int quantity; +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/repositories/OperationRepo.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/repositories/OperationRepo.java new file mode 100644 index 0000000..48cee57 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/repositories/OperationRepo.java @@ -0,0 +1,10 @@ +package com.microsoft.cse.cargoprocessing.operations.api.repositories; + +import com.microsoft.cse.cargoprocessing.operations.api.models.Operation; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OperationRepo extends CosmosRepository { + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/services/StateProcessor.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/services/StateProcessor.java new file mode 100644 index 0000000..5fdfce9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/java/com/microsoft/cse/cargoprocessing/operations/api/services/StateProcessor.java @@ -0,0 +1,154 @@ +package com.microsoft.cse.cargoprocessing.operations.api.services; + +import java.util.Date; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.messaging.servicebus.ServiceBusClientBuilder; +import com.azure.messaging.servicebus.ServiceBusErrorContext; +import com.azure.messaging.servicebus.ServiceBusException; +import com.azure.messaging.servicebus.ServiceBusFailureReason; +import com.azure.messaging.servicebus.ServiceBusProcessorClient; +import com.azure.messaging.servicebus.ServiceBusReceivedMessage; +import com.azure.messaging.servicebus.ServiceBusReceivedMessageContext; +import com.azure.messaging.servicebus.models.ServiceBusReceiveMode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.cse.cargoprocessing.operations.api.configuration.ServiceBusProperties; +import com.microsoft.cse.cargoprocessing.operations.api.models.Operation; +import com.microsoft.cse.cargoprocessing.operations.api.models.OperationState; +import com.microsoft.cse.cargoprocessing.operations.api.repositories.OperationRepo; +import io.opentelemetry.api.trace.Span; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import lombok.SneakyThrows; + +@Component +@Scope("application") +public class StateProcessor implements Runnable { + @Autowired + private OperationRepo repo; + @Autowired + ServiceBusProperties serviceBusProperties; + + private final Logger logger = LoggerFactory.getLogger(StateProcessor.class); + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public void run() { + try { + CountDownLatch countdownLatch = new CountDownLatch(1); + + ServiceBusProcessorClient processor = new ServiceBusClientBuilder() + .connectionString(serviceBusProperties.getConnectionString()) + .processor() + .queueName(serviceBusProperties.getQueueName()) + .disableAutoComplete() + .prefetchCount(serviceBusProperties.getPrefetchCount()) + .receiveMode(ServiceBusReceiveMode.PEEK_LOCK) + .processMessage(context -> processMessage(context)) + .processError(context -> processError(context, countdownLatch)) + .buildProcessorClient(); + + processor.start(); + logger.info("Stopped processing operation state messages"); + } catch (Exception e) { + logger.error("Error while reading from the operation state service bus", e); + } + } + + @SneakyThrows + private void processMessage(ServiceBusReceivedMessageContext context) { + ServiceBusReceivedMessage message = context.getMessage(); + OperationState state = mapper.readValue(message.getBody().toBytes(), OperationState.class); + Optional result = repo.findById(state.getOperationId(), + new PartitionKey(state.getOperationId())); + + Operation operation = null; + + if (result.isPresent()) { + operation = result.get(); + } else { + operation = new Operation(state.getOperationId()); + } + + if (validStateTransition(operation.getState(), state.getState())) { + Date messageEnqueue = Date.from(context.getMessage().getEnqueuedTime().toInstant()); + logger.info("Updating state for operation id: {} from {} to {}", state.getOperationId(), + operation.getState(), state.getState()); + operation.setState(state.getState()); + operation.setResult(state.getResult()); + operation.setUpdatedAt(messageEnqueue); + operation.setError(state.getError()); + + Span span = Span.current(); + span.setAttribute("operation-state", operation.getState()); + + repo.save(operation); + context.complete(); + } else { + logger.info( + "State transition for Operation Id {} from {} to {} is invalid, putting message back enqueue for processing.", + state.getOperationId(), operation.getState(), state.getState()); + + context.abandon(); + } + } + + private boolean validStateTransition(String from, String to) { + logger.info("Validating state transition from {} to {}", from, to); + + if (from.equals("New") && to.equals("CargoValidated")) { + return true; + } + if (from.equals("CargoValidated") && to.equals("Succeeded")) { + return true; + } + if (to.equals("Failed")) { + return true; + } + + return false; + } + + private void processError(ServiceBusErrorContext context, CountDownLatch countdownLatch) { + logger.error("Error when receiving messages from namespace: '%s'. Entity: '%s'%n", + context.getFullyQualifiedNamespace(), context.getEntityPath()); + + if (!(context.getException() instanceof ServiceBusException)) { + logger.error("Non-ServiceBusException occurred: %s%n", context.getException()); + return; + } + + ServiceBusException exception = (ServiceBusException) context.getException(); + ServiceBusFailureReason reason = exception.getReason(); + + if (reason == ServiceBusFailureReason.MESSAGING_ENTITY_DISABLED + || reason == ServiceBusFailureReason.MESSAGING_ENTITY_NOT_FOUND + || reason == ServiceBusFailureReason.UNAUTHORIZED) { + logger.error("An unrecoverable error occurred. Stopping processing with reason %s: %s%n", + reason, exception.getMessage()); + + countdownLatch.countDown(); + } else if (reason == ServiceBusFailureReason.MESSAGE_LOCK_LOST) { + logger.error("Message lock lost for message: %s%n", context.getException()); + } else if (reason == ServiceBusFailureReason.SERVICE_BUSY) { + try { + // Choosing an arbitrary amount of time to wait until trying again. + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + logger.error("Unable to sleep for period of time"); + } + } else { + logger.error("Error source %s, reason %s, message: %s%n", context.getErrorSource(), + reason, context.getException()); + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/resources/application.properties b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/resources/application.properties new file mode 100644 index 0000000..42cd8f6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/main/resources/application.properties @@ -0,0 +1,19 @@ +APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING} +APPLICATIONINSIGHTS_VERSION=${APPLICATIONINSIGHTS_VERSION} + +# Service Bus Information +servicebus.connection-string=${servicebus_connection_string} +servicebus.prefetch-count=${servicebus_prefetch_count} +accelerator.queue-name=${operation_state_queue_name} + +# Cosmos Db Information +spring.cloud.azure.cosmos.endpoint=${COSMOS_DB_ENDPOINT} +spring.cloud.azure.cosmos.key=${COSMOS_DB_KEY} +spring.cloud.azure.cosmos.database=${COSMOS_DB_DATABASE_NAME} + +server.port=8081 + +management.endpoints.web.exposure.include=health,info +endpoints.health.sensitive=false +management.endpoint.health.show-details=always +management.health.azure-cosmos.enabled=true diff --git a/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/test/java/com/microsoft/cse/cargoprocessing/operations/api/ApplicationTestsIT.java b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/test/java/com/microsoft/cse/cargoprocessing/operations/api/ApplicationTestsIT.java new file mode 100644 index 0000000..72db9a5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/operations-api/src/test/java/com/microsoft/cse/cargoprocessing/operations/api/ApplicationTestsIT.java @@ -0,0 +1,13 @@ +package com.microsoft.cse.cargoprocessing.operations.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTestsIT { + + @Test + void contextLoads() { + } + +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/solution/README.md b/accelerators/aks-sb-azmonitor-microservices/src/solution/README.md new file mode 100644 index 0000000..a5720f9 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/solution/README.md @@ -0,0 +1,5 @@ +# README + +This folder contains a helm chart for solution-level components. + +Currently, this deploys the ingress to provide access to the cargo-processing-api and operations-api. diff --git a/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/Chart.yaml new file mode 100644 index 0000000..a84495c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: aks-sb-azmonitor-microservices +description: solution-level components for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/NOTES.txt new file mode 100644 index 0000000..036956a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: + + export INGRESS_IP=$(kubectl get ingress --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aks-sb-azmonitor-microservices.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}') + + curl http://$INGRESS_IP/ \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/_helpers.tpl new file mode 100644 index 0000000..0172cb3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aks-sb-azmonitor-microservices.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aks-sb-azmonitor-microservices.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/ingress.yaml b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/ingress.yaml new file mode 100644 index 0000000..15732ab --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/solution/helm/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- $fullName := include "aks-sb-azmonitor-microservices.fullname" . -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /cargo + pathType: Prefix + backend: + service: + name: cargo-processing-api + port: + number: 80 + - path: /actuator/health + pathType: Prefix + backend: + service: + name: cargo-processing-api + port: + number: 80 + - path: /operations + pathType: Prefix + backend: + service: + name: operations-api + port: + number: 80 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/Dockerfile new file mode 100644 index 0000000..efa6649 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +ARG VARIANT="6.0" +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} + +ARG USERNAME=vscode + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install telnet -y diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/devcontainer.json b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/devcontainer.json new file mode 100644 index 0000000..94e90bb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "valid-cargo-manager", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "6.0" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp" + ], + "settings": { + "editor.formatOnType": true, + "editor.formatOnSave": true + } + } + }, + "remoteUser": "vscode" +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.dockerignore b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.dockerignore new file mode 100644 index 0000000..ebe6989 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.dockerignore @@ -0,0 +1,28 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md + +.env +helm diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.gitignore b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.gitignore new file mode 100644 index 0000000..d2e6b7d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/.gitignore @@ -0,0 +1,38 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +appsettings.json +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Controllers/HealthCheckController.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Controllers/HealthCheckController.cs new file mode 100644 index 0000000..9bc8bb6 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Controllers/HealthCheckController.cs @@ -0,0 +1,81 @@ +namespace ValidCargoProcessor.Services +{ + using System.Net.Sockets; + using System.Threading; + using System.Text.Json; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class HealthCheckController : IHostedService + { + private readonly HealthCheckService _healthCheckService; + private readonly ILogger _logger; + private readonly TcpListener _tcpServer; + + public HealthCheckController( + TcpListener tcpServer, + HealthCheckService healthCheckService, + IConfiguration configuration, + ILogger logger) + { + _healthCheckService = healthCheckService; + _logger = logger; + _tcpServer = tcpServer; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Starting Tcp Health Check Listener at {}", _tcpServer.LocalEndpoint); + _tcpServer.Start(); + while (!cancellationToken.IsCancellationRequested) + { + _logger.LogInformation("Performing health check"); + HealthReport report = await _healthCheckService.CheckHealthAsync().ConfigureAwait(false); + + if (report == null || report.Status == HealthStatus.Unhealthy) + { + // default log level for service is set to Debug in appsettings.json + _logger.LogDebug("Service is unhealthy, stopping health service", report); + _tcpServer.Stop(); + return; + } + + await ReportHealthCheckResult(report); + } + } + catch (SocketException e) + { + _logger.LogError("Tcp Socket Exception", e); + } + finally + { + _tcpServer.Stop(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Stopping Tcp Listener"); + return Task.FromResult(_tcpServer.Stop); + } + + private async Task ReportHealthCheckResult(HealthReport report) + { + TcpClient client = await _tcpServer.AcceptTcpClientAsync().ConfigureAwait(false); + try + { + NetworkStream stream = client.GetStream(); + String output = JsonSerializer.Serialize(report); + Byte[] results = System.Text.Encoding.UTF8.GetBytes(output); + await stream.WriteAsync(results); + client.Close(); + } + catch (Exception ex) + { + _logger.LogError("Exception occurred while attempting to provide health report", ex); + client.Close(); + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Dockerfile b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Dockerfile new file mode 100644 index 0000000..a27168e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Dockerfile @@ -0,0 +1,20 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["valid-cargo-manager.csproj", "."] +RUN dotnet restore "./valid-cargo-manager.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "valid-cargo-manager.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "valid-cargo-manager.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "valid-cargo-manager.dll"] \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/CosmosDbHealthChecker.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/CosmosDbHealthChecker.cs new file mode 100644 index 0000000..4df2f4c --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/CosmosDbHealthChecker.cs @@ -0,0 +1,152 @@ +namespace ValidCargoProcessor.HealthCheck +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos; + using ValidCargoProcessor.HealthCheck.Models; + using Microsoft.Extensions.Diagnostics.HealthChecks; + public class CosmosDbHealthChecker : IHealthCheck + { + private readonly CosmosClient _cosmosClient; + private readonly IConfiguration _configuration; + private readonly string _description = "CosmosDb:HealthCheck"; + private readonly ILogger _logger; + private readonly int _maxDurationMs; + public CosmosDbHealthChecker( + CosmosClient cosmosClient, IConfiguration configuration, ILogger logger) + { + _cosmosClient = cosmosClient; + _configuration = configuration; + _logger = logger; + _maxDurationMs = int.Parse(configuration["HealthCheck:CosmosDb:MaxDurationMs"]); + } + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + Dictionary data = new(); + + try + { + data.Add("CosmosDb:Account", await this.CheckClientAccountAsync().ConfigureAwait(false)); + + var database = _cosmosClient.GetDatabase(_configuration["CosmosDb:Database"]); + data.Add("CosmosDb:Database", await CheckDatabaseAsync(database, cancellationToken).ConfigureAwait(false)); + + var container = database.GetContainer(_configuration["CosmosDb:Container"]); + data.Add("CosmosDb:Container", await CheckContainerAsync(container, cancellationToken).ConfigureAwait(false)); + + return ToHealthCheckResult(data); + } + catch (CosmosException ce) + { + // log and return Unhealthy + _logger.LogError($"CosmosException:Healthz:{ce.StatusCode}:{ce.ActivityId}:{ce.Message}"); + + data.Add("CosmosException", ce.Message); + + return new HealthCheckResult(HealthStatus.Unhealthy, _description, ce, data); + } + catch (Exception ex) + { + // log and return unhealthy + _logger.LogError($"Exception:Health:{ex.Message}"); + + data.Add("Exception", ex.Message); + + return new HealthCheckResult(HealthStatus.Unhealthy, _description, ex, data); + } + } + + private HealthCheckResult ToHealthCheckResult(Dictionary data) + { + _logger.LogDebug("Converting data to HealthCheckResult"); + HealthStatus status = HealthStatus.Healthy; + + //Make sure we're reporting the least healthy result that was observed + foreach (object d in data.Values) + { + if (d is HealthCheck h && h.Status != HealthStatus.Healthy) + { + status = h.Status; + } + + if (status == HealthStatus.Unhealthy) + { + break; + } + } + + return new HealthCheckResult(status, this._description, data: data); + } + + private async Task CheckClientAccountAsync() + { + _logger.LogDebug("Checking that we can read the cosmos db account information"); + HealthCheckResultBuilder builder = new HealthCheckResultBuilder() + .ComponentId("CosmosDb") + .ComponentType("Data Store") + .Endpoint(this._configuration["CosmosDb:EndpointUri"]) + .TargetDurationMs(this._maxDurationMs); + + try + { + var account = await _cosmosClient.ReadAccountAsync().ConfigureAwait(false); + + return builder.build(); + } + catch (CosmosException ce) + { + _logger.LogError($"CosmosException:Health:{ce.StatusCode}:{ce.ActivityId}:{ce.Message}"); + builder.Exception(ce); + return builder.build(); + } + } + + private async Task CheckDatabaseAsync(Database database, CancellationToken cancellationToken = default) + { + _logger.LogDebug("Checking that we can read the cosmos db database information"); + HealthCheckResultBuilder builder = new HealthCheckResultBuilder() + .ComponentId("CosmosDb") + .ComponentType("Data Store") + .Endpoint(this._configuration["CosmosDb:Database"]) + .TargetDurationMs(this._maxDurationMs); + + try + { + var dbData = await database.ReadAsync(null, cancellationToken).ConfigureAwait(false); + + return builder.build(); + } + catch (CosmosException ce) + { + _logger.LogError($"CosmosException:Health:{ce.StatusCode}:{ce.ActivityId}:{ce.Message}"); + builder.Exception(ce); + return builder.build(); + } + } + + private async Task CheckContainerAsync(Container container, CancellationToken cancellationToken = default) + { + _logger.LogDebug("Checking that we can read the cosmos db container information"); + HealthCheckResultBuilder builder = new HealthCheckResultBuilder() + .ComponentId("CosmosDb") + .ComponentType("Data Store") + .Endpoint(this._configuration["CosmosDb:Container"]) + .TargetDurationMs(this._maxDurationMs); + + try + { + var containerData = await container.ReadContainerAsync(null, cancellationToken).ConfigureAwait(false); + + return builder.build(); + } + catch (CosmosException ce) + { + _logger.LogError($"CosmosException:Health:{ce.StatusCode}:{ce.ActivityId}:{ce.Message}"); + builder.Exception(ce); + return builder.build(); + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/HealthCheckResultBuilder.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/HealthCheckResultBuilder.cs new file mode 100644 index 0000000..0926054 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/HealthCheckResultBuilder.cs @@ -0,0 +1,115 @@ +namespace ValidCargoProcessor.HealthCheck +{ + using System.Diagnostics; + using System.Globalization; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using ValidCargoProcessor.HealthCheck.Models; + + public class HealthCheckResultBuilder + { + private string TimeoutMessage = "Request exceeded expected duration"; + private HealthStatus _status = HealthStatus.Healthy; + private string _componentId = string.Empty; + private string _componentType = string.Empty; + private TimeSpan _duration; + private TimeSpan _targetDuration; + private string _time = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); + private string _endpoint = String.Empty; + private string _message = String.Empty; + private Stopwatch stopwatch = new(); + + public HealthCheckResultBuilder() + { + stopwatch.Start(); + } + + public HealthCheck build() + { + if (stopwatch.IsRunning) + { + stopwatch.Stop(); + } + + this._duration = stopwatch.Elapsed; + + HealthCheck result = new() + { + Endpoint = this._endpoint, + Status = this._status, + Duration = stopwatch.Elapsed, + TargetDuration = this._targetDuration, + ComponentId = this._componentId, + ComponentType = this._componentType, + }; + + // check duration + if (result.Duration.TotalMilliseconds > this._targetDuration.TotalMilliseconds) + { + result.Status = HealthStatus.Degraded; + result.Message = this.TimeoutMessage; + } + + return result; + } + + public HealthCheckResultBuilder StartTimer() + { + this.stopwatch.Restart(); + return this; + } + + public HealthCheckResultBuilder StopTimer() + { + this.stopwatch.Stop(); + return this; + } + + public HealthCheckResultBuilder Endpoint(string endpoint) + { + this._endpoint = endpoint; + return this; + } + + public HealthCheckResultBuilder Message(string message) + { + this._message = message; + return this; + } + + public HealthCheckResultBuilder ComponentId(string componentId) + { + this._componentId = componentId; + return this; + } + + public HealthCheckResultBuilder ComponentType(string componentType) + { + this._componentType = componentType; + return this; + } + + public HealthCheckResultBuilder Status(HealthStatus status) + { + this._status = status; + return this; + } + + public HealthCheckResultBuilder TargetDurationMs(int targetDurationMs) + { + this._targetDuration = new System.TimeSpan(0, 0, 0, 0, (int)targetDurationMs); + return this; + } + + public HealthCheckResultBuilder Exception(Exception ex) + { + if (ex != null) + { + this._status = HealthStatus.Unhealthy; + this._message = ex.Message; + this.StopTimer(); + } + return this; + } + } + +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/HealthCheck.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/HealthCheck.cs new file mode 100644 index 0000000..3d2b271 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/HealthCheck.cs @@ -0,0 +1,26 @@ +namespace ValidCargoProcessor.HealthCheck.Models +{ + using System; + using System.Globalization; + using System.Text.Json.Serialization; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + + /// + /// Class used to define Health Checks that are performed and their results, + /// in the format supported by .NET's IHealthCheck + /// + public class HealthCheck + { + public const string TimeoutMessage = "Request exceeded expected duration"; + [JsonConverter(typeof(JsonStringEnumConverter))] + public HealthStatus Status { get; set; } + public string ComponentId { get; set; } = String.Empty; + public string ComponentType { get; set; } = String.Empty; + public TimeSpan Duration { get; set; } + public TimeSpan TargetDuration { get; set; } + public string Time { get; set; } = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); + public string Endpoint { get; set; } = String.Empty; + public string Message { get; set; } = String.Empty; + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/IetfHealthCheck.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/IetfHealthCheck.cs new file mode 100644 index 0000000..6e80705 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/Models/IetfHealthCheck.cs @@ -0,0 +1,62 @@ +namespace ValidCargoProcessor.HealthCheck.Models +{ + using System; + using System.Collections.Generic; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + /// + /// Class used to define Health Checks that are performed and their results, + /// in the format documented by the Internet Engineering Task Force (IETF) + /// https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06 + /// + public class IetfHealthCheck + { + public string Status { get; set; } = String.Empty; + public string ComponentId { get; set; } = String.Empty; + public string ComponentType { get; set; } = String.Empty; + public string ObservedUnit { get; set; } = String.Empty; + public double ObservedValue { get; set; } + public double TargetValue { get; set; } + public string Time { get; set; } = String.Empty; + public List AffectedEndpoints { get; } = new List(); + public string Message { get; set; } = String.Empty; + + public IetfHealthCheck(HealthCheck healthCheck) + { + if (healthCheck == null) + { + throw new ArgumentNullException(nameof(healthCheck)); + } + + Status = ToIetfStatus(healthCheck.Status); + ComponentId = healthCheck.ComponentId; + ComponentType = healthCheck.ComponentType; + ObservedValue = Math.Round(healthCheck.Duration.TotalMilliseconds, 2); + TargetValue = Math.Round(healthCheck.TargetDuration.TotalMilliseconds, 0); + ObservedUnit = "ms"; + Time = healthCheck.Time; + Message = healthCheck.Message; + + if (healthCheck.Status != HealthStatus.Healthy && !string.IsNullOrEmpty(healthCheck.Endpoint)) + { + AffectedEndpoints = new List { healthCheck.Endpoint }; + } + + } + + /// + /// Convert the dotnet HealthStatus to the IETF Status + /// + /// HealthStatus (dotnet) + /// string + public static string ToIetfStatus(HealthStatus status) + { + return status switch + { + HealthStatus.Healthy => "pass", + HealthStatus.Degraded => "warn", + _ => "fail" + }; + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/ServiceBusHealthChecker.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/ServiceBusHealthChecker.cs new file mode 100644 index 0000000..777cdea --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/HealthCheck/ServiceBusHealthChecker.cs @@ -0,0 +1,133 @@ +namespace ValidCargoProcessor.HealthCheck +{ + using System.Diagnostics; + using ValidCargoProcessor.HealthCheck.Models; + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using System.Reflection; + + public class ServiceBusHealthChecker : IHealthCheck + { + private Stopwatch _stopwatch = new(); + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ServiceBusClient _serviceBusClient; + private readonly int _maxDurationMs; + private readonly string _description = "ServiceBus:HealthCheck"; + + public ServiceBusHealthChecker(IConfiguration configuration, + ILogger logger, ServiceBusClient serviceBusClient) + { + _configuration = configuration; + _logger = logger; + _serviceBusClient = serviceBusClient; + _maxDurationMs = int.Parse(configuration["HealthCheck:ServiceBus:MaxDurationMs"]); + } + + public void Heartbeat() + { + _stopwatch.Restart(); + } + + public Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + return Task.Run(() => + { + Dictionary data = new(); + + try + { + data.Add("ServiceBus:IsClosed", CheckServiceBusConnectionIsClosed()); + return ToHealthCheckResult(data); + } + catch (ServiceBusException sbe) + { + // log and return Unhealthy + _logger.LogError($"ServiceBusException:Health:{sbe.Reason}:{sbe.Source}:{sbe.Message}"); + + data.Add("ServiceBusException", sbe.Message); + + return new HealthCheckResult(HealthStatus.Unhealthy, _description, sbe, data); + } + catch (Exception ex) + { + // log and return unhealthy + _logger.LogError($"Exception:Health:{ex.Message}"); + + data.Add("Exception", ex.Message); + + return new HealthCheckResult(HealthStatus.Unhealthy, _description, ex, data); + } + }); + } + + private HealthCheckResult ToHealthCheckResult(Dictionary data) + { + _logger.LogDebug("Converting data to HealthCheckResult"); + HealthStatus status = HealthStatus.Healthy; + + //Make sure we're reporting the least healthy result that was observed + foreach (object d in data.Values) + { + if (d is HealthCheck h && h.Status != HealthStatus.Healthy) + { + status = h.Status; + } + + if (status == HealthStatus.Unhealthy) + { + break; + } + } + + return new HealthCheckResult(status, this._description, data: data); + } + + private HealthCheck CheckServiceBusConnectionIsClosed() + { + _logger.LogDebug("Checking that we can read the cosmos db account information"); + HealthCheckResultBuilder builder = new HealthCheckResultBuilder() + .ComponentId("ServiceBus") + .ComponentType("PubSub") + .Endpoint("ServiceBus:EndpointUri") + .TargetDurationMs(this._maxDurationMs); + + try + { + // Using this approach until the SDK provides an alternative to testing the state of the service bus connection + // The ServiceBusConnection object is an internally scoped property and class to the ServiceBus SDK, + // so will need to use reflection to get access to it. + + // First we're getting the Connection value + Type sbcType = typeof(ServiceBusClient); + PropertyInfo? connectionProperty = sbcType.GetProperty("Connection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (connectionProperty == null) throw new NullReferenceException("Unable to get service bus connection property info"); + var connection = connectionProperty.GetValue(_serviceBusClient); + if (connection == null) throw new NullReferenceException("Unable to get service bus connection property"); + + // Next we need to get the value of the IsClosed property of the connection + PropertyInfo? isClosedProperty = connection.GetType() + .GetProperty("IsClosed", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (isClosedProperty == null) throw new NullReferenceException("Unable to get service bus connection IsClosed property info"); + + var isClosed = isClosedProperty.GetValue(connection); + if (isClosed == null) throw new NullReferenceException("Unable to get service bus connection IsClosed property"); + + // If the connection is closed, we have an unhealthy connection + builder.Status((Boolean)isClosed ? HealthStatus.Unhealthy : HealthStatus.Healthy); + + return builder.build(); + } + catch (Exception e) + { + _logger.LogError($"ServiceBusException:Health:{e.Message}"); + builder.Exception(e); + return builder.build(); + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Interfaces/ISubscriptionReceiver.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Interfaces/ISubscriptionReceiver.cs new file mode 100644 index 0000000..fe51dc5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Interfaces/ISubscriptionReceiver.cs @@ -0,0 +1,12 @@ +namespace ValidCargoProcessor +{ + + public interface ISubscriptionReceiver + { + Task ProcessMessagesAsync(); + + Task StopProcessingAsync(); + + Task AddItemToContainerAsync(ValidCargo cargo); + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/OperationState.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/OperationState.cs new file mode 100644 index 0000000..1ef3c4e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/OperationState.cs @@ -0,0 +1,15 @@ +namespace ValidCargoProcessor +{ + using Newtonsoft.Json; + public class OperationState + { + [JsonProperty(PropertyName = "operationId")] + public string? OperationId { get; set; } + [JsonProperty(PropertyName = "state")] + public string? State { get; set; } + [JsonProperty(PropertyName = "result")] + public ValidCargo? Result { get; set; } + [JsonProperty(PropertyName = "error")] + public string? Error { get; set; } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/ValidCargo.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/ValidCargo.cs new file mode 100644 index 0000000..373fe4f --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Models/ValidCargo.cs @@ -0,0 +1,54 @@ +namespace ValidCargoProcessor +{ + using Newtonsoft.Json; + + public class ValidCargo + { + [JsonProperty(PropertyName = "timestamp")] + public string Timestamp { get; set; } = String.Empty; + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } = String.Empty; + [JsonProperty(PropertyName = "product")] + public Product Product { get; set; } = new Product(); + [JsonProperty(PropertyName = "port")] + public Port Port { get; set; } = new Port(); + [JsonProperty(PropertyName = "demandDates")] + public DemandDates DemandDates { get; set; } = new DemandDates(); + [JsonProperty(PropertyName = "valid")] + public bool Valid { get; set; } + [JsonProperty(PropertyName = "errorMessage")] + public string ErrorMessage { get; set; } = String.Empty; + } + + public class DemandDates + { + [JsonProperty(PropertyName = "start")] + public string Start { get; set; } = String.Empty; + [JsonProperty(PropertyName = "end")] + public string End { get; set; } = String.Empty; + } + + public class Port + { + [JsonProperty(PropertyName = "source")] + public string Source { get; set; } = String.Empty; + [JsonProperty(PropertyName = "destination")] + public string Destination { get; set; } = String.Empty; + } + + public class Product + { + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } = String.Empty; + [JsonProperty(PropertyName = "quantity")] + public int Quantity { get; set; } + } + + public class MessageEnvelope + { + [JsonProperty(PropertyName = "operationId")] + public string? OperationId { get; set; } + [JsonProperty(PropertyName = "data")] + public ValidCargo Data { get; set; } = new ValidCargo(); + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Program.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Program.cs new file mode 100644 index 0000000..7787191 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Program.cs @@ -0,0 +1,89 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using ValidCargoProcessor.HealthCheck; +using ValidCargoProcessor.Services; +using ValidCargoProcessor.Telemetry; + +namespace ValidCargoProcessor +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((_, builder) => + { + builder + .Sources.OfType() + .First(x => x.Path == "appsettings.json") + .Optional = true; // config specified via environment variables when deployed + }) + .ConfigureServices((hostContext, services) => + { + IConfiguration configuration = hostContext.Configuration; + services.AddHostedService() + .AddSingleton() + .AddSingleton(s => CreateCosmosClient(s, configuration)) + .AddSingleton(s => CreateTcpServer(s, configuration)) + .AddSingleton() + .AddHostedService() + .AddAzureClients(builder => CreateServiceBusClient(builder, configuration)); + services.AddHealthChecks() + .AddCheck("CosmosDb", failureStatus: HealthStatus.Unhealthy) + .AddCheck("ServiceBus", failureStatus: HealthStatus.Unhealthy); + services.AddApplicationInsightsTelemetryWorkerService(); + }); + + public static CosmosClient CreateCosmosClient(IServiceProvider s, IConfiguration configuration) + { + var endpointUri = configuration["CosmosDB:EndpointUri"]; + var authKey = configuration["CosmosDB:PrimaryKey"]; + + if (string.IsNullOrEmpty(endpointUri)) + { + throw new ArgumentException("CosmosDB endpoint URI is missing"); + } + + if (string.IsNullOrEmpty(authKey)) + { + throw new ArgumentException("CosmosDB authentication key is missing"); + } + + return new CosmosClient(endpointUri, authKey); + } + + public static void CreateServiceBusClient(AzureClientFactoryBuilder builder, IConfiguration configuration) + { + var serviceBusConnectionString = configuration["ServiceBus:ConnectionString"]; + + if (string.IsNullOrEmpty(serviceBusConnectionString)) + { + throw new ArgumentException("Service Bus connection string is missing"); + } + + builder.AddServiceBusClient(serviceBusConnectionString) + .ConfigureOptions(options => + { + + }); + } + + public static TcpListener CreateTcpServer(IServiceProvider s, IConfiguration configuration) + { + int port = int.Parse(configuration["HealthCheck:TcpServer:Port"]); + string hostName = Dns.GetHostName(); + + IPAddress localAddress = Dns.GetHostEntry(hostName).AddressList[0]; + return new TcpListener(localAddress, port); + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Properties/launchSettings.json b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Properties/launchSettings.json new file mode 100644 index 0000000..3b62c91 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "service_3": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true + }, + "Docker": { + "commandName": "Docker" + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/README.md b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/README.md new file mode 100644 index 0000000..88a8844 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/README.md @@ -0,0 +1,56 @@ +# Running the service + +## Pre-Requisites + +1. Service Bus [namespace](https://docs.microsoft.com/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create) with [topic](https://docs.microsoft.com/cli/azure/servicebus/topic?view=azure-cli-latest#az-servicebus-topic-create) and [subscription](https://docs.microsoft.com/cli/azure/servicebus/topic/subscription?view=azure-cli-latest#az-servicebus-topic-subscription-create) +1. Application Insights [account](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource#azure-cli-preview) +1. Cosmos DB [account](https://docs.microsoft.com/cli/azure/cosmosdb?view=azure-cli-latest#az-cosmosdb-create) with [database](https://docs.microsoft.com/cli/azure/cosmosdb/sql/database?view=azure-cli-latest#az-cosmosdb-sql-database-create) and [container](https://docs.microsoft.com/cli/azure/cosmosdb/sql/container?view=azure-cli-latest) + +## Running from VSCode Dev Container + +* Open the project in the dev container. +* Rename `appsettings.json.sample` to `appsettings.json` and add connection information for Service Bus, Cosmos DB, and Application Insights. +* Run the application - `dotnet run --project .\valid-cargo-manager.csproj` +* Post a message to the the Service Bus Topic similar to the [sample message](#sample-message) + +## Docker Container + +* Rename `appsettings.json.sample` to `appsettings.json` and add connection information for Service Bus, Cosmos DB, and Application Insights. +* Run `docker compose up` to run the service. +* Post a message to the the Service Bus Topic similar to the [sample message](#sample-message) + +## Sample Message + +```json +{ + "operationId": "4be3aab6-0f8f-4d5e-9330-3c0d89950cfa", + "data": { + "id": "08e222e4-5180-4f35-a8d6-e41b47b6447c", + "timestamp": "2022-06-24T17:10:28.000+00:00", + "product": { + "name": "toys", + "quantity": 100 + }, + "port": { + "source": "New York City", + "destination": "Tacoma" + }, + "demandDates": { + "start": "2022-11-24T00:00:00.000+00:00", + "end": "2022-11-30T00:00:00.000+00:00" + }, + "valid": true, + "errorMessage": null + } +} +``` + +### Custom Properties + +Ensure the following custom properties are also set for the messages posted to the Service Bus topic: + +* Diagnostic-Id: When posting a message to the service bus, also ensure a traceparent custom property has been set to a value that conforms pattern defined [by w3c](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format). + + For example a value like: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 + +* valid: boolean property identifying whether the cargo object passed validation. For this service to respond to the message the value should be set to True. diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Services/SubscriptionReceiver.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Services/SubscriptionReceiver.cs new file mode 100644 index 0000000..66eac7e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Services/SubscriptionReceiver.cs @@ -0,0 +1,159 @@ +namespace ValidCargoProcessor +{ + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Newtonsoft.Json; + using System.Net; + using Microsoft.Azure.Cosmos; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.Extensibility; + using System.Diagnostics; + using Microsoft.ApplicationInsights.Metrics; + + public class SubscriptionReceiver : ISubscriptionReceiver + { + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ServiceBusClient _serviceBusClient; + private readonly ServiceBusProcessor _processor; + private readonly ServiceBusSender _sender; + private readonly CosmosClient _cosmosClient; + private readonly TelemetryClient _telemetryClient; + private readonly MetricConfiguration _customMetricConfiguration; + + private readonly Container _container; + public SubscriptionReceiver(IConfiguration configuration, + ILogger logger, + CosmosClient cosmosClient, ServiceBusClient serviceBusClient, TelemetryClient telemetryClient) + { + _configuration = configuration; + _logger = logger; + _cosmosClient = cosmosClient; + _serviceBusClient = serviceBusClient; + _container = _cosmosClient.GetDatabase(_configuration["CosmosDB:Database"]).GetContainer(_configuration["CosmosDB:Container"]); + var prefetchCount = _configuration["ServiceBus:PrefetchCount"] == string.Empty ? 10 : int.Parse(_configuration["ServiceBus:PrefetchCount"]); + var maxConcurrentCalls = _configuration["ServiceBus:MaxConcurrentCalls"] == string.Empty ? 10 : int.Parse(_configuration["ServiceBus:MaxConcurrentCalls"]); + _processor = _serviceBusClient.CreateProcessor(_configuration["ServiceBus:Topic"], _configuration["ServiceBus:Subscription"], new ServiceBusProcessorOptions + { + PrefetchCount = prefetchCount, + MaxConcurrentCalls = maxConcurrentCalls + }); + _sender = _serviceBusClient.CreateSender(_configuration["ServiceBus:Queue"]); + _telemetryClient = telemetryClient; + _customMetricConfiguration = new MetricConfiguration(seriesCountLimit: 100, valuesPerDimensionLimit: 40, new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false)); + } + + private async Task MessageHandler(ProcessMessageEventArgs args) + { + ServiceBusReceivedMessage receivedMessage = args.Message; + string body = receivedMessage.Body.ToString(); + _logger.LogInformation($"Received: {body} from subscription"); + + if (receivedMessage.ApplicationProperties.TryGetValue("Diagnostic-Id", out var objectId) && objectId is string diagnosticId) + { + string traceId = diagnosticId.Split('-')[1]; + string parentId = diagnosticId.Split('-')[2]; + using (var operation = _telemetryClient.StartOperation("ServiceBusTopic.ProcessMessage", traceId, parentId)) + { + operation.Telemetry.Url = new Uri($"sb://{_configuration["ServiceBus:Subscription"]}"); + await ProcessMessagesAsync(args, body, operation); + } + } + else + { + _logger.LogError("Message is missing telemetry tracking information."); + await args.DeadLetterMessageAsync(args.Message, "Missing telemetry tracking information"); + } + } + + private async Task ProcessMessagesAsync(ProcessMessageEventArgs args, string body, IOperationHolder operation) + { + try + { + MessageEnvelope? message = JsonConvert.DeserializeObject(body); + if (message != null) + { + ValidCargo cargo = message.Data; + await AddItemToContainerAsync(cargo); + + await SendOperationState(new OperationState + { + OperationId = message.OperationId, + State = "Succeeded", + Result = cargo + }); + + TrackMultiDimensionalMetrics(cargo); + + await args.CompleteMessageAsync(args.Message); + } + else + { + await args.DeadLetterMessageAsync(args.Message, "Null cargo."); + _logger.LogError($"Cargo object is null. Message deadlettered"); + } + } + catch (Exception ex) + { + _telemetryClient.TrackException(ex); + operation.Telemetry.Success = false; + _logger.LogError($"Exception encountered - ${ex.Message}. Message deadlettered."); + //Making sure our dead letter reason isn't larger than the max length ServiceBus will allow + await args.DeadLetterMessageAsync(args.Message, + ex.Message.Substring(0, Math.Min(4096, ex.Message.Length))); + } + } + + private void TrackMultiDimensionalMetrics(ValidCargo cargo) + { + var metric = _telemetryClient.GetMetric("port_product_qty", "product", "source", "destination", _customMetricConfiguration); + + metric.TrackValue(cargo.Product.Quantity, + cargo.Product.Name, + cargo.Port.Source, + cargo.Port.Destination); + } + + private async Task SendOperationState(OperationState operationState) + { + _logger.LogInformation($"Sending operation state {operationState.OperationId} message to {_configuration["ServiceBus:Queue"]} queue"); + var message = new ServiceBusMessage(JsonConvert.SerializeObject(operationState)); + await _sender.SendMessageAsync(message); + } + + private Task ErrorHandler(ProcessErrorEventArgs args) + { + _logger.LogError(args.Exception.ToString()); + return Task.CompletedTask; + } + + public async Task ProcessMessagesAsync() + { + _processor.ProcessMessageAsync += MessageHandler; + _processor.ProcessErrorAsync += ErrorHandler; + await _processor.StartProcessingAsync(); + } + + public async Task StopProcessingAsync() + { + await _processor.StopProcessingAsync(); + await _processor.DisposeAsync(); + await _serviceBusClient.DisposeAsync(); + } + + public async Task AddItemToContainerAsync(ValidCargo cargo) + { + try + { + ItemResponse cargoResponse = await _container.ReadItemAsync(cargo.Id, new PartitionKey(cargo.Id)); + _logger.LogInformation($"Item in database with id: {cargoResponse.Resource.Id} already exists."); + } + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + ItemResponse cargoResponse = await _container.CreateItemAsync(cargo, new PartitionKey(cargo.Id)); + _logger.LogInformation($"Created item in database with id: {cargoResponse.Resource.Id} Operation consumed {cargoResponse.RequestCharge} RUs."); + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Telemetry/TelemetryInitializer.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Telemetry/TelemetryInitializer.cs new file mode 100644 index 0000000..98105bf --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Telemetry/TelemetryInitializer.cs @@ -0,0 +1,16 @@ +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.Extensibility; + +namespace ValidCargoProcessor.Telemetry +{ + public class TelemetryInitializer : ITelemetryInitializer + { + public void Initialize(ITelemetry telemetry) + { + if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName)) + { + telemetry.Context.Cloud.RoleName = "valid-cargo-manager"; + } + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Worker.cs b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Worker.cs new file mode 100644 index 0000000..197fb56 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/Worker.cs @@ -0,0 +1,30 @@ +using Microsoft.ApplicationInsights; + +namespace ValidCargoProcessor +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + private readonly TelemetryClient _telemetryClient; + private readonly ISubscriptionReceiver _subscriptionReceiver; + + public Worker(ILogger logger, TelemetryClient tc, ISubscriptionReceiver subscriptionReceiver) + { + _logger = logger; + _telemetryClient = tc; + _subscriptionReceiver = subscriptionReceiver; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Starting processing"); + await _subscriptionReceiver.ProcessMessagesAsync(); + } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Stopping processing"); + await _subscriptionReceiver.StopProcessingAsync(); + } + } +} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/appsettings.sample.json b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/appsettings.sample.json new file mode 100644 index 0000000..e9a24a2 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/appsettings.sample.json @@ -0,0 +1,37 @@ +{ + "ApplicationInsights": { + "ConnectionString": "InstrumentationKey=00000000-0000-0000-0000-000000000000;" + }, + "ServiceBus": { + "ConnectionString": "Endpoint=00000000=", + "Topic": "topicName", + "Queue": "queueName", + "Subscription": "subscriptionName", + "PrefetchCount": 100, + "MaxConcurrentCalls": 10 + }, + "CosmosDB": { + "EndpointUri": "https://00000000.documents.azure.com:443/", + "PrimaryKey": "00000000==", + "Database": "databaseName", + "Container": "containerName" + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "HealthCheck": { + "TcpServer": { + "Port": 3030 + }, + "CosmosDB": { + "MaxDurationMs": 200 + }, + "ServiceBus": { + "MaxDurationMs": 200 + } + } +} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/docker-compose.yml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/docker-compose.yml new file mode 100644 index 0000000..620df46 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" + +services: + valid_cargo_manager: + build: + context: . + dockerfile: Dockerfile \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/.helmignore b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/Chart.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/Chart.yaml new file mode 100644 index 0000000..0d3287e --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: valid-cargo-manager +description: valid-cargo-manager for aks-sb-azmonitor-microservices + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: v1 diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/NOTES.txt b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/NOTES.txt new file mode 100644 index 0000000..dd366ce --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/NOTES.txt @@ -0,0 +1,5 @@ +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aks-sb-azmonitor-microservices.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/_helpers.tpl b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/_helpers.tpl new file mode 100644 index 0000000..0172cb3 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aks-sb-azmonitor-microservices.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aks-sb-azmonitor-microservices.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aks-sb-azmonitor-microservices.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aks-sb-azmonitor-microservices.labels" -}} +helm.sh/chart: {{ include "aks-sb-azmonitor-microservices.chart" . }} +{{ include "aks-sb-azmonitor-microservices.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aks-sb-azmonitor-microservices.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aks-sb-azmonitor-microservices.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aks-sb-azmonitor-microservices.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aks-sb-azmonitor-microservices.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/deployment.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/deployment.yaml new file mode 100644 index 0000000..8f5eaab --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/deployment.yaml @@ -0,0 +1,118 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: default + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: ServiceBus__Topic + value: "validated-cargo" + - name: ServiceBus__Subscription + value: "valid-cargo" + - name: ServiceBus__Queue + value: "operation-state" + - name: ServiceBus__PrefetchCount + value: "100" + - name: ServiceBus__MaxConcurrentCalls + value: "10" + - name: CosmosDB__Database + value: "cargo" + - name: CosmosDB__Container + value: "valid-cargo" + - name: Logging__LogLevel__Default + value: "Information" + - name: Logging__LogLevel__Microsoft + value: "Warning" + - name: Logging__LogLevel__Microsoft.Hosting.Lifetime + value: "Information" + - name: HealthCheck__TcpServer__Port + value: "3030" + - name: HealthCheck__CosmosDB__MaxDurationMS + value: "1000" + - name: HealthCheck__ServiceBus__MaxDurationMS + value: "1000" + - name: ApplicationInsights__ConnectionString + valueFrom: + secretKeyRef: + name: valid-cargo-manager-secrets + key: AppInsightsConnectionString + - name: ServiceBus__ConnectionString + valueFrom: + secretKeyRef: + name: valid-cargo-manager-secrets + key: ServiceBusConnectionString + - name: CosmosDB__EndpointUri + valueFrom: + secretKeyRef: + name: valid-cargo-manager-secrets + key: CosmosDBEndpoint + - name: CosmosDB__PrimaryKey + valueFrom: + secretKeyRef: + name: valid-cargo-manager-secrets + key: CosmosDBKey + ports: + - name: http + containerPort: 3030 + protocol: TCP + livenessProbe: + tcpSocket: + port: 3030 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "aks-sb-azmonitor-microservices.fullname" . }} \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/hpa.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/hpa.yaml new file mode 100644 index 0000000..0a3ca97 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/secretProviderClass.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/secretProviderClass.yaml new file mode 100644 index 0000000..15cceb5 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/secretProviderClass.yaml @@ -0,0 +1,41 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: {{ .Values.aksKeyVaultSecretProviderIdentityId }} + keyvaultName: {{ .Values.keyVault.name }} + cloudName: "" + objects: | + array: + - | + objectName: AppInsightsConnectionString + objectType: secret + - | + objectName: ServiceBusConnectionString + objectType: secret + - | + objectName: CosmosDBEndpoint + objectType: secret + - | + objectName: CosmosDBKey + objectType: secret + tenantId: {{ .Values.keyVault.tenantId }} + secretObjects: + - data: + - key: AppInsightsConnectionString + objectName: AppInsightsConnectionString + - key: ServiceBusConnectionString + objectName: ServiceBusConnectionString + - key: CosmosDBEndpoint + objectName: CosmosDBEndpoint + - key: CosmosDBKey + objectName: CosmosDBKey + secretName: valid-cargo-manager-secrets + type: Opaque \ No newline at end of file diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/service.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/service.yaml new file mode 100644 index 0000000..af3f13a --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aks-sb-azmonitor-microservices.fullname" . }} + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aks-sb-azmonitor-microservices.selectorLabels" . | nindent 4 }} diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/tests/test-connection.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5eb4bc4 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "aks-sb-azmonitor-microservices.fullname" . }}-test-connection" + labels: + {{- include "aks-sb-azmonitor-microservices.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "aks-sb-azmonitor-microservices.fullname" . }}:80'] + restartPolicy: Never diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/valid-cargo-manager.yaml b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/valid-cargo-manager.yaml new file mode 100644 index 0000000..3d7bf17 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/helm/valid-cargo-manager.yaml @@ -0,0 +1,35 @@ +image: + pullPolicy: Always + tag: "latest" + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + + +# When running one of the deploy-*.sh scripts, an additional env.yaml +# values file is created containing values specific to the deployed environment +# with the following values: +# image: +# repository: + +# keyVault: +# name: +# tenantId: + +# aksKeyVaultSecretProviderIdentityId: diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.csproj b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.csproj new file mode 100644 index 0000000..b726c16 --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + dotnet-service_3-ED6CA571-2254-4C5F-9683-8520F235A2C9 + ValidCargoProcessor + Linux + . + + + + + + + + + + + + + + + diff --git a/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.sln b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.sln new file mode 100644 index 0000000..2e4b28d --- /dev/null +++ b/accelerators/aks-sb-azmonitor-microservices/src/valid-cargo-manager/valid-cargo-manager.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "valid-cargo-manager", "valid-cargo-manager.csproj", "{D909D249-A340-4C52-8CE0-6F7BEEBA000B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{47F598B3-9435-4ADA-A2D7-0933D138468E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D909D249-A340-4C52-8CE0-6F7BEEBA000B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D909D249-A340-4C52-8CE0-6F7BEEBA000B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D909D249-A340-4C52-8CE0-6F7BEEBA000B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D909D249-A340-4C52-8CE0-6F7BEEBA000B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {92C81030-3C75-4F2B-A795-616979C0ABCF} + EndGlobalSection +EndGlobal