Przetwarzanie logów? Proszę bardzo!
Przydarzyła mi się ostatnio konieczność napisania konwertera do logów. Chodziło z grubsza o zamianę logów wyplutych z aplikacji railsowej na format khm khm XML :) tak aby można było logi wygodnie analizować za pomocą narzędzia Apache Chainsaw (o sposobie wykorzystania tego narzędzia w Railsach pisałem już wcześniej ). Sprawa wydaje się trywialnie prosta, piszemy kawałek kodu w Rubym który wczytuje poszczególne linie z pliku wejściowego, a każdą linię poddaje działaniu RegExpów i sprawa gotowa ale...
doświadczenie pokazuje, że takie podejście zwykle kończy się to powstaniem kodu "write once, read never". Tym razem postanowiłem powertować The Ruby Way w poszukiwaniu wygodniejszego sposobu niż tasiemcowe RegExpy... i natrafiłem na klasę StringScanner. Z jej pomocą zadanie stało się znacznie wygodniejsze w implementacji i mam wrażenie także czytelniejsze. StringScanner ułatwia podział ciągu znaków na poszukiwane fragmenty (czasem nazywane tokens). Są wyodrębniane albo za pomocą dopasowania RegExpami albo za pomocą przesunięcia wewnętrznego indexu skanera na określoną pozycję w analizowanym ciągu (np. przeskocz do 10-tego znak). W praktyce zastosowanie StringScannera wygląda następująco:
-
# Przykładowa linia z loga
-
line = "2007-09-18 12:28:49 INFO logger (app/controllers/application.rb:88:in `log_processing') - [GET] AuthController.index"
-
-
# Format:
-
# 2007-09-18 12:28:49 - timestamp wpisu
-
# INFO - poziom logowania
-
# logger - nazwa loggera
-
# (...) - informacja o miejscu wywolania, do ominięcia
-
# [GET] AuthController .... - wiadomosc zapisana w logach
-
-
# Analiza linii ma wyciągnąc z niej dane przedstawione powyżej
-
require 'strscan'
-
scanner = StringScanner.new(line)
-
-
# dopasowanie do calego time stampa
-
timestamp = scanner.scan(/^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}/)
-
-
# przeskoczenie pustych znakow pomiedzy ts a INFO
-
scanner.skip(/\s+/)
-
-
# level to jedno slowo
-
level = scanner.scan(/\w+/)
-
-
# spacje pomiedzy INFO a logger do przeskoczenia
-
scanner.skip(/\s+/)
-
-
logger = scanner.scan(/\w+/)
-
-
# inny sposob przeskoczenia - wiem ze jest tylko jedna spacja
-
# pomiedzy logger a (app/controller...) zmieniam wiec wewnętrzną pozycję
-
# zapamietana przez skaner zamiast przez scanner.skip(/\s+/)
-
scanner.pos += 1
-
-
# omijam wszystko az do zakonczenia danych z trace'a do napotkania ciagu ') - '
-
# ktory wystepuje przed komunikatem
-
scanner.skip_until(/\) - /)
-
-
# wiadomosc zapisana jest do konca linii
-
message = scanner.post_match
-
-
# i efekt na koncu
-
puts %Q(#{timestamp}\t#{logger}\t#{level}\t#{message})
Dla wprawki jeszcze analogiczny kod zrealizowany tylko za pomocą RegExpów:
-
# Dla porównania jeszcze wersja klasyczna - korzystająca tylko z regexpów
-
r = Regexp.new('^(\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(\w+)\s+\(.*?\) - (.*)')
-
match_data = r.match(line)
-
timestamp = match_data[1]
-
level = match_data[2]
-
logger = match_data[3]
-
message = match_data[4]
-
# i efekt na koncu
-
puts %Q(#{timestamp}\t#{logger}\t#{level}\t#{message})
Skoro już mamy dwie wersje kodu wstyd byłoby nie porównać ich wydajności :)
-
Rehearsal ---------------------------------------------
-
strscan 0.266667 0.016667 0.283333 ( 0.165437)
-
regexp 0.983333 0.016667 1.000000 ( 0.609420)
-
------------------------------------ total: 1.283333sec
-
-
user system total real
-
strscan 0.266667 0.000000 0.266667 ( 0.164356)
-
regexp 0.866667 0.016667 0.883333 ( 0.516766)
Jak widać czytelniejszy kod jest w dodatku szybszy! Oto niespodziewany uboczny efekt dążenia do doskonałości :)
Aktualizacja
Po uwzględnieniu zmiany zgłoszonej przez ciukesa (wyciągnięcie tworzenia RegExpa poza pętlę) wyniki dla RegExp oczywiście są lepsze, ale nadal StringScanner jest najszybszy. Dla porównania dodałem też różne sposoby tworzenia RegExpów
-
Rehearsal ----------------------------------------------
-
strscan 0.333333 0.000000 0.333333 ( 0.201591)
-
regexp new 1.016667 0.016667 1.033333 ( 0.620982)
-
regexp r 0.466667 0.000000 0.466667 ( 0.283883)
-
regexp // 0.466667 0.000000 0.466667 ( 0.290693)
-
regexp %r 0.466667 0.000000 0.466667 ( 0.275177)
-
------------------------------------- total: 2.766667sec
-
-
user system total real
-
strscan 0.300000 0.000000 0.300000 ( 0.183991)
-
regexp new 0.916667 0.016667 0.933333 ( 0.574049)
-
regexp r 0.516667 0.000000 0.516667 ( 0.315777)
-
regexp // 0.416667 0.000000 0.416667 ( 0.247187)
-
regexp %r 0.500000 0.016667 0.516667 ( 0.306069)









ciukes said,
wrzesień 19, 2007 @ 18:21
Czy moglbys powtorzyc pomiar z drobna zmiana?
W http://static.jarmark.org/scanner/scanner_benchmark.rb przesun linie:
r=Regexp.new….
poza petle.