From 91272d027b8bf651cc34fa8b464f8f06c6961f4d Mon Sep 17 00:00:00 2001 From: jblashka Date: Sat, 12 Dec 2020 03:00:15 -0500 Subject: [PATCH] Add millisecond data to splunk callback timestamp (#1462) * Add millisecond data to timestamp * Add flag to control splunk milliseconds * Update changelogs/fragments/1462-splunk-millisecond.yaml Co-authored-by: Amin Vakil * Apply suggestions from code review Co-authored-by: Felix Fontein * Apply more suggestions from review * Whitespace Co-authored-by: Amin Vakil Co-authored-by: Felix Fontein --- .../fragments/1462-splunk-millisecond.yaml | 2 + plugins/callback/splunk.py | 30 ++++++++- tests/unit/plugins/callback/__init__.py | 0 tests/unit/plugins/callback/test_splunk.py | 61 +++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/1462-splunk-millisecond.yaml create mode 100644 tests/unit/plugins/callback/__init__.py create mode 100644 tests/unit/plugins/callback/test_splunk.py diff --git a/changelogs/fragments/1462-splunk-millisecond.yaml b/changelogs/fragments/1462-splunk-millisecond.yaml new file mode 100644 index 0000000000..aa73f5efef --- /dev/null +++ b/changelogs/fragments/1462-splunk-millisecond.yaml @@ -0,0 +1,2 @@ +minor_changes: + - splunk callback - new parameter ``include_milliseconds`` to add milliseconds to existing timestamp field (https://github.com/ansible-collections/community.general/pull/1462). diff --git a/plugins/callback/splunk.py b/plugins/callback/splunk.py index 68480752e0..7e224f32cb 100644 --- a/plugins/callback/splunk.py +++ b/plugins/callback/splunk.py @@ -57,6 +57,17 @@ DOCUMENTATION = ''' type: bool default: true version_added: '1.0.0' + include_milliseconds: + description: Whether to include milliseconds as part of the generated timestamp field in the event + sent to the Splunk HTTP collector + env: + - name: SPLUNK_INCLUDE_MILLISECONDS + ini: + - section: callback_splunk + key: include_milliseconds + type: bool + default: false + version_added: 2.0.0 ''' EXAMPLES = ''' @@ -96,7 +107,7 @@ class SplunkHTTPCollectorSource(object): self.ip_address = socket.gethostbyname(socket.gethostname()) self.user = getpass.getuser() - def send_event(self, url, authtoken, validate_certs, state, result, runtime): + def send_event(self, url, authtoken, validate_certs, include_milliseconds, state, result, runtime): if result._task_fields['args'].get('_ansible_check_mode') is True: self.ansible_check_mode = True @@ -116,8 +127,13 @@ class SplunkHTTPCollectorSource(object): data['uuid'] = result._task._uuid data['session'] = self.session data['status'] = state - data['timestamp'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S ' - '+0000') + + if include_milliseconds: + time_format = '%Y-%m-%d %H:%M:%S.%f +0000' + else: + time_format = '%Y-%m-%d %H:%M:%S +0000' + + data['timestamp'] = datetime.utcnow().strftime(time_format) data['host'] = self.host data['ip_address'] = self.ip_address data['user'] = self.user @@ -158,6 +174,7 @@ class CallbackModule(CallbackBase): self.url = None self.authtoken = None self.validate_certs = None + self.include_milliseconds = None self.splunk = SplunkHTTPCollectorSource() def _runtime(self, result): @@ -193,6 +210,8 @@ class CallbackModule(CallbackBase): self.validate_certs = self.get_option('validate_certs') + self.include_milliseconds = self.get_option('include_milliseconds') + def v2_playbook_on_start(self, playbook): self.splunk.ansible_playbook = basename(playbook._file_name) @@ -207,6 +226,7 @@ class CallbackModule(CallbackBase): self.url, self.authtoken, self.validate_certs, + self.include_milliseconds, 'OK', result, self._runtime(result) @@ -217,6 +237,7 @@ class CallbackModule(CallbackBase): self.url, self.authtoken, self.validate_certs, + self.include_milliseconds, 'SKIPPED', result, self._runtime(result) @@ -227,6 +248,7 @@ class CallbackModule(CallbackBase): self.url, self.authtoken, self.validate_certs, + self.include_milliseconds, 'FAILED', result, self._runtime(result) @@ -237,6 +259,7 @@ class CallbackModule(CallbackBase): self.url, self.authtoken, self.validate_certs, + self.include_milliseconds, 'FAILED', result, self._runtime(result) @@ -247,6 +270,7 @@ class CallbackModule(CallbackBase): self.url, self.authtoken, self.validate_certs, + self.include_milliseconds, 'UNREACHABLE', result, self._runtime(result) diff --git a/tests/unit/plugins/callback/__init__.py b/tests/unit/plugins/callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/callback/test_splunk.py b/tests/unit/plugins/callback/test_splunk.py new file mode 100644 index 0000000000..291fb691ad --- /dev/null +++ b/tests/unit/plugins/callback/test_splunk.py @@ -0,0 +1,61 @@ +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.executor.task_result import TaskResult +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible_collections.community.general.tests.unit.compat.mock import patch, call, MagicMock, Mock +from ansible_collections.community.general.plugins.callback.splunk import SplunkHTTPCollectorSource +from datetime import datetime + +import json + + +class TestSplunkClient(unittest.TestCase): + def setUp(self): + self.splunk = SplunkHTTPCollectorSource() + self.mock_task = Mock('MockTask') + self.mock_task._role = 'myrole' + self.mock_task._uuid = 'myuuid' + self.task_fields = {'args': {}} + self.mock_host = Mock('MockHost') + self.mock_host.name = 'myhost' + + @patch('ansible_collections.community.general.plugins.callback.splunk.datetime') + @patch('ansible_collections.community.general.plugins.callback.splunk.open_url') + def test_timestamp_with_milliseconds(self, open_url_mock, mock_datetime): + mock_datetime.utcnow.return_value = datetime(2020, 12, 1) + result = TaskResult(host=self.mock_host, task=self.mock_task, return_data={}, task_fields=self.task_fields) + + self.splunk.send_event(url='endpoint', authtoken='token', validate_certs=False, include_milliseconds=True, state='OK', result=result, runtime=100) + + args, kwargs = open_url_mock.call_args + sent_data = json.loads(args[1]) + + self.assertEqual(sent_data['event']['timestamp'], '2020-12-01 00:00:00.000000 +0000') + + @patch('ansible_collections.community.general.plugins.callback.splunk.datetime') + @patch('ansible_collections.community.general.plugins.callback.splunk.open_url') + def test_timestamp_without_milliseconds(self, open_url_mock, mock_datetime): + mock_datetime.utcnow.return_value = datetime(2020, 12, 1) + result = TaskResult(host=self.mock_host, task=self.mock_task, return_data={}, task_fields=self.task_fields) + + self.splunk.send_event(url='endpoint', authtoken='token', validate_certs=False, include_milliseconds=False, state='OK', result=result, runtime=100) + + args, kwargs = open_url_mock.call_args + sent_data = json.loads(args[1]) + + self.assertEqual(sent_data['event']['timestamp'], '2020-12-01 00:00:00 +0000')