GrabDuck

Самописная система мониторинга IPTV | личный блог Кушманова Евгения aka IbZ

:

В этой статье я приведу пример самописной системы мониторинга IPTV каналов которой я пользуюсь в своей работе (и надо сказать вполне успешно). Моя система позволяет определять ошибки в потоках каналов и исправлять их самостоятельно без вручного вмешательства, ну и разумеется собирает статистику, которую потом можно анализировать и делать вполне определенные выводы. Например, я смог сделать выводы какие CAM-модули являются наиболее стабильными, какие ресиверы стабильнее работают с теми или иными спутниками или CAM-модулями

Кстати у меня еще есть система сбора статистики зрительского интереса к телепрограммам и я возможно опишу скоро и ее.

Кстати в статье про IPTV мониторинг с помощью ардуины и светодиодной матрицы - разумеется сам мониторинг осуществляла не ардуина, а вот данная система мониторинга котору я сейчас опишу

Идея этого мониторинга очень проста.

Раз в полчаса (для некоторых особо важных каналов, чаще, раз в 10 минут) по крону запускается скрипт, который делает следующее - запускает vlc на несколько секунд на определенный iptv канал, с записью куска эфира в файл с логгированием, затем останавливает запись, делает простейший анализ логов, накапливает информацию об ошибках (если они есть) и в конце принимает решение, если на ресивере более половины каналов идут с ошибками - нужно ли передергивать ресивер или нет. Откуда взято время - полчаса? У меня проверяется порядка 170 каналов, на каждый канал уходит примерно 8 секунд, т.е. на все каналы уходит 1360 секунд, или около 22-23 минут. Поэтому взято время полчаса чтобы проверки не накладывались друг на друга. Так же это время взято потому что если дергается ресивер - то дергаются все каналы с ресивера, а ведь часть каналов может быть и рабочими, идти без проблем.

Из этой системы я приведу только часть кода, которая может оказаться кому-то полезной и раскроет основные моменты. Весь код слишком специфичный, и заточен под мои конкретные нужды.

Для начала конфиг. Пусть он будет в виде файла mc_iptv.config

1
2
3
4
5
6
7
8
9
10
...
239.250.1.1     243   0  0    РБК
239.250.1.2     247   1  0    Discovery Channel
239.250.1.3     250   0  0    2*2
239.250.1.4     250   1  0    EuroSport
...
239.250.3.12    205   1  1954 TV1000 Action
...
239.250.7.2     212   1  1654,1655    myZen HD
...

