diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2163ddd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +raport/top.svg +raport/raport*.html +raport/bar.svg +raport/bar.png diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..5fef1d2 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,79 @@ +# Skriptide kasutamine + +Allolevates näidetes on programmid käivitatud nii et +kaust milles skript paikneb on lisatud ```PATH``` keskkonnamuutujasse: + +```bash +PATH=/tee/kaustani/logide-parsimine/bin:$PATH +``` + +Näiteks kodukataloogis bin kausta puhul: + +```bash +PATH=~/bin:$PATH +``` + +Skripti käisitsi alla laadimisel ja kausta paigutamisel tuleb anda ka käivitamise õigsed, näiteks: + +```bash +chmod +x ~/bin/top2 +``` + +Selleks et PATH muutujasse automaatselt näiteks ~/bin kaust lisataks võib ```~/.bashrc``` faili lisada ```PATH=~/bin/:$PATH```. + + +# top2 + +Bashis kirjutatud skript mis parsib standardsisendist ning kuvab tulemused standardväljundisse + +Käivitamiseks: + +(cat access.log; zcat access.log.1.gz) | top2 + +# top4 + +Pythonis kirjutatud skript mis leiab enim külastatud URL-id. + +Käivitamiseks: + +```bash +cat access.log | top4 | head +``` + +# top5 + +Leiab top 5 enim külastatud URL-i, päringuid teinud IP aadressi ning enim kasutatud veebilehitsejat. + +Käivitamiseks, nii et veateated kirjutatakse ```errors``` faili + +```bash +cat access.log | top4 2>errors +``` + +# filter2 + +Python programm kaustast Apache logisid otsimiseks ning nende seest +alguse kuupäeva/kellaaja ning ajavahemiku järgi logikirjete otsimiseks. + +Käsk võtab kolm argumenti: + +* tee logide kaustani +* otsitava ajavahemiku alguse kuupäev/kellaaeg, kujul "YYYY-MM-DD HH:MM:SS" +* ajavahemik minutites + +Käivitamiseks: + +```bash +filter2 /tee/logide/kaustani "2015-12-12 18:25:10" 10 +``` + +Käsurea analoog teatavate mööndustega on järgnev: + +```bash +grep -E '12/Dec/2015:18:(2[5-9]|3[0-5])' -r /tee/logide/kaustani +``` + +Pythoni variandi põhilised erisused: + +* Jooksvalt pakib lahti .gz lõpulised failid +* Jätab vahele failid mille sees otsitavat ajavahemikku kindlasti ei ole diff --git a/bin/filter2 b/bin/filter2 new file mode 100755 index 0000000..71c909e --- /dev/null +++ b/bin/filter2 @@ -0,0 +1,61 @@ +#!/usr/bin/python +# encoding: utf-8 +import os +import re +import gzip +import sys +from datetime import datetime, timedelta + +try: + dir_logs = sys.argv[1] +except IndexError: + dir_logs = "/var/log" + +try: + scope_begin = datetime.strptime(sys.argv[2], "%Y-%m-%d %H:%M:%S") +except IndexError: + scope_begin = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + +try: + duration = int(sys.argv[3]) +except IndexError: + duration = 5 + +scope_end = scope_begin + timedelta(minutes=duration) +sys.stderr.write("Otsin logikirjeid vahemikus %s kuni %s kaustast %s\n" % (scope_begin, scope_end, dir_logs)) + +RE_LOG_ENTRY = "(?P.+?) - (?P.+?) \[(?P.+?)\] \"(?P[A-Z]+) (?P.+) HTTP/1.[01]\" (?P\d+) (?P\d+) \"(?P.+?)\" \"(?P.+?)\"" + +# os.walk käib rekursiivselt kataloogipuu läbi +for root, dirs, files in os.walk(dir_logs): + # iga kausta kohta (root) on kättesaadav alamkataloogide nimekiri (dirs) + # ning failide nimekiri (files) + for filename in files: + if filename.startswith("access.log"): + path = os.path.join(root, filename) + file_end = datetime.fromtimestamp( + os.stat(path).st_mtime) + if scope_begin > file_end: + sys.stderr.write("Jätan vahele %s faili kuna logifaili lõpp oli %s\n" % + (path, file_end)) + continue + + file_start = None + with gzip.open(path) if path.endswith(".gz") else open(path) as fh: + for line in fh: + m = re.match(RE_LOG_ENTRY, line) + if not m: + continue + dt = datetime.strptime(m.group("timestamp")[:-6], "%d/%b/%Y:%H:%M:%S") + if not file_start: # loeme esimest rida sellest failist + file_start = dt + if scope_end < file_start: + sys.stderr.write("Jätan vahele %s faili kuna logifaili algus oli %s\n" % (path, file_start)) + # Hüppa ridade lugemise tsükklist välja, järgmise faili juurde + break + else: + sys.stderr.write("Otsin logikirjeid failist %s\n" % path) + if dt > scope_end: + break + if dt > scope_begin: + print line.strip() diff --git a/bin/top2 b/bin/top2 new file mode 100755 index 0000000..bc0872d --- /dev/null +++ b/bin/top2 @@ -0,0 +1,11 @@ +#!/bin/bash + +# Käivitamiseks: cat access.log | python top +echo "Enim külastatud URL-id:" +cat \ + | awk -F \" '{ print $2}' \ + | cut -d ' ' -f 2- \ + | sort \ + | uniq -c \ + | sort -n -r \ + | head diff --git a/bin/top4 b/bin/top4 new file mode 100755 index 0000000..9ad4d10 --- /dev/null +++ b/bin/top4 @@ -0,0 +1,68 @@ +#!/usr/bin/python +# encoding: utf-8 + +""" +Skript: + + PATH=path/to/bin:$PATH + cat access.log | top4 | head + +""" + +# Loo tühi dict tüüpi objekt, siia korjame kokku URL -> mitu korda külastati vastendused +hits = {} + +try: + import sys + filename = sys.argv[1] + if filename.endswith(".gz"): + import gzip + stream = gzip.open(filename) + else: + stream = open(filename) +except IndexError: + stream = sys.stdin + sys.stderr.write("Loen standardsisendist...\n") + +# Käi ridahaaval fail läbi +for line in stream: + + # Kui rea sees ei esine sõnet GET siis jäta vahele + if "GET" not in line: + # Hüppa järgmise tsükli algusse + continue + + # Rea sees oli GET, nüüd võime proovida lõpikuda rida tühikute järgi massiiviks + fields = line.split() + + # Massiivi indeksid algavad nullist, seitsmenda tulba indeks on 6 + path = fields[6] + + # Kontrollime kas URL on juba võtmena kasutuses dict objektis + if path in hits: + # Kui on siis lisame ühe juurde + hits[path] = hits[path] + 1 + else: + # Kui ei ole siis määra väärtuseks 1 + hits[path] = 1 + +# Kuna dict tüüpi objekti ei saa sorteerida tuleb ta kõigepealt viia sorteeritavale kujule +# nagu nt list: +hits = hits.items() + +# Nüüd hits on massiiv kahestest massiividest (path, count) +# Sellise asja sorteerimiseks saab kasutada list objekti funktsiooni sort +# Sorteerimisel on vaja ette anda ka funktsioon mis nopib välja asja mille järgi sorteerida +# See on siin argument nimega key, millele on väärtuseks antud +# nimetu (anonüümne, lambda) funktsioon mis massiivi iga elemendi (path, count) +# kohta tagastab count negatiivse väärtuse +hits.sort(key=lambda (path,count):-count) + +# Massiiv on nüüd sorteeritud, esimese 10 vaste kuvamiseks saame massiviist võtta alammassiivi [:10] +# Süntaks [algus:lõpp] kehtib samamoodi nii massiividel (list, tuple) kui ka sõnedel +# Kui algus jäetakse vahele asendatakse see algusega +# Kui lõpp jäetakse vahele asendatakse see lõpuga +# Indeksid võivad negatiivsed olla +for path, count in hits: + print "% 9d %s" % (count, path) + diff --git a/bin/top5 b/bin/top5 new file mode 100755 index 0000000..a76fc4a --- /dev/null +++ b/bin/top5 @@ -0,0 +1,51 @@ +#!/usr/bin/python +# encoding: utf-8 + +import argparse +import re +from datetime import datetime, timedelta +import sys +from collections import Counter + +hits = Counter() # IP-d kust tuldi +urls = Counter() # URL-id mida külastati +agents = Counter() # User agent mida kasutati külastamisel + +RE_LOG_ENTRY = "(?P.+?) - (?P.+?) \[(?P.+?) \+\d\d\d\d\] \"(?P[A-Z]+) (?P.+) HTTP/1.[01]\" (?P\d+) (?P\d+) \"(?P.+?)\" \"(?P.+?)\"" + +for line in sys.stdin: + m = re.match(RE_LOG_ENTRY, line) + if not m: + sys.stderr.write("Ei suutnud parsida rida: %s" % line) + continue + + # Ignoreeri localhostist ja sisevõrgust pärinevaid päringuid (a'la nagios) + if m.group("remote_addr").startswith("127.") or m.group("remote_addr").startswith("192.168."): + continue + + # Jäta vahele OPTIONS päringud + if m.group("verb") not in ("GET", "POST"): + continue + + # Jäta vahele botid ja otsingumootorid + if re.search("(crawler|spider|Nuhk|Googlebot|yahoo|yandex)", m.group("agent")): + continue + + dt = datetime.strptime(m.group("timestamp"), "%d/%b/%Y:%H:%M:%S") + hits[m.group("remote_addr")] += 1 + urls[m.group("path")] += 1 + agents[m.group("agent")] += 1 + + +print "Top 5 enim külastatud URL-i veebiserveris:" +for path, count in urls.most_common(5): + sys.stdout.write("% 9d %s\n" % (count, path)) +print +print "Top 5 enim külastusi teinud IP aadressid:" +for remote_addr, count in hits.most_common(5): + sys.stdout.write("% 9d %s\n" % (count, remote_addr)) +print +print "Top 5 enim kasutatud veebilehitsejad/OS-id:" +for user_agent, count in agents.most_common(5): + sys.stdout.write("% 9d %s\n" % (count, user_agent)) + diff --git a/raport/README.md b/raport/README.md index 62a74d2..4182a72 100644 --- a/raport/README.md +++ b/raport/README.md @@ -1,7 +1,42 @@ # Apache logide parsija -Komplektne näide kuidas Apache2 logisid parsida ning raporteerida huvitavamad killud: +Failis ```main.py``` on näide kuidas Apache2 logisid parsida ning raporteerida huvitavamad killud: * Parsib Apache logifaili kirjed ApacheLogParser klassi abil mis on kirjeldatud failis log_parsers.py -* Värvib kaardi faili BlankMap-World6.svg ning salvestab top.svg faili sisse +* Laadib ```requests``` mooduli abil alla BlankMap-World6.svg, värvib selle ära ```lxml``` mooduli abil ning salvestab top.svg faili sisse * Genereerib Bootstrap baasil koostatud veebilehe mallist template.html faili raport.html + +Sõltuvuste paigladamiseks: + +```bash +apt install -y python-jinja2 python-lxml python-pygal python-geoip python-numpy python-matplotlib +dnf install -y python-jinja2 python-lxml python-pygal python2-GeoIP python2-numpy python2-matplotlib +``` + +Käivitamiseks + +```bash +(cat /path/to/access.log; zcat /path/to/access.log.1.gz) | python main.py +(cat /path/to/access.log; zcat /path/to/access.log.[12].gz) | python main.py +(cat /path/to/access.log; zcat /path/to/access.log.[1-5].gz) | python main.py +(cat /path/to/access.log; zcat /path/to/access.log.*.gz) | python main.py +``` + +# Java rakenduste stack trace'de kokku korjaja + +Failis ```main2.py``` on näide kuidas Java rakenduse logikirjetest stack trace'd kokku koguda: + +* Ühisosa Apache logide parsijaga on ```GenericParser``` klass milles sisaldub üldine logide parsimise loogika + +Sõltuvuste paigladamiseks: + +```bash +apt install -y python-jinja2 +dnf install -y python-jinja2 +``` + +Käivitamiseks: + +```bash +cat blah.log | python main2.py > raport2.html +``` diff --git a/raport/log_parsers.py b/raport/log_parsers.py index b1f6a72..b5c3cf4 100644 --- a/raport/log_parsers.py +++ b/raport/log_parsers.py @@ -15,37 +15,53 @@ class GenericParser(object): line_count = 0 event_count = 0 for line in self.fh: - byte_count += len(line) # loenda baite + # Loenda logikirjete arv ja maht + byte_count += len(line) line_count += 1 - if not line.strip(): # jäta vahele tühjad read + # Normaliseeri reavahetused + line = line.replace("\r\n", "\n") + + # Jäta vahele tühjad read + if not line.strip(): continue + + # Püüa regulaaravaldise järgi rida tükkideks võtta m = re.match(self.RE_LOG_ENTRY, line) + + # Kui rida klappis regexiga if m: + # Väljasta eelmine kokku kleebitud logikirje if log_entry: - if self.errors_only and not self.is_serious(log_entry): - continue - stack_trace = "\n".join(multiline_message.split("\n")[1:]) - event_count += 1 - row = \ - datetime.strptime(log_entry.get("timestamp"), self.TIMESTAMP_FORMAT), \ - log_entry, stack_trace, byte_count, line_count, event_count - # See teeb funktsioonist generaatori/iteraatori - yield row - multiline_message = line + # Kui vaja filtreerida, välista logikirjed mis ei ole veateatega seotud + if not self.errors_only or self.is_serious(log_entry): + event_count += 1 + + # yield teeb funktsioonist generaatori/iteraatori + # https://pythontips.com/2013/09/29/the-python-yield-keyword-explained/ + yield datetime.strptime(log_entry.get("timestamp"), self.TIMESTAMP_FORMAT), \ + log_entry, multiline_message, byte_count, line_count, event_count + + + # Alusta järgmise sõnumi kokku kleepimist + multiline_message = m.group("message") log_entry = m.groupdict() - else: + + elif line.startswith("\t") or line.startswith("Caused by") or line.startswith("org."): multiline_message += line + else: + sys.stderr.write("Ei suutnud parsida rida:" + line) + class JavaLogParser(GenericParser): TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%f" - RE_LOG_ENTRY = "(?P.+?) (?P[A-Z]+) 1 --- \[(?P.+)\](?P.+) +: (?P.+)" + RE_LOG_ENTRY = "(?P.+?) +(?P[A-Z]+) 1 --- \[(?P.+)\](?P.+?) *: (?P.+)" def is_serious(self, log_entry): return log_entry.get("severity") == "ERROR" class ApacheLogParser(GenericParser): - RE_LOG_ENTRY = "(?P.+?) - (?P.+?) \[(?P.+?) \+\d\d\d\d\] \"(?P[A-Z]+) (?P.+) HTTP/1.[01]\" (?P\d+) (?P\d+) \"(?P.+?)\" \"(?P.+?)\"" + RE_LOG_ENTRY = "(?P.+?) - (?P.+?) \[(?P.+?) \+\d\d\d\d\] \"(?P(?P[A-Z]+) (?P.+) HTTP/1.[01])\" (?P\d+) (?P\d+) \"(?P.+?)\" \"(?P.+?)\"" TIMESTAMP_FORMAT = "%d/%b/%Y:%H:%M:%S" def is_serious(self, log_entry): diff --git a/raport/main.py b/raport/main.py index 1355158..0a6f914 100644 --- a/raport/main.py +++ b/raport/main.py @@ -1,21 +1,6 @@ #!/usr/bin/python # encoding: utf-8 -""" -Sõltuvuste paigladamiseks: - - apt install -y python-jinja2 python-lxml python-pygal python-geoip python-numpy python-matplotlib - dnf install -y python-jinja2 python-lxml python-pygal python2-GeoIP python2-numpy python2-matplotlib - -Käivitamiseks - - (cat /path/to/access.log; zcat /path/to/access.log.1.gz) | python main.py - (cat /path/to/access.log; zcat /path/to/access.log.[12].gz) | python main.py - (cat /path/to/access.log; zcat /path/to/access.log.[1-5].gz) | python main.py - (cat /path/to/access.log; zcat /path/to/access.log.*.gz) | python main.py - -""" - import GeoIP import re import sys @@ -42,7 +27,8 @@ for timestamp, log_entry, stack_trace, byte_count, line_count, event_count in Ap if int(log_entry.get("status")) < 400: # 2xx ja 3xx arvestamiseks hits_per_path[log_entry.get("path")] += 1 hits_per_remote_addr[log_entry.get("remote_addr")] += 1 - hits_per_user_agent[log_entry.get("user_agent")] += 1 + if "bot" not in log_entry.get("user_agent").lower(): + hits_per_user_agent[log_entry.get("user_agent")] += 1 hits_per_country[country_code] += 1 hits_per_date[timestamp.date()] += 1 bytes_per_date[timestamp.date()] += int(log_entry.get("size")) @@ -64,13 +50,26 @@ buf = requests.get("https://upload.wikimedia.org/wikipedia/commons/0/03/BlankMap # Parsi XML puu map_document = etree.fromstring(buf) + +# Iga riigikoodi ja sellest riigist pärit päringute arvu kohta for country, count in hits_per_country.items(): if not country: # Mõni IP ei pruukinud laheneda riigikoodiks (sisevõrk jms) continue + + # Interpoleeri päringute arv vahemikust 0 ... maksimaalsete päringutega riik + # vahemikku 180 (sinakas toon) ... 0 (punane) + # võrdväärne rida: hue = 180 - 180 * count / max(hits_per_country.values()) hue = interp(count, [0, max(hits_per_country.values())], [180, 0]) + + # Nopi rekursiivselt dokumendist välja kõik elemendid mille 'id' attribuut on riigikoodiga + # Kaardis vastab sellele küll ainult üks ehk grupi element for element in map_document.xpath("//*[@id='%s']" % country.lower()): + # Lisa CSS-i stiili attribuut elemendile taustavärvi muutmiseks element.set("style", "fill:hsl(%.2f, 60%%, 60%%)" % hue) + + # Polügonid mis on grupi sees, neilt eemalda attribuut 'class' mille + # abil pannakse vaikimisi hall värv külge for subelement in element: subelement.attrib.pop("class", "") diff --git a/raport/main2.py b/raport/main2.py new file mode 100644 index 0000000..cac9f8f --- /dev/null +++ b/raport/main2.py @@ -0,0 +1,51 @@ +#!/usr/bin/python +# encoding: utf-8 + +import GeoIP +import re +import sys +from collections import Counter +from log_parsers import JavaLogParser + +sys.stderr.write("Loen standardsisendist...\n") +errors = Counter() +for timestamp, log_entry, stack_trace, byte_count, line_count, event_count in JavaLogParser(sys.stdin, errors_only=True): + errors[stack_trace] += 1 + +from jinja2 import Template +import codecs + +HTML_TEMPLATE = u""" + + + Apache logide raport + + + + + + +
+ {% for stack_trace, count in errors.items() %} +
+
+ {{ count }} korda esinenud viga + +
+
+
+
+
+
{{ stack_trace }}
+
+
+
+ {% endfor %} +
+ + +""" + +template = Template(HTML_TEMPLATE) +print template.render(locals())