von Matthias Dillier

Flexibel Kibana Dashboards generieren – Teil 3/3

Dashboard mit Elastic-Beispieldaten

Elastic bietet Beispieldaten, die unter ‹Integrations → Sample data → Other sample data sets› installiert werden können. Im folgenden Beispiel werden die Beispiele aus «Sample eCommerce orders» verwendet. Mit diesen Daten kann das Beispiel nachvollzogen werden.

Dashboard als Vorlage

Daten aus dem eCommerce-Beispiel sind im Dashboard in einem Tag-cloud-, einem Liniengrafik-, zwei Balkendiagramm- und zwei Gauge-Panels (Pegeldiagramme) dargestellt worden. Die Gauge-Panels zeigen via Filter die Daten einzelner Kunden aus der eCommerce-Datei. Das Tag-cloud-Panel mit 25 Tags und das Panel mit der Liniengrafik sind als «Legacy → Aggregation based» Panels erstellt und dann in Kibana-Lens-Panels konvertiert worden, die anderen Panel sind direkt mit Kibana-Lens definiert worden. Das Dashboard sieht wie folgt aus:

Dieses Beispiel-Dashboard ist dann in die Datei blog_beispiel5.ndjson exportiert worden.

Inputdatei mit Variablen

In der Inputdatei blog_beispiel5_vars.json werden vorerst wieder einigen Variblen definiert.

{
    "tag_cloud": {
        "title": "Kunden mit grösstem Umsatz",
        "anzahl": 30
    },
    "area": {
        "title": "Durchschnittlicher Preis der Bestellungen über die Zeit"
    },
    "bars_stacked": {
        "title": "Totale Kosten der Bestellungen nach Kunde über die Zeit"
    },
    "bars": {
        "title": "Totaler Umsatz pro Kunde"
    },
    "gauge": {
        "hoehe": 10,
        "breite": 8
    },
    "dashboard_title": "generiertes_dashboard_blog_beispiel5"
}

Es wird wiederum ein Titel für das Dashboard definiert. Für die ersten 4 Grafiken werden nur andere Titel gesetzt und für die Tag-cloud die Anzahl Tags geändert und die Grösse der Gauge-Panel wird angegeben. Das sind alles nur triviale Anpassungen, die aber zeigen, wie ein solches Dashboard mit Variablen angepasst und neu generiert werden kann. Bei Bedarf lässt sich das dann weiter ausbauen.

Einfügen von jinja2 Elementen

Die Datei blog_beispiel5.j2 ist wiederum erst umgeformt worden, damit sie einfacher angepasst werden kann. Für das Tag-cloud-Panel wird der Titel und die Anzahl Tags wie folgt eingefügt:

        "panelsJSON": "[{\"type\":\"lens\",
