CLOVER🍀

That was when it all began.

TomcatのValveを使用して、アプリケーションのアクセス時にモニタリングを行う

前回は、JMX Remoteを使用してTomcatが管理しているMBeanにアクセスし、セッション情報などを取得しました。が、このアプローチだとHttpSessionなどの情報を直接見れず、不便です。開発中の監視とかを行いたいので、やっぱりアクセスをトリガーとして動く仕組みがよいですよね。

そこで着目したのが、この記事。
http://japan.internet.com/developer/20070123/26.html

TomcatのValveを使用して、アクセス時にリソース監視を行う方法を記載しています。今回はこれに習ってリソース情報をログ出力するようなValveを書いてみましょう。

というわけで、今回はTomcatのAPIを使用します。でもって、ValveはやっぱりScalaで書きます…。

以下、用意したファイル。
build.sbt

name := "tomcat-custom-valve"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.9.1"

organization := "littlewings"

libraryDependencies ++= Seq(
  "org.apache.tomcat" % "tomcat-catalina" % "7.0.21" % "provided",
  "org.apache.tomcat" % "tomcat-coyote" % "7.0.21" % "provided"
)

TomcatのAPIを使用するため、catalinaとcoyoteが入っております。

続いて、作成したValve。
MyTomcatCustomValve.scala

package littlewings

import scala.collection.JavaConverters._

import java.io.IOException
import java.util.Hashtable
import javax.management.openmbean.CompositeData
import javax.management.ObjectName
import javax.servlet.ServletException

import org.apache.catalina.connector.{Request, Response}
import org.apache.catalina.valves.ValveBase

object MyTomcatCustomValve {
  val DOMAIN: String = "Catalina"

  val HTTP_THREAD_POOL_OBJECT_NAME: ObjectName = {
    val hashtable = new Hashtable[String, String]
    hashtable.put("type", "ThreadPool")
    hashtable.put("name", "\"http-bio-8080\"")
    ObjectName.getInstance(DOMAIN, hashtable)
  }

  val MEMORY_OBJECT_NAME: ObjectName = {
    val hashtable = new Hashtable[String, String]
    hashtable.put("type", "Memory")
    ObjectName.getInstance("java.lang", hashtable)
  }
}

class MyTomcatCustomValve extends ValveBase {
  import MyTomcatCustomValve._

  @throws(classOf[IOException])
  @throws(classOf[ServletException])
  def invoke(request: Request, response: Response): Unit = {
    getNext.invoke(request, response)

    try {
      monitorResource(request, response)
    } catch {
      case t: Throwable => info(request,response, "", t)
    }
  }

  protected def monitorResource(request: Request, response: Response): Unit = {
    val infoLog = info(request, response,  _: Any)

    infoLog {
      request.getParameterNames.asScala.map {
        name => "[Name:%s, Value:%s]".format(name, request.getParameter(name))
      }.mkString("HttpParameters", ", ", "")
    }

    infoLog {
      request.getSession(false) match {
        case null => "Session Not Exist"
        case session =>
          session.getAttributeNames.asScala.map {
            name => "[Name:%s, Value:%s]".format(name, session.getAttribute(name))
          }.mkString("HttpSessionAttributes", ", ", "")
      }
    }

    val currentThreadCount: Int = attribute(HTTP_THREAD_POOL_OBJECT_NAME, "currentThreadCount")
    val currentThreadsBusy: Int = attribute(HTTP_THREAD_POOL_OBJECT_NAME, "currentThreadsBusy")
    infoLog {
      "CurrentThreadCount:%d, CurrentThreadsBusy:%d".format(currentThreadCount, currentThreadsBusy)
    }

    val heapMemoryUsage: CompositeData = attribute(MEMORY_OBJECT_NAME, "HeapMemoryUsage")
    val nonHeapMemoryUsage: CompositeData = attribute(MEMORY_OBJECT_NAME, "NonHeapMemoryUsage")
    infoLog {
      "HeapMemory Used:%d, Max:%d".format(heapMemoryUsage.get("used"), heapMemoryUsage.get("max"))
    }
    infoLog {
      "NonHeapMemory Used:%d, Max:%d".format(nonHeapMemoryUsage.get("used"), nonHeapMemoryUsage.get("max"))
    }
  }

  protected def attribute[T](objectName: ObjectName, attributeName: String): T =
    mserver.getAttribute(objectName, attributeName).asInstanceOf[T]

  protected def info(request: Request, response: Response, message: Any, t: Throwable = null): Unit =
    t match {
      case null => containerLog.info("Context[%s] %s".format(request.getServletContext.getContextPath, message))
      case _ => containerLog.info("Context[%s] %s".format(request.getServletContext.getContextPath, message, t))
    }
}

元のサイトに載っている通り、自作のValveを作成する際にはorg.apache.catalina.valves.ValveBaseクラスを継承して作成するのがよいみたいです。

