Sunday, March 13, 2005

Project Log Analyzer #1

เมือวันศุกร์แวะไปบริษัทฯมา
เจอหนูจิ๋วกำลังหมกมุ่นกับ
การทำเอกสารสรุปรูปแบบการใช้งานของ user
โดยทำการสรุปจาก Application log file
ที่มีขนาดของข้อมูลประมาณ 40000 รายการ ต่อวัน

ตัวอย่างของข้อมูล
2004-10-11 09:13:07,384  ananda 1400 20.17.99.19 /wpm/uc2050.do(method=listWi..
2004-10-11 09:13:19,456 ananda 1400 20.17.99.19 /wpm/uc2050.do(method=search..
2004-10-11 09:13:21,694 ananda 1400 20.17.99.19 /wpm/uc2050.do(method=select..
2004-10-11 09:13:28,390 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=gotoEn..
2004-10-11 09:16:04,091 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=create..
2004-10-11 09:16:07,814 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=print1..

ก็เลยคิดจะทำโปรแกรมช่วยวิเคราะห์ให้
ใช้ idea เรื่อง label จาก Taxonomie ที่คุณ bact note ไว้
ออกแบบให้มี rule ที่ทำหน้าที่ assign label
ให้กับแต่ละบรรทัดใน log file
แล้วผู้ใช้สามารถ query เลือกแสดงผลเฉพาะ
label ที่ต้องการได้

ส่วนแรกที่ต้องทำก็คือส่วน Engine ที่ใช้ในการ assign label ให้กับ log file
โดยออกแบบให้ Engine parse log file ทีละ line
จากนั้นก็ consult rule ว่าควรจะ assign label ให้หรือเปล่า
โดยแบ่ง rule เป็น 2 แบบคือ
  • พวกที่ assign label โดยดึงค่า label มาจากข้อมูลตรงๆ
  • พวกที่ assign fix label โดยดูว่าข้อมุลนั้นมี pattern ตามที่กำหนดหรือไม่
และเพื่อให้ flexible ยิ่งขึ้นก็กำหนดให้มีพวก logic
and, or ใน rule แบบที่ 2 ได้ด้วย
กำหนดให้เก็บข้อมูลการ config rule ในลักษณะ XML File

ตัวอย่าง xml file
<spec>
<tokenizer class="mx.laz.PatternTokenizer" pattern=" ,"/>

<elements>
<element id="user" mapPos="3"/>
<element id="date" mapPos="0"/>
<element id="time" mapPos="1"/>
<element id="action" mapPos="6"/>
</elements>

<labels>
<label id="user" >
<helper
class="mx.laz.helper.CopyFromElement"
elementId="user">
</helper>
</label>

<label id="action" labelValue="uc2110">
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050\.do\(method=selected.*</pattern>
</helper>
</label>

<label id="test" labelValue="2050_ananda">
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050.*</pattern>
</helper>
</left>
<right>
<helper
class="mx.laz.helper.PatternMatch"
elementId="user">
<pattern>ananda</pattern>
</helper>
</right>
</helper>
</label>

<label id="test2" labelValue="2050_ananda_id">
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050.*</pattern>
</helper>
</left>
<right>
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="user">
<pattern>ananda</pattern>
</helper>
</left>
<right>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*id.*</pattern>
</helper>
</right>
</helper>
</right>
</helper>
</label>
</labels>
<indexRepository class="mx.laz.repository.HsqlIndexRepository"/>
</spec>
ลักษณะการทำงาน
  • Engine ทำการอ่าน log file เข้ามาทีละราย
  • tokenizer ทำการ split line ออกเป็น element
  • จากนั้นก็ทำการ assign label ให้กับ line
  • ผลการ assign label จะเก็บไว้ใน indexRepository

การ parse xml file
ปกติเราสามารถแบ่ง tool ที่ใช้ parse xml ออกเป็น 2 พวกใหญ่ๆคือ
พวกที่ 1 ต้องเขียน code เยอะหน่อย ส่วนพวกที่ 2 ก้ต้องมีการเขียน
descriptor บางอย่างที่เป็นตัวกลางระหว่าง java object
กับ xml file เช่น xml schema

Tool ที่เลือกใช้ก็คือ Common Digester
ซึ่งจัดอยู่กึ่งกลางระหว่างพวกที่ 1 กับพวกที่ 2
ที่เลือก Digester ก็เพราะยังไม่เคยใช้
ก็เลยอยากรู้่ว่ามัน powerfull แค่ไหน
Note: subproject ของ jakarta ส่วนใหญ่จะใช้
Digester เป็นตัว parse config file
เช่น Struts ใช้ digester เป็นตัว parse
action mapping file ส่วนใครที่เคยเขียน
server.xml ของ tomcat ผิดๆ คงเคยเห็น stack trace
ของ digester ที่ชวนให้งงว่าเกิดอะไรขึ้น


ตัว Digester จะใช้ SAX event + Stack ในการ parse xml file
เราจะเขียน rule เพื่อ map ให้ Digester รู้ว่าเมื่อเกิด event หนึ่งๆขึ้น
ควรจะทำอะไรดี โดยมี stack เป็นตัวช่วยเก็บข้อมูล (เก็บ temporary object)
ตัวอย่าง event ที่เกิด
  <a>         -- Matches pattern "a"
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
</a>
ตัวอย่าง code ที implement
 
Digester digest = new Digester();
digest.push(this); //push this onto stack

digest.addObjectCreate("spec/tokenizer",
"class", WhiteSpaceTokenizer.class);
digest.addSetProperties("spec/tokenizer");
digest.addSetNext("spec/tokenizer", "setTokenizer");
addObjectCreate คือ rule ที่ใช้บอกว่า
ถ้าเจอ <tokenizer> ที่อยู่ภายใต้ <spec>
ก็ให้ทำการสร้าง Object ขึ้นมาจาก Class ที่กำหนดไว้ใน
Attribute "class" ของ <tokenizer>
โดยมี default class คือ WhiteSpaceTokenizer
เมื่อสร้างเสร็จแล้วก็ให้ push object นั้นไว้ใน stack ด้วย
addSetProperties จะเป็นการ scan หา attribute
ใน current tag แล้วนำไป set ให้กับ object
ที่อยู่บนสุดของ stack
addSetNext เป็นการกำหนดว่าเมื่อจบ
tag tokenizer แล้วให้นำ object ที่อยู่บนสุดของ
stack ออกมา set ให้กับ object บน stack อันถัดไป
โดยใช้ method ชื่อ setTokenizer (ในกรณีนี้ก็ืคือ
this.setTokenizer(XXXTokenizerImpl))
  digest.addObjectCreate("spec/elements/element", 
Element.class);
digest.addSetProperties("spec/elements/element");
digest.addSetNext("spec/elements/element", "addElement");

digest.addObjectCreate("spec/labels/label", Label.class);
digest.addSetProperties("spec/labels/label");
digest.addSetNext("spec/labels/label", "addLabel");
digest.addObjectCreate("spec/labels/label/helper",
"class", DummyHelper.class);
digest.addSetProperties("spec/labels/label/helper");
digest.addBeanPropertySetter(
"spec/labels/label/helper/pattern");
digest.addSetNext("spec/labels/label/helper", "setHelper");

digest.addObjectCreate("spec/indexRepository",
"class", IndexRepository.class);
digest.addSetNext("spec/indexRepository",
"setIndexRepository");
rule addBeanPropertySet คือ rule ที่ใช้ add
ค่า Text Element ที่อยู่ระหว่าง tag pattern ให้กับ property
ที่ชื่อ pattern ของ object ที่อยู่บนสุดของ Stack

ส่วนที่ยากขึ้นมาหน่อยก็คือ ส่วน helper
ที่มีลักษณะ recursive ลงไปได้เรื่อยๆ
digester ก็มี feature ในการ map
SAX Event ในลักษณะของ patern matching เช่นเดียวกัน
  digest.addCallMethod("*/left/helper", 
"setLeft", 1, new Class[]
{AbstractHelper.class});
digest.addCallMethod("*/right/helper",
"setRight", 1, new Class[]
{AbstractHelper.class});

digest.addObjectCreate("*/left/helper", "class", DummyHelper.class);
digest.addObjectCreate("*/right/helper", "class", DummyHelper.class);

digest.addSetProperties("*/left/helper");
digest.addSetProperties("*/right/helper");
digest.addBeanPropertySetter("*/left/helper/pattern");
digest.addBeanPropertySetter("*/right/helper/pattern");

CallParamRule rule = new CallParamRule(0, true);
digest.addRule("*/left/helper", rule);
digest.addRule("*/right/helper", rule);
เครื่องหมาย * แทนการ match อะไรก็ได้
rule addCallMethodเป็นการเตรียม method
object ที่ใช้ call object ที่อยู่บนสุดของ stack
จากนั้นก็ add Method object ไว้ใน stack
rule CallParamRule เป็นการระบุให้
นำเอก object ที่อยู่บนสุดของ stack
ไป set เป็น parameter ให้กับ object
ที่อยู่บนสุดของ stack

Note: สังเกตุว่าเราสามารถเขียน digester rule ได้
2 วิธีคือ digest.addXXX กับ digest.addRule
วิธีแรกเป็นแค่ shortcut ของวิธีที่ 2


Note: จริงแล้วเราสามารถลดรูปในส่วน left กับ right
ได้ โดยกำหนด pattern match เป็น "*/helper/*/helper"
ชึ่งสามารถแทน "*/left/helper" และ "*/right/helper"
แต่ด้วยเหตุผลกดใดไม่ทราบ digester ที่เขียนแบบนี้
จะ match SAX event ไม่เจอ


Note: กรณีที่ xml ซับซ้อนอาจจะต้องมีการ debug ดูว่า digester ทำงานตามที่เราต้องการหรือไม่
ก็ให้add log4j เข้าไปใน class path พร้อมทั้ง set log4j.properties
  log4j.rootLogger=DEBUG, 1
log4j.appender.1=org.apache.log4j.ConsoleAppender
log4j.appender.1.layout=org.apache.log4j.PatternLayout
log4j.appender.1.layout.conversionPattern=%c-%L-%p-%m%n
log4j.logger.org.apache.commons.digester=DEBUG,1



ข้อมูลเพิ่มเติม

Related link from Roti

No comments: