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

Feature: Support importing multiple dashboards from directory #27

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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