LE Blog

Инженер от души

05.11.2010 firtree_right Ротация логов рельсового приложения

hay roller

Введение

Это уже давно известная тема, и я не претендую на открытие Америки, но для себя зафиксирую это знание.

Даже если вы используете капистрано для выкладывания проекта в сеть, логи приложения хранятся в одном и том же месте (папка shared/log и разрастаются до огромных размеров. Можно, конечно, запускать после каждого обновления файлов проекта комманду:

rake log:clear

Но есть более цивилизованные методы. Тем более, после определённого времени код проекта начинает обновляться всё реже и реже.

С помощью системы

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

Чтобы организовать это удовольствие для своего проекта нужно создать файл /etc/logrotate.d/my_project:

/path/to/my_project/shared/log/*.log {
  weekly
  missingok
  rotate 10
  nomail
  compress
  delaycompress
  sharedscripts
  postrotate
    touch /path/to/my_project/current/tmp/restart.txt
  endscript
}

Здесь написано:

  1. weekly — разбивать лог еженедельно;
  2. missingok — не выходить с ошибкой, если файла нет;
  3. rotate 10 — хранить 10 предыдущих томов;
  4. nomail — не высылать удаляемые тома на электронную почту;
  5. compress — архивировать;
  6. delaycompress — архивировать не сразу, т.к. после переименования файла и до перезапуска пэссенджера логи пишутся в тот же переименованный файл;
  7. sharedscripts — запускать скрипт один раз для всех логов по маске;
  8. postrotate...endscript — скрипт, который нужно запустить после ротации: в данном случае перезапустить пэссенджер.

Файлом должен владеть root:root. Теперь можно проверить и запустить принудительно, убедившись, что наш файл включается в общий список:

sudo logrotate -dv /etc/logrotate.conf
sudo logrotate -fv /etc/logrotate.conf

С помощью руби

В руби есть встроенный метод ротации логов. Достаточно в файе config/environment.rb написать внутри блока Rails::Initializer.run один из вариантов:

config.logger = Logger.new(config.log_path, "weekly")

или

config.logger = Logger.new(config.log_path, 10, 1.megabyte)

Первый вариант осуществляет ротацию раз в неделю, а второй — по достижении файлом размера в 1 мегабайт и хранит 10 старых томов. Только в данном случае архивацию, если она нужна, придётся организовывать отдельно.

Было бы интересно

Для логротейт можно написать такую маску, которая бы включала в себя все логи всех рельсовых проектов. Но мне неизвестен способ потом написать такой скрипт, который бы перезапускал именно те проекты, для которых была сделана ротация. Например, если логротэйт не нашёл нужного файла, то и скрипт не запустит. А если мы указываем путь типа /path/to/*/shared/.log, то и скрипт должен перебирать все эти проекты и создавать или просто менять дату редактирования файлов restart.txt. Или можно просто перезапускать апач.

Материалы для самостоятельного изучения

  1. Документация logrotate (по-русски)
  2. Что ещё можно делать с логами приложения на рельсах

16.03.2009 firtree_right Автоматизация процесса публикации

Когда я впервые прочитал про, например, Capistrano, мне, конечно же, сразу захотелось тоже начать применять эту клёвую штуку. Но я, конечно же, не преодолел барьер входа. На тот момент у меня было полтора приложения на ruby on rails, которые я довольно редко обновлял. Позже, когда я начал регулярно обновлять несколько приложений, использовать средства автоматизации оказалось очень просто и очень естественно. Для этого достаточно было вручную обновить приложение раз двадцать. :)

Задача

Допустим, речь идёт не о веб-приложении, а о библиотеке, которая используется на сервере несколькими веб-приложениями и другими программами, которые так же исполняются на сервере. Вполне логичным представляется сделать её в виде rubygem. И тогда встает вопрос обновления этой библиотеки на сервере.

Если делать это вручную достаточно долго, то со временем, после упрощений и оптимизаций, становится понятно, что для обновления нужно зайти на сервер по ssh и выполнить простую комманду:

cd somedir && do_some_stuff && sudo do_some_sudo_stuff

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

Ресурсы

Нам понадобится две библиотеки: одна для использования ssh, и другая для защищенного от заглядывания через плечо ввода sudo-пароля. (Оказалось, что сделать на руби такой ввод не так просто, поэтому я просто взял готовую библиотеку, которую и так использует, например, Capistrano и ряд других приложений).

sudo gem i net-ssh highline

Решение

Первым делом я, конечно, попробовал:

Net::SSH.start("myserver", "sudouser") do |ssh|
  result = ssh.exec!("cd somedir && do_some_stuff && sudo do_some_sudo_stuff")
  puts result
end

Но никакого вывода просто не дождался. Потому что дойдя до sudo-команды, процесс просто оставался в вечном ожидании.

Чтобы сделать ввод пароля, нужно создавать канал. А так же неплохо было бы проверить возможность интерактивного взаимодействия:

Net::SSH.start("myserver", "sudouser") do |ssh|
  channel = ssh.open_channel do |ch|
    ch.request_pty do |c, success|
      raise "Cannot obtain pty" unless success
    end
    ...
  end
end

Теперь нужно отправить пароль в нужный момент. Чтобы узнать, когда наступил нужный момент, нужно использовать ключ -p (prompt) при вызове sudo, чтобы сказать ему, каким запросом спрашивать у нас пароль.

sudo -p 'sudo password: ' do_some_sudo_stuff

Когда нужно будет запросить пароль, воспользуемся библиотекой highline:

pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }

Это позволит нам получить пароль, не светя его на экране. Как это обычно и делает sudo.

Теперь посмотрим на всё решение целиком. В папке библиотеки создаем файл Rakefile. Записываем в него нашу задачу. В моём случае команда для сервера состояла примерно из следующего набора: «Перейти в папку, обновить исходники из scm, собрать джем, sudo установить джем, sudo удалить установленные старые версии джема».

Rakefile

require 'rubygems'
require 'rake'
require "net/ssh"
require 'highline'
...
desc "Update gem on the server by current version on remote origin"
task :deploy do
  Net::SSH.start("myserver", "sudouser") do |ssh|
    channel = ssh.open_channel do |ch|
      ch.request_pty do |c, success|
           # Если pseudo-tty недоступен, то невозможно никакого интерактива
        raise "Cannot obtain pty" unless success
      end

      ch.exec("cd somedir && do_some_stuff && sudo -p 'sudo password: ' do_some_sudo_stuff") do |c, success|
        abort "Could not execute command" unless success

        c.on_data do |c, data|
          if data =~ /sudo password: /
            pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }
            c.send_data "#{pwd}\n"
          else
            c[:result] ||= ""
            c[:result] << data # Можно, конечно, и в процессе выводить
          end
        end

        c.on_extended_data do |c, type, data|
          puts "STDERR : #{data}"
        end
      end
    end
    ssh.loop # Ожидаем, пока закончится сеанс
    puts channel[:result] # Выводим результат сеанса (можно было и в процессе)
  end
end
...

Теперь вместо всей той последовательности действий достаточно написать запустить rake deploy и ввести пароль.

Материалы для изучения

Пособия по использованию Capistrano Документация Net::SSH