Guile で df コマンドの出力からディスク使用率を確認してみた

はじめに

Guix System で動かしているサーバーのディスク容量を監視するために Guiledf コマンドの出力を読んでディスク容量を確認する手続きを Guile で作成したので紹介します。本記事によって Guile でコマンドの出力を一行づつ読んで処理するときのイメージが掴めると思います。なお、本プログラムは異常時の処理を書いていない雑なものなので参考にする場合は注意をお願いいたします。

本文中のプログラムのライセンス

本記事で例示するプログラムのライセンスは GPLv3 (or later) です。ライセンスの詳細は monitoring リポジトリの COPYING ファイルと tojo-tokyo/monitoring.scm ファイルのヘッダの部分を参照してください。

パイプを開いて df の出力を読む

まずは Guile から df コマンドを呼び出してみましょう パイプを開くために open-input-pipe 手続きを使います。 open-input-pipe には (ice-9 popen) モジュールが必要で、ポートから文字を読み込むのに (ice-9 textual-ports) モジュールの手続きを利用しています。

(use-modules (ice-9 popen)
             (ice-9 textual-ports))

(let ((port (open-input-pipe "df")))
  (display (get-string-all port))
  (close-pipe port))
Filesystem     1K-blocks     Used Available Use% Mounted on
none              492120        0    492120   0% /dev
/dev/vda3       24377696 10096084  13020252  44% /
tmpfs             501932        0    501932   0% /dev/shm
0

ヘッダ行が先頭に一つありそれより下の行に1個以上の空白で区切られた値として必要な情報が出力されるようです。ヘッダ行は無視することにしてそれ以下を Guile の連想リストのリストにできればよさそうです。

文字列を空白で区切る手続きを作る

1個以上の空白で区切られた文字列をリストにする手続きを Guile を軽く探したのですが見つけられなかったので自分で作ります。正規表現を扱う (ice-9 regex) を使って下記のように実装します。

(use-modules (ice-9 regex))

(define (split-with-spaces x)
  (map match:substring (list-matches "[^ ]+" x)))

(split-with-spaces " hello  world ")
("hello" "world")

空白以外の文字が一個以上連続したものを集めることで空白で区切られた文字列のリストを作っています。

df 手続きを実装する

これから作成する df は無引数で呼びだして、df コマンドの出力のそれぞれの行を連想リストに変換したものを返す手続きです。

今回は split-with-spaces を使用して下記のように実装しました。 なお、本記事の目的はコマンドの出力をパイプで一行づつ処理をするという Unix でよくある処理を Guile でやる方法を例示することなので意図的に手続き的に書いています。

(define (df)
  (let ((port (open-input-pipe "df")))
    (get-line port)                     ; ヘッダ行を読み飛ばす
    (let loop ((line (get-line port))
               (table '()))
      (cond ((eof-object? line)
             (close-pipe port)
             table)
            (else
             (loop (get-line port)
                   (cons (map cons
                              '(filesystem 1k-blocks used available use% mounted-on)
                              (split-with-spaces line))
                         table)))))))

df コマンドの出力を一行ずつ読んで連想リストを作成していき、最後まで読みきったらパイプをクローズした後に構築した連想リストのリストを返却します。

df 手続きの結果を整形して出力してみましょう(format 手続きのために (ice-9 format), match-lambda のために (ice-9 match) モジュールを使っています)。

(use-modules (ice-9 match)
             (ice-9 format))

(for-each (lambda (record) 
            (for-each (match-lambda 
                        ((key . val)
                         (format #t "~a: ~a~%"
                                 (symbol->string key)
                                 val)))
                      record)
            (newline))
          (df))
filesystem: tmpfs
1k-blocks: 501932
used: 0
available: 501932
use%: 0%
mounted-on: /dev/shm

filesystem: /dev/vda3
1k-blocks: 24377696
used: 10096084
available: 13020252
use%: 44%
mounted-on: /

filesystem: none
1k-blocks: 492120
used: 0
available: 492120
use%: 0%
mounted-on: /dev

意図したとおりに実装できているようです。

ディスク使用率がしきい値を超えているかを調べる

df 手続きを使ってしきい値以上のディスク使用率になっているファイルシステムがあったら真を返す、disk-use%-over? 手続きを作ってみましょう。

私は /dev/ から始まっているファイルシステムにしか興味ないので、/dev/ から始まっている文字列かどうかを判定する述語 prefix-/dev/? を作成します。

(define (prefix-/dev/? x)
  (and (string? x)
       (<= 5 (string-length x))
       (string=? (substring x 0 5) "/dev/")))

(map prefix-/dev/? '("/tmp/shm" "/dev/hello" "/neko/dev"))
(#f #t #f)

use%% を含んだ文字列になってしまっていて数値比較をするには扱いにくい状態です。x% のような形になっている文字列を数字に変換する手続きを作成します。

(define (use%->number x)
  (string->number (string-delete #\% x)))

(use%->number "42%")
42

それでは prefix-/dev/?use%->number を利用して disk-use%-over? を実装してみましょう ((srfi srfi-26)cut 構文を使っています。(cut f <> x)(lambda (y) (f y x)) のように書くのと同じです)。

(use-modules (srfi srfi-26)
             ((srfi srfi-1) #:select (any)))

(define (disk-use%-over? threshold)
  (any (cut < threshold <>)
       (map (compose use%->number (cut assoc-ref <> 'use%))
            (filter (compose prefix-/dev/? (cut assoc-ref <> 'filesystem))
                    (df)))))

(map disk-use%-over? '(40 50))
(#t #f)

しきい値が 40 の場合は真を返し、50 の場合は偽を返しました。/dev/vda3 の現在のディスク使用率は 44% なので意図した挙動をしているようです。

おわりに

無事に Guile を使ってディスク使用率を調べる手続きを作成することができました。この Web サイトは Guix System で動いていて現在上記のプログラムを使って実際に監視を実施しています(該当するコード、Guix System では G-Expressions という仕組みを利用して定期実行ジョブを Guile で気軽に実装できます)。このように GNU Guile では Unix のコマンドと連携する実用的なプログラムを簡単に書くことができるのでみなさんも是非 GNU Guile を使ってみてください。