で、ValveBaseクラスを継承したクラスで、invoke(Request, Response)メソッドをオーバーライドします。なお、ここで渡ってくるRequestとResponseは表向きの型こそorg.apache.catalina.connectorパッケージのものですが、Servlet APIのインターフェースを実装しているクラスでもあります。

つまり、Servletに渡っているRequest/Responseと同じだということですね。

なお、実体のServletとかもある意味Valveのような扱いを受けているらしく、

  @throws(classOf[IOException])
  @throws(classOf[ServletException])
  def invoke(request: Request, response: Response): Unit = {
    getNext.invoke(request, response)

でgetNext.invokeと次のValveを呼び出して実行していますが、これを忘れると実際に処理を行うServletが呼び出されなくなってしまうので、要注意です。Valveが例外でコケたりすると、そのリクエスト自体も失敗してしまうのでこの点も注意です。ちゃんとtry-catchしましょう。

なんか、Servlet Filterのノリですね。

あとは、好きな情報をログ出力とかしましょう。ValveBaseクラスを継承すると、containerLogというフィールドを利用できるようになります。これを使用してログ出力を行うと、$CATALINA_HOME/logs/localhost.yyyy-MM-dd.logファイルに結果が出力されます。ロガーの作成とかが面倒な場合は、こちらを利用するとよいでしょう。AccessLogValveのように専用のログファイルを利用したい場合は、自前で書いたりすることになります。やりたい場合は、AccessLogValveのソースを参考に。

また、ValveBaseクラスを継承すると、Tomcatが使用しているMBeanServerにアクセスすることができます。フィールド名はmserverです。これを使用すると、JMX RemoteなしでローカルアクセスのみでMBeanから情報が取得できます。

今回は、Attributeしか使用しなかったので、こんなショートカットで代替しました。

  protected def attribute[T](objectName: ObjectName, attributeName: String): T =
    mserver.getAttribute(objectName, attributeName).asInstanceOf[T]

では、作成したアプリケーションをパッケージングして、Tomcatに配置します。こういうやつは、$CATALINA_HOME/libに置く、でいいかな。

$ pwd
/xxxxx/apache-tomcat-7.0.21/lib
$ ls -1
annotations-api.jar
catalina-ant.jar
catalina-ha.jar
catalina.jar
…省略…
jsp-api.jar
scala-library.jar
servlet-api.jar
tomcat-api.jar
tomcat-coyote.jar
tomcat-custom-valve_2.9.1-0.0.1-SNAPSHOT.jar
tomcat-dbcp.jar
…省略…
tomcat-util.jar

今回作成したJARファイルは、「tomcat-custom-valve_2.9.1-0.0.1-SNAPSHOT.jar」という名前で配置してあります。あと、ソース自体はScalaで書いてあるので、scala-library.jarも置いています。

最後は$CATALINA_HOME/conf/server.xmlにValveの設定を追記します。

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
        <!-- ここから追加分 -->
        <Valve className="littlewings.MyTomcatCustomValve" />
        <!-- ここまで追加分 -->

      </Host>

AccessLogValveの次にでも書いておくといいでしょう。

では、この状態でTomcatを起動してWebアプリケーションにアクセスすると、localhost.yyyy-MM-dd.logに以下のような内容が出力されます。
※アクセスに使用したWebアプリケーションは、前回作成したリクエストパラメータをセッションに溜め込むサンプルを使用しています

Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info
INFO: Context[/hello-servlet-tomcat] HttpParameters[Name:param3, Value:hello], [Name:param2, Value:bar], [Name:param1, Value:foo]
Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info
INFO: Context[/hello-servlet-tomcat] HttpSessionAttributes[Name:param2, Value:bar], [Name:param1, Value:foo], [Name:param3, Value:hello]
Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info
INFO: Context[/hello-servlet-tomcat] CurrentThreadCount:10, CurrentThreadsBusy:1
Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info
INFO: Context[/hello-servlet-tomcat] HeapMemory Used:30423448, Max:924647424
Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info
INFO: Context[/hello-servlet-tomcat] NonHeapMemory Used:29494352, Max:136314880

リクエストパラメータ、セッションの情報、Tomcatのスレッドプールの情報、メモリ情報が出力されていますね。

これをもうちょっと工夫すると、よい情報が取れるようになるかも…と思いたいです。

最後に、元記事にちょっと疑問なのですが…ValveクラスのインスタンスはSingletonです。なので、どのリクエストを処理するのにも同じValveクラスのインスタンスが使用されるのですが、元記事はスレッド数とかをガシガシとフィールドに格納していますが、これはあんまりよくないんじゃあ…と思う次第です。

なお、全然関係ないですが、JMX Remoteを使用していた時には簡単に取得できていたプラットフォームMBeanの情報が、ローカルで取得する場合はCompositeDataから取得しなくてはならず、これの使い方がわからなくて少々ハマりました…。