Простой монитор системы на Flask

    Привет, Хабр!

    Недавно возникла необходимость сделать простой и расширяемый монитор использования системы для сервера на Debian. Хотелось строить диаграммы и наблюдать в реальном времени использование памяти, дисков и тп. Нашел много готовых решений, но в итоге сделал скрипт на python + Flask + psutil. Получилось очень просто и функционально. Можно легко добавлять новые модули.




    UPDATE: статья исправлена и дополнена с учетом замечаний в комментариях.

    Сначала сделаем небольшой файл конфигурации для настройки.

    Несколько настроек для монитора
    # configuration for server monitor
    
    #general info
    version = 1.0
    
    # web server info
    server_name = "monitor"
    server_port = 10000
    server_host = "localhost"
    
    #monitoring
    time_step = 1 #s
    max_items_count = 100
    
    #display
    fig_hw = 3
    



    Напишем монитор, который будет по таймеру собирать нужные нам данные.
    В примере — свободное место на дисках и доступная память.

    import threading
    import time
    import psutil
    from conf import config as cfg
    import datetime
    
    mem_info = list()
    disk_usage = list()
    
    def timer_thread():
        while True:
            time.sleep(cfg.time_step)
            mi = psutil.virtual_memory()
            if mem_info.__len__() >= cfg.max_items_count:
                mem_info.pop(0)
            if disk_usage.__len__() >= cfg.max_items_count:
                disk_usage.pop(0)
            di = list()
            for dp in psutil.disk_partitions():
                try:
                    du = psutil.disk_usage(dp.mountpoint)
                except:
                    continue
                di.append(du.free / 1024 / 1024)
            mem_info.append([mi.available / 1024 / 1024])
            disk_usage.append(di)
    
    def start():
        t = threading.Thread(target=timer_thread,
                             name="Monitor",
                             args=(),
                             daemon=True)
        t.start()
    


    И саму модель, которая реализует модули. Сюда можно добавлять любой функционал, позже я добавил еще доступность сервисов в интранет (пинг).
    Для построения временных графиков берем данные из монитора (см выше).

    Отображение модулей на странице
    import matplotlib
    matplotlib.use('agg')
    import psutil, datetime
    import mpld3
    from jinja2 import Markup
    from conf import config as cfg
    import platform
    from matplotlib import pyplot as plt
    import numpy
    from lib import timemon
    from operator import itemgetter
    
    def get_blocks():
        blocks = list()
        get_mem_info(blocks)
        get_disks_usage(blocks)
        return blocks
    
    def get_mem_info(blocks):
        fig = plt.figure(figsize=(2 * cfg.fig_hw, cfg.fig_hw))
        plt.subplot(121)
        mem = psutil.virtual_memory()
        labels = ['Available', 'Used', 'Free']
        fracs = [mem.available, mem.used, mem.free]
        lines = list()
        lines.append(str.format('Avaliable memory: {0} MB',mem.available))
        lines.append(str.format('Used memory: {0} MB', mem.used))
        lines.append( str.format('Free memory: {0} MB', mem.free))
        if psutil.LINUX:
            labels = numpy.hstack((labels, ['Active', 'Inactive', 'Cached', 'Buffers', 'Shared']))
            fracs = numpy.hstack((fracs, [mem.active, mem.inactive, mem.cached, mem.buffers, mem.shared]))
            lines.append(str.format('Active memory: {0} MB', mem.active))
            lines.append(str.format('Inactive memory: {0} MB', mem.inactive))
            lines.append(str.format('Cached memory: {0} MB', mem.cached))
            lines.append(str.format('Buffers memory: {0} MB', mem.buffers))
            lines.append(str.format('Shared memory: {0} MB', mem.shared))
        plt.pie(fracs, labels=labels, shadow=True, autopct='%1.1f%%')
        plt.subplot(122)
        plt.plot(timemon.mem_info)
        plt.ylabel('MBs')
        plt.xlabel(str.format('Interval {0} s', cfg.time_step))
        plt.title('Avaliable memory')
        plt.tight_layout()
        graph = mpld3.fig_to_html(fig)
        blocks.append({
                'title': 'Memory info',
                'graph': Markup(graph),
                'data':
                    {
                        'primary' : str.format("Total memory: {0} MB", mem.total / 1024 / 1024),
                        'lines' : lines
                    }
            })
        print( blocks)
    
    def get_disks_usage(blocks):
        num = 0
        for dp in psutil.disk_partitions():
            fig = plt.figure(figsize=(2 * cfg.fig_hw, cfg.fig_hw))
            plt.subplot(121)
            try:
                di = psutil.disk_usage(dp.mountpoint)
            # gets error on Windows, just continue anyway
            except:
                continue
            labels = ['Free', 'Used', ]
            fracs = [di.free, di.used]
            plt.pie(fracs, labels=labels, shadow=True, autopct='%1.1f%%')
            plt.subplot(122)
            plt.plot(list(map(itemgetter(num), timemon.disk_usage)))
            plt.ylabel('MBs')
            plt.xlabel(str.format('Interval {0} s', cfg.time_step))
            plt.title('Disk available space')
            plt.tight_layout()
            graph = mpld3.fig_to_html(fig)
            blocks.append({
                'title': str.format('Disk {0} info', dp.mountpoint),
                'graph': Markup(graph),
                'data':
                    {
                        'primary': '',
                        'lines': [ str.format('Free memory: {0} MB', di.free / 1024 / 1024),
                                   str.format('Used memory: {0} MB', di.used / 1024 / 1024) ]
                    }
            })
            num = num + 1
    


    Переходим к созданию веб-сервера и делаем шаблон для отображения страницы, который будет использовать данные из модели.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
        <title> Server monitor v. {{ script_version }} </title>
    </head>
    <body>
        <div class="container">
            <H1> Server information </H1>
            <H3>
                <span class="label label-success">
                    Active since {{ active_since }} ({{ days_active }} days)
                </span>
            </H3>
            <p class="text-info">
                {{ system }} {{ release }} {{ version }}
            </p>
        </div>
        {% for block in blocks %}
            <div class="container">
                <H2> {{ block.title }} </H2>
                <div class="panel panel-default">
                    <div class="panel-body">
                        <table>
                          <tr>
                          <td>
                              {{ block.graph }}
                          </td>
                          <td>
                          <div class = "container">
                            <p class="text-primary">
                                {{ block.data.primary }}
                            </p>
                                {% for line in block.data.lines %}
                                    <p class="text-info">
                                        {{ line }}
                                    </p>
                                {% endfor %}
                          </div>
                          </td>
                          </tr>
                          </table>
                    </div>
                </div>
            </div>
        {% endfor %}
    </body>
    </html>
    

    Наконец, добавим скрипт запуска сервера на Flask по адресу из настроек.

    #!/usr/bin/python3
    
    from flask import *
    from conf import config as cfg
    from lib import timemon as tm
    from lib import info
    import psutil
    import datetime
    import platform
    
    
    # server health monitoring tool
    
    app = Flask(cfg.server_name)
    
    
    @app.route('/')
    def index():
        active_since = datetime.datetime.fromtimestamp(psutil.boot_time())
        return render_template("index.html",
                               script_version=cfg.version,
                               active_since=active_since,
                               days_active=(datetime.datetime.now() - active_since).days,
                               system=platform.system(),
                               release=platform.release(),
                               version=platform.version(),
                               blocks=info.get_blocks())
    
    print("Starting time monitor for", cfg.time_step, "s period")
    tm.start()
    
    print("Starting web server", cfg.server_name, "at", cfg.server_host, ":", cfg.server_port)
    app.run(port=cfg.server_port, host=cfg.server_host)
    
    

    Вот и все. Запустив скрипт, можно посмотреть графики и диаграммы.

    Весь код, как обычно, на github.
    Проверено на windows и linux.

    Любые улучшения и пожелания приветствуются.

    Комментарии 27

      0
      from conf import config as cfg

      А сразу нельзя было нормально сделать? ;)
      ЗЫ: пошел смотреть код, а там зачем-то import psutil в конфиге, *.pyc в репозитории.
        +1
        html в python коде, конкатенация черезе '+', общий except, дальше не стал разбирать.
        :(
          0
          и? для простого скрипта за два часа вполне.
          Давайте тесты добавим, деплой и тд и тп. Только, зачем?
          Это же простой скрипт.
            0
            Там такие мелочи и портят общее впечатление. Ну если уж подняли тему «скрипта за два часа», зачем тогда он на хабре?
              0
              Я думаю, что кому то он может пригодиться. Например, вам.
                0
                Мне точно нет, особенно с таким кодом.
                  0
                  А покажите код своих проектов, посмотрим — интересно.
                    +1
                    Ну вот и подошли к ответу на критику — «Сперва добейся» (
                      0
                      Вообще то я редко пишу на python, стало интересно посмотреть на хороший код. Действительно хороший. Если есть что показать, посмотрю с удовольствием.
                        0
                        Гляньте исходники крупных проектов, того же Flask.
                          0

                          Далеко ходить не надо. В стандартной библиотеке отличный код

                    +1

                    Инструментов мониторинга 100500, от простых до сложных. Если нужен мониторинг, то лучше взять готовое, проверенное решение, а не чей-то простой скрипт

              0
              rSedoy Павел, поделитесь, как нужно делать конкатенацию строк на питоне? И почему? Откуда вы почерпнули это знание? Пользователям ведь хочется узнать не просто чьи-то оценки, а получить новые знания.
              Я вот тут прочитал (но этому обоснованию пять лет, все могло поменяться), что использование плюса не имеет существенных недостатков по производительности, а скорее наоборот, в общем случае оптимальный выбор. ( stackoverflow.com/questions/12169839/which-is-the-preferred-way-to-concatenate-a-string-in-python )
                0
                Да про это уже много написано и без проблем находится через поиск, а в данном случае, нужна даже не конкатенация, а форматирование строк. Быстрый поиск дает хорошее описание способов, да еще и на русском языке ;) shultais.education/blog/python-f-strings
                  +1
                  Сорри, оторвался от контекста, тут даже не форматирования, а использование шаблонизаторов, весь html код надо вынести в отдельные файлы и использовать jinja, вроде она чаще всего используется с flask'ом
                    0
                    Здесь согласен. Сегодня исправлю.
                  +1

                  А что плохого в конкатенации посредством оператора '+'? Читабельность? Так, извините, во-первых, '+' работает быстрее других способов конкатенации строк в Python, а во-вторых, врядли


                  s = 'String {}'.format('bad')

                  читабельнее


                  python
                  s = 'String' + 'bad'
                  '''
                    0
                    Про python судить не берусь, но вот в .net конкатенацию через «+» стоит избегать из-за соображений производительности. Возможно, считается (как в c#), что string.fomat() выглядит эстетичнее.
                      0
                      # Python >= 3.6
                      w = 'bad'
                      s = f'String {w}'
                      # Python < 3.6
                      s = ''.join([
                          'String ',
                          'bad'
                      ])
                      


                      На stackoverflow говорят, что вариант с join — самый быстрый.
                    0
                    да, поправим.
                    0
                    Статья обновлена и дополнена с учетом замечаний в комментариях.
                      +1
                      А я для этого пользуюсь dashboard.monitis.com
                      Там тоже есть какой то agent для линукса, который мониторит память, процессор, место на диске и т.д
                      Плохо только что уведомления о превышении лимитов там только в платной версии
                        0
                        По поводу try… except.
                        Из моего опыта .net: во всех учебниках, в том же Рихтере написано, что правила плохого тона — перехват самого общего эксепшна или всех сразу, так как перехват исключений в программе означает автоматом, что программист подразумевает возможность возникновения конкретных ошибок и их обрабатывает.
                        Единственное, что может быть правильно:
                        try
                        {
                        ...
                        }
                        catch (Exception ex)
                        {
                           // перекидываем общий эксепшн дальше по стеку
                           throw;
                        }
                        

                        В этом случае ошибка будет перекинута выше.

                        В этом скрипте на Windows при проверке свободного места на некоторых дисках вылетают разные ошибки из пакета psutil.
                        Как правильно реализовать перехват общего исключения на python?
                        Как я понял, просто try… except — это не правильно, даже если после try указать самый общий exception.
                          +1
                          Как по мне, видно, что python не является основным язык программирования для автора статьи. Есть общая проблема — Ваш код — громоздкий.
                          1. mem_info.__len__(), ну ладно почему нет.
                          2. объявление списка в виде mem_info = list(), а не mem_info = [], имеет право на существование
                          3. импорт модулей, которые в дальнейшем нигде не используются, по типу datetime и platform? тоже мелочи
                          4. str.format('Active memory: {0} MB', mem.active) — в каждой строке нагромоздили. Не читабельно и чрезмерно.
                          5. Ваша избыточная функция get_blocks(зачем она?),
                          6. общая структура flask в github
                          Это только за минуту беглого просмотра.
                          Я понимаю, что Вы реализовали данную работу за пару часов, но она написана безграмотно. Это не плохо. Но соглашусь с комментарием(https://habrahabr.ru/post/345848/#comment_10593082) — мелочи портят восприятие.
                          Хороший код с pythonic way? Первые 100 страниц, Бретт Слаткин — Effective Python рекомендую, там подобные стилистические ошибки разбираются.
                          Заранее извиняюсь, если что не так.
                            0
                            Спасибо за содержательный ответ :)
                            +1
                            Смело! Мне в целом статья понравилась! Я понимаю, что лучше можно всегда. Комментарии очень полезные.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое