Amazon CloudWatch Agent の procstat プラグインを利用したプロセス監視
- AmazonCloudWatch ユーザガイド:procstat プラグインでプロセスメトリクスを収集する
- (Classmethod, 2021/11) CloudWatch Agent の procstat プラグインで exe と pattern に指定するプロセス名・プロセス起動のコマンドラインを確認する方法
プロセスの死活監視自体は procstat プラグインで pid_count
を取得すればできる。exe
フィールドで取得できたプロセス数をカウントしてくれるってことかな。
- (Classmethod, 2023/12) CloudWatch エージェントでプロセスの死活監視をする方法
CloudWatch Agent procstat プラグインの exe と pattern の違い
どちらも指定したパターンを「含む」一致を使っている。
-
exe
- プロセス名に基づいてプロセスを選択する
- 正規表現の照合ルールを使用する(後述の syntax)
- 一致は「含む」一致
- 例:
exe
フィールドにagent
という文字列を指定すると、cloudwatchagent
のような名前を持つプロセスも一致する。
-
pattern
- プロセスの起動に使用されたコマンドライン全体に基づいてプロセスを選択する
- 正規表現の照合ルールを使用する(後述の syntax)
- コマンドで使用されるパラメータやオプションも含めて、コマンドライン全体がチェックされる
- 一致は「含む」一致
- 例:
pattern
に-c
を指定すると、-config
のようなコマンドラインオプションで起動したプロセスも一致する
syntax は以下のリンクを参照。
なるほど。理解した。
node
とか python
みたいなスクリプト言語のプロセスはたしかにプロセス名だけだと判別できない。
(by Claude 3.5 Sonnet)
プロセス名とコマンドライン全体は実際に異なる場合が多いです。実例を使って説明しましょう。
プロセス名とコマンドライン全体の違い:実例
以下に、いくつかの一般的なプロセスの例を示します。各例で、プロセス名とコマンドライン全体の違いを確認できます。
- Nginxウェブサーバー
- プロセス名:
nginx
- コマンドライン全体:
/usr/sbin/nginx -g daemon off;
- プロセス名:
- Pythonスクリプト
- プロセス名:
python3
- コマンドライン全体:
/usr/bin/python3 /home/user/scripts/my_script.py --config /etc/myapp/config.ini
- プロセス名:
- Node.jsアプリケーション
- プロセス名:
node
- コマンドライン全体:
/usr/bin/node /var/www/myapp/server.js --port 3000 --env production
- プロセス名:
- Javaアプリケーション
- プロセス名:
java
- コマンドライン全体:
/usr/bin/java -Xmx2g -jar /opt/myapp/application.jar --spring.profiles.active=prod
- プロセス名:
- データベースサーバー(MySQL)
- プロセス名:
mysqld
- コマンドライン全体:
/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
- プロセス名:
これらの例から分かるように、プロセス名は通常実行可能ファイルの名前であり、コマンドライン全体にはフルパス、引数、オプションなどが含まれます。
これらの例を踏まえて、CloudWatch Agentのprocstat
プラグインでのexe
とpattern
の使用例を示します:
-
Nginxプロセスの監視:
-
exe
を使用する場合:{ "exe": "nginx", "measurement": ["cpu_usage", "memory_rss"] }
-
pattern
を使用する場合:{ "pattern": ".*nginx.*daemon off;", "measurement": ["cpu_usage", "memory_rss"] }
-
-
特定のPythonスクリプトの監視:
-
exe
を使用する場合(全てのPythonプロセスが対象になるため、あまり適していません):{ "exe": "python3", "measurement": ["cpu_usage", "memory_rss"] }
-
pattern
を使用する場合:{ "pattern": ".*python3.*/home/user/scripts/my_script\\.py", "measurement": ["cpu_usage", "memory_rss"] }
-
exe
はプロセス名のみに基づいてマッチングを行うため、同じ実行可能ファイルを使用する複数のプロセスを区別するのが難しい場合があります。一方、pattern
はコマンドライン全体を使用するため、より細かい制御が可能です。
特定のアプリケーションやスクリプトを監視する場合、多くの場合pattern
の方が適しています。ただし、単純にプロセス名だけで十分な場合はexe
の方がシンプルで使いやすいでしょう。
この説明で、プロセス名とコマンドライン全体の違い、そしてそれに基づくexe
とpattern
の使い分けがより明確になりましたか?
取得できるプロセスのメトリクス
- Amazon CloudWatch ユーザガイド:Procstat で収集されるメトリクス
ソースコード上では以下のように定義されている
補足:CloudWatch Agent の tail の仕組み
ちょっと話しそれるけど、CloudWatch Agent によるログファイルの末尾を CloudWatch Logs に送信する tail の実装を覗いてみる。
CloudWatch Agent が CloudWatch Logs に送信するときの挙動はたしか「tail で取得した末尾業の文字列 → バッファ → CloudWatch Logs」のように CloudWatch Logs の送信前に一回バッファを挟んでいる。「バッファ → CloudWatch Logs」の処理はバッファの最大長に達するか、バッファの最大保持時間に達するかでトリガーされるという仕組みだった気がする。
-
Tail
構造体
type Tail struct {
Filename string
Lines chan *Line
Config
file *os.File
reader *bufio.Reader
watcher watch.FileWatcher
changes *watch.FileChanges
curOffset int64
tomb.Tomb // provides: Done, Kill, Dying
dropCnt int
lk sync.Mutex
FileDeletedCh chan bool
}
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tail/tail.go
、TailFile
関数- L133 の
go t.tailFileSync
で実行してる goroutine が tail のメインロジックっぽい
- L133 の
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tail/tail.go
、Tail.tailFileSync
メソッド- L347
// Read line by line
の行からの無限ループのブロックが tail の本体。ファイル末尾に追加される行を L353 のtail.readLine()
で読み取り、L357, L376 で出てくるtail.sendLine()
で読み取った行の文字列をどこかに送信している。 -
sendLine
メソッドの中で rate limit の制御もしている。sendLine
メソッドがfalse
を返すと 1 秒待つ処理を入れている(L357-L372)。
- L347
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tail/tail.go
、Tail.sendLine
メソッド- L520 の
case tail.Lines <- &Line{line, now, nil, offset}:
でtail.Lines
というチャネルにログファイルの末尾の文字列とそのメタデータ(オフセットとか)を送信している
- L520 の
tail.Lines
チャネルから「追加されたログの末尾行」の文字列を受け取って処理している(文字列をバッファに突っ込む)のは誰かというと、plugins/inputs/logfile/tailersrc.go
の tailerSrc.runTail
メソッド。
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.go
、tailerSrc
構造体-
tailerSrc.tailer
というフィールドに先ほどまで見ていたTail
構造体のポインタが入る - CloudWatch Logs のロググループ、ログストリームはここで定義されているため、ログファイルとロググループ、ログストリームの紐づけはここで行われていることが分かる
-
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.go
、tailerSrc.runTail
メソッド- このメソッドが CloudWatch Agent の logfile プラグインによる「ログファイルの末尾行に追加される行の処理」の実質的なエントリーポイント
- エラーチェックや文字列のエンコードのチェックを抜けると、L227 から始まる
else
ブロックでチャネルから受け取ったログファイルの末尾行の文字列をバッファに追加する - L238 から始まる
if
ブロックでは、バッファの長さが 0 より大きいならShouldPublish(ts.group, ts.stream, ts.filters, e)
で CloudWatch Logs へ送信するかどうかのチェックを行い、それがtrue
ならts.outputFn(e)
でログイベントを CloudWatch Logs へ送信する
「ts.outputFn(e)
でログイベントを CloudWatch Logs へ送信する」と書いたが、実際には tailerSrc
の定義にあるように tailerSrc.outputFn
フィールドの型は func(logs.LogEvent)
なので、Amazon CloudWatch のログイベントを受け取る関数である。推測で書いたが、ここで CloudWatch Logs に送信している実装はまだ出てきていない。(これは外部 API との連携部分だからテストを実装するときにモックしやすいようにこのような設計になっている?こうしとけばたしかにテストは書きやすい)
ts.outputFn
の中身がどのように設定されているかを見ていく。
まず、tailerSrc.outputFn
は tailerSrc
の初期化関数 NewTailerSrc
ではセットされない。
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.go
、NewTailerSrc
関数
実際に tailerSrc.outputFn
フィールドに値がセットされるのは tailerSrc.SetOutput
メソッドである。つまり、tailerSrc
が NewTailerSrc
で初期化された後、tailerSrc.SetOutput
がどこで呼ばれているかを探せば良い。
-
amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.go
、tailerSrc.SetOutput
メソッド
tailerSrc
はログファイルプラグインの amazon-cloudwatch-agent/plugins/inputs/logfile/logifle.go
の FindLogSrc
関数で初期化されている。
-
amazon-cloudwatch-agent/plugins/inputs/logfile/logifle.go
、FindLogSrc
関数- L232 で
NewTailerSrc
が呼ばれている
- L232 で
次に tailerSrc.SetOutput
がどこで呼ばれるかを探す。
ソースコードを追うと、amazon-cloudwatch-agent/logs/logs.go
の logAgent.runSrcToDest
メソッドで src.SetOutput(func(e LogEvent) {...})
という形で呼ばれている。
-
amazon-cloudwatch-agent/logs/logs.go
、logAgent.runSrcToDest
メソッド- L141-L148 を見ると、
src.SetOutput
メソッドには「エラーがなければログイベント(LogEvent
構造体)をeventsCh
というチャネルに送信する」という関数が渡されている - その直後の行 L150-L160 の
for
ブロックでeventsCh
の受信処理が書かれている。dest.Publish([]LogEvent{e})
というのがおそらく CloudWatch Logs にログイベントを送信している部分と推測される。
- L141-L148 を見ると、
dest.Publish
の実装を見る前に、logAgent.runSrcToDest
メソッドがどこで実行されているかを見ておく必要がある。このメソッドは amazon-cloudwatch-agent/logs/logs.go
の Run
関数で goroutine として実行されている(名前の通り Run
関数は CloudWatch Agent のログファイルに関する機能のエントリーポイントっぽい)。
-
amazon-cloudwatch-agent/logs/logs.go
、Run
関数- L106 から始まる無限ループの中で、CloudWatch Agent の設定ファイルの
logs.logs_collected.files.collect_list
フィールドに記述したログストリームのリストが展開され、各ログストリームに対してgo l.runSrcToDest(src, dest)
が実行される。これでファイルの末尾の監視(tail) が始まる。 - L125 の
dest
はdest := backend.CreateDest(logGroup, logStream, retention, logGroupClass)
というソースコードからも分かるように、監視するログファイルを CloudWatch Logs のどのロググループ、ログストリームに送信するかを定義している(下記に設定ファイルの例を示す)... { "file_path": "/var/opt/myapp.log", "log_group_class": "STANDARD", "log_group_name": "/myserver", "log_stream_name": "/myserver/{hostname}/myapp.log", "retention_in_days": 14, "timezone": "Local" }, ...
- L106 から始まる無限ループの中で、CloudWatch Agent の設定ファイルの
logAgent.runSrcToDest
が実行される場所がわかったところで、最後に dest.Publish
メソッドの実装を見る。
[長い...]
// A LogBackend is able to return a LogDest of a given name.
// The same name should always return the same LogDest.
type LogBackend interface {
CreateDest(string, string, int, string) LogDest
}
// A LogDest represents a final endpoint where log events are published to.
// e.g. a particular log stream in cloudwatchlogs.
type LogDest interface {
Publish(events []LogEvent) error
}