From f251adef2aa52ffeb9c488815ad34a82d7ab1a07 Mon Sep 17 00:00:00 2001
From: Amy Sutedja <asutedja@splunk.com>
Date: Wed, 8 Jul 2020 11:29:08 -0700
Subject: [PATCH 1/2] Telemetry args test

---
 tests/modularinput/test_script.py | 41 ++++++++++++++++++++++++++++++-
 1 file changed, 40 insertions(+), 1 deletion(-)

diff --git a/tests/modularinput/test_script.py b/tests/modularinput/test_script.py
index 0831d5f74..03cf4ad57 100644
--- a/tests/modularinput/test_script.py
+++ b/tests/modularinput/test_script.py
@@ -171,7 +171,6 @@ def stream_events(self, inputs, ew):
         assert xml_compare(expected, found)
         assert return_value != 0
 
-
 def test_write_events(capsys):
     """Check that passing an input definition and writing a couple events goes smoothly."""
 
@@ -217,6 +216,46 @@ def stream_events(self, inputs, ew):
         assert xml_compare(expected, found)
 
 
+def test_telemetry(capsys):
+    """Check that writing telemetry goes smoothly."""
+
+    # Override abstract methods
+    class NewScript(Script):
+        def get_scheme(self):
+            return None
+
+        def stream_events(self, _inputs, ew):
+            event = Event(
+                data="Test",
+            )
+
+            ew.write_event(event)
+
+    script = NewScript()
+    input_configuration = data_open("data/conf_with_2_inputs.xml")
+
+    event_writer = EventWriter(sys.stdout, sys.stderr)
+
+    with patch.object(Service, 'post') as patched_telemetry_post:
+        patched_telemetry_post.return_value = Mock(**PATCHED_TELEMETRY_RESPONSE)
+
+        return_value = script.run_script([TEST_SCRIPT_PATH], event_writer, input_configuration)
+
+        post_args, post_kwargs = patched_telemetry_post.call_args_list[0]
+
+        assert post_args == ('telemetry-metric/',)
+        assert post_kwargs == {
+            'app': None,
+            'body': '{"type": "event", "component": "splunk-sdk-python", "data": {"version": "1.6.13"}, "optInRequired": 2}',
+            'headers': [('Content-Type', 'application/json')],
+            'owner': None,
+            'sharing': None
+        }
+
+    output = capsys.readouterr()
+    assert output.err == ""
+    assert return_value == 0
+
 def test_service_property(capsys):
     """ Check that Script.service returns a valid Service instance as soon
     as the stream_events method is called, but not before.

From e18608e84c25109a5b275e1cfe0474fceaf7512f Mon Sep 17 00:00:00 2001
From: Amy Sutedja <asutedja@splunk.com>
Date: Wed, 8 Jul 2020 12:24:23 -0700
Subject: [PATCH 2/2] Class hierarchy for TelemetryMetrics

---
 splunklib/modularinput/script.py              |  5 +-
 splunklib/wire/_internal/__init__.py          |  3 +-
 .../_internal/aggregate_telemetry_metric.py   | 69 +++++++++++++++++++
 .../wire/_internal/event_telemetry_metric.py  | 69 +++++++++++++++++++
 splunklib/wire/_internal/telemetry_metric.py  | 64 ++++++++++++++++-
 tests/modularinput/test_script.py             | 13 +++-
 tests/test_telemetry.py                       | 46 +++++++++++--
 7 files changed, 256 insertions(+), 13 deletions(-)
 create mode 100644 splunklib/wire/_internal/aggregate_telemetry_metric.py
 create mode 100644 splunklib/wire/_internal/event_telemetry_metric.py

diff --git a/splunklib/modularinput/script.py b/splunklib/modularinput/script.py
index 96a566a18..1e66aa420 100644
--- a/splunklib/modularinput/script.py
+++ b/splunklib/modularinput/script.py
@@ -25,7 +25,7 @@
 from .event_writer import EventWriter
 from .input_definition import InputDefinition
 from .validation_definition import ValidationDefinition
-from ..wire._internal import Telemetry, TelemetryMetric
+from ..wire._internal import Telemetry, EventTelemetryMetric
 
 try:
     import xml.etree.cElementTree as ET
@@ -76,8 +76,7 @@ def run_script(self, args, event_writer, input_stream):
                 self._input_definition = InputDefinition.parse(input_stream)
 
                 # create a telemetry metric
-                metric = TelemetryMetric(**{
-                    'metric_type': 'event',
+                metric = EventTelemetryMetric(**{
                     'component': 'splunk-sdk-python',
                     'data': {
                         'version': splunklib.__version__
diff --git a/splunklib/wire/_internal/__init__.py b/splunklib/wire/_internal/__init__.py
index 83028bbf3..1fe646392 100644
--- a/splunklib/wire/_internal/__init__.py
+++ b/splunklib/wire/_internal/__init__.py
@@ -14,5 +14,6 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+from .aggregate_telemetry_metric import *
+from .event_telemetry_metric import *
 from .telemetry import *
-from .telemetry_metric import *
diff --git a/splunklib/wire/_internal/aggregate_telemetry_metric.py b/splunklib/wire/_internal/aggregate_telemetry_metric.py
new file mode 100644
index 000000000..34d25f406
--- /dev/null
+++ b/splunklib/wire/_internal/aggregate_telemetry_metric.py
@@ -0,0 +1,69 @@
+# coding=utf-8
+#
+# Copyright © 2011-2020 Splunk, Inc.
+#
+# 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.
+
+from splunklib.wire._internal.telemetry_metric import TelemetryMetric
+
+class AggregateTelemetryMetric(TelemetryMetric):
+    METRIC_TYPE = 'aggregate'
+
+    def __init__(self, component, data,
+                 opt_in_required=2,
+                 version=None,
+                 timestamp=None,
+                 visibility=None,
+                 index_data=None,
+                 begin=None,
+                 end=None):
+        super(AggregateTelemetryMetric, self).__init__(
+            AggregateTelemetryMetric.METRIC_TYPE,
+            component,
+            data,
+            opt_in_required=opt_in_required,
+            version=version,
+            timestamp=timestamp,
+            visibility=visibility,
+            index_data=index_data
+        )
+
+        self.begin = begin
+        self.end = end
+
+    @property
+    def begin(self):
+        return self._begin
+
+    @begin.setter
+    def begin(self, value):
+        self._begin = value
+
+    @property
+    def end(self):
+        return self._end
+
+    @end.setter
+    def end(self, value):
+        self._end = value
+
+    def to_wire(self):
+        wire = super(AggregateTelemetryMetric, self).to_wire()
+
+        if self.begin is not None:
+            wire['begin'] = self.begin
+
+        if self.end is not None:
+            wire['end'] = self.end
+
+        return wire
diff --git a/splunklib/wire/_internal/event_telemetry_metric.py b/splunklib/wire/_internal/event_telemetry_metric.py
new file mode 100644
index 000000000..3dd55dbb6
--- /dev/null
+++ b/splunklib/wire/_internal/event_telemetry_metric.py
@@ -0,0 +1,69 @@
+# coding=utf-8
+#
+# Copyright © 2011-2020 Splunk, Inc.
+#
+# 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.
+
+from splunklib.wire._internal.telemetry_metric import TelemetryMetric
+
+class EventTelemetryMetric(TelemetryMetric):
+    METRIC_TYPE = 'event'
+
+    def __init__(self, component, data,
+                 opt_in_required=2,
+                 version=None,
+                 timestamp=None,
+                 visibility=None,
+                 index_data=None,
+                 user_id=None,
+                 experience_id=None):
+        super(EventTelemetryMetric, self).__init__(
+            EventTelemetryMetric.METRIC_TYPE,
+            component,
+            data,
+            opt_in_required=opt_in_required,
+            version=version,
+            timestamp=timestamp,
+            visibility=visibility,
+            index_data=index_data
+        )
+
+        self.user_id = user_id
+        self.experience_id = experience_id
+
+    @property
+    def user_id(self):
+        return self._user_id
+
+    @user_id.setter
+    def user_id(self, value):
+        self._user_id = value
+
+    @property
+    def experience_id(self):
+        return self._experience_id
+
+    @experience_id.setter
+    def experience_id(self, value):
+        self._experience_id = value
+
+    def to_wire(self):
+        wire = super(EventTelemetryMetric, self).to_wire()
+
+        if self.user_id is not None:
+            wire['userID'] = self.user_id
+
+        if self.experience_id is not None:
+            wire['experienceID'] = self.experience_id
+
+        return wire
diff --git a/splunklib/wire/_internal/telemetry_metric.py b/splunklib/wire/_internal/telemetry_metric.py
index 478ce87ec..b97167bbb 100644
--- a/splunklib/wire/_internal/telemetry_metric.py
+++ b/splunklib/wire/_internal/telemetry_metric.py
@@ -14,12 +14,24 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-class TelemetryMetric:
-    def __init__(self, metric_type, component, data, opt_in_required=2):
+from abc import ABCMeta
+from splunklib import six
+
+class TelemetryMetric(six.with_metaclass(ABCMeta, object)):
+    def __init__(self, metric_type, component, data,
+                 opt_in_required=2,
+                 version=None,
+                 index_data=None,
+                 timestamp=None,
+                 visibility=None):
         self.metric_type = metric_type
         self.component = component
         self.data = data
         self.opt_in_required = opt_in_required
+        self.version = version
+        self.index_data = index_data
+        self.timestamp = timestamp
+        self.visibility = visibility
 
     @property
     def metric_type(self):
@@ -53,10 +65,56 @@ def opt_in_required(self):
     def opt_in_required(self, value):
         self._opt_in_required = value
 
+    @property
+    def version(self):
+        return self._version
+
+    @version.setter
+    def version(self, value):
+        self._version = value
+
+    @property
+    def index_data(self):
+        return self._index_data
+
+    @index_data.setter
+    def index_data(self, value):
+        self._index_data = value
+
+    @property
+    def timestamp(self):
+        return self._timestamp
+
+    @timestamp.setter
+    def timestamp(self, value):
+        self._timestamp = value
+
+    @property
+    def visibility(self):
+        return self._visibility
+
+    @visibility.setter
+    def visibility(self, value):
+        self._visibility = value
+
     def to_wire(self):
-        return {
+        wire = {
             'type': self.metric_type,
             'component': self.component,
             'data': self.data,
             'optInRequired': self.opt_in_required,
         }
+
+        if self.version is not None:
+            wire['version'] = self.version
+
+        if self.index_data is not None:
+            wire['indexData'] = self.index_data
+
+        if self.timestamp is not None:
+            wire['timestamp'] = self.timestamp
+
+        if self.visibility is not None:
+            wire['visibility'] = self.visibility
+
+        return wire
diff --git a/tests/modularinput/test_script.py b/tests/modularinput/test_script.py
index 03cf4ad57..ecbc26387 100644
--- a/tests/modularinput/test_script.py
+++ b/tests/modularinput/test_script.py
@@ -4,7 +4,7 @@
 
 from mock import Mock, patch
 
-from splunklib import six
+from splunklib import six, __version__
 from splunklib.client import Service
 from splunklib.modularinput import Script, EventWriter, Scheme, Argument, Event
 
@@ -219,6 +219,15 @@ def stream_events(self, inputs, ew):
 def test_telemetry(capsys):
     """Check that writing telemetry goes smoothly."""
 
+    EXPECTED_TELEMETRY_BODY = {
+        'type': 'event',
+        'component': 'splunk-sdk-python',
+        'data': {
+            'version': __version__,
+        },
+        'optInRequired': 2
+    }
+
     # Override abstract methods
     class NewScript(Script):
         def get_scheme(self):
@@ -246,7 +255,7 @@ def stream_events(self, _inputs, ew):
         assert post_args == ('telemetry-metric/',)
         assert post_kwargs == {
             'app': None,
-            'body': '{"type": "event", "component": "splunk-sdk-python", "data": {"version": "1.6.13"}, "optInRequired": 2}',
+            'body': json.dumps(EXPECTED_TELEMETRY_BODY),
             'headers': [('Content-Type', 'application/json')],
             'owner': None,
             'sharing': None
diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py
index 5e1cc5d6b..40fbe2c1e 100644
--- a/tests/test_telemetry.py
+++ b/tests/test_telemetry.py
@@ -18,8 +18,7 @@
 import pytest
 
 from tests import testlib
-from splunklib.wire._internal.telemetry import Telemetry
-from splunklib.wire._internal.telemetry_metric import TelemetryMetric
+from splunklib.wire._internal import Telemetry, EventTelemetryMetric, AggregateTelemetryMetric
 
 @pytest.mark.app
 class TestTelemetry(testlib.SDKTestCase):
@@ -33,8 +32,7 @@ def setUp(self):
 
     def test_submit(self):
         # create a telemetry metric
-        metric = TelemetryMetric(**{
-            'metric_type': 'event',
+        metric = EventTelemetryMetric(**{
             'component': 'telemetry_test_case',
             'data': {
                 'testValue': 32
@@ -46,3 +44,43 @@ def test_submit(self):
 
         # it should return a 201
         self.assertEqual(response.status, 201)
+
+    def test_event_submit(self):
+        # create a telemetry metric
+        metric = EventTelemetryMetric(**{
+            'component': 'telemetry_test_case',
+            'data': {
+                'testValue': 32
+            },
+            'version': 'test',
+            'index_data': False,
+            'timestamp': 0,
+            'visibility': ['anonymous'],
+        })
+
+        # call out to telemetry
+        response, _body = self.telemetry.submit(metric.to_wire())
+
+        # it should return a 201
+        self.assertEqual(response.status, 201)
+
+    def test_aggregate_submit(self):
+        # create a telemetry metric
+        metric = AggregateTelemetryMetric(**{
+            'component': 'telemetry_test_case',
+            'data': {
+                'testValue': 32
+            },
+            'version': 'test',
+            'index_data': False,
+            'timestamp': 3,
+            'visibility': ['anonymous'],
+            'begin': 0,
+            'end': 1,
+        })
+
+        # call out to telemetry
+        response, _body = self.telemetry.submit(metric.to_wire())
+
+        # it should return a 201
+        self.assertEqual(response.status, 201)