[Bps-public-commit] app-aws-cloudwatch-monitor branch master created. 4630892ac10503b0a8c395a40ac0d510f6a93651

BPS Git Server git at git.bestpractical.com
Tue Mar 8 14:35:36 UTC 2022


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "app-aws-cloudwatch-monitor".

The branch, master has been created
        at  4630892ac10503b0a8c395a40ac0d510f6a93651 (commit)

- Log -----------------------------------------------------------------
commit 4630892ac10503b0a8c395a40ac0d510f6a93651
Merge: 1977ee8 337f965
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Mar 8 08:33:45 2022 -0600

    Merge branch 'add-default-check-modules'

commit 337f965a100b4310fcf6989246495b8de3020813
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jan 26 18:18:38 2022 -0600

    Add README.md for display on GitHub

diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
index 60ea4b4..61c20fc 100644
--- a/MANIFEST.SKIP
+++ b/MANIFEST.SKIP
@@ -37,6 +37,8 @@ MYMETA\.json
 MYMETA\.yml$
 \.tar\.gz$$
 
+README.md
+
 # Author tests and files
 996_perl-tidy.t
 997_perl-critic.t
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..74e4e3b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+# NAME
+
+App::AWS::CloudWatch::Monitor - collect and send metrics to AWS CloudWatch
+
+# SYNOPSIS
+
+    use App::AWS::CloudWatch::Monitor;
+
+    my $monitor = App::AWS::CloudWatch::Monitor->new();
+    $monitor->run(\%opt, \@ARGV);
+
+    aws-cloudwatch-monitor [--check <module>]
+                           [--from-cron] [--verify] [--verbose]
+                           [--version] [--help]
+
+# DESCRIPTION
+
+`App::AWS::CloudWatch::Monitor` is an extensible framework for collecting and sending custom metrics to AWS CloudWatch from an AWS EC2 instance.
+
+For the commandline interface to `App::AWS::CloudWatch::Monitor`, see the documentation for [aws-cloudwatch-monitor](https://metacpan.org/pod/aws-cloudwatch-monitor).
+
+For adding check modules, see the documentation for [App::AWS::CloudWatch::Monitor::Check](https://metacpan.org/pod/App::AWS::CloudWatch::Monitor::Check).
+
+# CONSTRUCTOR
+
+- new
+
+    Returns a new `App::AWS::CloudWatch::Monitor` object.
+
+# METHODS
+
+- config
+
+    Returns the loaded config.
+
+- run
+
+    Loads and runs the specified check modules to gather metric data.
+
+    For options and arguments to `run`, see the documentation for [aws-cloudwatch-monitor](https://metacpan.org/pod/aws-cloudwatch-monitor).
+
+# CONFIGURATION
+
+To send metrics to AWS, you need to provide the access key id and secret access key for your configured AWS CloudWatch service.  You can set these in the file `config.ini`.
+
+An example is provided as part of this distribution.  The user running the metric script, like the user configured in cron for example, will need access to the configuration file.
+
+To set up the configuration file, copy `config.ini.example` into one of the following locations:
+
+- `$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini`
+- `/etc/aws-cloudwatch-monitor/config.ini`
+
+After creating the file, edit and update the values accordingly.
+
+    [aws]
+    aws_access_key_id = example
+    aws_secret_access_key = example
+
+**NOTE:** If the `$ENV{HOME}/.config/aws-cloudwatch-monitor/` directory exists, `config.ini` will be loaded from there regardless of a config file in `/etc/aws-cloudwatch-monitor/`.
+
+# KNOWN LIMITATIONS
+
+## AWS CloudWatch limits each upload to no more than 20 different metrics
+
+AWS CloudWatch will return a 400 response if attempting to upload more than 20 different metrics at once.
+
+[https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API\_PutMetricData.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html)
+
+A metrics collection can quickly exceed 20 metrics since each check module gathers multiple metrics.
+
+    aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs --check Memory --check DiskSpace --check Inode --disk-path /
+    Failed to call CloudWatch: HTTP 400. Message: The collection MetricData must not have a size greater than 20.
+
+Until this limitation is worked around in a future release of `App::AWS::CloudWatch::Monitor`, splitting the checks into separate [aws-cloudwatch-monitor](https://metacpan.org/pod/aws-cloudwatch-monitor) commands allows the uploads to succeed.
+
+    aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs
+    Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+    aws-cloudwatch-monitor --check Memory --check DiskSpace --check Inode --disk-path /
+    Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+# BUGS AND ENHANCEMENTS
+
+Please report any bugs or feature requests at [Github](https://github.com/bestpractical/app-aws-cloudwatch-monitor/issues).
+
+Please include in the bug report:
+
+- the operating system `aws-cloudwatch-monitor` is running on
+- the output of the command `aws-cloudwatch-monitor --version`
+- the command being run, error, and any additional steps to reproduce the issue
commit 181f33fda241b9cf985ca4a2d78d7797b2388049
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jan 26 18:14:48 2022 -0600

    Add additional information about --verbose
    
    The --verbose commandline option outputs the metrics payload to
    be sent to AWS, but is not apparent in the documentation.

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 900c950..40073c7 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -90,7 +90,7 @@ Checks configuration and prepares a remote call, but does not upload metrics to
 
 =item --verbose
 
-Print additional details while running.
+Print the metrics payload and additional details while running.
 
 =item --version
 
commit e613fe7228836b4b9bd0a9a5714eaf1e423f04f6
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Jan 10 18:17:20 2022 -0600

    Simplify imports in 00_load.t
    
    00_load.t was added as a simple safeguard to check for and BAIL_OUT
    during installation if deps were missing.
    
    The additional import and override within
    App::AWS::CloudWatch::Monitor::Test are unnecessary for 00_load.t
    as well as caused issue while requiring.

diff --git a/t/00_load.t b/t/00_load.t
index c6e483a..ec56611 100644
--- a/t/00_load.t
+++ b/t/00_load.t
@@ -2,11 +2,9 @@ use strict;
 use warnings;
 
 use FindBin;
-use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
-use App::AWS::CloudWatch::Monitor::Test;
-
 use File::Find ();
 use File::Spec ();
+use Test::More;
 
 foreach my $module (find_all_perl_modules()) {
     use_ok($module) or BAIL_OUT;
commit 05838efc6ac33e28d0c54fe3401210fe9782fd52
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Sat Dec 18 15:47:22 2021 -0600

    Add documentation for bugs and known issues

diff --git a/README b/README
index 1456363..5b68e73 100644
--- a/README
+++ b/README
@@ -60,3 +60,40 @@ CONFIGURATION
     exists, "config.ini" will be loaded from there regardless of a config
     file in "/etc/aws-cloudwatch-monitor/".
 
+KNOWN LIMITATIONS
+  AWS CloudWatch limits each upload to no more than 20 different metrics
+    AWS CloudWatch will return a 400 response if attempting to upload more
+    than 20 different metrics at once.
+
+    <https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_Pu
+    tMetricData.html>
+
+    A metrics collection can quickly exceed 20 metrics since each check
+    module gathers multiple metrics.
+
+     aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs --check Memory --check DiskSpace --check Inode --disk-path /
+     Failed to call CloudWatch: HTTP 400. Message: The collection MetricData must not have a size greater than 20.
+
+    Until this limitation is worked around in a future release of
+    "App::AWS::CloudWatch::Monitor", splitting the checks into separate
+    aws-cloudwatch-monitor commands allows the uploads to succeed.
+
+     aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs
+     Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+     aws-cloudwatch-monitor --check Memory --check DiskSpace --check Inode --disk-path /
+     Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+BUGS AND ENHANCEMENTS
+    Please report any bugs or feature requests at Github
+    <https://github.com/bestpractical/app-aws-cloudwatch-monitor/issues>.
+
+    Please include in the bug report:
+
+    *   the operating system "aws-cloudwatch-monitor" is running on
+
+    *   the output of the command "aws-cloudwatch-monitor --version"
+
+    *   the command being run, error, and any additional steps to reproduce
+        the issue
+
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 078421c..8b6048c 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -234,4 +234,41 @@ After creating the file, edit and update the values accordingly.
 
 B<NOTE:> If the C<$ENV{HOME}/.config/aws-cloudwatch-monitor/> directory exists, C<config.ini> will be loaded from there regardless of a config file in C</etc/aws-cloudwatch-monitor/>.
 
+=head1 KNOWN LIMITATIONS
+
+=head2 AWS CloudWatch limits each upload to no more than 20 different metrics
+
+AWS CloudWatch will return a 400 response if attempting to upload more than 20 different metrics at once.
+
+L<https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html>
+
+A metrics collection can quickly exceed 20 metrics since each check module gathers multiple metrics.
+
+ aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs --check Memory --check DiskSpace --check Inode --disk-path /
+ Failed to call CloudWatch: HTTP 400. Message: The collection MetricData must not have a size greater than 20.
+
+Until this limitation is worked around in a future release of C<App::AWS::CloudWatch::Monitor>, splitting the checks into separate L<aws-cloudwatch-monitor> commands allows the uploads to succeed.
+
+ aws-cloudwatch-monitor --check Process --process apache --process postgres --process master --process emacs
+ Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+ aws-cloudwatch-monitor --check Memory --check DiskSpace --check Inode --disk-path /
+ Successfully reported metrics to CloudWatch. Reference Id: <snip>
+
+=head1 BUGS AND ENHANCEMENTS
+
+Please report any bugs or feature requests at L<Github|https://github.com/bestpractical/app-aws-cloudwatch-monitor/issues>.
+
+Please include in the bug report:
+
+=over
+
+=item * the operating system C<aws-cloudwatch-monitor> is running on
+
+=item * the output of the command C<aws-cloudwatch-monitor --version>
+
+=item * the command being run, error, and any additional steps to reproduce the issue
+
+=back
+
 =cut
commit db974d9bd22dc21d1a2c4ca9ed3a16784d74676a
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Sat Dec 18 10:54:42 2021 -0600

    Update load test for running during make
    
    The load test was loading modules from within blib and lib causing
    redefined warnings.  Skip loading from the blib dir.
    
    Additionally, skip the Test* test modules, since we don't need to
    test loading those as part of the dist.

diff --git a/t/00_load.t b/t/00_load.t
index cc2f5f4..c6e483a 100644
--- a/t/00_load.t
+++ b/t/00_load.t
@@ -22,9 +22,10 @@ sub find_all_perl_modules {
         sub {
             my $file = $File::Find::name;
             return unless $file =~ /\.pm$/;
-            return if $file =~ /Test\.pm$/;
+            return if $file =~ /Test/;
 
             my $rel_path = File::Spec->abs2rel( $file, $base );
+            return if $rel_path =~ /^blib/;
             $rel_path =~ s/^[t\/]*lib\///;
             $rel_path =~ s/\//::/g;
             $rel_path =~ s/\.pm$//;
commit 8af97d2792a3cc7e2409830574fcde2ce31184a2
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Dec 16 18:47:51 2021 -0600

    Update module deps and min versions in Makefile

diff --git a/Makefile.PL b/Makefile.PL
index e2c79d6..6aa0cc7 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -23,32 +23,39 @@ WriteMakefile(
 
     MIN_PERL_VERSION => '5.10.1',
     BUILD_REQUIRES => {
-        'ExtUtils::MakeMaker' => '6.46',
+        'ExtUtils::MakeMaker' => '6.64',  # for TEST_REQUIRES
     },
     CONFIGURE_REQUIRES => {
-        'ExtUtils::MakeMaker' => '6.46',
+        'ExtUtils::MakeMaker' => '6.64',
     },
     TEST_REQUIRES => {
-        'Capture::Tiny' => 0,
+        'File::Find' => 0,
         'File::Spec' => 0,
-        'FindBin' => 0,
-        'Test::Exception' => 0,
-        'Test::More' => 0,
+        'Test::Exception' => '0.42',  # recommended by Test2
+        'Test::More' => '0.98',  # for subtest()
     },
     PREREQ_PM => {
         'base' => 0,
         'Capture::Tiny' => 0,
+        'Config::Tiny' => 0,
         'Compress::Zlib' => 0,
         'constant' => 0,
         'DateTime' => 0,
         'Digest::SHA' => 0,
+        'Exporter' => 0,
+        'File::Basename' => 0,
         'FindBin' => 0,
+        'Getopt::Long' => '2.45',  # 2.36 is required for options we need, but at least 2.45 for bugfixes
         'List::Util' => 0,
         'LWP' => 6,
         'LWP::Simple' => 0,
         'Module::Loader' => 0,
+        'parent' => 0,
+        'Pod::Usage' => '1.67',  # rewrite in 1.62, bugfixes in 1.67
+        'strict' => 0,
         'Try::Tiny' => 0,
         'URI::Escape' => 0,
+        'warnings' => 0,
     },
 
     EXE_FILES => [
commit 95dc26313a92ff5cd7e349c947d3e4b15596b4a9
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Dec 3 18:12:29 2021 -0600

    Fix process check math for zombie processes
    
    If the process being checked is marked as zombie (defunct), the
    ps output returns the <defunct> string in the second column where
    the pcpu column is expected to be.
    
    When the check module attempts to do math on a string, perl
    produces an "isn't numeric in addition" warning since the
    string doesn't "look like a number" to Perl.
    
    https://perldoc.perl.org/perlapi#looks_like_number
    
    This commit fixes the process check to ignore zombie processes by:
    
    - adding the stat column to the ps output
    - checking the stat column to see if the process is marked zombie
    - incrementing values only for processes not marked zombie

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
index 3a45b57..8142f81 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
@@ -32,7 +32,7 @@ sub check {
     die "Option: process is required" unless $opt{'process'};
 
     # TODO: investigate ps options to allow for different systems
-    my @ps_command = ( '/bin/ps', 'axco', 'command,pcpu,pmem' );
+    my @ps_command = ( '/bin/ps', 'axco', 'command,pcpu,pmem,stat' );
     my ( $exit, $stdout, $stderr ) = $self->run_command( \@ps_command );
 
     if ($exit) {
@@ -52,12 +52,24 @@ sub check {
         foreach my $line ( @{$stdout} ) {
             next unless $line =~ qr/^$process_name/;
 
-            $total_cnt += 1;
-
             my @fields = split /\s+/, $line;
 
-            $total_cpu += $fields[1];
-            $total_mem += $fields[2];
+            # don't count zombie (defunct) processes towards process metrics.
+            # zombie processes are finished and resources returned, so won't affect
+            # memory usage on the system in a noticable way.
+            # count every other process state as appropriate towards totals.
+
+            # COMMAND                     %CPU %MEM STAT
+            # zombie <defunct>             0.0  0.0 Z+
+
+            # with defunct processes, the field index count for cpu and mem are no
+            # longer accurate to count from the left.  decrement index count to check
+            # the last column for zombies before reading stats for cpu and mem.
+            if ( $fields[-1] !~ /^Z/ ) {
+                $total_cnt += 1;
+                $total_cpu += $fields[1];
+                $total_mem += $fields[2];
+            }
         }
 
         push @{$metrics},
commit 10e784f24d3a29dd784c3d94c556a1fdb0717ffa
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Dec 3 15:52:47 2021 -0600

    Add initial Changes file

diff --git a/Changes b/Changes
new file mode 100644
index 0000000..7184110
--- /dev/null
+++ b/Changes
@@ -0,0 +1,25 @@
+Revision history for perl module App::AWS::CloudWatch::Monitor
+
+0.01 YYYY-MM-DD
+ - Initial version
+
+ [Revisions to AwsSignatureV4.pm]
+ - Update formatting
+ - Update package namespace and VERSION
+ - Convert internal comments to pod
+ - Add NOTICE, LICENSE, and update copyright
+
+ [Revisions to CloudWatchClient.pm]
+ - Update formatting
+ - Update package namespace and VERSION
+ - Update open usage to modern Perl
+ - Add explicit returns from subroutines
+ - Avoid backtick operator in void context
+ - Update subroutine called with "&" sigil
+ - Update double sigil dereference
+ - Update indirect object call syntax
+ - Update reused variable name
+ - Convert internal comments to pod
+ - Add full namespace to AwsSignatureV4 calls
+ - Fix undef warning on chomp in read_meta_data
+ - Add NOTICE, LICENSE, and update copyright
diff --git a/MANIFEST b/MANIFEST
index ee9cb8e..6c34cae 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,5 @@
 bin/aws-cloudwatch-monitor
+Changes
 config.ini.example
 lib/App/AWS/CloudWatch/Monitor.pm
 lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
commit ef835a97d1c4cc36013a9af697388b71b5568cd7
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Dec 3 14:47:12 2021 -0600

    Add NOTICE, LICENSE, and update copyright
    
    This commit adds the Apache 2.0 license for all new files, as well
    as updates the AWS files with license headers.

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,203 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/MANIFEST b/MANIFEST
index 40431b4..ee9cb8e 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -10,9 +10,11 @@ lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
 lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
 lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
 lib/App/AWS/CloudWatch/Monitor/Config.pm
+LICENSE
 Makefile.PL
 MANIFEST			This list of files
 MANIFEST.SKIP
+NOTICE
 README
 t/00_load.t
 t/check-constants.t
diff --git a/Makefile.PL b/Makefile.PL
index f63d9a6..e2c79d6 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -5,7 +5,7 @@ WriteMakefile(
     AUTHOR => 'Best Practical Solutions, LLC <modules at bestpractical.com>',
     ABSTRACT_FROM => 'lib/App/AWS/CloudWatch/Monitor.pm',
     VERSION_FROM => 'lib/App/AWS/CloudWatch/Monitor.pm',
-    LICENSE => 'perl_5',
+    LICENSE => 'apache_2_0',
     META_MERGE => {
         'meta-spec' => { version => 2 },
         resources => {
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..7254e63
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,9 @@
+This product includes software developed at Amazon.com, Inc.
+
+* lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+* lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+
+Origin: https://aws-cloudwatch.s3.amazonaws.com/downloads/CloudWatchMonitoringScripts-1.2.2.zip
+License: Apache 2.0
+
+All other content, Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index f90cf80..900c950 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -1,5 +1,19 @@
 #!/usr/bin/env perl
 
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 use strict;
 use warnings;
 
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 83b2b33..078421c 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
index 0f11b88..c39bedd 100644
--- a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
@@ -1,3 +1,4 @@
+# Original file:
 # Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"). You may not
@@ -11,10 +12,20 @@
 # express or implied. See the License for the specific language governing
 # permissions and limitations under the License.
 
-# this package has been updated from the original version to:
-# Update formatting
-# Update package namespace and VERSION
-# Convert internal comments to pod
+# Additional development:
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
 package App::AWS::CloudWatch::Monitor::AwsSignatureV4;
 
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check.pm b/lib/App/AWS/CloudWatch/Monitor/Check.pm
index fe67a0e..24121ee 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
index 754a7c7..44e9e98 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check::DiskSpace;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
index 11eb018..d39f161 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check::Inode;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
index e22f6bb..9414c8c 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check::LoadAvg;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
index 2d40dd0..efbca99 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check::Memory;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
index f6c5913..3a45b57 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Check::Process;
 
 use strict;
diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 2387335..85eb584 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -1,3 +1,4 @@
+# Original file:
 # Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"). You may not
@@ -11,19 +12,20 @@
 # express or implied. See the License for the specific language governing
 # permissions and limitations under the License.
 
-# this package has been updated from the original version to:
-# Update formatting
-# Update package namespace and VERSION
-# Update open usage to modern Perl
-# Add explicit returns from subroutines
-# Avoid backtick operator in void context
-# Update subroutine called with "&" sigil
-# Update double sigil dereference
-# Update indirect object call syntax
-# Update reused variable name
-# Convert internal comments to pod
-# Add full namespace to AwsSignatureV4 calls
-# Fix undef warning on chomp in read_meta_data
+# Additional development:
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
diff --git a/lib/App/AWS/CloudWatch/Monitor/Config.pm b/lib/App/AWS/CloudWatch/Monitor/Config.pm
index 9dc2b43..03e9d70 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Config.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Config.pm
@@ -1,3 +1,17 @@
+# Copyright 2021 Best Practical Solutions, LLC <sales at bestpractical.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 package App::AWS::CloudWatch::Monitor::Config;
 
 use strict;
commit 6e29a57dd5273e3c0e106a267735adeaa04cfaf5
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Dec 2 18:39:39 2021 -0600

    Update project documentation
    
    This commit updates the documentation with better examples and
    instructions for setup.
    
    Additionally, generate the project README using
    App::AWS::CloudWatch::Monitor rather than aws-cloudwatch-monitor,
    which is more standard behavior for projects.

diff --git a/README b/README
index 4810952..1456363 100644
--- a/README
+++ b/README
@@ -1,80 +1,61 @@
 NAME
-    aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
+    App::AWS::CloudWatch::Monitor - collect and send metrics to AWS
+    CloudWatch
 
 SYNOPSIS
+     use App::AWS::CloudWatch::Monitor;
+
+     my $monitor = App::AWS::CloudWatch::Monitor->new();
+     $monitor->run(\%opt, \@ARGV);
+
      aws-cloudwatch-monitor [--check <module>]
                             [--from-cron] [--verify] [--verbose]
                             [--version] [--help]
 
 DESCRIPTION
-    "aws-cloudwatch-monitor" collects and sends custom metrics to AWS
-    CloudWatch from an AWS EC2 instance.
-
-OPTIONS
-    --check <module>
-        Defines the checks to run.
-
-        Multiple "--check" options may be defined and are run in the order
-        they're passed.
-
-    --from-cron
-        Specifies that this script is running from cron.
-
-        "--verbose" is forced to off and results information is suppressed
-        if "--from-cron" is set.
-
-        "--from-cron" additionally adds a random sleep interval up to 20
-        seconds.
-
-    --verify
-        Checks configuration and prepares a remote call, but does not upload
-        metrics to CloudWatch.
+    "App::AWS::CloudWatch::Monitor" is an extensible framework for
+    collecting and sending custom metrics to AWS CloudWatch from an AWS EC2
+    instance.
 
-    --verbose
-        Print additional details while running.
+    For the commandline interface to "App::AWS::CloudWatch::Monitor", see
+    the documentation for aws-cloudwatch-monitor.
 
-    --version
-        Print the version.
+    For adding check modules, see the documentation for
+    App::AWS::CloudWatch::Monitor::Check.
 
-    --help
-        Print the help menu.
+CONSTRUCTOR
+    new Returns a new "App::AWS::CloudWatch::Monitor" object.
 
-ADDITIONAL OPTIONS FOR CHECK MODULES
-    The check modules within this project may require additional options not
-    directly defined in "aws-cloudwatch-monitor".
+METHODS
+    config
+        Returns the loaded config.
 
-    All additional options defined on the commandline are passed to the
-    check modules.
+    run Loads and runs the specified check modules to gather metric data.
 
-    For example, the "DiskSpace" check module requires the "--disk-path"
-    option, which is passed through and verified in the check module itself.
-
-     aws-cloudwatch-monitor --check DiskSpace --disk-path /
-
-    If "--disk-path" isn't defined, the "DiskSpace" check module will warn
-    and skip gathering it's metrics.
-
-     aws-cloudwatch-monitor --check DiskSpace
-     error: Check::DiskSpace: Option: disk-path is required at lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm line 18.
-
-    Additional information about each check module can be found using the
-    "perldoc" program. For example, documentation for the included
-    "DiskSpace" check module can be read by running the following command:
-
-     perldoc App::AWS::CloudWatch::Monitor::Check::DiskSpace
+        For options and arguments to "run", see the documentation for
+        aws-cloudwatch-monitor.
 
 CONFIGURATION
-    An example configuration file, "config.ini.example", is provided in the
-    project root directory.
+    To send metrics to AWS, you need to provide the access key id and secret
+    access key for your configured AWS CloudWatch service. You can set these
+    in the file "config.ini".
+
+    An example is provided as part of this distribution. The user running
+    the metric script, like the user configured in cron for example, will
+    need access to the configuration file.
 
-    To set up the configuration file, copy the example into one of the
-    following locations:
+    To set up the configuration file, copy "config.ini.example" into one of
+    the following locations:
 
     "$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini"
     "/etc/aws-cloudwatch-monitor/config.ini"
 
     After creating the file, edit and update the values accordingly.
 
+     [aws]
+     aws_access_key_id = example
+     aws_secret_access_key = example
+
     NOTE: If the "$ENV{HOME}/.config/aws-cloudwatch-monitor/" directory
     exists, "config.ini" will be loaded from there regardless of a config
     file in "/etc/aws-cloudwatch-monitor/".
diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 4189269..f90cf80 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -50,7 +50,7 @@ aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
 
 =head1 DESCRIPTION
 
-C<aws-cloudwatch-monitor> collects and sends custom metrics to AWS CloudWatch from an AWS EC2 instance.
+C<aws-cloudwatch-monitor> is the commandline interface to L<App::AWS::CloudWatch::Monitor> for collecting and sending custom metrics to AWS CloudWatch from an AWS EC2 instance.
 
 =head1 OPTIONS
 
@@ -94,11 +94,11 @@ The check modules within this project may require additional options not directl
 
 All additional options defined on the commandline are passed to the check modules.
 
-For example, the C<DiskSpace> check module requires the C<--disk-path> option, which is passed through and verified in the check module itself.
+For example, the L<App::AWS::CloudWatch::Monitor::Check::DiskSpace> check module requires the C<--disk-path> option, which is passed through and verified in the check module itself.
 
  aws-cloudwatch-monitor --check DiskSpace --disk-path /
 
-If C<--disk-path> isn't defined, the C<DiskSpace> check module will warn and skip gathering it's metrics.
+If C<--disk-path> isn't defined, the C<DiskSpace> check module will warn and skip gathering its metrics.
 
  aws-cloudwatch-monitor --check DiskSpace
  error: Check::DiskSpace: Option: disk-path is required at lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm line 18.
@@ -109,9 +109,11 @@ Additional information about each check module can be found using the C<perldoc>
 
 =head1 CONFIGURATION
 
-An example configuration file, C<config.ini.example>, is provided in the project root directory.
+To send metrics to AWS, you need to provide the access key id and secret access key for your configured AWS CloudWatch service.  You can set these in the file C<config.ini>.
 
-To set up the configuration file, copy the example into one of the following locations:
+An example is provided as part of this distribution.  The user running the metric script, like the user configured in cron for example, will need access to the configuration file.
+
+To set up the configuration file, copy C<config.ini.example> into one of the following locations:
 
 =over
 
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index fe2ca63..83b2b33 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -156,10 +156,19 @@ App::AWS::CloudWatch::Monitor - collect and send metrics to AWS CloudWatch
  use App::AWS::CloudWatch::Monitor;
 
  my $monitor = App::AWS::CloudWatch::Monitor->new();
+ $monitor->run(\%opt, \@ARGV);
+
+ aws-cloudwatch-monitor [--check <module>]
+                        [--from-cron] [--verify] [--verbose]
+                        [--version] [--help]
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor> collects and sends custom metrics to AWS CloudWatch from an AWS EC2 instance.
+C<App::AWS::CloudWatch::Monitor> is an extensible framework for collecting and sending custom metrics to AWS CloudWatch from an AWS EC2 instance.
+
+For the commandline interface to C<App::AWS::CloudWatch::Monitor>, see the documentation for L<aws-cloudwatch-monitor>.
+
+For adding check modules, see the documentation for L<App::AWS::CloudWatch::Monitor::Check>.
 
 =head1 CONSTRUCTOR
 
@@ -183,6 +192,32 @@ Returns the loaded config.
 
 Loads and runs the specified check modules to gather metric data.
 
+For options and arguments to C<run>, see the documentation for L<aws-cloudwatch-monitor>.
+
 =back
 
+=head1 CONFIGURATION
+
+To send metrics to AWS, you need to provide the access key id and secret access key for your configured AWS CloudWatch service.  You can set these in the file C<config.ini>.
+
+An example is provided as part of this distribution.  The user running the metric script, like the user configured in cron for example, will need access to the configuration file.
+
+To set up the configuration file, copy C<config.ini.example> into one of the following locations:
+
+=over
+
+=item C<$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini>
+
+=item C</etc/aws-cloudwatch-monitor/config.ini>
+
+=back
+
+After creating the file, edit and update the values accordingly.
+
+ [aws]
+ aws_access_key_id = example
+ aws_secret_access_key = example
+
+B<NOTE:> If the C<$ENV{HOME}/.config/aws-cloudwatch-monitor/> directory exists, C<config.ini> will be loaded from there regardless of a config file in C</etc/aws-cloudwatch-monitor/>.
+
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check.pm b/lib/App/AWS/CloudWatch/Monitor/Check.pm
index 680f075..fe67a0e 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check.pm
@@ -75,11 +75,15 @@ App::AWS::CloudWatch::Monitor::Check - parent for Check modules
 
 C<App::AWS::CloudWatch::Monitor::Check> provides a contructor and methods for child modules.
 
-This module is not meant to be initialized directly, but through child modules.
+This module is not meant to be initialized directly.
 
 =head1 ADDING CHECK MODULES
 
-Additional check modules can be added as child modules to C<App::AWS::CloudWatch::Monitor::Check>.
+To add check functionality to L<App::AWS::CloudWatch::Monitor>, additional check modules can be created as child modules to C<App::AWS::CloudWatch::Monitor::Check>.
+
+For an example to start creating check modules, see L<App::AWS::CloudWatch::Monitor::Check::DiskSpace> or any check module released within this distribution.
+
+=head2 CHECK MODULE REQUIREMENTS
 
 Child modules must implement the C<check> method which gathers, formats, and returns the metrics.
 
@@ -162,3 +166,5 @@ Reads the specified file and returns an arrayref of the content.
 Returns the bytes constants for use in unit conversion.
 
 =back
+
+=cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
index cee10a7..754a7c7 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
@@ -98,17 +98,17 @@ App::AWS::CloudWatch::Monitor::Check::DiskSpace - gather disk metric data
 =head1 SYNOPSIS
 
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::DiskSpace->new();
- my $metrics = $plugin->check();
+ my $metrics = $plugin->check( $args_arrayref );
 
- perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path /
+ aws-cloudwatch-monitor --check DiskSpace --disk-path /
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers disk metric data.
+C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> is a L<App::AWS::CloudWatch::Monitor::Check> module which gathers disk metric data.
 
 =head1 METRICS
 
-The following metrics are gathered and returned.
+Data for this check is read from L<df(1)>.  The following metrics are returned.
 
 =over
 
@@ -134,10 +134,14 @@ Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricNa
 
 C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> requires the C<--disk-path> argument through the commandline.
 
- perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path /
+ aws-cloudwatch-monitor --check DiskSpace --disk-path /
 
 Multiple C<--disk-path> arguments may be defined to gather metrics for multiple paths.
 
- perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path / --disk-path /mnt/data
+ aws-cloudwatch-monitor --check DiskSpace --disk-path / --disk-path /mnt/data
+
+=head1 DEPENDENCIES
+
+C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> depends on the external program, L<df(1)>.
 
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
index ba47c9f..11eb018 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
@@ -72,17 +72,17 @@ App::AWS::CloudWatch::Monitor::Check::Inode - gather inode metric data
 =head1 SYNOPSIS
 
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::Inode->new();
- my $metrics = $plugin->check();
+ my $metrics = $plugin->check( $args_arrayref );
 
- perl bin/aws-cloudwatch-monitor --check Inode --disk-path /
+ aws-cloudwatch-monitor --check Inode --disk-path /
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::Inode> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers inode metric data.
+C<App::AWS::CloudWatch::Monitor::Check::Inode> is a L<App::AWS::CloudWatch::Monitor::Check> module which gathers inode metric data.
 
 =head1 METRICS
 
-The following metrics are gathered and returned.
+Data for this check is read from L<df(1)>.  The following metrics are returned.
 
 =over
 
@@ -104,10 +104,14 @@ Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricNa
 
 C<App::AWS::CloudWatch::Monitor::Check::Inode> requires the C<--disk-path> argument through the commandline.
 
- perl bin/aws-cloudwatch-monitor --check Inode --disk-path /
+ aws-cloudwatch-monitor --check Inode --disk-path /
 
 Multiple C<--disk-path> arguments may be defined to gather metrics for multiple paths.
 
- perl bin/aws-cloudwatch-monitor --check Inode --disk-path / --disk-path /mnt/data
+ aws-cloudwatch-monitor --check Inode --disk-path / --disk-path /mnt/data
+
+=head1 DEPENDENCIES
+
+C<App::AWS::CloudWatch::Monitor::Check::Inode> depends on the external program, L<df(1)>.
 
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
index 7ebde7d..e22f6bb 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
@@ -82,13 +82,15 @@ App::AWS::CloudWatch::Monitor::Check::LoadAvg - gather load average metric data
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::LoadAvg->new();
  my $metrics = $plugin->check();
 
+ aws-cloudwatch-monitor --check LoadAvg
+
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::LoadAvg> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers load average metric data.
+C<App::AWS::CloudWatch::Monitor::Check::LoadAvg> is a L<App::AWS::CloudWatch::Monitor::Check> module which gathers load average metric data.
 
 =head1 METRICS
 
-The following metrics are gathered and returned.
+Data for this check is read from C</proc/loadavg>.  The following metrics are returned.
 
 =over
 
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
index da42e7d..2d40dd0 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
@@ -81,13 +81,15 @@ App::AWS::CloudWatch::Monitor::Check::Memory - gather memory metric data
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::Memory->new();
  my $metrics = $plugin->check();
 
+ aws-cloudwatch-monitor --check Memory
+
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::Memory> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers memory metric data.
+C<App::AWS::CloudWatch::Monitor::Check::Memory> is a L<App::AWS::CloudWatch::Monitor::Check> module which gathers memory metric data.
 
 =head1 METRICS
 
-The following metrics are gathered and returned.
+Data for this check is read from C</proc/meminfo>.  The following metrics are returned.
 
 =over
 
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
index 2b0578f..f6c5913 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
@@ -17,6 +17,7 @@ sub check {
 
     die "Option: process is required" unless $opt{'process'};
 
+    # TODO: investigate ps options to allow for different systems
     my @ps_command = ( '/bin/ps', 'axco', 'command,pcpu,pmem' );
     my ( $exit, $stdout, $stderr ) = $self->run_command( \@ps_command );
 
@@ -79,25 +80,25 @@ App::AWS::CloudWatch::Monitor::Check::Process - gather process metric data
 =head1 SYNOPSIS
 
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::Process->new();
- my $metrics = $plugin->check();
+ my $metrics = $plugin->check( $args_arrayref );
 
- perl bin/aws-cloudwatch-monitor --check Process --process apache2
+ aws-cloudwatch-monitor --check Process --process apache2
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::Process> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers process metric data.
+C<App::AWS::CloudWatch::Monitor::Check::Process> is a L<App::AWS::CloudWatch::Monitor::Check> module which gathers process metric data.
 
 =head1 METRICS
 
-The following metrics are gathered and returned.
+Data for this check is read from L<ps(1)>.  The following metrics are returned.
 
 =over
 
-=item $process-Count
+=item [process-name]-Count
 
-=item $process-CpuUtilization
+=item [process-name]-CpuUtilization
 
-=item $process-MemoryUtilization
+=item [process-name]-MemoryUtilization
 
 =back
 
@@ -115,10 +116,14 @@ Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricNa
 
 C<App::AWS::CloudWatch::Monitor::Check::Process> requires the C<--process> argument through the commandline.
 
- perl bin/aws-cloudwatch-monitor --check Process --process apache2
+ aws-cloudwatch-monitor --check Process --process apache2
 
 Multiple C<--process> arguments may be defined to gather metrics for multiple processes.
 
- perl bin/aws-cloudwatch-monitor --check Process --process apache2 --process postgres
+ aws-cloudwatch-monitor --check Process --process apache2 --process postgres
+
+=head1 DEPENDENCIES
+
+C<App::AWS::CloudWatch::Monitor::Check::Process> depends on the external program, L<ps(1)>.
 
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Config.pm b/lib/App/AWS/CloudWatch/Monitor/Config.pm
index f95c8ed..9dc2b43 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Config.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Config.pm
@@ -72,7 +72,7 @@ App::AWS::CloudWatch::Monitor::Config - load and verify the config
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Config> loads settings for C<App::AWS::CloudWatch::Monitor>.
+C<App::AWS::CloudWatch::Monitor::Config> loads settings for L<App::AWS::CloudWatch::Monitor>.
 
 =head1 SUBROUTINES
 
@@ -86,9 +86,11 @@ Load and verify the config.
 
 =head1 CONFIGURATION
 
-An example configuration file, C<config.ini.example>, is provided in the project root directory.
+To send metrics to AWS, you need to provide the access key id and secret access key for your configured AWS CloudWatch service.  You can set these in the file C<config.ini>.
 
-To set up the configuration file, copy the example into one of the following locations:
+An example is provided as part of this distribution.  The user running the metric script, like the user configured in cron for example, will need access to the configuration file.
+
+To set up the configuration file, copy C<config.ini.example> into one of the following locations:
 
 =over
 
@@ -100,6 +102,10 @@ To set up the configuration file, copy the example into one of the following loc
 
 After creating the file, edit and update the values accordingly.
 
+ [aws]
+ aws_access_key_id = example
+ aws_secret_access_key = example
+
 B<NOTE:> If the C<$ENV{HOME}/.config/aws-cloudwatch-monitor/> directory exists, C<config.ini> will be loaded from there regardless of a config file in C</etc/aws-cloudwatch-monitor/>.
 
 =cut
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
index 4b596a9..4166809 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
@@ -47,7 +47,7 @@ App::AWS::CloudWatch::Monitor::Check::TestArgs - test metric for tests
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::TestArgs> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+C<App::AWS::CloudWatch::Monitor::Check::TestArgs> is a L<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
 
 This test check module verifies the presence of the C<--test> arg and dies on failure, or returns a metric for each C<--test>.
 
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
index 308e32f..53d741f 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
@@ -36,7 +36,7 @@ App::AWS::CloudWatch::Monitor::Check::TestSuccess - test metric for tests
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Check::TestSuccess> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+C<App::AWS::CloudWatch::Monitor::Check::TestSuccess> is a L<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
 
 This test check module doesn't verify args or run anything on the system, but only returns metric data in the expected format.
 
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
index 24810b7..22d2c1d 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
@@ -19,7 +19,6 @@ sub import {
         $class->builder->plan( skip_all => $args{skip_all} );
     }
 
-    # load the .aws-cloudwatch-monitor-rc file from t/ directory
     require FindBin;
     override(
         package => 'App::AWS::CloudWatch::Monitor::Config',
@@ -73,9 +72,9 @@ App::AWS::CloudWatch::Monitor::Test - testing module for App::AWS::CloudWatch::M
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor::Test> sets up the testing environment and modules needed for tests.
+C<App::AWS::CloudWatch::Monitor::Test> sets up the testing environment and modules for testing within the L<App::AWS::CloudWatch::Monitor> distribution.
 
-Methods from C<Test::More> and C<Test::Exception> are exported and available for the tests.
+Methods from L<Test::More> and L<Test::Exception> are exported and available for the tests.
 
 =head1 SUBROUTINES
 
commit d0d3f9ad6d687ceb37d75e1f54fc76e35722282f
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Dec 2 18:09:04 2021 -0600

    Allow pod links without text for perlcritic

diff --git a/t/perlcriticrc b/t/perlcriticrc
index f065624..b410ee0 100644
--- a/t/perlcriticrc
+++ b/t/perlcriticrc
@@ -198,3 +198,7 @@ max_mccabe = 30
 
 # Organize your POD into the customary sections.
 [-Documentation::RequirePodSections]
+
+# Don't require links to have text.
+# links to perl man pages are self explanatory.
+[-Documentation::RequirePodLinksIncludeText]
commit e6e02a6ad23479628707636d34354b5f217a2751
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Nov 15 15:35:06 2021 -0600

    Fix undef warning on chomp in read_meta_data
    
    Every time read_meta_data reads a metadata value, regardless if
    it's read from the cache or retrieved fresh from the special
    AWS mount, read_meta_data is re-writing the value back into the
    cache file.
    
    In situations where more than one aws-cloudwatch-monitor process
    is running, for example two cronjobs each running at the top of
    the minute, read_meta_data's frequent cache writing causes a
    race condition/collision where the value isn't written to the
    file as the other process reads it.
    
    If the code reads an empty value (in the case of the race
    condition), it will get a new value fresh from the instance (we'll
    never hit a situation where we don't have a value).
    
    More work needs to be done to improve the caching read/write so
    we're not writing the value again after every read (which is an
    inefficient cache design).  For now, this commit fixes the warning
    by not attempting to chomp a potentially undef value.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 0d35a39..2387335 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -23,6 +23,7 @@
 # Update reused variable name
 # Convert internal comments to pod
 # Add full namespace to AwsSignatureV4 calls
+# Fix undef warning on chomp in read_meta_data
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -157,7 +158,6 @@ sub read_meta_data {
         my $filename = $location . $resource;
         if ( -d $filename ) {
             $data_value = `/bin/ls $filename`;
-            chomp($data_value);
         }
         elsif ( -e $filename ) {
             my $updated  = ( stat($filename) )[9];
@@ -169,11 +169,11 @@ sub read_meta_data {
                     $data_value .= $line;
                 }
                 close $file_fh;
-                chomp $data_value;
             }
         }
     }
 
+    chomp($data_value) if $data_value;
     return $data_value;
 }
 
commit ea22ede717b7d2026fb56b3b15c6f3a5c1cadf80
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 17:27:14 2021 -0600

    Add remaining author tests
    
    The other author tests are now excluded from the dist MANIFEST.
    For completeness, the perl-tidy and perl-critic author tests are
    now in the repo.

diff --git a/t/996_perl-tidy.t b/t/996_perl-tidy.t
new file mode 100644
index 0000000..d4a8a51
--- /dev/null
+++ b/t/996_perl-tidy.t
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+
+use FindBin;
+use Test::More;
+
+unless ( $ENV{TEST_AUTHOR} ) {
+    my $msg = 'Author test. Set $ENV{TEST_AUTHOR} to a true value to run.';
+    plan( skip_all => $msg );
+}
+
+eval { require Test::PerlTidy; };
+
+if ($@) {
+    my $msg = 'Test::PerlTidy required to criticise code';
+    plan( skip_all => $msg );
+}
+
+Test::PerlTidy::run_tests(
+    path       => "$FindBin::RealBin/../lib",
+    perltidyrc => "$FindBin::RealBin/perltidyrc",
+);
diff --git a/t/997_perl-critic.t b/t/997_perl-critic.t
new file mode 100644
index 0000000..40637c2
--- /dev/null
+++ b/t/997_perl-critic.t
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+use FindBin;
+use File::Spec;
+use Test::More;
+
+unless ( $ENV{TEST_AUTHOR} ) {
+    my $msg = 'Author test. Set $ENV{TEST_AUTHOR} to a true value to run.';
+    plan( skip_all => $msg );
+}
+
+eval { require Test::Perl::Critic; };
+
+if ($@) {
+    my $msg = 'Test::Perl::Critic required to criticise code';
+    plan( skip_all => $msg );
+}
+
+my $rcfile = File::Spec->catfile( 't', 'perlcriticrc' );
+Test::Perl::Critic->import( -profile => $rcfile );
+
+all_critic_ok("$FindBin::RealBin/../lib");
diff --git a/t/perlcriticrc b/t/perlcriticrc
new file mode 100644
index 0000000..f065624
--- /dev/null
+++ b/t/perlcriticrc
@@ -0,0 +1,200 @@
+### PERLCRITIC OPTIONS
+color = 1
+verbose = %m at %f line %l [%p]\n
+severity = 2
+force = 1
+
+
+### POLICY SETTINGS
+# we don't unpack @_ right away as we mostly use named vars with defaults
+[-Subroutines::RequireArgUnpacking]
+
+# errstr for DBI is okay, and probably others since I like that convention.
+[-Variables::ProhibitPackageVars]
+#packages = DBI
+
+# HEREDOC doesn't adhere to my tidy eye
+[-ValuesAndExpressions::ProhibitImplicitNewlines]
+
+# don't require extended format for shortish regex
+# keeping this in here for documentation purposes
+[-RegularExpressions::RequireExtendedFormatting]
+#minimum_regex_length_to_complain_about = 60
+
+# don't require Carp
+[-ErrorHandling::RequireCarping]
+
+# postfix is okay, unless it's really weird
+[-ControlStructures::ProhibitPostfixControls]
+
+# needless ruleset for modern Perl versions
+[-ValuesAndExpressions::ProhibitInterpolationOfLiterals]
+
+# don't use backticks
+[InputOutput::ProhibitBacktickOperators]
+only_in_void_context = 1
+
+# 3 arg open is modern Perl
+[InputOutput::ProhibitTwoArgOpen]
+
+# close filehandles as soon as possible after opening them.
+[InputOutput::RequireBriefOpen]
+lines = 17
+
+# forbid a bare `## no critic'
+# shouldn't be turning off critic anyway
+[Miscellanea::ProhibitUnrestrictedNoCritic]
+
+# Minimize complexity in code that is outside of subroutines.
+[-Modules::ProhibitExcessMainComplexity]
+max_mccabe = 28
+
+# write `require Module' instead of `require 'Module.pm''.
+[Modules::RequireBarewordIncludes]
+
+# end each module with an explicitly `1;' instead of some funky expression.
+# sorry Charles ;)
+[Modules::RequireEndWithOne]
+
+# always make the 'package' explicit.
+[Modules::RequireExplicitPackage]
+
+# package declaration must match filename.
+[Modules::RequireFilenameMatchesPackage]
+
+# Give every module a `$VERSION' number.
+[Modules::RequireVersionVar]
+
+# don't use vague variable or subroutine names like 'last' or 'record'.
+[NamingConventions::ProhibitAmbiguousNames]
+forbid = last left right no abstract contract record second close
+
+# write `@{ $array_ref }' instead of `@$array_ref'.
+# consistency mostly.  I don't really care either way, but should be consistent.
+[References::ProhibitDoubleSigils]
+
+## REGEX
+[RegularExpressions::ProhibitUnusedCapture]
+[RegularExpressions::ProhibitUnusualDelimiters]
+[-RegularExpressions::RequireDotMatchAnything]
+[-RegularExpressions::RequireLineBoundaryMatching]
+[-RegularExpressions::ProhibitEnumeratedClasses]
+[-RegularExpressions::ProhibitFixedStringMatches]
+
+# don't name things the same as other things
+[Subroutines::ProhibitBuiltinHomonyms]
+
+# too many arguments
+[Subroutines::ProhibitManyArgs]
+
+# don't write `sub my_function (@@) {}'.
+[Subroutines::ProhibitSubroutinePrototypes]
+
+# prevent unused private subroutines.
+[Subroutines::ProhibitUnusedPrivateSubroutines]
+
+# prevent access to private subs in other packages.
+[Subroutines::ProtectPrivateSubs]
+
+# end every path through a subroutine with an explicit `return' statement.
+[Subroutines::RequireFinalReturn]
+
+# shouldn't be turning these off or not defining these
+[TestingAndDebugging::ProhibitNoStrict]
+[TestingAndDebugging::ProhibitNoWarnings]
+[TestingAndDebugging::RequireUseStrict]
+[TestingAndDebugging::RequireUseWarnings]
+
+# unless is okay sometimes
+[-ControlStructures::ProhibitUnlessBlocks]
+
+# don't require constants inplace of magic variables
+# I'm still on the fence about this
+[-ValuesAndExpressions::ProhibitMagicNumbers]
+
+# don't prohibit constant pragma
+[-ValuesAndExpressions::ProhibitConstantPragma]
+
+# don't prohibit long numbers
+[-ValuesAndExpressions::RequireNumberSeparators]
+
+# don't require check of close
+[-InputOutput::RequireCheckedClose]
+
+# allow error strings from syscalls
+[Variables::ProhibitPunctuationVars]
+allow = $! $0
+
+# don't care about noisy quotes
+[-ValuesAndExpressions::ProhibitNoisyQuotes]
+
+# don't care about empty quotes
+[-ValuesAndExpressions::ProhibitEmptyQuotes]
+
+# set sub complexity to 30
+[Subroutines::ProhibitExcessComplexity]
+max_mccabe = 30
+
+# dont prohibit C style for loops
+[-ControlStructures::ProhibitCStyleForLoops]
+
+
+## TODO: re-evaluate these
+# when working on old modules, export is okay
+[-Modules::ProhibitAutomaticExportation]
+
+# Write ` !$foo && $bar || $baz ' instead of ` not $foo && $bar or $baz'.
+[ValuesAndExpressions::ProhibitMixedBooleanOperators]
+
+# Use `my' instead of `local', except when you have to.
+[Variables::ProhibitLocalVars]
+
+# Don't ask for storage you don't need.
+[Variables::ProhibitUnusedVariables]
+
+# don't use 'grep' in void contexts.
+[BuiltinFunctions::ProhibitVoidGrep]
+
+# don't use 'map' in void contexts.
+[BuiltinFunctions::ProhibitVoidMap]
+
+# don't use 'grep' in boolean context
+[BuiltinFunctions::ProhibitBooleanGrep]
+
+# Write `bless {}, $class;' instead of just `bless {};'.
+[ClassHierarchies::ProhibitOneArgBless]
+
+# Use spaces instead of tabs.
+[CodeLayout::ProhibitHardTabs]
+
+# Don't use whitespace at the end of lines.
+[CodeLayout::ProhibitTrailingWhitespace]
+
+# Use the same newline through the source.
+[CodeLayout::RequireConsistentNewlines]
+
+# Must run code through perltidy.
+[CodeLayout::RequireTidyCode]
+
+# Put a comma at the end of every multi-line list declaration, including the last one.
+[CodeLayout::RequireTrailingCommas]
+
+# Don't write long "if-elsif-elsif-elsif-elsif...else" chains.
+[ControlStructures::ProhibitCascadingIfElse]
+
+# Don't use operators like `not', `!~', and `le' within `until' and `unless'.
+[ControlStructures::ProhibitNegativeExpressionsInUnlessAndUntilConditions]
+
+# Don't write code after an unconditional `die, exit, or next'.
+[ControlStructures::ProhibitUnreachableCode]
+
+
+## POD
+# The `=head1 NAME' section should match the package.
+# [Documentation::RequirePackageMatchesPodName]
+
+# All POD should be after `__END__'.
+# [Documentation::RequirePodAtEnd]
+
+# Organize your POD into the customary sections.
+[-Documentation::RequirePodSections]
diff --git a/t/perltidyrc b/t/perltidyrc
new file mode 100644
index 0000000..4348b2a
--- /dev/null
+++ b/t/perltidyrc
@@ -0,0 +1,34 @@
+-l=140   # Max line width is 140 cols
+-i=4     # Indent level is 4 cols
+-ci=4    # Continuation indent is 4 cols
+-vt=4    # Maximal vertical tightness
+-cti=0   # No extra indentation for closing brackets
+-pt=1    # Medium parenthesis tightness
+-sbt=1   # Medium square bracket tightness
+-bt=1    # Medium brace tightness (for non-code blocks)
+-bbt=1   # Medium block brace tightness (for code blocks)
+--nospace-for-semicolon
+-nolq    # Don't outdent long quoted strings
+         # Break before all operators:
+-wbb="% + - * / x != == >= <= =~ < > | & **= += *= &= <<= &&= -= /= |= >>= ||= .= %= ^= x="
+
+-fpsc=0  # Don't try to line up all comments to a fixed column
+-hsc     # Align hanging side comments
+-bbs     # Ensure a blank line before every sub definition (except one-liners)
+-bbb     # Ensure a blank line before code blocks (for, while, if, ....)
+
+-bar     # K&R style code braces
+-nolc    # Long comments indented, even when this make the total line length "too long"
+-noll    # Long lines indented, even when this make the total line length "too long"
+-nola    # Don't treat labels as special cases when indenting
+
+-nst     # Do NOT output to STDOUT (since we want to use -b)
+-b       # Backup the input file to .bak and perform all tidying in the original file
+-se      # Write errors to STDERR
+
+-it=4    # iterations
+--block-brace-vertical-tightness=1
+--nospace-terminal-semicolon
+--noadd-semicolons
+-nbl     # --noopening-brace-on-new-line
+--backup-file-extension=/    # delete bak file
commit 2f6563a87bed21f99b7b476c1fcac80b4cf102c9
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 17:25:33 2021 -0600

    Add MANIFEST and MANIFEST.SKIP

diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..40431b4
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,28 @@
+bin/aws-cloudwatch-monitor
+config.ini.example
+lib/App/AWS/CloudWatch/Monitor.pm
+lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+lib/App/AWS/CloudWatch/Monitor/Check.pm
+lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
+lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
+lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
+lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
+lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
+lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+lib/App/AWS/CloudWatch/Monitor/Config.pm
+Makefile.PL
+MANIFEST			This list of files
+MANIFEST.SKIP
+README
+t/00_load.t
+t/check-constants.t
+t/check.t
+t/config.ini
+t/config.t
+t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
+t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
+t/lib/App/AWS/CloudWatch/Monitor/Test.pm
+t/monitor-config.t
+t/monitor-run.t
+t/monitor-verify_metrics.t
+t/monitor.t
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644
index 0000000..60ea4b4
--- /dev/null
+++ b/MANIFEST.SKIP
@@ -0,0 +1,46 @@
+# Avoid version control files.
+\bRCS\b
+\bCVS\b
+\bSCCS\b
+,v$
+\B\.svn\b
+\B\.git\b
+\B\.gitignore\b
+\b_darcs\b
+
+# Avoid Makemaker generated and utility files.
+\bMANIFEST\.bak
+\bMakefile$
+\bblib/
+\bMakeMaker-\d
+\bpm_to_blib\.ts$
+\bpm_to_blib$
+\bblibdirs\.ts$         # 6.18 through 6.25 generated this
+
+# Avoid Module::Build generated and utility files.
+\bBuild$
+\b_build/
+
+# Avoid temp and backup files.
+~$
+\.old$
+\#$
+\b\.#
+\.bak$
+
+# Avoid Devel::Cover files.
+\bcover_db\b
+
+t/tmp/
+\.tagstagstags
+MYMETA\.json
+MYMETA\.yml$
+\.tar\.gz$$
+
+# Author tests and files
+996_perl-tidy.t
+997_perl-critic.t
+998_pod-checker.t
+999_pod-coverage.t
+perltidyrc
+perlcriticrc
commit 5f3fbe161ae84f3bf2e84043f82dfeae9130dc89
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 17:23:42 2021 -0600

    Update gitignore
    
    - fix dist name
    - add MANIFEST.bak
    - remove exclusions for author tests

diff --git a/.gitignore b/.gitignore
index bb9c923..9f650ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,6 @@ blib/
 cover_db/
 Makefile
 Makefile.old
-App-AWS-CloudWatch-Mon-*
+MANIFEST.bak
+App-AWS-CloudWatch-Monitor-*
 pm_to_blib
-996_perl-tidy.t
-997_perl-critic.t
-perltidyrc
-perlcriticrc
commit 4cb0e0d38eea6fa34f2e3290cf1ebb3dc1f26765
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 14:40:21 2021 -0600

    Mock get_avail_zone to avoid meta-data queries
    
    CloudWatchClient::call_json gathers various bit of instance
    information from the host through meta-data calls to the
    filesystem.  If tests are run on a machine that's not an AWS
    instance, the meta-data calls will fail and ultimately hit the 2
    second timeout.  Each failed meta-data call adds 2 seconds to
    the runtime of the test.
    
    This commit additionally mocks out the get_avail_zone meta-data
    call within monitor-run.t to speed up the test and remove
    unnecessary calls to resources that don't exist on development
    hosts.

diff --git a/t/monitor-run.t b/t/monitor-run.t
index ec70853..a52605d 100644
--- a/t/monitor-run.t
+++ b/t/monitor-run.t
@@ -7,24 +7,33 @@ use App::AWS::CloudWatch::Monitor::Test;
 
 use Capture::Tiny;
 
-# TODO: this test is slow to run, presuming more things might need to be mocked out.
-
 my $class = 'App::AWS::CloudWatch::Monitor';
 use_ok($class);
 
+# To allow testing the basic functionality of Monitor->run without needing to
+# run the tests on an AWS instance, this test mocks out the interactions with
+# the instance and AWS.
+
+# TODO: CloudWatchClient allows setting an alternate meta-data location using the
+# AWS_EC2CW_META_DATA ENV variable.  Instead of mocking subs in CloudWatchClient
+# for tests, it would be much better to create a directory structure inside of
+# t/var/ which contains meta-data for the calls to use.
+
 App::AWS::CloudWatch::Monitor::Test::override(
     package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
     name    => 'get_instance_id',
     subref  => sub { return 'i12345test' },
 );
 
-# To allow testing the basic functionality of Monitor->run without needing to
-# run the tests on an AWS instance, this test mocks out the interactions with
-# the instance and AWS.
+App::AWS::CloudWatch::Monitor::Test::override(
+    package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
+    name    => 'get_avail_zone',
+    subref  => sub { return 'us-east-1c' },
+);
+
 # Mocking call_json skips a lot of internal functionality that we should
-# verify.  More tests should be added which run through those internals, but
-# should first check if on an AWS instance and skip if not.
-# Those tests still shouldn't connect to CloudWatch to upload metrics.
+# verify.  For this first happy path test, we want to run without "verify"
+# but don't want to make calls to the AWS CloudWatch endpoints.
 
 my $reference_id = '12345-67a8';
 my $original_call_json = App::AWS::CloudWatch::Monitor::Test::override(
commit 80c79acc61af2aac6a0b60108a6e47067e8d70f6
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 12:17:17 2021 -0600

    Add tests for _verify_metrics

diff --git a/t/monitor-verify_metrics.t b/t/monitor-verify_metrics.t
new file mode 100644
index 0000000..ba292c5
--- /dev/null
+++ b/t/monitor-verify_metrics.t
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor';
+use_ok($class);
+
+my $metric = {
+    MetricName => 'TestVerifyMetrics',
+    Unit       => 'Count',
+    RawValue   => 1,
+};
+
+HAPPY_PATH: {
+    note( 'happy path' );
+
+    my $obj = $class->new();
+    ok( $obj->_verify_metrics( [$metric] ), 'response is success' );
+
+}
+
+FAILURES: {
+    my $obj = $class->new();
+    my ( $res, $msg );
+
+    note( 'failure no metric data' );
+
+    my $expected_response = 'no metric data was returned';
+    subtest 'undef metric' => sub {
+        plan tests => 2;
+        ( $res, $msg ) = $obj->_verify_metrics();
+        isnt( $res, 1, 'response is not success' );
+        like( $msg, qr/$expected_response/, "response indicates $expected_response" );
+    };
+    subtest 'empty arrayref metric' => sub {
+        plan tests => 2;
+        ( $res, $msg ) = $obj->_verify_metrics([]);
+        isnt( $res, 1, 'response is not success' );
+        like( $msg, qr/$expected_response/, "response indicates $expected_response" );
+    };
+
+    note( 'failure unexpected format' );
+
+    $expected_response = 'return is not in the expected format';
+    ( $res, $msg ) = $obj->_verify_metrics( $metric );
+    isnt( $res, 1, 'response is not success' );
+    like( $msg, qr/$expected_response/, "response indicates $expected_response" );
+
+    note( 'failure missing keys' );
+
+    foreach my $key ( keys %{$metric} ) {
+        my $modified = $metric;
+        my $value = delete $modified->{$key};
+
+        my $expected_response = 'return does not contain the required keys';
+        my ( $res, $msg ) = $obj->_verify_metrics( [$modified] );
+
+        subtest "missing $key" => sub {
+            plan tests => 2;
+            isnt( $res, 1, 'response is not success' );
+            like( $msg, qr/$expected_response/, "response indicates $expected_response" );
+        };
+
+        $modified->{$key} = $value;
+    }
+}
+
+done_testing();
commit 28edc057eabc73bd3a6434cb2f9d42117f0b9f09
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Nov 11 12:08:26 2021 -0600

    Add check for empty ARRAYREF to _verify_metrics
    
    Warn if the check module returns undef or no metrics in the
    $metrics ARRAYREF.  A truthy check of $metrics alone doesn't warn
    if the check module returns an empty ARRAYREF.

diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index beb8817..fe2ca63 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -122,7 +122,7 @@ sub _verify_metrics {
     my $self    = shift;
     my $metrics = shift;
 
-    unless ($metrics) {
+    if ( !$metrics || ( ref $metrics eq 'ARRAY' && !scalar @{$metrics} ) ) {
         return ( 0, 'no metric data was returned' );
     }
 
commit d7bdc4a148596f5e360161a3eae67c7078f6e5e4
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Nov 10 18:20:19 2021 -0600

    Remove Test::Warnings from import and requirements
    
    Test::Warnings was initially added as boilerplate to detect any
    expected warnings while running tests.
    
    The idea was to load and use Test::Warnings for all tests and
    methods, flagging any unexpected warnings that may have been
    inadvertently added by code changes or by changes to Perl itself.
    
    In principle, this works.  In practice, it proved problematic once
    tests were added to detect expected warnings in monitor-run.t.
    
    Test::Warnings has a function, allow_warnings, which is meant to
    disable the checks in situations exactly like monitor-run.t.
    Unfortunately, allow_warnings is noted in documentation as
    "EXPERIMENTAL - MAY BE REMOVED."
    
    This commit removes the module rather than relying on functionality
    in tests that may be removed, which could result in broken tests
    and installs.

diff --git a/Makefile.PL b/Makefile.PL
index 173368e..f63d9a6 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -34,8 +34,6 @@ WriteMakefile(
         'FindBin' => 0,
         'Test::Exception' => 0,
         'Test::More' => 0,
-        'Test::Warnings' => 0,
-
     },
     PREREQ_PM => {
         'base' => 0,
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
index 2c98e19..24810b7 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
@@ -32,8 +32,6 @@ sub import {
     require Test::Exception;
     Test::Exception->export_to_level(1);
 
-    require Test::Warnings;
-
     return;
 }
 
commit 3f37223b543d7a2c78d0826f5a57122aa987a880
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Nov 2 19:20:16 2021 -0500

    Add read_file method to check.t
    
    read_file was inadvertently missed when adding the check.t test in
    commit d9c3d65.

diff --git a/t/check.t b/t/check.t
index f9138df..a86caf9 100644
--- a/t/check.t
+++ b/t/check.t
@@ -14,7 +14,7 @@ OBJECT_AND_METHODS: {
     my $obj = $class->new();
     isa_ok( $obj, $class );
 
-    foreach my $method ( qw{ run_command constants } ) {
+    foreach my $method ( qw{ run_command read_file constants } ) {
         can_ok( $obj, $method );
     }
 }
commit 393f04e0d82e3fb012bca92724e6186be67020d7
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Nov 2 19:16:57 2021 -0500

    Add tests for additional args and opts

diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
new file mode 100644
index 0000000..4b596a9
--- /dev/null
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestArgs.pm
@@ -0,0 +1,64 @@
+package App::AWS::CloudWatch::Monitor::Check::TestArgs;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+use Getopt::Long qw(:config pass_through);
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+    my $arg  = shift;
+
+    Getopt::Long::GetOptionsFromArray( $arg, \my %opt, 'test=s@' );
+
+    die "Option: test is required" unless $opt{'test'};
+
+    my $metrics;
+    foreach my $value ( @{ $opt{'test'} } ) {
+        push @{$metrics},
+            {
+            MetricName => $value . '-Count',
+            Unit       => 'Count',
+            RawValue   => 1,
+            };
+    }
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::TestArgs - test metric for tests
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestArgs->new();
+ my $metrics = $plugin->check( [ @{$arg} ] );
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::TestArgs> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+
+This test check module verifies the presence of the C<--test> arg and dies on failure, or returns a metric for each C<--test>.
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
deleted file mode 100644
index 6b67e54..0000000
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
+++ /dev/null
@@ -1,56 +0,0 @@
-package App::AWS::CloudWatch::Monitor::Check::TestOne;
-
-use strict;
-use warnings;
-
-use parent 'App::AWS::CloudWatch::Monitor::Check';
-
-our $VERSION = '0.01';
-
-sub check {
-    my $self = shift;
-
-    my @echo_testing_command = (qw{ /bin/echo testone });
-    ( my $exit, my $stdout, my $stderr ) = $self->run_command( \@echo_testing_command );
-
-    my $value = ( $stdout ? 1 : 0 );
-
-    my $metric = {
-        MetricName => 'TestOne',
-        Unit       => 'Count',
-        RawValue   => $value,
-    };
-
-    return [ $metric ];
-}
-
-1;
-
-__END__
-
-=pod
-
-=head1 NAME
-
-App::AWS::CloudWatch::Monitor::Check::TestOne - test metric for tests
-
-=head1 SYNOPSIS
-
- my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestOne->new();
- my $metrics = $plugin->check();
-
-=head1 DESCRIPTION
-
-C<App::AWS::CloudWatch::Monitor::Check::TestOne> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
-
-=head1 METHODS
-
-=over
-
-=item check
-
-Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
-
-=back
-
-=cut
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
new file mode 100644
index 0000000..308e32f
--- /dev/null
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestSuccess.pm
@@ -0,0 +1,53 @@
+package App::AWS::CloudWatch::Monitor::Check::TestSuccess;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    my $metric = {
+        MetricName => 'TestSuccess',
+        Unit       => 'Count',
+        RawValue   => 1,
+    };
+
+    return [ $metric ];
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::TestSuccess - test metric for tests
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestSuccess->new();
+ my $metrics = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::TestSuccess> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+
+This test check module doesn't verify args or run anything on the system, but only returns metric data in the expected format.
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
deleted file mode 100644
index 99c1366..0000000
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
+++ /dev/null
@@ -1,56 +0,0 @@
-package App::AWS::CloudWatch::Monitor::Check::TestTwo;
-
-use strict;
-use warnings;
-
-use parent 'App::AWS::CloudWatch::Monitor::Check';
-
-our $VERSION = '0.01';
-
-sub check {
-    my $self = shift;
-
-    my @echo_testing_command = (qw{ /bin/echo testtwo });
-    ( my $exit, my $stdout, my $stderr ) = $self->run_command( \@echo_testing_command );
-
-    my $value = ( $stdout ? 1 : 0 );
-
-    my $metric = {
-        MetricName => 'TestTwo',
-        Unit       => 'Count',
-        RawValue   => $value,
-    };
-
-    return [ $metric ];
-}
-
-1;
-
-__END__
-
-=pod
-
-=head1 NAME
-
-App::AWS::CloudWatch::Monitor::Check::TestTwo - test metric for tests
-
-=head1 SYNOPSIS
-
- my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestTwo->new();
- my $metrics = $plugin->check();
-
-=head1 DESCRIPTION
-
-C<App::AWS::CloudWatch::Monitor::Check::TestTwo> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
-
-=head1 METHODS
-
-=over
-
-=item check
-
-Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
-
-=back
-
-=cut
diff --git a/t/monitor-run.t b/t/monitor-run.t
index 57e5075..ec70853 100644
--- a/t/monitor-run.t
+++ b/t/monitor-run.t
@@ -7,6 +7,8 @@ use App::AWS::CloudWatch::Monitor::Test;
 
 use Capture::Tiny;
 
+# TODO: this test is slow to run, presuming more things might need to be mocked out.
+
 my $class = 'App::AWS::CloudWatch::Monitor';
 use_ok($class);
 
@@ -16,41 +18,146 @@ App::AWS::CloudWatch::Monitor::Test::override(
     subref  => sub { return 'i12345test' },
 );
 
+# To allow testing the basic functionality of Monitor->run without needing to
+# run the tests on an AWS instance, this test mocks out the interactions with
+# the instance and AWS.
+# Mocking call_json skips a lot of internal functionality that we should
+# verify.  More tests should be added which run through those internals, but
+# should first check if on an AWS instance and skip if not.
+# Those tests still shouldn't connect to CloudWatch to upload metrics.
+
+my $reference_id = '12345-67a8';
+my $original_call_json = App::AWS::CloudWatch::Monitor::Test::override(
+    package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
+    name    => 'call_json',
+    subref  => sub {
+        return (
+            HTTP::Response->new(
+                200,
+                'This is a mocked response from the test',
+                [ 'x-amzn-requestid' => $reference_id ],
+            )
+        );
+    },
+);
+
 HAPPY_PATH_MOCKED: {
     note( 'happy path mocked' );
 
-    # To allow testing the basic functionality of Monitor->run without needing to
-    # run the tests on an AWS instance, this test mocks out the interactions with
-    # the instance and AWS.
-    # Mocking call_json skips a lot of internal functionality that we should
-    # verify.  More tests should be added which run through those internals, but
-    # should first check if on an AWS instance and skip if not.
-    # Those tests still shouldn't connect to CloudWatch to upload metrics.
-
-    App::AWS::CloudWatch::Monitor::Test::override(
-        package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
-        name    => 'call_json',
-        subref  => sub {
-            return (
-                HTTP::Response->new(
-                    200,
-                    'This is a mocked response from the test',
-                    [ 'x-amzn-requestid' => '12345-67a8' ],
-                )
-            );
-        },
-    );
-
     my $opt = {
-        check => [ qw{ TestOne TestTwo } ],
+        check => [ qw{ TestSuccess } ],
     };
 
+    my $arg = [];
+
     my $obj = $class->new();
-    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt) };
+    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
 
     # Successfully reported metrics to CloudWatch. Reference Id: 12345-67a8
     like( $stdout, qr/Successfully reported metrics/, 'response contains success message' );
-    like( $stdout, qr/Reference Id: \d+/, 'response contains reference id' );
+    like( $stdout, qr/Reference Id: $reference_id/, 'response contains reference id' );
+}
+
+App::AWS::CloudWatch::Monitor::Test::override(
+    package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
+    name    => 'call_json',
+    subref  => $original_call_json,
+);
+
+VERIFY_OPTION: {
+    note( 'verify option' );
+
+    my $opt = {
+        check   => [ qw{ TestSuccess } ],
+        verify  => 1,
+    };
+
+    my $arg = [ 'test', 'one', 'test', 'two' ];
+
+    my $obj = $class->new();
+    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
+
+    # Verification completed successfully. No actual metrics sent to CloudWatch.
+    like( $stdout, qr/Verification completed successfully/, 'response contains success message' );
+    like( $stdout, qr/No actual metrics sent to CloudWatch/, 'response indicates no metrics sent' );
+}
+
+VERBOSE_OPTION: {
+    note( 'verbose option' );
+
+    my $opt = {
+        check   => [ qw{ TestSuccess } ],
+        verify  => 1,
+        verbose => 1,
+    };
+
+    my $arg = [];
+
+    my $obj = $class->new();
+    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
+
+    like( $stdout, qr/MetricName/, 'payload metric data is present if verbose is set' );
+
+    $opt = {
+        check  => [ qw{ TestSuccess } ],
+        verify => 1,
+    };
+
+    $arg = [];
+
+    $obj = $class->new();
+    ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
+
+    unlike( $stdout, qr/MetricName/, 'payload metric data is not present if verbose is not set' );
+}
+
+ADDITIONAL_ARGS: {
+    note( 'additional args success' );
+
+    my $opt = {
+        check   => [ qw{ TestArgs } ],
+        verify  => 1,
+        verbose => 1,
+    };
+
+    my $arg = [ '--test', 'one', '--test', 'two' ];
+
+    my $obj = $class->new();
+    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
+
+    # the MetricName checks below are a cheap way to verify the args are correctly passed into the check modules
+    # by having the check module create metrics based on the expected values submitted.
+    like( $stdout, qr/"MetricName":"one-Count"/, 'arg one is correctly passed to the test module' );
+    like( $stdout, qr/"MetricName":"two-Count"/, 'arg two is correctly passed to the test module' );
+    like( $stdout, qr/Verification completed successfully/, 'response contains success message' );
+    like( $stdout, qr/No actual metrics sent to CloudWatch/, 'response indicates no metrics sent' );
+
+    note( 'additional args failure' );
+
+    $opt = {
+        check   => [ qw{ TestSuccess TestArgs } ],
+        verify  => 1,
+        verbose => 1,
+    };
+
+    $arg = [ '--not-test', 'value' ];
+
+    $obj = $class->new();
+    ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt,$arg) };
+
+    # here we need to verify TestSuccess check module still ran and succeeded, while Monitor->run catches
+    # and passes back the failure from TestArgs for the missing expected "test" arg.
+    like( $stderr, qr/error: Check::TestArgs: Option: test is required/, 'required option in check module throws error' );
+    like( $stdout, qr/"MetricName":"TestSuccess"/, 'successful check module metric was uploaded despite failure in another check module' );
+
+    # additionally check the output to ensure unknown args don't throw warnings.  since all additional
+    # args are ultimately passed to the check modules, check module authors need to be sure
+    # pass_though is enabled for Getopt::Long in their modules.  there isn't a way to enforce that for
+    # check modules, so the best this is achieving is an additional self documentation of functionality.
+    unlike( $stderr, qr/Unknown option/, 'unknown args do not throw errors' );
+
+    like( $stdout, qr/Verification completed successfully/, 'response contains success message' );
+    like( $stdout, qr/No actual metrics sent to CloudWatch/, 'response indicates no metrics sent' );
 }
 
 done_testing();
commit 1cfb0e445906527db289c9b9215620668e83be13
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Nov 2 19:18:31 2021 -0500

    Update Test::override to return original subref

diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
index 690e071..2c98e19 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
@@ -48,12 +48,13 @@ sub override {
     eval "require $args{package}";
 
     my $fullname = sprintf "%s::%s", $args{package}, $args{name};
+    my $original = \&$fullname;
 
     no strict 'refs';
     no warnings 'redefine', 'prototype';
     *$fullname = $args{subref};
 
-    return;
+    return $original;
 }
 
 1;
@@ -88,12 +89,14 @@ Overrides subroutines
 
 ARGS are C<package>, C<name>, and C<subref>.
 
- App::AWS::CloudWatch::Monitor::Test::override(
+ my $original_sub = App::AWS::CloudWatch::Monitor::Test::override(
      package => 'Package::To::Override',
      name    => 'subtooverride',
      subref  => sub { return 'faked' },
  );
 
+RETURNS the original, un-overridden sub.
+
 =back
 
 =cut
commit 9752cfc7f6c72069756cc975da2f7b82be5272dd
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Nov 1 19:41:37 2021 -0500

    Add Process check module

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
new file mode 100644
index 0000000..2b0578f
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Process.pm
@@ -0,0 +1,124 @@
+package App::AWS::CloudWatch::Monitor::Check::Process;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+use Getopt::Long qw(:config pass_through);
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+    my $arg  = shift;
+
+    Getopt::Long::GetOptionsFromArray( $arg, \my %opt, 'process=s@' );
+
+    die "Option: process is required" unless $opt{'process'};
+
+    my @ps_command = ( '/bin/ps', 'axco', 'command,pcpu,pmem' );
+    my ( $exit, $stdout, $stderr ) = $self->run_command( \@ps_command );
+
+    if ($exit) {
+        die "$stderr\n";
+    }
+
+    return unless $stdout;
+
+    shift @{$stdout};
+
+    my $metrics;
+    foreach my $process_name ( @{ $opt{'process'} } ) {
+        my $total_cnt = 0;
+        my $total_cpu = 0.0;
+        my $total_mem = 0.0;
+
+        foreach my $line ( @{$stdout} ) {
+            next unless $line =~ qr/^$process_name/;
+
+            $total_cnt += 1;
+
+            my @fields = split /\s+/, $line;
+
+            $total_cpu += $fields[1];
+            $total_mem += $fields[2];
+        }
+
+        push @{$metrics},
+            {
+            MetricName => $process_name . '-Count',
+            Unit       => 'Count',
+            RawValue   => $total_cnt,
+            },
+            {
+            MetricName => $process_name . '-CpuUtilization',
+            Unit       => 'Percent',
+            RawValue   => $total_cpu,
+            },
+            {
+            MetricName => $process_name . '-MemoryUtilization',
+            Unit       => 'Percent',
+            RawValue   => $total_mem,
+            };
+    }
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::Process - gather process metric data
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::Process->new();
+ my $metrics = $plugin->check();
+
+ perl bin/aws-cloudwatch-monitor --check Process --process apache2
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::Process> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers process metric data.
+
+=head1 METRICS
+
+The following metrics are gathered and returned.
+
+=over
+
+=item $process-Count
+
+=item $process-CpuUtilization
+
+=item $process-MemoryUtilization
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, C<RawValue>, and C<Dimensions>.
+
+=back
+
+=head1 ARGUMENTS
+
+C<App::AWS::CloudWatch::Monitor::Check::Process> requires the C<--process> argument through the commandline.
+
+ perl bin/aws-cloudwatch-monitor --check Process --process apache2
+
+Multiple C<--process> arguments may be defined to gather metrics for multiple processes.
+
+ perl bin/aws-cloudwatch-monitor --check Process --process apache2 --process postgres
+
+=cut
commit 383dc552d0cd7eb7ca78a0a6257d66261577c5f4
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Oct 27 19:00:23 2021 -0500

    Pass additional args to check modules

diff --git a/README b/README
index b606aa8..4810952 100644
--- a/README
+++ b/README
@@ -39,6 +39,30 @@ OPTIONS
     --help
         Print the help menu.
 
+ADDITIONAL OPTIONS FOR CHECK MODULES
+    The check modules within this project may require additional options not
+    directly defined in "aws-cloudwatch-monitor".
+
+    All additional options defined on the commandline are passed to the
+    check modules.
+
+    For example, the "DiskSpace" check module requires the "--disk-path"
+    option, which is passed through and verified in the check module itself.
+
+     aws-cloudwatch-monitor --check DiskSpace --disk-path /
+
+    If "--disk-path" isn't defined, the "DiskSpace" check module will warn
+    and skip gathering it's metrics.
+
+     aws-cloudwatch-monitor --check DiskSpace
+     error: Check::DiskSpace: Option: disk-path is required at lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm line 18.
+
+    Additional information about each check module can be found using the
+    "perldoc" program. For example, documentation for the included
+    "DiskSpace" check module can be read by running the following command:
+
+     perldoc App::AWS::CloudWatch::Monitor::Check::DiskSpace
+
 CONFIGURATION
     An example configuration file, "config.ini.example", is provided in the
     project root directory.
diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index d2405c5..4189269 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -3,8 +3,8 @@
 use strict;
 use warnings;
 
-use Getopt::Long ();
-use Pod::Usage   ();
+use Getopt::Long qw(:config pass_through);
+use Pod::Usage ();
 use App::AWS::CloudWatch::Monitor;
 
 my $VERSION = '0.01';
@@ -30,7 +30,7 @@ if ($opt{'from-cron'}) {
 }
 
 my $monitor = App::AWS::CloudWatch::Monitor->new();
-$monitor->run(\%opt);
+$monitor->run(\%opt, \@ARGV);
 
 exit 0;
 
@@ -88,6 +88,25 @@ Print the help menu.
 
 =back
 
+=head1 ADDITIONAL OPTIONS FOR CHECK MODULES
+
+The check modules within this project may require additional options not directly defined in C<aws-cloudwatch-monitor>.
+
+All additional options defined on the commandline are passed to the check modules.
+
+For example, the C<DiskSpace> check module requires the C<--disk-path> option, which is passed through and verified in the check module itself.
+
+ aws-cloudwatch-monitor --check DiskSpace --disk-path /
+
+If C<--disk-path> isn't defined, the C<DiskSpace> check module will warn and skip gathering it's metrics.
+
+ aws-cloudwatch-monitor --check DiskSpace
+ error: Check::DiskSpace: Option: disk-path is required at lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm line 18.
+
+Additional information about each check module can be found using the C<perldoc> program.  For example, documentation for the included C<DiskSpace> check module can be read by running the following command:
+
+ perldoc App::AWS::CloudWatch::Monitor::Check::DiskSpace
+
 =head1 CONFIGURATION
 
 An example configuration file, C<config.ini.example>, is provided in the project root directory.
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 6ccc1e8..beb8817 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -33,6 +33,7 @@ sub config {
 sub run {
     my $self = shift;
     my $opt  = shift;
+    my $arg  = shift;
 
     my $instance_id = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_instance_id();
     my $loader      = Module::Loader->new;
@@ -59,7 +60,7 @@ sub run {
         my $plugin = $class->new();
         my ( $metrics, $exception );
         $metrics = try {
-            return $plugin->check();
+            return $plugin->check( [ @{$arg} ] );
         }
         catch {
             chomp( $exception = $_ );
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
index 9dc2196..cee10a7 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
@@ -5,13 +5,21 @@ use warnings;
 
 use parent 'App::AWS::CloudWatch::Monitor::Check';
 
+use Getopt::Long qw(:config pass_through);
+
 our $VERSION = '0.01';
 
 sub check {
     my $self = shift;
+    my $arg  = shift;
+
+    Getopt::Long::GetOptionsFromArray( $arg, \my %opt, 'disk-path=s@' );
+
+    die "Option: disk-path is required" unless $opt{'disk-path'};
+
+    my @df_command = (qw{ /bin/df -k -l -P });
+    push @df_command, @{ $opt{'disk-path'} };
 
-    # TODO: pass in mount path
-    my @df_command = (qw{ /bin/df -k -l -P / });
     my ( $exit, $stdout, $stderr ) = $self->run_command( \@df_command );
 
     if ($exit) {
@@ -21,17 +29,20 @@ sub check {
     return unless $stdout;
 
     shift @{$stdout};
-    my @fields = split /\s+/, @{$stdout}[0];
 
-    # Result of df is reported in 1k blocks
-    my $disk_total = $fields[1] * $self->constants->{KILO};
-    my $disk_used  = $fields[2] * $self->constants->{KILO};
-    my $disk_avail = $fields[3] * $self->constants->{KILO};
-    my $filesystem = $fields[0];
-    my $mount_path = $fields[5];
+    my $metrics;
+    foreach my $line ( @{$stdout} ) {
+        my @fields = split /\s+/, $line;
+
+        my $disk_total = $fields[1] * $self->constants->{KILO};
+        my $disk_used  = $fields[2] * $self->constants->{KILO};
+        my $disk_avail = $fields[3] * $self->constants->{KILO};
+        my $filesystem = $fields[0];
+        my $mount_path = $fields[5];
 
-    my $metrics = [
-        {   MetricName => 'DiskSpaceUtilization',
+        push @{$metrics},
+            {
+            MetricName => 'DiskSpaceUtilization',
             Unit       => 'Percent',
             RawValue   => ( $disk_total > 0 ? 100 * $disk_used / $disk_total : 0 ),
             Dimensions => [
@@ -42,8 +53,9 @@ sub check {
                     Value => $mount_path,
                 },
             ],
-        },
-        {   MetricName => 'DiskSpaceUsed',
+            },
+            {
+            MetricName => 'DiskSpaceUsed',
             Unit       => 'Gigabytes',
             RawValue   => $disk_used / $self->constants->{GIGA},
             Dimensions => [
@@ -54,8 +66,9 @@ sub check {
                     Value => $mount_path,
                 },
             ],
-        },
-        {   MetricName => 'DiskSpaceAvailable',
+            },
+            {
+            MetricName => 'DiskSpaceAvailable',
             Unit       => 'Gigabytes',
             RawValue   => $disk_avail / $self->constants->{GIGA},
             Dimensions => [
@@ -66,8 +79,8 @@ sub check {
                     Value => $mount_path,
                 },
             ],
-        },
-    ];
+            };
+    }
 
     return $metrics;
 }
@@ -87,6 +100,8 @@ App::AWS::CloudWatch::Monitor::Check::DiskSpace - gather disk metric data
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::DiskSpace->new();
  my $metrics = $plugin->check();
 
+ perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path /
+
 =head1 DESCRIPTION
 
 C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers disk metric data.
@@ -115,4 +130,14 @@ Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricNa
 
 =back
 
+=head1 ARGUMENTS
+
+C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> requires the C<--disk-path> argument through the commandline.
+
+ perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path /
+
+Multiple C<--disk-path> arguments may be defined to gather metrics for multiple paths.
+
+ perl bin/aws-cloudwatch-monitor --check DiskSpace --disk-path / --disk-path /mnt/data
+
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
index 1a5774d..ba47c9f 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
@@ -5,13 +5,21 @@ use warnings;
 
 use parent 'App::AWS::CloudWatch::Monitor::Check';
 
+use Getopt::Long qw(:config pass_through);
+
 our $VERSION = '0.01';
 
 sub check {
     my $self = shift;
+    my $arg  = shift;
+
+    Getopt::Long::GetOptionsFromArray( $arg, \my %opt, 'disk-path=s@' );
+
+    die "Option: disk-path is required" unless $opt{'disk-path'};
+
+    my @df_command = (qw{ /bin/df -i -k -P });
+    push @df_command, @{ $opt{'disk-path'} };
 
-    # TODO: pass in mount path
-    my @df_command = (qw{ /bin/df -i -k -P / });
     my ( $exit, $stdout, $stderr ) = $self->run_command( \@df_command );
 
     if ($exit) {
@@ -21,16 +29,20 @@ sub check {
     return unless $stdout;
 
     shift @{$stdout};
-    my @fields = split /\s+/, @{$stdout}[0];
 
-    my $inode_total = $fields[1];
-    my $inode_used  = $fields[2];
-    my $inode_avail = $fields[3];
-    my $filesystem  = $fields[0];
-    my $mount_path  = $fields[5];
+    my $metrics;
+    foreach my $line ( @{$stdout} ) {
+        my @fields = split /\s+/, $line;
+
+        my $inode_total = $fields[1];
+        my $inode_used  = $fields[2];
+        my $inode_avail = $fields[3];
+        my $filesystem  = $fields[0];
+        my $mount_path  = $fields[5];
 
-    my $metrics = [
-        {   MetricName => 'InodeUtilization',
+        push @{$metrics},
+            {
+            MetricName => 'InodeUtilization',
             Unit       => 'Percent',
             RawValue   => ( $inode_total > 0 ? 100 * $inode_used / $inode_total : 0 ),
             Dimensions => [
@@ -41,8 +53,8 @@ sub check {
                     Value => $mount_path,
                 },
             ],
-        },
-    ];
+            };
+    }
 
     return $metrics;
 }
@@ -62,6 +74,8 @@ App::AWS::CloudWatch::Monitor::Check::Inode - gather inode metric data
  my $plugin  = App::AWS::CloudWatch::Monitor::Check::Inode->new();
  my $metrics = $plugin->check();
 
+ perl bin/aws-cloudwatch-monitor --check Inode --disk-path /
+
 =head1 DESCRIPTION
 
 C<App::AWS::CloudWatch::Monitor::Check::Inode> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers inode metric data.
@@ -86,4 +100,14 @@ Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricNa
 
 =back
 
+=head1 ARGUMENTS
+
+C<App::AWS::CloudWatch::Monitor::Check::Inode> requires the C<--disk-path> argument through the commandline.
+
+ perl bin/aws-cloudwatch-monitor --check Inode --disk-path /
+
+Multiple C<--disk-path> arguments may be defined to gather metrics for multiple paths.
+
+ perl bin/aws-cloudwatch-monitor --check Inode --disk-path / --disk-path /mnt/data
+
 =cut
commit f708092d4b23615714e6737dbf4bf1d847ed20b4
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:29:52 2021 -0500

    Add Inode check module
    
    This check module gathers and returns metrics for:
    - InodeUtilization

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
new file mode 100644
index 0000000..1a5774d
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Inode.pm
@@ -0,0 +1,89 @@
+package App::AWS::CloudWatch::Monitor::Check::Inode;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    # TODO: pass in mount path
+    my @df_command = (qw{ /bin/df -i -k -P / });
+    my ( $exit, $stdout, $stderr ) = $self->run_command( \@df_command );
+
+    if ($exit) {
+        die "$stderr\n";
+    }
+
+    return unless $stdout;
+
+    shift @{$stdout};
+    my @fields = split /\s+/, @{$stdout}[0];
+
+    my $inode_total = $fields[1];
+    my $inode_used  = $fields[2];
+    my $inode_avail = $fields[3];
+    my $filesystem  = $fields[0];
+    my $mount_path  = $fields[5];
+
+    my $metrics = [
+        {   MetricName => 'InodeUtilization',
+            Unit       => 'Percent',
+            RawValue   => ( $inode_total > 0 ? 100 * $inode_used / $inode_total : 0 ),
+            Dimensions => [
+                {   Name  => 'Filesystem',
+                    Value => $filesystem,
+                },
+                {   Name  => 'MountPath',
+                    Value => $mount_path,
+                },
+            ],
+        },
+    ];
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::Inode - gather inode metric data
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::Inode->new();
+ my $metrics = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::Inode> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers inode metric data.
+
+=head1 METRICS
+
+The following metrics are gathered and returned.
+
+=over
+
+=item InodeUtilization
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, C<RawValue>, and C<Dimensions>.
+
+=back
+
+=cut
commit cc0f2335c9565c7b3a395c677220a71a8e179680
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:29:13 2021 -0500

    Add LoadAvg check module
    
    This check module gathers and returns metrics for:
    - LoadAvg1Min
    - LoadAvg5Min
    - LoadAvg15Min
    - LoadAvgPerCPU1Min
    - LoadAvgPerCPU5Min
    - LoadAvgPerCPU15Min

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
new file mode 100644
index 0000000..7ebde7d
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/LoadAvg.pm
@@ -0,0 +1,119 @@
+package App::AWS::CloudWatch::Monitor::Check::LoadAvg;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    my $loadavg_filename = '/proc/loadavg';
+    my ( $ret, $msg ) = $self->read_file($loadavg_filename);
+
+    if ( !$ret && $msg ) {
+        die "$msg\n";
+    }
+
+    return unless $ret;
+
+    my @fields = split /\s+/, @{$ret}[0];
+
+    # TODO: parse into float
+    my $loadavg_1min  = $fields[0];
+    my $loadavg_5min  = $fields[1];
+    my $loadavg_15min = $fields[2];
+
+    my $cpuinfo_filename = '/proc/cpuinfo';
+    ( $ret, $msg ) = $self->read_file($cpuinfo_filename);
+
+    if ( !$ret && $msg ) {
+        die "$msg\n";
+    }
+
+    return unless $ret;
+
+    my $cpu_count = grep {/processor\t:/} @{$ret};
+
+    my $metrics = [
+        {   MetricName => 'LoadAvg1Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_1min,
+        },
+        {   MetricName => 'LoadAvg5Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_5min,
+        },
+        {   MetricName => 'LoadAvg15Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_15min,
+        },
+        {   MetricName => 'LoadAvgPerCPU1Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_1min / $cpu_count,
+        },
+        {   MetricName => 'LoadAvgPerCPU5Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_5min / $cpu_count,
+        },
+        {   MetricName => 'LoadAvgPerCPU15Min',
+            Unit       => 'Percent',
+            RawValue   => $loadavg_15min / $cpu_count,
+        },
+    ];
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::LoadAvg - gather load average metric data
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::LoadAvg->new();
+ my $metrics = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::LoadAvg> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers load average metric data.
+
+=head1 METRICS
+
+The following metrics are gathered and returned.
+
+=over
+
+=item LoadAvg1Min
+
+=item LoadAvg5Min
+
+=item LoadAvg15Min
+
+=item LoadAvgPerCPU1Min
+
+=item LoadAvgPerCPU5Min
+
+=item LoadAvgPerCPU15Min
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
commit 0859ce06cf39ddeefb155fffd6ef74e8d442de32
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:27:01 2021 -0500

    Add DiskSpace check module
    
    This check module gathers and returns metrics for:
    - DiskSpaceUtilization
    - DiskSpaceUsed
    - DiskSpaceAvailable

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
new file mode 100644
index 0000000..9dc2196
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/DiskSpace.pm
@@ -0,0 +1,118 @@
+package App::AWS::CloudWatch::Monitor::Check::DiskSpace;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    # TODO: pass in mount path
+    my @df_command = (qw{ /bin/df -k -l -P / });
+    my ( $exit, $stdout, $stderr ) = $self->run_command( \@df_command );
+
+    if ($exit) {
+        die "$stderr\n";
+    }
+
+    return unless $stdout;
+
+    shift @{$stdout};
+    my @fields = split /\s+/, @{$stdout}[0];
+
+    # Result of df is reported in 1k blocks
+    my $disk_total = $fields[1] * $self->constants->{KILO};
+    my $disk_used  = $fields[2] * $self->constants->{KILO};
+    my $disk_avail = $fields[3] * $self->constants->{KILO};
+    my $filesystem = $fields[0];
+    my $mount_path = $fields[5];
+
+    my $metrics = [
+        {   MetricName => 'DiskSpaceUtilization',
+            Unit       => 'Percent',
+            RawValue   => ( $disk_total > 0 ? 100 * $disk_used / $disk_total : 0 ),
+            Dimensions => [
+                {   Name  => 'Filesystem',
+                    Value => $filesystem,
+                },
+                {   Name  => 'MountPath',
+                    Value => $mount_path,
+                },
+            ],
+        },
+        {   MetricName => 'DiskSpaceUsed',
+            Unit       => 'Gigabytes',
+            RawValue   => $disk_used / $self->constants->{GIGA},
+            Dimensions => [
+                {   Name  => 'Filesystem',
+                    Value => $filesystem,
+                },
+                {   Name  => 'MountPath',
+                    Value => $mount_path,
+                },
+            ],
+        },
+        {   MetricName => 'DiskSpaceAvailable',
+            Unit       => 'Gigabytes',
+            RawValue   => $disk_avail / $self->constants->{GIGA},
+            Dimensions => [
+                {   Name  => 'Filesystem',
+                    Value => $filesystem,
+                },
+                {   Name  => 'MountPath',
+                    Value => $mount_path,
+                },
+            ],
+        },
+    ];
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::DiskSpace - gather disk metric data
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::DiskSpace->new();
+ my $metrics = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::DiskSpace> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers disk metric data.
+
+=head1 METRICS
+
+The following metrics are gathered and returned.
+
+=over
+
+=item DiskSpaceUtilization
+
+=item DiskSpaceUsed
+
+=item DiskSpaceAvailable
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, C<RawValue>, and C<Dimensions>.
+
+=back
+
+=cut
commit 4986da48eeaf539c2062a2f2d3df7e77892261aa
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:26:09 2021 -0500

    Add Memory check module
    
    This check module gathers and returns metrics for:
    - MemoryUtilization
    - MemoryUsed
    - MemoryAvailable
    - SwapUtilization
    - SwapUsed

diff --git a/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
new file mode 100644
index 0000000..da42e7d
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check/Memory.pm
@@ -0,0 +1,116 @@
+package App::AWS::CloudWatch::Monitor::Check::Memory;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    my $meminfo_filename = '/proc/meminfo';
+    my ( $ret, $msg ) = $self->read_file($meminfo_filename);
+
+    if ( !$ret && $msg ) {
+        die "$msg\n";
+    }
+
+    return unless $ret;
+
+    my %meminfo;
+    foreach my $line ( @{$ret} ) {
+        if ( $line =~ /^(.*?):\s+(\d+)/ ) {
+            $meminfo{$1} = $2;
+        }
+    }
+
+    my $mem_total   = $meminfo{MemTotal} * $self->constants->{KILO};
+    my $mem_free    = $meminfo{MemFree} * $self->constants->{KILO};
+    my $mem_cached  = $meminfo{Cached} * $self->constants->{KILO};
+    my $mem_buffers = $meminfo{Buffers} * $self->constants->{KILO};
+
+    # TODO: implement memory-units and mem-used-incl-cache-buff
+    my $mem_avail = $mem_free;
+    $mem_avail += $mem_cached + $mem_buffers;
+    my $mem_used = $mem_total - $mem_avail;
+
+    my $swap_total = $meminfo{SwapTotal} * $self->constants->{KILO};
+    my $swap_free  = $meminfo{SwapFree} * $self->constants->{KILO};
+    my $swap_used  = $swap_total - $swap_free;
+
+    my $metrics = [
+        {   MetricName => 'MemoryUtilization',
+            Unit       => 'Percent',
+            RawValue   => ( $mem_total > 0 ? 100 * $mem_used / $mem_total : 0 ),
+        },
+        {   MetricName => 'MemoryUsed',
+            Unit       => 'Megabytes',
+            RawValue   => $mem_used / $self->constants->{MEGA},
+        },
+        {   MetricName => 'MemoryAvailable',
+            Unit       => 'Megabytes',
+            RawValue   => $mem_avail / $self->constants->{MEGA},
+        },
+        {   MetricName => 'SwapUtilization',
+            Unit       => 'Percent',
+            RawValue   => ( $swap_total > 0 ? 100 * $swap_used / $swap_total : 0 ),
+        },
+        {   MetricName => 'SwapUsed',
+            Unit       => 'Megabytes',
+            RawValue   => $swap_used / $self->constants->{MEGA},
+        },
+    ];
+
+    return $metrics;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::Memory - gather memory metric data
+
+=head1 SYNOPSIS
+
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::Memory->new();
+ my $metrics = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::Memory> is a C<App::AWS::CloudWatch::Monitor::Check> module which gathers memory metric data.
+
+=head1 METRICS
+
+The following metrics are gathered and returned.
+
+=over
+
+=item MemoryUtilization
+
+=item MemoryUsed
+
+=item MemoryAvailable
+
+=item SwapUtilization
+
+=item SwapUsed
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
commit cf44c4fe970afe29d92b367f528baf78022f463b
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:25:00 2021 -0500

    Update test check modules to return arrayref

diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
index 31371e6..6b67e54 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
@@ -21,7 +21,7 @@ sub check {
         RawValue   => $value,
     };
 
-    return $metric;
+    return [ $metric ];
 }
 
 1;
@@ -36,8 +36,8 @@ App::AWS::CloudWatch::Monitor::Check::TestOne - test metric for tests
 
 =head1 SYNOPSIS
 
- my $plugin = App::AWS::CloudWatch::Monitor::Check::TestOne->new();
- my $metric = $plugin->check();
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestOne->new();
+ my $metrics = $plugin->check();
 
 =head1 DESCRIPTION
 
@@ -49,7 +49,7 @@ C<App::AWS::CloudWatch::Monitor::Check::TestOne> is a C<App::AWS::CloudWatch::Mo
 
 =item check
 
-Gathers the metric data and returns a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
 
 =back
 
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
index 4a4ef3e..99c1366 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
@@ -21,7 +21,7 @@ sub check {
         RawValue   => $value,
     };
 
-    return $metric;
+    return [ $metric ];
 }
 
 1;
@@ -36,8 +36,8 @@ App::AWS::CloudWatch::Monitor::Check::TestTwo - test metric for tests
 
 =head1 SYNOPSIS
 
- my $plugin = App::AWS::CloudWatch::Monitor::Check::TestTwo->new();
- my $metric = $plugin->check();
+ my $plugin  = App::AWS::CloudWatch::Monitor::Check::TestTwo->new();
+ my $metrics = $plugin->check();
 
 =head1 DESCRIPTION
 
@@ -49,7 +49,7 @@ C<App::AWS::CloudWatch::Monitor::Check::TestTwo> is a C<App::AWS::CloudWatch::Mo
 
 =item check
 
-Gathers the metric data and returns a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+Gathers the metric data and returns an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
 
 =back
 
commit 47061242ca7afd5a128a3c525548f2948f156a35
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 19:23:08 2021 -0500

    Update metrics parsing for multiple metrics
    
    This commit updates the metrics parsing to expect an arrayref of
    hashrefs returned, rather than just a hashref.  This change allows
    the check modules to gather and return multiple metrics, rather
    than only one per check module.
    
    Additionally, update the documentation in Check.pm to demonstrate
    building a return with multiple metrics.

diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 96acd16..6ccc1e8 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -57,8 +57,8 @@ sub run {
         };
 
         my $plugin = $class->new();
-        my ( $metric, $exception );
-        $metric = try {
+        my ( $metrics, $exception );
+        $metrics = try {
             return $plugin->check();
         }
         catch {
@@ -70,16 +70,18 @@ sub run {
             next;
         }
 
-        my ( $ret, $msg ) = $self->_verify_metric($metric);
+        my ( $ret, $msg ) = $self->_verify_metrics($metrics);
         unless ($ret) {
             warn "warning: Check::$module: $msg\n";
             next;
         }
 
-        push( @{ $metric->{Dimensions} }, { 'Name' => 'InstanceId', 'Value' => $instance_id } );
-        $metric->{Timestamp} = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_offset_time(NOW);
+        foreach my $metric ( @{$metrics} ) {
+            push( @{ $metric->{Dimensions} }, { 'Name' => 'InstanceId', 'Value' => $instance_id } );
+            $metric->{Timestamp} = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_offset_time(NOW);
 
-        push( @{ $param->{Input}{MetricData} }, $metric );
+            push( @{ $param->{Input}{MetricData} }, $metric );
+        }
     }
 
     unless ( scalar @{ $param->{Input}{MetricData} } ) {
@@ -115,21 +117,23 @@ sub run {
     return;
 }
 
-sub _verify_metric {
-    my $self   = shift;
-    my $metric = shift;
+sub _verify_metrics {
+    my $self    = shift;
+    my $metrics = shift;
 
-    unless ($metric) {
+    unless ($metrics) {
         return ( 0, 'no metric data was returned' );
     }
 
-    if ( ref $metric ne 'HASH' ) {
+    if ( ref $metrics ne 'ARRAY' ) {
         return ( 0, 'return is not in the expected format' );
     }
 
-    foreach my $key (qw{ MetricName Unit RawValue }) {
-        unless ( defined $metric->{$key} ) {
-            return ( 0, 'return does not contain the required keys' );
+    foreach my $metric ( @{$metrics} ) {
+        foreach my $key (qw{ MetricName Unit RawValue }) {
+            unless ( defined $metric->{$key} ) {
+                return ( 0, 'return does not contain the required keys' );
+            }
         }
     }
 
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check.pm b/lib/App/AWS/CloudWatch/Monitor/Check.pm
index cf5d4d1..680f075 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Check.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Check.pm
@@ -81,35 +81,47 @@ This module is not meant to be initialized directly, but through child modules.
 
 Additional check modules can be added as child modules to C<App::AWS::CloudWatch::Monitor::Check>.
 
-Child modules must implement the C<check> method which gathers, formats, and returns the metric.
+Child modules must implement the C<check> method which gathers, formats, and returns the metrics.
 
-The returned metric must be a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+The returned metrics must be an arrayref of hashrefs with keys C<MetricName>, C<Unit>, and C<RawValue>.
 
-The returned metric hashref may contain a C<Dimensions> key, but its value must be an arrayref containing hashrefs with the keys C<Name> and C<Value>.
+The returned metric hashrefs may contain a C<Dimensions> key, but its value must be an arrayref containing hashrefs with the keys C<Name> and C<Value>.
 
  # example MemoryUtilization check return without Dimensions data
- my $metric = {
-     MetricName => 'MemoryUtilization',
-     Unit       => 'Percent',
-     RawValue   => $mem_util,
- };
+ my $metric = [
+     {   MetricName => 'MemoryUtilization',
+         Unit       => 'Percent',
+         RawValue   => $mem_util,
+     },
+ ];
 
  # example DiskSpaceUtilization check return with Dimensions data
- my $metric = {
-     MetricName => 'DiskSpaceUtilization',
-     Unit       => 'Percent',
-     Value      => $disk_space_util,
-     Dimensions => [
-         {
-             Name  => 'Filesystem',
-             Value => $filesystem,
-         },
-         {
-             Name  => 'MountPath',
-             Value => $mount_path,
-         },
-     ],
- };
+ my $metric = [
+     {   MetricName => 'DiskSpaceUtilization',
+         Unit       => 'Percent',
+         RawValue   => $disk_space_util,
+         Dimensions => [
+             {   Name  => 'Filesystem',
+                 Value => $filesystem,
+             },
+             {   Name  => 'MountPath',
+                 Value => $mount_path,
+             },
+         ],
+     },
+ ];
+
+ # example Foo check return with multiple metrics
+ my $metrics = [
+     {   MetricName => 'FooOne',
+         Unit       => 'Percent',
+         RawValue   => $foo_one,
+     },
+     {   MetricName => 'FooTwo',
+         Unit       => 'Percent',
+         RawValue   => $foo_two,
+     },
+ ];
 
 =head1 CONSTRUCTOR
 
commit 1977ee86a9b4a5424d4079e89bf4f99f20386df5
Merge: a381030 883628b
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Oct 26 18:49:22 2021 -0500

    Merge branch 'add-framework'

commit 883628bdc852c789ae8b2edc0766ee98c3c0ff4f
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Oct 25 19:26:20 2021 -0500

    Add min perl version, tests, and exe to Makefile

diff --git a/Makefile.PL b/Makefile.PL
index c59ebcc..173368e 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -21,6 +21,7 @@ WriteMakefile(
         },
     },
 
+    MIN_PERL_VERSION => '5.10.1',
     BUILD_REQUIRES => {
         'ExtUtils::MakeMaker' => '6.46',
     },
@@ -51,4 +52,12 @@ WriteMakefile(
         'Try::Tiny' => 0,
         'URI::Escape' => 0,
     },
+
+    EXE_FILES => [
+        'bin/aws-cloudwatch-monitor',
+    ],
+
+    test => {
+        TESTS => 't/*.t',
+    },
 );
commit 3f3d7780479e818dd24482c346c7f2d7227846c5
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Oct 22 18:55:54 2021 -0500

    Initial add of build scaffolding

diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..c59ebcc
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,54 @@
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+    NAME => 'App::AWS::CloudWatch::Monitor',
+    AUTHOR => 'Best Practical Solutions, LLC <modules at bestpractical.com>',
+    ABSTRACT_FROM => 'lib/App/AWS/CloudWatch/Monitor.pm',
+    VERSION_FROM => 'lib/App/AWS/CloudWatch/Monitor.pm',
+    LICENSE => 'perl_5',
+    META_MERGE => {
+        'meta-spec' => { version => 2 },
+        resources => {
+            bugtracker => {
+                web => 'https://github.com/bestpractical/app-aws-cloudwatch-monitor/issues',
+            },
+            homepage => 'https://github.com/bestpractical/app-aws-cloudwatch-monitor',
+            repository => {
+                type => 'git',
+                url => 'https://github.com/bestpractical/app-aws-cloudwatch-monitor.git',
+                web => 'https://github.com/bestpractical/app-aws-cloudwatch-monitor',
+            },
+        },
+    },
+
+    BUILD_REQUIRES => {
+        'ExtUtils::MakeMaker' => '6.46',
+    },
+    CONFIGURE_REQUIRES => {
+        'ExtUtils::MakeMaker' => '6.46',
+    },
+    TEST_REQUIRES => {
+        'Capture::Tiny' => 0,
+        'File::Spec' => 0,
+        'FindBin' => 0,
+        'Test::Exception' => 0,
+        'Test::More' => 0,
+        'Test::Warnings' => 0,
+
+    },
+    PREREQ_PM => {
+        'base' => 0,
+        'Capture::Tiny' => 0,
+        'Compress::Zlib' => 0,
+        'constant' => 0,
+        'DateTime' => 0,
+        'Digest::SHA' => 0,
+        'FindBin' => 0,
+        'List::Util' => 0,
+        'LWP' => 6,
+        'LWP::Simple' => 0,
+        'Module::Loader' => 0,
+        'Try::Tiny' => 0,
+        'URI::Escape' => 0,
+    },
+);
commit 98e8639bc239d86d1bec56145ab0cc15093bf07d
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Aug 25 01:33:51 2021 +0000

    Update README with current documentation

diff --git a/README b/README
index 519cb06..b606aa8 100644
--- a/README
+++ b/README
@@ -1,2 +1,57 @@
-# App::AWS::CloudWatch::Monitor
+NAME
+    aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
+
+SYNOPSIS
+     aws-cloudwatch-monitor [--check <module>]
+                            [--from-cron] [--verify] [--verbose]
+                            [--version] [--help]
+
+DESCRIPTION
+    "aws-cloudwatch-monitor" collects and sends custom metrics to AWS
+    CloudWatch from an AWS EC2 instance.
+
+OPTIONS
+    --check <module>
+        Defines the checks to run.
+
+        Multiple "--check" options may be defined and are run in the order
+        they're passed.
+
+    --from-cron
+        Specifies that this script is running from cron.
+
+        "--verbose" is forced to off and results information is suppressed
+        if "--from-cron" is set.
+
+        "--from-cron" additionally adds a random sleep interval up to 20
+        seconds.
+
+    --verify
+        Checks configuration and prepares a remote call, but does not upload
+        metrics to CloudWatch.
+
+    --verbose
+        Print additional details while running.
+
+    --version
+        Print the version.
+
+    --help
+        Print the help menu.
+
+CONFIGURATION
+    An example configuration file, "config.ini.example", is provided in the
+    project root directory.
+
+    To set up the configuration file, copy the example into one of the
+    following locations:
+
+    "$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini"
+    "/etc/aws-cloudwatch-monitor/config.ini"
+
+    After creating the file, edit and update the values accordingly.
+
+    NOTE: If the "$ENV{HOME}/.config/aws-cloudwatch-monitor/" directory
+    exists, "config.ini" will be loaded from there regardless of a config
+    file in "/etc/aws-cloudwatch-monitor/".
 
commit 7555ac4ad41b0293d69f4335a19d5e3e70a3035f
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Aug 25 00:32:42 2021 +0000

    Add config documentation to aws-cloudwatch-monitor

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index cd04ac4..d2405c5 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -88,4 +88,22 @@ Print the help menu.
 
 =back
 
+=head1 CONFIGURATION
+
+An example configuration file, C<config.ini.example>, is provided in the project root directory.
+
+To set up the configuration file, copy the example into one of the following locations:
+
+=over
+
+=item C<$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini>
+
+=item C</etc/aws-cloudwatch-monitor/config.ini>
+
+=back
+
+After creating the file, edit and update the values accordingly.
+
+B<NOTE:> If the C<$ENV{HOME}/.config/aws-cloudwatch-monitor/> directory exists, C<config.ini> will be loaded from there regardless of a config file in C</etc/aws-cloudwatch-monitor/>.
+
 =cut
commit aeca5f33618dfe22237cd35d8cd57b9c20d2e5be
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Aug 24 00:57:26 2021 +0000

    Load config from a more standardized location
    
    To provide a more standardized configuration, the configuration
    is now loaded from either
    $ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini
    or
    /etc/aws-cloudwatch-monitor/config.ini
    if $ENV{HOME}/.config/aws-cloudwatch-monitor is not present.
    
    The config file has also been renamed to config.ini.

diff --git a/.aws-cloudwatch-monitor-rc.example b/config.ini.example
similarity index 100%
rename from .aws-cloudwatch-monitor-rc.example
rename to config.ini.example
diff --git a/lib/App/AWS/CloudWatch/Monitor/Config.pm b/lib/App/AWS/CloudWatch/Monitor/Config.pm
index 7edd68d..f95c8ed 100644
--- a/lib/App/AWS/CloudWatch/Monitor/Config.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/Config.pm
@@ -3,7 +3,6 @@ package App::AWS::CloudWatch::Monitor::Config;
 use strict;
 use warnings;
 
-use File::HomeDir;
 use Config::Tiny;
 
 our $VERSION = '0.01';
@@ -14,11 +13,28 @@ sub load {
     return $config;
 }
 
+sub _get_conf_dir {
+    my $name = 'aws-cloudwatch-monitor';
+
+    my $dir;
+    if ( $ENV{HOME} && -d "$ENV{HOME}/.config/$name" ) {
+        $dir = "$ENV{HOME}/.config";
+    }
+    elsif ( -d "/etc/$name" ) {
+        $dir = '/etc';
+    }
+    else {
+        die "error: unable to find config directory\n";
+    }
+
+    return "$dir/$name";
+}
+
 sub _load_and_verify {
-    my $rc = File::HomeDir->my_home . '/.aws-cloudwatch-monitor-rc';
+    my $rc = _get_conf_dir() . '/config.ini';
 
     unless ( -e $rc && -r $rc ) {
-        die "$rc does not exist or cannot be read\n";
+        die "error: $rc does not exist or cannot be read\n";
     }
 
     my $config = Config::Tiny->read($rc);
@@ -70,10 +86,20 @@ Load and verify the config.
 
 =head1 CONFIGURATION
 
-The configuration file is loaded from the running user's home directory.
+An example configuration file, C<config.ini.example>, is provided in the project root directory.
+
+To set up the configuration file, copy the example into one of the following locations:
+
+=over
+
+=item C<$ENV{HOME}/.config/aws-cloudwatch-monitor/config.ini>
+
+=item C</etc/aws-cloudwatch-monitor/config.ini>
+
+=back
 
-An example config, C<.aws-cloudwatch-monitor-rc.example>, is provided in the project root directory.
+After creating the file, edit and update the values accordingly.
 
-To set up the config, copy C<.aws-cloudwatch-monitor-rc.example> into the running user's home directory, then update the values accordingly.
+B<NOTE:> If the C<$ENV{HOME}/.config/aws-cloudwatch-monitor/> directory exists, C<config.ini> will be loaded from there regardless of a config file in C</etc/aws-cloudwatch-monitor/>.
 
 =cut
diff --git a/t/.aws-cloudwatch-monitor-rc b/t/config.ini
similarity index 100%
rename from t/.aws-cloudwatch-monitor-rc
rename to t/config.ini
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
index cc2c79d..690e071 100644
--- a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
@@ -22,8 +22,8 @@ sub import {
     # load the .aws-cloudwatch-monitor-rc file from t/ directory
     require FindBin;
     override(
-        package => 'File::HomeDir',
-        name    => 'my_home',
+        package => 'App::AWS::CloudWatch::Monitor::Config',
+        name    => '_get_conf_dir',
         subref  => sub { $FindBin::RealBin },
     );
 
commit 36a54fee26e9c5c2276f1e211e50d91fe04de898
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Aug 23 23:25:53 2021 +0000

    Add better error checking to Monitor run

diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 9222d09..96acd16 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -53,12 +53,28 @@ sub run {
             $loader->load($class);
         }
         catch {
-            my $exception = $_;
-            die "$exception\n";
+            die "$_\n";
         };
 
         my $plugin = $class->new();
-        my $metric = $plugin->check();
+        my ( $metric, $exception );
+        $metric = try {
+            return $plugin->check();
+        }
+        catch {
+            chomp( $exception = $_ );
+        };
+
+        if ($exception) {
+            warn "error: Check::$module: $exception\n";
+            next;
+        }
+
+        my ( $ret, $msg ) = $self->_verify_metric($metric);
+        unless ($ret) {
+            warn "warning: Check::$module: $msg\n";
+            next;
+        }
 
         push( @{ $metric->{Dimensions} }, { 'Name' => 'InstanceId', 'Value' => $instance_id } );
         $metric->{Timestamp} = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_offset_time(NOW);
@@ -66,6 +82,11 @@ sub run {
         push( @{ $param->{Input}{MetricData} }, $metric );
     }
 
+    unless ( scalar @{ $param->{Input}{MetricData} } ) {
+        print "\nNo metrics to upload; exiting\n\n";
+        exit;
+    }
+
     $opt->{'aws-access-key-id'} = $self->config->{aws}{aws_access_key_id};
     $opt->{'aws-secret-key'}    = $self->config->{aws}{aws_secret_access_key};
     $opt->{retries}             = 2;
@@ -94,6 +115,27 @@ sub run {
     return;
 }
 
+sub _verify_metric {
+    my $self   = shift;
+    my $metric = shift;
+
+    unless ($metric) {
+        return ( 0, 'no metric data was returned' );
+    }
+
+    if ( ref $metric ne 'HASH' ) {
+        return ( 0, 'return is not in the expected format' );
+    }
+
+    foreach my $key (qw{ MetricName Unit RawValue }) {
+        unless ( defined $metric->{$key} ) {
+            return ( 0, 'return does not contain the required keys' );
+        }
+    }
+
+    return 1;
+}
+
 1;
 
 __END__
commit d9c3d6569bfe2415d3435670e0bb41e87dc71f41
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Jun 4 00:45:37 2021 +0000

    Add test check modules, monitor and check tests

diff --git a/t/check-constants.t b/t/check-constants.t
new file mode 100644
index 0000000..76dd5b6
--- /dev/null
+++ b/t/check-constants.t
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor::Check';
+use_ok($class);
+
+HAPPY_PATH: {
+    note( 'happy path' );
+
+    my $obj = $class->new();
+    my $constants = $obj->constants;
+
+    my $expected = {
+        'GIGA' => 1073741824,
+        'KILO' => 1024,
+        'MEGA' => 1048576,
+        'BYTE' => 1
+    };
+
+    is_deeply( $constants, $expected, 'constants contains the expected keys and values' );
+}
+
+done_testing();
diff --git a/t/check.t b/t/check.t
new file mode 100644
index 0000000..f9138df
--- /dev/null
+++ b/t/check.t
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor::Check';
+use_ok($class);
+
+OBJECT_AND_METHODS: {
+    note( 'object and methods' );
+
+    my $obj = $class->new();
+    isa_ok( $obj, $class );
+
+    foreach my $method ( qw{ run_command constants } ) {
+        can_ok( $obj, $method );
+    }
+}
+
+done_testing();
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
new file mode 100644
index 0000000..31371e6
--- /dev/null
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestOne.pm
@@ -0,0 +1,56 @@
+package App::AWS::CloudWatch::Monitor::Check::TestOne;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    my @echo_testing_command = (qw{ /bin/echo testone });
+    ( my $exit, my $stdout, my $stderr ) = $self->run_command( \@echo_testing_command );
+
+    my $value = ( $stdout ? 1 : 0 );
+
+    my $metric = {
+        MetricName => 'TestOne',
+        Unit       => 'Count',
+        RawValue   => $value,
+    };
+
+    return $metric;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::TestOne - test metric for tests
+
+=head1 SYNOPSIS
+
+ my $plugin = App::AWS::CloudWatch::Monitor::Check::TestOne->new();
+ my $metric = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::TestOne> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
new file mode 100644
index 0000000..4a4ef3e
--- /dev/null
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Check/TestTwo.pm
@@ -0,0 +1,56 @@
+package App::AWS::CloudWatch::Monitor::Check::TestTwo;
+
+use strict;
+use warnings;
+
+use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+our $VERSION = '0.01';
+
+sub check {
+    my $self = shift;
+
+    my @echo_testing_command = (qw{ /bin/echo testtwo });
+    ( my $exit, my $stdout, my $stderr ) = $self->run_command( \@echo_testing_command );
+
+    my $value = ( $stdout ? 1 : 0 );
+
+    my $metric = {
+        MetricName => 'TestTwo',
+        Unit       => 'Count',
+        RawValue   => $value,
+    };
+
+    return $metric;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check::TestTwo - test metric for tests
+
+=head1 SYNOPSIS
+
+ my $plugin = App::AWS::CloudWatch::Monitor::Check::TestTwo->new();
+ my $metric = $plugin->check();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check::TestTwo> is a C<App::AWS::CloudWatch::Monitor::Check> module to use in tests.
+
+=head1 METHODS
+
+=over
+
+=item check
+
+Gathers the metric data and returns a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+=back
+
+=cut
diff --git a/t/monitor-config.t b/t/monitor-config.t
new file mode 100644
index 0000000..3b7cc01
--- /dev/null
+++ b/t/monitor-config.t
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor';
+use_ok($class);
+
+HAPPY_PATH: {
+    note( 'happy path' );
+
+    my $obj = $class->new();
+    my $config = $obj->config;
+
+    # we already verify the config contents in the config test
+    # and don't need to re-verify here.
+    # just check if it's a config object and has keys.
+    isa_ok( $config, 'Config::Tiny' );
+    ok( keys %{$config}, 'config contains keys' );
+}
+
+done_testing();
diff --git a/t/monitor-run.t b/t/monitor-run.t
new file mode 100644
index 0000000..57e5075
--- /dev/null
+++ b/t/monitor-run.t
@@ -0,0 +1,56 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+use Capture::Tiny;
+
+my $class = 'App::AWS::CloudWatch::Monitor';
+use_ok($class);
+
+App::AWS::CloudWatch::Monitor::Test::override(
+    package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
+    name    => 'get_instance_id',
+    subref  => sub { return 'i12345test' },
+);
+
+HAPPY_PATH_MOCKED: {
+    note( 'happy path mocked' );
+
+    # To allow testing the basic functionality of Monitor->run without needing to
+    # run the tests on an AWS instance, this test mocks out the interactions with
+    # the instance and AWS.
+    # Mocking call_json skips a lot of internal functionality that we should
+    # verify.  More tests should be added which run through those internals, but
+    # should first check if on an AWS instance and skip if not.
+    # Those tests still shouldn't connect to CloudWatch to upload metrics.
+
+    App::AWS::CloudWatch::Monitor::Test::override(
+        package => 'App::AWS::CloudWatch::Monitor::CloudWatchClient',
+        name    => 'call_json',
+        subref  => sub {
+            return (
+                HTTP::Response->new(
+                    200,
+                    'This is a mocked response from the test',
+                    [ 'x-amzn-requestid' => '12345-67a8' ],
+                )
+            );
+        },
+    );
+
+    my $opt = {
+        check => [ qw{ TestOne TestTwo } ],
+    };
+
+    my $obj = $class->new();
+    my ( $stdout, $stderr, @result ) = Capture::Tiny::capture { $obj->run($opt) };
+
+    # Successfully reported metrics to CloudWatch. Reference Id: 12345-67a8
+    like( $stdout, qr/Successfully reported metrics/, 'response contains success message' );
+    like( $stdout, qr/Reference Id: \d+/, 'response contains reference id' );
+}
+
+done_testing();
diff --git a/t/monitor.t b/t/monitor.t
new file mode 100644
index 0000000..ac13d13
--- /dev/null
+++ b/t/monitor.t
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor';
+use_ok($class);
+
+OBJECT_AND_METHODS: {
+    note( 'object and methods' );
+
+    my $obj = $class->new();
+    isa_ok( $obj, $class );
+
+    foreach my $method ( qw{ config run } ) {
+        can_ok( $obj, $method );
+    }
+}
+
+done_testing();
commit 4880f12d1d4d01f398ee29d29c862d8244211ac9
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Jun 3 14:44:36 2021 +0000

    Add opts, params, and payload to upload to AWS
    
    This commit adds the remaining parts to upload the metrics to AWS,
    including the opts, params, and payload, then calls the call_json
    method to upload.

diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 54b7b7b..9222d09 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -5,6 +5,7 @@ use warnings;
 
 use App::AWS::CloudWatch::Monitor::Config;
 use App::AWS::CloudWatch::Monitor::CloudWatchClient;
+use List::Util;
 use Try::Tiny;
 use Module::Loader;
 
@@ -12,6 +13,9 @@ our $VERSION = '0.01';
 
 my $config;
 
+use constant CLIENT_NAME => 'App-AWS-CloudWatch-Monitor';
+use constant NOW         => 0;
+
 sub new {
     my $class = shift;
     my $self  = {};
@@ -33,8 +37,17 @@ sub run {
     my $instance_id = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_instance_id();
     my $loader      = Module::Loader->new;
 
-    my @metrics;
-    foreach my $module ( @{ $opt->{check} } ) {
+    if ( $opt->{'from-cron'} ) {
+        sleep( rand(20) );
+    }
+
+    my $param = {};
+    $param->{Input}{Namespace}  = 'System/Linux';
+    $param->{Input}{MetricData} = [];
+
+    my $checks = delete $opt->{check};
+
+    foreach my $module ( List::Util::uniq @{$checks} ) {
         my $class = q{App::AWS::CloudWatch::Monitor::Check::} . $module;
         try {
             $loader->load($class);
@@ -47,8 +60,35 @@ sub run {
         my $plugin = $class->new();
         my $metric = $plugin->check();
 
-        push @{ $metric->{Dimensions} }, { Name => 'InstanceId', Value => $instance_id };
-        push @metrics, $metric;
+        push( @{ $metric->{Dimensions} }, { 'Name' => 'InstanceId', 'Value' => $instance_id } );
+        $metric->{Timestamp} = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_offset_time(NOW);
+
+        push( @{ $param->{Input}{MetricData} }, $metric );
+    }
+
+    $opt->{'aws-access-key-id'} = $self->config->{aws}{aws_access_key_id};
+    $opt->{'aws-secret-key'}    = $self->config->{aws}{aws_secret_access_key};
+    $opt->{retries}             = 2;
+    $opt->{'user-agent'}        = CLIENT_NAME . "/$VERSION";
+
+    my $response = App::AWS::CloudWatch::Monitor::CloudWatchClient::call_json( 'PutMetricData', $param, $opt );
+    my $code     = $response->code;
+    my $message  = $response->message;
+
+    if ( $code == 200 && !$opt->{'from-cron'} ) {
+        if ( $opt->{verify} ) {
+            print "\nVerification completed successfully. No actual metrics sent to CloudWatch.\n\n";
+        }
+        else {
+            my $request_id = $response->headers->{'x-amzn-requestid'};
+            print "\nSuccessfully reported metrics to CloudWatch. Reference Id: $request_id\n\n";
+        }
+    }
+    elsif ( $code < 100 ) {
+        die "error: $message\n";
+    }
+    elsif ( $code != 200 ) {
+        die "Failed to call CloudWatch: HTTP $code. Message: $message\n";
     }
 
     return;
commit d535326b3b7d1c7a18e0ca0f5f2a56a09d516cf1
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Jun 3 00:00:02 2021 +0000

    Add from-cron option to aws-cloudwatch-monitor
    
    This option forces verbose to 0 and add a random sleep interval to
    offset the calls to AWS at the top of the interval.

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 6203394..cd04ac4 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -12,6 +12,7 @@ my $VERSION = '0.01';
 Getopt::Long::GetOptions(
     \my %opt,
     'check=s@',
+    'from-cron',
     'verify',
     'verbose',
     'version' => sub { print "aws-cloudwatch-monitor version $VERSION\n"; exit 0 },
@@ -24,6 +25,10 @@ Pod::Usage::pod2usage( -message => 'Option check is required', -exitval => 1 ) u
 delete $opt{version};
 delete $opt{help};
 
+if ($opt{'from-cron'}) {
+    $opt{verbose} = 0;
+}
+
 my $monitor = App::AWS::CloudWatch::Monitor->new();
 $monitor->run(\%opt);
 
@@ -40,7 +45,7 @@ aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
 =head1 SYNOPSIS
 
  aws-cloudwatch-monitor [--check <module>]
-                        [--verify] [--verbose]
+                        [--from-cron] [--verify] [--verbose]
                         [--version] [--help]
 
 =head1 DESCRIPTION
@@ -57,6 +62,14 @@ Defines the checks to run.
 
 Multiple C<--check> options may be defined and are run in the order they're passed.
 
+=item --from-cron
+
+Specifies that this script is running from cron.
+
+C<--verbose> is forced to off and results information is suppressed if C<--from-cron> is set.
+
+C<--from-cron> additionally adds a random sleep interval up to 20 seconds.
+
 =item --verify
 
 Checks configuration and prepares a remote call, but does not upload metrics to CloudWatch.
commit 31c46d98b6b81b3372521ec8e01914d08b719e7f
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jun 2 23:40:21 2021 +0000

    Add verbose option to aws-cloudwatch-monitor
    
    This option tells the CloudWatchClient to display additional
    details about the run.

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 0d111ef..6203394 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -13,6 +13,7 @@ Getopt::Long::GetOptions(
     \my %opt,
     'check=s@',
     'verify',
+    'verbose',
     'version' => sub { print "aws-cloudwatch-monitor version $VERSION\n"; exit 0 },
     'help',
 ) or Pod::Usage::pod2usage( -exitval => 1 );
@@ -39,7 +40,7 @@ aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
 =head1 SYNOPSIS
 
  aws-cloudwatch-monitor [--check <module>]
-                        [--verify]
+                        [--verify] [--verbose]
                         [--version] [--help]
 
 =head1 DESCRIPTION
@@ -60,6 +61,10 @@ Multiple C<--check> options may be defined and are run in the order they're pass
 
 Checks configuration and prepares a remote call, but does not upload metrics to CloudWatch.
 
+=item --verbose
+
+Print additional details while running.
+
 =item --version
 
 Print the version.
commit 88ea9da1d1c6d8a1204c274742bf7a54ad8df819
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jun 2 23:35:01 2021 +0000

    Add full namespace to AwsSignatureV4 calls

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 25c8767..0d35a39 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -22,6 +22,7 @@
 # Update indirect object call syntax
 # Update reused variable name
 # Convert internal comments to pod
+# Add full namespace to AwsSignatureV4 calls
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -706,7 +707,7 @@ sub get_json_payload_and_headers {
     my $operation = $params->{'Operation'};
     my $json_data = construct_payload($params);
 
-    my $sigv4 = AwsSignatureV4->new_aws_json( $operation, $json_data, $opts );
+    my $sigv4 = App::AWS::CloudWatch::Monitor::AwsSignatureV4->new_aws_json( $operation, $json_data, $opts );
     if ( !( $sigv4->sign_http_post() ) ) {
         return { "code" => ERROR, "error" => $sigv4->error };
     }
@@ -866,7 +867,7 @@ sub call_query {
         return ( HTTP::Response->new( $validation_contents->{"code"}, $validation_contents->{"error"} ) );
     }
 
-    my $sigv4 = AwsSignatureV4->new_aws_query( $params, $opts );
+    my $sigv4 = App::AWS::CloudWatch::Monitor::AwsSignatureV4->new_aws_query( $params, $opts );
 
     if ( !$sigv4->sign_http_post() ) {
         return ( HTTP::Response->new( 400, $sigv4->{'error'} ) );
commit 62c6c16c497df159e486d096fae16343ffa66890
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jun 2 22:04:39 2021 +0000

    Add verify option to aws-cloudwatch-monitor
    
    This option tells the CloudWatchClient to not upload metrics.

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 291c5ee..0d111ef 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -12,6 +12,7 @@ my $VERSION = '0.01';
 Getopt::Long::GetOptions(
     \my %opt,
     'check=s@',
+    'verify',
     'version' => sub { print "aws-cloudwatch-monitor version $VERSION\n"; exit 0 },
     'help',
 ) or Pod::Usage::pod2usage( -exitval => 1 );
@@ -38,6 +39,7 @@ aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
 =head1 SYNOPSIS
 
  aws-cloudwatch-monitor [--check <module>]
+                        [--verify]
                         [--version] [--help]
 
 =head1 DESCRIPTION
@@ -54,6 +56,10 @@ Defines the checks to run.
 
 Multiple C<--check> options may be defined and are run in the order they're passed.
 
+=item --verify
+
+Checks configuration and prepares a remote call, but does not upload metrics to CloudWatch.
+
 =item --version
 
 Print the version.
commit 02d16b219aaedf86d8faaa0a8d30dc9f41e54688
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 17:07:34 2021 -0500

    Convert internal comments to pod
    
    This commit converts internal comments to pod for AwsSignatureV4

diff --git a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
index 18b1e55..0f11b88 100644
--- a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
@@ -14,6 +14,7 @@
 # this package has been updated from the original version to:
 # Update formatting
 # Update package namespace and VERSION
+# Convert internal comments to pod
 
 package App::AWS::CloudWatch::Monitor::AwsSignatureV4;
 
@@ -38,26 +39,45 @@ our $UNSAFE_CHARACTERS = '^' . $SAFE_CHARACTERS;
 # Name of the signature encoding algorithm.
 our $ALGORITHM_NAME = 'AWS4-HMAC-SHA256';
 
-#
-# Creates a new signing context object for signing an arbitrary request.
-#
-# Signing context object is a hash of all request data needed to create
-# a valid AWS Signature V4. After the signing takes place, the context object
-# gets populated with intermediate signing artifacts and the actual signature.
-#
-# Input:
-#
-#   $opts - reference to hash that contains control options for the request
-#     url => endpoint of the service to call, e.g. https://monitoring.us-west-2.amazonaws.com/
-#         (the URL can contain path but it should not include query string, i.e. args after ?)
-#     aws-region => explicitly specifies AWS region (if not specified, region is extracted
-#         from endpoint URL; if region is not part of URL, 'us-east-1' is used by default)
-#     aws-service => explicitly specifies AWS service name (this is necessary when service
-#         name is not part of the endpoint URL, e.g. mail/ses, but usually it is)
-#     aws-access-key-id => Access Key Id of AWS credentials
-#     aws-secret-key => Secret Key of AWS credentials
-#     aws-security-token => Security Token in case of STS call
-#
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::AwsSignatureV4 - methods for creating signing objects
+
+=head1 SYNOPSIS
+
+ use App::AWS::CloudWatch::Monitor::AwsSignatureV4;
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::AwsSignatureV4> contains methods for creating signing objects for interacting with AWS.
+
+=head1 CONSTRUCTORS
+
+=over
+
+=item new
+
+Creates a new signing context object for signing an arbitrary request.
+
+Signing context object is a hash of all request data needed to create
+a valid AWS Signature V4. After the signing takes place, the context object
+gets populated with intermediate signing artifacts and the actual signature.
+
+Input:
+
+ $opts - reference to hash that contains control options for the request
+   url => endpoint of the service to call, e.g. https://monitoring.us-west-2.amazonaws.com/
+       (the URL can contain path but it should not include query string, i.e. args after ?)
+   aws-region => explicitly specifies AWS region (if not specified, region is extracted
+       from endpoint URL; if region is not part of URL, 'us-east-1' is used by default)
+   aws-service => explicitly specifies AWS service name (this is necessary when service
+       name is not part of the endpoint URL, e.g. mail/ses, but usually it is)
+   aws-access-key-id => Access Key Id of AWS credentials
+   aws-secret-key => Secret Key of AWS credentials
+   aws-security-token => Security Token in case of STS call
+
+=cut
+
 sub new {
     my $class = shift;
     my $self  = { opts => shift };
@@ -66,20 +86,22 @@ sub new {
     return $self;
 }
 
-#
-# Creates a new signing context object for signing AWS/Query request.
-#
-# AWS/Query request can be signed for either HTTP GET method or POST method.
-# The recommended method is POST as it skips sorting of query string keys
-# and therefore performs faster.
-#
-# Input:
-#
-#   $params - reference to the hash that contains all (name, value) pairs of AWS/Query request
-#     (do not url-encode this data, it will be done as a part of signing and creating payload)
-#
-#   $opts - see defition of 'new' constructor
-#
+=item new_aws_query
+
+Creates a new signing context object for signing AWS/Query request.
+
+AWS/Query request can be signed for either HTTP GET method or POST method.
+The recommended method is POST as it skips sorting of query string keys
+and therefore performs faster.
+
+Input:
+
+ $params - reference to the hash that contains all (name, value) pairs of AWS/Query request
+   (do not url-encode this data, it will be done as a part of signing and creating payload)
+ $opts - see defition of 'new' constructor
+
+=cut
+
 sub new_aws_query {
     my $class = shift;
     my $self  = { params => shift, opts => shift };
@@ -88,15 +110,18 @@ sub new_aws_query {
     return bless $self, $class;
 }
 
-#
-# Creates a new signing context object for signing RPC/JSON request.
-# It only makes sense to sign JSON request for HTTP POST method.
-#
-# Input:
-#
-#   $payload - input data in RPC/JSON format
-#   $opts - see defition of 'new' constructor
-#
+=item new_rpc_json
+
+Creates a new signing context object for signing RPC/JSON request.
+It only makes sense to sign JSON request for HTTP POST method.
+
+Input:
+
+ $payload - input data in RPC/JSON format
+ $opts - see defition of 'new' constructor
+
+=cut
+
 sub new_rpc_json {
     my $class = shift;
     my $self  = { payload => shift, opts => shift };
@@ -105,16 +130,21 @@ sub new_rpc_json {
     return $self;
 }
 
-#
-# Creates a new signing context object for signing AWS/JSON request.
-# It only makes sense to sign JSON request for HTTP POST method.
-#
-# Input:
-#
-#   $operation - operation name to invoke
-#   $payload - input data in AWS/JSON format
-#   $opts - see defition of 'new' constructor
-#
+=item new_aws_json
+
+Creates a new signing context object for signing AWS/JSON request.
+It only makes sense to sign JSON request for HTTP POST method.
+
+Input:
+
+ $operation - operation name to invoke
+ $payload - input data in AWS/JSON format
+ $opts - see defition of 'new' constructor
+
+=back
+
+=cut
+
 sub new_aws_json {
     my $class     = shift;
     my $operation = shift;
@@ -135,15 +165,22 @@ sub new_aws_json {
     return $self;
 }
 
-#
-# Signs the generic HTTP request.
-#
-# Input: (all arguments optional and if specified override what is currently set)
-#
-#   $method - HTTP method
-#   $ctype - content-type of the body
-#   $payload - request body data
-#
+=head1 METHODS/SUBROUTINES
+
+=over
+
+=item sign_http_request
+
+Signs the generic HTTP request.
+
+Input: (all arguments optional and if specified override what is currently set)
+
+ $method - HTTP method
+ $ctype - content-type of the body
+ $payload - request body data
+
+=cut
+
 sub sign_http_request {
     my $self    = shift;
     my $method  = shift;
@@ -166,25 +203,34 @@ sub sign_http_request {
     return 1;
 }
 
-#
-# Signs request for HTTP POST.
-#
+=item sign_http_post
+
+Signs request for HTTP POST.
+
+=cut
+
 sub sign_http_post {
     my $self = shift;
     return $self->sign_http_request('POST');
 }
 
-#
-# Signs request for HTTP PUT.
-#
+=item sign_http_put
+
+Signs request for HTTP PUT.
+
+=cut
+
 sub sign_http_put {
     my $self = shift;
     return $self->sign_http_request('PUT');
 }
 
-#
-# Signs request for HTTP GET.
-#
+=item sign_http_get
+
+Signs request for HTTP GET.
+
+=cut
+
 sub sign_http_get {
     my $self = shift;
     my $opts = $self->{opts};
@@ -212,9 +258,12 @@ sub sign_http_get {
     return 1;
 }
 
-#
-# Prepares and signs the request data.
-#
+=item sign
+
+Prepares and signs the request data.
+
+=cut
+
 sub sign {
     my $self = shift;
 
@@ -229,57 +278,75 @@ sub sign {
     return 1;
 }
 
-#
-# Returns reference to a hash containing all required HTTP headers.
-# In case of HTTP POST and PUT methods it will also include the
-# Authorization header that carries the signature itself.
-#
+=item headers
+
+Returns reference to a hash containing all required HTTP headers.
+In case of HTTP POST and PUT methods it will also include the
+Authorization header that carries the signature itself.
+
+=cut
+
 sub headers {
     my $self = shift;
     return $self->{'headers'};
 }
 
-#
-# In case of AWS/Query request and HTTP POST or PUT method, returns
-# url-encoded query string to be used as a body of HTTP POST request.
-#
+=item payload
+
+In case of AWS/Query request and HTTP POST or PUT method, returns
+url-encoded query string to be used as a body of HTTP POST request.
+
+=cut
+
 sub payload {
     my $self = shift;
     return $self->{'payload'};
 }
 
-#
-# Returns complete signed URL to be used in HTTP GET request 'as is'.
-# You can place this value into Web browser location bar and make a call.
-#
+=item signed_url
+
+Returns complete signed URL to be used in HTTP GET request 'as is'.
+You can place this value into Web browser location bar and make a call.
+
+=cut
+
 sub signed_url {
     my $self = shift;
     return $self->{'signed-url'};
 }
 
-#
-# Returns URL to be used in HTTP GET request for the case,
-# when the signature is passed via Authorization HTTP header.
-#
-# You can not use this URL with the Web browser since it does
-# not contain the signature.
-#
+=item request_url
+
+Returns URL to be used in HTTP GET request for the case,
+when the signature is passed via Authorization HTTP header.
+
+You can not use this URL with the Web browser since it does
+not contain the signature.
+
+=cut
+
 sub request_url {
     my $self = shift;
     return $self->{'request-url'};
 }
 
-#
-# Returns an error message if any.
-#
+=item error
+
+Returns an error message if any.
+
+=cut
+
 sub error {
     my $self = shift;
     return $self->{'error'};
 }
 
-#
-# Returns both timestamp and daystamp in the format required for SigV4.
-#
+=item get_timestamp_daystamp
+
+Returns both timestamp and daystamp in the format required for SigV4.
+
+=cut
+
 sub get_timestamp_daystamp {
     my $time = shift;
     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime(time);
@@ -288,18 +355,24 @@ sub get_timestamp_daystamp {
     return ( $timestamp, $daystamp );
 }
 
-#
-# Applies regex to get FQDN from URL.
-#
+=item extract_fqdn_from_url
+
+Applies regex to get FQDN from URL.
+
+=cut
+
 sub extract_fqdn_from_url {
     my $fqdn = shift;
     $fqdn =~ s!^https?://([^/:?]*).*$!$1!;
     return $fqdn;
 }
 
-#
-# Applies regex to get service name from the FQDN.
-#
+=item extract_service_from_fqdn
+
+Applies regex to get service name from the FQDN.
+
+=cut
+
 sub extract_service_from_fqdn {
     my $fqdn    = shift;
     my $service = $fqdn;
@@ -307,9 +380,12 @@ sub extract_service_from_fqdn {
     return $service;
 }
 
-#
-# Applies regex to get region from the FQDN.
-#
+=item extract_region_from_fqdn
+
+Applies regex to get region from the FQDN.
+
+=cut
+
 sub extract_region_from_fqdn {
     my $fqdn  = shift;
     my @parts = split( /\./, $fqdn );
@@ -317,9 +393,12 @@ sub extract_region_from_fqdn {
     return 'us-east-1';
 }
 
-#
-# Applies regex to get the path part of the URL.
-#
+=item extract_path_from_url
+
+Applies regex to get the path part of the URL.
+
+=cut
+
 sub extract_path_from_url {
     my $url  = shift;
     my $path = $url;
@@ -328,17 +407,21 @@ sub extract_path_from_url {
     return $path;
 }
 
-#
-# Populates essential HTTP headers required for SigV4.
-#
-# CanonicalHeaders =
-#   CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN
-# CanonicalHeadersEntry =
-#   LOWERCASE(HeaderName) + ':' + TRIM(HeaderValue) '\n'
-#
-# SignedHeaders =
-#   LOWERCASE(HeaderName0) + ';' + LOWERCASE(HeaderName1) + ... + LOWERCASE(HeaderNameN)
-#
+=item create_basic_headers
+
+Populates essential HTTP headers required for SigV4.
+
+ CanonicalHeaders =
+   CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN
+
+ CanonicalHeadersEntry =
+   LOWERCASE(HeaderName) + ':' + TRIM(HeaderValue) '\n'
+
+ SignedHeaders =
+   LOWERCASE(HeaderName0) + ';' + LOWERCASE(HeaderName1) + ... + LOWERCASE(HeaderNameN)
+
+=cut
+
 sub create_basic_headers {
     my $self = shift;
     my $opts = $self->{opts};
@@ -407,9 +490,12 @@ sub create_basic_headers {
     return 1;
 }
 
-#
-# Validates input and populates essential pre-requisites.
-#
+=item initialize
+
+Validates input and populates essential pre-requisites.
+
+=cut
+
 sub initialize {
     my $self = shift;
     my $opts = $self->{opts};
@@ -498,12 +584,15 @@ sub initialize {
     return 1;
 }
 
-#
-# Builds up AWS Query request as a chain of url-encoded name=value pairs separated by &.
-#
-# Note that SigV4 is payload-agnostic when it comes to POST request body so there is no
-# need to sort arguments in the AWS Query string for the POST method.
-#
+=item create_query_string
+
+Builds up AWS Query request as a chain of url-encoded name=value pairs separated by &.
+
+Note that SigV4 is payload-agnostic when it comes to POST request body so there is no
+need to sort arguments in the AWS Query string for the POST method.
+
+=cut
+
 sub create_query_string {
     my $self   = shift;
     my $opts   = $self->{opts};
@@ -559,15 +648,18 @@ sub create_query_string {
     return 1;
 }
 
-#
-# CanonicalRequest =
-#   Method + '\n' +
-#   CanonicalURI + '\n' +
-#   CanonicalQueryString + '\n' +
-#   CanonicalHeaders + '\n' +
-#   SignedHeaders + '\n' +
-#   HEX(Hash(Payload))
-#
+=item create_canonical_request
+
+ CanonicalRequest =
+   Method + '\n' +
+   CanonicalURI + '\n' +
+   CanonicalQueryString + '\n' +
+   CanonicalHeaders + '\n' +
+   SignedHeaders + '\n' +
+   HEX(Hash(Payload))
+
+=cut
+
 sub create_canonical_request {
     my $self = shift;
     my $opts = $self->{opts};
@@ -583,13 +675,16 @@ sub create_canonical_request {
     return $canonical_request;
 }
 
-#
-# StringToSign =
-#   Algorithm + '\n' +
-#   Timestamp + '\n' +
-#   Scope + '\n' +
-#   HEX(Hash(CanonicalRequest))
-#
+=item create_string_to_sign
+
+ StringToSign =
+   Algorithm + '\n' +
+   Timestamp + '\n' +
+   Scope + '\n' +
+   HEX(Hash(CanonicalRequest))
+
+=cut
+
 sub create_string_to_sign {
     my $self = shift;
     my $opts = $self->{opts};
@@ -606,9 +701,12 @@ sub create_string_to_sign {
     return $string_to_sign;
 }
 
-#
-# Performs the actual signing of the request.
-#
+=item create_signature
+
+Performs the actual signing of the request.
+
+=cut
+
 sub create_signature {
     my $self = shift;
     my $opts = $self->{opts};
@@ -626,9 +724,14 @@ sub create_signature {
     return $signature;
 }
 
-#
-# Populates HTTP header that carries authentication data.
-#
+=item create_authz_header
+
+Populates HTTP header that carries authentication data.
+
+=back
+
+=cut
+
 sub create_authz_header {
     my $self = shift;
     my $opts = $self->{opts};
commit 2bb38cbc0ddb463dd9914cb6ecc828c51401dd17
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 16:35:10 2021 -0500

    Convert internal comments to pod
    
    This commit converts internal comments to pod for CloudWatchClient

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 8892e68..25c8767 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -21,6 +21,7 @@
 # Update double sigil dereference
 # Update indirect object call syntax
 # Update reused variable name
+# Convert internal comments to pod
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -92,9 +93,28 @@ our $image_id;
 our $as_group_name;
 our $meta_data_loc = '/var/tmp/aws-mon';
 
-#
-# Queries meta data for the current EC2 instance.
-#
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::CloudWatchClient - subroutines for interacting with AWS
+
+=head1 SYNOPSIS
+
+ use App::AWS::CloudWatch::Monitor::CloudWatchClient;
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::CloudWatchClient> contains subroutines for interacting with AWS EC2 instance meta data and CloudWatch.
+
+=head1 SUBROUTINES
+
+=over
+
+=item get_meta_data
+
+Queries meta data for the current EC2 instance.
+
+=cut
+
 sub get_meta_data {
     my $resource  = shift;
     my $use_cache = shift;
@@ -114,9 +134,12 @@ sub get_meta_data {
     return $data_value;
 }
 
-#
-# Reads meta-data from the local filesystem.
-#
+=item read_meta_data
+
+Reads meta-data from the local filesystem.
+
+=cut
+
 sub read_meta_data {
     my $resource    = shift;
     my $default_ttl = shift;
@@ -153,9 +176,12 @@ sub read_meta_data {
     return $data_value;
 }
 
-#
-# Writes meta-data to the local filesystem.
-#
+=item write_meta_data
+
+Writes meta-data to the local filesystem.
+
+=cut
+
 sub write_meta_data {
     my $resource   = shift;
     my $data_value = shift;
@@ -181,9 +207,12 @@ sub write_meta_data {
     return;
 }
 
-#
-# Builds up ec2 endpoint URL for this region.
-#
+=item get_ec2_endpoint
+
+Builds up ec2 endpoint URL for this region.
+
+=cut
+
 sub get_ec2_endpoint {
     my $reg      = get_region();
     my $endpoint = "https://ec2.amazonaws.com";
@@ -198,9 +227,12 @@ sub get_ec2_endpoint {
     return $endpoint;
 }
 
-#
-# Obtains Auto Scaling group name by making EC2 API call.
-#
+=item get_auto_scaling_group
+
+Obtains Auto Scaling group name by making EC2 API call.
+
+=cut
+
 sub get_auto_scaling_group {
     if ($as_group_name) {
         return ( 200, $as_group_name );
@@ -268,9 +300,12 @@ sub get_auto_scaling_group {
     return ( $response->code, $response->message );
 }
 
-#
-# Obtains EC2 instance id from meta data.
-#
+=item get_instance_id
+
+Obtains EC2 instance id from meta data.
+
+=cut
+
 sub get_instance_id {
     if ( !$instance_id ) {
         $instance_id = get_meta_data( '/instance-id', USE_CACHE );
@@ -278,9 +313,12 @@ sub get_instance_id {
     return $instance_id;
 }
 
-#
-# Obtains EC2 instance type from meta data.
-#
+=item get_instance_type
+
+Obtains EC2 instance type from meta data.
+
+=cut
+
 sub get_instance_type {
     if ( !$instance_type ) {
         $instance_type = get_meta_data( '/instance-type', USE_CACHE );
@@ -288,9 +326,12 @@ sub get_instance_type {
     return $instance_type;
 }
 
-#
-# Obtains EC2 image id from meta data.
-#
+=item get_image_id
+
+Obtains EC2 image id from meta data.
+
+=cut
+
 sub get_image_id {
     if ( !$image_id ) {
         $image_id = get_meta_data( '/ami-id', USE_CACHE );
@@ -298,9 +339,12 @@ sub get_image_id {
     return $image_id;
 }
 
-#
-# Obtains EC2 avilability zone from meta data.
-#
+=item get_avail_zone
+
+Obtains EC2 avilability zone from meta data.
+
+=cut
+
 sub get_avail_zone {
     if ( !$avail_zone ) {
         $avail_zone = get_meta_data( '/placement/availability-zone', USE_CACHE );
@@ -308,9 +352,12 @@ sub get_avail_zone {
     return $avail_zone;
 }
 
-#
-# Extracts region from avilability zone.
-#
+=item get_region
+
+Extracts region from avilability zone.
+
+=cut
+
 sub get_region {
     if ( !$region ) {
         my $azone = get_avail_zone();
@@ -321,9 +368,12 @@ sub get_region {
     return $region;
 }
 
-#
-# Buids up the endpoint based on the provided region.
-#
+=item get_endpoint
+
+Builds up the endpoint based on the provided region.
+
+=cut
+
 sub get_endpoint {
     my $reg      = get_region();
     my $endpoint = "https://monitoring.amazonaws.com";
@@ -338,9 +388,12 @@ sub get_endpoint {
     return $endpoint;
 }
 
-#
-# Read credentials from the IAM Role Metadata.
-#
+=item prepare_iam_role
+
+Read credentials from the IAM Role Metadata.
+
+=cut
+
 sub prepare_iam_role {
     my $opts     = shift;
     my $response = {};
@@ -425,9 +478,12 @@ sub prepare_iam_role {
     return { "code" => OK };
 }
 
-#
-# Checks if credential set is present. If not, reads credentials from file.
-#
+=item prepare_credentials
+
+Checks if credential set is present. If not, reads credentials from file.
+
+=cut
+
 sub prepare_credentials {
     my $opts                = shift;
     my $verbose             = $opts->{'verbose'};
@@ -495,9 +551,12 @@ sub prepare_credentials {
     return { "code" => OK };
 }
 
-#
-# Retrieves the current UTC time minus the offset (in hours).
-#
+=item get_offset_time
+
+Retrieves the current UTC time minus the offset (in hours).
+
+=cut
+
 sub get_offset_time {
     my $offset = shift;
     my $dt     = DateTime->now();
@@ -505,9 +564,12 @@ sub get_offset_time {
     return $dt->epoch;
 }
 
-#
-# Prints out diagnostic message to a file or standard output.
-#
+=item print_out
+
+Prints out diagnostic message to a file or standard output.
+
+=cut
+
 sub print_out {
     my $text     = shift;
     my $filename = shift;
@@ -525,10 +587,14 @@ sub print_out {
     return;
 }
 
-#
-# Retrieves the interface and type prefixes for the version and action supplied
-# e.g. 2010-08-01 => [GraniteService20100801, com.amazonaws.cloudwatch.v2010_08_01#]
-#
+=item get_interface_version_and_type
+
+Retrieves the interface and type prefixes for the version and action supplied.
+
+ e.g. 2010-08-01 => [GraniteService20100801, com.amazonaws.cloudwatch.v2010_08_01#]
+
+=cut
+
 sub get_interface_version_and_type {
     my $params  = shift;
     my $version = $params->{'Version'};
@@ -543,9 +609,12 @@ sub get_interface_version_and_type {
     return { "code" => OK, "version" => $version_prefix_map{$version}[0], "type" => $version_prefix_map{$version}[1] };
 }
 
-#
-# Creates a key-value pair string to get added to the JSON payload.
-#
+=item add_simple_parameter
+
+Creates a key-value pair string to get added to the JSON payload.
+
+=cut
+
 sub add_simple_parameter {
     my $param_name = shift;
     my $value      = shift;
@@ -562,9 +631,12 @@ sub add_simple_parameter {
     return $json_data;
 }
 
-#
-# Iterates through hash entries and adds them to the JSON payload.
-#
+=item add_hash
+
+Iterates through hash entries and adds them to the JSON payload.
+
+=cut
+
 sub add_hash {
     my $param_name = shift;
     my $hash_ref   = shift;
@@ -584,9 +656,12 @@ sub add_hash {
     return $json_data;
 }
 
-#
-# Iterates through array entries and adds them to the JSON payload.
-#
+=item add_array
+
+Iterates through array entries and adds them to the JSON payload.
+
+=cut
+
 sub add_array {
     my $param_name = shift;
     my $array_ref  = shift;
@@ -606,18 +681,24 @@ sub add_array {
     return $json_data;
 }
 
-#
-# Builds a JSON payload from the request parameters.
-#
+=item construct_payload
+
+Builds a JSON payload from the request parameters.
+
+=cut
+
 sub construct_payload {
     my $params    = shift;
     my $json_data = add_hash( "", $params->{'Input'} );
     return $json_data;
 }
 
-#
-# Prepares SigV4 request headers and JSON payload for the HTTP request.
-#
+=item get_json_payload_and_headers
+
+Prepares SigV4 request headers and JSON payload for the HTTP request.
+
+=cut
+
 sub get_json_payload_and_headers {
     my $params = shift;
     my $opts   = shift;
@@ -633,9 +714,12 @@ sub get_json_payload_and_headers {
     return { "code" => OK, "payload" => $json_data, "headers" => $sigv4->headers };
 }
 
-#
-# Shared call setup used for both AWS/JSON and AWS/Query HTTP requests.
-#
+=item call_setup
+
+Shared call setup used for both AWS/JSON and AWS/Query HTTP requests.
+
+=cut
+
 sub call_setup {
     my $params = shift;
     my $opts   = shift;
@@ -659,10 +743,14 @@ sub call_setup {
     return prepare_credentials($opts);
 }
 
-#
-# Helper method used by both call_json and call_query.
-# Configures and sends the HTTP request and passes result back to caller.
-#
+=item call
+
+Helper method used by both call_json and call_query.
+
+Configures and sends the HTTP request and passes result back to caller.
+
+=cut
+
 sub call {
     my $payload         = shift;
     my $headers         = shift;
@@ -754,10 +842,14 @@ sub call {
     return $response;
 }
 
-#
-# Makes a remote invocation to the CloudWatch service using the AWS/Query format.
-# Returns request ID, if successful, or error message if unsuccessful.
-#
+=item call_query
+
+Makes a remote invocation to the CloudWatch service using the AWS/Query format.
+
+Returns request ID, if successful, or error message if unsuccessful.
+
+=cut
+
 sub call_query {
     my $operation = shift;
     my $params    = shift;
@@ -786,10 +878,16 @@ sub call_query {
     return call( $payload, $headers, $opts, $failure_pattern );
 }
 
-#
-# Makes a remote invocation to the CloudWatch service using the AWS/JSON format.
-# Returns the full response if successful, or error message if unsuccessful.
-#
+=item call_json
+
+Makes a remote invocation to the CloudWatch service using the AWS/JSON format.
+
+Returns the full response if successful, or error message if unsuccessful.
+
+=back
+
+=cut
+
 sub call_json {
     my $operation = shift;
     my $params    = shift;
commit c9c344374ce338aefa532f534558d3f0351f59ac
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 15:52:03 2021 -0500

    Add InstanceId to metric Dimensions

diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 6c73b8d..54b7b7b 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use App::AWS::CloudWatch::Monitor::Config;
+use App::AWS::CloudWatch::Monitor::CloudWatchClient;
 use Try::Tiny;
 use Module::Loader;
 
@@ -29,7 +30,8 @@ sub run {
     my $self = shift;
     my $opt  = shift;
 
-    my $loader = Module::Loader->new;
+    my $instance_id = App::AWS::CloudWatch::Monitor::CloudWatchClient::get_instance_id();
+    my $loader      = Module::Loader->new;
 
     my @metrics;
     foreach my $module ( @{ $opt->{check} } ) {
@@ -44,6 +46,9 @@ sub run {
 
         my $plugin = $class->new();
         my $metric = $plugin->check();
+
+        push @{ $metric->{Dimensions} }, { Name => 'InstanceId', Value => $instance_id };
+        push @metrics, $metric;
     }
 
     return;
commit 99484790c64bafdcca55f99c33fe35ca2115d46e
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 15:48:54 2021 -0500

    Update reused variable name
    
    This commit updates the reused variable name, region, for
    CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 14ca932..8892e68 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -20,6 +20,7 @@
 # Update subroutine called with "&" sigil
 # Update double sigil dereference
 # Update indirect object call syntax
+# Update reused variable name
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -184,13 +185,13 @@ sub write_meta_data {
 # Builds up ec2 endpoint URL for this region.
 #
 sub get_ec2_endpoint {
-    my $region   = get_region();
+    my $reg      = get_region();
     my $endpoint = "https://ec2.amazonaws.com";
 
-    if ($region) {
-        $endpoint = "https://ec2.$region.amazonaws.com";
-        if ( exists $region_suffix_map{$region} ) {
-            $endpoint .= $region_suffix_map{$region};
+    if ($reg) {
+        $endpoint = "https://ec2.$reg.amazonaws.com";
+        if ( exists $region_suffix_map{$reg} ) {
+            $endpoint .= $region_suffix_map{$reg};
         }
     }
 
@@ -324,13 +325,13 @@ sub get_region {
 # Buids up the endpoint based on the provided region.
 #
 sub get_endpoint {
-    my $region   = get_region();
+    my $reg      = get_region();
     my $endpoint = "https://monitoring.amazonaws.com";
 
-    if ($region) {
-        $endpoint = "https://monitoring.$region.amazonaws.com";
-        if ( exists $region_suffix_map{$region} ) {
-            $endpoint .= $region_suffix_map{$region};
+    if ($reg) {
+        $endpoint = "https://monitoring.$reg.amazonaws.com";
+        if ( exists $region_suffix_map{$reg} ) {
+            $endpoint .= $region_suffix_map{$reg};
         }
     }
 
commit d1183c6e5c90bdfbf62943e76796efa1bd741b7a
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:51:24 2021 -0500

    Update indirect object call syntax
    
    This commit updates indirect object call syntax in CloudWatchClient

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 57f440d..14ca932 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -19,6 +19,7 @@
 # Avoid backtick operator in void context
 # Update subroutine called with "&" sigil
 # Update double sigil dereference
+# Update indirect object call syntax
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -667,11 +668,11 @@ sub call {
     my $opts            = shift;
     my $failure_pattern = shift;
 
-    my $user_agent = new LWP::UserAgent( agent => $opts->{'user-agent}'} );
+    my $user_agent = LWP::UserAgent->new( agent => $opts->{'user-agent}'} );
     $user_agent->timeout($http_request_timeout);
 
     my $http_headers = HTTP::Headers->new( %{$headers} );
-    my $request      = new HTTP::Request $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload;
+    my $request      = HTTP::Request->new( $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload );
 
     if ( defined( $opts->{'enable-compression'} ) && length($payload) > $compress_threshold_bytes ) {
         $request->encode('gzip');
commit 421ba8b8f300af3f0a9410edaa0cce028bbfc8b6
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:43:22 2021 -0500

    Update double sigil dereference
    
    This commit updates double sigil dereferences in CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index bea8b7d..57f440d 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -18,6 +18,7 @@
 # Add explicit returns from subroutines
 # Avoid backtick operator in void context
 # Update subroutine called with "&" sigil
+# Update double sigil dereference
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -575,7 +576,7 @@ sub add_hash {
             $json_data .= add_simple_parameter( $key, $value );
         }
     }
-    chop($json_data) unless ( ( keys %$hash_ref ) == 0 );
+    chop($json_data) unless ( ( keys %{$hash_ref} ) == 0 );
     $json_data .= "}";
 
     return $json_data;
@@ -597,7 +598,7 @@ sub add_array {
             $json_data .= qq("$array_val",);
         }
     }
-    chop($json_data) unless ( scalar @$array_ref == 0 );
+    chop($json_data) unless ( scalar @{$array_ref} == 0 );
     $json_data .= "]";
 
     return $json_data;
@@ -669,7 +670,7 @@ sub call {
     my $user_agent = new LWP::UserAgent( agent => $opts->{'user-agent}'} );
     $user_agent->timeout($http_request_timeout);
 
-    my $http_headers = HTTP::Headers->new(%$headers);
+    my $http_headers = HTTP::Headers->new( %{$headers} );
     my $request      = new HTTP::Request $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload;
 
     if ( defined( $opts->{'enable-compression'} ) && length($payload) > $compress_threshold_bytes ) {
commit c752eb0b4bbb6324bfa810e97ae2e49ad5569b66
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:38:12 2021 -0500

    Update subroutine called with "&" sigil
    
    This commit updates the dirname call to not use the "&" sigil.
    Additionally, update all dirname calls to the full package path.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 99bebc8..bea8b7d 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -17,6 +17,7 @@
 # Update open usage to modern Perl
 # Add explicit returns from subroutines
 # Avoid backtick operator in void context
+# Update subroutine called with "&" sigil
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -164,7 +165,7 @@ sub write_meta_data {
 
         if ($location) {
             my $filename  = $location . $resource;
-            my $directory = dirname($filename);
+            my $directory = File::Basename::dirname($filename);
             system( qw{ /bin/mkdir -p }, $directory ) unless -d $directory;
 
             open( my $file_fh, '>', $filename )
@@ -450,7 +451,7 @@ sub prepare_credentials {
     }
 
     if ( !$aws_credential_file ) {
-        my $conf_file = &File::Basename::dirname($0) . '/awscreds.conf';
+        my $conf_file = File::Basename::dirname($0) . '/awscreds.conf';
         if ( -e $conf_file ) {
             $aws_credential_file = $conf_file;
         }
commit 6f54b093c8a228b135efb0add659009a025dacbc
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:26:54 2021 -0500

    Avoid backtick operator in void context
    
    This commit replaces the usage of backtick in void context with
    system in CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 2dd5c42..99bebc8 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -16,6 +16,7 @@
 # Update package namespace and VERSION
 # Update open usage to modern Perl
 # Add explicit returns from subroutines
+# Avoid backtick operator in void context
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -164,7 +165,7 @@ sub write_meta_data {
         if ($location) {
             my $filename  = $location . $resource;
             my $directory = dirname($filename);
-            `/bin/mkdir -p $directory` unless -d $directory;
+            system( qw{ /bin/mkdir -p }, $directory ) unless -d $directory;
 
             open( my $file_fh, '>', $filename )
                 or warn "open: unable to write meta data from filesystem: $!\n";
commit 339de2c5a554778805e70e3ea7fe8e634b90fb0e
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:12:27 2021 -0500

    Add explicit returns from subroutines
    
    This commit adds explicit returns from subroutines to
    CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 3dbd275..2dd5c42 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -15,6 +15,7 @@
 # Update formatting
 # Update package namespace and VERSION
 # Update open usage to modern Perl
+# Add explicit returns from subroutines
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -171,6 +172,8 @@ sub write_meta_data {
             close $file_fh;
         }
     }
+
+    return;
 }
 
 #
@@ -513,6 +516,8 @@ sub print_out {
     else {
         print "$text\n";
     }
+
+    return;
 }
 
 #
commit c155a96bdf956b2c4712197d7681fa2cbf470d23
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 14:09:16 2021 -0500

    Update open usage to modern Perl
    
    This commit updates open usage to modern Perl for CloudWatchClient.
    
    - Update opens to 3 arg open
    - Update bareword filehandles
    - Add warn message for open failures
    - Move print_out before open to ensure correct error strings

diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index 0bba5f4..3dbd275 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -14,6 +14,7 @@
 # this package has been updated from the original version to:
 # Update formatting
 # Update package namespace and VERSION
+# Update open usage to modern Perl
 
 package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
@@ -132,11 +133,12 @@ sub read_meta_data {
             my $updated  = ( stat($filename) )[9];
             my $file_age = time() - $updated;
             if ( $file_age < $meta_data_ttl ) {
-                open MDATA, "$filename";
-                while ( my $line = <MDATA> ) {
+                open( my $file_fh, '<', $filename )
+                    or warn "open: unable to read meta data from filesystem: $!\n";
+                while ( my $line = <$file_fh> ) {
                     $data_value .= $line;
                 }
-                close MDATA;
+                close $file_fh;
                 chomp $data_value;
             }
         }
@@ -163,9 +165,10 @@ sub write_meta_data {
             my $directory = dirname($filename);
             `/bin/mkdir -p $directory` unless -d $directory;
 
-            open MDATA, ">$filename";
-            print MDATA $data_value;
-            close MDATA;
+            open( my $file_fh, '>', $filename )
+                or warn "open: unable to write meta data from filesystem: $!\n";
+            print $file_fh $data_value;
+            close $file_fh;
         }
     }
 }
@@ -450,11 +453,12 @@ sub prepare_credentials {
     }
 
     if ($aws_credential_file) {
-        my $file = $aws_credential_file;
-        open( FILE, '<:utf8', $file ) or return { "code" => ERROR, "error" => "Failed to open AWS credentials file <$file>" };
         print_out( "Using AWS credentials file <$aws_credential_file>", $outfile ) if $verbose;
+        my $file = $aws_credential_file;
+        open( my $file_fh, '<:encoding(UTF-8)', $file )
+            or return { "code" => ERROR, "error" => "Failed to open AWS credentials file <$file>" };
 
-        while ( my $line = <FILE> ) {
+        while ( my $line = <$file_fh> ) {
             $line =~ /^$/   and next;    # skip empty lines
             $line =~ /^#.*/ and next;    # skip commented lines
             $line =~ /^\s*(.*?)=(.*?)\s*$/
@@ -467,7 +471,7 @@ sub prepare_credentials {
                 $opts->{'aws-secret-key'} = $value;
             }
         }
-        close(FILE);
+        close($file_fh);
     }
 
     $aws_access_key_id = $opts->{'aws-access-key-id'};
@@ -501,9 +505,10 @@ sub print_out {
     my $filename = shift;
 
     if ($filename) {
-        open OUT_STREAM, ">>$filename";
-        print OUT_STREAM "$text\n";
-        close OUT_STREAM;
+        open( my $file_fh, '>>', $filename )
+            or warn "open: unable to write to $filename: $!\n";
+        print $file_fh "$text\n";
+        close $file_fh;
     }
     else {
         print "$text\n";
commit 6e5ef77cee44321ac34582980cf84da7269d15fc
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 12:55:05 2021 -0500

    Update package namespace and VERSION
    
    This commit updates the package namespace and adds a package
    version for AwsSignatureV4 and CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
index 9726925..18b1e55 100644
--- a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
@@ -13,8 +13,9 @@
 
 # this package has been updated from the original version to:
 # Update formatting
+# Update package namespace and VERSION
 
-package AwsSignatureV4;
+package App::AWS::CloudWatch::Monitor::AwsSignatureV4;
 
 use strict;
 use warnings;
@@ -28,6 +29,8 @@ use Digest::SHA qw(sha256_hex hmac_sha256 hmac_sha256_hex);
 # For using PurePerl implementation of SHA functions
 # use Digest::SHA::PurePerl qw(sha256_hex hmac_sha256 hmac_sha256_hex);
 
+our $VERSION = '0.01';
+
 # RFC3986 safe/unsafe characters
 our $SAFE_CHARACTERS   = 'A-Za-z0-9\-\._~';
 our $UNSAFE_CHARACTERS = '^' . $SAFE_CHARACTERS;
diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index d84f7fc..0bba5f4 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -13,8 +13,9 @@
 
 # this package has been updated from the original version to:
 # Update formatting
+# Update package namespace and VERSION
 
-package CloudWatchClient;
+package App::AWS::CloudWatch::Monitor::CloudWatchClient;
 
 use strict;
 use warnings;
@@ -32,6 +33,8 @@ use LWP 6;
 use LWP::Simple qw($ua get);
 $ua->timeout(2);    # timeout for meta-data calls
 
+our $VERSION = '0.01';
+
 our %version_prefix_map = ( '2010-08-01' => [ 'GraniteServiceVersion20100801', 'com.amazonaws.cloudwatch.v2010_08_01#' ] );
 
 our %supported_actions = (
commit 7ee3a061232da9e06013393a001b371ea83b4b25
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 12:52:02 2021 -0500

    Update formatting
    
    This commit updates formatting for AwsSignatureV4 and
    CloudWatchClient.

diff --git a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
index 8c5c8a0..9726925 100644
--- a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
@@ -1,16 +1,19 @@
 # Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 #
-# Licensed under the Apache License, Version 2.0 (the "License"). You may not 
-# use this file except in compliance with the License. A copy of the License 
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not
+# use this file except in compliance with the License. A copy of the License
 # is located at
 #
 #        http://aws.amazon.com/apache2.0/
 #
-# or in the "LICENSE" file accompanying this file. This file is distributed 
+# or in the "LICENSE" file accompanying this file. This file is distributed
 # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 # express or implied. See the License for the specific language governing
 # permissions and limitations under the License.
 
+# this package has been updated from the original version to:
+# Update formatting
+
 package AwsSignatureV4;
 
 use strict;
@@ -26,8 +29,8 @@ use Digest::SHA qw(sha256_hex hmac_sha256 hmac_sha256_hex);
 # use Digest::SHA::PurePerl qw(sha256_hex hmac_sha256 hmac_sha256_hex);
 
 # RFC3986 safe/unsafe characters
-our $SAFE_CHARACTERS = 'A-Za-z0-9\-\._~';
-our $UNSAFE_CHARACTERS = '^'.$SAFE_CHARACTERS;
+our $SAFE_CHARACTERS   = 'A-Za-z0-9\-\._~';
+our $UNSAFE_CHARACTERS = '^' . $SAFE_CHARACTERS;
 
 # Name of the signature encoding algorithm.
 our $ALGORITHM_NAME = 'AWS4-HMAC-SHA256';
@@ -52,13 +55,12 @@ our $ALGORITHM_NAME = 'AWS4-HMAC-SHA256';
 #     aws-secret-key => Secret Key of AWS credentials
 #     aws-security-token => Security Token in case of STS call
 #
-sub new
-{
-  my $class = shift;
-  my $self = {opts => shift};
-  $self->{'payload'} = '';
-  bless $self, $class;
-  return $self;
+sub new {
+    my $class = shift;
+    my $self  = { opts => shift };
+    $self->{'payload'} = '';
+    bless $self, $class;
+    return $self;
 }
 
 #
@@ -75,13 +77,12 @@ sub new
 #
 #   $opts - see defition of 'new' constructor
 #
-sub new_aws_query
-{
-  my $class = shift;
-  my $self = {params => shift, opts => shift};
-  $self->{'content-type'} = 'application/x-www-form-urlencoded; charset=utf-8';
-  $self->{'payload'} = '';
-  return bless $self, $class;
+sub new_aws_query {
+    my $class = shift;
+    my $self  = { params => shift, opts => shift };
+    $self->{'content-type'} = 'application/x-www-form-urlencoded; charset=utf-8';
+    $self->{'payload'}      = '';
+    return bless $self, $class;
 }
 
 #
@@ -93,13 +94,12 @@ sub new_aws_query
 #   $payload - input data in RPC/JSON format
 #   $opts - see defition of 'new' constructor
 #
-sub new_rpc_json
-{
-  my $class = shift;
-  my $self = {payload => shift, opts => shift};
-  $self->{'content-type'} = 'application/json; charset=utf-8';
-  bless $self, $class;
-  return $self;
+sub new_rpc_json {
+    my $class = shift;
+    my $self  = { payload => shift, opts => shift };
+    $self->{'content-type'} = 'application/json; charset=utf-8';
+    bless $self, $class;
+    return $self;
 }
 
 #
@@ -112,25 +112,24 @@ sub new_rpc_json
 #   $payload - input data in AWS/JSON format
 #   $opts - see defition of 'new' constructor
 #
-sub new_aws_json
-{
-  my $class = shift;
-  my $operation = shift;
-  my $payload = shift;
-  my $opts = shift;
-  my $self = {payload => $payload, opts => $opts};
-  
-  $self->{'content-type'} = 'application/x-amz-json-1.0';
-  
-  if (not exists $opts->{'extra-headers'}) {
-    $opts->{'extra-headers'} = {};
-  }
-  
-  my $extra_headers = $opts->{'extra-headers'};
-  $extra_headers->{'X-Amz-Target'} = $operation;
-  
-  bless $self, $class;
-  return $self;
+sub new_aws_json {
+    my $class     = shift;
+    my $operation = shift;
+    my $payload   = shift;
+    my $opts      = shift;
+    my $self      = { payload => $payload, opts => $opts };
+
+    $self->{'content-type'} = 'application/x-amz-json-1.0';
+
+    if ( not exists $opts->{'extra-headers'} ) {
+        $opts->{'extra-headers'} = {};
+    }
+
+    my $extra_headers = $opts->{'extra-headers'};
+    $extra_headers->{'X-Amz-Target'} = $operation;
+
+    bless $self, $class;
+    return $self;
 }
 
 #
@@ -142,94 +141,89 @@ sub new_aws_json
 #   $ctype - content-type of the body
 #   $payload - request body data
 #
-sub sign_http_request
-{
-  my $self = shift;
-  my $method = shift;
-  my $ctype = shift;
-  my $payload = shift;
-
-  $self->{'http-method'} = $method if $method;
-  $self->{'content-type'} = $ctype if $ctype;
-  $self->{'payload'} = $payload if $payload;
-  
-  my $opts = $self->{opts};
-  $opts->{'create-authz-header'} = 1;
-  $self->{'request-url'} = $opts->{'url'};
-  
-  if (!$self->sign()) {
-    return 0;
-  }
-  
-  $self->create_authz_header();
-  return 1;
+sub sign_http_request {
+    my $self    = shift;
+    my $method  = shift;
+    my $ctype   = shift;
+    my $payload = shift;
+
+    $self->{'http-method'}  = $method  if $method;
+    $self->{'content-type'} = $ctype   if $ctype;
+    $self->{'payload'}      = $payload if $payload;
+
+    my $opts = $self->{opts};
+    $opts->{'create-authz-header'} = 1;
+    $self->{'request-url'}         = $opts->{'url'};
+
+    if ( !$self->sign() ) {
+        return 0;
+    }
+
+    $self->create_authz_header();
+    return 1;
 }
 
 #
 # Signs request for HTTP POST.
 #
-sub sign_http_post
-{
-  my $self = shift;
-  return $self->sign_http_request('POST');
+sub sign_http_post {
+    my $self = shift;
+    return $self->sign_http_request('POST');
 }
 
 #
 # Signs request for HTTP PUT.
 #
-sub sign_http_put
-{
-  my $self = shift;
-  return $self->sign_http_request('PUT');
+sub sign_http_put {
+    my $self = shift;
+    return $self->sign_http_request('PUT');
 }
 
 #
 # Signs request for HTTP GET.
 #
-sub sign_http_get
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  $self->{'http-method'} = 'GET';
-  
-  if (!$self->sign()) {
-    return 0;
-  }
-
-  my $postfix = "";
-  my $query_string = $self->{'query-string'};
-
-  if ($query_string) {
-    $postfix = '?'.$query_string;
-  }
-  $self->{'request-url'} = $opts->{'url'}.$postfix;
-
-  $postfix .= ($query_string ? '&' : '?');
-  $self->{'signed-url'} = $opts->{'url'}.$postfix.'X-Amz-Signature='.$self->{'signature'};
-  
-  if ($opts->{'create-authz-header'}) {
-    $self->create_authz_header();
-  }
-  
-  return 1;
+sub sign_http_get {
+    my $self = shift;
+    my $opts = $self->{opts};
+    $self->{'http-method'} = 'GET';
+
+    if ( !$self->sign() ) {
+        return 0;
+    }
+
+    my $postfix      = "";
+    my $query_string = $self->{'query-string'};
+
+    if ($query_string) {
+        $postfix = '?' . $query_string;
+    }
+    $self->{'request-url'} = $opts->{'url'} . $postfix;
+
+    $postfix .= ( $query_string ? '&' : '?' );
+    $self->{'signed-url'} = $opts->{'url'} . $postfix . 'X-Amz-Signature=' . $self->{'signature'};
+
+    if ( $opts->{'create-authz-header'} ) {
+        $self->create_authz_header();
+    }
+
+    return 1;
 }
 
 #
 # Prepares and signs the request data.
 #
-sub sign
-{
-  my $self = shift;
-  
-  if (!$self->initialize()) {
-    return 0;
-  }
-  
-  $self->create_basic_headers();
-  $self->create_query_string();  
-  $self->create_signature();
-  
-  return 1;
+sub sign {
+    my $self = shift;
+
+    if ( !$self->initialize() ) {
+        return 0;
+    }
+
+    $self->create_basic_headers();
+    $self->create_query_string();
+    $self->create_signature();
+
+    return 1;
 }
 
 #
@@ -237,30 +231,27 @@ sub sign
 # In case of HTTP POST and PUT methods it will also include the
 # Authorization header that carries the signature itself.
 #
-sub headers
-{
-  my $self = shift;
-  return $self->{'headers'};
+sub headers {
+    my $self = shift;
+    return $self->{'headers'};
 }
 
 #
 # In case of AWS/Query request and HTTP POST or PUT method, returns
 # url-encoded query string to be used as a body of HTTP POST request.
 #
-sub payload
-{
-  my $self = shift;
-  return $self->{'payload'};
+sub payload {
+    my $self = shift;
+    return $self->{'payload'};
 }
 
 #
 # Returns complete signed URL to be used in HTTP GET request 'as is'.
 # You can place this value into Web browser location bar and make a call.
 #
-sub signed_url
-{
-  my $self = shift;
-  return $self->{'signed-url'};
+sub signed_url {
+    my $self = shift;
+    return $self->{'signed-url'};
 }
 
 #
@@ -270,75 +261,68 @@ sub signed_url
 # You can not use this URL with the Web browser since it does
 # not contain the signature.
 #
-sub request_url
-{
-  my $self = shift;
-  return $self->{'request-url'};
+sub request_url {
+    my $self = shift;
+    return $self->{'request-url'};
 }
 
 #
 # Returns an error message if any.
 #
-sub error
-{
-  my $self = shift;
-  return $self->{'error'};
+sub error {
+    my $self = shift;
+    return $self->{'error'};
 }
 
 #
 # Returns both timestamp and daystamp in the format required for SigV4.
 #
-sub get_timestamp_daystamp
-{
-  my $time = shift;
-  my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime(time);
-  my $timestamp = sprintf("%04d%02d%02dT%02d%02d%02dZ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
-  my $daystamp = substr($timestamp, 0, 8);
-  return ($timestamp, $daystamp);
+sub get_timestamp_daystamp {
+    my $time = shift;
+    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime(time);
+    my $timestamp = sprintf( "%04d%02d%02dT%02d%02d%02dZ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec );
+    my $daystamp  = substr( $timestamp, 0, 8 );
+    return ( $timestamp, $daystamp );
 }
 
 #
 # Applies regex to get FQDN from URL.
 #
-sub extract_fqdn_from_url
-{
-  my $fqdn = shift;
-  $fqdn =~ s!^https?://([^/:?]*).*$!$1!;
-  return $fqdn;
+sub extract_fqdn_from_url {
+    my $fqdn = shift;
+    $fqdn =~ s!^https?://([^/:?]*).*$!$1!;
+    return $fqdn;
 }
 
 #
 # Applies regex to get service name from the FQDN.
 #
-sub extract_service_from_fqdn
-{
-  my $fqdn = shift;
-  my $service = $fqdn;
-  $service =~ s!^([^\.]+)\..*$!$1!;
-  return $service;
+sub extract_service_from_fqdn {
+    my $fqdn    = shift;
+    my $service = $fqdn;
+    $service =~ s!^([^\.]+)\..*$!$1!;
+    return $service;
 }
 
 #
 # Applies regex to get region from the FQDN.
 #
-sub extract_region_from_fqdn
-{
-  my $fqdn = shift;
-  my @parts = split(/\./, $fqdn);
-  return $parts[1] if $parts[1] =~ /\w{2}-\w+-\d+/;
-  return 'us-east-1';
+sub extract_region_from_fqdn {
+    my $fqdn  = shift;
+    my @parts = split( /\./, $fqdn );
+    return $parts[1] if $parts[1] =~ /\w{2}-\w+-\d+/;
+    return 'us-east-1';
 }
 
 #
 # Applies regex to get the path part of the URL.
 #
-sub extract_path_from_url
-{
-  my $url = shift;
-  my $path = $url;
-  $path =~ s!^https?://[^/]+([^\?]*).*$!$1!;
-  $path = '/' if !$path;
-  return $path;
+sub extract_path_from_url {
+    my $url  = shift;
+    my $path = $url;
+    $path =~ s!^https?://[^/]+([^\?]*).*$!$1!;
+    $path = '/' if !$path;
+    return $path;
 }
 
 #
@@ -352,164 +336,163 @@ sub extract_path_from_url
 # SignedHeaders =
 #   LOWERCASE(HeaderName0) + ';' + LOWERCASE(HeaderName1) + ... + LOWERCASE(HeaderNameN)
 #
-sub create_basic_headers
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  
-  my %headers = ();
-  $headers{'Host'} = $self->{'fqdn'};
-  
-  my $extra_date_specified = 0;
-  my $extra_headers = $opts->{'extra-headers'};
-  
-  if ($extra_headers)
-  {
-    foreach my $extra_name ( keys %$extra_headers ) {
-      $headers{$extra_name} = $extra_headers->{$extra_name};
-      if (lc($extra_name) eq 'date' || lc($extra_name) eq 'x-amz-date') {
-        $extra_date_specified = 1;
-      }
+sub create_basic_headers {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my %headers = ();
+    $headers{'Host'} = $self->{'fqdn'};
+
+    my $extra_date_specified = 0;
+    my $extra_headers        = $opts->{'extra-headers'};
+
+    if ($extra_headers) {
+        foreach my $extra_name ( keys %{$extra_headers} ) {
+            $headers{$extra_name} = $extra_headers->{$extra_name};
+            if ( lc($extra_name) eq 'date' || lc($extra_name) eq 'x-amz-date' ) {
+                $extra_date_specified = 1;
+            }
+        }
     }
-  }
-  
-  if ($opts->{'aws-security-token'}) {
-    $headers{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
-  }
-  
-  if (!$extra_date_specified && $opts->{'create-authz-header'}) {
-    $headers{'X-Amz-Date'} = $self->{'timestamp'};
-  }
-  
-  if ($self->{'http-method'} ne 'GET' && $self->{'content-type'}) {
-    $headers{'Content-Type'} = $self->{'content-type'};
-  }
-  
-  my %lc_headers = ();
-  my $signed_headers = '';
-  my $canonical_headers = '';
-  
-  foreach my $header_name ( keys %headers ) {
-    my $header_value = $headers{$header_name};
-    # trim leading and trailing whitespaces, see
-    # http://perldoc.perl.org/perlfaq4.html#How-do-I-strip-blank-space-from-the-beginning%2fend-of-a-string%3f
-    $header_value =~ s/^\s+//;
-    $header_value =~ s/\s+$//;
-    # now convert sequential spaces to a single space, but do not remove
-    # extra spaces from any values that are inside quotation marks
-    my @parts = split /("[^"]*")/, $header_value;
-    foreach my $part (@parts) {
-      unless ($part =~ /^"/) {
-          $part =~ s/[ ]+/ /g;
-      }
+
+    if ( $opts->{'aws-security-token'} ) {
+        $headers{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
+    }
+
+    if ( !$extra_date_specified && $opts->{'create-authz-header'} ) {
+        $headers{'X-Amz-Date'} = $self->{'timestamp'};
     }
-    $header_value = join '', @parts;
-    $lc_headers{lc($header_name)} = $header_value;
-  }
-  
-  for my $lc_header (sort keys %lc_headers)
-  {
-    $signed_headers .= ';' if length($signed_headers) > 0;
-    $signed_headers .= $lc_header;
-    $canonical_headers .= $lc_header . ':' . $lc_headers{$lc_header} . "\n";
-  }
-  
-  $self->{'signed-headers'} = $signed_headers;
-  $self->{'canonical-headers'} = $canonical_headers;
-  $self->{'headers'} = \%headers;
-  
-  return 1;
+
+    if ( $self->{'http-method'} ne 'GET' && $self->{'content-type'} ) {
+        $headers{'Content-Type'} = $self->{'content-type'};
+    }
+
+    my %lc_headers        = ();
+    my $signed_headers    = '';
+    my $canonical_headers = '';
+
+    foreach my $header_name ( keys %headers ) {
+        my $header_value = $headers{$header_name};
+
+        # trim leading and trailing whitespaces, see
+        # http://perldoc.perl.org/perlfaq4.html#How-do-I-strip-blank-space-from-the-beginning%2fend-of-a-string%3f
+        $header_value =~ s/^\s+//;
+        $header_value =~ s/\s+$//;
+
+        # now convert sequential spaces to a single space, but do not remove
+        # extra spaces from any values that are inside quotation marks
+        my @parts = split /("[^"]*")/, $header_value;
+        foreach my $part (@parts) {
+            unless ( $part =~ /^"/ ) {
+                $part =~ s/[ ]+/ /g;
+            }
+        }
+        $header_value = join '', @parts;
+        $lc_headers{ lc($header_name) } = $header_value;
+    }
+
+    for my $lc_header ( sort keys %lc_headers ) {
+        $signed_headers    .= ';' if length($signed_headers) > 0;
+        $signed_headers    .= $lc_header;
+        $canonical_headers .= $lc_header . ':' . $lc_headers{$lc_header} . "\n";
+    }
+
+    $self->{'signed-headers'}    = $signed_headers;
+    $self->{'canonical-headers'} = $canonical_headers;
+    $self->{'headers'}           = \%headers;
+
+    return 1;
 }
 
 #
 # Validates input and populates essential pre-requisites.
 #
-sub initialize
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  
-  my $url = $opts->{'url'};
-  if (!$url) {
-    $self->{'error'} = 'Endpoint URL is not specified.';
-    return 0;
-  }  
-  if (index($url, '?') != -1) {
-    $self->{'error'} = 'Endpoint URL cannot contain query string.';
-    return 0;
-  }
-  
-  my $akid = $opts->{'aws-access-key-id'};
-  if (!$akid) {
-    $self->{'error'} = 'AWS Access Key Id is not specified.';
-    return 0;
-  }  
-  if (!$opts->{'aws-secret-key'}) {
-    $self->{'error'} = 'AWS Secret Key is not specified.';
-    return 0;
-  }
-  
-  # obtain FQDN from the endpoint url
-  my $fqdn = extract_fqdn_from_url($url);
-  if (!$fqdn) {
-    $self->{'error'} = 'Failed to extract FQDN from endpoint URL.';
-    return 0;
-  }
-  $self->{'fqdn'} = $fqdn;
-
-  # use pre-defined region if specified, otherwise grab it from url
-  my $region = $opts->{'aws-region'};
-  if (!$region) {
-    # if region is not part of url, the default region is returned 
-    $region = extract_region_from_fqdn($fqdn);
-  }
-  $self->{'region'} = $region;
-  
-  # use pre-defined service if specified, otherwise grab it from url
-  # this is specifically important when url does not include service name, e.g. ses/mail
-  my $service = $opts->{'aws-service'};
-  if (!$service) {
-    $service = extract_service_from_fqdn($fqdn);
-    if (!$service) {
-      $self->{'error'} = 'Failed to extract service name from endpoint URL.';
-      return 0;
+sub initialize {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my $url = $opts->{'url'};
+    if ( !$url ) {
+        $self->{'error'} = 'Endpoint URL is not specified.';
+        return 0;
+    }
+    if ( index( $url, '?' ) != -1 ) {
+        $self->{'error'} = 'Endpoint URL cannot contain query string.';
+        return 0;
+    }
+
+    my $akid = $opts->{'aws-access-key-id'};
+    if ( !$akid ) {
+        $self->{'error'} = 'AWS Access Key Id is not specified.';
+        return 0;
+    }
+    if ( !$opts->{'aws-secret-key'} ) {
+        $self->{'error'} = 'AWS Secret Key is not specified.';
+        return 0;
     }
-  }
-  $self->{'service'} = $service;
-
-  # obtain uri path part from the endpoint url
-  my $path = extract_path_from_url($url);
-  if (index($path, '.') != -1 || index($path, '//') != -1) {
-    $self->{'error'} = 'Endpoint URL path must be normalized.';
-    return 0;
-  }
-  $self->{'http-path'} = $path;
-  
-  # initialize time of the signature
-  
-  my ($timestamp, $daystamp);
-  
-  if ($opts->{'timestamp'}) {
-    $timestamp = $opts->{'timestamp'};
-    $daystamp = substr($timestamp, 0, 8);
-  }
-  else {
-    my $time = time();
-    $self->{'time'} = $time;
-    ($timestamp, $daystamp) = get_timestamp_daystamp($time);
-  }
-  $self->{'timestamp'} = $timestamp;
-  $self->{'daystamp'} = $daystamp;
-  
-  # initialize scope & credential
-  
-  my $scope = "$daystamp/$region/$service/aws4_request";
-  $self->{'scope'} = $scope;
-  
-  my $credential = "$akid/$scope";
-  $self->{'credential'} = $credential;
-  
-  return 1;
+
+    # obtain FQDN from the endpoint url
+    my $fqdn = extract_fqdn_from_url($url);
+    if ( !$fqdn ) {
+        $self->{'error'} = 'Failed to extract FQDN from endpoint URL.';
+        return 0;
+    }
+    $self->{'fqdn'} = $fqdn;
+
+    # use pre-defined region if specified, otherwise grab it from url
+    my $region = $opts->{'aws-region'};
+    if ( !$region ) {
+
+        # if region is not part of url, the default region is returned
+        $region = extract_region_from_fqdn($fqdn);
+    }
+    $self->{'region'} = $region;
+
+    # use pre-defined service if specified, otherwise grab it from url
+    # this is specifically important when url does not include service name, e.g. ses/mail
+    my $service = $opts->{'aws-service'};
+    if ( !$service ) {
+        $service = extract_service_from_fqdn($fqdn);
+        if ( !$service ) {
+            $self->{'error'} = 'Failed to extract service name from endpoint URL.';
+            return 0;
+        }
+    }
+    $self->{'service'} = $service;
+
+    # obtain uri path part from the endpoint url
+    my $path = extract_path_from_url($url);
+    if ( index( $path, '.' ) != -1 || index( $path, '//' ) != -1 ) {
+        $self->{'error'} = 'Endpoint URL path must be normalized.';
+        return 0;
+    }
+    $self->{'http-path'} = $path;
+
+    # initialize time of the signature
+
+    my ( $timestamp, $daystamp );
+
+    if ( $opts->{'timestamp'} ) {
+        $timestamp = $opts->{'timestamp'};
+        $daystamp  = substr( $timestamp, 0, 8 );
+    }
+    else {
+        my $time = time();
+        $self->{'time'} = $time;
+        ( $timestamp, $daystamp ) = get_timestamp_daystamp($time);
+    }
+    $self->{'timestamp'} = $timestamp;
+    $self->{'daystamp'}  = $daystamp;
+
+    # initialize scope & credential
+
+    my $scope = "$daystamp/$region/$service/aws4_request";
+    $self->{'scope'} = $scope;
+
+    my $credential = "$akid/$scope";
+    $self->{'credential'} = $credential;
+
+    return 1;
 }
 
 #
@@ -518,67 +501,59 @@ sub initialize
 # Note that SigV4 is payload-agnostic when it comes to POST request body so there is no
 # need to sort arguments in the AWS Query string for the POST method.
 #
-sub create_query_string
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  my $params = $self->{params};
-  
-  if (!$params) {
-    $self->{'query-string'} = '';
-    return 1;
-  }
-  
-  my @args = ();
-  my @keys = ();
-
-  my $http_method = $self->{'http-method'};
-
-  if ($http_method eq 'GET')
-  {
-    if (!$opts->{'create-authz-header'})
-    {
-      $params->{'X-Amz-Date'} = $self->{'timestamp'};
-      $params->{'X-Amz-Algorithm'} = $ALGORITHM_NAME;
-      $params->{'X-Amz-Credential'} = $self->{'credential'};
-      $params->{'X-Amz-SignedHeaders'} = $self->{'signed-headers'};
+sub create_query_string {
+    my $self   = shift;
+    my $opts   = $self->{opts};
+    my $params = $self->{params};
+
+    if ( !$params ) {
+        $self->{'query-string'} = '';
+        return 1;
     }
-    
-    if ($opts->{'aws-security-token'}) {
-      $params->{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
+
+    my @args = ();
+    my @keys = ();
+
+    my $http_method = $self->{'http-method'};
+
+    if ( $http_method eq 'GET' ) {
+        if ( !$opts->{'create-authz-header'} ) {
+            $params->{'X-Amz-Date'}          = $self->{'timestamp'};
+            $params->{'X-Amz-Algorithm'}     = $ALGORITHM_NAME;
+            $params->{'X-Amz-Credential'}    = $self->{'credential'};
+            $params->{'X-Amz-SignedHeaders'} = $self->{'signed-headers'};
+        }
+
+        if ( $opts->{'aws-security-token'} ) {
+            $params->{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
+        }
+
+        @keys = sort keys %{$params};
     }
-    
-    @keys = sort keys %{$params};
-  }
-  else # POST
-  {
-    @keys = keys %{$params};
-  }
-  
-  foreach my $key (@keys)
-  {
-    my $value = $params->{$key};
-    
-    my ($ekey, $evalue) = (uri_escape_utf8($key, $UNSAFE_CHARACTERS), 
-      uri_escape_utf8($value, $UNSAFE_CHARACTERS));
-    
-    push @args, "$ekey=$evalue";
-  }
-  
-  my $aws_query_string = join '&', @args;
-
-  if ($http_method eq 'GET')
-  {
-    $self->{'query-string'} = $aws_query_string;
-    $self->{'payload'} = '';
-  }
-  else # POST
-  {
-    $self->{'query-string'} = '';
-    $self->{'payload'} = $aws_query_string;
-  }
-  
-  return 1;
+    else    # POST
+    {   @keys = keys %{$params};
+    }
+
+    foreach my $key (@keys) {
+        my $value = $params->{$key};
+
+        my ( $ekey, $evalue ) = ( uri_escape_utf8( $key, $UNSAFE_CHARACTERS ), uri_escape_utf8( $value, $UNSAFE_CHARACTERS ) );
+
+        push @args, "$ekey=$evalue";
+    }
+
+    my $aws_query_string = join '&', @args;
+
+    if ( $http_method eq 'GET' ) {
+        $self->{'query-string'} = $aws_query_string;
+        $self->{'payload'}      = '';
+    }
+    else    # POST
+    {   $self->{'query-string'} = '';
+        $self->{'payload'}      = $aws_query_string;
+    }
+
+    return 1;
 }
 
 #
@@ -590,20 +565,19 @@ sub create_query_string
 #   SignedHeaders + '\n' +
 #   HEX(Hash(Payload))
 #
-sub create_canonical_request
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-
-  my $canonical_request = $self->{'http-method'} . "\n";
-  $canonical_request .= $self->{'http-path'} . "\n";
-  $canonical_request .= $self->{'query-string'} . "\n";
-  $canonical_request .= $self->{'canonical-headers'} . "\n";
-  $canonical_request .= $self->{'signed-headers'} . "\n";
-  $canonical_request .= sha256_hex($self->{'payload'});
-  
-  $self->{'canonical-request'} = $canonical_request;
-  return $canonical_request;
+sub create_canonical_request {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my $canonical_request = $self->{'http-method'} . "\n";
+    $canonical_request .= $self->{'http-path'} . "\n";
+    $canonical_request .= $self->{'query-string'} . "\n";
+    $canonical_request .= $self->{'canonical-headers'} . "\n";
+    $canonical_request .= $self->{'signed-headers'} . "\n";
+    $canonical_request .= sha256_hex( $self->{'payload'} );
+
+    $self->{'canonical-request'} = $canonical_request;
+    return $canonical_request;
 }
 
 #
@@ -613,65 +587,59 @@ sub create_canonical_request
 #   Scope + '\n' +
 #   HEX(Hash(CanonicalRequest))
 #
-sub create_string_to_sign
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  
-  my $canonical_request = $self->create_canonical_request();
-
-  my $string_to_sign = $ALGORITHM_NAME . "\n";
-  $string_to_sign .= $self->{'timestamp'} . "\n";
-  $string_to_sign .= $self->{'scope'} . "\n";
-  $string_to_sign .= sha256_hex($canonical_request);
-  
-  $self->{'string-to-sign'} = $string_to_sign;
-  
-  return $string_to_sign;
+sub create_string_to_sign {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my $canonical_request = $self->create_canonical_request();
+
+    my $string_to_sign = $ALGORITHM_NAME . "\n";
+    $string_to_sign .= $self->{'timestamp'} . "\n";
+    $string_to_sign .= $self->{'scope'} . "\n";
+    $string_to_sign .= sha256_hex($canonical_request);
+
+    $self->{'string-to-sign'} = $string_to_sign;
+
+    return $string_to_sign;
 }
 
 #
 # Performs the actual signing of the request.
 #
-sub create_signature
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  
-  my $ksecret = $opts->{'aws-secret-key'};
-  my $kdate = hmac_sha256($self->{'daystamp'}, 'AWS4' . $ksecret);
-  my $kregion = hmac_sha256($self->{'region'}, $kdate);
-  my $kservice = hmac_sha256($self->{'service'}, $kregion);
-  my $kcreds = hmac_sha256('aws4_request', $kservice);
-  
-  my $string_to_sign = $self->create_string_to_sign();
-  my $signature = hmac_sha256_hex($string_to_sign, $kcreds);
-  $self->{'signature'} = $signature;
-
-  return $signature;
+sub create_signature {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my $ksecret  = $opts->{'aws-secret-key'};
+    my $kdate    = hmac_sha256( $self->{'daystamp'}, 'AWS4' . $ksecret );
+    my $kregion  = hmac_sha256( $self->{'region'},   $kdate );
+    my $kservice = hmac_sha256( $self->{'service'},  $kregion );
+    my $kcreds   = hmac_sha256( 'aws4_request',      $kservice );
+
+    my $string_to_sign = $self->create_string_to_sign();
+    my $signature      = hmac_sha256_hex( $string_to_sign, $kcreds );
+    $self->{'signature'} = $signature;
+
+    return $signature;
 }
 
 #
 # Populates HTTP header that carries authentication data.
 #
-sub create_authz_header
-{
-  my $self = shift;
-  my $opts = $self->{opts};
-  
-  my $credential = $self->{'credential'};
-  my $signed_headers = $self->{'signed-headers'};
-  my $signature = $self->{'signature'};
-
-  my $authorization =
-    "$ALGORITHM_NAME Credential=$credential, ".
-    "SignedHeaders=$signed_headers, ".
-    "Signature=$signature";
-  
-  my $headers = $self->{'headers'};
-  $headers->{'Authorization'} = $authorization;
-  
-  return 1;
+sub create_authz_header {
+    my $self = shift;
+    my $opts = $self->{opts};
+
+    my $credential     = $self->{'credential'};
+    my $signed_headers = $self->{'signed-headers'};
+    my $signature      = $self->{'signature'};
+
+    my $authorization = "$ALGORITHM_NAME Credential=$credential, " . "SignedHeaders=$signed_headers, " . "Signature=$signature";
+
+    my $headers = $self->{'headers'};
+    $headers->{'Authorization'} = $authorization;
+
+    return 1;
 }
 
 1;
diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
index effa5df..d84f7fc 100644
--- a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -1,834 +1,815 @@
-# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"). You may not 
-# use this file except in compliance with the License. A copy of the License 
-# is located at
-#
-#        http://aws.amazon.com/apache2.0/
-#
-# or in the "LICENSE" file accompanying this file. This file is distributed 
-# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
-# express or implied. See the License for the specific language governing 
-# permissions and limitations under the License.
-
-package CloudWatchClient;
-
-use strict;
-use warnings;
-use base 'Exporter';
-our @EXPORT = qw();
-use File::Basename;
-use AwsSignatureV4;
-use DateTime;
-use Digest::SHA qw(hmac_sha256_base64);
-use URI::Escape qw(uri_escape_utf8);
-use Compress::Zlib;
-use LWP 6;
-
-use LWP::Simple qw($ua get);
-$ua->timeout(2); # timeout for meta-data calls
-
-our %version_prefix_map = (
-  '2010-08-01' => ['GraniteServiceVersion20100801', 'com.amazonaws.cloudwatch.v2010_08_01#']
-);
-
-our %supported_actions = (
-  'DescribeTags' => 1,
-  'PutMetricData' => 1,
-  'GetMetricStatistics' => 1,
-  'ListMetrics' => 1
-);
-
-our %numeric_parameters = (
-  'Timestamp' => 'Timestamp',
-  'RawValue' => 'Value',
-  'StartTime' => 'StartTime',
-  'EndTime' => 'EndTime',
-  'Period' => 'Period'
-);
-
-our %region_suffix_map = (
-    'cn-north-1' => '.cn',
-    'cn-northwest-1' => '.cn'
-);
-
-use constant {
-  DO_NOT_CACHE => 0,
-  USE_CACHE => 1,
-};
-
-use constant {
-  OK => 1,
-  ERROR => 0,
-};
-
-our $client_version = '1.2.0';
-our $service_version = '2010-08-01';
-our $compress_threshold_bytes = 2048;
-our $meta_data_short_ttl = 21600; # 6 hours
-our $meta_data_long_ttl = 86400; # 1 day
-our $http_request_timeout = 5; # seconds
-
-# RFC3986 unsafe characters
-our $unsafe_characters = "^A-Za-z0-9\-\._~";
-
-our $region;
-our $avail_zone;
-our $instance_id;
-our $instance_type;
-our $image_id;
-our $as_group_name; 	 
-our $meta_data_loc = '/var/tmp/aws-mon';
-
-#
-# Queries meta data for the current EC2 instance.
-#
-sub get_meta_data
-{
-  my $resource = shift;
-  my $use_cache = shift;
-  my $meta_data = read_meta_data($resource, $meta_data_short_ttl);
-
-  my $base_uri = 'http://169.254.169.254/latest/meta-data';
-  my $data_value = !$meta_data ? get $base_uri.$resource : $meta_data;
-
-  if (!$data_value) {
-    return "";
-  }
-
-  if ($use_cache) {
-    write_meta_data($resource, $data_value);
-  }
-
-  return $data_value;
-}
-
-#
-# Reads meta-data from the local filesystem.
-#
-sub read_meta_data
-{
-  my $resource = shift;
-  my $default_ttl = shift;
-
-  my $location = $ENV{'AWS_EC2CW_META_DATA'};
-  if (!$location) { 	 
-    $location = $meta_data_loc if ($meta_data_loc); 	 
-  }
-  my $meta_data_ttl = $ENV{'AWS_EC2CW_META_DATA_TTL'};
-  $meta_data_ttl = $default_ttl if (!defined($meta_data_ttl));
-
-  my $data_value;
-  if ($location)
-  {
-    my $filename = $location.$resource;
-    if (-d $filename) {
-      $data_value = `/bin/ls $filename`;
-      chomp($data_value);
-    } elsif (-e $filename) {
-      my $updated = (stat($filename))[9];
-      my $file_age = time() - $updated;
-      if ($file_age < $meta_data_ttl)
-      {
-        open MDATA, "$filename";
-        while(my $line = <MDATA>) {
-          $data_value .= $line;
-        }
-        close MDATA;
-        chomp $data_value;
-      }
-    }
-  }
-
-  return $data_value;
-}
-
-#
-# Writes meta-data to the local filesystem. 	 
-# 	 
-sub write_meta_data 	 
-{ 	 
-  my $resource = shift; 	 
-  my $data_value = shift; 	 
-
-  if ($resource && $data_value) 	 
-  { 	 
-    my $location = $ENV{'AWS_EC2CW_META_DATA'}; 	 
-    if (!$location) { 	 
-      $location = $meta_data_loc if ($meta_data_loc); 	 
-    } 	 
-
-    if ($location) 	 
-    { 	 
-      my $filename = $location.$resource; 	 
-      my $directory = dirname($filename); 	 
-      `/bin/mkdir -p $directory` unless -d $directory; 	 
-
-      open MDATA, ">$filename"; 	 
-      print MDATA $data_value; 	 
-      close MDATA; 	 
-    } 	 
-  } 	 
-}
-
-#
-# Builds up ec2 endpoint URL for this region.
-#
-sub get_ec2_endpoint
-{
-  my $region = get_region();
-  my $endpoint = "https://ec2.amazonaws.com";
-
-  if ($region) {
-    $endpoint = "https://ec2.$region.amazonaws.com"; 
-    if (exists $region_suffix_map{$region}) {
-      $endpoint .= $region_suffix_map{$region};
-    }
-  }
-
-  return $endpoint;
-}
-
-#
-# Obtains Auto Scaling group name by making EC2 API call.
-#
-sub get_auto_scaling_group
-{
-  if ($as_group_name) {
-    return (200, $as_group_name);
-  }
-
-  # Try to get AS group name from the local cache and avoid calling EC2 API for
-  # at least several hours. AS group name is not something that may changes but
-  # just in case it may change at some point, refresh the value from the tag.
-
-  my $resource = '/as-group-name';
-  $as_group_name = read_meta_data($resource, $meta_data_short_ttl);
-  if ($as_group_name) {
-    return (200, $as_group_name);
-  }
-
-  my $opts = shift;
-
-  my %ec2_opts = ();
-  $ec2_opts{'aws-credential-file'} = $opts->{'aws-credential-file'};
-  $ec2_opts{'aws-access-key-id'} = $opts->{'aws-access-key-id'};
-  $ec2_opts{'aws-secret-key'} = $opts->{'aws-secret-key'};
-  $ec2_opts{'short-response'} = 0;
-  $ec2_opts{'retries'} = 1;
-  $ec2_opts{'verbose'} = $opts->{'verbose'};
-  $ec2_opts{'verify'} = $opts->{'verify'};
-  $ec2_opts{'user-agent'} = $opts->{'user-agent'};
-  $ec2_opts{'version'} = '2011-12-15';
-  $ec2_opts{'url'} = get_ec2_endpoint();
-  $ec2_opts{'aws-iam-role'} = $opts->{'aws-iam-role'};
-
-  my %ec2_params = ();
-  $ec2_params{'Filter.1.Name'} = 'resource-id';
-  $ec2_params{'Filter.1.Value.1'} = get_instance_id();
-  $ec2_params{'Filter.2.Name'} = 'key';
-  $ec2_params{'Filter.2.Value.1'} = 'aws:autoscaling:groupName';
-
-  my $response = call_query('DescribeTags', \%ec2_params, \%ec2_opts);
-
-  my $pattern;
-  if ($response->code == 200)
-  {
-    $pattern = "<value>(.*?)<\/value>";
-    if ($response->content =~ /$pattern/s) {
-      $as_group_name = $1;
-      write_meta_data($resource, $as_group_name);
-      return (200, $as_group_name);
-    }
-    $response->message(undef);
-  }
-
-  # In case when EC2 API call fails for whatever reason, keep using the older
-  # value if it is present. Only ofter one day, assume this value is obsolete.
-  # AS group name is not something that is changing on the fly anyway.
-
-  if (!$as_group_name)
-  {
-    # EC2 call failed, so try using older value for AS group name
-    $as_group_name = read_meta_data($resource, $meta_data_long_ttl);
-    if ($as_group_name) {
-      return (200, $as_group_name);
-    }
-  }
-
-  # Unable to obtain AutoScaling group name.
-  # Return the response code and error message
-  return ($response->code, $response->message);
-}
-
-#
-# Obtains EC2 instance id from meta data.
-#
-sub get_instance_id
-{
-  if (!$instance_id) {
-    $instance_id = get_meta_data('/instance-id', USE_CACHE);
-  }
-  return $instance_id;
-}
-
-#
-# Obtains EC2 instance type from meta data.
-#
-sub get_instance_type
-{
-  if (!$instance_type) {
-    $instance_type = get_meta_data('/instance-type', USE_CACHE);
-  }
-  return $instance_type;
-}
-
-#
-# Obtains EC2 image id from meta data.
-#
-sub get_image_id
-{
-  if (!$image_id) {
-    $image_id = get_meta_data('/ami-id', USE_CACHE);
-  }
-  return $image_id;
-}
-
-#
-# Obtains EC2 avilability zone from meta data.
-#
-sub get_avail_zone
-{
-  if (!$avail_zone) {
-    $avail_zone = get_meta_data('/placement/availability-zone', USE_CACHE);
-  }
-  return $avail_zone;
-}
-
-#
-# Extracts region from avilability zone.
-#
-sub get_region
-{
-  if (!$region) {
-    my $azone = get_avail_zone();
-    if ($azone) {
-      $region = substr($azone, 0, -1);
-    }
-  }
-  return $region;
-}
-
-#
-# Buids up the endpoint based on the provided region.
-#
-sub get_endpoint
-{
-  my $region = get_region();
-  my $endpoint = "https://monitoring.amazonaws.com";
-  
-  if ($region) {
-    $endpoint = "https://monitoring.$region.amazonaws.com";
-    if (exists $region_suffix_map{$region}) {
-      $endpoint .= $region_suffix_map{$region};
-    }
-  }
-  
-
-  return $endpoint;
-}
-
-#
-# Read credentials from the IAM Role Metadata.
-#
-sub prepare_iam_role
-{
-  my $opts = shift;
-  my $response = {};
-  my $verbose = $opts->{'verbose'};
-  my $outfile = $opts->{'output-file'};
-  my $iam_role = $opts->{'aws-iam-role'};
-  my $iam_dir = "/iam/security-credentials/";
-
-  # if am_role is not explicitly specified 
-  if (!defined($iam_role)) {
-    my $roles = get_meta_data($iam_dir, DO_NOT_CACHE);
-    my $nr_of_roles = $roles =~ tr/\n//;
-
-    print_out("No credential methods are specified. Trying default IAM role.", $outfile) if $verbose;
-    if ($roles eq "") {
-      return $response = {"code" => ERROR, "error" => "No IAM role is associated with this EC2 instance."};
-    } elsif ($nr_of_roles == 0) {
-      # if only one role
-      $iam_role = $roles;
-    } else {
-      $roles =~ s/\n/, /g; # puts all the roles on one line 
-      $roles =~ s/, $// ; # deletes the comma at the end
-      return {"code" => ERROR, "error" => "More than one IAM roles are associated with this EC2 instance: $roles."};
-    }
-  }
-
-  my $role_content = get_meta_data(($iam_dir . $iam_role), DO_NOT_CACHE);
-
-  # Could not find the IAM role metadata
-  if(!$role_content) {
-    my $roles = get_meta_data($iam_dir, DO_NOT_CACHE);
-    my $roles_message;
-    if($roles) {
-      $roles =~ s/\n/, /g; # puts all the roles on one line
-      $roles =~ s/, $// ; # deletes the comma at the end
-      $roles_message = "Available roles: " . $roles;
-    } else {
-      $roles_message = "This EC2 instance does not have an IAM role associated with it.";
-    }
-    return {"code" => ERROR, "error" => "Failed to obtain credentials for IAM role $iam_role. $roles_message"};
-  }
-
-  print_out("Using IAM role <$iam_role>", $outfile) if $verbose;
-  my $id;
-  my $key;
-  my $token;
-  while ($role_content =~ /(.*)\n/g ) {
-    my $line = $1;
-    if ( $line =~ /"AccessKeyId"[ \t]*:[ \t]*"(.+)"/) {
-      $id = $1;
-      next;
-    }
-    if ( $line =~ /"SecretAccessKey"[ \t]*:[ \t]*"(.+)"/) {
-      $key = $1;
-      next;
-    }
-    if ( $line =~ /"Token"[ \t]*:[ \t]*"(.+)"/) {
-      $token = $1;
-      next;
-    }
-  }
-
-  my $role_statement = "from IAM role <$iam_role>";
-  if (!defined($id) && !defined($key)) {
-    return {"code" => ERROR, "error" => "Failed to parse AWS access key id and secret key $role_statement."};
-  } elsif (!defined($id)) {
-    return {"code" => ERROR, "error" => "Failed to parse AWS access key id $role_statement."};
-  } elsif (!defined($key)) {
-    return {"code" => ERROR, "error" => "Failed to parse AWS secret key $role_statement."};
-  }
-
-  $opts->{'aws-access-key-id'} = $id;
-  $opts->{'aws-secret-key'} = $key;
-  $opts->{'aws-security-token'} = $token;
-  
-  return {"code" => OK};
-}
-
-#
-# Checks if credential set is present. If not, reads credentials from file.
-#
-sub prepare_credentials
-{
-  my $opts = shift;
-  my $verbose = $opts->{'verbose'};
-  my $outfile = $opts->{'output-file'};
-  my $aws_access_key_id = $opts->{'aws-access-key-id'};
-  my $aws_secret_key = $opts->{'aws-secret-key'};
-  my $aws_credential_file = $opts->{'aws-credential-file'};
-
-  if (defined($aws_access_key_id) && !$aws_access_key_id) {
-    return {"code" => ERROR, "error" => "Provided empty AWS access key id."};
-  }
-  if (defined($aws_secret_key) && !$aws_secret_key) {
-    return {"code" => ERROR, "error" => "Provided empty AWS secret key."};
-  }  
-  if ($aws_access_key_id && $aws_secret_key) {
-    return {"code" => OK};
-  }
-
-  if (!$aws_credential_file) {
-    my $env_creds_file = $ENV{'AWS_CREDENTIAL_FILE'};
-    if (defined($env_creds_file) && length($env_creds_file) > 0) {
-      $aws_credential_file = $env_creds_file;
-    }
-  }
-
-  if (!$aws_credential_file) {
-    my $conf_file = &File::Basename::dirname($0) . '/awscreds.conf' ;
-    if (-e $conf_file) {
-      $aws_credential_file = $conf_file;
-    }
-  }
-
-  if ($aws_credential_file) {
-    my $file = $aws_credential_file;
-    open(FILE, '<:utf8', $file) or return {"code" => ERROR, "error" => "Failed to open AWS credentials file <$file>"};
-    print_out("Using AWS credentials file <$aws_credential_file>", $outfile) if $verbose;
-
-    while (my $line = <FILE>)
-    {
-      $line =~ /^$/ and next; # skip empty lines
-      $line =~ /^#.*/ and next; # skip commented lines
-      $line =~ /^\s*(.*?)=(.*?)\s*$/ or return {"code" => ERROR, "error" => "Failed to parse AWS credential entry '$line' in <$file>."};
-      my ($key, $value) = ($1, $2);
-      if ($key eq 'AWSAccessKeyId') {
-        $opts->{'aws-access-key-id'} = $value;
-      } elsif ($key eq 'AWSSecretKey') {
-        $opts->{'aws-secret-key'} = $value;
-      }
-    }
-    close (FILE);
-  }
-
-  $aws_access_key_id = $opts->{'aws-access-key-id'};
-  $aws_secret_key = $opts->{'aws-secret-key'};
-
-  if (!$aws_access_key_id || !$aws_secret_key) {
-    # if all the credential methods failed, try iam_role
-    # either the default or user specified IAM role
-    return prepare_iam_role($opts);
-  }
-
-  return {"code" => OK};
-}
-
-#
-# Retrieves the current UTC time minus the offset (in hours).
-#
-sub get_offset_time
-{
-  my $offset = shift;
-  my $dt = DateTime->now();
-  $dt->subtract(hours => $offset);
-  return $dt->epoch;
-}
-
-#
-# Prints out diagnostic message to a file or standard output.
-#
-sub print_out
-{
-  my $text = shift;
-  my $filename = shift;
-  
-  if ($filename) {
-    open OUT_STREAM, ">>$filename";
-    print OUT_STREAM "$text\n";
-    close OUT_STREAM;
-  }
-  else {
-    print "$text\n";
-  }
-}
-
-#
-# Retrieves the interface and type prefixes for the version and action supplied 
-# e.g. 2010-08-01 => [GraniteService20100801, com.amazonaws.cloudwatch.v2010_08_01#]
-#
-sub get_interface_version_and_type
-{
-  my $params = shift;
-  my $version = $params->{'Version'};
-
-  if (!(defined($version))) {
-    $version = $service_version;
-  }
-  if (!(exists $version_prefix_map{$version})) {
-    return {"code" => ERROR, "error" => 'Unsupported version'};
-  }
-
-  return {"code" => OK, "version" => $version_prefix_map{$version}[0], "type" => $version_prefix_map{$version}[1]};
-}
-
-#
-# Creates a key-value pair string to get added to the JSON payload.
-#
-sub add_simple_parameter
-{
-  my $param_name = shift;
-  my $value = shift;
-
-  my $json_data = '';
-  if (exists $numeric_parameters{$param_name}) {
-    my $key = $numeric_parameters{$param_name};
-    $json_data = qq("$key":$value,);
-  }
-  else {
-    $json_data = qq("$param_name":"$value",);
-  }
-
-  return $json_data;
-} 
-
-#
-# Iterates through hash entries and adds them to the JSON payload.
-#
-sub add_hash
-{
-  my $param_name = shift;
-  my $hash_ref = shift;
-
-  my $json_data = (($param_name eq '')? $param_name : qq("$param_name":)) . "{";
-  while (my ($key, $value) = each %{$hash_ref})
-  {
-    if (ref $value eq 'ARRAY') {
-      $json_data .= add_array($key, $value) . ",";
-    }
-    else {
-      $json_data .= add_simple_parameter($key, $value);
-    }
-  }
-  chop($json_data) unless ((keys %$hash_ref) == 0);
-  $json_data .= "}";
-
-  return $json_data;
-}
-
-#
-# Iterates through array entries and adds them to the JSON payload.
-#
-sub add_array
-{
-  my $param_name = shift;
-  my $array_ref = shift;
-
-  my $json_data = (($param_name eq '')? $param_name : qq("$param_name":)) . "[";
-  for my $array_val (@{$array_ref})
-  {
-    if (ref $array_val eq 'HASH') {
-      $json_data .= add_hash('', $array_val) . ",";
-    } else {
-      $json_data .= qq("$array_val",);
-    }
-  }
-  chop($json_data) unless (scalar @$array_ref == 0);
-  $json_data .= "]";
-
-  return $json_data;
-}
-
-#
-# Builds a JSON payload from the request parameters.
-#
-sub construct_payload
-{
-  my $params = shift;
-  my $json_data = add_hash("", $params->{'Input'});
-  return $json_data;
-}
-
-#
-# Prepares SigV4 request headers and JSON payload for the HTTP request.
-#
-sub get_json_payload_and_headers
-{
-  my $params = shift;
-  my $opts = shift;
-
-  my $operation = $params->{'Operation'};
-  my $json_data = construct_payload($params);
-
-  my $sigv4 = AwsSignatureV4->new_aws_json($operation, $json_data, $opts);
-  if (!($sigv4->sign_http_post())) {
-    return {"code" => ERROR, "error" => $sigv4->error};
-  }
-
-  return {"code" => OK, "payload" => $json_data, "headers" => $sigv4->headers};
-}
-
-#
-# Shared call setup used for both AWS/JSON and AWS/Query HTTP requests.
-#
-sub call_setup
-{
-  my $params = shift;
-  my $opts = shift;
-  my $validation_contents;
-  
-  $opts->{'http-method'} = 'POST';
-
-  if (!defined($opts->{'url'})) {
-    $opts->{'url'} = get_endpoint();
-  }
-
-  if (!defined($opts->{'version'})) {
-    $opts->{'version'} = $service_version;
-  }
-  $params->{'Version'} = $opts->{'version'};
-
-  if (!defined($opts->{'user-agent'})) {
-    $opts->{'user-agent'} = "CloudWatch-Scripting/$client_version";
-  }
-
-  return prepare_credentials($opts);
-}
-
-#
-# Helper method used by both call_json and call_query.
-# Configures and sends the HTTP request and passes result back to caller.
-#
-sub call
-{
-  my $payload = shift;
-  my $headers = shift;
-  my $opts = shift;
-  my $failure_pattern = shift;
-
-  my $user_agent = new LWP::UserAgent(agent => $opts->{'user-agent}'});
-  $user_agent->timeout($http_request_timeout);
-
-  my $http_headers = HTTP::Headers->new(%$headers);
-  my $request = new HTTP::Request $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload;
-  
-  if (defined($opts->{'enable-compression'}) && length($payload) > $compress_threshold_bytes) {
-    $request->encode('gzip');
-  }
-
-  my $response;
-  my $keep_trying = 1;
-  my $call_attempts = 1;
-  my $endpoint = $opts->{'url'};
-  my $verbose = $opts->{'verbose'};
-  my $outfile = $opts->{'output-file'};
-
-  print_out("Endpoint: $endpoint", $outfile) if $verbose;
-  print_out("Payload: $payload", $outfile) if $verbose;
-
-  # initial and max delay in seconds between retries
-  my $delay = 4; 
-  my $max_delay = 16;
-
-  if (defined($opts->{'retries'})) {
-    $call_attempts += $opts->{'retries'};
-  }  
-  if (defined($opts->{'max-backoff-sec'})) {
-    $max_delay = $opts->{'max-backoff-sec'};
-  }
-
-  my $response_code = 0;
-
-  if ($opts->{'verify'}) {
-    return (HTTP::Response->new(200, 'This is a verification run, not an actual response.'));
-  }
-
-  for (my $i = 0; $i < $call_attempts && $keep_trying; ++$i)
-  {
-    my $attempt = $i + 1;
-    $response = $user_agent->request($request);
-    $response_code = $response->code;
-    if ($verbose) {
-      print_out("Received HTTP status $response_code on attempt $attempt", $outfile);
-      if ($response_code != 200) {
-        print_out($response->content, $outfile);
-      }
-    }
-    $keep_trying = 0;
-    if ($response_code >= 500) {
-      $keep_trying = 1;
-    } elsif ($response_code == 400) {
-      # special case to handle throttling fault
-      my $pattern = "Throttling";
-      if ($response->content =~ m/$pattern/) {
-        print_out("Request throttled.", $outfile) if $verbose;
-        $keep_trying = 1;
-      }
-    }
-    if ($keep_trying && $attempt < $call_attempts) {
-      print_out("Waiting $delay seconds before next retry.", $outfile) if $verbose;
-      sleep($delay);
-      my $incdelay = $delay * 2;
-      $delay = $incdelay > $max_delay ? $max_delay : $incdelay;
-    }
-  }
-
-  print_out($response->content, $outfile) if ($verbose && $response_code == 200);
-
-  if (!$response->is_success) {
-    if ($response->content =~ /$failure_pattern/s) {
-      $response->message($1);
-    } elsif ($response->content =~ m/__type.*?#(.*?)"}/) {
-      $response->message($1);
-    } else {
-      $response->message($response->content);
-    }
-  }
-
-  return $response;
-}
- 
-#
-# Makes a remote invocation to the CloudWatch service using the AWS/Query format.
-# Returns request ID, if successful, or error message if unsuccessful.
-#
-sub call_query
-{
-  my $operation = shift;
-  my $params = shift;
-  my $opts = shift;
-  my $validation_contents;
-  my $payload;
-  my $headers = {};
-  my $failure_pattern = "<Message>(.*?)<\/Message>";
-  $params->{'Action'} = $operation;
-
-  $validation_contents = call_setup($params, $opts);
-
-  if ($validation_contents->{"code"} == ERROR) {
-    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
-  }
-
-  my $sigv4 = AwsSignatureV4->new_aws_query($params, $opts);
-
-  if (!$sigv4->sign_http_post()) {
-    return (HTTP::Response->new(400, $sigv4->{'error'}));
-  }
-
-  $payload = $sigv4->{'payload'};
-  $headers = $sigv4->{'headers'};
-
-  return call($payload, $headers, $opts, $failure_pattern);
-}
-
-
-#
-# Makes a remote invocation to the CloudWatch service using the AWS/JSON format.
-# Returns the full response if successful, or error message if unsuccessful.
-#
-sub call_json
-{
-  my $operation = shift;
-  my $params = shift;
-  my $opts = shift;
-  my $validation_contents;
-  my $payload;
-  my $headers = {};
-  my $failure_pattern =  "\"message\":\"(.*?)\"";
-
-  $validation_contents = call_setup($params, $opts);
-
-  if ($validation_contents->{"code"} == ERROR) {
-    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
-  }
-
-  if(!(exists $supported_actions{$operation})) {
-    return(HTTP::Response->new(ERROR, 'Unsupported Operation'));
-  }
-
-  $validation_contents = get_interface_version_and_type($params);
-
-  if ($validation_contents->{"code"} == ERROR) {
-    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
-  }
-
-  $params->{'Operation'} = $validation_contents->{"version"} . "." . $operation;
-  $params->{'Input'}->{'__type'} = $validation_contents->{"type"} . $operation . "Input";
-
-  $validation_contents = get_json_payload_and_headers($params, $opts);
-
-  if ($validation_contents->{"code"} == ERROR) {
-    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
-  }
-
-  $payload = $validation_contents->{"payload"};
-  $headers = $validation_contents->{"headers"};
-
-  return call($payload, $headers, $opts, $failure_pattern);
-}
-
-1;
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not
+# use this file except in compliance with the License. A copy of the License
+# is located at
+#
+#        http://aws.amazon.com/apache2.0/
+#
+# or in the "LICENSE" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+# this package has been updated from the original version to:
+# Update formatting
+
+package CloudWatchClient;
+
+use strict;
+use warnings;
+
+use base 'Exporter';
+our @EXPORT = qw();
+use File::Basename;
+use App::AWS::CloudWatch::Monitor::AwsSignatureV4;
+use DateTime;
+use Digest::SHA qw(hmac_sha256_base64);
+use URI::Escape qw(uri_escape_utf8);
+use Compress::Zlib;
+use LWP 6;
+
+use LWP::Simple qw($ua get);
+$ua->timeout(2);    # timeout for meta-data calls
+
+our %version_prefix_map = ( '2010-08-01' => [ 'GraniteServiceVersion20100801', 'com.amazonaws.cloudwatch.v2010_08_01#' ] );
+
+our %supported_actions = (
+    'DescribeTags'        => 1,
+    'PutMetricData'       => 1,
+    'GetMetricStatistics' => 1,
+    'ListMetrics'         => 1
+);
+
+our %numeric_parameters = (
+    'Timestamp' => 'Timestamp',
+    'RawValue'  => 'Value',
+    'StartTime' => 'StartTime',
+    'EndTime'   => 'EndTime',
+    'Period'    => 'Period'
+);
+
+our %region_suffix_map = (
+    'cn-north-1'     => '.cn',
+    'cn-northwest-1' => '.cn'
+);
+
+use constant {
+    DO_NOT_CACHE => 0,
+    USE_CACHE    => 1,
+};
+
+use constant {
+    OK    => 1,
+    ERROR => 0,
+};
+
+our $client_version           = '1.2.0';
+our $service_version          = '2010-08-01';
+our $compress_threshold_bytes = 2048;
+our $meta_data_short_ttl      = 21600;          # 6 hours
+our $meta_data_long_ttl       = 86400;          # 1 day
+our $http_request_timeout     = 5;              # seconds
+
+# RFC3986 unsafe characters
+our $unsafe_characters = "^A-Za-z0-9\-\._~";
+
+our $region;
+our $avail_zone;
+our $instance_id;
+our $instance_type;
+our $image_id;
+our $as_group_name;
+our $meta_data_loc = '/var/tmp/aws-mon';
+
+#
+# Queries meta data for the current EC2 instance.
+#
+sub get_meta_data {
+    my $resource  = shift;
+    my $use_cache = shift;
+    my $meta_data = read_meta_data( $resource, $meta_data_short_ttl );
+
+    my $base_uri   = 'http://169.254.169.254/latest/meta-data';
+    my $data_value = !$meta_data ? get $base_uri. $resource : $meta_data;
+
+    if ( !$data_value ) {
+        return "";
+    }
+
+    if ($use_cache) {
+        write_meta_data( $resource, $data_value );
+    }
+
+    return $data_value;
+}
+
+#
+# Reads meta-data from the local filesystem.
+#
+sub read_meta_data {
+    my $resource    = shift;
+    my $default_ttl = shift;
+
+    my $location = $ENV{'AWS_EC2CW_META_DATA'};
+    if ( !$location ) {
+        $location = $meta_data_loc if ($meta_data_loc);
+    }
+    my $meta_data_ttl = $ENV{'AWS_EC2CW_META_DATA_TTL'};
+    $meta_data_ttl = $default_ttl if ( !defined($meta_data_ttl) );
+
+    my $data_value;
+    if ($location) {
+        my $filename = $location . $resource;
+        if ( -d $filename ) {
+            $data_value = `/bin/ls $filename`;
+            chomp($data_value);
+        }
+        elsif ( -e $filename ) {
+            my $updated  = ( stat($filename) )[9];
+            my $file_age = time() - $updated;
+            if ( $file_age < $meta_data_ttl ) {
+                open MDATA, "$filename";
+                while ( my $line = <MDATA> ) {
+                    $data_value .= $line;
+                }
+                close MDATA;
+                chomp $data_value;
+            }
+        }
+    }
+
+    return $data_value;
+}
+
+#
+# Writes meta-data to the local filesystem.
+#
+sub write_meta_data {
+    my $resource   = shift;
+    my $data_value = shift;
+
+    if ( $resource && $data_value ) {
+        my $location = $ENV{'AWS_EC2CW_META_DATA'};
+        if ( !$location ) {
+            $location = $meta_data_loc if ($meta_data_loc);
+        }
+
+        if ($location) {
+            my $filename  = $location . $resource;
+            my $directory = dirname($filename);
+            `/bin/mkdir -p $directory` unless -d $directory;
+
+            open MDATA, ">$filename";
+            print MDATA $data_value;
+            close MDATA;
+        }
+    }
+}
+
+#
+# Builds up ec2 endpoint URL for this region.
+#
+sub get_ec2_endpoint {
+    my $region   = get_region();
+    my $endpoint = "https://ec2.amazonaws.com";
+
+    if ($region) {
+        $endpoint = "https://ec2.$region.amazonaws.com";
+        if ( exists $region_suffix_map{$region} ) {
+            $endpoint .= $region_suffix_map{$region};
+        }
+    }
+
+    return $endpoint;
+}
+
+#
+# Obtains Auto Scaling group name by making EC2 API call.
+#
+sub get_auto_scaling_group {
+    if ($as_group_name) {
+        return ( 200, $as_group_name );
+    }
+
+    # Try to get AS group name from the local cache and avoid calling EC2 API for
+    # at least several hours. AS group name is not something that may changes but
+    # just in case it may change at some point, refresh the value from the tag.
+
+    my $resource = '/as-group-name';
+    $as_group_name = read_meta_data( $resource, $meta_data_short_ttl );
+    if ($as_group_name) {
+        return ( 200, $as_group_name );
+    }
+
+    my $opts = shift;
+
+    my %ec2_opts = ();
+    $ec2_opts{'aws-credential-file'} = $opts->{'aws-credential-file'};
+    $ec2_opts{'aws-access-key-id'}   = $opts->{'aws-access-key-id'};
+    $ec2_opts{'aws-secret-key'}      = $opts->{'aws-secret-key'};
+    $ec2_opts{'short-response'}      = 0;
+    $ec2_opts{'retries'}             = 1;
+    $ec2_opts{'verbose'}             = $opts->{'verbose'};
+    $ec2_opts{'verify'}              = $opts->{'verify'};
+    $ec2_opts{'user-agent'}          = $opts->{'user-agent'};
+    $ec2_opts{'version'}             = '2011-12-15';
+    $ec2_opts{'url'}                 = get_ec2_endpoint();
+    $ec2_opts{'aws-iam-role'}        = $opts->{'aws-iam-role'};
+
+    my %ec2_params = ();
+    $ec2_params{'Filter.1.Name'}    = 'resource-id';
+    $ec2_params{'Filter.1.Value.1'} = get_instance_id();
+    $ec2_params{'Filter.2.Name'}    = 'key';
+    $ec2_params{'Filter.2.Value.1'} = 'aws:autoscaling:groupName';
+
+    my $response = call_query( 'DescribeTags', \%ec2_params, \%ec2_opts );
+
+    my $pattern;
+    if ( $response->code == 200 ) {
+        $pattern = "<value>(.*?)<\/value>";
+        if ( $response->content =~ /$pattern/s ) {
+            $as_group_name = $1;
+            write_meta_data( $resource, $as_group_name );
+            return ( 200, $as_group_name );
+        }
+        $response->message(undef);
+    }
+
+    # In case when EC2 API call fails for whatever reason, keep using the older
+    # value if it is present. Only ofter one day, assume this value is obsolete.
+    # AS group name is not something that is changing on the fly anyway.
+
+    if ( !$as_group_name ) {
+
+        # EC2 call failed, so try using older value for AS group name
+        $as_group_name = read_meta_data( $resource, $meta_data_long_ttl );
+        if ($as_group_name) {
+            return ( 200, $as_group_name );
+        }
+    }
+
+    # Unable to obtain AutoScaling group name.
+    # Return the response code and error message
+    return ( $response->code, $response->message );
+}
+
+#
+# Obtains EC2 instance id from meta data.
+#
+sub get_instance_id {
+    if ( !$instance_id ) {
+        $instance_id = get_meta_data( '/instance-id', USE_CACHE );
+    }
+    return $instance_id;
+}
+
+#
+# Obtains EC2 instance type from meta data.
+#
+sub get_instance_type {
+    if ( !$instance_type ) {
+        $instance_type = get_meta_data( '/instance-type', USE_CACHE );
+    }
+    return $instance_type;
+}
+
+#
+# Obtains EC2 image id from meta data.
+#
+sub get_image_id {
+    if ( !$image_id ) {
+        $image_id = get_meta_data( '/ami-id', USE_CACHE );
+    }
+    return $image_id;
+}
+
+#
+# Obtains EC2 avilability zone from meta data.
+#
+sub get_avail_zone {
+    if ( !$avail_zone ) {
+        $avail_zone = get_meta_data( '/placement/availability-zone', USE_CACHE );
+    }
+    return $avail_zone;
+}
+
+#
+# Extracts region from avilability zone.
+#
+sub get_region {
+    if ( !$region ) {
+        my $azone = get_avail_zone();
+        if ($azone) {
+            $region = substr( $azone, 0, -1 );
+        }
+    }
+    return $region;
+}
+
+#
+# Buids up the endpoint based on the provided region.
+#
+sub get_endpoint {
+    my $region   = get_region();
+    my $endpoint = "https://monitoring.amazonaws.com";
+
+    if ($region) {
+        $endpoint = "https://monitoring.$region.amazonaws.com";
+        if ( exists $region_suffix_map{$region} ) {
+            $endpoint .= $region_suffix_map{$region};
+        }
+    }
+
+    return $endpoint;
+}
+
+#
+# Read credentials from the IAM Role Metadata.
+#
+sub prepare_iam_role {
+    my $opts     = shift;
+    my $response = {};
+    my $verbose  = $opts->{'verbose'};
+    my $outfile  = $opts->{'output-file'};
+    my $iam_role = $opts->{'aws-iam-role'};
+    my $iam_dir  = "/iam/security-credentials/";
+
+    # if am_role is not explicitly specified
+    if ( !defined($iam_role) ) {
+        my $roles       = get_meta_data( $iam_dir, DO_NOT_CACHE );
+        my $nr_of_roles = $roles =~ tr/\n//;
+
+        print_out( "No credential methods are specified. Trying default IAM role.", $outfile ) if $verbose;
+        if ( $roles eq "" ) {
+            return $response = { "code" => ERROR, "error" => "No IAM role is associated with this EC2 instance." };
+        }
+        elsif ( $nr_of_roles == 0 ) {
+
+            # if only one role
+            $iam_role = $roles;
+        }
+        else {
+            $roles =~ s/\n/, /g;    # puts all the roles on one line
+            $roles =~ s/, $//;      # deletes the comma at the end
+            return { "code" => ERROR, "error" => "More than one IAM roles are associated with this EC2 instance: $roles." };
+        }
+    }
+
+    my $role_content = get_meta_data( ( $iam_dir . $iam_role ), DO_NOT_CACHE );
+
+    # Could not find the IAM role metadata
+    if ( !$role_content ) {
+        my $roles = get_meta_data( $iam_dir, DO_NOT_CACHE );
+        my $roles_message;
+        if ($roles) {
+            $roles =~ s/\n/, /g;    # puts all the roles on one line
+            $roles =~ s/, $//;      # deletes the comma at the end
+            $roles_message = "Available roles: " . $roles;
+        }
+        else {
+            $roles_message = "This EC2 instance does not have an IAM role associated with it.";
+        }
+        return { "code" => ERROR, "error" => "Failed to obtain credentials for IAM role $iam_role. $roles_message" };
+    }
+
+    print_out( "Using IAM role <$iam_role>", $outfile ) if $verbose;
+    my $id;
+    my $key;
+    my $token;
+    while ( $role_content =~ /(.*)\n/g ) {
+        my $line = $1;
+        if ( $line =~ /"AccessKeyId"[ \t]*:[ \t]*"(.+)"/ ) {
+            $id = $1;
+            next;
+        }
+        if ( $line =~ /"SecretAccessKey"[ \t]*:[ \t]*"(.+)"/ ) {
+            $key = $1;
+            next;
+        }
+        if ( $line =~ /"Token"[ \t]*:[ \t]*"(.+)"/ ) {
+            $token = $1;
+            next;
+        }
+    }
+
+    my $role_statement = "from IAM role <$iam_role>";
+    if ( !defined($id) && !defined($key) ) {
+        return { "code" => ERROR, "error" => "Failed to parse AWS access key id and secret key $role_statement." };
+    }
+    elsif ( !defined($id) ) {
+        return { "code" => ERROR, "error" => "Failed to parse AWS access key id $role_statement." };
+    }
+    elsif ( !defined($key) ) {
+        return { "code" => ERROR, "error" => "Failed to parse AWS secret key $role_statement." };
+    }
+
+    $opts->{'aws-access-key-id'}  = $id;
+    $opts->{'aws-secret-key'}     = $key;
+    $opts->{'aws-security-token'} = $token;
+
+    return { "code" => OK };
+}
+
+#
+# Checks if credential set is present. If not, reads credentials from file.
+#
+sub prepare_credentials {
+    my $opts                = shift;
+    my $verbose             = $opts->{'verbose'};
+    my $outfile             = $opts->{'output-file'};
+    my $aws_access_key_id   = $opts->{'aws-access-key-id'};
+    my $aws_secret_key      = $opts->{'aws-secret-key'};
+    my $aws_credential_file = $opts->{'aws-credential-file'};
+
+    if ( defined($aws_access_key_id) && !$aws_access_key_id ) {
+        return { "code" => ERROR, "error" => "Provided empty AWS access key id." };
+    }
+    if ( defined($aws_secret_key) && !$aws_secret_key ) {
+        return { "code" => ERROR, "error" => "Provided empty AWS secret key." };
+    }
+    if ( $aws_access_key_id && $aws_secret_key ) {
+        return { "code" => OK };
+    }
+
+    if ( !$aws_credential_file ) {
+        my $env_creds_file = $ENV{'AWS_CREDENTIAL_FILE'};
+        if ( defined($env_creds_file) && length($env_creds_file) > 0 ) {
+            $aws_credential_file = $env_creds_file;
+        }
+    }
+
+    if ( !$aws_credential_file ) {
+        my $conf_file = &File::Basename::dirname($0) . '/awscreds.conf';
+        if ( -e $conf_file ) {
+            $aws_credential_file = $conf_file;
+        }
+    }
+
+    if ($aws_credential_file) {
+        my $file = $aws_credential_file;
+        open( FILE, '<:utf8', $file ) or return { "code" => ERROR, "error" => "Failed to open AWS credentials file <$file>" };
+        print_out( "Using AWS credentials file <$aws_credential_file>", $outfile ) if $verbose;
+
+        while ( my $line = <FILE> ) {
+            $line =~ /^$/   and next;    # skip empty lines
+            $line =~ /^#.*/ and next;    # skip commented lines
+            $line =~ /^\s*(.*?)=(.*?)\s*$/
+                or return { "code" => ERROR, "error" => "Failed to parse AWS credential entry '$line' in <$file>." };
+            my ( $key, $value ) = ( $1, $2 );
+            if ( $key eq 'AWSAccessKeyId' ) {
+                $opts->{'aws-access-key-id'} = $value;
+            }
+            elsif ( $key eq 'AWSSecretKey' ) {
+                $opts->{'aws-secret-key'} = $value;
+            }
+        }
+        close(FILE);
+    }
+
+    $aws_access_key_id = $opts->{'aws-access-key-id'};
+    $aws_secret_key    = $opts->{'aws-secret-key'};
+
+    if ( !$aws_access_key_id || !$aws_secret_key ) {
+
+        # if all the credential methods failed, try iam_role
+        # either the default or user specified IAM role
+        return prepare_iam_role($opts);
+    }
+
+    return { "code" => OK };
+}
+
+#
+# Retrieves the current UTC time minus the offset (in hours).
+#
+sub get_offset_time {
+    my $offset = shift;
+    my $dt     = DateTime->now();
+    $dt->subtract( hours => $offset );
+    return $dt->epoch;
+}
+
+#
+# Prints out diagnostic message to a file or standard output.
+#
+sub print_out {
+    my $text     = shift;
+    my $filename = shift;
+
+    if ($filename) {
+        open OUT_STREAM, ">>$filename";
+        print OUT_STREAM "$text\n";
+        close OUT_STREAM;
+    }
+    else {
+        print "$text\n";
+    }
+}
+
+#
+# Retrieves the interface and type prefixes for the version and action supplied
+# e.g. 2010-08-01 => [GraniteService20100801, com.amazonaws.cloudwatch.v2010_08_01#]
+#
+sub get_interface_version_and_type {
+    my $params  = shift;
+    my $version = $params->{'Version'};
+
+    if ( !( defined($version) ) ) {
+        $version = $service_version;
+    }
+    if ( !( exists $version_prefix_map{$version} ) ) {
+        return { "code" => ERROR, "error" => 'Unsupported version' };
+    }
+
+    return { "code" => OK, "version" => $version_prefix_map{$version}[0], "type" => $version_prefix_map{$version}[1] };
+}
+
+#
+# Creates a key-value pair string to get added to the JSON payload.
+#
+sub add_simple_parameter {
+    my $param_name = shift;
+    my $value      = shift;
+
+    my $json_data = '';
+    if ( exists $numeric_parameters{$param_name} ) {
+        my $key = $numeric_parameters{$param_name};
+        $json_data = qq("$key":$value,);
+    }
+    else {
+        $json_data = qq("$param_name":"$value",);
+    }
+
+    return $json_data;
+}
+
+#
+# Iterates through hash entries and adds them to the JSON payload.
+#
+sub add_hash {
+    my $param_name = shift;
+    my $hash_ref   = shift;
+
+    my $json_data = ( ( $param_name eq '' ) ? $param_name : qq("$param_name":) ) . "{";
+    while ( my ( $key, $value ) = each %{$hash_ref} ) {
+        if ( ref $value eq 'ARRAY' ) {
+            $json_data .= add_array( $key, $value ) . ",";
+        }
+        else {
+            $json_data .= add_simple_parameter( $key, $value );
+        }
+    }
+    chop($json_data) unless ( ( keys %$hash_ref ) == 0 );
+    $json_data .= "}";
+
+    return $json_data;
+}
+
+#
+# Iterates through array entries and adds them to the JSON payload.
+#
+sub add_array {
+    my $param_name = shift;
+    my $array_ref  = shift;
+
+    my $json_data = ( ( $param_name eq '' ) ? $param_name : qq("$param_name":) ) . "[";
+    for my $array_val ( @{$array_ref} ) {
+        if ( ref $array_val eq 'HASH' ) {
+            $json_data .= add_hash( '', $array_val ) . ",";
+        }
+        else {
+            $json_data .= qq("$array_val",);
+        }
+    }
+    chop($json_data) unless ( scalar @$array_ref == 0 );
+    $json_data .= "]";
+
+    return $json_data;
+}
+
+#
+# Builds a JSON payload from the request parameters.
+#
+sub construct_payload {
+    my $params    = shift;
+    my $json_data = add_hash( "", $params->{'Input'} );
+    return $json_data;
+}
+
+#
+# Prepares SigV4 request headers and JSON payload for the HTTP request.
+#
+sub get_json_payload_and_headers {
+    my $params = shift;
+    my $opts   = shift;
+
+    my $operation = $params->{'Operation'};
+    my $json_data = construct_payload($params);
+
+    my $sigv4 = AwsSignatureV4->new_aws_json( $operation, $json_data, $opts );
+    if ( !( $sigv4->sign_http_post() ) ) {
+        return { "code" => ERROR, "error" => $sigv4->error };
+    }
+
+    return { "code" => OK, "payload" => $json_data, "headers" => $sigv4->headers };
+}
+
+#
+# Shared call setup used for both AWS/JSON and AWS/Query HTTP requests.
+#
+sub call_setup {
+    my $params = shift;
+    my $opts   = shift;
+    my $validation_contents;
+
+    $opts->{'http-method'} = 'POST';
+
+    if ( !defined( $opts->{'url'} ) ) {
+        $opts->{'url'} = get_endpoint();
+    }
+
+    if ( !defined( $opts->{'version'} ) ) {
+        $opts->{'version'} = $service_version;
+    }
+    $params->{'Version'} = $opts->{'version'};
+
+    if ( !defined( $opts->{'user-agent'} ) ) {
+        $opts->{'user-agent'} = "CloudWatch-Scripting/$client_version";
+    }
+
+    return prepare_credentials($opts);
+}
+
+#
+# Helper method used by both call_json and call_query.
+# Configures and sends the HTTP request and passes result back to caller.
+#
+sub call {
+    my $payload         = shift;
+    my $headers         = shift;
+    my $opts            = shift;
+    my $failure_pattern = shift;
+
+    my $user_agent = new LWP::UserAgent( agent => $opts->{'user-agent}'} );
+    $user_agent->timeout($http_request_timeout);
+
+    my $http_headers = HTTP::Headers->new(%$headers);
+    my $request      = new HTTP::Request $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload;
+
+    if ( defined( $opts->{'enable-compression'} ) && length($payload) > $compress_threshold_bytes ) {
+        $request->encode('gzip');
+    }
+
+    my $response;
+    my $keep_trying   = 1;
+    my $call_attempts = 1;
+    my $endpoint      = $opts->{'url'};
+    my $verbose       = $opts->{'verbose'};
+    my $outfile       = $opts->{'output-file'};
+
+    print_out( "Endpoint: $endpoint", $outfile ) if $verbose;
+    print_out( "Payload: $payload",   $outfile ) if $verbose;
+
+    # initial and max delay in seconds between retries
+    my $delay     = 4;
+    my $max_delay = 16;
+
+    if ( defined( $opts->{'retries'} ) ) {
+        $call_attempts += $opts->{'retries'};
+    }
+    if ( defined( $opts->{'max-backoff-sec'} ) ) {
+        $max_delay = $opts->{'max-backoff-sec'};
+    }
+
+    my $response_code = 0;
+
+    if ( $opts->{'verify'} ) {
+        return ( HTTP::Response->new( 200, 'This is a verification run, not an actual response.' ) );
+    }
+
+    for ( my $i = 0; $i < $call_attempts && $keep_trying; ++$i ) {
+        my $attempt = $i + 1;
+        $response      = $user_agent->request($request);
+        $response_code = $response->code;
+        if ($verbose) {
+            print_out( "Received HTTP status $response_code on attempt $attempt", $outfile );
+            if ( $response_code != 200 ) {
+                print_out( $response->content, $outfile );
+            }
+        }
+        $keep_trying = 0;
+        if ( $response_code >= 500 ) {
+            $keep_trying = 1;
+        }
+        elsif ( $response_code == 400 ) {
+
+            # special case to handle throttling fault
+            my $pattern = "Throttling";
+            if ( $response->content =~ m/$pattern/ ) {
+                print_out( "Request throttled.", $outfile ) if $verbose;
+                $keep_trying = 1;
+            }
+        }
+        if ( $keep_trying && $attempt < $call_attempts ) {
+            print_out( "Waiting $delay seconds before next retry.", $outfile ) if $verbose;
+            sleep($delay);
+            my $incdelay = $delay * 2;
+            $delay = $incdelay > $max_delay ? $max_delay : $incdelay;
+        }
+    }
+
+    print_out( $response->content, $outfile ) if ( $verbose && $response_code == 200 );
+
+    if ( !$response->is_success ) {
+        if ( $response->content =~ /$failure_pattern/s ) {
+            $response->message($1);
+        }
+        elsif ( $response->content =~ m/__type.*?#(.*?)"}/ ) {
+            $response->message($1);
+        }
+        else {
+            $response->message( $response->content );
+        }
+    }
+
+    return $response;
+}
+
+#
+# Makes a remote invocation to the CloudWatch service using the AWS/Query format.
+# Returns request ID, if successful, or error message if unsuccessful.
+#
+sub call_query {
+    my $operation = shift;
+    my $params    = shift;
+    my $opts      = shift;
+    my $validation_contents;
+    my $payload;
+    my $headers         = {};
+    my $failure_pattern = "<Message>(.*?)<\/Message>";
+    $params->{'Action'} = $operation;
+
+    $validation_contents = call_setup( $params, $opts );
+
+    if ( $validation_contents->{"code"} == ERROR ) {
+        return ( HTTP::Response->new( $validation_contents->{"code"}, $validation_contents->{"error"} ) );
+    }
+
+    my $sigv4 = AwsSignatureV4->new_aws_query( $params, $opts );
+
+    if ( !$sigv4->sign_http_post() ) {
+        return ( HTTP::Response->new( 400, $sigv4->{'error'} ) );
+    }
+
+    $payload = $sigv4->{'payload'};
+    $headers = $sigv4->{'headers'};
+
+    return call( $payload, $headers, $opts, $failure_pattern );
+}
+
+#
+# Makes a remote invocation to the CloudWatch service using the AWS/JSON format.
+# Returns the full response if successful, or error message if unsuccessful.
+#
+sub call_json {
+    my $operation = shift;
+    my $params    = shift;
+    my $opts      = shift;
+    my $validation_contents;
+    my $payload;
+    my $headers         = {};
+    my $failure_pattern = "\"message\":\"(.*?)\"";
+
+    $validation_contents = call_setup( $params, $opts );
+
+    if ( $validation_contents->{"code"} == ERROR ) {
+        return ( HTTP::Response->new( $validation_contents->{"code"}, $validation_contents->{"error"} ) );
+    }
+
+    if ( !( exists $supported_actions{$operation} ) ) {
+        return ( HTTP::Response->new( ERROR, 'Unsupported Operation' ) );
+    }
+
+    $validation_contents = get_interface_version_and_type($params);
+
+    if ( $validation_contents->{"code"} == ERROR ) {
+        return ( HTTP::Response->new( $validation_contents->{"code"}, $validation_contents->{"error"} ) );
+    }
+
+    $params->{'Operation'} = $validation_contents->{"version"} . "." . $operation;
+    $params->{'Input'}->{'__type'} = $validation_contents->{"type"} . $operation . "Input";
+
+    $validation_contents = get_json_payload_and_headers( $params, $opts );
+
+    if ( $validation_contents->{"code"} == ERROR ) {
+        return ( HTTP::Response->new( $validation_contents->{"code"}, $validation_contents->{"error"} ) );
+    }
+
+    $payload = $validation_contents->{"payload"};
+    $headers = $validation_contents->{"headers"};
+
+    return call( $payload, $headers, $opts, $failure_pattern );
+}
+
+1;
commit f915c204390226a8089f6d2c5a5daa41580ab1b7
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 12 12:05:28 2021 -0500

    Add original AwsSignatureV4 and CloudWatchClient
    
    This commit adds the original AwsSignatureV4 and CloudWatchClient
    modules from the aws-scripts-mon project.

diff --git a/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
new file mode 100644
index 0000000..8c5c8a0
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/AwsSignatureV4.pm
@@ -0,0 +1,677 @@
+# Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not 
+# use this file except in compliance with the License. A copy of the License 
+# is located at
+#
+#        http://aws.amazon.com/apache2.0/
+#
+# or in the "LICENSE" file accompanying this file. This file is distributed 
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+package AwsSignatureV4;
+
+use strict;
+use warnings;
+
+# Package URI::Escape is implemented in Perl.
+use URI::Escape qw(uri_escape_utf8);
+
+# Package Digest::SHA is implemnted in C for performance.
+use Digest::SHA qw(sha256_hex hmac_sha256 hmac_sha256_hex);
+
+# For using PurePerl implementation of SHA functions
+# use Digest::SHA::PurePerl qw(sha256_hex hmac_sha256 hmac_sha256_hex);
+
+# RFC3986 safe/unsafe characters
+our $SAFE_CHARACTERS = 'A-Za-z0-9\-\._~';
+our $UNSAFE_CHARACTERS = '^'.$SAFE_CHARACTERS;
+
+# Name of the signature encoding algorithm.
+our $ALGORITHM_NAME = 'AWS4-HMAC-SHA256';
+
+#
+# Creates a new signing context object for signing an arbitrary request.
+#
+# Signing context object is a hash of all request data needed to create
+# a valid AWS Signature V4. After the signing takes place, the context object
+# gets populated with intermediate signing artifacts and the actual signature.
+#
+# Input:
+#
+#   $opts - reference to hash that contains control options for the request
+#     url => endpoint of the service to call, e.g. https://monitoring.us-west-2.amazonaws.com/
+#         (the URL can contain path but it should not include query string, i.e. args after ?)
+#     aws-region => explicitly specifies AWS region (if not specified, region is extracted
+#         from endpoint URL; if region is not part of URL, 'us-east-1' is used by default)
+#     aws-service => explicitly specifies AWS service name (this is necessary when service
+#         name is not part of the endpoint URL, e.g. mail/ses, but usually it is)
+#     aws-access-key-id => Access Key Id of AWS credentials
+#     aws-secret-key => Secret Key of AWS credentials
+#     aws-security-token => Security Token in case of STS call
+#
+sub new
+{
+  my $class = shift;
+  my $self = {opts => shift};
+  $self->{'payload'} = '';
+  bless $self, $class;
+  return $self;
+}
+
+#
+# Creates a new signing context object for signing AWS/Query request.
+#
+# AWS/Query request can be signed for either HTTP GET method or POST method.
+# The recommended method is POST as it skips sorting of query string keys
+# and therefore performs faster.
+#
+# Input:
+#
+#   $params - reference to the hash that contains all (name, value) pairs of AWS/Query request
+#     (do not url-encode this data, it will be done as a part of signing and creating payload)
+#
+#   $opts - see defition of 'new' constructor
+#
+sub new_aws_query
+{
+  my $class = shift;
+  my $self = {params => shift, opts => shift};
+  $self->{'content-type'} = 'application/x-www-form-urlencoded; charset=utf-8';
+  $self->{'payload'} = '';
+  return bless $self, $class;
+}
+
+#
+# Creates a new signing context object for signing RPC/JSON request.
+# It only makes sense to sign JSON request for HTTP POST method.
+#
+# Input:
+#
+#   $payload - input data in RPC/JSON format
+#   $opts - see defition of 'new' constructor
+#
+sub new_rpc_json
+{
+  my $class = shift;
+  my $self = {payload => shift, opts => shift};
+  $self->{'content-type'} = 'application/json; charset=utf-8';
+  bless $self, $class;
+  return $self;
+}
+
+#
+# Creates a new signing context object for signing AWS/JSON request.
+# It only makes sense to sign JSON request for HTTP POST method.
+#
+# Input:
+#
+#   $operation - operation name to invoke
+#   $payload - input data in AWS/JSON format
+#   $opts - see defition of 'new' constructor
+#
+sub new_aws_json
+{
+  my $class = shift;
+  my $operation = shift;
+  my $payload = shift;
+  my $opts = shift;
+  my $self = {payload => $payload, opts => $opts};
+  
+  $self->{'content-type'} = 'application/x-amz-json-1.0';
+  
+  if (not exists $opts->{'extra-headers'}) {
+    $opts->{'extra-headers'} = {};
+  }
+  
+  my $extra_headers = $opts->{'extra-headers'};
+  $extra_headers->{'X-Amz-Target'} = $operation;
+  
+  bless $self, $class;
+  return $self;
+}
+
+#
+# Signs the generic HTTP request.
+#
+# Input: (all arguments optional and if specified override what is currently set)
+#
+#   $method - HTTP method
+#   $ctype - content-type of the body
+#   $payload - request body data
+#
+sub sign_http_request
+{
+  my $self = shift;
+  my $method = shift;
+  my $ctype = shift;
+  my $payload = shift;
+
+  $self->{'http-method'} = $method if $method;
+  $self->{'content-type'} = $ctype if $ctype;
+  $self->{'payload'} = $payload if $payload;
+  
+  my $opts = $self->{opts};
+  $opts->{'create-authz-header'} = 1;
+  $self->{'request-url'} = $opts->{'url'};
+  
+  if (!$self->sign()) {
+    return 0;
+  }
+  
+  $self->create_authz_header();
+  return 1;
+}
+
+#
+# Signs request for HTTP POST.
+#
+sub sign_http_post
+{
+  my $self = shift;
+  return $self->sign_http_request('POST');
+}
+
+#
+# Signs request for HTTP PUT.
+#
+sub sign_http_put
+{
+  my $self = shift;
+  return $self->sign_http_request('PUT');
+}
+
+#
+# Signs request for HTTP GET.
+#
+sub sign_http_get
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  $self->{'http-method'} = 'GET';
+  
+  if (!$self->sign()) {
+    return 0;
+  }
+
+  my $postfix = "";
+  my $query_string = $self->{'query-string'};
+
+  if ($query_string) {
+    $postfix = '?'.$query_string;
+  }
+  $self->{'request-url'} = $opts->{'url'}.$postfix;
+
+  $postfix .= ($query_string ? '&' : '?');
+  $self->{'signed-url'} = $opts->{'url'}.$postfix.'X-Amz-Signature='.$self->{'signature'};
+  
+  if ($opts->{'create-authz-header'}) {
+    $self->create_authz_header();
+  }
+  
+  return 1;
+}
+
+#
+# Prepares and signs the request data.
+#
+sub sign
+{
+  my $self = shift;
+  
+  if (!$self->initialize()) {
+    return 0;
+  }
+  
+  $self->create_basic_headers();
+  $self->create_query_string();  
+  $self->create_signature();
+  
+  return 1;
+}
+
+#
+# Returns reference to a hash containing all required HTTP headers.
+# In case of HTTP POST and PUT methods it will also include the
+# Authorization header that carries the signature itself.
+#
+sub headers
+{
+  my $self = shift;
+  return $self->{'headers'};
+}
+
+#
+# In case of AWS/Query request and HTTP POST or PUT method, returns
+# url-encoded query string to be used as a body of HTTP POST request.
+#
+sub payload
+{
+  my $self = shift;
+  return $self->{'payload'};
+}
+
+#
+# Returns complete signed URL to be used in HTTP GET request 'as is'.
+# You can place this value into Web browser location bar and make a call.
+#
+sub signed_url
+{
+  my $self = shift;
+  return $self->{'signed-url'};
+}
+
+#
+# Returns URL to be used in HTTP GET request for the case,
+# when the signature is passed via Authorization HTTP header.
+#
+# You can not use this URL with the Web browser since it does
+# not contain the signature.
+#
+sub request_url
+{
+  my $self = shift;
+  return $self->{'request-url'};
+}
+
+#
+# Returns an error message if any.
+#
+sub error
+{
+  my $self = shift;
+  return $self->{'error'};
+}
+
+#
+# Returns both timestamp and daystamp in the format required for SigV4.
+#
+sub get_timestamp_daystamp
+{
+  my $time = shift;
+  my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime(time);
+  my $timestamp = sprintf("%04d%02d%02dT%02d%02d%02dZ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
+  my $daystamp = substr($timestamp, 0, 8);
+  return ($timestamp, $daystamp);
+}
+
+#
+# Applies regex to get FQDN from URL.
+#
+sub extract_fqdn_from_url
+{
+  my $fqdn = shift;
+  $fqdn =~ s!^https?://([^/:?]*).*$!$1!;
+  return $fqdn;
+}
+
+#
+# Applies regex to get service name from the FQDN.
+#
+sub extract_service_from_fqdn
+{
+  my $fqdn = shift;
+  my $service = $fqdn;
+  $service =~ s!^([^\.]+)\..*$!$1!;
+  return $service;
+}
+
+#
+# Applies regex to get region from the FQDN.
+#
+sub extract_region_from_fqdn
+{
+  my $fqdn = shift;
+  my @parts = split(/\./, $fqdn);
+  return $parts[1] if $parts[1] =~ /\w{2}-\w+-\d+/;
+  return 'us-east-1';
+}
+
+#
+# Applies regex to get the path part of the URL.
+#
+sub extract_path_from_url
+{
+  my $url = shift;
+  my $path = $url;
+  $path =~ s!^https?://[^/]+([^\?]*).*$!$1!;
+  $path = '/' if !$path;
+  return $path;
+}
+
+#
+# Populates essential HTTP headers required for SigV4.
+#
+# CanonicalHeaders =
+#   CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN
+# CanonicalHeadersEntry =
+#   LOWERCASE(HeaderName) + ':' + TRIM(HeaderValue) '\n'
+#
+# SignedHeaders =
+#   LOWERCASE(HeaderName0) + ';' + LOWERCASE(HeaderName1) + ... + LOWERCASE(HeaderNameN)
+#
+sub create_basic_headers
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  
+  my %headers = ();
+  $headers{'Host'} = $self->{'fqdn'};
+  
+  my $extra_date_specified = 0;
+  my $extra_headers = $opts->{'extra-headers'};
+  
+  if ($extra_headers)
+  {
+    foreach my $extra_name ( keys %$extra_headers ) {
+      $headers{$extra_name} = $extra_headers->{$extra_name};
+      if (lc($extra_name) eq 'date' || lc($extra_name) eq 'x-amz-date') {
+        $extra_date_specified = 1;
+      }
+    }
+  }
+  
+  if ($opts->{'aws-security-token'}) {
+    $headers{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
+  }
+  
+  if (!$extra_date_specified && $opts->{'create-authz-header'}) {
+    $headers{'X-Amz-Date'} = $self->{'timestamp'};
+  }
+  
+  if ($self->{'http-method'} ne 'GET' && $self->{'content-type'}) {
+    $headers{'Content-Type'} = $self->{'content-type'};
+  }
+  
+  my %lc_headers = ();
+  my $signed_headers = '';
+  my $canonical_headers = '';
+  
+  foreach my $header_name ( keys %headers ) {
+    my $header_value = $headers{$header_name};
+    # trim leading and trailing whitespaces, see
+    # http://perldoc.perl.org/perlfaq4.html#How-do-I-strip-blank-space-from-the-beginning%2fend-of-a-string%3f
+    $header_value =~ s/^\s+//;
+    $header_value =~ s/\s+$//;
+    # now convert sequential spaces to a single space, but do not remove
+    # extra spaces from any values that are inside quotation marks
+    my @parts = split /("[^"]*")/, $header_value;
+    foreach my $part (@parts) {
+      unless ($part =~ /^"/) {
+          $part =~ s/[ ]+/ /g;
+      }
+    }
+    $header_value = join '', @parts;
+    $lc_headers{lc($header_name)} = $header_value;
+  }
+  
+  for my $lc_header (sort keys %lc_headers)
+  {
+    $signed_headers .= ';' if length($signed_headers) > 0;
+    $signed_headers .= $lc_header;
+    $canonical_headers .= $lc_header . ':' . $lc_headers{$lc_header} . "\n";
+  }
+  
+  $self->{'signed-headers'} = $signed_headers;
+  $self->{'canonical-headers'} = $canonical_headers;
+  $self->{'headers'} = \%headers;
+  
+  return 1;
+}
+
+#
+# Validates input and populates essential pre-requisites.
+#
+sub initialize
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  
+  my $url = $opts->{'url'};
+  if (!$url) {
+    $self->{'error'} = 'Endpoint URL is not specified.';
+    return 0;
+  }  
+  if (index($url, '?') != -1) {
+    $self->{'error'} = 'Endpoint URL cannot contain query string.';
+    return 0;
+  }
+  
+  my $akid = $opts->{'aws-access-key-id'};
+  if (!$akid) {
+    $self->{'error'} = 'AWS Access Key Id is not specified.';
+    return 0;
+  }  
+  if (!$opts->{'aws-secret-key'}) {
+    $self->{'error'} = 'AWS Secret Key is not specified.';
+    return 0;
+  }
+  
+  # obtain FQDN from the endpoint url
+  my $fqdn = extract_fqdn_from_url($url);
+  if (!$fqdn) {
+    $self->{'error'} = 'Failed to extract FQDN from endpoint URL.';
+    return 0;
+  }
+  $self->{'fqdn'} = $fqdn;
+
+  # use pre-defined region if specified, otherwise grab it from url
+  my $region = $opts->{'aws-region'};
+  if (!$region) {
+    # if region is not part of url, the default region is returned 
+    $region = extract_region_from_fqdn($fqdn);
+  }
+  $self->{'region'} = $region;
+  
+  # use pre-defined service if specified, otherwise grab it from url
+  # this is specifically important when url does not include service name, e.g. ses/mail
+  my $service = $opts->{'aws-service'};
+  if (!$service) {
+    $service = extract_service_from_fqdn($fqdn);
+    if (!$service) {
+      $self->{'error'} = 'Failed to extract service name from endpoint URL.';
+      return 0;
+    }
+  }
+  $self->{'service'} = $service;
+
+  # obtain uri path part from the endpoint url
+  my $path = extract_path_from_url($url);
+  if (index($path, '.') != -1 || index($path, '//') != -1) {
+    $self->{'error'} = 'Endpoint URL path must be normalized.';
+    return 0;
+  }
+  $self->{'http-path'} = $path;
+  
+  # initialize time of the signature
+  
+  my ($timestamp, $daystamp);
+  
+  if ($opts->{'timestamp'}) {
+    $timestamp = $opts->{'timestamp'};
+    $daystamp = substr($timestamp, 0, 8);
+  }
+  else {
+    my $time = time();
+    $self->{'time'} = $time;
+    ($timestamp, $daystamp) = get_timestamp_daystamp($time);
+  }
+  $self->{'timestamp'} = $timestamp;
+  $self->{'daystamp'} = $daystamp;
+  
+  # initialize scope & credential
+  
+  my $scope = "$daystamp/$region/$service/aws4_request";
+  $self->{'scope'} = $scope;
+  
+  my $credential = "$akid/$scope";
+  $self->{'credential'} = $credential;
+  
+  return 1;
+}
+
+#
+# Builds up AWS Query request as a chain of url-encoded name=value pairs separated by &.
+#
+# Note that SigV4 is payload-agnostic when it comes to POST request body so there is no
+# need to sort arguments in the AWS Query string for the POST method.
+#
+sub create_query_string
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  my $params = $self->{params};
+  
+  if (!$params) {
+    $self->{'query-string'} = '';
+    return 1;
+  }
+  
+  my @args = ();
+  my @keys = ();
+
+  my $http_method = $self->{'http-method'};
+
+  if ($http_method eq 'GET')
+  {
+    if (!$opts->{'create-authz-header'})
+    {
+      $params->{'X-Amz-Date'} = $self->{'timestamp'};
+      $params->{'X-Amz-Algorithm'} = $ALGORITHM_NAME;
+      $params->{'X-Amz-Credential'} = $self->{'credential'};
+      $params->{'X-Amz-SignedHeaders'} = $self->{'signed-headers'};
+    }
+    
+    if ($opts->{'aws-security-token'}) {
+      $params->{'X-Amz-Security-Token'} = $opts->{'aws-security-token'};
+    }
+    
+    @keys = sort keys %{$params};
+  }
+  else # POST
+  {
+    @keys = keys %{$params};
+  }
+  
+  foreach my $key (@keys)
+  {
+    my $value = $params->{$key};
+    
+    my ($ekey, $evalue) = (uri_escape_utf8($key, $UNSAFE_CHARACTERS), 
+      uri_escape_utf8($value, $UNSAFE_CHARACTERS));
+    
+    push @args, "$ekey=$evalue";
+  }
+  
+  my $aws_query_string = join '&', @args;
+
+  if ($http_method eq 'GET')
+  {
+    $self->{'query-string'} = $aws_query_string;
+    $self->{'payload'} = '';
+  }
+  else # POST
+  {
+    $self->{'query-string'} = '';
+    $self->{'payload'} = $aws_query_string;
+  }
+  
+  return 1;
+}
+
+#
+# CanonicalRequest =
+#   Method + '\n' +
+#   CanonicalURI + '\n' +
+#   CanonicalQueryString + '\n' +
+#   CanonicalHeaders + '\n' +
+#   SignedHeaders + '\n' +
+#   HEX(Hash(Payload))
+#
+sub create_canonical_request
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+
+  my $canonical_request = $self->{'http-method'} . "\n";
+  $canonical_request .= $self->{'http-path'} . "\n";
+  $canonical_request .= $self->{'query-string'} . "\n";
+  $canonical_request .= $self->{'canonical-headers'} . "\n";
+  $canonical_request .= $self->{'signed-headers'} . "\n";
+  $canonical_request .= sha256_hex($self->{'payload'});
+  
+  $self->{'canonical-request'} = $canonical_request;
+  return $canonical_request;
+}
+
+#
+# StringToSign =
+#   Algorithm + '\n' +
+#   Timestamp + '\n' +
+#   Scope + '\n' +
+#   HEX(Hash(CanonicalRequest))
+#
+sub create_string_to_sign
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  
+  my $canonical_request = $self->create_canonical_request();
+
+  my $string_to_sign = $ALGORITHM_NAME . "\n";
+  $string_to_sign .= $self->{'timestamp'} . "\n";
+  $string_to_sign .= $self->{'scope'} . "\n";
+  $string_to_sign .= sha256_hex($canonical_request);
+  
+  $self->{'string-to-sign'} = $string_to_sign;
+  
+  return $string_to_sign;
+}
+
+#
+# Performs the actual signing of the request.
+#
+sub create_signature
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  
+  my $ksecret = $opts->{'aws-secret-key'};
+  my $kdate = hmac_sha256($self->{'daystamp'}, 'AWS4' . $ksecret);
+  my $kregion = hmac_sha256($self->{'region'}, $kdate);
+  my $kservice = hmac_sha256($self->{'service'}, $kregion);
+  my $kcreds = hmac_sha256('aws4_request', $kservice);
+  
+  my $string_to_sign = $self->create_string_to_sign();
+  my $signature = hmac_sha256_hex($string_to_sign, $kcreds);
+  $self->{'signature'} = $signature;
+
+  return $signature;
+}
+
+#
+# Populates HTTP header that carries authentication data.
+#
+sub create_authz_header
+{
+  my $self = shift;
+  my $opts = $self->{opts};
+  
+  my $credential = $self->{'credential'};
+  my $signed_headers = $self->{'signed-headers'};
+  my $signature = $self->{'signature'};
+
+  my $authorization =
+    "$ALGORITHM_NAME Credential=$credential, ".
+    "SignedHeaders=$signed_headers, ".
+    "Signature=$signature";
+  
+  my $headers = $self->{'headers'};
+  $headers->{'Authorization'} = $authorization;
+  
+  return 1;
+}
+
+1;
diff --git a/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
new file mode 100644
index 0000000..effa5df
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/CloudWatchClient.pm
@@ -0,0 +1,834 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not 
+# use this file except in compliance with the License. A copy of the License 
+# is located at
+#
+#        http://aws.amazon.com/apache2.0/
+#
+# or in the "LICENSE" file accompanying this file. This file is distributed 
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
+# express or implied. See the License for the specific language governing 
+# permissions and limitations under the License.
+
+package CloudWatchClient;
+
+use strict;
+use warnings;
+use base 'Exporter';
+our @EXPORT = qw();
+use File::Basename;
+use AwsSignatureV4;
+use DateTime;
+use Digest::SHA qw(hmac_sha256_base64);
+use URI::Escape qw(uri_escape_utf8);
+use Compress::Zlib;
+use LWP 6;
+
+use LWP::Simple qw($ua get);
+$ua->timeout(2); # timeout for meta-data calls
+
+our %version_prefix_map = (
+  '2010-08-01' => ['GraniteServiceVersion20100801', 'com.amazonaws.cloudwatch.v2010_08_01#']
+);
+
+our %supported_actions = (
+  'DescribeTags' => 1,
+  'PutMetricData' => 1,
+  'GetMetricStatistics' => 1,
+  'ListMetrics' => 1
+);
+
+our %numeric_parameters = (
+  'Timestamp' => 'Timestamp',
+  'RawValue' => 'Value',
+  'StartTime' => 'StartTime',
+  'EndTime' => 'EndTime',
+  'Period' => 'Period'
+);
+
+our %region_suffix_map = (
+    'cn-north-1' => '.cn',
+    'cn-northwest-1' => '.cn'
+);
+
+use constant {
+  DO_NOT_CACHE => 0,
+  USE_CACHE => 1,
+};
+
+use constant {
+  OK => 1,
+  ERROR => 0,
+};
+
+our $client_version = '1.2.0';
+our $service_version = '2010-08-01';
+our $compress_threshold_bytes = 2048;
+our $meta_data_short_ttl = 21600; # 6 hours
+our $meta_data_long_ttl = 86400; # 1 day
+our $http_request_timeout = 5; # seconds
+
+# RFC3986 unsafe characters
+our $unsafe_characters = "^A-Za-z0-9\-\._~";
+
+our $region;
+our $avail_zone;
+our $instance_id;
+our $instance_type;
+our $image_id;
+our $as_group_name; 	 
+our $meta_data_loc = '/var/tmp/aws-mon';
+
+#
+# Queries meta data for the current EC2 instance.
+#
+sub get_meta_data
+{
+  my $resource = shift;
+  my $use_cache = shift;
+  my $meta_data = read_meta_data($resource, $meta_data_short_ttl);
+
+  my $base_uri = 'http://169.254.169.254/latest/meta-data';
+  my $data_value = !$meta_data ? get $base_uri.$resource : $meta_data;
+
+  if (!$data_value) {
+    return "";
+  }
+
+  if ($use_cache) {
+    write_meta_data($resource, $data_value);
+  }
+
+  return $data_value;
+}
+
+#
+# Reads meta-data from the local filesystem.
+#
+sub read_meta_data
+{
+  my $resource = shift;
+  my $default_ttl = shift;
+
+  my $location = $ENV{'AWS_EC2CW_META_DATA'};
+  if (!$location) { 	 
+    $location = $meta_data_loc if ($meta_data_loc); 	 
+  }
+  my $meta_data_ttl = $ENV{'AWS_EC2CW_META_DATA_TTL'};
+  $meta_data_ttl = $default_ttl if (!defined($meta_data_ttl));
+
+  my $data_value;
+  if ($location)
+  {
+    my $filename = $location.$resource;
+    if (-d $filename) {
+      $data_value = `/bin/ls $filename`;
+      chomp($data_value);
+    } elsif (-e $filename) {
+      my $updated = (stat($filename))[9];
+      my $file_age = time() - $updated;
+      if ($file_age < $meta_data_ttl)
+      {
+        open MDATA, "$filename";
+        while(my $line = <MDATA>) {
+          $data_value .= $line;
+        }
+        close MDATA;
+        chomp $data_value;
+      }
+    }
+  }
+
+  return $data_value;
+}
+
+#
+# Writes meta-data to the local filesystem. 	 
+# 	 
+sub write_meta_data 	 
+{ 	 
+  my $resource = shift; 	 
+  my $data_value = shift; 	 
+
+  if ($resource && $data_value) 	 
+  { 	 
+    my $location = $ENV{'AWS_EC2CW_META_DATA'}; 	 
+    if (!$location) { 	 
+      $location = $meta_data_loc if ($meta_data_loc); 	 
+    } 	 
+
+    if ($location) 	 
+    { 	 
+      my $filename = $location.$resource; 	 
+      my $directory = dirname($filename); 	 
+      `/bin/mkdir -p $directory` unless -d $directory; 	 
+
+      open MDATA, ">$filename"; 	 
+      print MDATA $data_value; 	 
+      close MDATA; 	 
+    } 	 
+  } 	 
+}
+
+#
+# Builds up ec2 endpoint URL for this region.
+#
+sub get_ec2_endpoint
+{
+  my $region = get_region();
+  my $endpoint = "https://ec2.amazonaws.com";
+
+  if ($region) {
+    $endpoint = "https://ec2.$region.amazonaws.com"; 
+    if (exists $region_suffix_map{$region}) {
+      $endpoint .= $region_suffix_map{$region};
+    }
+  }
+
+  return $endpoint;
+}
+
+#
+# Obtains Auto Scaling group name by making EC2 API call.
+#
+sub get_auto_scaling_group
+{
+  if ($as_group_name) {
+    return (200, $as_group_name);
+  }
+
+  # Try to get AS group name from the local cache and avoid calling EC2 API for
+  # at least several hours. AS group name is not something that may changes but
+  # just in case it may change at some point, refresh the value from the tag.
+
+  my $resource = '/as-group-name';
+  $as_group_name = read_meta_data($resource, $meta_data_short_ttl);
+  if ($as_group_name) {
+    return (200, $as_group_name);
+  }
+
+  my $opts = shift;
+
+  my %ec2_opts = ();
+  $ec2_opts{'aws-credential-file'} = $opts->{'aws-credential-file'};
+  $ec2_opts{'aws-access-key-id'} = $opts->{'aws-access-key-id'};
+  $ec2_opts{'aws-secret-key'} = $opts->{'aws-secret-key'};
+  $ec2_opts{'short-response'} = 0;
+  $ec2_opts{'retries'} = 1;
+  $ec2_opts{'verbose'} = $opts->{'verbose'};
+  $ec2_opts{'verify'} = $opts->{'verify'};
+  $ec2_opts{'user-agent'} = $opts->{'user-agent'};
+  $ec2_opts{'version'} = '2011-12-15';
+  $ec2_opts{'url'} = get_ec2_endpoint();
+  $ec2_opts{'aws-iam-role'} = $opts->{'aws-iam-role'};
+
+  my %ec2_params = ();
+  $ec2_params{'Filter.1.Name'} = 'resource-id';
+  $ec2_params{'Filter.1.Value.1'} = get_instance_id();
+  $ec2_params{'Filter.2.Name'} = 'key';
+  $ec2_params{'Filter.2.Value.1'} = 'aws:autoscaling:groupName';
+
+  my $response = call_query('DescribeTags', \%ec2_params, \%ec2_opts);
+
+  my $pattern;
+  if ($response->code == 200)
+  {
+    $pattern = "<value>(.*?)<\/value>";
+    if ($response->content =~ /$pattern/s) {
+      $as_group_name = $1;
+      write_meta_data($resource, $as_group_name);
+      return (200, $as_group_name);
+    }
+    $response->message(undef);
+  }
+
+  # In case when EC2 API call fails for whatever reason, keep using the older
+  # value if it is present. Only ofter one day, assume this value is obsolete.
+  # AS group name is not something that is changing on the fly anyway.
+
+  if (!$as_group_name)
+  {
+    # EC2 call failed, so try using older value for AS group name
+    $as_group_name = read_meta_data($resource, $meta_data_long_ttl);
+    if ($as_group_name) {
+      return (200, $as_group_name);
+    }
+  }
+
+  # Unable to obtain AutoScaling group name.
+  # Return the response code and error message
+  return ($response->code, $response->message);
+}
+
+#
+# Obtains EC2 instance id from meta data.
+#
+sub get_instance_id
+{
+  if (!$instance_id) {
+    $instance_id = get_meta_data('/instance-id', USE_CACHE);
+  }
+  return $instance_id;
+}
+
+#
+# Obtains EC2 instance type from meta data.
+#
+sub get_instance_type
+{
+  if (!$instance_type) {
+    $instance_type = get_meta_data('/instance-type', USE_CACHE);
+  }
+  return $instance_type;
+}
+
+#
+# Obtains EC2 image id from meta data.
+#
+sub get_image_id
+{
+  if (!$image_id) {
+    $image_id = get_meta_data('/ami-id', USE_CACHE);
+  }
+  return $image_id;
+}
+
+#
+# Obtains EC2 avilability zone from meta data.
+#
+sub get_avail_zone
+{
+  if (!$avail_zone) {
+    $avail_zone = get_meta_data('/placement/availability-zone', USE_CACHE);
+  }
+  return $avail_zone;
+}
+
+#
+# Extracts region from avilability zone.
+#
+sub get_region
+{
+  if (!$region) {
+    my $azone = get_avail_zone();
+    if ($azone) {
+      $region = substr($azone, 0, -1);
+    }
+  }
+  return $region;
+}
+
+#
+# Buids up the endpoint based on the provided region.
+#
+sub get_endpoint
+{
+  my $region = get_region();
+  my $endpoint = "https://monitoring.amazonaws.com";
+  
+  if ($region) {
+    $endpoint = "https://monitoring.$region.amazonaws.com";
+    if (exists $region_suffix_map{$region}) {
+      $endpoint .= $region_suffix_map{$region};
+    }
+  }
+  
+
+  return $endpoint;
+}
+
+#
+# Read credentials from the IAM Role Metadata.
+#
+sub prepare_iam_role
+{
+  my $opts = shift;
+  my $response = {};
+  my $verbose = $opts->{'verbose'};
+  my $outfile = $opts->{'output-file'};
+  my $iam_role = $opts->{'aws-iam-role'};
+  my $iam_dir = "/iam/security-credentials/";
+
+  # if am_role is not explicitly specified 
+  if (!defined($iam_role)) {
+    my $roles = get_meta_data($iam_dir, DO_NOT_CACHE);
+    my $nr_of_roles = $roles =~ tr/\n//;
+
+    print_out("No credential methods are specified. Trying default IAM role.", $outfile) if $verbose;
+    if ($roles eq "") {
+      return $response = {"code" => ERROR, "error" => "No IAM role is associated with this EC2 instance."};
+    } elsif ($nr_of_roles == 0) {
+      # if only one role
+      $iam_role = $roles;
+    } else {
+      $roles =~ s/\n/, /g; # puts all the roles on one line 
+      $roles =~ s/, $// ; # deletes the comma at the end
+      return {"code" => ERROR, "error" => "More than one IAM roles are associated with this EC2 instance: $roles."};
+    }
+  }
+
+  my $role_content = get_meta_data(($iam_dir . $iam_role), DO_NOT_CACHE);
+
+  # Could not find the IAM role metadata
+  if(!$role_content) {
+    my $roles = get_meta_data($iam_dir, DO_NOT_CACHE);
+    my $roles_message;
+    if($roles) {
+      $roles =~ s/\n/, /g; # puts all the roles on one line
+      $roles =~ s/, $// ; # deletes the comma at the end
+      $roles_message = "Available roles: " . $roles;
+    } else {
+      $roles_message = "This EC2 instance does not have an IAM role associated with it.";
+    }
+    return {"code" => ERROR, "error" => "Failed to obtain credentials for IAM role $iam_role. $roles_message"};
+  }
+
+  print_out("Using IAM role <$iam_role>", $outfile) if $verbose;
+  my $id;
+  my $key;
+  my $token;
+  while ($role_content =~ /(.*)\n/g ) {
+    my $line = $1;
+    if ( $line =~ /"AccessKeyId"[ \t]*:[ \t]*"(.+)"/) {
+      $id = $1;
+      next;
+    }
+    if ( $line =~ /"SecretAccessKey"[ \t]*:[ \t]*"(.+)"/) {
+      $key = $1;
+      next;
+    }
+    if ( $line =~ /"Token"[ \t]*:[ \t]*"(.+)"/) {
+      $token = $1;
+      next;
+    }
+  }
+
+  my $role_statement = "from IAM role <$iam_role>";
+  if (!defined($id) && !defined($key)) {
+    return {"code" => ERROR, "error" => "Failed to parse AWS access key id and secret key $role_statement."};
+  } elsif (!defined($id)) {
+    return {"code" => ERROR, "error" => "Failed to parse AWS access key id $role_statement."};
+  } elsif (!defined($key)) {
+    return {"code" => ERROR, "error" => "Failed to parse AWS secret key $role_statement."};
+  }
+
+  $opts->{'aws-access-key-id'} = $id;
+  $opts->{'aws-secret-key'} = $key;
+  $opts->{'aws-security-token'} = $token;
+  
+  return {"code" => OK};
+}
+
+#
+# Checks if credential set is present. If not, reads credentials from file.
+#
+sub prepare_credentials
+{
+  my $opts = shift;
+  my $verbose = $opts->{'verbose'};
+  my $outfile = $opts->{'output-file'};
+  my $aws_access_key_id = $opts->{'aws-access-key-id'};
+  my $aws_secret_key = $opts->{'aws-secret-key'};
+  my $aws_credential_file = $opts->{'aws-credential-file'};
+
+  if (defined($aws_access_key_id) && !$aws_access_key_id) {
+    return {"code" => ERROR, "error" => "Provided empty AWS access key id."};
+  }
+  if (defined($aws_secret_key) && !$aws_secret_key) {
+    return {"code" => ERROR, "error" => "Provided empty AWS secret key."};
+  }  
+  if ($aws_access_key_id && $aws_secret_key) {
+    return {"code" => OK};
+  }
+
+  if (!$aws_credential_file) {
+    my $env_creds_file = $ENV{'AWS_CREDENTIAL_FILE'};
+    if (defined($env_creds_file) && length($env_creds_file) > 0) {
+      $aws_credential_file = $env_creds_file;
+    }
+  }
+
+  if (!$aws_credential_file) {
+    my $conf_file = &File::Basename::dirname($0) . '/awscreds.conf' ;
+    if (-e $conf_file) {
+      $aws_credential_file = $conf_file;
+    }
+  }
+
+  if ($aws_credential_file) {
+    my $file = $aws_credential_file;
+    open(FILE, '<:utf8', $file) or return {"code" => ERROR, "error" => "Failed to open AWS credentials file <$file>"};
+    print_out("Using AWS credentials file <$aws_credential_file>", $outfile) if $verbose;
+
+    while (my $line = <FILE>)
+    {
+      $line =~ /^$/ and next; # skip empty lines
+      $line =~ /^#.*/ and next; # skip commented lines
+      $line =~ /^\s*(.*?)=(.*?)\s*$/ or return {"code" => ERROR, "error" => "Failed to parse AWS credential entry '$line' in <$file>."};
+      my ($key, $value) = ($1, $2);
+      if ($key eq 'AWSAccessKeyId') {
+        $opts->{'aws-access-key-id'} = $value;
+      } elsif ($key eq 'AWSSecretKey') {
+        $opts->{'aws-secret-key'} = $value;
+      }
+    }
+    close (FILE);
+  }
+
+  $aws_access_key_id = $opts->{'aws-access-key-id'};
+  $aws_secret_key = $opts->{'aws-secret-key'};
+
+  if (!$aws_access_key_id || !$aws_secret_key) {
+    # if all the credential methods failed, try iam_role
+    # either the default or user specified IAM role
+    return prepare_iam_role($opts);
+  }
+
+  return {"code" => OK};
+}
+
+#
+# Retrieves the current UTC time minus the offset (in hours).
+#
+sub get_offset_time
+{
+  my $offset = shift;
+  my $dt = DateTime->now();
+  $dt->subtract(hours => $offset);
+  return $dt->epoch;
+}
+
+#
+# Prints out diagnostic message to a file or standard output.
+#
+sub print_out
+{
+  my $text = shift;
+  my $filename = shift;
+  
+  if ($filename) {
+    open OUT_STREAM, ">>$filename";
+    print OUT_STREAM "$text\n";
+    close OUT_STREAM;
+  }
+  else {
+    print "$text\n";
+  }
+}
+
+#
+# Retrieves the interface and type prefixes for the version and action supplied 
+# e.g. 2010-08-01 => [GraniteService20100801, com.amazonaws.cloudwatch.v2010_08_01#]
+#
+sub get_interface_version_and_type
+{
+  my $params = shift;
+  my $version = $params->{'Version'};
+
+  if (!(defined($version))) {
+    $version = $service_version;
+  }
+  if (!(exists $version_prefix_map{$version})) {
+    return {"code" => ERROR, "error" => 'Unsupported version'};
+  }
+
+  return {"code" => OK, "version" => $version_prefix_map{$version}[0], "type" => $version_prefix_map{$version}[1]};
+}
+
+#
+# Creates a key-value pair string to get added to the JSON payload.
+#
+sub add_simple_parameter
+{
+  my $param_name = shift;
+  my $value = shift;
+
+  my $json_data = '';
+  if (exists $numeric_parameters{$param_name}) {
+    my $key = $numeric_parameters{$param_name};
+    $json_data = qq("$key":$value,);
+  }
+  else {
+    $json_data = qq("$param_name":"$value",);
+  }
+
+  return $json_data;
+} 
+
+#
+# Iterates through hash entries and adds them to the JSON payload.
+#
+sub add_hash
+{
+  my $param_name = shift;
+  my $hash_ref = shift;
+
+  my $json_data = (($param_name eq '')? $param_name : qq("$param_name":)) . "{";
+  while (my ($key, $value) = each %{$hash_ref})
+  {
+    if (ref $value eq 'ARRAY') {
+      $json_data .= add_array($key, $value) . ",";
+    }
+    else {
+      $json_data .= add_simple_parameter($key, $value);
+    }
+  }
+  chop($json_data) unless ((keys %$hash_ref) == 0);
+  $json_data .= "}";
+
+  return $json_data;
+}
+
+#
+# Iterates through array entries and adds them to the JSON payload.
+#
+sub add_array
+{
+  my $param_name = shift;
+  my $array_ref = shift;
+
+  my $json_data = (($param_name eq '')? $param_name : qq("$param_name":)) . "[";
+  for my $array_val (@{$array_ref})
+  {
+    if (ref $array_val eq 'HASH') {
+      $json_data .= add_hash('', $array_val) . ",";
+    } else {
+      $json_data .= qq("$array_val",);
+    }
+  }
+  chop($json_data) unless (scalar @$array_ref == 0);
+  $json_data .= "]";
+
+  return $json_data;
+}
+
+#
+# Builds a JSON payload from the request parameters.
+#
+sub construct_payload
+{
+  my $params = shift;
+  my $json_data = add_hash("", $params->{'Input'});
+  return $json_data;
+}
+
+#
+# Prepares SigV4 request headers and JSON payload for the HTTP request.
+#
+sub get_json_payload_and_headers
+{
+  my $params = shift;
+  my $opts = shift;
+
+  my $operation = $params->{'Operation'};
+  my $json_data = construct_payload($params);
+
+  my $sigv4 = AwsSignatureV4->new_aws_json($operation, $json_data, $opts);
+  if (!($sigv4->sign_http_post())) {
+    return {"code" => ERROR, "error" => $sigv4->error};
+  }
+
+  return {"code" => OK, "payload" => $json_data, "headers" => $sigv4->headers};
+}
+
+#
+# Shared call setup used for both AWS/JSON and AWS/Query HTTP requests.
+#
+sub call_setup
+{
+  my $params = shift;
+  my $opts = shift;
+  my $validation_contents;
+  
+  $opts->{'http-method'} = 'POST';
+
+  if (!defined($opts->{'url'})) {
+    $opts->{'url'} = get_endpoint();
+  }
+
+  if (!defined($opts->{'version'})) {
+    $opts->{'version'} = $service_version;
+  }
+  $params->{'Version'} = $opts->{'version'};
+
+  if (!defined($opts->{'user-agent'})) {
+    $opts->{'user-agent'} = "CloudWatch-Scripting/$client_version";
+  }
+
+  return prepare_credentials($opts);
+}
+
+#
+# Helper method used by both call_json and call_query.
+# Configures and sends the HTTP request and passes result back to caller.
+#
+sub call
+{
+  my $payload = shift;
+  my $headers = shift;
+  my $opts = shift;
+  my $failure_pattern = shift;
+
+  my $user_agent = new LWP::UserAgent(agent => $opts->{'user-agent}'});
+  $user_agent->timeout($http_request_timeout);
+
+  my $http_headers = HTTP::Headers->new(%$headers);
+  my $request = new HTTP::Request $opts->{'http-method'}, $opts->{'url'}, $http_headers, $payload;
+  
+  if (defined($opts->{'enable-compression'}) && length($payload) > $compress_threshold_bytes) {
+    $request->encode('gzip');
+  }
+
+  my $response;
+  my $keep_trying = 1;
+  my $call_attempts = 1;
+  my $endpoint = $opts->{'url'};
+  my $verbose = $opts->{'verbose'};
+  my $outfile = $opts->{'output-file'};
+
+  print_out("Endpoint: $endpoint", $outfile) if $verbose;
+  print_out("Payload: $payload", $outfile) if $verbose;
+
+  # initial and max delay in seconds between retries
+  my $delay = 4; 
+  my $max_delay = 16;
+
+  if (defined($opts->{'retries'})) {
+    $call_attempts += $opts->{'retries'};
+  }  
+  if (defined($opts->{'max-backoff-sec'})) {
+    $max_delay = $opts->{'max-backoff-sec'};
+  }
+
+  my $response_code = 0;
+
+  if ($opts->{'verify'}) {
+    return (HTTP::Response->new(200, 'This is a verification run, not an actual response.'));
+  }
+
+  for (my $i = 0; $i < $call_attempts && $keep_trying; ++$i)
+  {
+    my $attempt = $i + 1;
+    $response = $user_agent->request($request);
+    $response_code = $response->code;
+    if ($verbose) {
+      print_out("Received HTTP status $response_code on attempt $attempt", $outfile);
+      if ($response_code != 200) {
+        print_out($response->content, $outfile);
+      }
+    }
+    $keep_trying = 0;
+    if ($response_code >= 500) {
+      $keep_trying = 1;
+    } elsif ($response_code == 400) {
+      # special case to handle throttling fault
+      my $pattern = "Throttling";
+      if ($response->content =~ m/$pattern/) {
+        print_out("Request throttled.", $outfile) if $verbose;
+        $keep_trying = 1;
+      }
+    }
+    if ($keep_trying && $attempt < $call_attempts) {
+      print_out("Waiting $delay seconds before next retry.", $outfile) if $verbose;
+      sleep($delay);
+      my $incdelay = $delay * 2;
+      $delay = $incdelay > $max_delay ? $max_delay : $incdelay;
+    }
+  }
+
+  print_out($response->content, $outfile) if ($verbose && $response_code == 200);
+
+  if (!$response->is_success) {
+    if ($response->content =~ /$failure_pattern/s) {
+      $response->message($1);
+    } elsif ($response->content =~ m/__type.*?#(.*?)"}/) {
+      $response->message($1);
+    } else {
+      $response->message($response->content);
+    }
+  }
+
+  return $response;
+}
+ 
+#
+# Makes a remote invocation to the CloudWatch service using the AWS/Query format.
+# Returns request ID, if successful, or error message if unsuccessful.
+#
+sub call_query
+{
+  my $operation = shift;
+  my $params = shift;
+  my $opts = shift;
+  my $validation_contents;
+  my $payload;
+  my $headers = {};
+  my $failure_pattern = "<Message>(.*?)<\/Message>";
+  $params->{'Action'} = $operation;
+
+  $validation_contents = call_setup($params, $opts);
+
+  if ($validation_contents->{"code"} == ERROR) {
+    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
+  }
+
+  my $sigv4 = AwsSignatureV4->new_aws_query($params, $opts);
+
+  if (!$sigv4->sign_http_post()) {
+    return (HTTP::Response->new(400, $sigv4->{'error'}));
+  }
+
+  $payload = $sigv4->{'payload'};
+  $headers = $sigv4->{'headers'};
+
+  return call($payload, $headers, $opts, $failure_pattern);
+}
+
+
+#
+# Makes a remote invocation to the CloudWatch service using the AWS/JSON format.
+# Returns the full response if successful, or error message if unsuccessful.
+#
+sub call_json
+{
+  my $operation = shift;
+  my $params = shift;
+  my $opts = shift;
+  my $validation_contents;
+  my $payload;
+  my $headers = {};
+  my $failure_pattern =  "\"message\":\"(.*?)\"";
+
+  $validation_contents = call_setup($params, $opts);
+
+  if ($validation_contents->{"code"} == ERROR) {
+    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
+  }
+
+  if(!(exists $supported_actions{$operation})) {
+    return(HTTP::Response->new(ERROR, 'Unsupported Operation'));
+  }
+
+  $validation_contents = get_interface_version_and_type($params);
+
+  if ($validation_contents->{"code"} == ERROR) {
+    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
+  }
+
+  $params->{'Operation'} = $validation_contents->{"version"} . "." . $operation;
+  $params->{'Input'}->{'__type'} = $validation_contents->{"type"} . $operation . "Input";
+
+  $validation_contents = get_json_payload_and_headers($params, $opts);
+
+  if ($validation_contents->{"code"} == ERROR) {
+    return (HTTP::Response->new($validation_contents->{"code"}, $validation_contents->{"error"}));
+  }
+
+  $payload = $validation_contents->{"payload"};
+  $headers = $validation_contents->{"headers"};
+
+  return call($payload, $headers, $opts, $failure_pattern);
+}
+
+1;
commit 5bcd164f80ee47162ae927b3a5fcb1c6fd2798db
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri May 7 15:57:35 2021 -0500

    Add check option and load check modules
    
    This commit adds the ability to load and run check modules.
    The check modules can be added as children of Check.pm. The check
    modules will inherit Check.pm's methods and constants for
    running commands and returning data.
    
    Instructions and examples for expected methods and return data
    structure have been added to the documentation of the parent
    module, Check.pm.

diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
index 22726e8..291c5ee 100755
--- a/bin/aws-cloudwatch-monitor
+++ b/bin/aws-cloudwatch-monitor
@@ -11,16 +11,19 @@ my $VERSION = '0.01';
 
 Getopt::Long::GetOptions(
     \my %opt,
+    'check=s@',
     'version' => sub { print "aws-cloudwatch-monitor version $VERSION\n"; exit 0 },
     'help',
 ) or Pod::Usage::pod2usage( -exitval => 1 );
 
 Pod::Usage::pod2usage( -exitval => 0 ) if ( $opt{help} );
+Pod::Usage::pod2usage( -message => 'Option check is required', -exitval => 1 ) unless ( $opt{check} );
 
 delete $opt{version};
 delete $opt{help};
 
 my $monitor = App::AWS::CloudWatch::Monitor->new();
+$monitor->run(\%opt);
 
 exit 0;
 
@@ -30,20 +33,27 @@ __END__
 
 =head1 NAME
 
-aws-cloudwatch-monitor -
+aws-cloudwatch-monitor - collect and send metrics to AWS CloudWatch
 
 =head1 SYNOPSIS
 
- aws-cloudwatch-monitor [--version] [--help]
+ aws-cloudwatch-monitor [--check <module>]
+                        [--version] [--help]
 
 =head1 DESCRIPTION
 
-C<aws-cloudwatch-monitor>
+C<aws-cloudwatch-monitor> collects and sends custom metrics to AWS CloudWatch from an AWS EC2 instance.
 
 =head1 OPTIONS
 
 =over
 
+=item --check <module>
+
+Defines the checks to run.
+
+Multiple C<--check> options may be defined and are run in the order they're passed.
+
 =item --version
 
 Print the version.
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
index 24f0da5..6c73b8d 100644
--- a/lib/App/AWS/CloudWatch/Monitor.pm
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -4,6 +4,8 @@ use strict;
 use warnings;
 
 use App::AWS::CloudWatch::Monitor::Config;
+use Try::Tiny;
+use Module::Loader;
 
 our $VERSION = '0.01';
 
@@ -23,6 +25,30 @@ sub config {
     return $config;
 }
 
+sub run {
+    my $self = shift;
+    my $opt  = shift;
+
+    my $loader = Module::Loader->new;
+
+    my @metrics;
+    foreach my $module ( @{ $opt->{check} } ) {
+        my $class = q{App::AWS::CloudWatch::Monitor::Check::} . $module;
+        try {
+            $loader->load($class);
+        }
+        catch {
+            my $exception = $_;
+            die "$exception\n";
+        };
+
+        my $plugin = $class->new();
+        my $metric = $plugin->check();
+    }
+
+    return;
+}
+
 1;
 
 __END__
@@ -31,7 +57,7 @@ __END__
 
 =head1 NAME
 
-App::AWS::CloudWatch::Monitor -
+App::AWS::CloudWatch::Monitor - collect and send metrics to AWS CloudWatch
 
 =head1 SYNOPSIS
 
@@ -41,7 +67,7 @@ App::AWS::CloudWatch::Monitor -
 
 =head1 DESCRIPTION
 
-C<App::AWS::CloudWatch::Monitor>
+C<App::AWS::CloudWatch::Monitor> collects and sends custom metrics to AWS CloudWatch from an AWS EC2 instance.
 
 =head1 CONSTRUCTOR
 
@@ -61,6 +87,10 @@ Returns a new C<App::AWS::CloudWatch::Monitor> object.
 
 Returns the loaded config.
 
+=item run
+
+Loads and runs the specified check modules to gather metric data.
+
 =back
 
 =cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Check.pm b/lib/App/AWS/CloudWatch/Monitor/Check.pm
new file mode 100644
index 0000000..cf5d4d1
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Check.pm
@@ -0,0 +1,152 @@
+package App::AWS::CloudWatch::Monitor::Check;
+
+use strict;
+use warnings;
+
+use Capture::Tiny;
+
+our $VERSION = '0.01';
+
+sub new {
+    my $class = shift;
+    my $self  = {};
+
+    return bless $self, $class;
+}
+
+sub run_command {
+    my $self    = shift;
+    my $command = shift;
+
+    my ( $stdout, $stderr, $exit ) = Capture::Tiny::capture {
+        system( @{$command} );
+    };
+
+    chomp($stderr);
+
+    return ( $exit, [ split( /\n/, $stdout ) ], $stderr );
+}
+
+sub read_file {
+    my $self     = shift;
+    my $filename = shift;
+
+    open( my $fh, '<', $filename )
+        or return ( 0, "open $filename: $!" );
+
+    my $contents = [];
+    while ( my $line = <$fh> ) {
+        chomp $line;
+        push @{$contents}, $line;
+    }
+    close($fh);
+
+    return $contents;
+}
+
+use constant UNITS => (
+    BYTE => 1,
+    KILO => 1024,
+    MEGA => 1048576,
+    GIGA => 1073741824,
+);
+
+sub constants {
+    my $self = shift;
+
+    return { UNITS() };
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Check - parent for Check modules
+
+=head1 SYNOPSIS
+
+ use parent 'App::AWS::CloudWatch::Monitor::Check';
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Check> provides a contructor and methods for child modules.
+
+This module is not meant to be initialized directly, but through child modules.
+
+=head1 ADDING CHECK MODULES
+
+Additional check modules can be added as child modules to C<App::AWS::CloudWatch::Monitor::Check>.
+
+Child modules must implement the C<check> method which gathers, formats, and returns the metric.
+
+The returned metric must be a hashref with keys C<MetricName>, C<Unit>, and C<RawValue>.
+
+The returned metric hashref may contain a C<Dimensions> key, but its value must be an arrayref containing hashrefs with the keys C<Name> and C<Value>.
+
+ # example MemoryUtilization check return without Dimensions data
+ my $metric = {
+     MetricName => 'MemoryUtilization',
+     Unit       => 'Percent',
+     RawValue   => $mem_util,
+ };
+
+ # example DiskSpaceUtilization check return with Dimensions data
+ my $metric = {
+     MetricName => 'DiskSpaceUtilization',
+     Unit       => 'Percent',
+     Value      => $disk_space_util,
+     Dimensions => [
+         {
+             Name  => 'Filesystem',
+             Value => $filesystem,
+         },
+         {
+             Name  => 'MountPath',
+             Value => $mount_path,
+         },
+     ],
+ };
+
+=head1 CONSTRUCTOR
+
+=over
+
+=item new
+
+Returns the C<App::AWS::CloudWatch::Monitor::Check> object.
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item run_command
+
+Runs the specified command and returns a list with three members consisting of:
+
+=over
+
+=item C<exit code> returned from the system command
+
+=item output from C<STDOUT>
+
+=item output from C<STDERR>
+
+=back
+
+C<STDOUT> is split by newline and returned as an arrayref.
+
+=item read_file
+
+Reads the specified file and returns an arrayref of the content.
+
+=item constants
+
+Returns the bytes constants for use in unit conversion.
+
+=back
commit 5a2afc562304e1f1a67161dd705b77f295a95c82
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue May 4 13:23:57 2021 -0500

    Add Test module and pod and config tests

diff --git a/.gitignore b/.gitignore
index ff12186..bb9c923 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,7 @@ Makefile
 Makefile.old
 App-AWS-CloudWatch-Mon-*
 pm_to_blib
+996_perl-tidy.t
+997_perl-critic.t
+perltidyrc
+perlcriticrc
diff --git a/t/.aws-cloudwatch-monitor-rc b/t/.aws-cloudwatch-monitor-rc
new file mode 100644
index 0000000..6ff74b7
--- /dev/null
+++ b/t/.aws-cloudwatch-monitor-rc
@@ -0,0 +1,3 @@
+[aws]
+aws_access_key_id = test
+aws_secret_access_key = test
diff --git a/t/00_load.t b/t/00_load.t
new file mode 100644
index 0000000..cc2f5f4
--- /dev/null
+++ b/t/00_load.t
@@ -0,0 +1,38 @@
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+use File::Find ();
+use File::Spec ();
+
+foreach my $module (find_all_perl_modules()) {
+    use_ok($module) or BAIL_OUT;
+}
+
+done_testing();
+
+sub find_all_perl_modules {
+    my $base = "$FindBin::RealBin/../";
+
+    my @modules;
+    File::Find::find(
+        sub {
+            my $file = $File::Find::name;
+            return unless $file =~ /\.pm$/;
+            return if $file =~ /Test\.pm$/;
+
+            my $rel_path = File::Spec->abs2rel( $file, $base );
+            $rel_path =~ s/^[t\/]*lib\///;
+            $rel_path =~ s/\//::/g;
+            $rel_path =~ s/\.pm$//;
+
+            push( @modules, $rel_path );
+        },
+        $base,
+    );
+
+    return @modules;
+}
diff --git a/t/998_pod-checker.t b/t/998_pod-checker.t
new file mode 100644
index 0000000..503ec4d
--- /dev/null
+++ b/t/998_pod-checker.t
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+use FindBin;
+use Test::More;
+
+unless ( $ENV{TEST_AUTHOR} ) {
+    my $msg = 'Author test. Set $ENV{TEST_AUTHOR} to a true value to run.';
+    plan( skip_all => $msg );
+}
+
+eval { require Test::Pod; };
+
+if ($@) {
+    my $msg = 'Test::Pod required to criticise code';
+    plan( skip_all => $msg );
+}
+
+Test::Pod::all_pod_files_ok( Test::Pod::all_pod_files("$FindBin::RealBin/../lib") );
diff --git a/t/999_pod-coverage.t b/t/999_pod-coverage.t
new file mode 100644
index 0000000..77ab714
--- /dev/null
+++ b/t/999_pod-coverage.t
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+unless ( $ENV{TEST_AUTHOR} ) {
+    my $msg = 'Author test. Set $ENV{TEST_AUTHOR} to a true value to run.';
+    plan( skip_all => $msg );
+}
+
+eval { require Test::Pod::Coverage; };
+
+if ($@) {
+    my $msg = 'Test::Pod::Coverage required to criticise code';
+    plan( skip_all => $msg );
+}
+
+Test::Pod::Coverage::all_pod_coverage_ok();
diff --git a/t/config.t b/t/config.t
new file mode 100644
index 0000000..9c611b9
--- /dev/null
+++ b/t/config.t
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+
+use FindBin ();
+use lib "$FindBin::RealBin/../lib", "$FindBin::RealBin/lib";
+use App::AWS::CloudWatch::Monitor::Test;
+
+my $class = 'App::AWS::CloudWatch::Monitor::Config';
+use_ok($class);
+
+HAPPY_PATH: {
+    note( 'happy path' );
+    lives_ok { $class->load() } 'config loads and verifies';
+}
+
+done_testing();
diff --git a/t/lib/App/AWS/CloudWatch/Monitor/Test.pm b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
new file mode 100644
index 0000000..cc2c79d
--- /dev/null
+++ b/t/lib/App/AWS/CloudWatch/Monitor/Test.pm
@@ -0,0 +1,99 @@
+package App::AWS::CloudWatch::Monitor::Test;
+
+use strict;
+use warnings;
+
+use parent 'Test::More';
+
+our $VERSION = '0.01';
+
+sub import {
+    my $class = shift;
+    my %args  = @_;
+
+    if ( $args{tests} ) {
+        $class->builder->plan( tests => $args{tests} )
+            unless $args{tests} eq 'no_declare';
+    }
+    elsif ( $args{skip_all} ) {
+        $class->builder->plan( skip_all => $args{skip_all} );
+    }
+
+    # load the .aws-cloudwatch-monitor-rc file from t/ directory
+    require FindBin;
+    override(
+        package => 'File::HomeDir',
+        name    => 'my_home',
+        subref  => sub { $FindBin::RealBin },
+    );
+
+    Test::More->export_to_level(1);
+
+    require Test::Exception;
+    Test::Exception->export_to_level(1);
+
+    require Test::Warnings;
+
+    return;
+}
+
+sub override {
+    my %args = (
+        package => undef,
+        name    => undef,
+        subref  => undef,
+        @_,
+    );
+
+    eval "require $args{package}";
+
+    my $fullname = sprintf "%s::%s", $args{package}, $args{name};
+
+    no strict 'refs';
+    no warnings 'redefine', 'prototype';
+    *$fullname = $args{subref};
+
+    return;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Test - testing module for App::AWS::CloudWatch::Monitor
+
+=head1 SYNOPSIS
+
+ use App::AWS::CloudWatch::Monitor::Test;
+
+ ok($got eq $expected, $test_name);
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Test> sets up the testing environment and modules needed for tests.
+
+Methods from C<Test::More> and C<Test::Exception> are exported and available for the tests.
+
+=head1 SUBROUTINES
+
+=over
+
+=item override
+
+Overrides subroutines
+
+ARGS are C<package>, C<name>, and C<subref>.
+
+ App::AWS::CloudWatch::Monitor::Test::override(
+     package => 'Package::To::Override',
+     name    => 'subtooverride',
+     subref  => sub { return 'faked' },
+ );
+
+=back
+
+=cut
commit b09ea1c35851113c7a99bd37fb5b01eb87e9ea7a
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue May 4 12:33:09 2021 -0500

    Add initial framework
    
    This commit adds the start of the App::AWS::CloudWatch::Monitor
    framework including bin script, main package, and config loading.

diff --git a/.aws-cloudwatch-monitor-rc.example b/.aws-cloudwatch-monitor-rc.example
new file mode 100644
index 0000000..9a0035e
--- /dev/null
+++ b/.aws-cloudwatch-monitor-rc.example
@@ -0,0 +1,3 @@
+[aws]
+aws_access_key_id = example
+aws_secret_access_key = example
diff --git a/bin/aws-cloudwatch-monitor b/bin/aws-cloudwatch-monitor
new file mode 100755
index 0000000..22726e8
--- /dev/null
+++ b/bin/aws-cloudwatch-monitor
@@ -0,0 +1,57 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use Getopt::Long ();
+use Pod::Usage   ();
+use App::AWS::CloudWatch::Monitor;
+
+my $VERSION = '0.01';
+
+Getopt::Long::GetOptions(
+    \my %opt,
+    'version' => sub { print "aws-cloudwatch-monitor version $VERSION\n"; exit 0 },
+    'help',
+) or Pod::Usage::pod2usage( -exitval => 1 );
+
+Pod::Usage::pod2usage( -exitval => 0 ) if ( $opt{help} );
+
+delete $opt{version};
+delete $opt{help};
+
+my $monitor = App::AWS::CloudWatch::Monitor->new();
+
+exit 0;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+aws-cloudwatch-monitor -
+
+=head1 SYNOPSIS
+
+ aws-cloudwatch-monitor [--version] [--help]
+
+=head1 DESCRIPTION
+
+C<aws-cloudwatch-monitor>
+
+=head1 OPTIONS
+
+=over
+
+=item --version
+
+Print the version.
+
+=item --help
+
+Print the help menu.
+
+=back
+
+=cut
diff --git a/lib/App/AWS/CloudWatch/Monitor.pm b/lib/App/AWS/CloudWatch/Monitor.pm
new file mode 100644
index 0000000..24f0da5
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor.pm
@@ -0,0 +1,66 @@
+package App::AWS::CloudWatch::Monitor;
+
+use strict;
+use warnings;
+
+use App::AWS::CloudWatch::Monitor::Config;
+
+our $VERSION = '0.01';
+
+my $config;
+
+sub new {
+    my $class = shift;
+    my $self  = {};
+
+    $config = App::AWS::CloudWatch::Monitor::Config->load();
+
+    return bless $self, $class;
+}
+
+sub config {
+    my $self = shift;
+    return $config;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor -
+
+=head1 SYNOPSIS
+
+ use App::AWS::CloudWatch::Monitor;
+
+ my $monitor = App::AWS::CloudWatch::Monitor->new();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor>
+
+=head1 CONSTRUCTOR
+
+=over
+
+=item new
+
+Returns a new C<App::AWS::CloudWatch::Monitor> object.
+
+=back
+
+=head1 METHODS
+
+=over
+
+=item config
+
+Returns the loaded config.
+
+=back
+
+=cut
diff --git a/lib/App/AWS/CloudWatch/Monitor/Config.pm b/lib/App/AWS/CloudWatch/Monitor/Config.pm
new file mode 100644
index 0000000..7edd68d
--- /dev/null
+++ b/lib/App/AWS/CloudWatch/Monitor/Config.pm
@@ -0,0 +1,79 @@
+package App::AWS::CloudWatch::Monitor::Config;
+
+use strict;
+use warnings;
+
+use File::HomeDir;
+use Config::Tiny;
+
+our $VERSION = '0.01';
+
+sub load {
+    my $config = _load_and_verify();
+
+    return $config;
+}
+
+sub _load_and_verify {
+    my $rc = File::HomeDir->my_home . '/.aws-cloudwatch-monitor-rc';
+
+    unless ( -e $rc && -r $rc ) {
+        die "$rc does not exist or cannot be read\n";
+    }
+
+    my $config = Config::Tiny->read($rc);
+
+    foreach my $required (qw{ aws }) {
+        unless ( defined $config->{$required} ) {
+            die "$required section in $rc is not defined\n";
+        }
+    }
+
+    foreach my $required (qw{ aws_access_key_id aws_secret_access_key }) {
+        unless ( defined $config->{aws}{$required} ) {
+            die "$required key for aws section in $rc is not defined\n";
+        }
+    }
+
+    return $config;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+App::AWS::CloudWatch::Monitor::Config - load and verify the config
+
+=head1 SYNOPSIS
+
+ use App::AWS::CloudWatch::Monitor::Config;
+
+ my $config = App::AWS::CloudWatch::Monitor::Config->load();
+
+=head1 DESCRIPTION
+
+C<App::AWS::CloudWatch::Monitor::Config> loads settings for C<App::AWS::CloudWatch::Monitor>.
+
+=head1 SUBROUTINES
+
+=over
+
+=item load
+
+Load and verify the config.
+
+=back
+
+=head1 CONFIGURATION
+
+The configuration file is loaded from the running user's home directory.
+
+An example config, C<.aws-cloudwatch-monitor-rc.example>, is provided in the project root directory.
+
+To set up the config, copy C<.aws-cloudwatch-monitor-rc.example> into the running user's home directory, then update the values accordingly.
+
+=cut
commit a381030e7724cd9f05398d2f042a29eea52edd92
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue May 4 10:38:03 2021 -0500

    Add initial gitignore and README

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff12186
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+MYMETA.*
+blib/
+/nytprof.out*
+/.prove
+*~
+*.swp
+*.swo
+cover_db/
+Makefile
+Makefile.old
+App-AWS-CloudWatch-Mon-*
+pm_to_blib
diff --git a/README b/README
new file mode 100644
index 0000000..519cb06
--- /dev/null
+++ b/README
@@ -0,0 +1,2 @@
+# App::AWS::CloudWatch::Monitor
+
-----------------------------------------------------------------------


hooks/post-receive
-- 
app-aws-cloudwatch-monitor


More information about the Bps-public-commit mailing list