Log alert DSL


lørdag 19. januar 2013 DSL Ruby

Jeg eksperimenterer MYE med ulike måter å lage og eksekvere programmeringsspråk på for tiden. Denne gangen har jeg gjort et proof-of-concept på å lage et lite domenespesifikt språk. Med dette språket skal en kunne definere opp regler for overvåking og trigging av alarmer basert på logfiler.

Dette er bare en kjapp blogpost hvor jeg vil dokumentere litt av hvordan jeg gjør det.

1. Språket

Dette er et veldig enkelt eksempel på hvordan en overvåkingsdefinisjon kan se ut:

Alert definition
  When line matches /\[ERROR\]/
  When next matches /NRDB/
  Trigger MEDIUM alert "NRDB seem to be down"
end

Alert definition
  When line matches /\[ERROR\]/
  Trigger CRITICAL alert "#{ line }"
end

PS: NRDB er den nasjonale databasen over alle norske telefonnummer – noe vi bruker flittig i SMS-bransjen.

DSL'en bør være grei å forstå – en alarm består av en eller flere regler, og reglene har regulære uttrykk som vil brukes til å matche mot linjene i loggfilen. Alarmdefinisjonen vil også ha en eller flere triggere som avgjør hvordan det logges. Definisjonene listes i prioritert rekkefølge, og hvis en av dem slår til skal ikke noen andre trigge en alarm på den linjen.

Her er et eksempel på en ekstremt enkel logfil som jeg har brukt til å teste med:

[DEBUG] Some stuff
[DEBUG] Some more stuff
[ERROR] in log file
  The errror is in NRDB
[ERROR] some other error
[DEBUG] foo bar quux

2. Lexing

Jeg har implementert en ganske standard lexer i Ruby basert på regulære uttrykk. Koden for Lexeren og alle de andre delene av DSL-implementasjonen kan du se her – ta en titt nå eller etter at du har les hele blogposten.

Hvis jeg bruker Lexeren til å parse eksempelloggen på denne måten:

code = File.read("logalert_1.txt")
p Lexer.new.tokenize(code)

..får jeg følgende resultat:

[ [:ALERT, "Alert"], 
  [:DEFINITION, "definition"], 
  [:WHEN, "When"], 
  [:LINE, "line"], 
  [:MATCHES, "matches"], 
  [:REGEX, /\[ERROR\]/], 
  [:WHEN, "When"], 
  [:NEXT, "next"], 
  [:MATCHES, "matches"], 
  [:REGEX, /NRDB/], 
  [:TRIGGER, "Trigger"], 
  [:MEDIUM, "MEDIUM"], 
  [:ALERT, "alert"], 
  [:STRING, "Comoyo: NRDB error"], 
  [:END, "end"], 
  [:ALERT, "Alert"], 
  [:DEFINITION, "definition"], 
  [:WHEN, "When"], 
  [:LINE, "line"], 
  [:MATCHES, "matches"], 
  [:REGEX, /\[ERROR\]/], 
  [:TRIGGER, "Trigger"], 
  [:CRITICAL, "CRITICAL"], 
  [:ALERT, "alert"], 
  [:STRING, "\#{ line }"], 
  [:END, "end"]
]

3. Parsing

For å parse token-listen jeg har fått via lexingen, og bygge et abstrakt syntakstre (AST), har jeg benyttet en Ruby gem som heter Racc. Racc er en såkalt LALR(1) parser generator. LALR står for Look-Ahead, Left to Right, og denne teknikken for å bygge syntakstrær ble funnet opp på 60-tallet. Noen mere kjente slike generatorer er Yacc og GNU Bison.

Det Racc trenger er en grammar-fil. Den er også endel av det du ser her. Jeg bruker så Racc til å generere en parser-klasse i Ruby. Når jeg tester denne parseren på følgende måte:

code = File.read("logalert_1.txt")
p Parser.new.parse(code)

..får jeg følgende resultat:

#<Alerts:0x28926d8 @nodes=[
  #<Alert:0x2892798 
    @when_rules=[
      #<WhenRule:0x2892900 @order=0, @regex=/\[ERROR\]/>, 
      #<WhenRule:0x2892870 @order=1, @regex=/NRDB/>], 
    @trigger_rules=[
      #<TriggerRule:0x2892810 @severity="MEDIUM", 
        @message="Comoyo: NRDB error">]>, 
  #<Alert:0x28925d0 
    @when_rules=[
      #<WhenRule:0x28926a8 @order=0, @regex=/\[ERROR\]/>], 
    @trigger_rules=[
      #<TriggerRule:0x2892630 @severity="CRITICAL", 
        @message="\#{ line }">]>]>

4. Evaluering

Dette er (foreløpig) en ganske enkel DSL, og jeg bestemte meg for at det enkleste var å evaluere AST'en direkte. Alternativt kunne jeg definert det Martin Fowler kaller en semantisk modell i Ruby, og eksekvert denne. Jeg lager også en "runtime" som tre-nodene kan benytte når de skal evalueres.

Igjen finner du alle detaljer her – se nodes_eval.rb for evalueringskoden, og Logalert.rb for runtimen.

Når jeg så kjører Logalert fra kommandolinjen med eksempelfilen som parameter, får jeg følgende output:

MEDIUM ALERT: NRDB seem to be down
CRITICAL ALERT: [ERROR] some other error

Her er selvsagt tanken at alarmene ikke skal skrives til konsollet, men sendes til et sentralt overvåkingssystem.

Konklusjon så langt

Å lage dette her var ganske enkelt. Lexing er en smal sak, og parsingen ble også enkel når jeg brukte en parser generator. Jeg tror jeg nå har et brukbart utgangspunktet får å kunne lage en fleksibel og samtidig enkel DSL for å definere komplekse overvåkingsregler.


comments powered by Disqus