├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── conf ├── RdsMonitoringSolution.template ├── api.core.service ├── server.conf └── setup.sh ├── frontend ├── package.json ├── public │ ├── aws-exports.json │ ├── index.html │ ├── loading.html │ ├── manifest.json │ └── robots.txt └── src │ ├── components │ ├── ChartArea01.js │ ├── ChartCLW01.js │ ├── ChartLine02.js │ ├── Functions.js │ ├── Header.js │ ├── HeaderApp.js │ ├── Layout.js │ ├── Metric02.js │ ├── Metric03.js │ ├── ProtectedApp.js │ └── ProtectedDb.js │ ├── index.css │ ├── index.js │ ├── pages │ ├── Authentication.js │ ├── Configs.js │ ├── Home.js │ ├── Login.js │ ├── Logout.js │ ├── Sm-mssql-01.js │ ├── Sm-mysql-01.js │ ├── Sm-mysql-02.js │ ├── Sm-oracle-01.js │ ├── Sm-postgresql-01.js │ └── Sm-postgresql-02.js │ ├── reportWebVitals.js │ ├── setupTests.js │ └── styles │ └── css │ ├── base.css │ ├── default.css │ └── top-navigation.css ├── images ├── img01.png ├── img02.png ├── img03.png ├── img04.png ├── img05.gif ├── launch-stack.png └── vid02.mp4 └── server ├── api.core.js ├── aws-exports.json └── package.json /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | * A reproducible test case or series of steps 17 | * The version of our code being used 18 | * Any modifications you've made relevant to the bug 19 | * Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the *main* branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | RDSTop Monitoring 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | ********************** 5 | THIRD PARTY COMPONENTS 6 | ********************** 7 | This software includes third party software subject to the following copyrights: 8 | 9 | aws-amplify/ui-react under the Apache License 2.0 10 | cloudscape-design/components under the Apache License 2.0 11 | cloudscape-design/design-tokens under the Apache License 2.0 12 | cloudscape-design/global-styles under the Apache License 2.0 13 | testing-library/jest-dom under the Massachusetts Institute of Technology (MIT) license 14 | testing-library/react under the Massachusetts Institute of Technology (MIT) license 15 | testing-library/user-event under the Massachusetts Institute of Technology (MIT) license 16 | axios under the Massachusetts Institute of Technology (MIT) license 17 | crypto-js under the Massachusetts Institute of Technology (MIT) license 18 | react under the Massachusetts Institute of Technology (MIT) license 19 | react-apexcharts under the Massachusetts Institute of Technology (MIT) license 20 | react-dom under the Massachusetts Institute of Technology (MIT) license 21 | react-router-dom under the Massachusetts Institute of Technology (MIT) license 22 | react-scripts under the Massachusetts Institute of Technology (MIT) license 23 | web-vitals under the Apache License 2.0 24 | aws-sdk under the Apache License 2.0 25 | request under the Apache License 2.0 26 | cors under the Massachusetts Institute of Technology (MIT) license 27 | express under the Massachusetts Institute of Technology (MIT) license 28 | jsonwebtoken under the Massachusetts Institute of Technology (MIT) license 29 | jwk-to-pem under the Apache License 2.0 30 | mysql under the Massachusetts Institute of Technology (MIT) license 31 | mssql under the Massachusetts Institute of Technology (MIT) license 32 | oracledb under the Apache License 2.0 33 | pg under the Massachusetts Institute of Technology (MIT) license 34 | uuid under the Massachusetts Institute of Technology (MIT) license 35 | 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RDSTop Monitoring Solution for AWS RDS 2 | 3 | > **Disclaimer:** The sample code; software libraries; command line tools; proofs of concept; templates; or other related technology (including any of the foregoing that are provided by our personnel) is provided to you as AWS Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You are responsible for testing, securing, and optimizing the AWS Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances, using Amazon CloudWatch or Amazon Cognito. 4 | 5 | 6 | ## What is RDSTop Monitoring ? 7 | 8 | RDSTop is lightweight application to perform realtime monitoring for AWS RDS and Amazon Aurora instances. 9 | Based on same simplicity concept of Unix top utility, provide quick and fast view of database performance, just all in one screen. 10 | 11 | image 12 | 13 | 14 | ## How looks like RDSTop Monitoring ? 15 | 16 | image 17 | 18 | https://github.com/aws-samples/rds-top-monitoring/assets/135755418/6bd2d036-78cc-4010-8311-f1d6f27f70d6 19 | 20 | 21 | ## How it works? 22 | 23 | image 24 | 25 | 26 | 27 | 28 | ## Database engine support 29 | 30 | RDSTop Monitoring Solution currently supports following database engines: 31 | 32 | - AWS RDS for MySQL 33 | - AWS RDS for PostgreSQL 34 | - AWS RDS for MariaDB 35 | - Amazon Aurora MySQL-Compatible Edition 36 | - Amazon Aurora PostgreSQL-Compatible Edition 37 | - AWS RDS for Oracle 38 | - AWS RDS for SQLServer 39 | 40 | 41 | Additional expanded support coming later to : 42 | 43 | - Amazon DocumentDB 44 | - Amazon ElastiCache for Redis 45 | - Amazon MemoryDB for Redis 46 | 47 | 48 | 49 | 50 | ## Solution Components 51 | 52 | - **Frontend.** React Developed Application to provide user interface to visualize performance database information. 53 | 54 | - **Backend.** NodeJS API Component to gather performance information from database engines, AWS CloudWatch and RDS Enhanced Monitoring. 55 | 56 | 57 | ## Architecture 58 | 59 | image 60 | 61 | 62 | ## Use cases 63 | 64 | - **Monitor instance performance.** 65 | Visualize performance data on realtime, and correlate data to understand and resolve the root cause of performance issues in your database instances. 66 | 67 | - **Perform root cause analysis.** 68 | Analyze database and operating system metrics to speed up debugging and reduce overall mean time to resolution. 69 | 70 | - **Optimize resources proactively.** 71 | Identify top consumer sessions, gather SQL statements and resource usages. 72 | 73 | 74 | 75 | ## Solution Requirements 76 | 77 | #### Amazon RDS Enhanced Monitoring 78 | 79 | Amazon RDS provides metrics in real time for the operating system (OS) that your DB instance runs on. RDSTop Monitoring solution integrate metrics from Enhanced Monitoring and it has to be enabled. 80 | Follow procedure below to turn on Enhanced Monitoring. 81 | 82 | https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.Enabling.html 83 | 84 | 85 | #### VPC Network Access to AWS RDS Instances 86 | 87 | RDSTop Monitoring Solution needs to access privately AWS RDS Instances, grant access inboud rules and security groups. 88 | 89 | 90 | 91 | 92 | ## Solution Deployment 93 | 94 | 95 | 96 | > **Time to deploy:** Approximately 10 minutes. 97 | 98 | 99 | ### Create database monitoring users 100 | 101 | Database credentials are needed to connect to the database engine and gather real-time metrics, use following statements to create the monitoring users. 102 | 103 | #### MySQL 104 | ``` 105 | CREATE USER 'monitor'@'%' IDENTIFIED BY ''; 106 | GRANT PROCESS ON *.* TO 'monitor'@'%' ; 107 | ``` 108 | 109 | #### PostgreSQL 110 | ``` 111 | CREATE USER monitor WITH PASSWORD ''; 112 | GRANT pg_read_all_stats TO monitor; 113 | ``` 114 | 115 | #### MS SQLServer 116 | ``` 117 | USE [master] 118 | GO 119 | CREATE LOGIN [monitor] WITH PASSWORD=N'', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=ON, CHECK_POLICY=ON 120 | GO 121 | use [master] 122 | GO 123 | GRANT CONNECT SQL TO [monitor] 124 | GO 125 | GRANT VIEW SERVER STATE TO [monitor] 126 | GO 127 | ``` 128 | 129 | #### Oracle 130 | ``` 131 | CREATE USER monitor IDENTIFIED BY ''; 132 | GRANT CREATE SESSION,SELECT ANY DICTIONARY TO monitor; 133 | ``` 134 | 135 | 136 | ### Launch CloudFormation Stack 137 | 138 | Follow the step-by-step instructions to configure and deploy the RDSTop into your account. 139 | 140 | 1. Make sure you have sign in AWS Console already. 141 | 2. Click the following button to launch the CloudFormation Console in your account and use Cloudformation template (RdsMonitoringSolution.template) located on conf folder. 142 | 143 | [![Launch Stack](./images/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/create/template?stackName=RDSTopMonitoringSolution) 144 | 145 | 3. Input **Stack name** parameter. 146 | 4. Input **Username** parameter, this username will be used to access the application. An email will be sent with temporary password from AWS Cognito Service. 147 | 5. Input **AWS Linux AMI** parameter, this parameter specify AWS AMI to build App EC2 Server. Keep default value. 148 | 6. Select **Instance Type** parameter, indicate what instance size is needed. 149 | 7. Select **VPC Name** parameter, indicate VPC to be used to deploy application server. 150 | 8. Select **Subnet Name** parameter, indicate private subnet to be used to deploy application server. This private subnet needs to have internet access, also application server needs to be able to reach AWS RDS Instances, add appropiate inboud rules and security groups. 151 | 9. Select **PublicAccess** parameter, indicate if Public Address is needed. 152 | 10. Input CIDR Inbound access rule for RDSTop Monitoring Solution. 153 | 11. Click **Next**, Click **Next**, select **acknowledge that AWS CloudFormation might create IAM resources with custom names**. and Click **Submit**. 154 | 12. Once Cloudformation has been deployed, gather application URL from output stack section. Username will be same you introduce on step 4 and temporary password will be sent by AWS Cognito Service. 155 | 13. Application deployment will take around 5 minutes to be completed. 156 | 157 | > **Note:** Because you are connecting to a site with a self-signed, untrusted host certificate, your browser may display a series of security warnings. 158 | Override the warnings and proceed to the site. To prevent site visitors from encountering warning screens, you must obtain a trusted, 159 | CA-signed certificate that not only encrypts, but also publicly authenticates you as the owner of the site. 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /conf/RdsMonitoringSolution.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "RDSTop Monitoring Solution" 3 | Metadata: 4 | AWS::CloudFormation::Interface: 5 | ParameterGroups: 6 | - 7 | Label: 8 | default: "General Configuration" 9 | Parameters: 10 | - Username 11 | - AMIId 12 | - InstanceType 13 | 14 | - 15 | Label: 16 | default: "Network Configuration" 17 | Parameters: 18 | - VPCParam 19 | - SubnetParam 20 | - PublicAccess 21 | - SGInboundAccess 22 | 23 | ParameterLabels: 24 | Username: 25 | default: "Specify Username for access" 26 | AMIId: 27 | default: "Specify AWS Linux AMI" 28 | InstanceType: 29 | default: "What Instance Type is needed?" 30 | VPCParam: 31 | default: "Which VPC should this be deployed to?" 32 | SubnetParam: 33 | default: "Which Subnet should this be deployed to?" 34 | PublicAccess: 35 | default: "Is Public Access Needed?" 36 | SGInboundAccess: 37 | default: "Specify CIDR Inbound Access Rule" 38 | 39 | 40 | Parameters: 41 | 42 | VPCParam: 43 | Type: AWS::EC2::VPC::Id 44 | Description: Select VPC 45 | 46 | SubnetParam: 47 | Type: AWS::EC2::Subnet::Id 48 | Description: Select VPC Subnet 49 | 50 | AMIId: 51 | Type: AWS::SSM::Parameter::Value 52 | Description: AWS AMI 53 | Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' 54 | 55 | Username: 56 | Type: String 57 | Description: Username (email) 58 | AllowedPattern: "\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}" 59 | 60 | InstanceType: 61 | Type: String 62 | Description: InstanceType 63 | Default: t3a.medium 64 | AllowedValues: 65 | - t3a.micro 66 | - t3a.small 67 | - t3a.medium 68 | - t3a.large 69 | - t3a.xlarge 70 | PublicAccess: 71 | Type: String 72 | Description: Public Access 73 | Default: "false" 74 | AllowedValues: 75 | - "true" 76 | - "false" 77 | 78 | SGInboundAccess: 79 | Type: String 80 | Description: Segurity Group Inbound Access (0.0.0.0/0) 81 | 82 | Conditions: 83 | IsPublicAccess: 84 | !Equals [!Ref PublicAccess, "true"] 85 | 86 | Resources: 87 | InstanceProfile: 88 | Type: "AWS::IAM::InstanceProfile" 89 | DependsOn: IAMRoleEC2 90 | Properties: 91 | Path: "/" 92 | Roles: 93 | - 94 | Ref: IAMRoleEC2 95 | 96 | IAMRoleEC2: 97 | Type: "AWS::IAM::Role" 98 | Properties: 99 | Path: "/" 100 | RoleName: !Join [ "-", ["role-ec2-rdstop-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 101 | AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" 102 | MaxSessionDuration: 3600 103 | Description: "Allows EC2 instance to call AWS services on your behalf." 104 | 105 | 106 | IAMRoleCognito: 107 | Type: "AWS::IAM::Role" 108 | Properties: 109 | Path: "/" 110 | RoleName: !Join [ "-", ["role-cognito-rdstop-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 111 | AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cognito-idp.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" 112 | MaxSessionDuration: 3600 113 | Description: "Allows Cognito to use SMS MFA on your behalf." 114 | Policies: 115 | - PolicyName: "CognitoPolicy" 116 | PolicyDocument: 117 | Version: "2012-10-17" 118 | Statement: 119 | - Effect: "Allow" 120 | Action: 121 | - "sns:publish" 122 | Resource: "*" 123 | 124 | IAMPolicy: 125 | Type: "AWS::IAM::Policy" 126 | Properties: 127 | PolicyDocument: !Sub | 128 | { 129 | "Version": "2012-10-17", 130 | "Statement": [ 131 | { 132 | "Effect": "Allow", 133 | "Action": [ 134 | "logs:GetLogEvents" 135 | ], 136 | "Resource": [ 137 | "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:RDSOSMetrics:log-stream:*" 138 | ] 139 | }, 140 | { 141 | "Effect": "Allow", 142 | "Action": [ 143 | "cloudwatch:GetMetricData" 144 | ], 145 | "Resource": "*" 146 | }, 147 | { 148 | "Effect": "Allow", 149 | "Action": [ 150 | "rds:DescribeDBInstances" 151 | ], 152 | "Resource": "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:db:*" 153 | } 154 | 155 | 156 | ] 157 | } 158 | Roles: 159 | - !Ref IAMRoleEC2 160 | PolicyName: !Join [ "-", ["policy-rds-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 161 | 162 | 163 | EC2Instance: 164 | Type: "AWS::EC2::Instance" 165 | DependsOn: CognitoUserPool 166 | Properties: 167 | ImageId: !Ref AMIId 168 | InstanceType: !Ref InstanceType 169 | Tenancy: "default" 170 | EbsOptimized: true 171 | SourceDestCheck: true 172 | BlockDeviceMappings: 173 | - 174 | DeviceName: "/dev/xvda" 175 | Ebs: 176 | Encrypted: false 177 | VolumeSize: 20 178 | VolumeType: "gp2" 179 | DeleteOnTermination: true 180 | IamInstanceProfile: !Ref InstanceProfile 181 | NetworkInterfaces: 182 | - AssociatePublicIpAddress: !Ref PublicAccess 183 | DeviceIndex: "0" 184 | GroupSet: 185 | - Ref: VPCSecurityGroup 186 | SubnetId: 187 | Ref: SubnetParam 188 | UserData: 189 | Fn::Base64: 190 | !Sub | 191 | #!/bin/bash 192 | sudo mkdir -p /aws/apps 193 | 194 | cd /tmp 195 | sudo yum install -y git 196 | git clone https://github.com/aws-samples/rds-top-monitoring.git 197 | cd rds-top-monitoring 198 | sudo cp -r server frontend conf /aws/apps 199 | 200 | echo '{ "aws_region": "${AWS::Region}","aws_cognito_user_pool_id": "${CognitoUserPool}","aws_cognito_user_pool_web_client_id": "${CognitoUserPoolClient}","aws_api_port": 3000, "aws_token_expiration":24 }' > /aws/apps/conf/aws-exports.json 201 | cd /aws/apps 202 | sudo -u ec2-user sh conf/setup.sh 203 | 204 | Tags: 205 | - 206 | Key: "Name" 207 | Value: !Join [ "-", ["ec2-rds-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 208 | 209 | 210 | VPCSecurityGroup: 211 | Type: "AWS::EC2::SecurityGroup" 212 | Properties: 213 | GroupDescription: !Join [ "_", ["sg_security_group_rds_top_solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 214 | GroupName: !Join [ "_", ["sg_security_group_rds_top_solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 215 | VpcId: !Ref VPCParam 216 | SecurityGroupIngress: 217 | - 218 | CidrIp: !Ref SGInboundAccess 219 | FromPort: 443 220 | IpProtocol: "tcp" 221 | ToPort: 443 222 | 223 | SecurityGroupEgress: 224 | - 225 | CidrIp: "0.0.0.0/0" 226 | IpProtocol: "-1" 227 | 228 | 229 | CognitoUserPool: 230 | Type: "AWS::Cognito::UserPool" 231 | Properties: 232 | UserPoolName: !Join [ "-", ["AwsRdsTopSolutionUserPool", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 233 | Policies: 234 | PasswordPolicy: 235 | MinimumLength: 8 236 | RequireUppercase: true 237 | RequireLowercase: true 238 | RequireNumbers: true 239 | RequireSymbols: true 240 | TemporaryPasswordValidityDays: 7 241 | LambdaConfig: {} 242 | AutoVerifiedAttributes: 243 | - "email" 244 | UsernameAttributes: 245 | - "email" 246 | MfaConfiguration: "OPTIONAL" 247 | SmsConfiguration: 248 | SnsCallerArn: !GetAtt IAMRoleCognito.Arn 249 | SnsRegion: !Ref AWS::Region 250 | EmailConfiguration: 251 | EmailSendingAccount: "COGNITO_DEFAULT" 252 | AdminCreateUserConfig: 253 | AllowAdminCreateUserOnly: true 254 | UserPoolTags: {} 255 | AccountRecoverySetting: 256 | RecoveryMechanisms: 257 | - 258 | Priority: 1 259 | Name: "verified_email" 260 | UsernameConfiguration: 261 | CaseSensitive: false 262 | VerificationMessageTemplate: 263 | DefaultEmailOption: "CONFIRM_WITH_CODE" 264 | 265 | CognitoUserPoolClient: 266 | Type: "AWS::Cognito::UserPoolClient" 267 | Properties: 268 | UserPoolId: !Ref CognitoUserPool 269 | ClientName: !Join [ "-", ["AwsRdsTopSolutionUserPoolClient", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 270 | RefreshTokenValidity: 1 271 | ReadAttributes: 272 | - "address" 273 | - "birthdate" 274 | - "email" 275 | - "email_verified" 276 | - "family_name" 277 | - "gender" 278 | - "given_name" 279 | - "locale" 280 | - "middle_name" 281 | - "name" 282 | - "nickname" 283 | - "phone_number" 284 | - "phone_number_verified" 285 | - "picture" 286 | - "preferred_username" 287 | - "profile" 288 | - "updated_at" 289 | - "website" 290 | - "zoneinfo" 291 | WriteAttributes: 292 | - "address" 293 | - "birthdate" 294 | - "email" 295 | - "family_name" 296 | - "gender" 297 | - "given_name" 298 | - "locale" 299 | - "middle_name" 300 | - "name" 301 | - "nickname" 302 | - "phone_number" 303 | - "picture" 304 | - "preferred_username" 305 | - "profile" 306 | - "updated_at" 307 | - "website" 308 | - "zoneinfo" 309 | ExplicitAuthFlows: 310 | - "ALLOW_REFRESH_TOKEN_AUTH" 311 | - "ALLOW_USER_SRP_AUTH" 312 | PreventUserExistenceErrors: "ENABLED" 313 | AllowedOAuthFlowsUserPoolClient: false 314 | IdTokenValidity: 1440 315 | AccessTokenValidity: 1440 316 | TokenValidityUnits: 317 | AccessToken: "minutes" 318 | IdToken: "minutes" 319 | RefreshToken: "days" 320 | 321 | CognitoUserPoolUser: 322 | Type: "AWS::Cognito::UserPoolUser" 323 | Properties: 324 | Username: !Ref Username 325 | UserPoolId: !Ref CognitoUserPool 326 | UserAttributes: 327 | - 328 | Name: "email_verified" 329 | Value: "true" 330 | - 331 | Name: "email" 332 | Value: !Ref Username 333 | 334 | Outputs: 335 | PublicAppURL: 336 | Condition: IsPublicAccess 337 | Description: Public Endpoint 338 | Value: !Join [ "", ["https://", !GetAtt EC2Instance.PublicIp]] 339 | 340 | PrivateAppURL: 341 | Description: Private Endpoint 342 | Value: !Join [ "", ["https://", !GetAtt EC2Instance.PrivateIp]] 343 | 344 | -------------------------------------------------------------------------------- /conf/api.core.service: -------------------------------------------------------------------------------- 1 | #/usr/lib/systemd/system/api.core.service 2 | [Unit] 3 | Description=API Webservice - Core 4 | Documentation=https://apicore.aws 5 | After=network.target 6 | 7 | [Service] 8 | Environment=NODE_PORT=3002 9 | Type=simple 10 | User=ec2-user 11 | WorkingDirectory=/aws/apps/server/ 12 | ExecStart=/home/ec2-user/.nvm/versions/node/v16.17.1/bin/node api.core.js 13 | Restart=on-failure 14 | 15 | [Install] 16 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /conf/server.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name _; 4 | ssl_protocols TLSv1.2; 5 | ssl_certificate /etc/nginx/ssl/server.crt; 6 | ssl_certificate_key /etc/nginx/ssl/server.key; 7 | root /aws/apps/frontend/build; 8 | index index.html; 9 | location / { 10 | try_files $uri /index.html; 11 | } 12 | location /api { 13 | proxy_pass http://127.0.0.1:3000/api; 14 | } 15 | } -------------------------------------------------------------------------------- /conf/setup.sh: -------------------------------------------------------------------------------- 1 | 2 | #Install Software Packages 3 | sudo yum install -y openssl 4 | sudo yum install -y nginx 5 | sudo service nginx restart 6 | 7 | #Create Certificates 8 | sudo mkdir /etc/nginx/ssl/ 9 | sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt -subj "/C=US/ST=US/L=US/O=Global Security/OU=IT Department/CN=127.0.0.1" 10 | 11 | #Copy Configurations 12 | sudo cp conf/api.core.service /usr/lib/systemd/system/api.core.service 13 | sudo cp conf/server.conf /etc/nginx/conf.d/ 14 | 15 | #Enable Auto-Start 16 | sudo chkconfig nginx on 17 | sudo chkconfig api.core on 18 | 19 | 20 | #Change permissions 21 | sudo chown -R ec2-user:ec2-user /aws/apps 22 | 23 | #NodeJS Installation 24 | curl https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh --output install.sh 25 | sh install.sh 26 | . ~/.nvm/nvm.sh 27 | nvm install 16.17 28 | 29 | #NodeJS API Core Installation 30 | cd /aws/apps/server/; npm install; 31 | 32 | 33 | #React Application Installation 34 | cd /aws/apps/frontend/; npm install; npm run build; 35 | 36 | #Copy aws-exports 37 | cp /aws/apps/conf/aws-exports.json /aws/apps/frontend/build/ 38 | cp /aws/apps/conf/aws-exports.json /aws/apps/server/ 39 | 40 | 41 | #Restart the services 42 | sudo service nginx restart 43 | sudo service api.core restart 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^4.6.4", 7 | "@cloudscape-design/components": "^3.0.277", 8 | "@cloudscape-design/design-tokens": "^3.0.12", 9 | "@cloudscape-design/global-styles": "^1.0.9", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "axios": "^1.4.0", 14 | "crypto-js": "^4.1.1", 15 | "react": "^18.2.0", 16 | "react-apexcharts": "^1.4.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.11.1", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "devDependencies": { 22 | "react-scripts": "^5.0.1" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "proxy": "http://44.217.218.9:3000", 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/public/aws-exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "aws_region": "us-east-1", 3 | "aws_cognito_user_pool_id": "", 4 | "aws_cognito_user_pool_web_client_id": "", 5 | "aws_token_expiration": "24h" 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 29 | RDSTop Monitoring Solution 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/public/loading.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/frontend/public/loading.html -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/components/ChartArea01.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect} from 'react'; 2 | import Chart from 'react-apexcharts'; 3 | 4 | function ChartArea({series,serie,history, height, width="100%", title, colors=[]}) { 5 | 6 | const [chartData, setChartData] = useState(series); 7 | 8 | var options = { 9 | chart: { 10 | height: height, 11 | type: 'area', 12 | stacked: true, 13 | zoom: { 14 | enabled: false 15 | }, 16 | animations: { 17 | enabled: false, 18 | }, 19 | dynamicAnimation : 20 | { 21 | enabled: true, 22 | }, 23 | toolbar: { 24 | show: false, 25 | } 26 | 27 | }, 28 | colors: colors, 29 | markers: { 30 | size: 0, 31 | }, 32 | dataLabels: { 33 | enabled: false 34 | }, 35 | stroke: { 36 | curve: 'straight', 37 | width: 1, 38 | }, 39 | fill: { 40 | type: 'gradient', 41 | gradient: { 42 | opacityFrom: 0.6, 43 | opacityTo: 0.8, 44 | } 45 | }, 46 | title: { 47 | text : title, 48 | align: "center", 49 | show: false 50 | }, 51 | grid: { 52 | show: false, 53 | yaxis: { 54 | lines: { 55 | show: false 56 | } 57 | }, 58 | }, 59 | xaxis: { 60 | labels: { 61 | show: false, 62 | } 63 | }, 64 | yaxis: { 65 | tickAmount: 5, 66 | axisTicks: { 67 | show: true, 68 | }, 69 | axisBorder: { 70 | show: true, 71 | color: '#78909C', 72 | offsetX: 0, 73 | offsetY: 0 74 | }, 75 | min : 0, 76 | labels : { 77 | formatter: function(val, index) { 78 | 79 | if(val === 0) return '0'; 80 | if(val < 1000) return parseFloat(val).toFixed(1); 81 | 82 | var k = 1000, 83 | sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], 84 | i = Math.floor(Math.log(val) / Math.log(k)); 85 | return parseFloat((val / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; 86 | 87 | }, 88 | style: { 89 | colors: ['#C6C2C1'], 90 | fontSize: '12px', 91 | fontFamily: 'Helvetica, Arial, sans-serif', 92 | }, 93 | }, 94 | 95 | } 96 | }; 97 | 98 | // eslint-disable-next-line 99 | useEffect(() => { 100 | updateMetrics(); 101 | // eslint-disable-next-line react-hooks/exhaustive-deps 102 | }, [serie]); 103 | 104 | 105 | function updateMetrics(){ 106 | 107 | var currentData = []; 108 | var iCursor=0; 109 | chartData.forEach(function(item) { 110 | item.data.push(serie[iCursor] || null); 111 | if (item.data.length > history ){ 112 | item.data = item.data.slice(item.data.length-history) 113 | } 114 | currentData.push({name : item.name, data : item.data}); 115 | iCursor++; 116 | }) 117 | setChartData(currentData); 118 | 119 | } 120 | 121 | 122 | return ( 123 |
124 | 125 |
126 | ); 127 | } 128 | 129 | export default ChartArea; 130 | -------------------------------------------------------------------------------- /frontend/src/components/ChartCLW01.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect,useRef,memo} from 'react' 2 | import Axios from 'axios' 3 | import { configuration } from '../pages/Configs'; 4 | import Chart from 'react-apexcharts'; 5 | 6 | const ChartCLW = memo(({title,subtitle,height,color,namespace,dimension_name,dimension_value,metric_name,stat_type,period,interval,metric_per_second,metric_precision,format}) => { 7 | 8 | const [chartData, setChartData] = useState({ 9 | dataset : [], 10 | metric : 0, 11 | stats : {avg:"0",max:"0", min:"0" } 12 | }); 13 | 14 | const timestampMetric = useRef(""); 15 | 16 | var options = { 17 | chart: { 18 | height: height, 19 | type: 'line', 20 | zoom: { 21 | enabled: false 22 | }, 23 | animations: { 24 | enabled: true, 25 | }, 26 | dynamicAnimation : 27 | { 28 | enabled: true, 29 | }, 30 | toolbar: { 31 | show: false, 32 | } 33 | 34 | }, 35 | markers: { 36 | size: 4, 37 | strokeColors: '#29313e', 38 | }, 39 | dataLabels: { 40 | enabled: false 41 | }, 42 | stroke: { 43 | curve: 'straight', 44 | width: 1, 45 | }, 46 | grid: { 47 | show: false, 48 | yaxis: { 49 | lines: { 50 | show: false 51 | } 52 | }, 53 | }, 54 | tooltip: { 55 | theme: "dark", 56 | }, 57 | xaxis: { 58 | labels: { 59 | show: false, 60 | } 61 | }, 62 | yaxis: { 63 | tickAmount: 5, 64 | axisTicks: { 65 | show: true, 66 | }, 67 | axisBorder: { 68 | show: true, 69 | color: '#78909C', 70 | offsetX: 0, 71 | offsetY: 0 72 | }, 73 | min : 0, 74 | labels : { 75 | formatter: function(val, index) { 76 | 77 | if(val === 0) return '0'; 78 | if(val < 1000) return parseFloat(val).toFixed(1); 79 | 80 | var k = 1000, 81 | sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], 82 | i = Math.floor(Math.log(val) / Math.log(k)); 83 | return parseFloat((val / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; 84 | 85 | }, 86 | style: { 87 | colors: ['#C6C2C1'], 88 | fontSize: '12px', 89 | fontFamily: 'Helvetica, Arial, sans-serif', 90 | }, 91 | }, 92 | 93 | } 94 | }; 95 | 96 | 97 | 98 | function fetchMetrics(){ 99 | 100 | var d_end_time = new Date(); 101 | var d_start_time = new Date(d_end_time - interval ); 102 | var queryclw = { 103 | MetricDataQueries: [ 104 | { 105 | Id: "m01", 106 | MetricStat: { 107 | Metric: { 108 | Namespace: namespace, 109 | MetricName: metric_name, 110 | Dimensions: [{ Name: dimension_name,Value: dimension_value}] 111 | }, 112 | Period: period, 113 | Stat: stat_type 114 | }, 115 | Label: title 116 | } 117 | ], 118 | "StartTime": d_start_time, 119 | "EndTime": d_end_time 120 | }; 121 | 122 | return Axios.get(`${configuration["apps-settings"]["api_url"]}/api/aws/clw/region/query/`,{ 123 | params: queryclw 124 | }).then((data)=>{ 125 | 126 | var currentData = []; 127 | var average = 0; 128 | var max = 0; 129 | var min = 0; 130 | var stats = {}; 131 | var metric = ""; 132 | 133 | if ( timestampMetric.current != data.data.MetricDataResults[0].Timestamps[0]) { 134 | 135 | data.data.MetricDataResults.forEach(function(item) { 136 | 137 | currentData.push({name : item.Label, data : item.Values.reverse()}); 138 | average = item.Values.reduce((a, b) => a + b, 0) / item.Values.length; 139 | max = Math.max(...item.Values); 140 | min = Math.min(...item.Values); 141 | switch (format) { 142 | case 1: 143 | metric = (CustomFormatNumberRaw(item.Values[item.Values.length-1],metric_precision)); 144 | stats = ({ 145 | avg : CustomFormatNumberRaw(average,metric_precision), 146 | max : CustomFormatNumberRaw(max,metric_precision), 147 | min : CustomFormatNumberRaw(min,metric_precision) 148 | }); 149 | break; 150 | 151 | case 2: 152 | metric = (CustomFormatNumberData(item.Values[item.Values.length-1],metric_precision)); 153 | stats = ({ 154 | avg : CustomFormatNumberData(average,metric_precision), 155 | max : CustomFormatNumberData(max,metric_precision), 156 | min : CustomFormatNumberData(min,metric_precision) 157 | }); 158 | break; 159 | 160 | case 3: 161 | metric = (CustomFormatNumberRawInteger(item.Values[item.Values.length-1],0)); 162 | stats = ({ 163 | avg : CustomFormatNumberRawInteger(average,0), 164 | max : CustomFormatNumberRawInteger(max,0), 165 | min : CustomFormatNumberRawInteger(min,0) 166 | }); 167 | break; 168 | 169 | } 170 | 171 | 172 | }) 173 | 174 | timestampMetric.current = data.data.MetricDataResults[0].Timestamps[0]; 175 | setChartData({ 176 | dataset : currentData, 177 | metric : metric, 178 | stats : stats 179 | }); 180 | 181 | } 182 | 183 | 184 | 185 | 186 | 187 | }); 188 | 189 | 190 | } 191 | 192 | 193 | function CustomFormatNumberData(value,decimalLength) { 194 | value = parseFloat(value); 195 | if(value == 0) return '0'; 196 | if(value < 1024) return parseFloat(value).toFixed(decimalLength); 197 | 198 | var k = 1024, 199 | sizes = ['', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 200 | i = Math.floor(Math.log(value) / Math.log(k)); 201 | return parseFloat((value / Math.pow(k, i)).toFixed(decimalLength)) + ' ' + sizes[i]; 202 | } 203 | 204 | 205 | function CustomFormatNumberRaw(value,decimalLength) { 206 | value = parseFloat(value); 207 | if (value < 100 && decimalLength == 0 ) 208 | decimalLength=2; 209 | 210 | if (value==0) 211 | decimalLength=0; 212 | 213 | return value.toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 214 | 215 | } 216 | 217 | function CustomFormatNumberRawInteger(value,decimalLength) { 218 | value = parseFloat(value); 219 | return parseFloat(value).toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 220 | } 221 | 222 | 223 | // eslint-disable-next-line 224 | useEffect(() => { 225 | fetchMetrics(); 226 | const id = setInterval(fetchMetrics, configuration["apps-settings"]["refresh-interval-clw"]); 227 | return () => clearInterval(id); 228 | }, []); 229 | 230 | 231 | return ( 232 |
233 | 234 | 235 | 267 | 272 | 273 |
236 | 237 |
{chartData.metric}
238 |
239 |
{title}({subtitle})
240 |
241 | 242 | 243 | 246 | 249 | 252 | 253 | 254 | 257 | 260 | 263 | 264 |
244 | Avg 245 | 247 | Max 248 | 250 | Min 251 |
255 | {chartData.stats.avg} 256 | 258 | {chartData.stats.max} 259 | 261 | {chartData.stats.min} 262 |
265 | 266 |
268 |
269 | 270 |
271 |
274 |
275 | ) 276 | }); 277 | export default ChartCLW; 278 | -------------------------------------------------------------------------------- /frontend/src/components/ChartLine02.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect,useRef} from 'react'; 2 | import Chart from 'react-apexcharts'; 3 | 4 | function ChartLine({series,history, height, width="100%", title, colors=[], border=2, timestamp}) { 5 | 6 | var options = { 7 | chart: { 8 | height: height, 9 | type: 'line', 10 | foreColor: '#C6C2C1', 11 | zoom: { 12 | enabled: false 13 | }, 14 | animations: { 15 | enabled: false, 16 | }, 17 | dynamicAnimation : 18 | { 19 | enabled: true, 20 | }, 21 | toolbar: { 22 | show: false, 23 | } 24 | 25 | }, 26 | markers: { 27 | size: 4, 28 | strokeColors: '#29313e', 29 | }, 30 | dataLabels: { 31 | enabled: false 32 | }, 33 | colors: colors, 34 | stroke: { 35 | curve: 'straight', 36 | width: border 37 | }, 38 | title: { 39 | text : title, 40 | align: "center", 41 | show: false, 42 | style: { 43 | fontSize: '13px', 44 | fontWeight: 'bold', 45 | fontFamily: undefined, 46 | color : "#C6C2C1" 47 | } 48 | 49 | }, 50 | grid: { 51 | show: false, 52 | yaxis: { 53 | lines: { 54 | show: false 55 | } 56 | }, 57 | }, 58 | tooltip: { 59 | theme: "dark", 60 | }, 61 | xaxis: { 62 | labels: { 63 | show: false, 64 | }, 65 | 66 | }, 67 | yaxis: { 68 | tickAmount: 5, 69 | axisTicks: { 70 | show: true, 71 | }, 72 | axisBorder: { 73 | show: true, 74 | color: '#78909C', 75 | offsetX: 0, 76 | offsetY: 0 77 | }, 78 | min : 0, 79 | labels : { 80 | formatter: function(val, index) { 81 | 82 | if(val === 0) return '0'; 83 | if(val < 1000) return parseFloat(val).toFixed(1); 84 | 85 | var k = 1000, 86 | sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], 87 | i = Math.floor(Math.log(val) / Math.log(k)); 88 | return parseFloat((val / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; 89 | 90 | }, 91 | style: { 92 | colors: ['#C6C2C1'], 93 | fontSize: '12px', 94 | fontFamily: 'Helvetica, Arial, sans-serif', 95 | }, 96 | }, 97 | 98 | } 99 | }; 100 | 101 | 102 | return ( 103 |
104 | 105 |
106 | ); 107 | } 108 | 109 | export default ChartLine; 110 | -------------------------------------------------------------------------------- /frontend/src/components/Functions.js: -------------------------------------------------------------------------------- 1 | export function customFormatNumber(value,decimalLength) { 2 | if(value == 0) return '0'; 3 | if(value < 1024) return parseFloat(value).toFixed(decimalLength); 4 | 5 | var k = 1024, 6 | sizes = ['', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 7 | i = Math.floor(Math.log(value) / Math.log(k)); 8 | return parseFloat((value / Math.pow(k, i)).toFixed(decimalLength)) + ' ' + sizes[i]; 9 | } 10 | 11 | 12 | 13 | 14 | export class classMetric { 15 | 16 | 17 | constructor(arrayMetrics) { 18 | 19 | this.dataHistory = {}; 20 | 21 | arrayMetrics.forEach(metric => { 22 | 23 | this.dataHistory = {...this.dataHistory, [metric.name]: { name : metric.name, history : metric.history, data : Array(50).fill(null) } } 24 | 25 | }); 26 | 27 | 28 | } 29 | 30 | init(currentObject,currentTime,oldObject,oldTime) { 31 | this.currentObject = currentObject; 32 | this.currentTime = currentTime; 33 | this.oldObject = oldObject; 34 | this.oldTime = oldTime; 35 | } 36 | 37 | newSnapshot(currentObject,currentTime) { 38 | 39 | this.oldObject = this.currentObject; 40 | this.oldTime = this.currentTime; 41 | 42 | this.currentObject = currentObject; 43 | this.currentTime = currentTime; 44 | 45 | } 46 | 47 | 48 | getDelta(propertyName) { 49 | try 50 | { 51 | return ( 52 | ( 53 | (this.currentObject[propertyName]['Value'] - this.oldObject[propertyName]['Value']) / 54 | (Math.abs(this.currentTime - this.oldTime) / 1000) 55 | ) || 0 56 | ) ; 57 | } 58 | catch(e) { 59 | return 0; 60 | } 61 | } 62 | 63 | getDeltaByValue(propertyName,propertyValue) { 64 | try 65 | { 66 | return ( 67 | ( 68 | (this.currentObject[propertyName][propertyValue] - this.oldObject[propertyName][propertyValue]) / 69 | (Math.abs(this.currentTime - this.oldTime) / 1000) 70 | ) || 0 71 | ) ; 72 | } 73 | catch(e) { 74 | return 0; 75 | } 76 | } 77 | 78 | getDeltaByIndex(propertyName) { 79 | try 80 | { 81 | return ( 82 | ( 83 | (this.currentObject[propertyName] - this.oldObject[propertyName]) / 84 | (Math.abs(this.currentTime - this.oldTime) / 1000) 85 | ) || 0 86 | ) ; 87 | } 88 | catch(e) { 89 | return 0; 90 | } 91 | } 92 | 93 | getValue(propertyName) { 94 | try 95 | { 96 | return (this.currentObject[propertyName]['Value']) ; 97 | } 98 | catch(e) { 99 | return 0; 100 | } 101 | } 102 | 103 | getValueByValue(propertyName,propertyValue) { 104 | try 105 | { 106 | return (this.currentObject[propertyName][propertyValue]) ; 107 | } 108 | catch(e) { 109 | return 0; 110 | } 111 | } 112 | 113 | getValueByIndex(propertyName) { 114 | try 115 | { 116 | return (this.currentObject[propertyName]) ; 117 | } 118 | catch(e) { 119 | return 0; 120 | } 121 | } 122 | 123 | addProperty(propertyName) 124 | { 125 | this.dataHistory = {...this.dataHistory, [propertyName]: Array(50).fill(null) } 126 | } 127 | 128 | getPropertyValues (propertyName){ 129 | return this.dataHistory[propertyName]; 130 | } 131 | 132 | 133 | addPropertyValue (propertyName,propertyValue){ 134 | this.dataHistory[propertyName].data.push(propertyValue); 135 | this.dataHistory[propertyName].data = this.dataHistory[propertyName].data.slice(this.dataHistory[propertyName].data.length-this.dataHistory[propertyName].history); 136 | 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import TopNavigation from '@cloudscape-design/components/top-navigation'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { configuration } from '../pages/Configs'; 4 | 5 | export default function App({sessionInformation,onClickMenu, onClickDisconnect}) { 6 | 7 | //-- Navigate Component 8 | const navigate = useNavigate(); 9 | 10 | //-- Navigation settings 11 | const i18nStrings = { 12 | searchIconAriaLabel: 'Search', 13 | searchDismissIconAriaLabel: 'Close search', 14 | overflowMenuTriggerText: 'More', 15 | overflowMenuTitleText: 'All', 16 | overflowMenuBackIconAriaLabel: 'Back', 17 | overflowMenuDismissIconAriaLabel: 'Close menu', 18 | }; 19 | 20 | //-- Navigate Profiling 21 | const profileActions = [ 22 | { type: 'button', id: 'profile', text: 'SessionID : ' + sessionInformation["session_id"]}, 23 | { id: 'version', text: 'AppVersion : ' + configuration["apps-settings"]["version"]}, 24 | { type: 'button', id: 'preferences', text: 'Preferences' }, 25 | { 26 | type: 'menu-dropdown', 27 | id: 'support-group', 28 | text: 'Support', 29 | items: [ 30 | {id: 'documentation',text: 'Documentation'}, 31 | { id: 'feedback', text: 'Feedback'}, 32 | { id: 'support', text: 'Customer support' }, 33 | ], 34 | } 35 | ]; 36 | 37 | 38 | 39 | return ( 40 | 71 | 72 | ); 73 | } 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/components/HeaderApp.js: -------------------------------------------------------------------------------- 1 | import TopNavigation from '@cloudscape-design/components/top-navigation'; 2 | import { Authenticator } from "@aws-amplify/ui-react"; 3 | import { configuration } from '../pages/Configs'; 4 | 5 | export default function App() { 6 | 7 | const i18nStrings = { 8 | searchIconAriaLabel: 'Search', 9 | searchDismissIconAriaLabel: 'Close search', 10 | overflowMenuTriggerText: 'More', 11 | overflowMenuTitleText: 'All', 12 | overflowMenuBackIconAriaLabel: 'Back', 13 | overflowMenuDismissIconAriaLabel: 'Close menu', 14 | }; 15 | 16 | const profileActions = [ 17 | { type: 'button', id: 'profile', text: 'AppVersion : ' + configuration["apps-settings"]["version"]}, 18 | { type: 'button', id: 'preferences', text: 'Preferences' }, 19 | { 20 | type: 'menu-dropdown', 21 | id: 'support-group', 22 | text: 'Support', 23 | items: [ 24 | {id: 'documentation',text: 'Documentation'}, 25 | { id: 'feedback', text: 'Feedback' }, 26 | { id: 'support', text: 'Customer support' }, 27 | ], 28 | } 29 | ]; 30 | 31 | 32 | return ( 33 | 34 | 35 | 36 | {({ signOut, user }) => ( 37 | 38 | 68 | 69 | 70 | )} 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | import * as React from "react"; 4 | import AppLayout from "@cloudscape-design/components/app-layout"; 5 | 6 | export const splitPanelI18nStrings: SplitPanelProps.I18nStrings = { 7 | preferencesTitle: 'Split panel preferences', 8 | preferencesPositionLabel: 'Split panel position', 9 | preferencesPositionDescription: 'Choose the default split panel position for the service.', 10 | preferencesPositionSide: 'Side', 11 | preferencesPositionBottom: 'Bottom', 12 | preferencesConfirm: 'Confirm', 13 | preferencesCancel: 'Cancel', 14 | closeButtonAriaLabel: 'Close panel', 15 | openButtonAriaLabel: 'Open panel', 16 | resizeHandleAriaLabel: 'Resize split panel', 17 | }; 18 | 19 | 20 | export default function App({pageContent,activeLink,breadCrumbs,contentType,navItems,navHeader,splitPanel,splitPanelSize,splitPanelOpen,onSplitPanelToggle}) { 21 | 22 | const appLayout = useRef(); 23 | 24 | return ( 25 | <> 26 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /frontend/src/components/Metric02.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect} from 'react' 2 | 3 | function Metric({value,title,precision,format=1}) { 4 | 5 | const [counterValue,setCountervalue] = useState(0); 6 | 7 | function updateMetrics(){ 8 | try { 9 | switch (format) { 10 | case 1: 11 | setCountervalue(CustomFormatNumberRaw(value,precision)); 12 | break; 13 | 14 | case 2: 15 | setCountervalue(CustomFormatNumberData(value,precision)); 16 | break; 17 | 18 | case 3: 19 | setCountervalue(CustomFormatNumberRawInteger(value,0)); 20 | break; 21 | 22 | } 23 | 24 | } 25 | catch{ 26 | console.log('error'); 27 | } 28 | 29 | 30 | } 31 | 32 | // eslint-disable-next-line 33 | useEffect(() => { 34 | updateMetrics(); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, [value]); 37 | 38 | 39 | 40 | function CustomFormatNumberData(value,decimalLength) { 41 | if(value == 0) return '0'; 42 | if(value < 1024) return parseFloat(value).toFixed(decimalLength); 43 | 44 | var k = 1024, 45 | sizes = ['', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZT', 'YB'], 46 | i = Math.floor(Math.log(value) / Math.log(k)); 47 | return parseFloat((value / Math.pow(k, i)).toFixed(decimalLength)) + ' ' + sizes[i]; 48 | } 49 | 50 | 51 | function CustomFormatNumberRaw(value,decimalLength) { 52 | if (value < 100 && decimalLength == 0 ) 53 | decimalLength=2; 54 | 55 | if (value==0) 56 | decimalLength=0; 57 | 58 | return value.toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 59 | 60 | } 61 | 62 | function CustomFormatNumberRawInteger(value,decimalLength) { 63 | return value.toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 64 | } 65 | 66 | return ( 67 |
68 |
69 | {counterValue} 70 |
71 |
72 | {title} 73 |
74 | 75 |
76 | ) 77 | } 78 | 79 | export default Metric 80 | -------------------------------------------------------------------------------- /frontend/src/components/Metric03.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect} from 'react' 2 | 3 | function Metric({value,title,precision,format=1}) { 4 | 5 | const [counterValue,setCountervalue] = useState(0); 6 | 7 | function updateMetrics(){ 8 | try { 9 | switch (format) { 10 | case 1: 11 | setCountervalue(CustomFormatNumberRaw(value,precision)); 12 | break; 13 | 14 | case 2: 15 | setCountervalue(CustomFormatNumberData(value,precision)); 16 | break; 17 | 18 | case 3: 19 | setCountervalue(CustomFormatNumberRawInteger(value,0)); 20 | break; 21 | 22 | } 23 | 24 | } 25 | catch{ 26 | console.log('error'); 27 | } 28 | 29 | 30 | } 31 | 32 | // eslint-disable-next-line 33 | useEffect(() => { 34 | updateMetrics(); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, [value]); 37 | 38 | 39 | 40 | function CustomFormatNumberData(value,decimalLength) { 41 | if(value == 0) return '0'; 42 | if(value < 1024) return parseFloat(value).toFixed(decimalLength); 43 | 44 | var k = 1024, 45 | sizes = ['', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZT', 'YB'], 46 | i = Math.floor(Math.log(value) / Math.log(k)); 47 | return parseFloat((value / Math.pow(k, i)).toFixed(decimalLength)) + ' ' + sizes[i]; 48 | } 49 | 50 | 51 | function CustomFormatNumberRaw(value,decimalLength) { 52 | if (value < 100 && decimalLength == 0 ) 53 | decimalLength=2; 54 | 55 | if (value==0) 56 | decimalLength=0; 57 | 58 | return value.toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 59 | 60 | } 61 | 62 | function CustomFormatNumberRawInteger(value,decimalLength) { 63 | return value.toLocaleString('en-US', {minimumFractionDigits:decimalLength, maximumFractionDigits:decimalLength}); 64 | } 65 | 66 | return ( 67 |
68 |
69 | {counterValue} 70 |
71 |
72 | {title} 73 |
74 | 75 |
76 | ) 77 | } 78 | 79 | export default Metric 80 | -------------------------------------------------------------------------------- /frontend/src/components/ProtectedApp.js: -------------------------------------------------------------------------------- 1 | import { useAuthenticator } from "@aws-amplify/ui-react"; 2 | import { Navigate,useLocation } from "react-router-dom"; 3 | 4 | export default function Protected({ children }) { 5 | const { user } = useAuthenticator(); 6 | const location = useLocation(); 7 | 8 | if (!(user)) { 9 | return ; 10 | } 11 | 12 | sessionStorage.setItem("x-token-cognito",user.signInUserSession.accessToken.jwtToken); 13 | 14 | return children; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/ProtectedDb.js: -------------------------------------------------------------------------------- 1 | import { Navigate,useLocation } from "react-router-dom"; 2 | import { useSearchParams } from 'react-router-dom'; 3 | 4 | 5 | const Protected = ({ children }) => { 6 | 7 | //-- Gather Parameters 8 | const [params]=useSearchParams(); 9 | const parameter_code_id=params.get("code_id"); 10 | 11 | //-- Gather URL location 12 | const location = useLocation(); 13 | 14 | if (sessionStorage.getItem(parameter_code_id) === null ) { 15 | return ; 16 | } 17 | 18 | return children; 19 | 20 | }; 21 | 22 | export default Protected; -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import { 3 | BrowserRouter, 4 | Routes, 5 | Route, 6 | } from "react-router-dom"; 7 | 8 | //-- Libraries 9 | import '@cloudscape-design/global-styles/index.css'; 10 | import { Amplify } from "aws-amplify"; 11 | import { AmplifyProvider, Authenticator } from "@aws-amplify/ui-react"; 12 | import { StrictMode } from "react"; 13 | import Axios from "axios"; 14 | 15 | //-- Pages 16 | import Authentication from "./pages/Authentication"; 17 | import Home from "./pages/Home"; 18 | import Login from "./pages/Login"; 19 | import Logout from "./pages/Logout"; 20 | import SmMysql01 from "./pages/Sm-mysql-01"; 21 | import SmMysql02 from "./pages/Sm-mysql-02"; 22 | import SmPostgresql01 from "./pages/Sm-postgresql-01"; 23 | import SmPostgresql02 from "./pages/Sm-postgresql-02"; 24 | import SmMssql01 from "./pages/Sm-mssql-01"; 25 | import SmOracle01 from "./pages/Sm-oracle-01"; 26 | 27 | //-- Components 28 | import ProtectedDb from "./components/ProtectedDb"; 29 | import ProtectedApp from "./components/ProtectedApp"; 30 | 31 | //import { applyMode, Mode } from '@cloudscape-design/global-styles'; 32 | 33 | // Apply a color mode 34 | //applyMode(Mode.Dark); 35 | //applyMode(Mode.Light); 36 | 37 | 38 | 39 | Axios.get(`/aws-exports.json`,).then((data)=>{ 40 | 41 | var configData = data.data; 42 | Amplify.configure({ 43 | Auth: { 44 | region: configData.aws_region, 45 | userPoolId: configData.aws_cognito_user_pool_id, 46 | userPoolWebClientId: configData.aws_cognito_user_pool_web_client_id, 47 | }, 48 | }); 49 | 50 | const rootElement = document.getElementById("root"); 51 | render( 52 | 53 | 54 | 55 | 56 | 57 | } /> 58 | } /> 59 | } /> 60 | } /> 61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | 68 | 69 | 70 | 71 | , 72 | rootElement 73 | ); 74 | 75 | }) 76 | .catch((err) => { 77 | console.log('API Call error : ./aws-exports.json' ); 78 | console.log(err) 79 | }); 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Authentication.js: -------------------------------------------------------------------------------- 1 | import { 2 | Authenticator, 3 | Flex, 4 | Grid, 5 | useTheme, 6 | View, 7 | Heading, 8 | Text 9 | } from "@aws-amplify/ui-react"; 10 | 11 | import { Navigate,useLocation } from "react-router-dom"; 12 | 13 | export default function Auth({children}) { 14 | 15 | const location = useLocation(); 16 | const from = location.state?.from || "/"; 17 | 18 | const { tokens } = useTheme(); 19 | const components = { 20 | Header, 21 | SignIn: { 22 | Header: SignInHeader 23 | }, 24 | Footer 25 | }; 26 | 27 | 28 | function Header() { 29 | const { tokens } = useTheme(); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | 38 | function Footer() { 39 | const { tokens } = useTheme(); 40 | 41 | return ( 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | 49 | function SignInHeader() { 50 | const { tokens } = useTheme(); 51 | 52 | return ( 53 | 54 | Sign in to RDSTop Monitoring Console 55 | 56 | ); 57 | } 58 | 59 | 60 | return ( 61 | 62 | 66 | 67 | {({ signOut, user }) => ( 68 |
69 | ; 70 |
71 | )} 72 |
73 |
74 | 77 | 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/pages/Configs.js: -------------------------------------------------------------------------------- 1 | 2 | export const configuration = 3 | { 4 | "apps-settings": { 5 | "refresh-interval": 10*1000, 6 | "refresh-interval-clw": 10*1000, 7 | "api_url": "", 8 | "version" : "0.1.2", 9 | "application_title": "RDSTop Monitoring" 10 | } 11 | }; 12 | 13 | export const SideMainLayoutHeader = { text: 'Service', href: '#/' }; 14 | 15 | export const SideMainLayoutMenu = [ 16 | { 17 | text: 'Explore resources', 18 | type: 'section', 19 | defaultExpanded: true, 20 | items: [ 21 | { type: 'link', text: 'Home', href: '/' }, 22 | { type: 'link', text: 'Instances', href: '/login' }, 23 | { type: 'link', text: 'Settings', href: '/settings' }, 24 | { type: 'link', text: 'Logout', href: '/logout' } 25 | ], 26 | } 27 | ]; 28 | 29 | 30 | export const breadCrumbs = [{text: 'Service',href: '#',},{text: 'Resource search',href: '#',},]; 31 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { SideMainLayoutHeader,SideMainLayoutMenu, breadCrumbs } from './Configs'; 2 | 3 | import CustomHeader from "../components/HeaderApp"; 4 | import AppLayout from "@cloudscape-design/components/app-layout"; 5 | import SideNavigation from '@cloudscape-design/components/side-navigation'; 6 | import ContentLayout from '@cloudscape-design/components/content-layout'; 7 | 8 | import Button from "@cloudscape-design/components/button"; 9 | import Container from "@cloudscape-design/components/container"; 10 | import Header from "@cloudscape-design/components/header"; 11 | import Box from "@cloudscape-design/components/box"; 12 | import ColumnLayout from "@cloudscape-design/components/column-layout"; 13 | import Badge from "@cloudscape-design/components/badge"; 14 | 15 | import '@aws-amplify/ui-react/styles.css'; 16 | 17 | export const splitPanelI18nStrings: SplitPanelProps.I18nStrings = { 18 | preferencesTitle: 'Split panel preferences', 19 | preferencesPositionLabel: 'Split panel position', 20 | preferencesPositionDescription: 'Choose the default split panel position for the service.', 21 | preferencesPositionSide: 'Side', 22 | preferencesPositionBottom: 'Bottom', 23 | preferencesConfirm: 'Confirm', 24 | preferencesCancel: 'Cancel', 25 | closeButtonAriaLabel: 'Close panel', 26 | openButtonAriaLabel: 'Open panel', 27 | resizeHandleAriaLabel: 'Resize split panel', 28 | }; 29 | 30 | 31 | function Login() { 32 | 33 | 34 | return ( 35 | 36 |
37 | 38 | } 42 | contentType="table" 43 | content={ 44 | 49 |
50 |
51 | Welcome to RDSTop Monitoring 52 |
53 |
54 |
55 | Gain Monitoring Insight and Take Action on AWS RDS. 56 |
57 |
58 | 59 |
60 |
61 |
62 | View performance data for AWS RDS and Amazon Aurora instance, so you can quickly identify and act on any issues that might impact database instances. 63 |
64 | 65 | 66 | } 67 | 68 | > 69 | 70 | 71 | 72 | } 73 | > 74 | 75 |
76 | 77 | 78 |
79 | 82 | How it works? 83 | 84 | 85 | } 86 | > 87 |
88 | 1 Connect to your AWS RDS or Amazon Aurora instance. 89 |
90 |
91 | 2 Gather realtime performance database metrics from engine itself. 92 |
93 |
94 | 3 Extract performance from AWS Cloudwatch metrics and Enhanced Monitoring. 95 |
96 |
97 | 4 Consolidate all information into centralized dashboard. 98 |
99 |
100 | 101 |
102 | 103 |
104 | 107 | Getting Started 108 | 109 | 110 | } 111 | > 112 |
113 | 114 | Start connecting to your AWS RDS or Amazon Aurora Instance. 115 | 116 |
117 | 118 |
119 |
120 |
121 |
122 | 123 |
124 | 125 | 126 |
127 |
128 | 131 | Use cases 132 | 133 | 134 | } 135 | > 136 | 137 |
138 |
139 | Monitor instance performance 140 |
141 | 142 | Visualize performance data on realtime, and correlate data to understand and resolve the root cause of performance issues in your database instances. 143 | 144 |
145 |
146 |
147 | Perform root cause analysis 148 |
149 | 150 | Analyze database and operating system metrics to speed up debugging and reduce overall mean time to resolution. 151 | 152 |
153 |
154 |
155 | Optimize resources proactively 156 |
157 | 158 | Identify top consumer sessions, gather SQL statements and resource usages. 159 | 160 |
161 | 162 |
163 | 164 |
165 | 166 | 167 |
168 |
169 | 170 | } 171 | /> 172 | 173 |
174 | ); 175 | } 176 | 177 | export default Login; 178 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import {useState,useEffect} from 'react' 2 | import { createSearchParams } from "react-router-dom"; 3 | import Axios from 'axios' 4 | import { configuration, SideMainLayoutHeader,SideMainLayoutMenu, breadCrumbs } from './Configs'; 5 | 6 | import CustomHeader from "../components/HeaderApp"; 7 | import AppLayout from "@cloudscape-design/components/app-layout"; 8 | import SideNavigation from '@cloudscape-design/components/side-navigation'; 9 | 10 | 11 | import { StatusIndicator } from '@cloudscape-design/components'; 12 | import Modal from "@cloudscape-design/components/modal"; 13 | import SpaceBetween from "@cloudscape-design/components/space-between"; 14 | import Button from "@cloudscape-design/components/button"; 15 | import FormField from "@cloudscape-design/components/form-field"; 16 | import Input from "@cloudscape-design/components/input"; 17 | import Table from "@cloudscape-design/components/table"; 18 | import Header from "@cloudscape-design/components/header"; 19 | import Box from "@cloudscape-design/components/box"; 20 | import ColumnLayout from "@cloudscape-design/components/column-layout"; 21 | 22 | 23 | import '@aws-amplify/ui-react/styles.css'; 24 | 25 | import { SplitPanel } from '@cloudscape-design/components'; 26 | 27 | import { applyMode, Mode } from '@cloudscape-design/global-styles'; 28 | 29 | // Apply a color mode 30 | //applyMode(Mode.Dark); 31 | applyMode(Mode.Light); 32 | 33 | 34 | export const splitPanelI18nStrings: SplitPanelProps.I18nStrings = { 35 | preferencesTitle: 'Split panel preferences', 36 | preferencesPositionLabel: 'Split panel position', 37 | preferencesPositionDescription: 'Choose the default split panel position for the service.', 38 | preferencesPositionSide: 'Side', 39 | preferencesPositionBottom: 'Bottom', 40 | preferencesConfirm: 'Confirm', 41 | preferencesCancel: 'Cancel', 42 | closeButtonAriaLabel: 'Close panel', 43 | openButtonAriaLabel: 'Open panel', 44 | resizeHandleAriaLabel: 'Resize split panel', 45 | }; 46 | 47 | 48 | 49 | 50 | //-- Encryption 51 | var CryptoJS = require("crypto-js"); 52 | 53 | function Login() { 54 | 55 | //-- Variable for Split Panels 56 | const [splitPanelShow,setsplitPanelShow] = useState(false); 57 | const [selectedItems,setSelectedItems] = useState([{ identifier: "" }]); 58 | 59 | //-- Variables RDS Table 60 | const [dataRds,setDataRds] = useState([]); 61 | const columnsRds=[ 62 | { id: "identifier",header: "DB identifier",cell: item => item['identifier'] || "-",sortingField: "identifier",isRowHeader: true, width: 250, }, 63 | { id: "status",header: "Status",cell: item => ( <> {item.status} ),sortingField: "status",isRowHeader: true }, 64 | { id: "size",header: "Size",cell: item => item['size'] || "-",sortingField: "size",isRowHeader: true }, 65 | { id: "engine",header: "Engine",cell: item => item['engine'] || "-",sortingField: "engine",isRowHeader: true }, 66 | { id: "version",header: "Engine Version",cell: item => item['version'] || "-",sortingField: "version",isRowHeader: true }, 67 | { id: "az",header: "Region & AZ",cell: item => item['az'] || "-",sortingField: "az",isRowHeader: true }, 68 | { id: "multiaz",header: "MultiAZ",cell: item => item['multiaz'] || "-",sortingField: "multiaz",isRowHeader: true }, 69 | ]; 70 | 71 | 72 | 73 | //-- Variable for textbox components 74 | const [txtUser, settxtUser] = useState(''); 75 | const [txtPassword, settxtPassword] = useState(''); 76 | 77 | const [modalConnectVisible, setModalConnectVisible] = useState(false); 78 | 79 | //-- Add Header Cognito Token 80 | Axios.defaults.headers.common['x-token-cognito'] = sessionStorage.getItem("x-token-cognito"); 81 | Axios.defaults.withCredentials = true; 82 | 83 | //-- Handle Click Events 84 | const handleClickLogin = () => { 85 | 86 | console.log(selectedItems[0]['engine']); 87 | // Add CSRF Token 88 | Axios.defaults.headers.common['x-csrf-token'] = sessionStorage.getItem("x-csrf-token"); 89 | 90 | // Get Authentication 91 | Axios.post(`${configuration["apps-settings"]["api_url"]}/api/security/rds/auth/`,{ 92 | params: { 93 | host: selectedItems[0]['endpoint'], 94 | port: selectedItems[0]['port'], 95 | username: txtUser, 96 | password: txtPassword, 97 | engine: selectedItems[0]['engine'], 98 | instance : selectedItems[0]['instance'] 99 | 100 | } 101 | }).then((data)=>{ 102 | console.log(data); 103 | if (data.data.result === "auth1") { 104 | sessionStorage.setItem(data.data.session_id, data.data.session_token ); 105 | var session_id = CryptoJS.AES.encrypt(JSON.stringify({ 106 | session_id : data.data.session_id, 107 | rds_id : selectedItems[0]['identifier'], 108 | rds_user : txtUser, 109 | rds_host : selectedItems[0]['endpoint'], 110 | rds_engine : selectedItems[0]['engine'], 111 | rds_class : selectedItems[0]['size'], 112 | rds_az : selectedItems[0]['az'], 113 | rds_version : selectedItems[0]['version'], 114 | rds_resource_id : selectedItems[0]['resourceId'], 115 | rds_storage : selectedItems[0]['storage'], 116 | rds_storage_size : selectedItems[0]['storageSize'] 117 | }), 118 | data.data.session_id 119 | ).toString(); 120 | 121 | 122 | var path_name = ""; 123 | switch (selectedItems[0]['engine']) { 124 | case "mysql": 125 | path_name = "/sm-mysql-01"; 126 | break; 127 | 128 | case "mariadb": 129 | path_name = "/sm-mysql-01"; 130 | break; 131 | 132 | case "aurora-mysql": 133 | path_name = "/sm-mysql-02"; 134 | break; 135 | 136 | case "postgres": 137 | path_name = "/sm-postgresql-01"; 138 | break; 139 | 140 | case "aurora-postgresql": 141 | path_name = "/sm-postgresql-02"; 142 | break; 143 | 144 | case "sqlserver-se": 145 | path_name = "/sm-mssql-01"; 146 | break; 147 | 148 | case "oracle-ee": 149 | case "oracle-ee-cdb": 150 | case "oracle-se2": 151 | case "oracle-se2-cdb": 152 | path_name = "/sm-oracle-01"; 153 | break; 154 | 155 | 156 | default: 157 | break; 158 | 159 | 160 | } 161 | 162 | setModalConnectVisible(false); 163 | settxtUser(''); 164 | settxtPassword(''); 165 | window.open(path_name + '?' + createSearchParams({ 166 | session_id: session_id, 167 | code_id: data.data.session_id 168 | }).toString() ,'_blank'); 169 | 170 | 171 | } 172 | else { 173 | 174 | 175 | } 176 | 177 | 178 | }) 179 | .catch((err) => { 180 | 181 | console.log('Timeout API Call : /api/security/auth/'); 182 | console.log(err) 183 | }); 184 | 185 | 186 | }; 187 | 188 | 189 | 190 | //-- Call API to gather instances 191 | async function gatherInstances (){ 192 | 193 | //--- GATHER INSTANCES 194 | var rdsItems=[]; 195 | 196 | try{ 197 | 198 | const { data } = await Axios.get(`${configuration["apps-settings"]["api_url"]}/api/aws/rds/instance/region/list/`); 199 | sessionStorage.setItem("x-csrf-token", data.csrfToken ); 200 | data.data.DBInstances.forEach(function(item) { 201 | if (item['Engine']==='mysql' || item['Engine']==='postgres' || item['Engine']==='mariadb' || item['Engine']==='aurora-mysql' || item['Engine']==='aurora-postgresql' || item['Engine']==='sqlserver-se' || item['Engine']==='sqlserver-ee' || item['Engine']==='sqlserver-web' || item['Engine']==='sqlserver-ex' || item['Engine']==='oracle-ee' || item['Engine']==='oracle-ee-cdb' || item['Engine']==='oracle-se2' || item['Engine']==='oracle-se2-cdb'){ 202 | 203 | try{ 204 | rdsItems.push({ 205 | identifier: item['DBInstanceIdentifier'], 206 | engine: item['Engine'] , 207 | version: item['EngineVersion'] , 208 | az: item['AvailabilityZone'], 209 | size: item['DBInstanceClass'], 210 | status: item['DBInstanceStatus'], 211 | multiaz: item['MultiAZ'], 212 | pi: item['PerformanceInsightsEnabled'], 213 | resourceId: item['DbiResourceId'], 214 | storage: item['StorageType'], 215 | storageSize: item['AllocatedStorage'], 216 | username: item['MasterUsername'], 217 | endpoint: item['Endpoint']['Address'], 218 | port: item['Endpoint']['Port'], 219 | instance : item['DBName'] 220 | 221 | }); 222 | 223 | } 224 | catch{ 225 | console.log('Timeout API error : /api/aws/rds/instance/region/list/'); 226 | } 227 | 228 | } 229 | 230 | }) 231 | 232 | 233 | } 234 | catch{ 235 | console.log('Timeout API error : /api/aws/rds/instance/region/list/'); 236 | } 237 | 238 | setDataRds(rdsItems); 239 | if (rdsItems.length > 0 ) { 240 | setSelectedItems([rdsItems[0]]); 241 | setsplitPanelShow(true); 242 | } 243 | 244 | } 245 | 246 | 247 | //-- Handle Object Events KeyDown 248 | const handleKeyDowntxtLogin= (event) => { 249 | if (event.detail.key === 'Enter') { 250 | handleClickLogin(); 251 | } 252 | } 253 | 254 | 255 | 256 | 257 | 258 | //-- Init Function 259 | 260 | // eslint-disable-next-line 261 | useEffect(() => { 262 | gatherInstances(); 263 | // eslint-disable-next-line react-hooks/exhaustive-deps 264 | }, []); 265 | 266 | 267 | return ( 268 |
269 | 270 | } 273 | splitPanelOpen={splitPanelShow} 274 | onSplitPanelToggle={() => setsplitPanelShow(false)} 275 | splitPanelSize={350} 276 | splitPanel={ 277 | 287 | 288 | 289 | } 290 | 291 | > 292 | {"Instance : " + selectedItems[0].identifier} 293 | 294 | 295 | } 296 | i18nStrings={splitPanelI18nStrings} closeBehavior="hide" 297 | onSplitPanelToggle={({ detail }) => { 298 | console.log(detail); 299 | } 300 | } 301 | > 302 | 303 | 304 |
305 | DB Identifier 306 | {selectedItems[0]['identifier']} 307 |
308 |
309 | Engine 310 | {selectedItems[0]['engine']} 311 |
312 |
313 | Version 314 | {selectedItems[0]['version']} 315 |
316 |
317 | Region & AZ 318 | {selectedItems[0]['az']} 319 |
320 |
321 |
322 |
323 | 324 |
325 | Master User 326 | {selectedItems[0]['username']} 327 |
328 |
329 | Endpoint 330 | {selectedItems[0]['endpoint']} 331 |
332 |
333 | Port 334 | {selectedItems[0]['port']} 335 |
336 |
337 | Size 338 | {selectedItems[0]['size']} 339 |
340 | 341 |
342 |
343 |
344 | 345 |
346 | ResourceID 347 | {selectedItems[0]['resourceId']} 348 |
349 |
350 | Storage Type 351 | {selectedItems[0]['storage']} 352 |
353 |
354 | Storage Size(GB) 355 | {selectedItems[0]['storageSize']} 356 |
357 |
358 | Instance 359 | {selectedItems[0]['instance']} 360 |
361 | 362 |
363 | 364 | 365 |
366 | } 367 | contentType="table" 368 | content={ 369 | <> 370 |
371 | { 380 | setSelectedItems(detail.selectedItems); 381 | setsplitPanelShow(true); 382 | } 383 | } 384 | selectedItems={selectedItems} 385 | empty={ 386 | 387 | No records 388 | 393 | No records to display. 394 | 395 | 396 | } 397 | resizableColumns 398 | header={ 399 |
407 | 408 | 409 | 410 | } 411 | 412 | > 413 | Instances 414 |
415 | } 416 | 417 | 418 | /> 419 | 420 | 421 | setModalConnectVisible(false)} 423 | visible={modalConnectVisible} 424 | closeAriaLabel="Close modal" 425 | footer={ 426 | 427 | 428 | 429 | 430 | 431 | 432 | } 433 | header={ 434 |
437 | {"Instance : " + selectedItems[0].identifier } 438 |
439 | 440 | } 441 | > 442 | 445 | settxtUser(event.detail.value)} 446 | 447 | /> 448 | 449 | 450 | 453 | settxtPassword(event.detail.value)} onKeyDown={handleKeyDowntxtLogin} 454 | type="password" 455 | /> 456 | 457 | 458 | 459 |
460 | 461 | 462 | 463 | 464 | } 465 | /> 466 | 467 | 468 | ); 469 | } 470 | 471 | export default Login; 472 | -------------------------------------------------------------------------------- /frontend/src/pages/Logout.js: -------------------------------------------------------------------------------- 1 | import { useAuthenticator } from '@aws-amplify/ui-react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const App = () => { 5 | const navigate = useNavigate(); 6 | 7 | const { signOut } = useAuthenticator((context) => [context.user]); 8 | signOut(); 9 | navigate('/login'); 10 | 11 | return ( 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/styles/css/base.css: -------------------------------------------------------------------------------- 1 | @use '~@cloudscape-design/design-tokens' as cs; 2 | 3 | body { 4 | background: cs.$color-background-layout-main; 5 | position: relative; 6 | } 7 | 8 | .custom-main-header { 9 | display: block; 10 | position: sticky; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | z-index: 1000; 15 | margin: 0; 16 | background-color: #0f1b2a; 17 | font-family: cs.$font-family-base; 18 | } 19 | 20 | ul.menu-list { 21 | display: flex; 22 | align-items: center; 23 | height: 40px; 24 | margin: 0; 25 | padding: 0 40px; 26 | list-style: none; 27 | font-size: 14px; 28 | 29 | & > li { 30 | padding: 0; 31 | margin: 0; 32 | margin-right: 8px; 33 | 34 | > a { 35 | padding: 0 6px; 36 | } 37 | 38 | a, 39 | div, 40 | button, 41 | input, 42 | label { 43 | float: left; 44 | color: cs.$color-text-interactive-default; 45 | line-height: 16px; 46 | } 47 | 48 | #visual-refresh-toggle { 49 | margin-right: 5px; 50 | margin-top: 1px; 51 | } 52 | 53 | a, 54 | a:hover { 55 | cursor: pointer; 56 | text-decoration: none; 57 | } 58 | 59 | &.title { 60 | font-weight: bold; 61 | } 62 | } 63 | 64 | @media only screen and (max-width: 493px) { 65 | padding: 4px 20px; 66 | flex-wrap: wrap; 67 | height: fit-content; 68 | 69 | .title { 70 | flex: 1 1 100%; 71 | margin-bottom: 8px; 72 | } 73 | 74 | li { 75 | width: min-content; 76 | 77 | button, 78 | a { 79 | text-align: left; 80 | } 81 | 82 | a { 83 | padding: 0; 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /frontend/src/styles/css/default.css: -------------------------------------------------------------------------------- 1 | #tableflat { 2 | font-family: Arial, Helvetica, sans-serif; 3 | border-collapse: collapse; 4 | width: 100%; 5 | padding: 1em 6 | } 7 | 8 | #tableflat td, #tableflat th { 9 | border: 1px solid #eaeded; 10 | padding: 8px; 11 | } 12 | 13 | 14 | 15 | #tableflat th { 16 | padding-top: 12px; 17 | padding-bottom: 12px; 18 | text-align: left; 19 | background-color: #fafafa; 20 | color: black; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/styles/css/top-navigation.css: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | #h { 5 | z-index: 1002; 6 | } 7 | 8 | .menu-list { 9 | border-bottom: 1px solid #414750; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /images/img01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/img01.png -------------------------------------------------------------------------------- /images/img02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/img02.png -------------------------------------------------------------------------------- /images/img03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/img03.png -------------------------------------------------------------------------------- /images/img04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/img04.png -------------------------------------------------------------------------------- /images/img05.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/img05.gif -------------------------------------------------------------------------------- /images/launch-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/launch-stack.png -------------------------------------------------------------------------------- /images/vid02.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-top-monitoring/e7c3a93b6532737431314536914ea025cfcaf99d/images/vid02.mp4 -------------------------------------------------------------------------------- /server/api.core.js: -------------------------------------------------------------------------------- 1 | // AWS API Variables 2 | const fs = require('fs'); 3 | var configData = JSON.parse(fs.readFileSync('./aws-exports.json')); 4 | 5 | // API Application Variables 6 | const express = require('express'); 7 | const cors = require('cors') 8 | const uuid = require('uuid'); 9 | 10 | const app = express(); 11 | const port = configData.aws_api_port; 12 | app.use(cors()); 13 | app.use(express.json()) 14 | 15 | // API Protection 16 | var cookieParser = require('cookie-parser') 17 | var csrf = require('csurf') 18 | var bodyParser = require('body-parser') 19 | const csrfProtection = csrf({ 20 | cookie: true, 21 | }); 22 | 23 | app.use(bodyParser.json()); 24 | app.use(bodyParser.urlencoded({ extended: false })); 25 | app.use(cookieParser()); 26 | app.use(csrfProtection); 27 | 28 | 29 | 30 | // AWS Variables 31 | var AWS = require('aws-sdk'); 32 | AWS.config.update({region: configData.aws_region}); 33 | 34 | var rds = new AWS.RDS(); 35 | var cloudwatch = new AWS.CloudWatch(); 36 | var cloudwatchlogs = new AWS.CloudWatchLogs(); 37 | 38 | 39 | // Security Variables 40 | const crypto = require('crypto'); 41 | const jwt = require('jsonwebtoken'); 42 | var jwkToPem = require('jwk-to-pem'); 43 | var request = require('request'); 44 | var secretKey = crypto.randomBytes(32).toString('hex') 45 | var pems; 46 | var issCognitoIdp = "https://cognito-idp." + configData.aws_region + ".amazonaws.com/" + configData.aws_cognito_user_pool_id; 47 | 48 | 49 | // Mysql Variables 50 | const mysql = require('mysql') 51 | var db=[]; 52 | 53 | 54 | // Postgresql Variables 55 | const postgresql = require('pg').Pool 56 | 57 | 58 | // SQLServer Variables 59 | const mssql = require('mssql') 60 | 61 | // ORACLE Variables 62 | const oracle = require('oracledb') 63 | 64 | 65 | // Startup - Download PEMs Keys 66 | gatherPemKeys(issCognitoIdp); 67 | 68 | 69 | //--#################################################################################################### 70 | // ---------------------------------------- SECURITY 71 | //--#################################################################################################### 72 | 73 | 74 | //-- Generate new standard token 75 | function generateToken(tokenData){ 76 | const token = jwt.sign(tokenData, secretKey, { expiresIn: 60 * 60 * configData.aws_token_expiration }); 77 | return token ; 78 | }; 79 | 80 | 81 | //-- Verify standard token 82 | const verifyToken = (token) => { 83 | 84 | try { 85 | const decoded = jwt.verify(token, secretKey); 86 | return {isValid : true, session_id: decoded.session_id}; 87 | } 88 | catch (ex) { 89 | return {isValid : false, session_id: ""}; 90 | } 91 | 92 | }; 93 | 94 | 95 | //-- Gather PEMs keys from Cognito 96 | function gatherPemKeys(iss) 97 | { 98 | 99 | if (!pems) { 100 | //Download the JWKs and save it as PEM 101 | return new Promise((resolve, reject) => { 102 | request({ 103 | url: iss + '/.well-known/jwks.json', 104 | json: true 105 | }, function (error, response, body) { 106 | 107 | if (!error && response.statusCode === 200) { 108 | pems = {}; 109 | var keys = body['keys']; 110 | for(var i = 0; i < keys.length; i++) { 111 | //Convert each key to PEM 112 | var key_id = keys[i].kid; 113 | var modulus = keys[i].n; 114 | var exponent = keys[i].e; 115 | var key_type = keys[i].kty; 116 | var jwk = { kty: key_type, n: modulus, e: exponent}; 117 | var pem = jwkToPem(jwk); 118 | pems[key_id] = pem; 119 | } 120 | } else { 121 | //Unable to download JWKs, fail the call 122 | console.log("error"); 123 | } 124 | 125 | resolve(body); 126 | 127 | }); 128 | }); 129 | 130 | } 131 | 132 | 133 | } 134 | 135 | 136 | //-- Validate Cognito Token 137 | function verifyTokenCognito(token) { 138 | 139 | try { 140 | //Fail if the token is not jwt 141 | var decodedJwt = jwt.decode(token, {complete: true}); 142 | if (!decodedJwt) { 143 | console.log("Not a valid JWT token"); 144 | return {isValid : false, session_id: ""}; 145 | } 146 | 147 | 148 | if (decodedJwt.payload.iss != issCognitoIdp) { 149 | console.log("invalid issuer"); 150 | return {isValid : false, session_id: ""}; 151 | } 152 | 153 | //Reject the jwt if it's not an 'Access Token' 154 | if (decodedJwt.payload.token_use != 'access') { 155 | console.log("Not an access token"); 156 | return {isValid : false, session_id: ""}; 157 | } 158 | 159 | //Get the kid from the token and retrieve corresponding PEM 160 | var kid = decodedJwt.header.kid; 161 | var pem = pems[kid]; 162 | if (!pem) { 163 | console.log('Invalid access token'); 164 | return {isValid : false, session_id: ""}; 165 | } 166 | 167 | const decoded = jwt.verify(token, pem, { issuer: issCognitoIdp }); 168 | return {isValid : true, session_id: ""}; 169 | } 170 | catch (ex) { 171 | console.log("Unauthorized Token"); 172 | return {isValid : false, session_id: ""}; 173 | } 174 | 175 | }; 176 | 177 | 178 | 179 | //-- Authenticate User Database 180 | app.post("/api/security/rds/auth/", csrfProtection, (req,res)=>{ 181 | 182 | // Token Validation 183 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 184 | 185 | if (cognitoToken.isValid === false) 186 | return res.status(511).send({ data: [], message : "Token is invalid"}); 187 | 188 | // API Call 189 | var params = req.body.params; 190 | 191 | try { 192 | 193 | switch(params.engine) 194 | { 195 | //-- MYSQL CONNECTION 196 | case 'mysql': 197 | case 'mariadb': 198 | case 'aurora-mysql': 199 | var dbconnection= mysql.createConnection({ 200 | host: params.host, 201 | user: params.username, 202 | password: params.password, 203 | port: params.port 204 | }) 205 | 206 | dbconnection.connect(function(err) { 207 | if (err) { 208 | res.status(200).send( {"result":"auth0", "session_id": 0}); 209 | } else { 210 | dbconnection.end(); 211 | var session_id=uuid.v4(); 212 | mysqlOpenConnection(session_id,params.host,params.port,params.username,params.password); 213 | 214 | var token = generateToken({ session_id: session_id}); 215 | res.status(200).send( {"result":"auth1", "session_id": session_id, "session_token": token }); 216 | } 217 | 218 | }); 219 | 220 | break; 221 | 222 | //-- POSTGRESQL CONNECTION 223 | case 'postgres': 224 | case 'aurora-postgresql': 225 | 226 | var dbconnection = new postgresql({ 227 | user: params.username, 228 | host: params.host, 229 | database: 'postgres', 230 | password: params.password, 231 | port: params.port, 232 | max: 1, 233 | }) 234 | dbconnection.connect(function(err) { 235 | if (err) { 236 | res.status(200).send( {"result":"auth0", "session_id": 0}); 237 | } else { 238 | dbconnection.end(); 239 | var session_id=uuid.v4(); 240 | postgresqlOpenConnection(session_id,params.host,params.port,params.username,params.password); 241 | 242 | var token = generateToken({ session_id: session_id}); 243 | res.status(200).send( {"result":"auth1", "session_id": session_id, "session_token": token}); 244 | } 245 | 246 | }); 247 | break; 248 | 249 | //-- MSSQL CONNECTION 250 | case 'sqlserver-se': 251 | case 'sqlserver-ee': 252 | case 'sqlserver-ex': 253 | case 'sqlserver-web': 254 | 255 | 256 | var dbconnection = new mssql.ConnectionPool({ 257 | user: params.username, 258 | password: params.password, 259 | server: params.host, 260 | database: 'master', 261 | port : params.port, 262 | pool: { 263 | max: 2, 264 | min: 0, 265 | idleTimeoutMillis: 30000 266 | }, 267 | options: { 268 | trustServerCertificate: true, 269 | } 270 | }); 271 | 272 | 273 | dbconnection.connect(function(err) { 274 | if (err) { 275 | res.status(200).send( {"result":"auth0", "session_id": 0}); 276 | } else { 277 | dbconnection.close(); 278 | var session_id=uuid.v4(); 279 | mssqlOpenConnection(session_id,params.host,params.port,params.username,params.password); 280 | 281 | var token = generateToken({ session_id: session_id}); 282 | res.status(200).send( {"result":"auth1", "session_id": session_id, "session_token": token}); 283 | } 284 | 285 | }); 286 | 287 | break; 288 | 289 | //-- ORACLE CONNECTION 290 | case 'oracle-ee': 291 | case 'oracle-ee-cdb': 292 | case 'oracle-se2': 293 | case 'oracle-se2-cdb': 294 | 295 | oracle.getConnection({ 296 | user: params.username, 297 | password: params.password, 298 | connectString: params.host + ":" + params.port + "/" + params.instance 299 | }, function(err,connection) { 300 | if (err) { 301 | console.log(err); 302 | res.status(200).send( {"result":"auth0", "session_id": 0}); 303 | } 304 | else { 305 | connection.close(function(err) { 306 | if (err) {console.log(err);} 307 | }); 308 | var session_id=uuid.v4(); 309 | oracleOpenConnection(session_id,params.host,params.port,params.username,params.password,params.instance); 310 | 311 | var token = generateToken({ session_id: session_id}); 312 | res.status(200).send( {"result":"auth1", "session_id": session_id, "session_token": token}); 313 | 314 | } 315 | 316 | }); 317 | 318 | break; 319 | 320 | } 321 | 322 | 323 | 324 | } catch(error) { 325 | console.log(error) 326 | res.status(200).send({"result":"auth0"}); 327 | 328 | } 329 | 330 | 331 | }); 332 | 333 | 334 | //-- Database Disconnection 335 | app.get("/api/security/rds/disconnect/", (req,res)=>{ 336 | 337 | // Token Validation 338 | var standardToken = verifyToken(req.headers['x-token']); 339 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 340 | 341 | if (standardToken.isValid === false || cognitoToken.isValid === false) 342 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 343 | 344 | 345 | // API Call 346 | 347 | try { 348 | 349 | switch(req.query.engine) 350 | { 351 | case 'mysql': 352 | case 'aurora-mysql': 353 | console.log("API MYSQL Disconnection - SessionID : " + req.query.session_id) 354 | db[req.query.session_id].end(); 355 | delete db[req.query.session_id]; 356 | res.status(200).send( {"result":"disconnected", "session_id": req.query.session_id}); 357 | break; 358 | 359 | case 'postgres': 360 | case 'aurora-postgresql': 361 | console.log("API POSTGRESQL Disconnection - SessionID : " + req.query.session_id) 362 | db[req.query.session_id].end(); 363 | delete db[req.query.session_id]; 364 | res.status(200).send( {"result":"disconnected", "session_id": req.query.session_id}); 365 | break; 366 | 367 | case 'mariadb': 368 | console.log("API MARIADB Disconnection - SessionID : " + req.query.session_id) 369 | db[req.query.session_id].end(); 370 | delete db[req.query.session_id]; 371 | res.status(200).send( {"result":"disconnected", "session_id": req.query.session_id}); 372 | break; 373 | 374 | case 'sqlserver-se': 375 | case 'sqlserver-ee': 376 | case 'sqlserver-ex': 377 | case 'sqlserver-web': 378 | console.log("API MSSQL Disconnection - SessionID : " + req.query.session_id) 379 | db[req.query.session_id].close(); 380 | delete db[req.query.session_id]; 381 | res.status(200).send( {"result":"disconnected", "session_id": req.query.session_id}); 382 | break; 383 | 384 | case 'oracle-ee': 385 | case 'oracle-ee-cdb': 386 | case 'oracle-se2': 387 | case 'oracle-se2-cdb': 388 | 389 | console.log("API ORACLE Disconnection - SessionID : " + req.query.session_id) 390 | db[req.query.session_id].close(function(err) { 391 | if (err) { 392 | delete db[req.query.session_id]; 393 | res.status(401).send( {"result":"disconnected", "session_id": req.query.session_id}); 394 | } 395 | else { 396 | delete db[req.query.session_id]; 397 | res.status(200).send( {"result":"disconnected", "session_id": req.query.session_id}); 398 | 399 | } 400 | 401 | }); 402 | 403 | break; 404 | 405 | } 406 | 407 | 408 | } 409 | 410 | catch(error) { 411 | 412 | res.status(200).send( {"result":"failed", "session_id": req.query.session_id}); 413 | console.log(error) 414 | 415 | } 416 | 417 | }); 418 | 419 | 420 | 421 | //--#################################################################################################### 422 | // ---------------------------------------- MSSQL 423 | //--#################################################################################################### 424 | 425 | // MSSQL : Create Connection 426 | function mssqlOpenConnection(session_id,host,port,user,password){ 427 | 428 | db[session_id] = new mssql.ConnectionPool({ 429 | user: user, 430 | password: password, 431 | server: host, 432 | database: 'master', 433 | port : port, 434 | pool: { 435 | max: 2, 436 | min: 0, 437 | idleTimeoutMillis: 30000 438 | }, 439 | options: { 440 | trustServerCertificate: true, 441 | } 442 | }); 443 | 444 | db[session_id].connect(function(err) { 445 | if (err) { 446 | console.log("mssql error connection"); 447 | } 448 | 449 | }); 450 | 451 | console.log("Mssql Connection opened for session_id : " + session_id); 452 | 453 | 454 | } 455 | 456 | 457 | // MSSQL : API Execute SQL Query 458 | app.get("/api/mssql/sql/", (req,res)=>{ 459 | 460 | // Token Validation 461 | var standardToken = verifyToken(req.headers['x-token']); 462 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 463 | 464 | if (standardToken.isValid === false || cognitoToken.isValid === false) 465 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 466 | 467 | 468 | // API Call 469 | var params = req.query; 470 | 471 | try { 472 | 473 | db[standardToken.session_id].query(params.sql_statement, (err,result)=>{ 474 | if(err) { 475 | console.log(err) 476 | res.status(404).send(err); 477 | } 478 | else { 479 | res.status(200).send(result); 480 | } 481 | 482 | } 483 | ); 484 | 485 | 486 | } catch(error) { 487 | console.log(error) 488 | 489 | } 490 | 491 | }); 492 | 493 | 494 | //--#################################################################################################### 495 | // ---------------------------------------- POSTGRESQL 496 | //--#################################################################################################### 497 | 498 | // POSTGRESQL : Create Connection 499 | function postgresqlOpenConnection(session_id,host,port,user,password){ 500 | 501 | db[session_id] = new postgresql({ 502 | host: host, 503 | user: user, 504 | password: password, 505 | database: "postgres", 506 | port: port, 507 | max: 2, 508 | }) 509 | 510 | console.log("Postgresql Connection opened for session_id : " + session_id); 511 | 512 | 513 | } 514 | 515 | 516 | // POSTGRESQL : API Execute SQL Query 517 | app.get("/api/postgres/sql/", (req,res)=>{ 518 | 519 | // Token Validation 520 | var standardToken = verifyToken(req.headers['x-token']); 521 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 522 | 523 | if (standardToken.isValid === false || cognitoToken.isValid === false) 524 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 525 | 526 | 527 | // API Call 528 | var params = req.query; 529 | 530 | try { 531 | 532 | db[standardToken.session_id].query(params.sql_statement, (err,result)=>{ 533 | if(err) { 534 | console.log(err) 535 | res.status(404).send(err); 536 | } 537 | else { 538 | res.status(200).send(result); 539 | } 540 | 541 | } 542 | ); 543 | 544 | 545 | } catch(error) { 546 | console.log(error) 547 | 548 | } 549 | 550 | }); 551 | 552 | 553 | 554 | //--#################################################################################################### 555 | // ---------------------------------------- MYSQL 556 | //--#################################################################################################### 557 | 558 | 559 | // MYSQL : Create Connection 560 | function mysqlOpenConnection(session_id,host,port,user,password){ 561 | 562 | db[session_id] = mysql.createPool({ 563 | host: host, 564 | user: user, 565 | password: password, 566 | database: "", 567 | acquireTimeout: 3000, 568 | port: port, 569 | connectionLimit:2 570 | }) 571 | 572 | console.log("Mysql Connection opened for session_id : " + session_id); 573 | 574 | } 575 | 576 | 577 | 578 | 579 | // MYSQL : API Execute SQL Query 580 | app.get("/api/mysql/sql/", (req,res)=>{ 581 | 582 | // Token Validation 583 | var standardToken = verifyToken(req.headers['x-token']); 584 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 585 | 586 | if (standardToken.isValid === false || cognitoToken.isValid === false) 587 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 588 | 589 | // API Call 590 | var params = req.query; 591 | 592 | try { 593 | 594 | db[standardToken.session_id].query(params.sql_statement, (err,result)=>{ 595 | if(err) { 596 | console.log(err) 597 | res.status(404).send(err); 598 | } 599 | else 600 | { 601 | res.status(200).send(result); 602 | } 603 | 604 | } 605 | ); 606 | 607 | 608 | } catch(error) { 609 | console.log(error) 610 | 611 | } 612 | 613 | }); 614 | 615 | 616 | 617 | //--#################################################################################################### 618 | // ---------------------------------------- ORACLE 619 | //--#################################################################################################### 620 | 621 | app.get('/api/oracle/sql/', function (req, res) { 622 | 623 | var standardToken = verifyToken(req.headers['x-token']); 624 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 625 | 626 | if (standardToken.isValid === false || cognitoToken.isValid === false) 627 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 628 | 629 | // API Call 630 | var params = req.query; 631 | db[standardToken.session_id].execute(params.sql_statement, (err,result)=>{ 632 | if(err) { 633 | console.log(err) 634 | res.status(404).send(err); 635 | } 636 | else { 637 | return res.status(200).send({ rows : result.rows, metadata : result.metaData }); 638 | } 639 | 640 | } 641 | ); 642 | 643 | }) 644 | 645 | // ORACLE : Create Connection 646 | async function oracleOpenConnection(session_id,host,port,user,password,instance){ 647 | 648 | try { 649 | db[session_id] = await oracle.getConnection({ 650 | user: user, 651 | password: password, 652 | connectString: host + ":" + port + "/" + instance 653 | }); 654 | } 655 | catch (err) { 656 | console.log(err.message); 657 | } 658 | console.log("Oracle Connection opened for session_id : " + session_id); 659 | 660 | } 661 | 662 | 663 | 664 | 665 | //--#################################################################################################### 666 | // ---------------------------------------- AWS 667 | //--#################################################################################################### 668 | 669 | 670 | // AWS : List Instances - by Region 671 | app.get("/api/aws/rds/instance/region/list/", (req,res)=>{ 672 | 673 | // Token Validation 674 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 675 | 676 | if (cognitoToken.isValid === false) 677 | return res.status(511).send({ data: [], message : "Token is invalid"}); 678 | 679 | // API Call 680 | var rds_region = new AWS.RDS({region: configData.aws_region}); 681 | 682 | var params = { 683 | MaxRecords: 100 684 | }; 685 | 686 | try { 687 | rds_region.describeDBInstances(params, function(err, data) { 688 | if (err) 689 | console.log(err, err.stack); // an error occurred 690 | res.status(200).send({ csrfToken: req.csrfToken(), data:data }); 691 | }); 692 | 693 | } catch(error) { 694 | console.log(error) 695 | 696 | } 697 | 698 | }); 699 | 700 | 701 | 702 | 703 | 704 | 705 | // AWS : Cloudwatch Information 706 | app.get("/api/aws/clw/query/", (req,res)=>{ 707 | 708 | var standardToken = verifyToken(req.headers['x-token']); 709 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 710 | 711 | if (standardToken.isValid === false || cognitoToken.isValid === false) 712 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 713 | 714 | try { 715 | 716 | var params = req.query; 717 | 718 | params.MetricDataQueries.forEach(function(metric) { 719 | metric.MetricStat.Metric.Dimensions[0]={ Name: metric.MetricStat.Metric.Dimensions[0]['[Name]'], Value: metric.MetricStat.Metric.Dimensions[0]['[Value]']}; 720 | 721 | }) 722 | 723 | cloudwatch.getMetricData(params, function(err, data) { 724 | if (err) 725 | console.log(err, err.stack); // an error occurred 726 | res.status(200).send(data); 727 | }); 728 | 729 | 730 | 731 | } catch(error) { 732 | console.log(error) 733 | 734 | } 735 | 736 | 737 | }); 738 | 739 | 740 | // AWS : Cloudwatch Information 741 | app.get("/api/aws/clw/region/query/", (req,res)=>{ 742 | 743 | var standardToken = verifyToken(req.headers['x-token']); 744 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 745 | 746 | if (standardToken.isValid === false || cognitoToken.isValid === false) 747 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 748 | 749 | try { 750 | 751 | var params = req.query; 752 | var cloudwatch_region = new AWS.CloudWatch({region: configData.aws_region, apiVersion: '2010-08-01'}); 753 | params.MetricDataQueries.forEach(function(metric) { 754 | 755 | for(i_dimension=0; i_dimension < metric.MetricStat.Metric.Dimensions.length; i_dimension++) { 756 | metric.MetricStat.Metric.Dimensions[i_dimension]={ Name: metric.MetricStat.Metric.Dimensions[i_dimension]['[Name]'], Value: metric.MetricStat.Metric.Dimensions[i_dimension]['[Value]']}; 757 | } 758 | }) 759 | 760 | cloudwatch_region.getMetricData(params, function(err, data) { 761 | if (err) 762 | console.log(err, err.stack); // an error occurred 763 | res.status(200).send(data); 764 | }); 765 | 766 | 767 | 768 | } catch(error) { 769 | console.log(error) 770 | 771 | } 772 | 773 | 774 | }); 775 | 776 | 777 | // AWS : Cloudwatch Information 778 | app.get("/api/aws/clw/region/logs/", (req,res)=>{ 779 | 780 | var standardToken = verifyToken(req.headers['x-token']); 781 | var cognitoToken = verifyTokenCognito(req.headers['x-token-cognito']); 782 | 783 | if (standardToken.isValid === false || cognitoToken.isValid === false) 784 | return res.status(511).send({ data: [], message : "Token is invalid. StandardToken : " + String(standardToken.isValid) + ", CognitoToken : " + String(cognitoToken.isValid) }); 785 | 786 | 787 | try { 788 | 789 | var params = req.query; 790 | var params_logs = { 791 | logStreamName: params.resource_id, 792 | limit: '1', 793 | logGroupName: 'RDSOSMetrics', 794 | startFromHead: false 795 | }; 796 | 797 | cloudwatchlogs.getLogEvents(params_logs, function(err, data) { 798 | if (err) 799 | console.log(err, err.stack); // an error occurred 800 | else { 801 | res.status(200).send(data); 802 | 803 | } 804 | }); 805 | 806 | 807 | 808 | 809 | 810 | } catch(error) { 811 | console.log(error) 812 | 813 | } 814 | 815 | 816 | }); 817 | 818 | 819 | 820 | //--#################################################################################################### 821 | // ---------------------------------------- MAIN API CORE 822 | //--#################################################################################################### 823 | 824 | 825 | app.listen(port, ()=>{ 826 | console.log(`Server is running on ${port}`) 827 | }) -------------------------------------------------------------------------------- /server/aws-exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "aws_region": "us-east-1", 3 | "aws_cognito_user_pool_id": "", 4 | "aws_cognito_user_pool_web_client_id": "", 5 | "aws_api_port": 3000, 6 | "aws_token_expiration": 24 7 | } 8 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "devStart": "nodemon index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "aws-sdk": "^2.1390.0", 15 | "body-parser": "^1.20.2", 16 | "cookie-parser": "^1.4.6", 17 | "cors": "^2.8.5", 18 | "csurf": "^1.11.0", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.0", 21 | "jwk-to-pem": "^2.0.5", 22 | "mssql": "^9.1.2", 23 | "mysql": "^2.18.1", 24 | "oracledb": "^6.0.3", 25 | "pg": "^8.9.0", 26 | "request": "^2.88.2", 27 | "uuid": "^9.0.0" 28 | } 29 | } 30 | --------------------------------------------------------------------------------