Организация регулярного дайджеста логов с помощью python и ansible на примере asterisk

от автора

Когда я создавал данный инструмент, я не был знаком с logwatch. Мне захотелось видеть ситуацию с логами на своих серверах в целом, и, так я сделал этот велосипед. Думаю, что данный механизм может помочь новичкам в понимании альтернативных возможностей ansible.

Используемые программные продукты:

  • python 2.7.14
  • ansible 2.3
  • сервера asterisk на базе FreePBX 13

Механизм состоит из двух частей – python скрипт, который занимается обработкой лог файла и отправкой отчета на почту, и плейбука для сбора логов с серверов и передачи их скрипту для обработки.

Сам плейбук:

--- - name: parseastlogs   hosts: production_asterisks   vars:     date: "{{ lookup('pipe', 'date +%Y%m%d') }}"     ipaddr: "{{ ansible_default_ipv4.address }}"   tasks:    - debug: var=date    - debug: var=ipaddr    - fetch:	        src: /var/log/asterisk/full-{{ date }}       dest: /tmp/full-{{ date }}-{{ ipaddr }}       flat: yes     - local_action: "shell /etc/ansible/localscripts/astReporter.py {{ ipaddr }} full-{{ date }}-{{ ipaddr }}"  В плейбуке задаем переменные, ответственные за имя файла и забираем лог файлы с сервера модулем fetch. Так как у нас будут уникальные файлы - используем параметр flat, чтобы избежать длинных путей к лог файлам.  

Скрипт:

