Repository: cloud-custodian/cloud-custodian

Issue:
aws - member-role againt metrics on central account
Hello,

Coming back again with the policy to detect S3 buckets from central account to many account based on event bus and using the member-role.  :)
I activate the generation of metrics on it, but metrics are created on distant account and not on central account as I would like.

Maybe I'm missing something, is there any chance to have a configuration to allow to have this metric on central account?

Here my configuration:
```
policies:
  - name: S3Public
    comment: "Detection  S3 public"
    mode:
          timeout: 200
          type: cloudtrail
          events:
              - source: s3.amazonaws.com
                ids: requestParameters.bucketName
                event: CreateBucket
              - source: s3.amazonaws.com
                ids: requestParameters.bucketName
                event: PutBucketAcl
          function-prefix: lambda-custod-
          role: arn:aws:iam::xxxxxxxx:role/role-cloudcustodianmgt
          member-role: arn:aws:iam::{account_id}:role/role-cloudcustodian
          execution-options:
            metrics_enabled: true
    description: |
        Cloud Custodian Policy - Based on bus event, catch S3 ACL Bucket update and remove public access.
    resource: s3
    filters:
       - type: global-grants
    actions:
        - type: delete-global-grants
          grantees:
            - "http://acs.amazonaws.com/groups/global/AllUsers"
            - "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
        - type: notify
          to:
            - arn...
          template: template
          transport:
            type: sqs
            queue: queue...
```

Thanks for your support,
Regards,



Discussion:
there's a refactoring of outputs in general in an extant pr, that will make this and other customizations easier, but at the moment metrics are always sent to the target account that the policy executed against. part of that is on a metrics basis there is no way to distinguish accounts with the default metrics dimensions if collecting centrally. there's several tools in `tools` directory that will aggregate the target/distant account metrics into a time series db.

Resolving patch:
diff --git a/c7n/output.py b/c7n/output.py
--- a/c7n/output.py
+++ b/c7n/output.py
@@ -33,6 +33,7 @@
 
 from c7n.exceptions import InvalidOutputConfig
 from c7n.registry import PluginRegistry
+from c7n.utils import parse_url_config
 
 try:
     import psutil
@@ -59,7 +60,7 @@ def select(self, selector, ctx):
                 self.default_protocol, selector)
         for k in self.keys():
             if selector.startswith(k):
-                return self[k](ctx, {'url': selector})
+                return self[k](ctx, parse_url_config(selector))
         raise InvalidOutputConfig("Invalid %s: %s" % (
             self.plugin_type,
             selector))
diff --git a/c7n/resources/aws.py b/c7n/resources/aws.py
--- a/c7n/resources/aws.py
+++ b/c7n/resources/aws.py
@@ -160,6 +160,10 @@ class MetricsOutput(Metrics):
     def __init__(self, ctx, config=None):
         super(MetricsOutput, self).__init__(ctx, config)
         self.namespace = self.config.get('namespace', DEFAULT_NAMESPACE)
+        self.region = self.config.get('region')
+        self.destination = (
+            self.config.scheme == 'aws' and
+            self.config.get('netloc') == 'master') and 'master' or None
 
     def _format_metric(self, key, value, unit, dimensions):
         d = {
@@ -171,11 +175,23 @@ def _format_metric(self, key, value, unit, dimensions):
             {"Name": "Policy", "Value": self.ctx.policy.name},
             {"Name": "ResType", "Value": self.ctx.policy.resource_type}]
         for k, v in dimensions.items():
+            # Skip legacy static dimensions if using new capabilities
+            if (self.destination or self.region) and k == 'Scope':
+                continue
             d['Dimensions'].append({"Name": k, "Value": v})
+        if self.region:
+            d['Dimensions'].append({'Name': 'Region', 'Value': self.ctx.region})
+        if self.destination:
+            d['Dimensions'].append({'Name': 'Account', 'Value': self.ctx.account_id or ''})
         return d
 
     def _put_metrics(self, ns, metrics):
