As demonstrated in a previous post
, you can query checkmk
from Emacs
.
Which turns it into an interface for your data. The next step is to create some
basic views of checkmk
to skip using its Web UI.
Most of checkmk
’s data is tabular, most of its Web UI shows tables. With
Emacs-29
the traditional and limited tabulated-list-mode
gained an
alternative: the vtable
(variable pitch tables). It is way more flexible and
useful. Instead of being a major mode for a single table in the entire buffer, a
vtable
can show up anywhere in your buffer and you can even have many on the
same buffer.
There are two tables I want to use for my Emacs
UI from the checkmk
’s main
dashboard: service problems and recent events. I don’t need all the data
from the dashboard and those tables, just a simpler view with enough information
to take action.
First I define the specifications for those tables. In LISP
’s spirit of a data
driven design I keep the specification simple using the list basic structure.
This is the column specification for a vtable
plus the extra key value pair of
:column
.
1(defconst cmk-problem-col-spec
2 `((:name "STATE" :column "service_state" :formatter cmk-state-coloring :min-width 6)
3 (:name "Host" :column "host_name")
4 (:name "Service" :column "service_description")
5 (:name "Since" :column "service_last_state_change" :formatter cmk-timestamp-to-date :min-width 18)
6 (:name "Check output" :column "service_plugin_output" :formatter cmk-purge-bangs)))
7
8(defconst cmk-log-col-spec
9 '((:name "STATE" :column "log_state" :min-width 6 :formatter cmk-state-coloring)
10 (:name "Host" :column "host_name")
11 (:name "Service" :column "service_description")
12 (:name "Time" :column "log_time" :min-width 18 :formatter cmk-timestamp-to-date)
13 (:name "Check output" :column "log_plugin_output" :formatter cmk-purge-bangs)))
Observe the symmetry in data between those tables. They are almost the same,
only that their data comes from different tables. As we’ll see later the filters
are different too.
To render the vtable
from the spec I define the next function. It creates the
appropriate livestatus
query, including the header for a JSON
output format,
which way more usable than a csv, because Emacs
has a native parser for it. It
gets the data, parses the JSON
and dumps it directly into the make-vtable
function.
1(defun cmk-vtable (table spec filters)
2 "Render a tabular view for livestatus TABLE with SPEC columns using FILTERS."
3 (let ((livestatus-query
4 (format "GET %s\nColumns: %s\n%s\n%s\n\n"
5 table
6 (cmk-colums-from-spec spec)
7 filters
8 "OutputFormat: json")))
9 (when-let ((objects (cmk-livestatus->json livestatus-query)))
10 (make-vtable
11 :columns (mapcar (lambda (li) (map-delete (map-copy li) :column)) spec)
12 :use-header-line nil
13 :objects objects
14 :keymap (define-keymap "q" #'kill-current-buffer)))))
The tables specification gets processed in two places, one to extract the
columns for the livestatus
query, shown next. Then the rest for the vtable
columns definition, removing precisely the :column
data.
1(defun cmk-colums-from-spec (spec)
2 "Extract the column names from SPEC and return a livestatus column command."
3 (mapconcat
4 (lambda (column)
5 (or (plist-get column :column)
6 (plist-get column :name)))
7 spec
8 " "))
The data request and processing from livestatus
is similar to how we queried
data in the previous post
. This time we must however wait for the process to
return before we try to parse the JSON
data and return a LISP
list.
1(defun cmk-livestatus->json (query)
2 "Send QUERY to livestatus then parse the result assuming is JSON."
3 (let ((cmks (cmk-livestatus-query query)))
4 (accept-process-output cmks 0.5)
5 (with-current-buffer (process-buffer cmks)
6 (goto-char (point-min))
7 (json-parse-buffer :array-type 'list))))
Next thing is to define the formatting functions for some of the table columns.
1(defun cmk-state-coloring (state)
2 "Font color for numeric STATE input as string."
3 (pcase state
4 (0 (propertize "OK" 'face 'font-lock-string-face))
5 (1 (propertize "WARN" 'face 'font-lock-warning-face))
6 (2 (propertize "CRIT" 'face 'font-lock-keyword-face))
7 (3 (propertize "UNKN" 'face 'font-lock-builtin-face))
8 (_ state)))
9
10(defun cmk-timestamp-to-date (timestamp &optional fmt-str)
11 "TIMESTAMP to human readable date follownig FMT-STR.
12Default is \"%Y-%m-%d %H:%M\"."
13 (thread-last
14 (if (stringp timestamp)
15 (string-to-number timestamp)
16 timestamp)
17 (seconds-to-time)
18 (format-time-string (or fmt-str "%Y-%m-%d %H:%M"))))
19
20(defun cmk-purge-bangs (str)
21 "Remove cmk status output bangs from STR."
22 (replace-regexp-in-string (rx "(" (+ "!") ")") "" str))
The last thing is to implement the function that renders both of the tables in
the same buffer. Here I implement the livestatus
filters as string literals.
They are that way the exact statements for the filters. They don’t need any
processing.
1(defun cmk-dashboard ()
2 "Render problems view."
3 (interactive)
4 (let ((inhibit-read-only t))
5 (with-current-buffer (get-buffer-create "*CMK View*")
6 (special-mode)
7 (erase-buffer)
8 (insert "Active Service Problems\n")
9 (cmk-vtable "services" cmk-problem-col-spec
10 "Filter: service_state = 0
11Filter: service_has_been_checked = 1
12And: 2
13Negate:
14Filter: service_has_been_checked = 1
15Filter: service_scheduled_downtime_depth = 0
16Filter: host_scheduled_downtime_depth = 0
17And: 2
18Filter: service_acknowledged = 0
19Filter: host_state = 1
20Filter: host_has_been_checked = 1
21And: 2
22Negate:
23Filter: host_state = 2
24Filter: host_has_been_checked = 1
25And: 2
26Negate:")
27 (goto-char (point-max))
28 (insert "\nEvent in recent 24 hours\n")
29 (cmk-vtable "log" cmk-log-col-spec
30 (format
31 "Filter: log_time >= %d
32Filter: class = 1
33Filter: class = 3
34Filter: class = 8
35Or: 3
36Filter: log_state_type = HARD"
37 (floor
38 (- (time-to-seconds (current-time))
39 (* 24 3600)))))
40 (display-buffer (current-buffer)))))
That is all I need, I have now a almost instant view of the main dashboard. Web
rendering is really slow once you have an alternative to compare.
The next step would be to consider other tables to implement, yet I don’t use
them myself. This is good enough for me.