Просто хочу показать, как с помощью Perl можно сделать простой веб-сервис, на примере мониторинга свободного места на диске и свободной памяти.

Почему именно на Perl - потому что на примере минимального дистрибутива Debian, где Perl уже установлен по умолчанию, и для создания вебсервиса потребуются минимальные усилия.
Ну и заодно - как делаются библиотеки функций и автотесты.

Итак, сам Perl установлен в дистрибутиве.
Устанавливаем дополнительно фреймворк Mojolicious:

apt install libmojolicious-perl

Он займет дополнительно около 70 мегабайт места, но значительно сэкономит время "разработки" данного вебсервиса.

Как уже говорил, в данном примере просто контролируем свободное место на диске и доступную память. Разумеется, задача может быть любой другой, это просто пример.
Чтобы не изобретать велосипед - данные будем получать через системные утилиты, просто добавим обертку над ними, в виде двух функций.

Но это пока еще не веб-приложение, это просто пара функций. Чтобы не забивать себе голову, куда и как их добавить - вынесем их в отдельный модуль, который назовем MyStat.pm

#!/usr/bin/perl

# название модуля должно соответствовать имени файла MyStat.pm
package MyStat;

# Получение информации о свободном месте на диске
sub get_disk_space {
    my $df_output = `df -h /`;  # Получаем информацию о корневом разделе
    my @lines = split("\n", $df_output);
    my @fields = split(/\s+/, $lines[1]);
    return $fields[3];  # Возвращаем доступное пространство
}

# Получение информации о доступной памяти
sub get_memory {
    my $free_output = `free -h`;  # Получаем информацию о памяти
    my @lines = split("\n", $free_output);
    my @fields = split(/\s+/, $lines[1]);
    return $fields[6];  # Возвращаем доступную память
}

# модуль всегда завершается возвратом 1
1;

Теперь в любой программе достаточно будет подключить этот модуль и вызвать функции, примерно так:

#!/usr/bin/perl

# эта строчка нужна чтобы указать, где лежит модуль, 
# если он не находится в системных каталогах типа /usr/share/perl...
# в данном случае он лежит в каталоге lib в домашнем каталоге пользователя
use lib $ENV{HOME}.'/lib';

# подключаем его
use MyStat;

# получаем данные
my $df = MyStat::get_disk_space;
my $mem = MyStat::get_memory;
....

Теперь напишем для модуля автотест. Это необязательно, но когда со временем модули разрастаются, усложняются и изменяются - успешное прохождение автотеста поможет выявить баг до того, как обновление поломает работующую систему.
И лучше это делать сразу - потом будет просто некогда.

Традиционно, его можно назвать MyStat.t, но вообще-то это непринципиально, просто так удобнее понять потом что это было.

#!/usr/bin/perl

use lib $ENV{HOME}.'/lib';
use MyStat;
use Test::More;

my $df = MyStat::get_disk_space;
ok(defined $df, "disk space is $df");


my $mem = MyStat::get_memory;
ok(defined $mem, "free memory is $mem");

done_testing();

По сути - это обычный скрипт, который просто вызывает функции, а команда ok проверяет, то ли мы получили что хотели или нет.
Если выполнить команду perl MyStat.t - получим примерно следующее:

ok 1 - disk space is 70G
ok 2 - free memory is 3.3Gi
1..2

Первый тест ОК, результат "70G", второй тест ОК, результат 3.3Gi.
Оба теста успешно отработали, вернули значения.
Модуль исправен.

Теперь делаем собственно вебсервис: он будет по запросу выдавать JSON с данными о наличии свободного места.
Для этого выполняем команду:

mojo generate lite-app MyApp

У нас получилась заготовка мини-приложения MyApp.pl

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;

