Skip to content

Commit 3e6f9de

Browse files
authored
feat(grafana): add grafana support (#137)
### Description This PR builds on the [previously-added Prometheus support](#134) to add Grafana support by: - adding a `grafana_params` section to the top-level `observability` parameter section - deploying a Grafana server - implementing dashboard provisioning The `grafana` module from the `ethereum-package` package was used as inspiration, but modified to simplify devX by removing support for inline dashboards and improving remote dashboard source support. Additionally, this PR implements API provisioning using the official [grizzly](https://grafana.github.io/grizzly/) tool, over the existing file-based provisioning approach to simplify the process of keeping Kurtosis Grafana in-sync with hosted Grafana. To this end, two new repositories ([`grafana-dashboards`](https://github.com/ethereum-optimism/grafana-dashboards), [`grafana-dashboards-public`](https://github.com/ethereum-optimism/grafana-dashboards-public)) have been created, with the intention of tracking extant public & private dashboards in hosted Grafana. This PR has been tested and successfully deploys a Grafana server with including all public dashboards present on our hosted Grafana instance, organized into the same folder structure: <img width="1214" alt="image" src="https://github.com/user-attachments/assets/9a007f0e-fb95-4399-9a96-8be3f33e4eba" /> Not all dashboards are yet at full parity, but a fair number of them do show data: <img width="1807" alt="image" src="https://github.com/user-attachments/assets/d1223067-9d31-4b8c-a76a-a9d7b4b4c787" /> If you want to try this out locally, add the following snippet to your params file: ```yaml optimism_package: observability: grafana_params: dashboard_sources: - github.com/ethereum-optimism/grafana-dashboards-public/resources@aa35389fc5dec4043838757e2372368c3efb0a29 ``` Remaining work: - continue converging metrics to enable additional dashboards - implement promtail/loki support to enable log-based dashboard panels - deploy any services required for certain dashboards (ie replica-healthcheck) (?) - support a subset of existing dashboards using tags/folders - automatic updates of the [`grafana-dashboards`](https://github.com/ethereum-optimism/grafana-dashboards) repository
1 parent 81fa02f commit 3e6f9de

File tree

9 files changed

+230
-4
lines changed

9 files changed

+230
-4
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ optimism_package:
9393
# Prometheus docker image to use
9494
# Defaults to the latest image
9595
image: "prom/prometheus:latest"
96+
# Default grafana configuration
97+
grafana_params:
98+
# A list of locators for grafana dashboards to be loaded be the grafana service
99+
dashboard_sources: []
100+
# Resource management for grafana container
101+
# CPU is milicores
102+
# RAM is in MB
103+
min_cpu: 10
104+
max_cpu: 1000
105+
min_mem: 128
106+
max_mem: 2048
107+
# Grafana docker image to use
108+
# Defaults to the latest image
109+
image: "grafana/grafana:latest"
96110
# Interop configuration
97111
interop:
98112
# Whether or not to enable interop mode

main.star

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ op_supervisor_launcher = import_module(
77

88
observability = import_module("./src/observability/observability.star")
99
prometheus = import_module("./src/observability/prometheus/prometheus_launcher.star")
10+
grafana = import_module("./src/observability/grafana/grafana_launcher.star")
1011

1112
wait_for_sync = import_module("./src/wait/wait_for_sync.star")
1213
input_parser = import_module("./src/package_io/input_parser.star")
@@ -131,14 +132,22 @@ def run(plan, args):
131132
observability_helper,
132133
)
133134

134-
if observability_helper.enabled:
135+
if observability_helper.enabled and len(observability_helper.metrics_jobs) > 0:
135136
plan.print("Launching prometheus...")
136137
prometheus_private_url = prometheus.launch_prometheus(
137138
plan,
138139
observability_helper,
139140
global_node_selectors,
140141
)
141142

143+
plan.print("Launching grafana...")
144+
grafana.launch_grafana(
145+
plan,
146+
prometheus_private_url,
147+
global_node_selectors,
148+
observability_params.grafana_params,
149+
)
150+
142151

143152
def get_l1_config(all_l1_participants, l1_network_params, l1_network_id):
144153
env_vars = {}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
util = import_module("../../util.star")
2+
3+
ethereum_package_shared_utils = import_module(
4+
"github.com/ethpandaops/ethereum-package/src/shared_utils/shared_utils.star"
5+
)
6+
7+
SERVICE_NAME = "grafana"
8+
9+
HTTP_PORT_ID = "http"
10+
HTTP_PORT_NUMBER_UINT16 = 3000
11+
12+
TEMPLATES_FILEPATH = "./templates"
13+
14+
DATASOURCE_UID = "grafanacloud-prom"
15+
DATASOURCE_CONFIG_TEMPLATE_FILEPATH = TEMPLATES_FILEPATH + "/datasource.yml.tmpl"
16+
DATASOURCE_CONFIG_REL_FILEPATH = "datasources/datasource.yml"
17+
18+
CONFIG_DIRPATH_ON_SERVICE = "/config"
19+
20+
USED_PORTS = {
21+
HTTP_PORT_ID: ethereum_package_shared_utils.new_port_spec(
22+
HTTP_PORT_NUMBER_UINT16,
23+
ethereum_package_shared_utils.TCP_PROTOCOL,
24+
ethereum_package_shared_utils.HTTP_APPLICATION_PROTOCOL,
25+
)
26+
}
27+
28+
29+
def launch_grafana(
30+
plan,
31+
prometheus_private_url,
32+
global_node_selectors,
33+
grafana_params,
34+
):
35+
datasource_config_template = read_file(DATASOURCE_CONFIG_TEMPLATE_FILEPATH)
36+
37+
grafana_config_artifact_name = upload_grafana_config(
38+
plan,
39+
datasource_config_template,
40+
prometheus_private_url,
41+
)
42+
43+
config = get_config(
44+
grafana_config_artifact_name,
45+
global_node_selectors,
46+
grafana_params,
47+
)
48+
49+
service = plan.add_service(SERVICE_NAME, config)
50+
51+
service_url = "http://{0}:{1}".format(
52+
service.ip_address, service.ports[HTTP_PORT_ID].number
53+
)
54+
55+
provision_dashboards(plan, service_url, grafana_params.dashboard_sources)
56+
57+
return service_url
58+
59+
60+
def upload_grafana_config(
61+
plan,
62+
datasource_config_template,
63+
prometheus_private_url,
64+
):
65+
datasource_data = new_datasource_config_template_data(prometheus_private_url)
66+
datasource_template_and_data = ethereum_package_shared_utils.new_template_and_data(
67+
datasource_config_template, datasource_data
68+
)
69+
70+
template_and_data_by_rel_dest_filepath = {
71+
DATASOURCE_CONFIG_REL_FILEPATH: datasource_template_and_data,
72+
}
73+
74+
grafana_config_artifact_name = plan.render_templates(
75+
template_and_data_by_rel_dest_filepath, name="grafana-config"
76+
)
77+
78+
return grafana_config_artifact_name
79+
80+
81+
def new_datasource_config_template_data(prometheus_url):
82+
return {"PrometheusUID": DATASOURCE_UID, "PrometheusURL": prometheus_url}
83+
84+
85+
def get_config(
86+
grafana_config_artifact_name,
87+
node_selectors,
88+
grafana_params,
89+
):
90+
return ServiceConfig(
91+
image=grafana_params.image,
92+
ports=USED_PORTS,
93+
env_vars={
94+
"GF_PATHS_PROVISIONING": CONFIG_DIRPATH_ON_SERVICE,
95+
"GF_AUTH_ANONYMOUS_ENABLED": "true",
96+
"GF_AUTH_ANONYMOUS_ORG_ROLE": "Admin",
97+
"GF_AUTH_ANONYMOUS_ORG_NAME": "Main Org.",
98+
# "GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH": "/dashboards/default.json",
99+
},
100+
files={
101+
CONFIG_DIRPATH_ON_SERVICE: grafana_config_artifact_name,
102+
},
103+
min_cpu=grafana_params.min_cpu,
104+
max_cpu=grafana_params.max_cpu,
105+
min_memory=grafana_params.min_mem,
106+
max_memory=grafana_params.max_mem,
107+
node_selectors=node_selectors,
108+
)
109+
110+
111+
def provision_dashboards(plan, service_url, dashboard_sources):
112+
if len(dashboard_sources) == 0:
113+
return
114+
115+
def grr_push(dir):
116+
return 'grr push "{0}" -e --disable-reporting'.format(dir)
117+
118+
def grr_push_dashboards(name):
119+
return [
120+
grr_push("{0}/folders".format(name)),
121+
grr_push("{0}/dashboards".format(name)),
122+
]
123+
124+
grr_commands = [
125+
"grr config create-context kurtosis",
126+
]
127+
128+
files = {}
129+
for index, dashboard_src in enumerate(dashboard_sources):
130+
dashboard_name = "dashboards-{0}".format(index)
131+
dashboard_artifact_name = plan.upload_files(dashboard_src, name=dashboard_name)
132+
133+
files[dashboard_name] = dashboard_artifact_name
134+
grr_commands += grr_push_dashboards(dashboard_name)
135+
136+
plan.run_sh(
137+
description="upload dashboards",
138+
image="grafana/grizzly:main-0b88d01",
139+
env_vars={
140+
"GRAFANA_URL": service_url,
141+
},
142+
files=files,
143+
run=util.join_cmds(grr_commands),
144+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Prometheus
5+
type: prometheus
6+
access: proxy
7+
orgId: 1
8+
uid: {{ .PrometheusUID }}
9+
url: {{ .PrometheusURL }}
10+
basicAuth: false
11+
isDefault: true
12+
editable: true

src/observability/observability.star

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ def register_service_metrics_job(
9797
):
9898
labels = {
9999
"service": service_name,
100+
"namespace": "kurtosis",
101+
"stack_optimism_io_network": "kurtosis",
100102
}
101103
labels.update(additional_labels)
102104

src/observability/prometheus/prometheus_launcher.star

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ def launch_prometheus(
66
observability_helper,
77
global_node_selectors,
88
):
9-
if len(observability_helper.metrics_jobs) == 0:
10-
return None
11-
129
prometheus_params = observability_helper.params.prometheus_params
1310

1411
prometheus_url = prometheus.run(

src/package_io/input_parser.star

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ def input_parser(plan, input_args):
7777
min_mem=results["observability"]["prometheus_params"]["min_mem"],
7878
max_mem=results["observability"]["prometheus_params"]["max_mem"],
7979
),
80+
grafana_params=struct(
81+
image=results["observability"]["grafana_params"]["image"],
82+
dashboard_sources=results["observability"]["grafana_params"][
83+
"dashboard_sources"
84+
],
85+
min_cpu=results["observability"]["grafana_params"]["min_cpu"],
86+
max_cpu=results["observability"]["grafana_params"]["max_cpu"],
87+
min_mem=results["observability"]["grafana_params"]["min_mem"],
88+
max_mem=results["observability"]["grafana_params"]["max_mem"],
89+
),
8090
),
8191
interop=struct(
8292
enabled=results["interop"]["enabled"],
@@ -225,6 +235,11 @@ def parse_network_params(plan, input_args):
225235
input_args.get("observability", {}).get("prometheus_params", {})
226236
)
227237

238+
results["observability"]["grafana_params"] = default_grafana_params()
239+
results["observability"]["grafana_params"].update(
240+
input_args.get("observability", {}).get("grafana_params", {})
241+
)
242+
228243
# configure interop
229244

230245
results["interop"] = default_interop_params()
@@ -383,6 +398,17 @@ def default_prometheus_params():
383398
}
384399