#!/usr/bin/python2.7 import re from collections import Counter import yagmail import sys import datetime import os  servername=sys.argv[1] yag=yagmail.SMTP(user='ansible@2.1',password=None,host='192.168.2.1',port='25',smtp_starttls=False,smtp_set_debuglevel=0,smtp_skip_login=True) recipients=['user@mydomain.local']   filename=sys.argv[2] workdir='/tmp/' log_file=open(workdir+filename,'r')#sys.argv[1] loglist=list() verbosity=['NOTICE','ERROR','WARNING'] regexes = ["Call from '.*' .* to extension '.*' rejected because extension not found in context '.*'.", "Identifier \d+, identifier_type \d+ not found in identifier list", "Invalid result identifier \d+ passed in aMYSQL_clear", "This function can only be used on SIP channels.", "fwrite() returned error: Broken pipe", "CDR requires a value \(CDR\(variable\)=value\)", "Received SIP subscribe for peer without mailbox: .*", "Removed interface '.*' from queue '.*'", "Peer '.*' is trying to register, but not configured as host=dynamic", "Registration from '.*' failed for '.*' - Peer is not supposed to register", "Unable to join queue '.*'", "Attempt to pause interface Local/@from-queue/n, not found", "PRESENCE_STATE unknown", "EXTENSION_STATE requires an extension", "Prodding channel '.*' failed", "Channel '.*' sent to invalid extension but no invalid handler: context,exten,priority=.*", "Can't send 10 type frames with PJSIP", "Attempt to pause interface .+, not found", "Attempt to unpause interface .+, not found", "no samples for ulawtolin", "Could not find matching INVITE transaction for CANCEL request", "Peer '.*' is now Reachable. \(.*\)", "Peer '.*' is now UNREACHABLE!  Last qualify: .*", "Registration from .* failed for '.*' - Wrong password", "Retransmission timeout reached on transmission .*", "no samples for alawtolin", "Peer '.*' is now Lagged. \(\d+ms / \d+ms\)", "Call completed to .*", "Invalid retrytime at line \d+ of .*", "Not accepting call completion offers from call-forward recipient .*", "No such context '.*' for macro '.*'\. Was called by .*", "[pP]ickup .* attempt by .*", "Call failed to go through, reason \(5\) Remote end is Busy", "Deprecated syntax found\..* ", "No digits dialed for atxfer.", "Unable to create channel of type 'SIP' \(cause \d+ - Subscriber absent\)", "'tls' is not a valid transport type when tlsenable=no\. If no other is specified, the defaults from general will be used\.", "'tcp' is not a valid transport type when tcpenable=no\. If no other is specified, the defaults from general will be used\.", "Queued call to .* expired without completion after \d+ attempts", "Re-invite to non-existing call leg on other UA. SIP dialog .*\. Giving up.", "Channel .* not found!  Variable 'BLKVM' not set to .*\.", "Remote host can't match request CANCEL to call .*\. Giving up\.", "Unable to execute query \[.*\]", "SQL Exec Direct failed \(-1\)!\[", "SQL Execute returned an error .*", "   -- Re-registration for .*", "Outbound Registration\: Expiry for .* is \d+ sec \(Scheduling reregistration in \d+ s\)", "Correct auth, but based on stale nonce received from '.*'", "Unable to write frametype: 2", "Received response: \"Forbidden\" from '\".*\" .*'", "Huh\?  Not an RDNIS SIP header .*", "Hanging up call .* - no reply to our critical packet .*", "Cancelling retransmit of OPTIONs \(call id .*\)  ", "Still have a QUALIFY dialog active, deleting", "The use of '_\.' for an extension is strongly discouraged and can have unexpected behavior.  Please use '_X\.' instead at line .*", "Context '.*' tries to include nonexistent context '(.*)'", "aMYSQL_query: mysql_query failed\. Error: Duplicate entry .*", "RTCP SR transmission error to .*, rtcp halted Operation not permitted", "Failed to write frame to '.*': Resource temporarily unavailable", "Unable to forward frametype: 2", "Timeout on .* on non-critical invite transaction.", "Unexpected control subclass '\d+'", "Context '.*' for macro '.*' lacks .*", "No response received from '.*' on registration attempt to '.*', retrying in '\d+'", "Unknown RTP codec 90 received from '.*'", "Invalid extension '.*', but no rule 'i' or 'e' in context '.*'", "Added interface '.*' to queue '.*'", "Exceptionally long voice queue length queuing to .*", "Request from '.*' failed for '.*' \(callid: .*\) - No matching endpoint found", "Junk at the beginning of frame \d+", "Unable to register extension at line .*", "Unable to register extension '.*' priority .* in '.*', already in use", "Unable to load config file .*", 'Purely numeric hostname \(\d+\), and not a peer--rejecting!', "Unknown directive .* at line \d+ .*", "Context '.*' already included in '.*' context on include at line \d+ of .*", "No closing parenthesis found\? '.*' at line \d+ of .*", "Can't use '.*' priority on the first entry at line \d+ of .*", "The .* options are deprecated. Please see UPGRADE.txt for information", "Can't use '.*' priority on the first entry at line \d+ of .*", "Call failed to go through, reason .*", "Unable to open .*", "Playback of message .* failed", "File .* does not exist in any format", "Playback failed on .* for .*", "Adding .* to .*", "Can't move channel. One or both is dead .*", "Unable to complete call pickup of .*", "Pickup .* failed by .*", "No entry in voicemail config file for .*", "   \-\- Registration for '.*' timed out, trying again \(Attempt #[0-9]+\)", "Disconnecting call .* for lack of RTP activity in [0-9]+ seconds", "Failed to initialize .* declining image stream", "Can't send 10 type frames with SIP write", "Set requires an '=' to be a valid assignment.", "Timeout but no rule 't' or 'e' in context .*", "RTCP RR transmission error to .*, rtcp halted Operation not permitted", "Unable to acquire target extension for attended transfer.", "Unterminated comment detected beginning on line"] notmatched=list() matchedregex=list()  combinedre="("+")|(".join(regexes) + ")"  # данный класс сделан для удобной работы со строками из лог файла. Функционал несколько избыточен и не все поля используются. class logitem:     ldatetime=None     ltype=None     lchan=None     lsource=None     lcontent=None     def __init__(self,ldate,ltype,lch,lsrc,lcont):         self.ldatetime=ldate         self.ltype=ltype         self.lchan=lch         self.lsource=lsrc         self.lcontent=lcont   # данная функция осуществляет работу с лог файлом def main():      i=0     dumpchanstart=False      for line in log_file: #игнорируем сообщения отладки Dumpchan     	if 'app_dumpchan.c' in line:             dumpchanstart = 0             for line in log_file:                 if dumpchanstart==2:                     break                  if "================================================================================" in line:                     dumpchanstart+=1 #обрабатываем строки со стандартными сообщениями, которые начинаются с [ 	if line[0]=="[":           buf=re.split('] ', line) # обрабатываем строки с заданной verbosity           if any(x in buf[1] for x in verbosity):                 buf1spl=buf[1].split('-')                 channum=''                 buf2spl=re.split('c: ',buf[2])                 if len(buf1spl)>1:                     channum=buf1spl[1]                 itemcheck=logitem(buf[0][1:],buf[1].split('[')[0],channum,buf2spl[0],buf2spl[1])                 filtration(itemcheck.lcontent)  #функция распределяет получившийся объект в список matchedregex, если он попал под шаблон и в notmatched, если нет.	 def filtration(logitem):      found = False     for regexitem in regexes:          if  re.match(regexitem,logitem):             found= True             matchedregex.append(regexitem)             continue      if found==False:         notmatched.append(logitem)   #функция генерации основного тела письма. В блоке if..elif производится цветовое выделение важных сообщений def genContents():     content=u"<html>"     content+=u"<head></head>"     content+=u"<body>"     content+=u"<h1>Log digest for " +servername+ u"</h1>"     content+=u"<table  style='width: 60%;' border = \"1\" cellpadding = \"1\"'>"     for key, val in Counter(matchedregex).most_common():         if u"rejected because extension not found in context" in key:             key=u"<b style='background-color:#e8edff'>"+key+u"</b>"         elif u"No matching endpoint found" in key:             key=u"<b style='background-color:#fd6161'>"+key+u"</b>"         elif u"No closing parenthesis " in key:             key = u"<b style='background-color:#98862a'>" + key + u"</b>"         elif u"Wrong password" in key:             key=u"<b style='background-color:#fd6161'>"+key+u"</b>"             elif u"Unterminated comment detected beginning on line" in key:             key=u"<b style='background-color:#fd6161'>"+key+u"</b>"             content += u"<tr>"         content+= u"<td>"+key+u"</td><td>" +str(val) +u"</td>"         content += u"</tr>"     content+=u"</table><br>"     content+=u"<h1>not matched logitems:</h1><br>"     for item in notmatched:         content+=item     content+=u"</body>\n"     content+=u"</html>\n"     return  content  main() cont=genContents() yag.send(recipients,subject='Log digest for '+servername,contents=cont) os.remove(workdiir+filename) 

запуск скрипта осуществляется командой: /usr/local/bin/ansible-playbook /etc/ansible/playbooks/parseastlogs.yml

Результатом выполнения команды будет некоторое количество писем(по одному письму на каждый сервер из группы production_asterisk) с примерно следующим содержанием:

image

Если есть вопросы или предложения, готов на них ответить.


ссылка на оригинал статьи https://habr.com/post/419547/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *