Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom dashboards #979

Merged
merged 7 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,70 @@ index=_internal source=*<addon_name>* ERROR
```

> Note: <addon_name> is being replaced by the actual value during the build time.

<br>
# Custom components

UCC also supports adding your own components to the dashboard. To do this, create a **dashboard_components.txt** file in the addon's base directory.
This file should only contain specific <row></row> tags which you would like to add to your dashboard.

```
...
├── dashboard_components.txt
├── package
...
```

sample **dashboard_components.txt** structure:
```
<row>
<panel>
<title>MY PANEL IN ROW 1</title>
<chart>
...
</chart>
</panel>
</row>
<row>
<panel>
<title>MY PANEL IN ROW 2</title>
<chart>
<search>
<query>
...
</query>
</search>
<option name="charting.axisTitleX.text">...</option>
</chart>
</panel>
<panel>
<title>MY SECOND PANEL IN ROW 2</title>
</panel>
</row>
```

Next you have to add **custom** panel to your dashboard page in globalConfig.json.
The order of panels in the globalConfig corresponds to the order of rows on the dashboard.

```json
{
...
"dashboard": {
"panels": [
{
"name": "addon_version"
},
{
"name": "events_ingested_by_sourcetype"
},
{
"name": "errors_in_the_addon"
},
{
"name": "custom"
}
]
}
...
}
```
7 changes: 2 additions & 5 deletions splunk_add_on_ucc_framework/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,8 @@ def generate(
"views",
"dashboard.xml",
)
dashboard.generate_dashboard(
global_config,
ta_name,
dashboard_xml_path,
)
dashboard.generate_dashboard(global_config, ta_name, dashboard_xml_path)

else:
global_config = None
conf_file_names = []
Expand Down
32 changes: 29 additions & 3 deletions splunk_add_on_ucc_framework/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#
import logging
import os
import sys
from typing import Sequence

from splunk_add_on_ucc_framework import global_config as global_config_lib
Expand All @@ -24,12 +25,14 @@
PANEL_ADDON_VERSION = "addon_version"
PANEL_EVENTS_INGESTED_BY_SOURCETYPE = "events_ingested_by_sourcetype"
PANEL_ERRORS_IN_THE_ADDON = "errors_in_the_addon"
PANEL_CUSTOM = "custom"

SUPPORTED_PANEL_NAMES = frozenset(
[
PANEL_ADDON_VERSION,
PANEL_EVENTS_INGESTED_BY_SOURCETYPE,
PANEL_ERRORS_IN_THE_ADDON,
PANEL_CUSTOM,
]
)
SUPPORTED_PANEL_NAMES_READABLE = ", ".join(SUPPORTED_PANEL_NAMES)
Expand All @@ -48,7 +51,6 @@
"""
DASHBOARD_END = """</form>"""


PANEL_ADDON_VERSION_TEMPLATE = """ <row>
<panel>
<title>Add-on version</title>
Expand Down Expand Up @@ -103,7 +105,9 @@
"""


def generate_dashboard_content(addon_name: str, panel_names: Sequence[str]) -> str:
def generate_dashboard_content(
addon_name: str, panel_names: Sequence[str], custom_components: str
) -> str:
content = DASHBOARD_START
for panel_name in panel_names:
logger.info(f"Including {panel_name} into the dashboard page")
Expand All @@ -117,6 +121,8 @@ def generate_dashboard_content(addon_name: str, panel_names: Sequence[str]) -> s
content += PANEL_ERRORS_IN_THE_ADDON_TEMPLATE.format(
addon_name=addon_name.lower()
)
elif panel_name == PANEL_CUSTOM:
content += custom_components
else:
raise AssertionError("Should not be the case!")
content += DASHBOARD_END
Expand All @@ -136,6 +142,26 @@ def generate_dashboard(
else:
panels = global_config.dashboard.get("panels", [])
panel_names = [panel["name"] for panel in panels]
content = generate_dashboard_content(addon_name, panel_names)
custom_components = ""
if PANEL_CUSTOM in panel_names:
try:
dashboard_components_path = os.path.abspath(
os.path.join(
global_config.original_path,
os.pardir,
"dashboard_components.txt",
)
)
with open(dashboard_components_path) as file:
custom_components = "".join(file.readlines())
except FileNotFoundError:
sys.exit(
"custom dashboard page set but dashboard_components.txt file not found"
)
if not custom_components:
sys.exit(
"custom dashboard page set but dashboard_components.txt file is empty"
)
content = generate_dashboard_content(addon_name, panel_names, custom_components)
with open(dashboard_xml_file_path, "w") as dashboard_xml_file:
dashboard_xml_file.write(content)
145 changes: 130 additions & 15 deletions tests/unit/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
from splunk_add_on_ucc_framework import dashboard


def test_generate_dashboard_when_dashboard_does_not_exist(
global_config_all_json, tmp_path
):
dashboard_xml_file_path = tmp_path / "dashboard.xml"
import os.path
from unittest import mock
import pytest

dashboard.generate_dashboard(
global_config_all_json,
"Splunk_TA_UCCExample",
str(dashboard_xml_file_path),
)
from splunk_add_on_ucc_framework import dashboard
import tests.unit.helpers as helpers
from splunk_add_on_ucc_framework import global_config as gc

expected_dashboard_content = """<form version="1.1">
default_dashboard_content_start = """<form version="1.1">
<label>Monitoring Dashboard</label>
<fieldset submitButton="false">
<input type="time" token="log_time">
Expand Down Expand Up @@ -72,9 +66,49 @@ def test_generate_dashboard_when_dashboard_does_not_exist(
</event>
</panel>
</row>
</form>"""
"""

default_dashboard_content_end = "</form>"

custom_dashboard_components = """<row>
<panel>
<title>CUSTOM ROW 1</title>
</panel>
</row>
<row>
<panel>
<title>CUSTOM ROW 2</title>
<chart>
<search>
<query>index=_internal sourcetype="test*" "is processing SQS messages:" sqs_msg_action
</query>
<earliest>-14d@d</earliest>
<latest>now</latest>
<sampleRatio>1</sampleRatio>
</search>
<option name="charting.axisTitleX.text">Time</option>
<option name="charting.axisTitleX.visibility">visible</option>
</chart>
</panel>
</row>
"""


def test_generate_dashboard_when_dashboard_does_not_exist(
global_config_all_json, tmp_path
):
dashboard_xml_file_path = tmp_path / "dashboard.xml"

dashboard.generate_dashboard(
global_config_all_json,
"Splunk_TA_UCCExample",
str(dashboard_xml_file_path),
)

expected_content = default_dashboard_content_start + default_dashboard_content_end

with open(dashboard_xml_file_path) as dashboard_xml_file:
assert expected_dashboard_content == dashboard_xml_file.read()
assert expected_content == dashboard_xml_file.read()


def test_generate_dashboard_when_dashboard_already_exists(
Expand All @@ -97,3 +131,84 @@ def test_generate_dashboard_when_dashboard_already_exists(
f"the existing dashboard file."
)
assert expected_log_warning_message in caplog.text


def test_generate_dashboard_with_custom_components(tmp_path):
global_config_path = helpers.get_testdata_file_path(
"valid_config_with_custom_dashboard.json"
)
global_config = gc.GlobalConfig(global_config_path, False)
tmp_ta_path = tmp_path / "test_ta"
os.makedirs(tmp_ta_path)
custom_dash_path = os.path.join(tmp_ta_path, "dashboard_components.txt")
with open(custom_dash_path, "w") as file:
file.write(custom_dashboard_components)

with mock.patch("os.path.abspath") as path_abs:
path_abs.return_value = custom_dash_path
dashboard_path = tmp_path / "dashboard.xml"
dashboard.generate_dashboard(
global_config,
"Splunk_TA_UCCExample",
str(dashboard_path),
)

expected_content = (
default_dashboard_content_start
+ custom_dashboard_components
+ default_dashboard_content_end
)

with open(dashboard_path) as dashboard_xml_file:
assert expected_content == dashboard_xml_file.read()


def test_generate_dashboard_with_custom_components_no_file(tmp_path):
global_config_path = helpers.get_testdata_file_path(
"valid_config_with_custom_dashboard.json"
)
global_config = gc.GlobalConfig(global_config_path, False)

expected_msg = (
"custom dashboard page set but dashboard_components.txt file not found"
)
with pytest.raises(SystemExit) as sys_exit:
dashboard_path = tmp_path / "dashboard.xml"
dashboard.generate_dashboard(
global_config,
"Splunk_TA_UCCExample",
str(dashboard_path),
)
assert expected_msg in sys_exit.value.args


def test_generate_dashboard_with_custom_components_empty_file(tmp_path):
global_config_path = helpers.get_testdata_file_path(
"valid_config_with_custom_dashboard.json"
)
global_config = gc.GlobalConfig(global_config_path, False)
tmp_ta_path = tmp_path / "test_ta"
os.makedirs(tmp_ta_path)
custom_dash_path = os.path.join(tmp_ta_path, "dashboard_components.txt")
with open(custom_dash_path, "w") as file:
file.write("")

expected_msg = (
"custom dashboard page set but dashboard_components.txt file is empty"
)
with pytest.raises(SystemExit) as sys_exit:
with mock.patch("os.path.abspath") as path_abs:
path_abs.return_value = custom_dash_path
dashboard_path = tmp_path / "dashboard.xml"
dashboard.generate_dashboard(
global_config,
"Splunk_TA_UCCExample",
str(dashboard_path),
)
dashboard_path = tmp_path / "dashboard.xml"
dashboard.generate_dashboard(
global_config,
"Splunk_TA_UCCExample",
str(dashboard_path),
)
assert expected_msg in sys_exit.value.args
70 changes: 70 additions & 0 deletions tests/unit/testdata/valid_config_with_custom_dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"pages": {
"configuration": {
"tabs": [
{
"name": "logging",
"entity": [
{
"type": "singleSelect",
"label": "Log level",
"options": {
"disableSearch": true,
"autoCompleteFields": [
{
"value": "DEBUG",
"label": "DEBUG"
},
{
"value": "INFO",
"label": "INFO"
},
{
"value": "WARNING",
"label": "WARNING"
},
{
"value": "ERROR",
"label": "ERROR"
},
{
"value": "CRITICAL",
"label": "CRITICAL"
}
]
},
"defaultValue": "INFO",
"field": "loglevel"
}
],
"title": "Logging"
}
],
"title": "Configuration",
"description": "Set up your add-on"
},
"dashboard": {
"panels": [
{
"name": "addon_version"
},
{
"name": "events_ingested_by_sourcetype"
},
{
"name": "errors_in_the_addon"
},
{
"name": "custom"
}
]
}
},
"meta": {
"name": "Splunk_TA_UCCExample",
"restRoot": "splunk_ta_uccexample",
"version": "1.0.0",
"displayName": "Splunk UCC test Add-on",
"schemaVersion": "0.0.3"
}
}