;;; nsd.el --- Major mode for AOLServer error logs
;;
;; Author: David Lutterkort <lutter@arsdigita.com>
;; Created: 2000-12-15
;; $Id: nsd.el,v 1.6 2000/12/27 23:31:03 lutter Exp $

;; Purpose:

;; Mode for looking at AOLServer error logs and extracting SQL queries
;; from them.

;;; Commentary:

;; INTRODUCTION:

;; nsd.el lets you tail AOLServer error logs within Emacs. It performs
;; some simplifications on the contents of the log files to increase
;; readability, provides colorful highlighting of the error log and
;; lets you extract SQL queries with one keystroke.
;;
;; The extracted queries are put both into a special buffer and on
;; the kill-ring, so that running a query only requires a yank in
;; your SQL*Plus buffer.

;; INSTALLATION:

;; Put this file somewhere on your load-path and add the following
;; lines to your .emacs file:
;; 
;; (require 'nsd)
;; (global-set-key "\C-c\C-t" 'nsd-tail-error)
;; (add-to-list 'nsd-error-log-dirs "some-dir")
;;
;; The directory SOME-DIR is the directory where your AOLServer keeps
;; error logs. 
;;
;; Type \C-c\C-t and type the name of an AOLServer at the pompt. For an
;; error log that is kept in SOME-DIR/SERVER-error.log, the server name is
;; SERVER.  A buffer *SERVER-error* is created and the error log is tailed
;; into it.  To extract a query, put point somewhere before the "SQL()"
;; string and press "\C-c\M-w". You can now yank the query (with bind
;; variables substituted for their values :) into your SQL*Plus buffer.
;;
;; The queries are assembled in buffer *aol-query*, where you can admire
;; them in all their glory.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Required Modules
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Generic mode. 
;; In older versions of Emacs, generic.el provides
;; generic-mode, stupidly
(condition-case nil
    (require 'generic)
  (error
   (load "generic")))

;; SQL Mode for making the scratch buffer prettier
;; We don't really need SQL mode. If it's not there
;; alias it to fundamental-mode
(condition-case nil
    (require 'sql)
  (error
   (defun sql-mode () 
     (interactive)
     (fundamental-mode))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Variables
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defvar nsd-error-log-dirs
  '("/webroot/aol30f/log" "/home/aol32/log" "/var/log/aolserver")
  "*List of directories that might contain AOLServer error log files.")

(defvar nsd-query-buffer-name
  "*aol-query*"
  "*Buffer to use for assembling SQL queries that were pulled out of the
error log.")

(defvar nsd-filter-regexps
  '(("^\\(\\[.*\\]\\)\\[.*\\]\\[.*\\]" . "\\1")
    ("bind variable '\\([^']*\\)' = \\('[^']*'\\)" . ":\\1 = \\2"))
  "List of substitutions to make on the AOLServer error log to make it more
readable. Each element of the list is a pair whose car contains the regexp
to search for and whose cdr contains the replacement text.  See
query-replace-regexp for the syntax of those strings.

If you change these regexps you probably have to also change
nsd-query-regexp and nsd-bindvar-regexp.")

(defvar nsd-query-regexp
  "SQL():\\s *\\([^[]*\\)\\s-*\\["
  "Regexp to find a SQL Query in the logfile. The value of \\1 after doing
a re-search-forward needs to contain the query. This regexp is matched
against the results of nsd-error-filter.")

(defvar nsd-bindvar-regexp
  ":\\([A-Za-z0-9_]+\\) = \\('[^']*'\\)"
  "Regexp that matches bind variables and their values immediately
following a query. This regexp is matched against the results of
nsd-error-filter. 

After matching, \\1 must contain the name of the bind variable and \\2 
its value.")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The major mode for error logfiles
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define-generic-mode 'nsd-error-mode
  nil
  '("Notice" "Error" "Warning")
  '(("^\\(\\[.*\\]\\)" 1 font-lock-string-face)
    ("\\(:[A-Za-z_0-9]+\\)" 1 font-lock-variable-name-face)
    ("SQL():\\([^[]*\\)" 1 font-lock-function-name-face)
    ("\\(SQL()\\|bind variable\\)" 1 font-lock-comment-face))
  nil
  (list
   (lambda ()
     (local-set-key "\C-c\M-w" 'nsd-query-extract)
     (local-set-key "\C-c\C-n" 'nsd-next-error)
     (local-set-key "\C-c\C-p" 'nsd-prev-error)
     (local-set-key '[C-down]  'nsd-next-warning)
     (local-set-key '[C-up]    'nsd-prev-warning)
     (local-set-key "\C-c\C-f" 'nsd-next-query)
     (local-set-key "\C-c\C-b" 'nsd-prev-query)
     (local-set-key "\C-c>"    'nsd-next-start)
     (local-set-key "\C-c<"    'nsd-prev-start)
     (local-set-key "\C-c\M-f" 'nsd-error-filter)))
  "Major mode for AOLServer error logs. Does some replacements on the contents
of the error log to tighten the display up and provides a way to extract
SQL queries into the kill-ring (via nsd-query-extract).

If you turn on font-lock-mode, you also get nice highlighting.")


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Movement in the error log
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun nsd-next-start (n)
  "Move to the next AOLserver starting message"
  (interactive "p")
  (condition-case nil 
      (re-search-forward "AOLServer.*starting$" nil nil n)
    (error (error "No more server starts"))))

(defun nsd-prev-start (n)
  "Move to the previous AOLServer starting message"
  (interactive "p")
  (nsd-next-start (- 1)))

(defun nsd-next-log (n kind)
  "Move cursor to next/prev log message of a given class"
  (condition-case nil
      (search-forward (concat kind ":") nil nil n)
    (error (error (format "No more %ss" kind)))))

(defun nsd-next-error (n)
  "Move cursor to the next error"
  (interactive "p")
  (nsd-next-log n "error"))

(defun nsd-prev-error (n)
  "Move cursor to the previous error"
  (interactive "p")
  (nsd-next-error (- n)))

(defun nsd-next-warning (n)
  "Move cursor to the next warning"
  (interactive "p")
  (nsd-next-log n "warning"))

(defun nsd-prev-warning (n)
  "Move cursor to the previous warning"
  (interactive "p")
  (nsd-next-warning (- n)))

(defun nsd-next-query (n)
  "Move cursor to the next SQL query"
  (interactive "p")
  (condition-case nil
      (progn
	(re-search-forward nsd-query-regexp nil nil n)
	(goto-char (/ (+ (match-beginning 1) (match-end 1)) 2)))
    (error (error "No more queries"))))

(defun nsd-prev-query (n)
  "Move cursor to the previous SQL query"
  (interactive "p")
  (nsd-next-query (- n)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Functions for the tailing of error logs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun nsd-error-filter (start end)
  "Make the replacements specified in nsd-filter-regexps in the
   current region."
  (interactive "r")
  (let ((repls nsd-filter-regexps))
    (save-excursion
      (while repls
	(let ((pattern (caar repls))
	      (subst (cdar repls)))
	  (goto-char start)
	  (while (re-search-forward pattern end t)
	    (replace-match subst)))
	(setq repls (cdr repls))))))

(defun nsd-error-proc-filter (proc string)
  "Process filter that takes input from nsd-tail-error and runs
   nsd-error-filter on it."
  (with-current-buffer (process-buffer proc)
    (save-excursion
      (goto-char (process-mark proc))
      (insert string)
      (nsd-error-filter (process-mark proc) (point-max)))
    (set-marker (process-mark proc) (point-max))))

(defun nsd-tail-error (server)
"Tail an AOLServer error log in a buffer. SERVER is the name of the AOLServer.
The log will be tailed in a buffer named *SERVER-error*.

The logfile must be called SERVER-error.log. The directories
nsd-error-log-dirs are sought until one containing a logfile is found. The
logfile is processed by nsd-error-filter and tailed in *SERVER-error*.

The buffer is put in aolserver-error-mode."
  (interactive "sServer: ")
  (let* ((proc-name (concat server "-error"))
	 (buffer-name (concat "*" proc-name "*"))
	 (dir-list nsd-error-log-dirs)
	 (error-dir nil)
	 (errlog "/*"))
    (when (not (get-buffer buffer-name))
      (while (and dir-list (not (file-readable-p errlog)))
	(setq error-dir (file-name-as-directory (car dir-list)))
	(setq errlog (concat error-dir proc-name ".log"))
	(message "Trying %s" errlog)
	(setq dir-list (cdr dir-list)))
      (if (not (file-readable-p errlog))
	  (error "No logfile for %s found" server))
      ; found an error log
      (message "Starting tail on %s" errlog)
      (start-process proc-name buffer-name 
		     "tail" "-f" errlog)
      (process-kill-without-query (get-process proc-name) nil)
      (set-process-filter (get-process proc-name) 'nsd-error-proc-filter)
      (save-excursion
	(set-buffer buffer-name)
	(setq default-directory error-dir)
	(nsd-error-mode)))
    (set-window-buffer (selected-window) (get-buffer-create buffer-name))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Functions for extracting SQL queries from the error log
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun nsd-set-query-buffer ()
  "Setup the scratch buffer for treating queries and make it the
current buffer."
  (let* ((exists (get-buffer nsd-query-buffer-name))
	 (buffer (or exists (get-buffer-create nsd-query-buffer-name))))
    (set-buffer buffer)
    (when (not exists)
      (sql-mode)
      ; Make _ a word constituent character
      (modify-syntax-entry ?_ "w"))))
  
(defun nsd-query-extract ()
  "Extract a SQL query from the AOLServer error log. Search forward from point
for a match of nsd-query-regexp. Bind variables are found by matching
nsd-bindvar-regexp on the lines immediately following the query. The
end of the bind variables for a query is recoginized by a line that does not
match nsd-bindvar-regexp.

The bind variables are substituted into the query and the substituted query
is pasted into a buffer called nsd-query-buffer-name"
  (interactive)
  (re-search-backward "^\\[" nil t)
  (when (re-search-forward nsd-query-regexp nil t)
    (goto-char (match-end 0))
    (let ((query (match-string 1))
	  (line (count-lines (point-min) (match-beginning 1)))
	  (error-log (buffer-name (current-buffer)))
	  (bind-vars '()))
      ;; Put a mark at the beginning of query
      (push-mark (match-beginning 1))
      ;; Find the values of the bind variables
      ;; They have to be in consecutive lines immediately
      ;; following the query
      (while (re-search-forward nsd-bindvar-regexp (line-end-position) t)
	(let ((var (match-string 1))
	      (val (match-string 2)))
	(if (not (string= var "1"))
	    (setq bind-vars (cons (cons var val) bind-vars))))
	(goto-char (line-beginning-position 2)))
      ;; Go to the scratch buffer and substitute bind variables
      ;; by their values
      (nsd-set-query-buffer)
      (goto-char (point-max))
      (insert (format "\n\n-- Query from %s, line %d\n" error-log line))
      (let ((start (point)))
	(insert query "\n")
	;; Erase all the whitespace at the end of the buffer
	;; Is there a better way to do this ?
	(goto-char (point-max))
	(delete-blank-lines)
	(backward-char)
	(fixup-whitespace)
	(insert ";")
	(while bind-vars
	  (goto-char start)
	  (insert (format "-- %s = %s\n" (caar bind-vars) (cdar bind-vars)))
	  (setq start (point))
	  (while (re-search-forward 
		  (concat ":\\<" (caar bind-vars) "\\>") nil t)
	    (replace-match (cdar bind-vars)))
	  (setq bind-vars (cdr bind-vars)))
	(kill-new (buffer-substring start (point-max)))))))
	

(provide 'nsd)