--------->\"gridData\":{\"x\":0,
--------->\"y\":0,
--------->\"w\":24,
--------->\"h\":15,
--------->\"i\":\"7338d1e1-501e-4084-8d8a-5b7593a87e66\"},
--------->\"panelIndex\":\"7338d1e1-501e-4084-8d8a-5b7593a87e66\",
--------->\"embeddableConfig\":{\"attributes\":{\"title\":\"{{ tag_cloud.title }}\",
--------->\"visualizationType\":\"lnsTagcloud\",
--------->\"type\":\"lens\",
--------->\"references\":[{\"type\":\"index-pattern\",
--------->\"id\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",
--------->\"name\":\"indexpattern-datasource-layer-83a576c3-0e7c-4f46-8284-f87ab836f522\"}],
--------->\"state\":{\"visualization\":{\"layerId\":\"83a576c3-0e7c-4f46-8284-f87ab836f522\",
--------->\"tagAccessor\":\"8ff218c8-cd38-42a9-90bf-06ba8c0543a1\",
--------->\"valueAccessor\":\"d0227fac-22a7-4277-aa2e-cabe22f58736\",
--------->\"maxFontSize\":72,
--------->\"minFontSize\":18,
--------->\"orientation\":\"single\",
--------->\"showLabel\":true,
--------->\"colorMapping\":{\"assignments\":[],
--------->\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},
--------->\"color\":{\"type\":\"loop\"},
--------->\"touched\":false}],
--------->\"paletteId\":\"eui_amsterdam_color_blind\",
--------->\"colorMode\":{\"type\":\"categorical\"}},
--------->\"layerType\":\"data\",
--------->\"palette\":{\"name\":\"default\",
--------->\"type\":\"palette\"}},
--------->\"query\":{\"query\":\"\",
--------->\"language\":\"kuery\"},
--------->\"filters\":[],
--------->\"datasourceStates\":{\"formBased\":{\"layers\":{\"83a576c3-0e7c-4f46-8284-f87ab836f522\":{\"ignoreGlobalFilters\":false,
--------->\"columns\":{\"8ff218c8-cd38-42a9-90bf-06ba8c0543a1\":{\"label\":\"customer_full_name.keyword: Descending\",
--------->\"dataType\":\"string\",
--------->\"operationType\":\"terms\",
--------->\"scale\":\"ordinal\",
--------->\"sourceField\":\"customer_full_name.keyword\",
--------->\"isBucketed\":true,
--------->\"params\":{\"size\":{{ tag_cloud.anzahl }},
--------->\"orderBy\":{\"type\":\"column\",

Die Id des Dashboards wird wiederum gelöscht, damit Kibana beim Import eine neue, eindeutige Dashboard-Id generiert.

    "created_at": "2024-12-11T16:21:16.255Z",
    "id": "24248d69-cca9-4e52-be60-bd2fa96d24ac",

Das mit dem Template und der obigen Inputdatei generierte Dashboard sieht dann wie folgt aus, wobei das Tag-cloud-Panel nun 30 Tags darstellt und die Titel der Panels angepasst sind (im Screenshot rot umrahmt):

Inputdatei für eine flexible Anzahl Gauge-Panels

Um das Beispiel 5 etwas interessanter zu machen, wird die Inputdatei mit Variablen für mehrere Gauge-Panels ergänzt. Diese sollen wie im früheren Beispiel die Markdown-Panels in Reihen und Spalten angeordnet werden und sie sollen Angaben für verschiedene Kunden aus den eCommerce-Beispieldaten anzeigen.


    "tag_cloud": {
        "title": "Kunden mit grösstem Umsatz",
        "anzahl": 30
    },
    "area": {
        "title": "Durchschnittlicher Preis der Bestellungen über die Zeit"
    },
    "bars_stacked": {
        "title": "Totale Kosten der Bestellungen nach Kunde über die Zeit"
    },
    "bars": {
        "title": "Totaler Umsatz pro Kunde"
    },
    "gauge": {
        "hoehe": 10,
        "breite": 8,
        "spalten": 6,
        "anzahl": 20
    },
    "namen": [
        "Wagdi Shaw",
        "Elyssa Summers",
        "Abd Shaw",
        "Elyssa Hart",
        "Abd Graham",
        "Wilhemina St. Strickland",
        "Tariq Rivera",
        "Rabbia Al Baker",
        "Elyssa Martin",
        "Elyssa Lewis",
        "Elyssa Daniels",
        // etc. ...
        "Elyssa Hale",
        "Abd Burton",
        "Sultan Al Marshall",
        "Betty Morrison",
        "Mary Hampton",
        "Elyssa Rowe",
        "Elyssa Austin"
    ],
    "dashboard_title": "generiertes_dashboard_blog_beispiel5"
}

Das Array mit den Namen von Kunden aus eCommerce kann für die Filter und Beschriftungen der Gauge-Panels verwendet werden.

Data-View-Ids

