Open6

Amazon CloudWatch Agent の procstat プラグインを利用したプロセス監視

nukopynukopy
  • AmazonCloudWatch ユーザガイド:procstat プラグインでプロセスメトリクスを収集する

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-procstat-process-metrics.html

  • (Classmethod, 2021/11) CloudWatch Agent の procstat プラグインで exe と pattern に指定するプロセス名・プロセス起動のコマンドラインを確認する方法

https://dev.classmethod.jp/articles/cloudwatch-agent-procstat-exe-pattern/

nukopynukopy

CloudWatch Agent procstat プラグインの exe と pattern の違い

どちらも指定したパターンを「含む」一致を使っている。

  1. exe
    • プロセス名に基づいてプロセスを選択する
    • 正規表現の照合ルールを使用する(後述の syntax)
    • 一致は「含む」一致
    • 例: exe フィールドに agent という文字列を指定すると、cloudwatchagent のような名前を持つプロセスも一致する。
  2. pattern
    • プロセスの起動に使用されたコマンドライン全体に基づいてプロセスを選択する
    • 正規表現の照合ルールを使用する(後述の syntax)
    • コマンドで使用されるパラメータやオプションも含めて、コマンドライン全体がチェックされる
    • 一致は「含む」一致
    • 例: pattern-c を指定すると、-config のようなコマンドラインオプションで起動したプロセスも一致する

syntax は以下のリンクを参照。

https://github.com/google/re2/wiki/Syntax

nukopynukopy

なるほど。理解した。

node とか python みたいなスクリプト言語のプロセスはたしかにプロセス名だけだと判別できない。


(by Claude 3.5 Sonnet)

プロセス名とコマンドライン全体は実際に異なる場合が多いです。実例を使って説明しましょう。

プロセス名とコマンドライン全体の違い:実例

以下に、いくつかの一般的なプロセスの例を示します。各例で、プロセス名とコマンドライン全体の違いを確認できます。

  1. Nginxウェブサーバー
    • プロセス名: nginx
    • コマンドライン全体: /usr/sbin/nginx -g daemon off;
  2. Pythonスクリプト
    • プロセス名: python3
    • コマンドライン全体: /usr/bin/python3 /home/user/scripts/my_script.py --config /etc/myapp/config.ini
  3. Node.jsアプリケーション
    • プロセス名: node
    • コマンドライン全体: /usr/bin/node /var/www/myapp/server.js --port 3000 --env production
  4. Javaアプリケーション
    • プロセス名: java
    • コマンドライン全体: /usr/bin/java -Xmx2g -jar /opt/myapp/application.jar --spring.profiles.active=prod
  5. データベースサーバー(MySQL)
    • プロセス名: mysqld
    • コマンドライン全体: /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid

これらの例から分かるように、プロセス名は通常実行可能ファイルの名前であり、コマンドライン全体にはフルパス、引数、オプションなどが含まれます。

これらの例を踏まえて、CloudWatch Agentのprocstatプラグインでのexepatternの使用例を示します:

  1. Nginxプロセスの監視:

    • exeを使用する場合:
      {
        "exe": "nginx",
        "measurement": ["cpu_usage", "memory_rss"]
      }
      
    • patternを使用する場合:
      {
        "pattern": ".*nginx.*daemon off;",
        "measurement": ["cpu_usage", "memory_rss"]
      }
      
  2. 特定の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の方がシンプルで使いやすいでしょう。

この説明で、プロセス名とコマンドライン全体の違い、そしてそれに基づくexepatternの使い分けがより明確になりましたか?

nukopynukopy

補足: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.goTailFile 関数
    • L133 の go t.tailFileSync で実行してる goroutine が tail のメインロジックっぽい

https://github.com/aws/amazon-cloudwatch-agent/blob/ad1a3773d5019acd9886b78b1a9398f50ca4af75/plugins/inputs/logfile/tail/tail.go#L97-L136

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tail/tail.goTail.tailFileSync メソッド
    • L347 // Read line by line の行からの無限ループのブロックが tail の本体。ファイル末尾に追加される行を L353 の tail.readLine() で読み取り、L357, L376 で出てくる tail.sendLine() で読み取った行の文字列をどこかに送信している。
    • sendLine メソッドの中で rate limit の制御もしている。sendLine メソッドが false を返すと 1 秒待つ処理を入れている(L357-L372)。

https://github.com/aws/amazon-cloudwatch-agent/blob/ad1a3773d5019acd9886b78b1a9398f50ca4af75/plugins/inputs/logfile/tail/tail.go#L314-L426

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tail/tail.goTail.sendLine メソッド
    • L520 の case tail.Lines <- &Line{line, now, nil, offset}:tail.Lines というチャネルにログファイルの末尾の文字列とそのメタデータ(オフセットとか)を送信している

