Skip to content

Commit 3a341f1

Browse files
DPDV-4549: Do not crash when invalid query is used (#96)
* DPDV-4549: Do not crash when invalid query is used * Added error handling also for long running queries * Increase timeout to 180s * Try to wait little bit longer * Add unit test to test the function - search_error_exit
1 parent 215b318 commit 3a341f1

File tree

7 files changed

+117
-27
lines changed

7 files changed

+117
-27
lines changed

TA_dataset/bin/dataset_api.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# adjust paths to make the Splunk app working
77
import import_declare_test # noqa: F401
8-
from dataset_common import normalize_time
8+
from dataset_common import logger, normalize_time
99

1010
# Dataset V2 API client (generated)
1111
from dataset_query_api_client import AuthenticatedClient
@@ -24,6 +24,13 @@
2424
)
2525

2626

27+
# APIException stores response payload received by API.
28+
# Payload is passed as is into search_error_exit.
29+
class APIException(Exception):
30+
def __init__(self, payload):
31+
self.payload = payload
32+
33+
2734
# TODO: Convert to the expected format
2835
# https://www.python-httpx.org/advanced/#http-proxying
2936
def convert_proxy(proxy):
@@ -39,7 +46,13 @@ def convert_proxy(proxy):
3946

4047
# Executes Dataset LongRunningQuery for log events
4148
def ds_lrq_log_query(
42-
base_url, api_key, start_time, end_time, filter_expr, limit, proxy
49+
base_url,
50+
api_key,
51+
start_time,
52+
end_time,
53+
filter_expr,
54+
limit,
55+
proxy,
4356
):
4457
client = AuthenticatedClient(
4558
base_url=base_url, token=api_key, proxy=convert_proxy(proxy)
@@ -69,7 +82,14 @@ def ds_lrq_power_query(base_url, api_key, start_time, end_time, query, proxy):
6982

7083
# Executes Dataset LongRunningQuery to fetch facet values
7184
def ds_lrq_facet_values(
72-
base_url, api_key, start_time, end_time, filter, name, max_values, proxy
85+
base_url,
86+
api_key,
87+
start_time,
88+
end_time,
89+
filter,
90+
name,
91+
max_values,
92+
proxy,
7393
):
7494
client = AuthenticatedClient(
7595
base_url=base_url, token=api_key, proxy=convert_proxy(proxy)
@@ -87,29 +107,53 @@ def ds_lrq_facet_values(
87107

88108
# Executes LRQ run loop of launch-ping-remove API requests until the query completes
89109
# with a result
110+
# Returns tuple - value, error message
90111
def ds_lrq_run_loop(
91112
client: AuthenticatedClient, body: PostQueriesLaunchQueryRequestBody
92113
):
93114
body.query_priority = PostQueriesLaunchQueryRequestBodyQueryPriority.HIGH
94115
response = post_queries.sync_detailed(client=client, json_body=body)
116+
logger().debug(response)
95117
result = response.parsed
96-
forward_tag = response.headers["x-dataset-query-forward-tag"]
97-
steps_done = result.steps_completed
98-
steps_total = result.steps_total
99-
query_id = result.id
100-
while steps_done < steps_total:
101-
response = get_queries.sync_detailed(
102-
id=query_id,
103-
query_type=body.query_type,
104-
client=client,
105-
last_step_seen=steps_done,
106-
forward_tag=forward_tag,
107-
)
108-
result = response.parsed
118+
if result:
119+
forward_tag = response.headers["x-dataset-query-forward-tag"]
109120
steps_done = result.steps_completed
110-
delete_queries.sync_detailed(id=query_id, client=client, forward_tag=forward_tag)
121+
steps_total = result.steps_total
122+
query_id = result.id
123+
retry = 0
124+
while steps_done < steps_total:
125+
response = get_queries.sync_detailed(
126+
id=query_id,
127+
query_type=body.query_type,
128+
client=client,
129+
last_step_seen=steps_done,
130+
forward_tag=forward_tag,
131+
)
132+
logger().debug(response)
133+
result = response.parsed
134+
if result:
135+
steps_done = result.steps_completed
136+
retry = 0
137+
else:
138+
# 2023-11-01: QA server is sometimes returns 500 Operation not
139+
# permitted, after several batches have been received.
140+
# Idea is to try few retries. However, based on my handful of examples
141+
# when this happened, retries has never helped.
142+
retry += 1
143+
logger().warning("Retrying: {}; {}".format(retry, response))
144+
if retry > 5:
145+
logger().error(response)
146+
raise APIException(json.loads(response.content))
147+
else:
148+
time.sleep(retry)
149+
150+
delete_queries.sync_detailed(
151+
id=query_id, client=client, forward_tag=forward_tag
152+
)
153+
return result
111154

112-
return result
155+
logger().error(response)
156+
raise APIException(json.loads(response.content))
113157

114158

115159
# Returns a valid PowerQuery incorporating provided filter, columns and limit

TA_dataset/bin/dataset_common.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import os.path as op
55
import sys
66
import time
7+
from logging import Logger
8+
from typing import Optional # noqa: F401
79

810
# adjust paths to make the Splunk app working
911
import import_declare_test # noqa: F401
@@ -12,6 +14,8 @@
1214
APP_NAME = __file__.split(op.sep)[-3]
1315
CONF_NAME = "ta_dataset"
1416

17+
LOGGER = None # type: Optional[Logger]
18+
1519

1620
# define DataSet API URL for all environments
1721
def get_url(base_url, ds_method):
@@ -29,12 +33,22 @@ def get_url(base_url, ds_method):
2933
return base_url + "/api/" + ds_api_endpoint
3034

3135

36+
def logger() -> Logger:
37+
if LOGGER:
38+
return LOGGER
39+
return logging.getLogger()
40+
41+
3242
# returns logger that logs data into file
3343
# /opt/splunk/var/log/splunk/${APP_NAME}/${suffix}
34-
def get_logger(session_key, suffix: str):
44+
# you should call this function as soon as possible, to set up proper logging
45+
def get_logger(session_key, suffix: str) -> Logger:
3546
logger = log.Logs().get_logger("{}_{}".format(APP_NAME, suffix))
3647
log_level = get_log_level(session_key, logging)
3748
logger.setLevel(log_level)
49+
50+
global LOGGER
51+
LOGGER = logger
3852
return logger
3953

4054

TA_dataset/bin/dataset_search_command.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
# -*- coding: utf-8 -*-
33

44
import json
5-
import logging
65
import re
76
import sys
87
import time
8+
from typing import Any, Dict, Union
99

1010
# ignore flake8 rule unused import, see
1111
# https://splunk.github.io/addonfactory-ucc-generator/troubleshooting/#modulenotfounderror-no-module-named-library-name
1212
import import_declare_test # noqa: F401
1313
import requests
1414
from dataset_api import (
15+
APIException,
1516
build_payload,
1617
ds_build_pq,
1718
ds_lrq_facet_values,
@@ -26,6 +27,7 @@
2627
get_logger,
2728
get_proxy,
2829
get_url,
30+
logger,
2931
relative_to_epoch,
3032
)
3133

@@ -116,17 +118,19 @@ def get_search_arguments(self):
116118
)
117119

118120

119-
def search_error_exit(self, r_json):
121+
def search_error_exit(self, r_json: Union[Dict[str, Any], str]) -> None:
122+
logger().error(r_json)
120123
if "message" in r_json:
121-
logging.error(r_json["message"])
122124
if r_json["message"].startswith("Couldn't decode API token"):
123125
error_message = ( # make API error more user-friendly
124126
"API token rejected, check add-on configuration"
125127
)
126128
else:
127129
error_message = r_json["message"]
130+
131+
if "code" in r_json:
132+
error_message += " (" + r_json["code"] + ")"
128133
else:
129-
logging.error(r_json)
130134
try:
131135
error_message = str(r_json)
132136
except Exception as e:
@@ -313,13 +317,13 @@ def generate(self):
313317
limit=ds_maxcount,
314318
proxy=proxy,
315319
)
320+
316321
logger.debug("QUERY RESULT, result={}".format(result))
317322

318323
matches_list = result.data.matches # List<LogEvent>
319324

320325
if len(matches_list) == 0:
321326
logger.warning("DataSet response success, no matches returned")
322-
# logger.warning(r_json)
323327

324328
for event in matches_list:
325329
ds_event = json.loads(
@@ -429,6 +433,9 @@ def generate(self):
429433
"DataSetFunction=getResponse, elapsed={}".format(r.elapsed)
430434
)
431435
r_json = r.json()
436+
status = r_json.get("status", "error")
437+
if status != "success":
438+
search_error_exit(self, r_json)
432439
if "results" in r_json:
433440
bucket_time = get_bucket_increments(
434441
ds_start, ds_end, ts_buckets
@@ -470,6 +477,8 @@ def generate(self):
470477
)
471478
)
472479
GeneratingCommand.flush
480+
except APIException as e:
481+
search_error_exit(self, e.payload)
473482
except Exception as e:
474483
search_error_exit(self, str(e))
475484

