Skip to content

Commit f5817a0

Browse files
added comprehensive task to get all dashboard variables and their values
1 parent fc1dd10 commit f5817a0

File tree

5 files changed

+333
-19
lines changed

5 files changed

+333
-19
lines changed

integrations/source_api_processors/grafana_api_processor.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def fetch_dashboards(self):
7070
def fetch_dashboard_details(self, uid):
7171
try:
7272
url = '{}/api/dashboards/uid/{}'.format(self.__host, uid)
73+
print(url)
7374
response = requests.get(url, headers=self.headers, verify=self.__ssl_verify)
75+
print(response.text)
7476
if response and response.status_code == 200:
7577
return response.json()
7678
except Exception as e:
@@ -139,6 +141,212 @@ def fetch_dashboard_variable_label_values(self, promql_datasource_uid, label_nam
139141
logger.error(f"Exception occurred while fetching promql metric labels for {label_name} with error: {e}")
140142
return []
141143

144+
def get_datasource_by_uid(self, ds_uid):
145+
"""Fetches datasource details by its UID."""
146+
try:
147+
url = f'{self.__host}/api/datasources/uid/{ds_uid}'
148+
response = requests.get(url, headers=self.headers, verify=self.__ssl_verify)
149+
if response and response.status_code == 200:
150+
return response.json()
151+
logger.error(f"Failed to get datasource for uid {ds_uid}. Status: {response.status_code}, Body: {response.text}")
152+
return None
153+
except Exception as e:
154+
logger.error(f"Exception fetching datasource {ds_uid}: {e}")
155+
return None
156+
157+
def get_default_datasource_by_type(self, ds_type):
158+
"""Fetches the default datasource of a given type."""
159+
try:
160+
datasources = self.fetch_data_sources()
161+
if not datasources:
162+
return None
163+
164+
# Find the default datasource of the specified type
165+
for ds in datasources:
166+
if ds.get('type') == ds_type and ds.get('isDefault', False):
167+
return ds
168+
# If no default found, return the first one of that type
169+
for ds in datasources:
170+
if ds.get('type') == ds_type:
171+
return ds
172+
return None
173+
except Exception as e:
174+
logger.error(f"Exception fetching default datasource of type {ds_type}: {e}")
175+
return None
176+
177+
def get_dashboard_variables(self, dashboard_uid):
178+
"""
179+
Fetches and resolves all variables for a given Grafana dashboard.
180+
Handles dependencies between variables and supports multiple variable types.
181+
"""
182+
import re
183+
184+
try:
185+
dashboard_data = self.fetch_dashboard_details(dashboard_uid)
186+
187+
if not dashboard_data or 'dashboard' not in dashboard_data:
188+
logger.error("Could not fetch or parse dashboard data.")
189+
return {}
190+
191+
dashboard_json = dashboard_data['dashboard']
192+
variables = dashboard_json.get('templating', {}).get('list', [])
193+
194+
resolved_variables = {}
195+
196+
for var in variables:
197+
var_name = var.get('name')
198+
var_type = var.get('type')
199+
200+
if not var_name or not var_type:
201+
continue
202+
203+
values = []
204+
if var_type == 'query':
205+
values = self._resolve_query_variable(var, resolved_variables)
206+
elif var_type == 'datasource':
207+
values = self._resolve_datasource_variable(var)
208+
elif var_type == 'custom':
209+
query = self._substitute_variables(var.get('query', ''), resolved_variables)
210+
values = [v.strip() for v in query.split(',')]
211+
elif var_type == 'constant':
212+
values = [self._substitute_variables(var.get('query', ''), resolved_variables)]
213+
elif var_type == 'textbox':
214+
current_val = var.get('current', {}).get('value')
215+
query_val = self._substitute_variables(var.get('query', ''), resolved_variables)
216+
values = [current_val or query_val]
217+
elif var_type == 'interval':
218+
query = self._substitute_variables(var.get('query', ''), resolved_variables)
219+
values = [v.strip() for v in query.split(',')]
220+
221+
if values:
222+
resolved_variables[var_name] = values
223+
224+
logger.info(f"For dashboard '{dashboard_json.get('title')}', fetched variable values: {resolved_variables}")
225+
return {
226+
'dashboard_title': dashboard_json.get('title'),
227+
'dashboard_uid': dashboard_uid,
228+
'variables': resolved_variables
229+
}
230+
except Exception as e:
231+
logger.error(f"Exception occurred while fetching dashboard variables for {dashboard_uid}: {e}")
232+
return {}
233+
234+
def _substitute_variables(self, query_string, resolved_variables):
235+
"""Substitutes variables in query strings."""
236+
import re
237+
238+
for name, value in resolved_variables.items():
239+
sub_value = value[0] if isinstance(value, list) and value else (value if isinstance(value, str) else "")
240+
query_string = re.sub(r'\$' + re.escape(name) + r'\b', sub_value, query_string)
241+
query_string = re.sub(r'\$\{' + re.escape(name) + r'\}', sub_value, query_string)
242+
return query_string
243+
244+
def _resolve_datasource_variable(self, var):
245+
"""Resolves a 'datasource' type variable."""
246+
ds_type = var.get('query')
247+
if not ds_type:
248+
return []
249+
250+
try:
251+
datasources = self.fetch_data_sources()
252+
if not datasources:
253+
return []
254+
255+
# Get all datasources of this type
256+
matching_datasources = [ds['uid'] for ds in datasources if ds.get('type') == ds_type]
257+
258+
# If the current value is 'default', we should return the UID of the default datasource
259+
current_value = var.get('current', {}).get('value')
260+
if current_value == 'default':
261+
default_ds = self.get_default_datasource_by_type(ds_type)
262+
if default_ds:
263+
return [default_ds['uid']]
264+
265+
return matching_datasources
266+
except Exception as e:
267+
logger.error(f"Exception fetching datasources: {e}")
268+
return []
269+
270+
def _resolve_query_variable(self, var, resolved_variables):
271+
"""
272+
Resolves a 'query' type variable.
273+
Currently supports Prometheus datasources.
274+
"""
275+
import re
276+
277+
datasource = var.get('datasource')
278+
query = var.get('query')
279+
280+
if not datasource or not query:
281+
return []
282+
283+
ds_uid = datasource.get('uid') if isinstance(datasource, dict) else datasource
284+
ds_uid = self._substitute_variables(ds_uid, resolved_variables)
285+
286+
# Handle the case where ds_uid is "default" - need to resolve it to actual datasource
287+
if ds_uid == 'default':
288+
ds_type = datasource.get('type') if isinstance(datasource, dict) else 'prometheus' # assume prometheus if not specified
289+
datasource_details = self.get_default_datasource_by_type(ds_type)
290+
if datasource_details:
291+
ds_uid = datasource_details['uid']
292+
else:
293+
logger.warning(f"Could not find default datasource of type '{ds_type}' for query variable '{var.get('name')}'.")
294+
return []
295+
else:
296+
datasource_details = self.get_datasource_by_uid(ds_uid)
297+
298+
if not datasource_details or datasource_details.get('type') != 'prometheus':
299+
logger.warning(f"Unsupported or unknown datasource type for query variable '{var.get('name')}'.")
300+
return []
301+
302+
query = self._substitute_variables(str(query), resolved_variables)
303+
304+
# Prometheus query handling
305+
# Case 1: label_values(label) or label_values(metric, label)
306+
label_values_match = re.search(r'label_values\((?:.*\s*,\s*)?(\w+)\)', query)
307+
if label_values_match:
308+
label = label_values_match.group(1)
309+
return self.fetch_dashboard_variable_label_values(ds_uid, label)
310+
311+
# Case 2: metrics(pattern) -> label_values(__name__)
312+
if re.match(r'metrics\(.*\)', query):
313+
return self.fetch_dashboard_variable_label_values(ds_uid, '__name__')
314+
315+
# Case 3: Generic PromQL query (including query_result(query))
316+
try:
317+
if query.startswith('query_result(') and query.endswith(')'):
318+
query = query[len('query_result('):-1]
319+
320+
url = f'{self.__host}/api/datasources/proxy/uid/{ds_uid}/api/v1/query'
321+
params = {'query': query}
322+
response = requests.get(url, headers=self.headers, params=params, verify=self.__ssl_verify)
323+
if response and response.status_code == 200:
324+
results = response.json().get('data', {}).get('result', [])
325+
values = []
326+
for res in results:
327+
metric_labels = res.get('metric', {})
328+
metric_str = "{" + ", ".join([f'{k}="{v}"' for k,v in metric_labels.items()]) + "}"
329+
330+
if 'regex' in var and var['regex']:
331+
match = re.search(var['regex'], metric_str)
332+
if match:
333+
values.append(match.group(1) if len(match.groups()) > 0 else match.group(0))
334+
else:
335+
# Default behavior: extract value of a label if there is one other than __name__
336+
# otherwise, the __name__
337+
non_name_labels = {k: v for k, v in metric_labels.items() if k != '__name__'}
338+
if len(non_name_labels) == 1:
339+
values.append(list(non_name_labels.values())[0])
340+
else:
341+
values.append(metric_labels.get('__name__', metric_str))
342+
return sorted(list(set(values)))
343+
else:
344+
logger.error(f"Query failed for '{query}'. Status: {response.status_code}, Body: {response.text}")
345+
return []
346+
except Exception as e:
347+
logger.error(f"Exception during generic query execution for '{query}': {e}")
348+
return []
349+
142350
def panel_query_datasource_api(self, tr: TimeRange, queries, interval_ms=300000):
143351
try:
144352
if not queries or len(queries) == 0:

integrations/source_manangers/grafana_source_manager.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import re
34
import string
@@ -52,6 +53,7 @@ def __init__(self):
5253
self.task_type_callable_map = {
5354
Grafana.TaskType.PROMETHEUS_DATASOURCE_METRIC_EXECUTION: {
5455
"executor": self.execute_prometheus_datasource_metric_execution,
56+
"asset_descriptor": self.execute_prometheus_datasource_metric_asset_descriptor,
5557
"model_types": [SourceModelType.GRAFANA_PROMETHEUS_DATASOURCE],
5658
"result_type": PlaybookTaskResultType.API_RESPONSE,
5759
"display_name": "Query any of your Prometheus Data Sources from Grafana",
@@ -95,6 +97,7 @@ def __init__(self):
9597
},
9698
Grafana.TaskType.QUERY_DASHBOARD_PANEL_METRIC: {
9799
"executor": self.execute_query_dashboard_panel_metric_execution,
100+
"asset_descriptor": self.query_dashboard_panel_metric_asset_descriptor,
98101
"model_types": [SourceModelType.GRAFANA_DASHBOARD],
99102
"result_type": PlaybookTaskResultType.API_RESPONSE,
100103
"display_name": "Query any of your dashboard panels from Grafana",
@@ -197,6 +200,23 @@ def __init__(self):
197200
),
198201
],
199202
},
203+
Grafana.TaskType.FETCH_DASHBOARD_VARIABLES: {
204+
"executor": self.execute_fetch_dashboard_variables,
205+
"asset_descriptor": self.query_dashboard_panel_metric_asset_descriptor,
206+
"model_types": [SourceModelType.GRAFANA_DASHBOARD],
207+
"result_type": PlaybookTaskResultType.API_RESPONSE,
208+
"display_name": "Fetch all variables and their values from a Grafana Dashboard",
209+
"category": "Metrics",
210+
"form_fields": [
211+
FormField(
212+
key_name=StringValue(value="dashboard_uid"),
213+
display_name=StringValue(value="Dashboard UID"),
214+
description=StringValue(value="Select Dashboard UID to fetch variables from"),
215+
data_type=LiteralType.STRING,
216+
form_field_type=FormFieldType.TYPING_DROPDOWN_FT,
217+
),
218+
],
219+
},
200220
}
201221

