diff --git a/clab2drawio.py b/clab2drawio.py index f5c31c6..42fc801 100644 --- a/clab2drawio.py +++ b/clab2drawio.py @@ -1218,12 +1218,23 @@ def main( output_filename = os.path.basename(grafana_output_file) diagram.grafana_dashboard_file = grafana_output_file os.makedirs(output_folder, exist_ok=True) + grafana = GrafanaDashboard(diagram) - grafana_json = grafana.create_dashboard() - # dump the json to the file + + # Create flow panel YAML + panel_config = grafana.create_panel_yaml() + + # Write flow panel YAML to file + flow_panel_output_file = os.path.splitext(grafana_output_file)[0] + ".flow_panel.yaml" + with open(flow_panel_output_file, "w") as f: + f.write(panel_config) + print("Saved flow panel YAML to:", flow_panel_output_file) + + grafana_json = grafana.create_dashboard(panel_config) + # Dump the JSON to the file with open(grafana_output_file, "w") as f: f.write(grafana_json) - print("Saved file to:", grafana_output_file) + print("Saved Grafana dashboard JSON to:", grafana_output_file) else: add_links(diagram, styles) diff --git a/docs/grafana.md b/docs/grafana.md index eabf0cf..cd09bc0 100644 --- a/docs/grafana.md +++ b/docs/grafana.md @@ -1,6 +1,6 @@ ### Grafana Dashboard Generation Option -The `-g, --gf_dashboard` command line option is designed to automate the generation of Grafana dashboards with all it's rules from your YAML configuration files. This feature is currently in a **work-in-progress (WIP)** stage. When using this option, it is important to note the following specifics: +The `-g, --gf_dashboard` command line option is designed to automate the generation of Grafana dashboards with all it's rules from your YAML configuration files. When using this option, it is important to note the following specifics: ![Grafana ](img/grafana.png) @@ -10,16 +10,32 @@ The `-g, --gf_dashboard` command line option is designed to automate the generat - All trafic is outgoing metric #### Compatibility -- **Grafana Version:** This option is tailored to work optimally with Grafana version **10.3.5**. Buggy from 10.4 upwords -- **Plugin Requirement:** It requires the Flowcharting plugin version **1.0.0.e**, which is available via a specific fork maintained by [skyfrank on GitHub](https://github.com/skyfrank/grafana-flowcharting). This plugin is essential for rendering the custom visualizations generated by the script. Lower version also work, but this one is recommended +- **Grafana Version:** Works optimally with Grafana version **>10.0.0**. Recomendation: **11.2.0**. +- **Plugin Requirement:** It requires the Flowcplugin [Flow plugin](https://grafana.com/grafana/plugins/andrewbmchugh-flow-panel). This plugin is essential for rendering the custom visualizations generated by the script. -#### Usage +### Usage To generate a dashboard, execute the following command: ```bash python clab2drawio.py -i -g --theme grafana_dark ``` Ensure that you replace `` with the actual path to your YAML configuration file. Use it with grafana_dark or your own grafana compatible theme. +When the `-g` flag is used, the script generates the following: +1. Grafana dashboard JSON file +2. Panel YAML configuration file +3. draw.io diagram + +#### To export the diagram as an SVG: +To get a full guide: [https://github.com/andymchugh/andrewbmchugh-flow-panel/blob/main/src/README.md#using-drawio-to-create-your-svg](https://github.com/andymchugh/andrewbmchugh-flow-panel/blob/main/src/README.md#using-drawio-to-create-your-svg) +1. Open the generated draw.io diagram using the draw.io application with the svgdata plugin enabled, or use the online version at [https://app.diagrams.net/?p=svgdata](https://app.diagrams.net/?p=svgdata). +2. Go to File -> Export -> SVG to export the diagram as an SVG file. + +The generated dashboard JSON will include the panel configuration but without the SVG data. To complete the dashboard, you need to either: +- Copy and paste the SVG data into the designated SVG box in the Grafana dashboard editor. +- Upload the SVG file to a hosting service and reference the URL in the Grafana dashboard editor. + +By following these steps, you can generate a complete Grafana dashboard with the diagram, panel configuration, and dashboard JSON file. + #### Current Limitations - **Hardcoded Queries:** Currently, the dashboard queries are hardcoded and are specifically optimized for Nokia's SRLinux and SROS platforms. This means they may not be directly applicable to other environments without modifications. - **Data Sources:** The dashboard assumes specific data sources (Prometheus) are already configured in your Grafana instance that align with the hardcoded queries. diff --git a/lib/Grafana.py b/lib/Grafana.py index 7f82754..38ad55c 100644 --- a/lib/Grafana.py +++ b/lib/Grafana.py @@ -1,197 +1,120 @@ import json import os import xml.etree.ElementTree as ET - +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap, CommentedSeq +import yaml class GrafanaDashboard: - def __init__(self, diagram=None): + def __init__(self, diagram=None, panel_config=None): self.diagram = diagram self.links = self.diagram.get_links_from_nodes() self.dashboard_filename = self.diagram.grafana_dashboard_file - def create_dashboard(self): - # We just need the subtree objects from mxGraphModel.Single page drawings only - xmlTree = ET.fromstring(self.diagram.dump_xml()) - subXmlTree = xmlTree.findall(".//mxGraphModel")[0] - - # Define Query rules for the Panel, rule_expr needs to match the collector metric name - # Legend format needs to match the format expected by the metric - panelQueryList = { - "IngressTraffic": { - "rule_expr": "interface_traffic_rate_in_bps", - "legend_format": "{{source}}:{{interface_name}}:in", - }, - "EgressTraffic": { - "rule_expr": "interface_traffic_rate_out_bps", - "legend_format": "{{source}}:{{interface_name}}:out", - }, - "ItfOperState": { - "rule_expr": "interface_oper_state", - "legend_format": "oper_state:{{source}}:{{interface_name}}", - }, - "ItfOperState2": { - "rule_expr": "port_oper_state", - "legend_format": "oper_state:{{source}}:{{interface_name}}", - }, - "EgressTraffic2": { - "rule_expr": "irate(port_ethernet_statistics_out_octets[$__rate_interval])*8", - "legend_format": "{{source}}:{{interface_name}}:out", - }, - } - # Create a targets list to embed in the JSON object, we add all the other default JSON attributes to the list - targetsList = [] - for query in panelQueryList: - targetsList.append( - self.gf_dashboard_datasource_target( - rule_expr=panelQueryList[query]["rule_expr"], - legend_format=panelQueryList[query]["legend_format"], - refId=query, - ) - ) - - # Create the Rules Data - rulesData = [] - i = 0 - for link in self.links: - link_id = f"link_id:{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}" - - # Traffic out - rulesData.append( - self.gf_flowchart_rule_traffic( - ruleName=f"{link.source.name}:{link.source_intf}:out", - metric=f"{link.source.name.lower()}:{link.source_intf}:out", - link_id=link_id, - order=i, - ) - ) - - i = i + 2 - - port_id = f"{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}" - # Port State: - rulesData.append( - self.gf_flowchart_rule_operstate( - ruleName=f"oper_state:{link.source.name}:{link.source_intf}", - metric=f"oper_state:{link.source.name.lower()}:{link.source_intf}", - link_id=port_id, - order=i + 3, - ) - ) - i = i + 2 - - # Create the Panel - flowchart_panel = self.gf_flowchart_panel_template( - xml=ET.tostring(subXmlTree, encoding="unicode"), - rulesData=rulesData, - panelTitle="Network Telemetry", - targetsList=targetsList, - ) - # Create a dashboard from the panel - dashboard_json = json.dumps( - self.gf_dashboard_template( - panels=flowchart_panel, - dashboard_name=os.path.splitext(self.dashboard_filename)[0], - ), - indent=4, - ) - return dashboard_json - - def gf_dashboard_datasource_target( - self, rule_expr="promql_query", legend_format=None, refId="Query1" - ): - """ - Dictionary containing information relevant to the Targets queried - """ - target = { - "datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"}, - "editorMode": "code", - "expr": rule_expr, - "instant": False, - "legendFormat": legend_format, - "range": True, - "refId": refId, - } - return target - - def gf_flowchart_rule_traffic( - self, ruleName="traffic:inOrOut", metric=None, link_id=None, order=1 - ): - """ - Dictionary containing information relevant to the traffic Rules - """ - # Load the traffic rule template from file + def create_dashboard(self, panel_config): + # Path to the dashboard JSON template base_dir = os.getenv("APP_BASE_DIR", "") + template_path = os.path.join(base_dir, "lib/templates/flow_panel_template.json") - with open( - os.path.join(base_dir, "lib/templates/traffic_rule_template.json"), "r" - ) as f: - rule = json.load(f) - - rule["alias"] = ruleName - rule["pattern"] = metric - rule["mapsDat"]["shapes"]["dataList"][0]["pattern"] = link_id - rule["mapsDat"]["texts"]["dataList"][0]["pattern"] = link_id - rule["order"] = order - - return rule - - def gf_flowchart_rule_operstate( - self, ruleName="oper_state", metric=None, link_id=None, order=1 - ): - """ - Dictionary containing information relevant to the Operational State Rules - """ - # Load the operstate rule template from file - base_dir = os.getenv("APP_BASE_DIR", "") + # Load the dashboard template from file + with open(template_path, 'r') as file: + dashboard_json = json.load(file) - with open( - os.path.join(base_dir, "lib/templates/operstate_rule_template.json"), "r" - ) as f: - rule = json.load(f) - - rule["alias"] = ruleName - rule["pattern"] = metric - rule["mapsDat"]["shapes"]["dataList"][0]["pattern"] = link_id - rule["order"] = order - - return rule - - def gf_flowchart_panel_template( - self, xml=None, rulesData=None, targetsList=None, panelTitle="Network Topology" - ): - """ - Dictionary containing information relevant to the Panels Section in the JSON Dashboard - Embedding of the XML diagram, the Rules and the Targets - """ - # Load the panel template from file - base_dir = os.getenv("APP_BASE_DIR", "") - with open( - os.path.join(base_dir, "lib/templates/panel_template.json"), "r" - ) as f: - panel = json.load(f) + # Insert the YAML configuration as a string into the panelConfig of the relevant panel + for panel in dashboard_json['panels']: + if 'options' in panel: + panel['options']['panelConfig'] = panel_config - panel[0]["flowchartsData"]["flowcharts"][0]["xml"] = xml - panel[0]["rulesData"]["rulesData"] = rulesData - panel[0]["targets"] = targetsList - panel[0]["title"] = panelTitle + return json.dumps(dashboard_json, indent=2) - return panel + def create_panel_yaml(self): + from ruamel.yaml import YAML, CommentedMap, CommentedSeq - def gf_dashboard_template(self, panels=None, dashboard_name="lab-telemetry"): - """ - Dictionary containing information relevant to the Grafana Dashboard Root JSON object - """ + yaml = YAML() + yaml.explicit_start = True # To include '---' at the start + yaml.width = 4096 # prevent line wrapping - base_dir = os.getenv("APP_BASE_DIR", "") + root = CommentedMap() - # Load the dashboard template from file - with open( - os.path.join(base_dir, "lib/templates/traffic_rule_template.json") - ) as f: - dashboard = json.load(f) + # Anchors and Aliases + thresholds_operstate = CommentedSeq() + thresholds_operstate.append({'color': 'red', 'level': 0}) + thresholds_operstate.append({'color': 'green', 'level': 1}) + + thresholds_operstate.yaml_set_anchor('thresholds-operstate', always_dump=True) + + thresholds_traffic = CommentedSeq() + thresholds_traffic.append({'color': 'gray', 'level': 0}) + thresholds_traffic.append({'color': 'green', 'level': 199999}) + thresholds_traffic.append({'color': 'yellow', 'level': 500000}) + thresholds_traffic.append({'color': 'orange', 'level': 1000000}) + thresholds_traffic.append({'color': 'red', 'level': 5000000}) + + thresholds_traffic.yaml_set_anchor('thresholds-traffic', always_dump=True) + + label_config = CommentedMap() + label_config['separator'] = "replace" + label_config['units'] = "bps" + label_config['decimalPoints'] = 1 + label_config['valueMappings'] = [ + {'valueMax': 199999, 'text': "\u200B"}, + {'valueMin': 200000} + ] + + label_config.yaml_set_anchor('label-config', always_dump=True) + + # Anchors entry in root + root['anchors'] = anchors = CommentedMap() + + anchors['thresholds-operstate'] = thresholds_operstate + anchors['thresholds-traffic'] = thresholds_traffic + anchors['label-config'] = label_config + + # cellIdPreamble + root['cellIdPreamble'] = 'cell-' + + # cells + cells = CommentedMap() + root['cells'] = cells + for link in self.links: + source_name = link.source.name + source_intf = link.source_intf + target_name = link.target.name + target_intf = link.target_intf + + # Operstate cell + cell_id_operstate = f"{source_name}:{source_intf}:{target_name}:{target_intf}" + dataRef_operstate = f"oper-state:{source_name}:{source_intf}" + + # fillColor thresholds referencing the anchor + fillColor_operstate = CommentedMap() + fillColor_operstate['thresholds'] = thresholds_operstate # reference anchor + + cell_operstate = CommentedMap() + cell_operstate['dataRef'] = dataRef_operstate + cell_operstate['fillColor'] = fillColor_operstate + + cells[cell_id_operstate] = cell_operstate + + # Traffic cell + cell_id_traffic = f"link_id:{source_name}:{source_intf}:{target_name}:{target_intf}" + + dataRef_traffic = f"{source_name}:{source_intf}:out" + + strokeColor_traffic = CommentedMap() + strokeColor_traffic['thresholds'] = thresholds_traffic # reference anchor + + cell_traffic = CommentedMap() + cell_traffic['dataRef'] = dataRef_traffic + cell_traffic['label'] = label_config # reference anchor + cell_traffic['strokeColor'] = strokeColor_traffic - dashboard["panels"] = panels - dashboard["title"] = dashboard_name + cells[cell_id_traffic] = cell_traffic - return dashboard + # Now write root to YAML + import io + stream = io.StringIO() + yaml.dump(root, stream) + panel_yaml = stream.getvalue() + return panel_yaml \ No newline at end of file diff --git a/lib/templates/dashboard_template.json b/lib/templates/dashboard_template.json deleted file mode 100644 index b31bd60..0000000 --- a/lib/templates/dashboard_template.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "Prometheus", - "description": "Autogenerated by clab2grafana.py", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ], - "__elements": {}, - "__requires": [ - { - "type": "panel", - "id": "agenty-flowcharting-panel", - "name": "FlowCharting", - "version": "1.0.0d" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "10.3.3" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "1.0.0" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": null, - "links": [], - "liveNow": false, - "panels": "", - "refresh": "5s", - "schemaVersion": 39, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "", - "uid": "", - "version": 1, - "weekStart": "" - } \ No newline at end of file diff --git a/lib/templates/flow_panel_template.json b/lib/templates/flow_panel_template.json new file mode 100644 index 0000000..6c0711b --- /dev/null +++ b/lib/templates/flow_panel_template.json @@ -0,0 +1,106 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "prometheus" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus" + }, + "gridPos": { + "h": 23, + "w": 13, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "animationControlEnabled": true, + "animationsEnabled": true, + "debuggingCtr": { + "colorsCtr": 1, + "dataCtr": 0, + "displaySvgCtr": 0, + "mappingsCtr": 0, + "timingsCtr": 0 + }, + "highlighterEnabled": true, + "panZoomEnabled": true, + "panelConfig": "", + "siteConfig": "", + "svg": "", + "testDataEnabled": false, + "timeSliderEnabled": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus" + }, + "editorMode": "code", + "expr": "interface_oper_state", + "instant": false, + "legendFormat": "oper-state:{{source}}:{{interface_name}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus" + }, + "editorMode": "code", + "expr": "interface_traffic_rate_out_bps", + "hide": false, + "instant": false, + "legendFormat": "{{source}}:{{interface_name}}:out", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus" + }, + "editorMode": "code", + "expr": "interface_traffic_rate_in_bps", + "hide": false, + "instant": false, + "legendFormat": "{{source}}:{{interface_name}}:in", + "range": true, + "refId": "C" + } + ], + "title": "Network Telemetry", + "type": "andrewbmchugh-flow-panel" + } + ], + "refresh": "5s", + "schemaVersion": 38, + "tags": [], + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Network Telemetry", + "version": 6, + "weekStart": "" + } \ No newline at end of file diff --git a/lib/templates/operstate_rule_template.json b/lib/templates/operstate_rule_template.json deleted file mode 100644 index c653c31..0000000 --- a/lib/templates/operstate_rule_template.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "aggregation": "current", - "alias": "", - "column": "Time", - "dateColumn": "Time", - "dateFormat": "YYYY-MM-DD HH:mm:ss", - "dateTHData": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "ge", - "level": 0, - "value": "0d" - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "comparator": "ge", - "level": 0, - "value": "-1d" - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "comparator": "ge", - "level": 0, - "value": "-1w" - } - ], - "decimals": 0, - "gradient": false, - "hidden": false, - "invert": false, - "mappingType": 1, - "mapsDat": { - "events": { - "dataList": [], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "links": { - "dataList": [], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "shapes": { - "dataList": [ - { - "colorOn": "a", - "hidden": false, - "pattern": "", - "style": "fillColor" - } - ], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "texts": { - "dataList": [], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - } - }, - "metricType": "serie", - "newRule": false, - "numberTHData": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "ge", - "level": 0 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "comparator": "ge", - "level": 0, - "value": 1 - } - ], - "order": 0, - "overlayIcon": false, - "pattern": "", - "rangeData": [], - "reduce": true, - "refId": "A", - "sanitize": false, - "stringTHData": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "eq", - "level": 0, - "value": "/.*/" - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "comparator": "eq", - "level": 0, - "value": "/.*warning.*/" - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "comparator": "eq", - "level": 0, - "value": "/.*(up|ok).*/" - } - ], - "tooltip": true, - "tooltipColors": false, - "tooltipLabel": "", - "tooltipOn": "a", - "tpDirection": "v", - "tpGraph": true, - "tpGraphScale": "linear", - "tpGraphSize": "100%", - "tpGraphType": "line", - "tpMetadata": false, - "type": "number", - "unit": "short", - "valueData": [] - } \ No newline at end of file diff --git a/lib/templates/panel_template.json b/lib/templates/panel_template.json deleted file mode 100644 index a933957..0000000 --- a/lib/templates/panel_template.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "flowchartsData": { - "allowDrawio": true, - "editorTheme": "kennedy", - "editorUrl": "https://embed.diagrams.net/", - "flowcharts": [ - { - "center": true, - "csv": "", - "download": false, - "enableAnim": true, - "grid": false, - "lock": true, - "name": "Main", - "scale": true, - "tooltip": true, - "type": "xml", - "url": "http:///", - "xml": null, - "zoom": "100%" - } - ] - }, - "format": "short", - "graphId": "flowchart_1", - "gridPos": { - "h": 20, - "w": 17, - "x": 0, - "y": 0 - }, - "id": 1, - "rulesData": { - "rulesData": null - }, - "targets": null, - "title": "Network Topology", - "type": "agenty-flowcharting-panel", - "valueName": "current", - "version": "1.0.0d" - } - ] \ No newline at end of file diff --git a/lib/templates/traffic_rule_template.json b/lib/templates/traffic_rule_template.json deleted file mode 100644 index 85d2589..0000000 --- a/lib/templates/traffic_rule_template.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "aggregation": "current", - "alias": "", - "column": "Time", - "dateColumn": "Time", - "dateFormat": "YYYY-MM-DD HH:mm:ss", - "dateTHData": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "ge", - "level": 0, - "value": "0d" - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "comparator": "ge", - "level": 0, - "value": "-1d" - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "comparator": "ge", - "level": 0, - "value": "-1w" - } - ], - "decimals": 1, - "gradient": false, - "hidden": false, - "invert": true, - "mappingType": 1, - "mapsDat": { - "events": { - "dataList": [], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "links": { - "dataList": [], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "shapes": { - "dataList": [ - { - "colorOn": "a", - "hidden": false, - "pattern": "", - "style": "strokeColor" - } - ], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - }, - "texts": { - "dataList": [ - { - "hidden": false, - "pattern": "", - "textOn": "wc", - "textPattern": "/.*/", - "textReplace": "content" - } - ], - "options": { - "enableRegEx": true, - "identByProp": "id", - "metadata": "" - } - } - }, - "metricType": "serie", - "newRule": false, - "numberTHData": [ - { - "color": "rgba(171, 187, 187, 1)", - "comparator": "ge", - "level": 0 - }, - { - "color": "rgba(75, 221, 51, 1)", - "comparator": "ge", - "value": 500000, - "level": 0 - }, - { - "color": "rgba(255, 128, 0, 1)", - "comparator": "gt", - "value": 2000000, - "level": 0 - }, - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "ge", - "value": 5000000, - "level": 0 - } - ], - "order": 0, - "overlayIcon": false, - "pattern": "", - "rangeData": [], - "reduce": true, - "refId": "A", - "sanitize": false, - "stringTHData": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "comparator": "eq", - "level": 0, - "value": "/.*/" - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "comparator": "eq", - "level": 0, - "value": "/.*warning.*/" - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "comparator": "eq", - "level": 0, - "value": "/.*(success|ok).*/" - } - ], - "tooltip": true, - "tooltipColors": false, - "tooltipLabel": "", - "tooltipOn": "a", - "tpDirection": "v", - "tpGraph": true, - "tpGraphScale": "linear", - "tpGraphSize": "100%", - "tpGraphType": "line", - "tpMetadata": false, - "type": "number", - "unit": "bps", - "valueData": [] - } \ No newline at end of file