Ver código fonte

20171205 src/mesh.sml (graph.json -> Meshes (= Zusammenhangskomponenten des Verbindungsgraphen) und mit status-mesh.json visualisieren)

Altlast 7 anos atrás
pai
commit
694d4902a9

+ 16 - 2
dashboard/Makefile.in

@@ -3,7 +3,7 @@ all:	%%PROM_GROUPS_PREFIX%%.prom dashboard
 %%PROM_GROUPS_PREFIX%%.prom: groups.prom.in
 	../conf/substitute.sh $> $@
 
-dashboard: %%DASHBOARD_PREFIX%%status.json %%DASHBOARD_PREFIX%%status-render.json %%DASHBOARD_PREFIX%%status-group.json %%DASHBOARD_PREFIX%%status-group-render.json
+dashboard: %%DASHBOARD_PREFIX%%status.json %%DASHBOARD_PREFIX%%status-render.json %%DASHBOARD_PREFIX%%status-group.json %%DASHBOARD_PREFIX%%status-group-render.json %%DASHBOARD_PREFIX%%status-mesh.json %%DASHBOARD_PREFIX%%status-mesh-render.json
 
 %%DASHBOARD_PREFIX%%status.json: status.json.in
 	../conf/substitute.sh $> $@
@@ -17,7 +17,13 @@ dashboard: %%DASHBOARD_PREFIX%%status.json %%DASHBOARD_PREFIX%%status-render.jso
 %%DASHBOARD_PREFIX%%status-group-render.json: status-group-render.json.in
 	../conf/substitute.sh $> $@
 
-install: %%EXPORT_DIR%%/%%PROM_GROUPS_PREFIX%%.prom %%DASHBOARD_DIR%% %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-render.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-group.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-group-render.json
+%%DASHBOARD_PREFIX%%status-mesh.json: status-mesh.json.in
+	../conf/substitute.sh $> $@
+
+%%DASHBOARD_PREFIX%%status-mesh-render.json: status-mesh-render.json.in
+	../conf/substitute.sh $> $@
+
+install: %%EXPORT_DIR%%/%%PROM_GROUPS_PREFIX%%.prom %%DASHBOARD_DIR%% %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-render.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-group.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-group-render.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-mesh.json %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-mesh-render.json
 
 %%DASHBOARD_DIR%%:
 	%%DASHBOARD_DIR_INSTALL%% $@
@@ -37,9 +43,17 @@ install: %%EXPORT_DIR%%/%%PROM_GROUPS_PREFIX%%.prom %%DASHBOARD_DIR%% %%DASHBOAR
 %%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-group-render.json: %%DASHBOARD_PREFIX%%status-group-render.json
 	%%INSTALL_DATA_CMD%% $> $@
 
+%%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-mesh.json: %%DASHBOARD_PREFIX%%status-mesh.json
+	%%INSTALL_DATA_CMD%% $> $@
+
+%%DASHBOARD_DIR%%/%%DASHBOARD_PREFIX%%status-mesh-render.json: %%DASHBOARD_PREFIX%%status-mesh-render.json
+	%%INSTALL_DATA_CMD%% $> $@
+
 clean:
 	rm -f %%PROM_GROUPS_PREFIX%%.prom
 	rm -f %%DASHBOARD_PREFIX%%status.json
 	rm -f %%DASHBOARD_PREFIX%%status-render.json
 	rm -f %%DASHBOARD_PREFIX%%status-group.json
 	rm -f %%DASHBOARD_PREFIX%%status-group-render.json
+	rm -f %%DASHBOARD_PREFIX%%status-mesh.json
+	rm -f %%DASHBOARD_PREFIX%%status-mesh-render.json

+ 347 - 0
dashboard/status-mesh-render.json.in