202222
def get_connector_processor(self, grafana_connector, **kwargs):
@@ -299,6 +319,63 @@ def execute_fetch_dashboard_variable_label_values(self, time_range: TimeRange, g
299319
except Exception as e:
300320
raise Exception(f"Error while executing Grafana fetch dashboard variable label values task: {e}") from e
301321

322+
def execute_fetch_dashboard_variables(self, time_range: TimeRange, grafana_task: Grafana,
323+
grafana_connector: ConnectorProto):
324+
try:
325+
if not grafana_connector:
326+
raise Exception("Task execution Failed:: No Grafana source found")
327+
328+
# Access the task using the correct attribute name from the proto
329+
if hasattr(grafana_task, 'fetch_dashboard_variables'):
330+
task = grafana_task.fetch_dashboard_variables
331+
else:
332+
# Fallback for proto generation issues
333+
logger.warning("fetch_dashboard_variables attribute not found, trying alternative access")
334+
# Try to access by index in the oneof if the attribute is not available
335+
return PlaybookTaskResult(
336+
type=PlaybookTaskResultType.TEXT,
337+
text=TextResult(output=StringValue(value="Task type not properly configured in proto")),
338+
source=self.source,
339+
)
340+
341+
dashboard_uid = task.dashboard_uid.value
342+
343+
grafana_api_processor = self.get_connector_processor(grafana_connector)
344+
345+
print(
346+
f"Playbook Task Downstream Request: Type -> Grafana FETCH_DASHBOARD_VARIABLES, Dashboard_UID -> {dashboard_uid}",
347+
flush=True,
348+
)
349+
350+
variables_data = grafana_api_processor.get_dashboard_variables(dashboard_uid)
351+
352+
if not variables_data or not variables_data.get('variables'):
353+
return PlaybookTaskResult(
354+
type=PlaybookTaskResultType.TEXT,
355+
text=TextResult(output=StringValue(value=f"No variables found for dashboard: {dashboard_uid}")),
356+
source=self.source,
357+
)
358+
359+
# Ensure we have a proper Struct instance
360+
if isinstance(variables_data, dict):
361+
response_struct = Struct()
362+
response_struct.update(variables_data)
363+
else:
364+
response_struct = dict_to_proto(variables_data, Struct)
365+
366+
output = ApiResponseResult(response_body=response_struct)
367+
368+
task_result = PlaybookTaskResult(source=self.source, type=PlaybookTaskResultType.API_RESPONSE,
369+
api_response=output)
370+
return task_result
371+
except Exception as e:
372+
logger.error(f"Error while executing Grafana fetch dashboard variables task: {e}")
373+
return PlaybookTaskResult(
374+
type=PlaybookTaskResultType.TEXT,
375+
text=TextResult(output=StringValue(value=f"Error executing dashboard variables task: {str(e)}")),
376+
source=self.source,
377+
)
378+
302379
def execute_query_dashboard_panel_metric_execution(self, time_range: TimeRange, grafana_task: Grafana,
303380
grafana_connector: ConnectorProto):
304381
try:

protos/playbooks/source_task_definitions/grafana_task.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,18 @@ message Grafana {
4545
google.protobuf.StringValue label_name = 2;
4646
}
4747

48+
message FetchDashboardVariablesTask {
49+
google.protobuf.StringValue dashboard_uid = 1;
50+
}
51+
4852
enum TaskType {
4953
UNKNOWN = 0;
5054
PROMQL_METRIC_EXECUTION = 1;
5155
PROMETHEUS_DATASOURCE_METRIC_EXECUTION = 2;
5256
QUERY_DASHBOARD_PANEL_METRIC = 3;
5357
EXECUTE_ALL_DASHBOARD_PANELS = 4;
5458
FETCH_DASHBOARD_VARIABLE_LABEL_VALUES = 5;
59+
FETCH_DASHBOARD_VARIABLES = 6;
5560
}
5661

5762
TaskType type = 1;
@@ -61,5 +66,6 @@ message Grafana {
6166
QueryDashboardPanelMetricTask query_dashboard_panel_metric = 5;
6267
ExecuteAllDashboardPanelsTask execute_all_dashboard_panels = 6;
6368
FetchDashboardVariableLabelValuesTask fetch_dashboard_variable_label_values = 7;
69+
FetchDashboardVariablesTask fetch_dashboard_variables = 8;
6470
}
6571
}

0 commit comments

Comments
 (0)