get '/' => sub ($c) {
  $c->render(template => 'index');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

В данный момент приложение содержит один единственный метод GET "/", который отрисовывает шаблон index, причем сам шаблон определен в этом же файле, в секции __DATA__ , и состоит из двух частей: шаблона самой страницы, @@ index.html.ep, и шаблона макета default.html.ep.

Такая двойная конструкция позволяет засунуть в макет общие для всего приложения стили, скрипты и прочее, а рендеринг отдельных страниц делать отдельными шаблонами, используя этот макет, или другой, или без макета выводить просто кусок текста, или вообще обходиться без шаблонов.

Но сейчас и всего этого не нужно, рисовать отдельные страницы мы не будем, просто выведем JSON.

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
use Mojolicious::Static;

# ==============================================
use lib $ENV{HOME}.'/lib';
use MyStat;

get '/getstat' => sub ($c) {

  my $mem = MyStat::get_memory;
  $mem =~ s/([\d\.]+).*/$1/;
  my $drive = MyStat::get_disk_space;
  $drive =~ s/([\d\.]+).*/$1/;

  my $t = {
    mem => $mem,
    drive => $drive,
  };

  $c->res->headers->header('Access-Control-Allow-Origin' => '*');
  $c->render(json => $t);
};
# ==============================================

get '/' => sub ($c) {
  $c->render(template => 'index');
};

app->start;
__DATA__
........

Добавлен метод GET "/getstat" который выводит строку наподобие {"mem":2.6,"drive":102.4}
Для этого он вызывает ранее созданные функции из модуля, получает данные, отрезает от них буквы и формирует JSON.
Его можно вызвать из внешней системы, чтобы получить данные о ресурсах на этом сервере.

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

А теперь добавим на "главную страницу" простейшие графики, чтобы можно было сразу в графическом виде смотреть, что оно там нам выдает.
Для этого создаем обыкновенную html-страницу с javascript, вызывающим созданный ранее метод и подставляющим данные в график. А потом внедряем ее в скрипт:

__DATA__
@@ layouts/default.html.ep
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Memory and Disk Usage</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        canvas {
            max-width: 600px;
            margin: 20px auto;
            display: block;
        }
    </style>
</head>
<body><%= content %></body>
</html>

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>System Monitoring</h1>
<p>Графики загруженности памяти и занятости диска</p>
<canvas id="memChart"></canvas>
<canvas id="diskChart"></canvas>

<script>
  // Конфигурация графиков
  const maxDataPoints = 1000; // Максимальная ширина графика (600 измерений)

  // Инициализация графика 
  const memChartCtx = document.getElementById('memChart').getContext('2d');
  const memChart = new Chart(memChartCtx, {
    type: 'line',
    data: {
      labels: Array(maxDataPoints).fill(''), // Метки пустые, обновляем по мере необходимости
      datasets: [{
        label: 'Mem free (Gb)',
        borderColor: 'rgb(75, 192, 192)',
        tension: 0.1,
        data: [] // Данные свободной памяти
      }]
    },
    options: {
        responsive: true,
      scales: {
        x: { display: false },
        y: { min: 0 }
      }
    }
  });

  // Инициализация графика использования диска
  const diskChartCtx = document.getElementById('diskChart').getContext('2d');
  const diskChart = new Chart(diskChartCtx, {
    type: 'line',
    data: {
      labels: Array(maxDataPoints).fill(''), // Метки пустые, обновляем по мере необходимости
      datasets: [{
        label: 'Disk Usage (Gb)',
        borderColor: 'rgb(255, 99, 132)',
        tension: 0.1,
        data: [] // Данные использования диска
      }]
    },
    options: {
      responsive: true,
      scales: {
        x: { display: false },
        y: { min: 0 }
      }
    }
  });

  // Функция для добавления новых данных на график
  function updateChart(chart, newData) {
    if (chart.data.datasets[0].data.length >= maxDataPoints) {
      chart.data.datasets[0].data.shift(); // Удаляем старые данные
      chart.data.labels.shift();
    }
    chart.data.datasets[0].data.push(newData); // Добавляем новые данные
    chart.data.labels.push(''); // Пустая метка
    chart.update();
  }

  // Функция для запроса данных с сервера
  async function fetchData() {
    try {
      const response = await fetch('/getstat');
      if (!response.ok) throw new Error('Network response was not ok');
      const { mem, drive } = await response.json();
      updateChart(memChart, mem);
      updateChart(diskChart, drive);
    } catch (error) {
      console.error('Ошибка при запросе данных:', error);
    }
  }

  // Запрашиваем данные раз в 30 секунд
  setInterval(fetchData, 1000);
  // Первоначальный запуск
  fetchData();
</script>

По сути, в данном случае при запросе к "/" сервер просто выдаст страничку, которая начнет запрашивать "/getstat" и заполнять графики.

А теперь всё это запустим, например так:

morbo -l http://*:8080 MyApp.pl

При подключении браузером к порту 8080 получим примерно такую страницу:

В принципе ничего не мешает сделать страницу красивой, настроить Nginx frontend - и получится небольшое веб-приложение, возможно с десятком страниц и методов. Для большего лучше создавать приложение чуть по другому, но это другая история.
Ну а для нужд внутреннего мониторинга можно оставить и так.

Писать этот текст было дольше...

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


  1. Di-Ger
    15.01.2025 03:19

    apt install libmojolicious-perl

    Он займет дополнительно около 70 мегабайт места, но значительно сэкономит время "разработки" данного вебсервиса.

    Для "минимальной" реализации, а тем более для вашего примера, установка Mojo, на мой взгляд избыточна. Для поднятия веб-сервиса достаточно воспользоваться IO::Socket::INET

    my $server = new IO::Socket::INET(
        Proto => 'tcp',
        LocalPort => $port,
        Listen => SOMAXCONN,
        Reuse => 1
    ) or die 'Unable to create server socket!' ;

    А html шаблон парсить стандартными средствами Perl.