Repository: ansible/ansible

Issue:
JSON output plugin to display custom stats summary collected by set_stats module
##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Feature Idea

##### COMPONENT NAME
json callback output plugin

<!---
Name of the module, plugin, task or feature
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path
-->

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
v2.0.0-0.5.beta3-19153-gd45ceb3a4f
```

##### CONFIGURATION
##### OS / ENVIRONMENT
ANSIBLE_SHOW_CUSTOM_STATS=true env variable set or
show_custom_stats = True setting in ansible.cfg

##### SUMMARY
JSON output plugin is not displaying custom stats collected by set_stats module.

##### STEPS TO REPRODUCE
<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- hosts: all
  connection: local
  gather_facts: no
  tasks:
    - name: Set a custom stat
      set_stats:
        data:
          my_stat: 4
```

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
In order to easily use custom stats, the summary of those should be displayed. The default output plugin shows the summary in a "CUSTOM STATS" section, like this:
```
$ ANSIBLE_STDOUT_CALLBACK=default ANSIBLE_SHOW_CUSTOM_STATS=true ansible-playbook -i inv pb.yml 

PLAY [all] *************************************************************************************************************************************************************************************************

TASK [Set a custom stat] ***********************************************************************************************************************************************************************************
ok: [test.example.com]

PLAY RECAP *************************************************************************************************************************************************************************************************
test.example.com           : ok=1    changed=0    unreachable=0    failed=0   


CUSTOM STATS: **********************************************************************************************************************************************************************************************

	RUN: { "my_stat": 4}

```
##### ACTUAL RESULTS

When using json output module, the custom stats are not summarized:

```
$ ANSIBLE_STDOUT_CALLBACK=json ANSIBLE_SHOW_CUSTOM_STATS=true ansible-playbook -i inv pb.yml 
{
    "plays": [
        {
            "play": {
                "id": "34e6d735-c355-074e-bdd0-000000000006",
                "name": "all"
            },
            "tasks": [
                {
                    "hosts": {
                        "test.example.com": {
                            "_ansible_no_log": false,
                            "ansible_stats": {
                                "aggregate": true,
                                "data": {
                                    "my_stat": 4
                                },
                                "per_host": false
                            },
                            "changed": false
                        }
                    },
                    "task": {
                        "id": "34e6d735-c355-074e-bdd0-000000000008",
                        "name": "Set a custom stat"
                    }
                }
            ]
        }
    ],
    "stats": {
        "test.example.com": {
            "changed": 0,
            "failures": 0,
            "ok": 1,
            "skipped": 0,
            "unreachable": 0
        }
    }
}
```

One could expect that when ANSIBLE_SHOW_CUSTOM_STATS is set:

- Per host summary of custom stats are present in the stats object, or alternatively, in custom_stats object to make a clear difference between custom and 'standard' stats.

- Per run stat summary is available via some other object (custom_run_stats perhaps?)

Like this with the example playbook:
```
  [...]
   "stats": {
        "test.example.com": {
            "changed": 0,
            "failures": 0,
            "ok": 1,
            "skipped": 0,
            "unreachable": 0
        }
    },
   "custom_stats": {
   },
   "custom_run_stats": {
       "my_stat": 4
   }
```


Discussion:
Files identified in the description:
* [lib/ansible/plugins/callback/json.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/callback/json.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/lib/ansible/plugins/callback/json.py b/lib/ansible/plugins/callback/json.py
--- a/lib/ansible/plugins/callback/json.py
+++ b/lib/ansible/plugins/callback/json.py
@@ -15,10 +15,26 @@
     type: stdout
     requirements:
       - Set as stdout in config
+    options:
+      show_custom_stats:
+        version_added: "2.6"
+        name: Show custom stats
+        description: 'This adds the custom stats set via the set_stats plugin to the play recap'
+        default: False
+        env:
+          - name: ANSIBLE_SHOW_CUSTOM_STATS
+        ini:
+          - key: show_custom_stats
+            section: defaults
+        type: bool
 '''
 
 import json
 
+from functools import partial
+
+from ansible.inventory.host import Host
+
 from ansible.plugins.callback import CallbackBase
 
 
@@ -34,7 +50,7 @@ def __init__(self, display=None):
     def _new_play(self, play):
         return {
             'play': {
-                'name': play.name,
+                'name': play.get_name(),
                 'id': str(play._uuid)
             },
             'tasks': []
@@ -43,7 +59,7 @@ def _new_play(self, play):
     def _new_task(self, task):
         return {
             'task': {
-                'name': task.name,
+                'name': task.get_name(),
                 'id': str(task._uuid)
             },
             'hosts': {}
@@ -58,9 +74,10 @@ def v2_playbook_on_task_start(self, task, is_conditional):
     def v2_playbook_on_handler_task_start(self, task):
         self.results[-1]['tasks'].append(self._new_task(task))
 
-    def v2_runner_on_ok(self, result, **kwargs):
-        host = result._host
-        self.results[-1]['tasks'][-1]['hosts'][host.name] = result._result
+    def _convert_host_to_name(self, key):
+        if isinstance(key, (Host,)):
+            return key.get_name()
+        return key
 
     def v2_playbook_on_stats(self, stats):
         """Display info about playbook statistics"""
@@ -72,13 +89,37 @@ def v2_playbook_on_stats(self, stats):
             s = stats.summarize(h)
             summary[h] = s
 
+        custom_stats = {}
+        if self.get_option('show_custom_stats') and stats.custom:
+            custom_stats.update(dict((self._convert_host_to_name(k), v) for k, v in stats.custom.items()))
+            custom_stats.pop('_run', None)
+
         output = {
             'plays': self.results,
-            'stats': summary
+            'stats': summary,
+            'custom_stats': custom_stats,
         }
 
         self._display.display(json.dumps(output, indent=4, sort_keys=True))
 
-    v2_runner_on_failed = v2_runner_on_ok
-    v2_runner_on_unreachable = v2_runner_on_ok
-    v2_runner_on_skipped = v2_runner_on_ok
+    def _record_task_result(self, on_info, result, **kwargs):
+        """This function is used as a partial to add failed/skipped info in a single method"""
+        host = result._host
+        task = result._task
+        task_result = result._result.copy()
+        task_result.update(on_info)
+        task_result['action'] = task.action
+        self.results[-1]['tasks'][-1]['hosts'][host.name] = task_result
+
+    def __getattribute__(self, name):
+        """Return ``_record_task_result`` partial with a dict containing skipped/failed if necessary"""
+        if name not in ('v2_runner_on_ok', 'v2_runner_on_failed', 'v2_runner_on_unreachable', 'v2_runner_on_skipped'):
+            return object.__getattribute__(self, name)
+
+        on = name.rsplit('_', 1)[1]
+
+        on_info = {}
+        if on in ('failed', 'skipped'):
+            on_info[on] = True
+
+        return partial(self._record_task_result, on_info)


Test patch: