Guile で df コマンドの出力からディスク使用率を確認してみた
目次
1. はじめに
Guix System
で動かしているサーバーのディスク容量を監視するために
Guile で df
コマンドの出力を読んでディスク容量を確認する手続きを Guile
で作成したので紹介します。本記事によって Guile
でコマンドの出力を一行づつ読んで処理するときのイメージが掴めると思います。なお、本プログラムは異常時の処理を書いていない雑なものなので参考にする場合は注意をお願いいたします。
2. 本文中のプログラムのライセンス
本記事で例示するプログラムのライセンスは GPLv3 (or later)
です。ライセンスの詳細は
monitoring リポジトリの
COPYING
ファイルと
tojo-tokyo/monitoring.scm
ファイルのヘッダの部分を参照してください。
3. パイプを開いて 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 の連想リストのリストにできればよさそうです。
4. 文字列を空白で区切る手続きを作る
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")
空白以外の文字が一個以上連続したものを集めることで空白で区切られた文字列のリストを作っています。
5. 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
意図したとおりに実装できているようです。
6. ディスク使用率がしきい値を超えているかを調べる
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%
なので意図した挙動をしているようです。
7. おわりに
無事に Guile を使ってディスク使用率を調べる手続きを作成することができました。この Web サイトは Guix System で動いていて現在上記のプログラムを使って実際に監視を実施しています(該当するコード、Guix System では G-Expressions という仕組みを利用して定期実行ジョブを Guile で気軽に実装できます)。このように GNU Guile では Unix のコマンドと連携する実用的なプログラムを簡単に書くことができるのでみなさんも是非 GNU Guile を使ってみてください。