385400

401+
def default_grafana_params():
402+
return {
403+
"image": "grafana/grafana:latest",
404+
"dashboard_sources": [],
405+
"min_cpu": 10,
406+
"max_cpu": 1000,
407+
"min_mem": 128,
408+
"max_mem": 2048,
409+
}
410+
411+
386412
def default_interop_params():
387413
return {
388414
"enabled": False,

src/package_io/sanity_check.star

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
OBSERVABILITY_PARAMS = [
22
"enabled",
33
"prometheus_params",
4+
"grafana_params",
45
]
56

67
PROMETHEUS_PARAMS = [
@@ -13,6 +14,15 @@ PROMETHEUS_PARAMS = [
1314
"max_mem",
1415
]
1516

17+
GRAFANA_PARAMS = [
18+
"image",
19+
"dashboard_sources",
20+
"min_cpu",
21+
"max_cpu",
22+
"min_mem",
23+
"max_mem",
24+
]
25+
1626
INTEROP_PARAMS = [
1727
"enabled",
1828
"supervisor_params",
@@ -166,6 +176,14 @@ def sanity_check(plan, optimism_config):
166176
PROMETHEUS_PARAMS,
167177
)
168178

179+
if "grafana_params" in optimism_config["observability"]:
180+
validate_params(
181+
plan,
182+
optimism_config["observability"],
183+
"grafana_params",
184+
GRAFANA_PARAMS,
185+
)
186+
169187
if "interop" in optimism_config:
170188
validate_params(
171189
plan,

src/util.star

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@ def label_from_image(image):
6666
if len(label) > max_length:
6767
label = label[-max_length:]
6868
return label
69+
70+
71+
def join_cmds(commands):
72+
return " && ".join(commands)

0 commit comments

Comments
 (0)