As demonstrated in a previous post , you can query checkmk from Emacs1. 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.


  1. I know I’m abusing the availability of checkmk’s and Emacs’ SVG logo files to dare to draw a new image that puts them together through a jigsaw puzzle and make it the leading image of this post. I understand I shouldn’t modify logos, I hope in good will it showcases compatibility between the projects and nobody gets offended. In no way does this image represent that any of software projects considered or intended for them to work together or endorse the other. In the good will spirit of the GPL to study, modify and share, I daringly put them together. ↩︎