@@ -0,0 +1,347 @@
+{
+  "annotations": {
+    "list": []
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "hideControls": false,
+  "id": null,
+  "links": [],
+  "links": [
+    {
+      "icon": "info",
+      "tags": [],
+      "title": "[[mesh_count]] Meshes, [[node_sum]] Knoten, [[link_sum]] Links",
+      "type": "link"
+    }
+  ],
+  "rows": [
+    {
+      "collapse": false,
+      "height": 278,
+      "panels": [
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": null,
+          "fill": 0,
+          "id": 3,
+          "legend": {
+            "alignAsTable": true,
+            "avg": true,
+            "current": true,
+            "hideEmpty": true,
+            "max": true,
+            "min": true,
+            "rightSide": false,
+            "show": true,
+            "total": false,
+            "values": true
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "percentage": false,
+          "pointradius": 5,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "spaceLength": 10,
+          "span": 12,
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "expr": "%%PROM_STATS_PREFIX%%%%PROM_SEPERATOR%%clients{node_id=~\"([[regex]])\"}",
+              "format": "time_series",
+              "hide": false,
+              "interval": "",
+              "intervalFactor": 2,
+              "legendFormat": "{{hostname}}",
+              "refId": "A",
+              "step": 120
+            },
+            {
+              "expr": "sum(%%PROM_STATS_PREFIX%%%%PROM_SEPERATOR%%clients{node_id=~\"([[regex]])\"})",
+              "format": "time_series",
+              "hide": false,
+              "interval": "",
+              "intervalFactor": 2,
+              "legendFormat": "Summe",
+              "refId": "B",
+              "step": 120
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "clients im Mesh des Knotens [[mesh_name]]",
+          "tooltip": {
+            "shared": true,
+            "sort": 1,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "decimals": 0,
+              "format": "short",
+              "label": "",
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ]
+        }
+      ],
+      "repeat": null,
+      "repeatIteration": null,
+      "repeatRowId": null,
+      "showTitle": false,
+      "title": "Dashboard Row",
+      "titleSize": "h6"
+    },
+    {
+      "collapse": false,
+      "height": "100",
+      "panels": [
+        {
+          "content": "<p>Links zu Router [[hostname]], Node ID [[node_id]]: <a href=\"%%DASHBOARD_PATH%%%%DASHBOARD_PREFIX%%status.json?var-hostname=[[hostname]]&var-node_id=[[node_id]]\">Status Dashboard</a>, <a href=\"https://map.ffdo.de/meshviewer/#!v:m;n:[[node_id]]\">Map</a>.</p>\n",
+          "height": "80px",
+          "id": 4,
+          "links": [],
+          "mode": "html",
+          "repeat": null,
+          "span": 12,
+          "title": "Links zu [[hostname]], Node ID [[node_id]]",
+          "type": "text"
+        }
+      ],
+      "repeat": null,
+      "repeatIteration": null,
+      "repeatRowId": null,
+      "showTitle": false,
+      "title": "Dashboard Row",
+      "titleSize": "h6"
+    }
+  ],
+  "schemaVersion": 14,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 0,
+        "includeAll": false,
+        "label": "Mesh auswählen:",
+        "multi": false,
+        "name": "mesh_name",
+        "options": [],
+        "query": "query_result(%%PROM_INFO_PREFIX%% and on (node_id) %%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)",
+        "refresh": 2,
+        "regex": "/.*hostname=\"([^\\\"]+)\".*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 1,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "mesh_id",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{hostname=\"[[mesh_name]]\"},node_id)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "regex",
+        "options": [],
+        "query": "label_values(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh{node_id=\"[[mesh_id]]\"},members)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 0,
+        "includeAll": false,
+        "label": "Knoten auswählen:",
+        "multi": false,
+        "name": "hostname",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{node_id=~\"[[regex]]\"},hostname)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 1,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "node_id",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{hostname=\"[[hostname]]\"},node_id)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "mesh_count",
+        "options": [],
+        "query": "query_result(count(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "node_sum",
+        "options": [],
+        "query": "query_result(floor(sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "link_sum",
+        "options": [],
+        "query": "query_result(round(1000 * (sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh) - floor(sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)))))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      }
+    ]
+  },
+  "time": {
+    "from": "now-24h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "%%DASHBOARD_PREFIX%%status-mesh-render",
+  "version": 5
+}

+ 347 - 0
dashboard/status-mesh.json.in

@@ -0,0 +1,347 @@
+{
+  "annotations": {
+    "list": []
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "hideControls": false,
+  "id": null,
+  "links": [],
+  "links": [
+    {
+      "icon": "info",
+      "tags": [],
+      "title": "[[mesh_count]] Meshes, [[node_sum]] Knoten, [[link_sum]] Links",
+      "type": "link"
+    }
+  ],
+  "rows": [
+    {
+      "collapse": false,
+      "height": 278,
+      "panels": [
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": null,
+          "fill": 0,
+          "id": 3,
+          "legend": {
+            "alignAsTable": true,
+            "avg": true,
+            "current": true,
+            "hideEmpty": true,
+            "max": false,
+            "min": false,
+            "rightSide": true,
+            "show": true,
+            "total": false,
+            "values": true
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "percentage": false,
+          "pointradius": 5,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "spaceLength": 10,
+          "span": 12,
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "expr": "%%PROM_STATS_PREFIX%%%%PROM_SEPERATOR%%clients{node_id=~\"([[regex]])\"}",
+              "format": "time_series",
+              "hide": false,
+              "interval": "",
+              "intervalFactor": 2,
+              "legendFormat": "{{hostname}}",
+              "refId": "A",
+              "step": 120
+            },
+            {
+              "expr": "sum(%%PROM_STATS_PREFIX%%%%PROM_SEPERATOR%%clients{node_id=~\"([[regex]])\"})",
+              "format": "time_series",
+              "hide": false,
+              "interval": "",
+              "intervalFactor": 2,
+              "legendFormat": "Summe",
+              "refId": "B",
+              "step": 120
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "clients im Mesh des Knotens [[mesh_name]]",
+          "tooltip": {
+            "shared": true,
+            "sort": 1,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "decimals": 0,
+              "format": "short",
+              "label": "",
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ]
+        }
+      ],
+      "repeat": null,
+      "repeatIteration": null,
+      "repeatRowId": null,
+      "showTitle": false,
+      "title": "Dashboard Row",
+      "titleSize": "h6"
+    },
+    {
+      "collapse": false,
+      "height": "100",
+      "panels": [
+        {
+          "content": "<p>Links zu Router [[hostname]], Node ID [[node_id]]: <a href=\"%%DASHBOARD_PATH%%%%DASHBOARD_PREFIX%%status.json?var-hostname=[[hostname]]&var-node_id=[[node_id]]\">Status Dashboard</a>, <a href=\"https://map.ffdo.de/meshviewer/#!v:m;n:[[node_id]]\">Map</a>.</p>\n",
+          "height": "80px",
+          "id": 4,
+          "links": [],
+          "mode": "html",
+          "repeat": null,
+          "span": 12,
+          "title": "Links zu [[hostname]], Node ID [[node_id]]",
+          "type": "text"
+        }
+      ],
+      "repeat": null,
+      "repeatIteration": null,
+      "repeatRowId": null,
+      "showTitle": false,
+      "title": "Dashboard Row",
+      "titleSize": "h6"
+    }
+  ],
+  "schemaVersion": 14,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 0,
+        "includeAll": false,
+        "label": "Mesh auswählen:",
+        "multi": false,
+        "name": "mesh_name",
+        "options": [],
+        "query": "query_result(%%PROM_INFO_PREFIX%% and on (node_id) %%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)",
+        "refresh": 2,
+        "regex": "/.*hostname=\"([^\\\"]+)\".*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 1,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "mesh_id",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{hostname=\"[[mesh_name]]\"},node_id)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "regex",
+        "options": [],
+        "query": "label_values(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh{node_id=\"[[mesh_id]]\"},members)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 0,
+        "includeAll": false,
+        "label": "Knoten auswählen:",
+        "multi": false,
+        "name": "hostname",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{node_id=~\"[[regex]]\"},hostname)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 1,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "node_id",
+        "options": [],
+        "query": "label_values(%%PROM_INFO_PREFIX%%{hostname=\"[[hostname]]\"},node_id)",
+        "refresh": 2,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "mesh_count",
+        "options": [],
+        "query": "query_result(count(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "node_sum",
+        "options": [],
+        "query": "query_result(floor(sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": null,
+        "hide": 2,
+        "includeAll": false,
+        "label": null,
+        "multi": false,
+        "name": "link_sum",
+        "options": [],
+        "query": "query_result(round(1000 * (sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh) - floor(sum(%%PROM_GRAPH_PREFIX%%%%PROM_SEPERATOR%%mesh)))))",
+        "refresh": 2,
+        "regex": "/.* ([0-9]+) .*/",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      }
+    ]
+  },
+  "time": {
+    "from": "now-24h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "%%DASHBOARD_PREFIX%%status-mesh",
+  "version": 5
+}

+ 1 - 0
src/graph2prom.cm

@@ -5,4 +5,5 @@ is
 	$/smlnj-lib.cm
 	JSON/json-lib.cm
 	promconfig.sml
+	mesh.sml
 	graph2prom.sml

+ 23 - 1
src/graph2prom.sml

@@ -11,6 +11,8 @@ struct
 	val link_prefix = PC.link_prefix
 	val mesh_prefix = PC.mesh_prefix
 
+	structure M = Mesh
+
 	(* val timestamp = LargeInt.toString (Time.toMilliseconds (Time.now ())) *)
 
 	val newline = String.str #"\n"
@@ -18,7 +20,7 @@ struct
 		"# HELP " ^ link_prefix ^ " link quality between two nodes" ^ newline ^
 		"# TYPE " ^ link_prefix ^ " gauge" ^ newline
 	val mesh_header =
-		"# HELP " ^ mesh_prefix ^ " path length between two nodes in a mesh" ^ newline ^
+		"# HELP " ^ mesh_prefix ^ " members and (nodes + edges / 1000) of a mesh" ^ newline ^
 		"# TYPE " ^ mesh_prefix ^ " gauge" ^ newline
 
 	fun prom2string (metric, labels, scalar) =
@@ -97,6 +99,20 @@ struct
 				Real.toString tq)
 		end
 
