blob: c2622e68a9b2b01b749bff3ee395d72b331fb55e [file] [log] [blame]
;; Licensed to the Apache Software Foundation (ASF) under one
;; or more contributor license agreements. See the NOTICE file
;; distributed with this work for additional information
;; regarding copyright ownership. The ASF licenses this file
;; to you under the Apache License, Version 2.0 (the
;; "License"); you may not use this file except in compliance
;; with the License. You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
(ns backtype.storm.daemon.logviewer
(:use compojure.core)
(:use [hiccup core page-helpers])
(:use [backtype.storm config util log])
(:use [backtype.storm.ui helpers])
(:use [ring.adapter.jetty :only [run-jetty]])
(:import [org.slf4j LoggerFactory])
(:import [ch.qos.logback.classic Logger])
(:import [ch.qos.logback.core FileAppender])
(:import [java.io File FileInputStream])
(:import [backtype.storm.ui InvalidRequestException])
(:require [compojure.route :as route]
[compojure.handler :as handler]
[ring.middleware.keyword-params]
[ring.util.response :as resp]
[clojure.string :as string])
(:gen-class))
(defn page-file
([path tail]
(let [flen (.length (clojure.java.io/file path))
skip (- flen tail)]
(page-file path skip tail)))
([path start length]
(with-open [input (FileInputStream. path)
output (java.io.ByteArrayOutputStream.)]
(if (>= start (.length (clojure.java.io/file path)))
(throw
(InvalidRequestException. "Cannot start past the end of the file")))
(if (> start 0)
;; FileInputStream#skip may not work the first time.
(loop [skipped 0]
(let [skipped (+ skipped (.skip input (- start skipped)))]
(if (< skipped start) (recur skipped)))))
(let [buffer (make-array Byte/TYPE 1024)]
(loop []
(when (< (.size output) length)
(let [size (.read input buffer 0 (min 1024 (- length (.size output))))]
(when (pos? size)
(.write output buffer 0 size)
(recur)))))
(.toString output)))))
(defn log-root-dir
"Given an appender name, as configured, get the parent directory of the appender's log file.
Note that if anything goes wrong, this will throw an Error and exit."
[appender-name]
(let [appender (.getAppender (LoggerFactory/getLogger Logger/ROOT_LOGGER_NAME) appender-name)]
(if (and appender-name appender (instance? FileAppender appender))
(.getParent (File. (.getFile appender)))
(throw
(RuntimeException. "Log viewer could not find configured appender, or the appender is not a FileAppender. Please check that the appender name configured in storm and logback agree.")))))
(defn pager-links [fname start length file-size]
(let [prev-start (max 0 (- start length))
next-start (if (> file-size 0)
(min (max 0 (- file-size length)) (+ start length))
(+ start length))]
[[:div.pagination
[:ul
(concat
[[(if (< prev-start start) (keyword "li") (keyword "li.disabled"))
(link-to (url "/log"
{:file fname
:start (max 0 (- start length))
:length length})
"Prev")]]
[[:li (link-to
(url "/log"
{:file fname
:start 0
:length length})
"First")]]
[[:li (link-to
(url "/log"
{:file fname
:length length})
"Last")]]
[[(if (> next-start start) (keyword "li.next") (keyword "li.next.disabled"))
(link-to (url "/log"
{:file fname
:start (min (max 0 (- file-size length))
(+ start length))
:length length})
"Next")]])]]]))
(defn- download-link [fname]
[[:p (link-to (url-format "/download/%s" fname) "Download Full Log")]])
(defn log-page [fname start length grep root-dir]
(let [file (.getCanonicalFile (File. root-dir fname))
file-length (.length file)
path (.getCanonicalPath file)]
(if (= (File. root-dir)
(.getParentFile file))
(let [default-length 51200
length (if length
(min 10485760 length)
default-length)
log-string (escape-html
(if start
(page-file path start length)
(page-file path length)))
start (or start (- file-length length))]
(if grep
(html [:pre#logContent
(if grep
(filter #(.contains % grep)
(.split log-string "\n"))
log-string)])
(let [pager-data (pager-links fname start length file-length)]
(html (concat pager-data
(download-link fname)
[[:pre#logContent log-string]]
pager-data)))))
(-> (resp/response "Page not found")
(resp/status 404)))))
(defn download-log-file [fname req resp log-root]
(let [file (.getCanonicalFile (File. log-root fname))
path (.getCanonicalPath file)]
(if (= (File. log-root) (.getParentFile file))
(-> (resp/response file)
(resp/content-type "application/octet-stream"))
(-> (resp/response "Page not found")
(resp/status 404)))))
(defn log-template
([body] (log-template body nil))
([body fname]
(html4
[:head
[:title (str (escape-html fname) " - Storm Log Viewer")]
(include-css "/css/bootstrap-1.4.0.css")
(include-css "/css/style.css")
]
[:body
(concat
[[:h3 (escape-html fname)]]
(seq body))
])))
(defn- parse-long-from-map [m k]
(try
(Long/parseLong (k m))
(catch NumberFormatException ex
(throw (InvalidRequestException.
(str "Could not make an integer out of the query parameter '"
(name k) "'")
ex)))))
(defroutes log-routes
(GET "/log" [:as req & m]
(try
(let [start (if (:start m) (parse-long-from-map m :start))
length (if (:length m) (parse-long-from-map m :length))
root-dir (:log-root req)]
(log-template (log-page (:file m) start length (:grep m) root-dir)
(:file m)))
(catch InvalidRequestException ex
(log-error ex)
(ring-response-from-exception ex))))
(GET "/download/:file" [:as {:keys [servlet-request servlet-response]} file & m]
(download-log-file file servlet-request servlet-response (:log-root m)))
(route/resources "/")
(route/not-found "Page not found"))
(def logapp
(handler/api log-routes)
)
(defn conf-middleware
"For passing the storm configuration with each request."
[app log-root]
(fn [req]
(app (assoc req :log-root log-root))))
(defn start-logviewer [port log-root]
(run-jetty (conf-middleware logapp log-root) {:port port}))
(defn -main []
(let [conf (read-storm-config)
log-root (log-root-dir (conf LOGVIEWER-APPENDER-NAME))]
(start-logviewer (int (conf LOGVIEWER-PORT)) log-root)))