https://github.com/aws/amazon-cloudwatch-agent/blob/ad1a3773d5019acd9886b78b1a9398f50ca4af75/plugins/inputs/logfile/tail/tail.go#L506-L542

tail.Lines チャネルから「追加されたログの末尾行」の文字列を受け取って処理している(文字列をバッファに突っ込む)のは誰かというと、plugins/inputs/logfile/tailersrc.gotailerSrc.runTail メソッド。

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.gotailerSrc 構造体
    • tailerSrc.tailer というフィールドに先ほどまで見ていた Tail 構造体のポインタが入る
    • CloudWatch Logs のロググループ、ログストリームはここで定義されているため、ログファイルとロググループ、ログストリームの紐づけはここで行われていることが分かる

https://github.com/aws/amazon-cloudwatch-agent/blob/main/plugins/inputs/logfile/tailersrc.go#L59-L80

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.gotailerSrc.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 へ送信する

https://github.com/aws/amazon-cloudwatch-agent/blob/main/plugins/inputs/logfile/tailersrc.go#L169-L282

ts.outputFn(e) でログイベントを CloudWatch Logs へ送信する」と書いたが、実際には tailerSrc の定義にあるように tailerSrc.outputFn フィールドの型は func(logs.LogEvent) なので、Amazon CloudWatch のログイベントを受け取る関数である。推測で書いたが、ここで CloudWatch Logs に送信している実装はまだ出てきていない。(これは外部 API との連携部分だからテストを実装するときにモックしやすいようにこのような設計になっている?こうしとけばたしかにテストは書きやすい)

ts.outputFn の中身がどのように設定されているかを見ていく。

まず、tailerSrc.outputFntailerSrc の初期化関数 NewTailerSrc ではセットされない。

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.goNewTailerSrc 関数

https://github.com/aws/amazon-cloudwatch-agent/blob/main/plugins/inputs/logfile/tailersrc.go#L85-L118

実際に tailerSrc.outputFn フィールドに値がセットされるのは tailerSrc.SetOutput メソッドである。つまり、tailerSrcNewTailerSrc で初期化された後、tailerSrc.SetOutput がどこで呼ばれているかを探せば良い。

  • amazon-cloudwatch-agent/plugins/inputs/logfile/tailersrc.gotailerSrc.SetOutput メソッド

https://github.com/aws/amazon-cloudwatch-agent/blob/main/plugins/inputs/logfile/tailersrc.go#L120-L126

tailerSrc はログファイルプラグインの amazon-cloudwatch-agent/plugins/inputs/logfile/logifle.goFindLogSrc 関数で初期化されている。

  • amazon-cloudwatch-agent/plugins/inputs/logfile/logifle.goFindLogSrc 関数
    • L232 で NewTailerSrc が呼ばれている

https://github.com/aws/amazon-cloudwatch-agent/blob/main/plugins/inputs/logfile/logfile.go#L145-L265

次に tailerSrc.SetOutput がどこで呼ばれるかを探す。

ソースコードを追うと、amazon-cloudwatch-agent/logs/logs.gologAgent.runSrcToDest メソッドで src.SetOutput(func(e LogEvent) {...}) という形で呼ばれている。

  • amazon-cloudwatch-agent/logs/logs.gologAgent.runSrcToDest メソッド
    • L141-L148 を見ると、src.SetOutput メソッドには「エラーがなければログイベント(LogEvent 構造体)を eventsCh というチャネルに送信する」という関数が渡されている
    • その直後の行 L150-L160 の for ブロックで eventsCh の受信処理が書かれている。dest.Publish([]LogEvent{e}) というのがおそらく CloudWatch Logs にログイベントを送信している部分と推測される。

https://github.com/aws/amazon-cloudwatch-agent/blob/main/logs/logs.go#L137-L161

dest.Publish の実装を見る前に、logAgent.runSrcToDest メソッドがどこで実行されているかを見ておく必要がある。このメソッドは amazon-cloudwatch-agent/logs/logs.goRun 関数で goroutine として実行されている(名前の通り Run 関数は CloudWatch Agent のログファイルに関する機能のエントリーポイントっぽい)。

  • amazon-cloudwatch-agent/logs/logs.goRun 関数
    • L106 から始まる無限ループの中で、CloudWatch Agent の設定ファイルの logs.logs_collected.files.collect_list フィールドに記述したログストリームのリストが展開され、各ログストリームに対して go l.runSrcToDest(src, dest) が実行される。これでファイルの末尾の監視(tail) が始まる。
    • L125 の destdest := 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"
      },
      ...
      

https://github.com/aws/amazon-cloudwatch-agent/blob/main/logs/logs.go#L78-L135

logAgent.runSrcToDest が実行される場所がわかったところで、最後に dest.Publish メソッドの実装を見る。

[長い...]

https://github.com/aws/amazon-cloudwatch-agent/blob/main/logs/logs.go#L45-L55

// 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
}