+	fun mesh2prom nodes_id_vector { nodes, edges } =
+		let val node_count = Real.fromInt (M.Set.numItems nodes)
+		    val edge_count = Real.fromInt (M.PairSet.numItems edges)
+		    val members = ListMergeSort.sort String.>
+					(map (fn i => Vector.sub (nodes_id_vector, i))
+					     (M.Set.listItems nodes))
+		in prom2string (mesh_prefix,
+				[("node_id", hd members),
+				 ("members", ListFormat.fmt { init = "", final = "", sep = "|",
+							      fmt = fn m => m }
+							    members)],
+				Real.toString (node_count + edge_count / 1000.0))
+		end
+
 	fun complain (p, s) =
 		(TextIO.output (TextIO.stdErr, concat [p, ": ", s, "\n"]);
 		 OS.Process.failure)
@@ -112,8 +128,14 @@ struct
 				      handle exn => (json_handler "get_links" exn ; raise Fail "get_links")
 		     val links_extract = map extract_link links_json
 		     val links_prom = map (link2prom nodes_id_vector) links_extract
+		     val meshes = foldl (fn (l, ms) => M.add_link (#source l, #target l) ms)
+					[]
+					links_extract
+		     val meshes_prom = map (mesh2prom nodes_id_vector) meshes
 		 in (print link_header ;
+		     print mesh_header ;
 		     app print links_prom ;
+		     app print meshes_prom ;
 		     OS.Process.success)
 		 end handle e => complain (p, exnMessage e))
 	  | main (p, _) = complain (p, "usage: " ^ p ^ " nodes.json")

+ 79 - 0
src/mesh.sml

@@ -0,0 +1,79 @@
+(* Mesh = Zusammenhangskomponente eines einfachen Graphen *)
+
+structure Mesh =
+struct
+
+	structure OrdInt : ORD_KEY =
+	struct
+		type ord_key = Int.int
+		val compare = Int.compare
+	end
+
+	structure OrdIntPair : ORD_KEY =
+	struct
+		type ord_key = OrdInt.ord_key * OrdInt.ord_key
+		fun compare (i as (i1, i2), j as (j1, j2)) =
+			let val (ia, ib) = if i2 < i1 then (i2, i1) else i
+			    val (ja, jb) = if j2 < j1 then (j2, j1) else j
+			in case OrdInt.compare (ia, ja)
+			     of EQUAL => OrdInt.compare (ib, jb)
+			      | unequal => unequal
+		end
+	end
+
+	structure Set = SplaySetFn (OrdInt)
+	structure PairSet = SplaySetFn (OrdIntPair)
+
+	type mesh = { nodes : Set.set,
+		      edges : PairSet.set }
+	type meshes = mesh list
+
+	fun find_mesh _  [] = NONE
+	  | find_mesh id ((mesh : mesh) :: meshes) =
+		if Set.member ((#nodes mesh), id)
+		then SOME mesh
+		else find_mesh id meshes
+
+	local
+		fun add_link' (k as (k1, k2)) [] =
+			[{ nodes = Set.add (Set.singleton k1, k2),
+			   edges = PairSet.singleton k }]
+		  | add_link' (k as (k1, k2)) ((mesh as { nodes, edges }) :: meshes) =
+			if Set.member (nodes, k1)
+			then
+				if Set.member (nodes, k2)
+				then { nodes = nodes,
+				       edges = PairSet.add (edges, k) }
+				     :: meshes
+				else case find_mesh k2 meshes
+				       of NONE => { nodes = Set.add (nodes, k2),
+						    edges = PairSet.add (edges, k) }
+						  :: meshes
+					| SOME { nodes = nodes2, edges = edges2 }
+					       => { nodes = Set.union (nodes, nodes2),
+						    edges = PairSet.add
+								(PairSet.union (edges, edges2),
+								 k) }
+						  :: (List.filter
+							(fn m => not (Set.member (#nodes m, k2)))
+							meshes)
+			else
+				if Set.member (nodes, k2)
+				then case find_mesh k1 meshes
+				       of NONE => { nodes = Set.add (nodes, k1),
+						    edges = PairSet.add (edges, k) }
+						  :: meshes
+					| SOME { nodes = nodes1, edges = edges1 }
+					       => { nodes = Set.union (nodes, nodes1),
+						    edges = PairSet.add (PairSet.union (edges, edges1), k) }
+						  :: (List.filter
+							(fn m => not (Set.member (#nodes m, k1)))
+							meshes)
+				else mesh :: (add_link' k meshes)
+	in
+		fun add_link (k as (k1, k2)) meshes =
+			let val (ka, kb) = if k2 < k1 then (k2, k1) else k
+			in add_link' (ka, kb) meshes
+			end
+	end
+end