Skip to content

Commit

Permalink
Implemented import dashboards from a directory
Browse files Browse the repository at this point in the history
  • Loading branch information
vitaliirymar authored and amotl committed Oct 16, 2024
1 parent fbd5375 commit 88b89bb
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 36 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Fixed folder argument issue
- Fixed import dashboards into a folder
- Added keep-uid argument to preserve the dashboard uid provided in file
- Added an option to import dashboards from a directory

Thanks, @vrymar.

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ Watch the input dashboard for changes on disk, and re-upload it, when changed.
grafana-import import --overwrite --reload -i gd-prometheus.py
```

### Import dashboards from a directory
Import all dashboards from provided directory
```shell
grafana-import import -i "./dashboards_folder"
```

### Export
Export the dashboard titled `my-first-dashboard` to the default export directory.
```bash
Expand Down Expand Up @@ -198,19 +204,19 @@ jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main
#### Usage
Render dashboard defined in [Grafonnet]/[Jsonnet].
```shell
grafana-import import --overwrite -i /path/to/faro.jsonnet
grafana-import import --overwrite -i ./path/to/faro.jsonnet
```

### grafana-dashboard
Render dashboard defined using [grafana-dashboard].
```shell
grafana-import import --overwrite -i /path/to/gd-dashboard.py
grafana-import import --overwrite -i ./path/to/gd-dashboard.py
```

### grafanalib
Render dashboard defined using [grafanalib].
```shell
grafana-import import --overwrite -i /path/to/gl-dashboard.py
grafana-import import --overwrite -i ./path/to/gl-dashboard.py
```


Expand Down
69 changes: 45 additions & 24 deletions grafana_import/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def save_dashboard(config, args, base_path, dashboard_name, dashboard, action):
try:
output = open(output_file, "w")
except OSError as e:
print("File {0} error: {1}.".format(output_file, e.strerror))
logger.error("File {0} error: {1}.".format(output_file, e.strerror))

Check warning on line 49 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L49

Added line #L49 was not covered by tests
sys.exit(2)

content = None
Expand All @@ -56,7 +56,7 @@ def save_dashboard(config, args, base_path, dashboard_name, dashboard, action):
content = json.dumps(dashboard["dashboard"])
output.write(content)
output.close()
print(f"OK: Dashboard '{dashboard_name}' {action} to: {output_file}")
logger.info(f"OK: Dashboard '{dashboard_name}' {action} to: {output_file}")


class myArgs:
Expand Down Expand Up @@ -212,7 +212,7 @@ def main():
if args.action == "exporter" and (
"dashboard_name" not in config["general"] or config["general"]["dashboard_name"] is None
):
print("ERROR: no dashboard has been specified.")
logger.error("ERROR: no dashboard has been specified.")

Check warning on line 215 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L215

Added line #L215 was not covered by tests
sys.exit(1)

config["check_folder"] = False
Expand All @@ -238,29 +238,46 @@ def main():
try:
grafana_api = Grafana.Grafana(**params)
except Exception as e:
print(f"ERROR: {e}")
logger.error(f"ERROR: {e}")

Check warning on line 241 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L241

Added line #L241 was not covered by tests
sys.exit(1)

# Import
if args.action == "import":
if args.dashboard_file is None:
print("ERROR: no file to import provided!")
logger.error("ERROR: no file to import provided!")

Check warning on line 247 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L247

Added line #L247 was not covered by tests
sys.exit(1)

# Compute effective input file path.
import_path = ""
import_file = args.dashboard_file
import_files = []

if not re.search(r"^(?:(?:/)|(?:\.?\./))", import_file):
import_path = base_path
if "imports_path" in config["general"]:
import_path = os.path.join(import_path, config["general"]["imports_path"])
import_file = os.path.join(import_path, import_file)

def process_dashboard():
if "import_path" in config["general"]:
import_path = os.path.join(import_path, config["general"]["import_path"])
import_file = os.path.join(import_path, import_file)
import_files.append(import_file)

Check warning on line 260 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L257-L260

Added lines #L257 - L260 were not covered by tests
else:
if os.path.isfile(import_file):
logger.info(f"The path is a file: '{import_file}'")
import_file = os.path.join(import_path, import_file)
import_files.append(import_file)

if os.path.isdir(import_file):
logger.info(f"The path is a directory: '{import_file}'")
import_files = [

Check warning on line 269 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L268-L269

Added lines #L268 - L269 were not covered by tests
os.path.join(import_file, f)
for f in os.listdir(import_file)
if os.path.isfile(os.path.join(import_file, f))
]
logger.info(f"Found the following files: '{import_files}' in dir '{import_file}'")

Check warning on line 274 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L274

Added line #L274 was not covered by tests

def process_dashboard(file_path):
try:
dash = read_dashboard_file(import_file)
dash = read_dashboard_file(file_path)
except Exception as ex:
msg = f"Failed to load dashboard from: {import_file}. Reason: {ex}"
msg = f"Failed to load dashboard from: {file_path}. Reason: {ex}"

Check warning on line 280 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L280

Added line #L280 was not covered by tests
logger.exception(msg)
raise IOError(msg) from ex

Expand All @@ -280,13 +297,17 @@ def process_dashboard():
logger.error(msg)
raise IOError(msg)

