24. Detecting Errors with a Log Monitoring Application

Imagine you have a particular bug in production, something that only occurs once a day. When it does occur, you want to know about it right away. You need a tool to monitor the log files and trigger a warning when a bug occurs. What a great opportunity to use Clojure in your day job!

Assumptions

In this chapter we assume you have Leiningen set up on your machine.

Benefits

The benefit of this chapter is that you learn a way to use Clojure in your day-to-day job when maintaining a production system.

The Recipe—Code

In this recipe we’ll produce a simple Clojure project to read a log file being updated in real time, and we’ll take action if an error is detected. Let’s get started.

1. Create a new Leiningen project monitor-log in your projects directory, and change to that directory:

lein new app monitor-log
cd monitor-log

2. Modify the projects.clj to look like the following:

(defproject monitor-log "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.7.0-beta2"]]
  :main monitor-log.core)

3. Modify the file src/monitor_log/core.clj to look like the following:

(ns monitor-log.core
  (:gen-class)
  (:import (java.io RandomAccessFile)))

(def sleep-interval 5000)
(def pattern "^([\d.]+) (\S+) (\S+) \[([\w:/]+\s[+\-]\d{4})\]
"(.+?)" (\d{3}) (\d+) "([^"]+)" "([^"]+)"")

(defn process-log-entry
  "Extract information from the log file line."
  [line]
  (let [log-line-seq (first (re-seq (re-pattern pattern) line))
  log-line-info
(zipmap [:everything :ipaddress :block1 :block2 :datetime :request :response
:bytessent :referer :browser] log-line-seq)]
  (if (not (empty? log-line-seq))
    (do
      (println "IP Address: " (:ipaddress log-line-info))
      (println "Date and Time: " (:datetime log-line-info))
      (println "Request: " (:request log-line-info))
      (println "Response: " (:response log-line-info))
      (println "Bytes Sent: " (:bytessent log-line-info))
      (println "Referer: " (:referer log-line-info))
      (println "Browser: " (:browser log-line-info)))
    (println "line: " line " doesn't match regex"))
  (println "")))

(defn read-log
  "Loop through the log file - sit in a loop whilst waiting for updates."
  [^RandomAccessFile log-file]
  (let [line (.readLine log-file)]
    (if line
      (process-log-entry line)))
    (if (= (.getFilePointer log-file) (.length log-file))
      (Thread/sleep sleep-interval))
    (recur log-file))

(defn -main
  "Called from the :main entry in the Leiningen file."
  [& args]
  (println "main run")
  (read-log (RandomAccessFile. "logfile.log" "r")))

4. Now create a file called logfile.log:

touch logfile.log

5. Add the following contents to logfile.log:

157.56.93.84 - - [30/Jun/2015:06:25:15 +1000] "GET / HTTP/1.1" 200 962 "-"
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
72.14.199.77 - - [30/Jun/2015:06:27:11 +1000] "GET /blog/feed/ HTTP/1.1" 304
220 "-" "Feedfetcher-Google; (+http://www.google.com/feedfetcher.html; 26
subscribers; feed-id=11698687291650174072)"
213.186.119.141 - - [30/Jun/2015:06:28:54 +1000] "GET /feed/ HTTP/1.1" 200
26309 "-" "AhrefsBot.Feeds v0.1; http://ahrefs.com/"
66.249.75.234 - - [30/Jun/2015:06:29:37 +1000] "GET /bin/ HTTP/1.1" 200 723
"-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"

Testing the Solution

Let’s test the solution.

1. Start the program with Leiningen:

lein run

2. Observe output like the following:

IP Address:  213.186.119.141
Date and Time:  30/Jun/2015:06:28:54 +1000
Request:  GET /feed/ HTTP/1.1
Response:  200
Bytes Sent:  26309
Referer:  -
Browser:  AhrefsBot.Feeds v0.1; http://ahrefs.com/

3. Open the file logfile.log and add two lines:

test line 1
test line 2

4. Observe the following in the output:

line:
 test line 1
doesn't match regex

line:
 test line 2
doesn't match regex

5. Press CTRL-C to kill the log monitoring loop.

Notes on the Recipe

The function process-log-entry takes a String representing a line in the log file. It then runs a Clojure regex over it, returning a vector of items from each of the regex capture groups. This way you have a vector for each of the different ‘columns’ in the log file. Then we convert this to a map with keys representing each of the column names. It then prints out information corresponding to each of the ‘columns’ that we have extracted from the map.

The function read-log takes a file and reads it line by line, calling itself in a recursive fashion. If a line is read, it is fed off to the process-log-entry function we just defined. If the file pointer is at the end of the file, then the function sleeps the thread for the sleep-interval, which in our case is 5 seconds. Then the function calls itself to try to read the next line. This means this function will run indefinitely, until the JVM is terminated. It will keep looking to see if the log file has been updated and if it can read more from the file.

The function –main is called by the Leiningen process and is used by the (gen-class) namespace function to generate the main method in the class file. This kicks off the read-log function.

Conclusion

We’ve used Clojure to continuously monitor a log file and read information from each line via regex. This should be easy to roll out in your workplace to keep an eye on production systems. The first thing to do would be to add the capability to email you when a particular event occurs in the log file. The next step is to add the capability to monitor rolling log files. Think about what else you could add to make this more valuable for your production applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.106.7