-        watch = utils.local_session(self.ctx.session_factory).client('cloudwatch')
+        if self.destination == 'master':
+            watch = self.ctx.session_factory(
+                assume=False).client('cloudwatch', region_name=self.region)
+        else:
+            watch = utils.local_session(
+                self.ctx.session_factory).client('cloudwatch', region_name=self.region)
         return self.retry(
             watch.put_metric_data, Namespace=ns, MetricData=metrics)
 
diff --git a/c7n/utils.py b/c7n/utils.py
--- a/c7n/utils.py
+++ b/c7n/utils.py
@@ -28,9 +28,10 @@
 import six
 import sys
 
+from six.moves.urllib import parse as urlparse
 
 from c7n.exceptions import ClientError, PolicyValidationError
-from c7n import ipaddress
+from c7n import ipaddress, config
 
 # Try to place nice in lambda exec environment
 # where we don't require yaml
@@ -521,6 +522,19 @@ def format_string_values(obj, err_fallback=(IndexError, KeyError), *args, **kwar
         return obj
 
 
+def parse_url_config(url):
+    if url and '://' not in url:
+        url += "://"
+    conf = config.Bag()
+    parsed = urlparse.urlparse(url)
+    for k in ('scheme', 'netloc', 'path'):
+        conf[k] = getattr(parsed, k)
+    for k, v in urlparse.parse_qs(parsed.query).items():
+        conf[k] = v[0]
+    conf['url'] = url
+    return conf
+
+
 class FormatDate(object):
     """a datetime wrapper with extended pyformat syntax"""
 


Test patch:
diff --git a/tests/test_aws.py b/tests/test_aws.py
--- a/tests/test_aws.py
+++ b/tests/test_aws.py
@@ -119,6 +119,33 @@ def test_tracer(self):
 
 class OutputMetricsTest(BaseTest):
 
+    def test_metrics_destination_dims(self):
+        tmetrics = []
+
+        class Metrics(aws.MetricsOutput):
+
+            def _put_metrics(self, ns, metrics):
+                tmetrics.extend(metrics)
+
+        conf = Bag({'region': 'us-east-2', 'scheme': 'aws', 'netloc': 'master'})
+        ctx = Bag(session_factory=None, account_id='001100', region='us-east-1',
+                  policy=Bag(name='test', resource_type='ec2'))
+        moutput = Metrics(ctx, conf)
+
+        moutput.put_metric('Calories', 400, 'Count', Scope='Policy', Food='Pizza')
+        moutput.flush()
+
+        tmetrics[0].pop('Timestamp')
+        self.assertEqual(tmetrics, [{
+            'Dimensions': [{'Name': 'Policy', 'Value': 'test'},
+                           {'Name': 'ResType', 'Value': 'ec2'},
+                           {'Name': 'Food', 'Value': 'Pizza'},
+                           {'Name': 'Region', 'Value': 'us-east-1'},
+                           {'Name': 'Account', 'Value': '001100'}],
+            'MetricName': 'Calories',
+            'Unit': 'Count',
+            'Value': 400}])
+
     def test_metrics(self):
         session_factory = self.replay_flight_data('output-aws-metrics')
         policy = Bag(name='test', resource_type='ec2')
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -114,6 +114,31 @@ def rabbit(err=False):
         self.assertTrue("more carrots" in log_output.getvalue())
 
 
+class UrlConfTest(BaseTest):
+
+    def test_parse_url(self):
+        self.assertEqual(
+            dict(utils.parse_url_config('aws://target?format=json&region=us-west-2')),
+            dict(url='aws://target?format=json&region=us-west-2',
+                 netloc='target',
+                 path='',
+                 scheme='aws',
+                 region='us-west-2',
+                 format='json'))
+
+        self.assertEqual(
+            dict(utils.parse_url_config('')),
+            {'netloc': '', 'path': '', 'scheme': '', 'url': ''})
+
+        self.assertEqual(
+            dict(utils.parse_url_config('aws')),
+            {'path': '', 'scheme': 'aws', 'netloc': '', 'url': 'aws://'})
+
+        self.assertEqual(
+            dict(utils.parse_url_config('aws://')),
+            {'path': '', 'scheme': 'aws', 'netloc': '', 'url': 'aws://'})
+
+
 class UtilTest(BaseTest):
 
     def test_local_session_region(self):