Skip to content

Commit 241db06

Browse files
committed
feat: Add telemetry to SDK
1 parent 1bc49be commit 241db06

File tree

10 files changed

+183
-1
lines changed

10 files changed

+183
-1
lines changed

example.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from splunklib import client
2+
3+
service = client.connect(host="localhost", username="admin", password="changed!", autologin=True)
4+
5+
for app in service.apps:
6+
print(app.setupInfo)

splunklib/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
from time import sleep
6969
from urllib import parse
7070

71+
from splunklib.internal.telemetry.sdk_usage import log_telemetry_sdk_usage
72+
7173
from . import data
7274
from .data import record
7375
from .binding import (
@@ -364,6 +366,7 @@ def connect(**kwargs):
364366
"""
365367
s = Service(**kwargs)
366368
s.login()
369+
log_telemetry_sdk_usage(s, module="custom_script")
367370
return s
368371

369372

splunklib/internal/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright © 2011-2025 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright © 2011-2025 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from .sender import TelemetrySender
16+
from .metric import Metric, MetricType
17+
from .sdk_usage import log_telemetry_sdk_usage
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright © 2011-2025 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from dataclasses import dataclass
16+
from enum import Enum
17+
from typing import Dict
18+
19+
20+
class MetricType(Enum):
21+
Event = "event"
22+
Aggregate = "aggregate"
23+
24+
25+
@dataclass
26+
class Metric:
27+
type: MetricType
28+
component: str
29+
data: Dict
30+
opt_in_required: int # TODO: find what the values mean
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import sys
2+
import splunklib
3+
from .metric import Metric, MetricType
4+
from .sender import TelemetrySender
5+
6+
import logging
7+
from splunklib import setup_logging
8+
9+
setup_logging(logging.DEBUG)
10+
11+
log = logging.getLogger()
12+
13+
SDK_USAGE_COMPONENT = "splunk.sdk.usage"
14+
15+
16+
# FIXME: adding Service typehint produces circular dependency
17+
def log_telemetry_sdk_usage(service, **kwargs):
18+
metric = Metric(
19+
MetricType.Event,
20+
SDK_USAGE_COMPONENT,
21+
{
22+
"sdk-language": "python",
23+
"python-version": sys.version,
24+
"sdk-version": splunklib.__version__,
25+
**kwargs,
26+
},
27+
4,
28+
)
29+
try:
30+
log.debug(f"sending new telemetry {metric}")
31+
telemetry = TelemetrySender(service)
32+
# TODO: handle possible errors
33+
_, _ = telemetry.send(metric)
34+
except Exception as e:
35+
log.error("Could not send telemetry", exc_info=True)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright © 2011-2025 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from typing import Any, Optional, Tuple
16+
from splunklib.data import Record
17+
from splunklib.internal.telemetry.metric import Metric
18+
import json
19+
20+
# TODO: decide: either struggle with the type hints or get rid of them and stick to the convention
21+
22+
CONTENT_TYPE = [('Content-Type', 'application/json')]
23+
DEFAULT_TELEMETRY_USER = "nobody" # User `nobody` always exists
24+
DEFAULT_TELEMETRY_APP = "splunk_instrumentation" # This app is shipped with Splunk and has `telemetry-metric` endpoint
25+
TELEMETRY_ENDPOINT = "telemetry-metric"
26+
27+
28+
class TelemetrySender:
29+
# FIXME: adding Service typehint produces circular dependency
30+
# service: Service
31+
32+
def __init__(self, service):
33+
self.service = service
34+
35+
def send(self, metric: Metric, user: Optional[str] = None, app: Optional[str] = None) -> Tuple[Record, Any]:
36+
"""Sends the metric to the `telemetry-metric` endpoint.
37+
38+
:param user: Optional user that sends the telemetry.
39+
:param app: Optional app that is used to send the telemetry.
40+
41+
If those values are omitted, the default values are used.
42+
This makes sure that, even if missing some info, the event will be sent.
43+
"""
44+
45+
metric_body = self._metric_to_json(metric)
46+
47+
user = user or DEFAULT_TELEMETRY_USER
48+
app = app or DEFAULT_TELEMETRY_APP
49+
50+
response = self.service.post(
51+
"telemetry-metric",
52+
user,
53+
app,
54+
headers=[('Content-Type', 'application/json')],
55+
body=metric_body,
56+
)
57+
58+
body = json.loads(response.body.read().decode('utf-8'))
59+
60+
return response, body
61+
62+
def _metric_to_json(self, metric: Metric) -> str:
63+
m = {
64+
"type": metric.type.value,
65+
"component": metric.component,
66+
"data": metric.data,
67+
"optInRequired": metric.opt_in_required
68+
}
69+
70+
return json.dumps(m)

splunklib/modularinput/script.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from .input_definition import InputDefinition
2323
from .validation_definition import ValidationDefinition
2424

25+
from splunklib.internal.telemetry import log_telemetry_sdk_usage
26+
2527

2628
class Script(metaclass=ABCMeta):
2729
"""An abstract base class for implementing modular inputs.
@@ -69,6 +71,7 @@ def run_script(self, args, event_writer, input_stream):
6971
self._session_key = input_definition.metadata["session_key"]
7072
self.stream_events(input_definition, event_writer)
7173
event_writer.close()
74+
log_telemetry_sdk_usage(self.service, module="modularinput")
7275
return 0
7376

7477
if str(args[1]).lower() == "--scheme":

splunklib/searchcommands/search_command.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from warnings import warn
3636
from xml.etree import ElementTree
3737

38+
from splunklib.internal.telemetry.sdk_usage import log_telemetry_sdk_usage
39+
3840
# Relative imports
3941
from . import Boolean, Option, environment
4042
from .internals import (
@@ -468,6 +470,9 @@ def process(
468470
else:
469471
self._process_protocol_v2(argv, ifile, ofile)
470472

473+
cmd_name = self.__class__.__name__
474+
log_telemetry_sdk_usage(self.service, module=cmd_name)
475+
471476
def _map_input_header(self):
472477
metadata = self._metadata
473478
searchinfo = metadata.searchinfo

tests/system/test_apps/modularinput_app/bin/modularinput.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def stream_events(self, inputs, ew):
5757
for input_name, input_item in list(inputs.inputs.items()):
5858
event = Event()
5959
event.stanza = input_name
60-
event.data = "example message"
60+
event.data = f"New endpoint received: {input_item[self.endpoint_arg]}"
6161
ew.write_event(event)
6262

6363
def check_service_access(self):

0 commit comments

Comments
 (0)