DataView-Ids werden in Kibana für Referenzen auf die Daten verwendet. In den ndjson-Dateien wird noch der frühere Name Indexpattern verwendet. Die DataView-Ids sind in der ndjson-Vorlage meist schon vorhanden und sie können einfach weiter verwendet werden. Ansonsten findet man die DataView-Ids unter «Stack Management→DataViews». Dort wählt man die DataView aus, klickt auf «Edit» und «Show advanced settings» und bekommt dann die DataView-Id angezeigt. Die Id ist zudem Teil der Url:

Referenzen und Panel-Ids

Anders als bei den Beispielen mit den Markdown-Panels werden in Beispiel 5 in Elastic gespeicherte Daten verwendet, auf die aus dem Dashboard zugegriffen wird. Dazu werden im Dashboard gegen Ende der ndjson-Datei Referenzen und Ids definiert und z.B. in den Gauge-Panels wird im Array «references» darauf referenziert.

Etwas Dokumentation dazu findet man unter den folgenden Links:

References

References (references) are regular saved object references forming a graph of saved objects which depend on each other. For the Lens case, these references can be annotation groups or data views (called type: «index-pattern» in code), referencing permanent data views which are used in the current Lens visualization. Often there is just a single data view in use, but it’s possible to use multiple data views for multiple layers in a Lens xy chart. The id of a reference needs to be the saved object id of the referenced data view (see the «Handling data views» section below). The name of the reference is comprised out of multiple parts used to map the data view to the correct layer : indexpattern-datasource-layer-<id of the layer>. Even if multiple layers are using the same data view, there has to be one reference per layer (all pointing to the same data view id). References array can be empty in case of adhoc dataviews (see section below).

references array

Objects with name, id, and type properties that describe the other saved objects that this object references. Use name in attributes to refer to the other saved object, but never the id, which can update automatically during migrations or import and export.

In der exportierten Dashboardvorlage sieht die Definition der Referenzen beispielsweise wie folgt aus:

    "references": [
        {
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "7338d1e1-501e-4084-8d8a-5b7593a87e66:indexpattern-datasource-layer-83a576c3-0e7c-4f46-8284-f87ab836f522",
            "type": "index-pattern"
        },
//… …
        {
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "1400e794-2571-4b4a-b78b-2d5aab20263b:indexpattern-datasource-layer-38148eb9-48c3-40ac-8090-011abf2cdefe",
            "type": "index-pattern"
        },
        {
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "1400e794-2571-4b4a-b78b-2d5aab20263b:08984911-134f-4ff2-8c1d-bf86d034351c",
            "type": "index-pattern"
        }
    ],

Die Einträge mit

"name": "1400e794-2571-4b4a-b78b-2d5aab20263b

sind jene, die im einen Gauge-Panel referenziert werden. Dort kommt der «name» als «panelIndex» und als Komponente «i» in «gridData» vor. So kann man diese Einträge im Referenz-Array finden. Die übrigen Einträge dort gehören zu den anderen Panels. «id» ist überall die DataView-Id für die eCommerce-Beispieldaten, auf die von allen Panels aus referenziert wird.

Wenn im Dashboard mehrere Gauge-Panels definiert werden sollen, so müssen diese alle einen eindeutigen Panel-Index haben. Für das Format der Panel-Indices gelten offenbar keine grossen Vorgaben, so dass man ziemlich frei ist bei der Wahl eindeutiger Werte. Die Panel-Indices müssen bei den Gauge-Paneldefinitionen und übereinstimmend im Array mit den Referenzen definiert werden.

Einfügen von jinja2 Elementen

Die Definitionen der Gauge-Panels werden in einer Schleife generiert wie in beispiel4.j2 jene für die Markdown-Panels. Als Panelindex wird einfach «gauge_panel_nri» definiert, wobei «i» eine Laufnummer ist. So können die Indices einfach für die Panels und die Referenzen in zwei unabhängigen Schleifen generiert werden.

Die wichtigsten jinja2-Elemente bei der Definition der Gauge-Panels in blog_beisiel5.j2 sind dann:

