From d63a6b60039ca8209e76e90a00504e96c75ef098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20N=C3=A1jera?= Date: Wed, 7 Sep 2022 21:59:43 +0200 Subject: Query systemd journalctl Can specify my hosts --- elisp/journalctl.el | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 elisp/journalctl.el diff --git a/elisp/journalctl.el b/elisp/journalctl.el new file mode 100644 index 0000000..fd6582c --- /dev/null +++ b/elisp/journalctl.el @@ -0,0 +1,226 @@ +;;; journalctl.el --- Query journalctl -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Óscar Nájera +;; +;; Author: Óscar Nájera +;; Maintainer: Óscar Nájera +;; Created: September 07, 2022 +;; Modified: September 07, 2022 +;; Version: 0.0.1 +;; Homepage: https://github.com/titan/journalctl +;; Package-Requires: ((emacs "27.1")) +;; +;; This file is not part of GNU Emacs. +;; +;;; Commentary: +;; +;; Query journalctl +;; +;;; Code: +(require 'org) +(require 'cl-extra) + +(defvar-local journalctl-current-host nil + "Keeps the optetes of the last call of journalctl.") + +(defvar-local journalctl-current-opts nil + "Keeps the optetes of the last call of journalctl.") + +(defcustom journalctl-error-keywords + '("Failed" "failed" "Error" "error" "critical" "couldn't" "Can't" "not" "Not" "unreachable") + "Keywords that mark errors in journalctl output." + :group 'journalctl + :type 'string) + +(defcustom journalctl-warn-keywords + '("Warning" "warn" "debug") + "Keywords that mark warnings in journalctl output." + :group 'journalctl + :type 'string) + +(defcustom journalctl-starting-keywords + '("Starting" "Activating" "Listening" "Reloading" "connect") + "Keywords that mark start of processes or steps in journalctl output." + :group 'journalctl + :type 'string) + +(defcustom journalctl-finished-keywords + '("Stopped" "Stopping" "Reached" "Closed" "finished" "Started" "Successfully activated" + "Received" "opened" "success" "enabled" "removed" "active" "Created" "loaded" "detected") + "Keywords that mark finished processes or steps in journalctl output." + :group 'journalctl + :type 'string) + +;;; faces +(defface journalctl-error-face + '((t :inherit error)) + "Face to mark errors in journalctl's output." + :group 'journalctl) + +(defface journalctl-warning-face + '((t :inherit warning)) + "Face to mark warnings in journalctl's output." + :group 'journalctl) + +(defface journalctl-starting-face + '((t :inherit success)) + "Face to mark starting units in journalctl's output." + :group 'journalctl) + +(defface journalctl-finished-face + '((t :inherit success :bold t)) + "Face to mark finished units in journalctl's output." + :group 'journalctl) + +(defface journalctl-timestamp-face + '((t :inherit font-lock-type-face)) + "Face for timestamps in journalctl's output." + :group 'journalctl) + +(defface journalctl-host-face + '((t :inherit font-lock-keyword-face)) + "Face for hosts in journalctl's output." + :group 'journalctl) + +(defface journalctl-process-face + '((t :inherit font-lock-function-name-face)) + "Face for hosts in journalctl's output." + :group 'journalctl) + +(defvar journalctl-font-lock-keywords + (let* ( + ;; generate regex string for each category of keywords + (error-keywords-regexp (regexp-opt journalctl-error-keywords 'words)) + (warn-keywords-regexp (regexp-opt journalctl-warn-keywords 'words)) + (starting-keywords-regexp (regexp-opt journalctl-starting-keywords 'words)) + (finished-keywords-regexp (regexp-opt journalctl-finished-keywords 'words))) + `( + (,warn-keywords-regexp . 'journalctl-warning-face) + (,error-keywords-regexp . 'journalctl-error-face) + (,starting-keywords-regexp . 'journalctl-starting-face) + (,finished-keywords-regexp . 'journalctl-finished-face) + ("^\\([A-Z][a-z]+ [0-9]+ [0-9:]+\\)" . (1 'journalctl-timestamp-face)) + ("^\\([A-Z][a-z]+ [0-9]+ [0-9:]+\\) \\(\\(?:[[:alnum:]]\\|\\.\\)+\\)" . (2 'journalctl-host-face)) + ("^\\([A-Z][a-z]+ [0-9]+ [0-9:]+\\) \\(\\(?:[[:alnum:]]\\|\\.\\)+\\) \\(.*?:\\)" . (3 'journalctl-process-face)) + + ;; note: order above matters, because once colored, that part won't change. + ;; in general, put longer words first + ))) +(defcustom journalctl-hosts + '("/ssh:alina|sudo::" + "/ssh:nina|sudo::" + "/sudo::") + "Valid hosts to connect for journal data." + :type (list 'string) + :group 'file) + +(defconst journalctl-list-of-options + '("--unit" + "--follow" + "--lines" + "--reverse" + "--grep" + "--boot" + "--since" + "--until" + "--priority") + "List of possible options to be given to journalctl." ) + +(defun journalctl-system-units (host-location) + "Query HOST-LOCATION (a tramp path) for its systemd units." + (interactive (list (completing-read "Tramp host: " journalctl-hosts))) + (let ((default-directory host-location)) + (with-temp-buffer + (start-file-process "units" (current-buffer) "systemctl" "list-units" "--all" "--quiet" "--full") + (sit-for 0.1) + (thread-last (split-string (buffer-string) "\n") + (mapcar (lambda (line) (car (split-string line)) )) + (delq nil) + (completing-read "Unit: "))))) + + +(defun journalctl (host options) + "Query the log of HOST given OPTIONS." + (interactive + (let* ((picked-host (completing-read "Tramp host: " journalctl-hosts)) + (default-options `(("--unit" ,(journalctl-system-units picked-host)) ("--follow")))) + (list picked-host default-options))) + (let ((buffer (get-buffer-create "JOURNAL LOG")) + (default-directory host)) + (apply #'start-file-process "Journal" buffer "journalctl" (flatten-tree options)) + (with-current-buffer buffer + (journalctl-mode) + (setq-local journalctl-current-host host) + (setq-local journalctl-current-opts options) + (switch-to-buffer (current-buffer))))) + +(defun journalctl-remove-opt (opt) + "Remove option from journalctl call. + +If OPT is set, remove this option." + (interactive (list (completing-read "remove option" (mapcar #'car journalctl-current-opts) nil t))) + (let ((new-opts (delq (assoc opt journalctl-current-opts) journalctl-current-opts)) + (host journalctl-current-host)) + (kill-buffer (get-buffer "JOURNAL LOG")) + (journalctl host new-opts))) + + +(defun journalctl-add-opt (opt) + "Remove option from journalctl call. + +If OPT is set, remove this option." + (interactive (list (completing-read "add option" journalctl-list-of-options nil t))) + (let* ((buff (get-buffer "JOURNAL LOG")) + (opts (delq (assoc opt journalctl-current-opts) journalctl-current-opts)) + (host journalctl-current-host)) + (kill-buffer buff) + (thread-last + (pcase opt + ((or "--since" "--until") (org-read-date t)) + ((or "--follow" "--reverse")) + (_ (read-string (concat opt "= ")))) + (list opt) + (list) + (append opts) + (journalctl host)))) + +(defun journalctl-edit-opts () + "Edit the value of 'journalctl-current-opts'." + (interactive) + (let* ((buff (get-buffer "JOURNAL LOG")) + (host journalctl-current-host) + (opts (when buff (with-current-buffer buff journalctl-current-opts)))) + (with-current-buffer (get-buffer-create "Edit Journalctl options") + (emacs-lisp-mode) + (cl-prettyprint opts) + (local-set-key "\C-c\C-c" + (lambda () + (interactive) + (goto-char (point-min)) + (kill-buffer buff) + (let ((standard-input (current-buffer))) + (journalctl host (read))) + (kill-buffer))) + (switch-to-buffer (current-buffer))))) + + +;;; keymap +(defvar journalctl-mode-map + (let ((map (make-keymap "journalctl"))) + (define-key map (kbd "n") 'journalctl-next-chunk) + (define-key map (kbd "p") 'journalctl-previous-chunk) + (define-key map (kbd "+") 'journalctl-add-opt) + (define-key map (kbd "-") 'journalctl-remove-opt) + (define-key map (kbd "e") 'journalctl-edit-opts) + (define-key map (kbd "q") 'kill-current-buffer) + map) + "Keymap for journalctl mode.") + +;;;###autoload +(define-derived-mode journalctl-mode fundamental-mode "journalctl" + "Major mode for viewing journalctl output." + ;; code for syntax highlighting + (setq font-lock-defaults '((journalctl-font-lock-keywords)))) + +(provide 'journalctl) +;;; journalctl.el ends here -- cgit v1.2.3