try:
process_dashboard()
except Exception:
sys.exit(1)
for file in import_files:
print(f"Processing file: {file}")
try:
process_dashboard(file)
except Exception as e:
logger.error(f"Failed to process file {file}. Reason: {str(e)}")
continue

Check warning on line 306 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L304-L306

Added lines #L304 - L306 were not covered by tests

if args.reload:
watchdog_service(import_file, process_dashboard)
for file in import_files:
watchdog_service(import_file, process_dashboard(file))

Check warning on line 310 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L309-L310

Added lines #L309 - L310 were not covered by tests

sys.exit(0)

Expand All @@ -295,19 +316,19 @@ def process_dashboard():
dashboard_name = config["general"]["dashboard_name"]
try:
grafana_api.remove_dashboard(dashboard_name)
print(f"OK: Dashboard removed: {dashboard_name}")
logger.info(f"OK: Dashboard removed: {dashboard_name}")
sys.exit(0)
except Grafana.GrafanaDashboardNotFoundError as exp:
print(f"KO: Dashboard not found in folder '{exp.folder}': {exp.dashboard}")
logger.info(f"KO: Dashboard not found in folder '{exp.folder}': {exp.dashboard}")

Check warning on line 322 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L322

Added line #L322 was not covered by tests
sys.exit(1)
except Grafana.GrafanaFolderNotFoundError as exp:
print(f"KO: Folder not found: {exp.folder}")
logger.info(f"KO: Folder not found: {exp.folder}")

Check warning on line 325 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L325

Added line #L325 was not covered by tests
sys.exit(1)
except GrafanaApi.GrafanaBadInputError as exp:
print(f"KO: Removing dashboard failed: {dashboard_name}. Reason: {exp}")
logger.info(f"KO: Removing dashboard failed: {dashboard_name}. Reason: {exp}")

Check warning on line 328 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L328

Added line #L328 was not covered by tests
sys.exit(1)
except Exception:
print("ERROR: Dashboard '{0}' remove exception '{1}'".format(dashboard_name, traceback.format_exc()))
logger.info("ERROR: Dashboard '{0}' remove exception '{1}'".format(dashboard_name, traceback.format_exc()))

Check warning on line 331 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L331

Added line #L331 was not covered by tests
sys.exit(1)

# Export
Expand All @@ -316,10 +337,10 @@ def process_dashboard():
try:
dash = grafana_api.export_dashboard(dashboard_name)
except (Grafana.GrafanaFolderNotFoundError, Grafana.GrafanaDashboardNotFoundError):
print("KO: Dashboard name not found: {0}".format(dashboard_name))
logger.info("KO: Dashboard name not found: {0}".format(dashboard_name))
sys.exit(1)
except Exception:
print("ERROR: Dashboard '{0}' export exception '{1}'".format(dashboard_name, traceback.format_exc()))
logger.info("ERROR: Dashboard '{0}' export exception '{1}'".format(dashboard_name, traceback.format_exc()))

Check warning on line 343 in grafana_import/cli.py

View check run for this annotation

Codecov / codecov/patch

grafana_import/cli.py#L343

Added line #L343 was not covered by tests
sys.exit(1)

if dash is not None:
Expand Down
15 changes: 6 additions & 9 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_import_dashboard_success(mocked_grafana, mocked_responses, tmp_path, ca


@pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"])
def test_export_dashboard_success(mocked_grafana, mocked_responses, capsys, use_settings):
def test_export_dashboard_success(mocked_grafana, mocked_responses, caplog, use_settings):
"""
Verify "export dashboard" works.
"""
Expand All @@ -66,12 +66,11 @@ def test_export_dashboard_success(mocked_grafana, mocked_responses, capsys, use_
m.stop()
assert ex.match("0")

out, err = capsys.readouterr()
assert re.match(r"OK: Dashboard 'foobar' exported to: ./foobar_\d+.json", out)
assert re.match(r".*OK: Dashboard 'foobar' exported to: ./foobar_\d+.json.*", caplog.text, re.DOTALL)


@pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"])
def test_export_dashboard_notfound(mocked_grafana, mocked_responses, capsys, use_settings):
def test_export_dashboard_notfound(mocked_grafana, mocked_responses, caplog, use_settings):
"""
Verify "export dashboard" fails appropriately when addressed dashboard does not exist.
"""
Expand All @@ -88,12 +87,11 @@ def test_export_dashboard_notfound(mocked_grafana, mocked_responses, capsys, use
main()
assert ex.match("1")

out, err = capsys.readouterr()
assert "Dashboard name not found: foobar" in out
assert "Dashboard name not found: foobar" in caplog.text


@pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"])
def test_remove_dashboard_success(mocked_grafana, mocked_responses, capsys, use_settings):
def test_remove_dashboard_success(mocked_grafana, mocked_responses, caplog, use_settings):
"""
Verify "remove dashboard" works.
"""
Expand All @@ -110,5 +108,4 @@ def test_remove_dashboard_success(mocked_grafana, mocked_responses, capsys, use_
main()
assert ex.match("0")

out, err = capsys.readouterr()
assert "OK: Dashboard removed: foobar" in out
assert "OK: Dashboard removed: foobar" in caplog.text

0 comments on commit 88b89bb

Please sign in to comment.