Repository: ansible-collections/community.general

Issue:
Splunk plugin: Configurable session IDs
### Summary

I am trying to correlate related Ansible runs in Splunk, and I think adding a configurable session ID to the Splunk callback plugin would be a fairly straightforward way to do this.

Right now the Splunk plugin generates a random UUID at startup and attaches it to every task executed during that run (lifetime of the SplunkCallback instance.) This makes it easy to correlate tasks from the same process using a simple filter.  I would like this ID, or something similar, to be settable via an environment variable, which would allow me to use the same ID across multiple instances of Ansible.  With this I could break up very large batches into many smaller runs, but still correlate the results.

I think this could be done either as an amendment to the behavior of the existing `session` ID, or as a new ID entirely.  A new ID might be better, as it is still useful to correlate tasks from the same process.  In this case, I think the name `job` fits well, as in "batch job."

### Issue Type

Feature Idea

### Component Name

splunk

### Additional Information

My actual use case is more complicated than this, but here is a simple shell example that could be used to test it.

```sh
# Set environment variables for existing Splunk options
export SPLUNK_URL='http://splunk.example.com:8088/services/collector/event'
export SPLUNK_AUTHTOKEN='f23blad6-5965-4537-bf69-5b5a545blabla88'

# New variable example
export ANSIBLE_SPLUNK_JOB_ID="$(uuidgen -r)"

ansible-playbook -i host1,host2 main.yaml &
ansible-playbook -i host3,host4 main.yaml &

wait
```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`lib/ansible/playbook`](https://github.com/ansible-collections/community.general/blob/master/lib/ansible/playbook)
* [`tests/utils/shippable/shippable.sh`](https://github.com/ansible-collections/community.general/blob/master/tests/utils/shippable/shippable.sh)

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 --->
cc @gundalow
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`lib/ansible/playbook`](https://github.com/ansible-collections/community.general/blob/master/lib/ansible/playbook)
* [`plugins/callback/splunk.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/callback/splunk.py)
* [`tests/utils/shippable/shippable.sh`](https://github.com/ansible-collections/community.general/blob/master/tests/utils/shippable/shippable.sh)

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 --->
!component =plugins/callback/splunk.py
Files identified in the description:
* [`plugins/callback/splunk.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/callback/splunk.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/plugins/callback/splunk.py b/plugins/callback/splunk.py
--- a/plugins/callback/splunk.py
+++ b/plugins/callback/splunk.py
@@ -68,6 +68,16 @@
         type: bool
         default: false
         version_added: 2.0.0
+      batch:
+        description:
+          - Correlation ID which can be set across multiple playbook executions.
+        env:
+          - name: SPLUNK_BATCH
+        ini:
+          - section: callback_splunk
+            key: batch
+        type: str
+        version_added: 3.3.0
 '''
 
 EXAMPLES = '''
@@ -107,7 +117,7 @@ def __init__(self):
         self.ip_address = socket.gethostbyname(socket.gethostname())
         self.user = getpass.getuser()
 
-    def send_event(self, url, authtoken, validate_certs, include_milliseconds, state, result, runtime):
+    def send_event(self, url, authtoken, validate_certs, include_milliseconds, batch, state, result, runtime):
         if result._task_fields['args'].get('_ansible_check_mode') is True:
             self.ansible_check_mode = True
 
@@ -126,6 +136,8 @@ def send_event(self, url, authtoken, validate_certs, include_milliseconds, state
         data = {}
         data['uuid'] = result._task._uuid
         data['session'] = self.session
+        if batch is not None:
+            data['batch'] = batch
         data['status'] = state
 
         if include_milliseconds:
@@ -175,6 +187,7 @@ def __init__(self, display=None):
         self.authtoken = None
         self.validate_certs = None
         self.include_milliseconds = None
+        self.batch = None
         self.splunk = SplunkHTTPCollectorSource()
 
     def _runtime(self, result):
@@ -212,6 +225,8 @@ def set_options(self, task_keys=None, var_options=None, direct=None):
 
         self.include_milliseconds = self.get_option('include_milliseconds')
 
+        self.batch = self.get_option('batch')
+
     def v2_playbook_on_start(self, playbook):
         self.splunk.ansible_playbook = basename(playbook._file_name)
 
@@ -227,6 +242,7 @@ def v2_runner_on_ok(self, result, **kwargs):
             self.authtoken,
             self.validate_certs,
             self.include_milliseconds,
+            self.batch,
             'OK',
             result,
             self._runtime(result)
@@ -238,6 +254,7 @@ def v2_runner_on_skipped(self, result, **kwargs):
             self.authtoken,
             self.validate_certs,
             self.include_milliseconds,
+            self.batch,
             'SKIPPED',
             result,
             self._runtime(result)
@@ -249,6 +266,7 @@ def v2_runner_on_failed(self, result, **kwargs):
             self.authtoken,
             self.validate_certs,
             self.include_milliseconds,
+            self.batch,
             'FAILED',
             result,
             self._runtime(result)
@@ -260,6 +278,7 @@ def runner_on_async_failed(self, result, **kwargs):
             self.authtoken,
             self.validate_certs,
             self.include_milliseconds,
+            self.batch,
             'FAILED',
             result,
             self._runtime(result)
@@ -271,6 +290,7 @@ def v2_runner_on_unreachable(self, result, **kwargs):
             self.authtoken,
             self.validate_certs,
             self.include_milliseconds,
+            self.batch,
             'UNREACHABLE',
             result,
             self._runtime(result)


Test patch:
diff --git a/tests/unit/plugins/callback/test_splunk.py b/tests/unit/plugins/callback/test_splunk.py
--- a/tests/unit/plugins/callback/test_splunk.py
+++ b/tests/unit/plugins/callback/test_splunk.py
@@ -43,7 +43,10 @@ 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)
+        self.splunk.send_event(
+            url='endpoint', authtoken='token', validate_certs=False, include_milliseconds=True,
+            batch="abcefghi-1234-5678-9012-abcdefghijkl", state='OK', result=result, runtime=100
+        )
 
         args, kwargs = open_url_mock.call_args
         sent_data = json.loads(args[1])
@@ -58,7 +61,10 @@ 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)
+        self.splunk.send_event(
+            url='endpoint', authtoken='token', validate_certs=False, include_milliseconds=False,
+            batch="abcefghi-1234-5678-9012-abcdefghijkl", state='OK', result=result, runtime=100
+        )
 
         args, kwargs = open_url_mock.call_args
         sent_data = json.loads(args[1])