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

Недавно возникла необходимость сделать простой и расширяемый монитор использования системы для сервера на 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.

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

Комментарии (24)


  1. rSedoy
    29.12.2017 14:07

    from conf import config as cfg

    А сразу нельзя было нормально сделать? ;)
    ЗЫ: пошел смотреть код, а там зачем-то import psutil в конфиге, *.pyc в репозитории.


    1. rSedoy
      29.12.2017 14:14
      +1

      html в python коде, конкатенация черезе '+', общий except, дальше не стал разбирать.
      :(


      1. AntonioGrande Автор
        29.12.2017 14:22

        и? для простого скрипта за два часа вполне.
        Давайте тесты добавим, деплой и тд и тп. Только, зачем?
        Это же простой скрипт.


        1. rSedoy
          29.12.2017 14:24

          Там такие мелочи и портят общее впечатление. Ну если уж подняли тему «скрипта за два часа», зачем тогда он на хабре?


          1. AntonioGrande Автор
            29.12.2017 14:26

            Я думаю, что кому то он может пригодиться. Например, вам.


            1. rSedoy
              29.12.2017 14:29

              Мне точно нет, особенно с таким кодом.


              1. AntonioGrande Автор
                29.12.2017 14:30

                А покажите код своих проектов, посмотрим — интересно.


                1. rSedoy
                  29.12.2017 14:33

                  Ну вот и подошли к ответу на критику — «Сперва добейся» (


                  1. AntonioGrande Автор
                    29.12.2017 14:36

                    Вообще то я редко пишу на python, стало интересно посмотреть на хороший код. Действительно хороший. Если есть что показать, посмотрю с удовольствием.


                    1. rSedoy
                      29.12.2017 14:39

                      Гляньте исходники крупных проектов, того же Flask.


                    1. foldr
                      29.12.2017 15:46

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


            1. foldr
              29.12.2017 15:52
              +1

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


              1. denaspireone
                30.12.2017 17:52

                Есть munin-monitoring.org и тоже на python и даже ля него есть exporter в Graphite


      1. lxsmkv
        29.12.2017 20:34

        rSedoy Павел, поделитесь, как нужно делать конкатенацию строк на питоне? И почему? Откуда вы почерпнули это знание? Пользователям ведь хочется узнать не просто чьи-то оценки, а получить новые знания.
        Я вот тут прочитал (но этому обоснованию пять лет, все могло поменяться), что использование плюса не имеет существенных недостатков по производительности, а скорее наоборот, в общем случае оптимальный выбор. ( stackoverflow.com/questions/12169839/which-is-the-preferred-way-to-concatenate-a-string-in-python )


        1. rSedoy
          29.12.2017 20:44

          Да про это уже много написано и без проблем находится через поиск, а в данном случае, нужна даже не конкатенация, а форматирование строк. Быстрый поиск дает хорошее описание способов, да еще и на русском языке ;) shultais.education/blog/python-f-strings


        1. rSedoy
          29.12.2017 20:53

          Сорри, оторвался от контекста, тут даже не форматирования, а использование шаблонизаторов, весь html код надо вынести в отдельные файлы и использовать jinja, вроде она чаще всего используется с flask'ом


          1. AntonioGrande Автор
            29.12.2017 20:58

            Здесь согласен. Сегодня исправлю.


      1. HeaTTheatR
        31.12.2017 14:28
        +1

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


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

        читабельнее


        python
        s = 'String' + 'bad'
        '''


        1. AntonioGrande Автор
          31.12.2017 15:47

          Про python судить не берусь, но вот в .net конкатенацию через «+» стоит избегать из-за соображений производительности. Возможно, считается (как в c#), что string.fomat() выглядит эстетичнее.


        1. Tihon_V
          01.01.2018 22:05

          # Python >= 3.6
          w = 'bad'
          s = f'String {w}'
          # Python < 3.6
          s = ''.join([
              'String ',
              'bad'
          ])
          


          На stackoverflow говорят, что вариант с join — самый быстрый.


    1. AntonioGrande Автор
      29.12.2017 14:22

      да, поправим.


  1. AntonioGrande Автор
    29.12.2017 23:10

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


  1. ProstoDenis
    30.12.2017 17:53
    +1

    А я для этого пользуюсь dashboard.monitis.com
    Там тоже есть какой то agent для линукса, который мониторит память, процессор, место на диске и т.д
    Плохо только что уведомления о превышении лимитов там только в платной версии


  1. AntonioGrande Автор
    31.12.2017 16:01

    По поводу try… except.
    Из моего опыта .net: во всех учебниках, в том же Рихтере написано, что правила плохого тона — перехват самого общего эксепшна или всех сразу, так как перехват исключений в программе означает автоматом, что программист подразумевает возможность возникновения конкретных ошибок и их обрабатывает.
    Единственное, что может быть правильно:

    try
    {
    ...
    }
    catch (Exception ex)
    {
       // перекидываем общий эксепшн дальше по стеку
       throw;
    }
    

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

    В этом скрипте на Windows при проверке свободного места на некоторых дисках вылетают разные ошибки из пакета psutil.
    Как правильно реализовать перехват общего исключения на python?
    Как я понял, просто try… except — это не правильно, даже если после try указать самый общий exception.