Первый столбец адрес мультикаст группы, второй - номер ресивера (я сделал еще и так чтобы он совпадал с последним октетом ип адреса менеджмента ресивера, т.е. если ресивер 250 - то его менеджмент ип = 192.168.0.250. Второй столбец - это кодированный канал (1), или FTA (0), четвертый столбец - это пиды ES которые надо игнорировать (понятнее станет для чего это нужно по ходу изложения) и затем идет название канала

Следующий компонент системы - скрипт test.sh - которому скармливаются параметры из конфига, он читает и проверяет мультикаст поток, и затем возвращает 0 (все хорошо) или 1 (все плохо) в зависимости от найденных/ненайденных ошибок. Попутно он скидывает сообщение в центральный сислог сервер нашей сети если канал закодирован или нет потока, логгирует состояние и вносит запись в базу данных.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/bin/sh
<br />
arg1=$1
arg2=$2
arg3=$3
arg4=$4
 
test_dir=/home/ibz/iptv.watchdog
test_filename=test_file
test_file=$test_dir/$test_filename
log_filename=vlc_watchdog.log
log_file=$test_dir/$log_filename
log_file2=$test_dir/watchdogger.log
syslog_ip=192.168.0.200  # ип сервера сислогов куда централизовано скидываются сообщения со всей сети
ip_for_syslog=192.168.0.1 # наш ип для сервера сислогов
 
rm $test_file
rm $log_file
vlc -d -vvv udp://@$arg1:1234 --file-logging --logfile $log_file --sout='#duplicate{dst=std{access=file,mux=raw,dst="'"$test_file"'"}}'
sleep 7
kill `ps axw|grep $test_filename|grep "access=file"|grep -v grep|gawk '{print $1}'`
 
if [ -f $test_file ]; then
  filesize=`du --bytes $test_file|gawk '{print $1}'`
  if [ $filesize -lt 10000 ]; then
    if cat $log_file | grep "ts warning: invalid header"
      then
        echo "`date`: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" >>$log_file2
        mv -f $log_file "$test_dir/vlc.logs/$arg4" #на всякий случай копируем лог-файл от vlc отдельно
        /usr/bin/mysql -ptv_pass -utv_user tv_db -e "update chan_stat set last_state=1, last_check_date=Now() where ip='$arg1';"
        echo "`date`: SCRAMBLED" >> /home/ibz/iptv.watchdog/logs/$arg1.log
        echo "<8>`date '+%b %e %k:%M:%S'` $ip_for_syslog IPTV: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" | nc -i 2 -w 2 -q 2 -u $syslog_ip 514
      else
        echo "`date`: Umer $arg4 -> $arg2 ( $arg1:1234 )!!!" >>$log_file2
        mv -f $log_file "$test_dir/vlc.logs/$arg4"
        /usr/bin/mysql -ptv_pass -utv_user tv_db -e "update chan_stat set last_state=2, last_check_date=Now() where ip='$arg1';"
        echo "`date`: NO STREAM" >> /home/ibz/iptv.watchdog/logs/$arg1.log
        echo "<8>`date '+%b %e %k:%M:%S'` $ip_for_syslog IPTV: NO STREAM $arg4 -> $arg2 ( $arg1:1234 )!!!" | nc -i 2 -w 2 -q 2 -u $syslog_ip 514
    fi
    exit 1
  else
    if [ $arg3 != "0" ]; then
      echo "test pids: $arg3"
      for pid in `echo "$arg3" | awk -F"," '{ print $1" "$2" "$3" "$4" "$5 }'`; do
        echo "ignore pid: $pid"
        cat $log_file | grep "ts warning: invalid header" | grep -v "(pid: $pid)" > $log_file."_".$pid
        rm $log_file
        mv $log_file."_".$pid $log_file
      done
      if cat $log_file | grep "ts warning: invalid header"
        then
          echo "`date`: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" >>$log_file2
          mv -f $log_file "$test_dir/vlc.logs/$arg4"
          /usr/bin/mysql -ptv_pass -utv_user tv_db -e "update chan_stat set last_state=1, last_check_date=Now() where ip='$arg1';"
          echo "`date`: SCRAMBLED" >> /home/ibz/iptv.watchdog/logs/$arg1.log
          echo "<8>`date '+%b %e %k:%M:%S'` $ip_for_syslog IPTV: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" | nc -i 2 -w 2 -q 2 -u $syslog_ip 514
          exit 1
      fi
    else
      if cat $log_file | grep "ts warning: invalid header"
        then
          echo "`date`: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" >>$log_file2
          mv -f $log_file "$test_dir/vlc.logs/$arg4"
          /usr/bin/mysql -ptv_pass -utv_user tv_db -e "update chan_stat set last_state=1, last_check_date=Now() where ip='$arg1';"
          echo "`date`: SCRAMBLED" >> /home/ibz/iptv.watchdog/logs/$arg1.log
          echo "<8>`date '+%b %e %k:%M:%S'` $ip_for_syslog IPTV: Scrambled $arg4 -> $arg2 ( $arg1:1234 )!!!" | nc -i 2 -w 2 -q 2 -u $syslog_ip 514
          exit 1
      fi
    fi
  fi
fi
 
/usr/bin/mysql -ptv_pass -utv_user tv_db -e "update chan_stat set last_state=0, last_check_date=Now(), prev_state=0, sms_sended=0, first_failed=NULL where ip='$arg1';"
echo "`date`: clear" >> /home/ibz/iptv.watchdog/logs/$arg1.log
 
sleep 1
exit 0

Когда поток идет скремблированный то vlc пишет в логах нечто подобное

1
2
3
4
5
ts warning: invalid header [0x1:d3:86:e3] (pid: 330)
ts warning: invalid header [0x13:29:1f:de] (pid: 330)
ts warning: invalid header [0xd1:86:f9:18] (pid: 330)
ts warning: invalid header [0x13:40:20:e] (pid: 330)
ts warning: invalid header [0x99:9f:d8:99] (pid: 410)

где в скобочках как раз pid ошибочного es. Но не всегда это признак того что целиком с каналом все плохо. Бывает просто что текущая подписка на карте доступа не открывает дорожку субтитров, или аудиодорожку определенного языка. Именно для этого и исключаем для некоторых каналов эти es. Следующий компонент - php-скрипт, консольный, watchdog_iptv.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/php
<br />
<?php 
  $arduino_is_live = true;
  $arduino_channels = array('239.250.0.3', // Культура
                            '239.250.0.4', // НТВ
                            '239.250.0.5', // Питер 5 Канал
                           ...   // чтобы сократить код, здесь я убрал перечисление каналов
                            '239.250.7.8', // Женский Мир HD
                            '239.250.7.9', // HD Life
                            '239.250.8.1'); // Наш Футбол
 
  mysql_connect("localhost","tv_user","tv_pass") or die(mysql_error());
  mysql_select_db("tv_db");
 
  $total_failed = 0;
  $fh = fopen("/home/ibz/iptv.watchdog/mc_iptv.config","r");
  $i=0;
  if($fh)
  {
     while (!feof($fh))
     {
       $buffer = fgets($fh, 4096);
       $buffer = trim($buffer);
       list($arg1, $arg2, $fta, $arg3, $arg4) = split("[ ]+",$buffer,5);
       if($fta==1)
       {
         if( isset($cnt_prog[$arg2]) )
         {
           $cnt_prog[$arg2]++;
         }
         else
         {
           $cnt_prog[$arg2]=1;
         }
       }
       if($arg2!="0")
       {
         echo "/home/ibz/iptv.watchdog/test.sh $arg1 $arg2 $arg3 $arg4\n";
// здесь начинается блок для моей arduino. Выбираются все сбойнувшие каналы, добавляется текущий проверяемый канал, потом все кодируется и отправляются на ethernet shield
         if($arduino_is_live)
         {
           $sql = "select ip from chan_stat where last_state<>0 ";
           $result=mysql_query($sql);
 
           $arduino_msg = "IbZ ";
           if(mysql_num_rows($result)>0)
           {
             while($row=mysql_fetch_array($result))
             {
               $arduino_ip = $row["ip"];
               if($arduino_idx = array_search($arduino_ip,$arduino_channels))
               {
                 $arduino_msg .= pack("C*", $arduino_idx+1);
               }
             }
           }
           if($arduino_idx = array_search($arg1,$arduino_channels))
           {
             $arduino_msg .= pack("C*", $arduino_idx+1);
           }
           $arduino_msg .= pack("C*", 0);
 
           $fp=fsockopen("tcp://10.121.17.220", 3333, $errno, $errstr, 3);
           if($fp)
           {
             fwrite($fp, $arduino_msg);
             fclose($fp);
           }
           else
           {
             $arduino_is_live = false;
           }
         }
// оканчание блока для ардуины
         system("/home/ibz/iptv.watchdog/test.sh \"$arg1\" \"$arg2\" \"$arg3\" \"$arg4\"",$rc);
         print "retcode = $rc\n\n";
         if($rc==1)
         {
            $total_failed++;
            if( isset($cnt_fail[$arg2]) )
            {
              $cnt_fail[$arg2]++;
            }
            else
            {
              $cnt_fail[$arg2]=1;
            }
         }
       }
     }
  };
  fclose($fh);
 
 
  $fh = fopen("/home/ibz/iptv.watchdog/watchdogger.log","a");
  foreach($cnt_fail as $rec_id => $failed)
  {
    fwrite($fh,"$failed of ".$cnt_prog[$rec_id]." failed on 192.168.0.$rec_id !!!\n");
    if($cnt_prog[$rec_id]==0)
    {
      if($failed>0)
      {
        if(($rec_id>=230)&&($rec_id<=242))
        {
          fwrite($fh,"need to reboot PBI 192.168.0.$rec_id !!!\n");
          system("echo \"\\nreboot PBI 192.168.0.$rec_id:\\n\" >> /home/ibz/iptv.watchdog/reboot.log");
          system("/usr/bin/snmpset -v1 -c private 192.168.0.$rec_id 1.3.6.1.4.1.1070.3.1.1.23.0 i 1 2>>/home/ibz/iptv.watchdog/reboot.log 1>>/home/ibz/iptv.watchdog/reboot.log");
        }
        else
        {
          fwrite($fh,"need to reboot 192.168.0.$rec_id !!!\n");
          system("echo \"\\nreboot 192.168.0.$rec_id:\\n\" >> /home/ibz/iptv.watchdog/reboot.log");
          system("echo -e \"enable adi\\n\\n\\nCamRst\\nCamPmtRst\\nRESET_UNIT\\n\" | nc -i 2 -q 2 -w 2 192.168.0.$rec_id 23 2>>/home/ibz/iptv.watchdog/reboot.log 1>>/home/ibz/iptv.watchdog/reboot.log");
        }
      }
    }
    else if($failed/$cnt_prog[$rec_id]>=0.5)
    {
      if(($rec_id>=230)&&($rec_id<=242))
      {
        fwrite($fh,"need to reboot PBI 192.168.0.$rec_id !!!\n");
        system("echo \"\\nreboot PBI 192.168.0.$rec_id:\\n\" >> /home/ibz/iptv.watchdog/reboot.log");
        system("/usr/bin/snmpset -v1 -c private 192.168.0.$rec_id 1.3.6.1.4.1.1070.3.1.1.23.0 i 1 2>>/home/ibz/iptv.watchdog/reboot.log 1>>/home/ibz/iptv.watchdog/reboot.log");
      }
      else
      {
        fwrite($fh,"need to reboot 192.168.0.$rec_id !!!\n");
        system("echo \"\\nreboot 192.168.0.$rec_id:\\n\" >> /home/ibz/iptv.watchdog/reboot.log");
        system("echo -e \"enable adi\\n\\n\\nCamRst\\nCamPmtRst\\nRESET_UNIT\\n\" | nc -i 2 -q 2 -w 2 192.168.0.$rec_id 23 2>>/home/ibz/iptv.watchdog/reboot.log 1>>/home/ibz/iptv.watchdog/reboot.log");
      }
    }
  }
  fclose($fh);
?>

У меня с 230 по 242 - это ресиверы фирмы PBI, а остальные - ADI. В массиве $arduino_channels перечисляются каналы так как они расположены на плате со светодиодами. Соответственно в процессе работы скрипта все время горят светодиоды сбойнувших каналов и еще зажигается на время проверки тестируемый канал. Внимательный читатель скажет - а где блок который окончательно устанавливает состояние светодиодов на ардуине? Дело в том что у меня список каналов для проверки заведомо больший, и там есть каналы которые не перечислены в массиве - соответственно необязательно в конце еще раз посылать сообщение с неработающими каналами - оно и так пошлется.

И скрипт который запускается в кроне, который все и выполняет

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
<br />
dt_st=`date`
 
/home/ibz/iptv.watchdog/watchdog_iptv2.php >>/home/ibz/iptv.watchdog/watchdog_iptv.php.log #основной скрипт которы проверяет все каналы
/home/ibz/iptv.watchdog/send_sms.php #скрипт отправки смс при появлении новых сбойных каналов, его код приводить не буду
 
filesize=`du --bytes /home/ibz/iptv.watchdog/watchdogger.log | gawk '{print $1}'`
if [ $filesize -gt 1 ]; then
  echo $dt_st >/home/ibz/iptv.watchdog/watchdogger2.log
  cat /home/ibz/iptv.watchdog/watchdogger.log >> /home/ibz/iptv.watchdog/watchdogger2.log
  date >>/home/ibz/iptv.watchdog/watchdogger2.log
 
  cat /home/ibz/iptv.watchdog/watchdogger2.log | sendmail <a href="mailto:my@mail.com">my@mail.com</a>
  rm /home/ibz/iptv.watchdog/watchdogger.log
  rm /home/ibz/iptv.watchdog/watchdogger2.log
fi
 
/home/ibz/iptv.watchdog/get_avg_stat.pl # скрипт который подсчитывает разные параметры по кол-ву аварий и другую статистику. Его я приводить тоже не буду, скажу лишь что он складывает эту статистику по каналам в БД.

Далее рисуется вполне банальная веб-страничка, которая аккумулирует все собранные скриптами данные. У меня она выглядит так. Много пришлось заретушировать, извините, секрет :)

IPTV monitoring statistics