TA_dataset/default/data/ui/views/sdl_by_example.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
<input type="time" token="myTime" searchWhenChanged="true">
102102
<label>Time</label>
103103
<default>
104-
<earliest>-4h@m</earliest>
104+
<earliest>-15m@m</earliest>
105105
<latest>now</latest>
106106
</default>
107107
</input>

e2e/alert_action.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {setTimeout} from 'timers/promises';
44

55
test('Alert action - create and delete alert with results propagation to DataSet', async ({page}) => {
66
// GIVEN
7-
test.setTimeout(120000); //default 60s may time out since job are scheduled for every 60s
7+
test.setTimeout(200000); //default 60s may time out since job are scheduled for every 60s
88
const serverHost = 'dataset_addon_for_splunk_playwright_CI_CD_e2e_test_host';
99
const alertName = 'splunk_addon_test_alert_'+ Math.random().toString(36).substring(2,7);
1010
await removeAlertIfExists(page, alertName);
1111
// WHEN create alert from query
1212
await searchDataSet(page, "| dataset");
1313
await saveAsAlertWithDataSetTrigger(page, alertName, serverHost);
1414
// AND wait for alert job to be triggered (cron job every 1 minute)
15-
const waitTotalS = 60;
15+
const waitTotalS = 120;
1616
const waitStepS = 5;
1717
for (let i = 0; i <= waitTotalS; i = i + waitStepS) {
1818
console.log(`Waiting for splunk Job (${i}/${waitTotalS})`)

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ flake8==6.0.0; python_version <= '3.8'
22
pip==23.2.1; python_version <= '3.8'
33
pre-commit==3.3.3; python_version <= '3.8'
44
pytest==7.4.0; python_version <= '3.8'
5+
pytest-mock==3.12.0; python_version <= '3.8'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
from dataset_search_command import DataSetSearch, search_error_exit
4+
5+
6+
@pytest.mark.parametrize(
7+
"payload,expected",
8+
[
9+
({"message": "foo - A"}, "foo - A"),
10+
({"message": "foo - B", "code": "bar"}, "foo - B (bar)"),
11+
(
12+
{"message": "Couldn't decode API token"},
13+
"API token rejected, check add-on configuration",
14+
),
15+
("foo - C", "foo - C"),
16+
],
17+
)
18+
def test_search_error_exit(mocker, payload, expected):
19+
s = DataSetSearch()
20+
m = mocker.patch.object(s, "error_exit", return_value=True, autospec=True)
21+
search_error_exit(s, payload)
22+
m.assert_called_with(error="ERROR", message=expected)

0 commit comments

Comments
 (0)