--------->{% set global = namespace(row = 0) %}{% for i in range(0, gauge.anzahl) %}{\"type\":\"lens\",
--------->\"gridData\":{\"x\":{{ (i) % gauge.spalten * gauge.breite }},
--------->\"y\":{{ 30 + gauge.hoehe * global.row }}{% if (i + 1) % gauge.spalten == 0 %}{% set global.row = global.row + 1 %}{% endif %},
--------->\"w\":{{ gauge.breite }},
--------->\"h\":{{ gauge.hoehe }},
--------->\"i\":\"gauge_panel_nr{{ i }}\"},
--------->\"panelIndex\":\"gauge_panel_nr{{ i }}\",
--------->\"embeddableConfig\":{\"attributes\":{\"title\":\"Gauge visualization\",
--------->\"visualizationType\":\"lnsGauge\",
--------->\"type\":\"lens\",
--------->\"references\":[{\"type\":\"index-pattern\",
--------->\"id\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",
--------->\"name\":\"indexpattern-datasource-layer-38148eb9-48c3-40ac-8090-011abf2cdefe\"}],
--------->\"state\":{\"visualization\":{\"shape\":\"arc\",
--------->\"layerId\":\"38148eb9-48c3-40ac-8090-011abf2cdefe\",
--------->\"layerType\":\"data\",
--------->\"ticksPosition\":\"auto\",
--------->\"labelMajorMode\":\"auto\",
--------->\"metricAccessor\":\"e4f872bd-8510-456b-ac48-8da24c92b19d\",
--------->\"colorMode\":\"palette\",
--------->\"percentageMode\":false,
--------->\"palette\":{\"name\":\"custom\",
--------->\"params\":{\"maxSteps\":5,
--------->\"name\":\"custom\",
--------->\"progression\":\"fixed\",
--------->\"rangeMax\":100,
--------->\"rangeMin\":0,
--------->\"rangeType\":\"number\",
--------->\"reverse\":false,
--------->\"continuity\":\"none\",
--------->\"colorStops\":[{\"color\":\"#A50026\",
--------->\"stop\":0},
--------->{\"color\":\"#FEFEBD\",
--------->\"stop\":50},
--------->{\"color\":\"#006837\",
--------->\"stop\":75}],
--------->\"stops\":[{\"color\":\"#A50026\",
--------->\"stop\":50},
--------->{\"color\":\"#FEFEBD\",
--------->\"stop\":75},
--------->{\"color\":\"#006837\",
--------->\"stop\":100}],
--------->\"steps\":5},
--------->\"type\":\"palette\"},
--------->\"minAccessor\":\"4399d773-95c6-4ecb-bc1b-a26a9d9f0d09\",
--------->\"maxAccessor\":\"9a3f8db0-a822-4627-9906-8fab5bf235df\",
--------->\"labelMinor\":\"{{ namen[i] }}\"},
--------->\"query\":{\"query\":\"customer_full_name.keyword : \\\"{{ namen[i] }}\\\"  \",
--------->\"language\":\"kuery\"},
--------->\"filters\":[{\"meta\":{\"alias\":null,
--------->\"disabled\":false,
--------->\"index\":\"5c0dea8b-d51a-4f34-8240-520a78d12164\",
--------->\"key\":\"products.taxless_price\",
--------->\"negate\":false,
--------->\"type\":\"exists\",
--------->\"value\":\"exists\"},
--------->\"query\":{\"exists\":{\"field\":\"products.taxless_price\"}},
//… ...
--------->\"enhancements\":{},
--------->\"hidePanelTitles\":true},
--------->\"title\":\"Gauge visualization\"}{% if loop.index != loop.length %},{% endif %}{% endfor %}]",

Am Anfang und Ende stehen die Angaben für die Schleife, wobei am Ende wieder geprüft werden muss, ob ein Komma nötig ist oder ob das letzte Element des Arrays erreicht ist. Das uns die Berechnung der Position der Panels ist analog Beispiel 4 gemacht.

Weiter wird noch der Name aus dem Array mit den Kundennamen als Label definiert und in der Filterabfrage angegeben.

Wichtig sind die zwei Definitionen für die Panelindices.

--------->\"i\":\"gauge_panel_nr{{ i }}\"},
--------->\"panelIndex\":\"gauge_panel_nr{{ i }}\",

Zudem müssen weiter unten die Referenzen für alle Gauge-Panels in einer Schleife generiert werden.

        {
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "51c3912f-6d82-4be5-82e4-31a6549d9df4:indexpattern-datasource-layer-446d03ef-469d-419a-90ab-40632cd777f5",
            "type": "index-pattern"
        },
        {% for i in range(0, gauge.anzahl) %}{
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "gauge_panel_nr{{ i }}:indexpattern-datasource-layer-38148eb9-48c3-40ac-8090-011abf2cdefe",
            "type": "index-pattern"
        },{% endfor %}
        {
            "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
            "name": "gauge_panel_nr0:08984911-134f-4ff2-8c1d-bf86d034351c",
            "type": "index-pattern"
        }

Der Befehl

jinjanate blog_beispiel5.j2 blog_beispiel5_vars.json -o generiertes_dashboard_blog_beispiel5.ndjson

generiert nun folgendes Dashboard, in dem die Gauge-Panels wiederholt werden und regelmässig in 6 Spalten angeordnet sind:

Variante mit Gauge-Panels zu verschiedenen Metriken

Als weitere Variante von Beispiel 5 habe ich Gauge-Panels zu zwei Metriken generiert und die Grenzen für die Farben sowie die Maximalwerte in den Gauge-Panels über Variablen in der Inputdatei definiert. Wenn nur eine Metrik angegeben wird, kann damit auch das vorherige Dashboard generiert werden.

Inputdatei mit zwei Metriken für die Gauge-Panels

Der folgende Teil der Inputdatei ist angepasst worden, wobei für die Angaben jeweils Arrays verwendet worden sind.

    "gauge": {
        "hoehe": 10,
        "breite": 8,
        "spalten": 6,
        "anzahl": 18,
        "query_fields": [
            "products.taxless_price",
            "products.quantity"
        ],
        "range_max" : [500, 12],
        "color_start" : [[0, 100, 300],
                                        [0,3,6]],
        "color_stop" : [[100, 300, 500],
                                       [3,6,12]]
    },
    "namen": [
        "Wagdi Shaw",
        "Elyssa Summers",

Einfügen von jinja2 Elementen

Hier sind nun zwei verschachtelte Schleifen nötig, eine über die Metriken und innerhalb dieser Schleife jeweils eine über die Anzahl gewünschter Gauge-Panels.

Als Hilfestellung hier noch die wichtigen Teile der json-Datei vor der Umformatierung in blog_beispiel5.j2:

--------->{% set global = namespace(row = 0) %}{% for field in gauge.query_fields %}{% set outer_loop = loop %}{% for i in range(0, gauge.anzahl) %}{\"type\":\"lens\",
--------->\"gridData\":{\"x\":{{ (i + ( outer_loop.index0 * gauge.anzahl ) ) % gauge.spalten * gauge.breite }},
--------->\"y\":{{ 30 + gauge.hoehe * global.row }}{% if (i + 1 + ( outer_loop.index0 * gauge.anzahl ) ) % gauge.spalten == 0 %}{% set global.row = global.row + 1 %}{% endif %},
--------->\"w\":{{ gauge.breite }},
--------->\"h\":{{ gauge.hoehe }},
--------->\"i\":\"gauge_panel_nr{{ i + ( outer_loop.index0 * gauge.anzahl ) }}\"},
--------->\"panelIndex\":\"gauge_panel_nr{{ i + ( outer_loop.index0 * gauge.anzahl ) }}\",
--------->\"embeddableConfig\":{\"attributes\":{\"title\":\"Gauge visualization\",
//… …
--------->\"rangeMax\":{{ gauge.range_max[outer_loop.index0] }},
--------->\"rangeMin\":0,
--------->\"rangeType\":\"number\",
--------->\"reverse\":false,
--------->\"continuity\":\"none\",
--------->\"colorStops\":[{\"color\":\"#A50026\",
--------->\"stop\":{{ gauge.color_start[outer_loop.index0][0] }}},
--------->{\"color\":\"#FEFEBD\",
--------->\"stop\":{{ gauge.color_start[outer_loop.index0][1] }}},
--------->{\"color\":\"#006837\",
--------->\"stop\":{{ gauge.color_start[outer_loop.index0][2] }}}],
--------->\"stops\":[{\"color\":\"#A50026\",
--------->\"stop\":{{ gauge.color_stop[outer_loop.index0][0] }}},
--------->{\"color\":\"#FEFEBD\",
--------->\"stop\":{{ gauge.color_stop[outer_loop.index0][1] }}},
--------->{\"color\":\"#006837\",
--------->\"stop\":{{ gauge.color_stop[outer_loop.index0][2] }}}],
--------->\"steps\":5},
//… …
--------->\"9a3f8db0-a822-4627-9906-8fab5bf235df\":{\"label\":\"Static value: {{ gauge.range_max[outer_loop.index0] }}\",
--------->\"dataType\":\"number\",
--------->\"operationType\":\"static_value\",
--------->\"isStaticValue\":true,
--------->\"isBucketed\":false,
--------->\"scale\":\"ratio\",
--------->\"params\":{\"value\":\"{{ gauge.range_max[outer_loop.index0] }}\"},
//… …
--------->\"title\":\"Gauge visualization\"}{% if outer_loop.index != outer_loop.length or loop.index != loop.length %},{% endif %}{% endfor %}{% endfor %}]",

Das generierte Dashboard mit den Gauge-Panels zu den zwei verschiedenen Metriken sieht dann wie folgt aus:

Weitere Ideen

Es geht in dieser Blogreihe darum, das Prinzip zur Generierung von Dashboards mit jinja2-Templates vorzustellen. Analog können viele weitere Ideen zur Automatisierung umgesetzt werden und zwar auch in anderen Bereichen.

Mit Shell-Skripts oder Skripts in anderen Skriptsprachen kann die Automatisierung erweitert werden. Beispielsweise können in einem Skript mehrere Dashboards generiert oder in einer Schleife können Dashboards für verschiedene Umgebungen oder Kunden generiert werden.

Es können auch mehrere Dashboards in eine einzelne ndjson-Datei geschrieben werden.

Der Import von Dashboards in Kibana per REST-Schnittstelle kann ebenfalls nach der Generierung von Dashboards in dasselbe Skript eingefügt werden. Die Dokumentation dazu findet man hier:

Fazit

Mit den vorgestellten Ideen und Werkzeugen lassen sich Kibana-Dashboards flexibel generieren. Leider fehlt eine Referenzdokumentation für das Format der beim Export und Import von Dashboards und weiteren gespeicherten Objekten verwendeten ndjson-Dateien. So braucht es manchmal etwas Fantasie und Tests für die konkrete Umsetzung. Das Vorgehen lohnt sich aber schnell, wenn mehrere ähnliche Dashboards gebraucht werden.

Dasselbe Vorgehen zur Automatisierung kann nicht nur im Elastic Stack sondern mit vielen anderen Softwarepaketen verwendet werden.

Melden Sie sich ohne zu zögern für ein kostenloses Beratungsgespräch bei Swissmakers, um zu erfahren, wie wir Sie in den Bereichen Automatisierung und Elastic unterstützen können.

Foto des Autors

Matthias Dillier

Mathematiker, Informatiker und Saxophonist mit langjähriger Erfahrung in diversen Bereichen der Informatik. Aktuell befasst er sich besonders mit Monitoring, Problemanalysen, Logauswertungen, Grafiken und der Automatisierung von Betriebsprozessen.

Hinterlassen Sie einen Kommentar

2 × fünf =

de_CH