🍆

Grafanaで異アーキ、OSな自宅鯖を管理~VictoriaMetrics, Grafana Alloy, Lokiを添えて~

に公開
1

まえがき

皆さん、自宅の端末管理に疲弊していませんか?
私は多段sshでログを見るのに疲弊しました。

そこで各端末のデータをhttp GETでpull出来るPrometheusと
Linux journalctl、Windowsイベントビューアーを統合して
Lokiでクエリ検索出来るようにGrafanaを利用することにしました。
Grafana-DrilldownImage

ついでにlmsensors_exporterの大手がx64しか作ってなかったので、
色んなCPUに対応させて好きなSBC(RaspberryPi, Radxa, L3スイッチ)から温度等を取得できるようにしました。
curl, wgetで取りに行けます。
なんでバイナリを配布してくれないんですか?ビルドしなきゃじゃないですか

この記事で出来ること

  • WindowsのシステムログとLinuxのjournalログを統合、ラベリングしてGrafanaでクエリ検索
  • N100やRyzen等の自作マシン、ラズパイなどのARMボードのlmsensorsから温度や電圧を監視
  • 上記の要素を時系列で比較できる

忙しい人向け

監視対象への作業

Windows用のセットアップ
$LOKI_URL = "http://🚧🔩lokiのURL🚧"	# 例: http://tailman.hoge-fuga.ts.net

# 無同意でExporter、Alloyをインストール
winget install --accept-package-agreements Prometheus.WindowsExporter GrafanaLabs.Alloy

# WindowsExporterの設定を流し込む
$CONFIG = @'
collectors:
  enabled: cache,cpu,cpu_info,diskdrive,gpu,license,logical_disk,memory,net,os,pagefile,physical_disk,process,remote_fx,scheduled_task,service,smbclient,system,tcp,time,udp,update
collector:
  service:
    include: windows_exporter
log:
  level: warn
'@
$CONFIG | Set-Content -Path "$env:PROGRAMFILES\windows_exporter\config.yaml" -Encoding UTF8 -NoNewline


# Alloyの設定を流し込む
$TEMPLATE = @'
// Windows用 Alloy設定
// イベントログの内容を抽出してlokiへ送る
logging {
	level  = "info"	 	// error, warn, info, debug
	format = "logfmt"	// logfmt, json
}
//========================================
// Lokiへの出力設定 ( push方式 )
loki.write "db" {
	endpoint {
		url = "LOKI_URL:3100/loki/api/v1/push"
	}
}
//========================================
//// 加工の流れ
// [各source]
// -> [loki.process.windows.receiver]
// -> [loki.write.db.receiver]
//========================================
// 各チャンネルごとに分けて取得
loki.source.windowsevent "application"  {
	//locale = 1033	// 英語表記で取得する場合はlocaleを10進数で指定する
	eventlog_name = "Application"
	labels = {
		service = "windows.application",
		channel = "Application",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "security"  {
	eventlog_name = "Security"
	labels = {
		service = "windows.security",
		channel  = "Security",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "setup"  {
	eventlog_name = "Setup"
	labels = {
		service = "windows.setup",
		channel  = "Setup",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "system"  {
	eventlog_name = "System"
	labels = {
		service = "windows.system",
		channel  = "System",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
//---------------------------------------
loki.process "windows" {
	stage.json{
		expressions = {
			event_data = "",
			event_id = "",
			eventRecordID = "",
			unit = "execution.processName",
			execution = "",
			level = "",
			levelText = "",
			message = "",
			source = "",
			timeCreated = "",
			Overwritten = "",
		}
	}
	// timestamp共通化
	stage.timestamp{
		source = "timeCreated"
		format = "2026-04-07T23:27:31.0174426Z"
	}
	// Windowsのログレベルをsyslogに準拠させる
	stage.template {
		source = "level"
		template = `{{- if eq .level "0" -}}7
			{{- else if eq .level "1" -}}2
			{{- else if eq .level "2" -}}3
			{{- else if eq .level "3" -}}4
			{{- else if eq .level "4" -}}6
			{{- else if eq .level "5" -}}7
			{{- else -}}{{- end -}}`
	}
	// Win,Linux共通コード: syslog -> loki テンプレコピペ
	stage.template {
		source = "level"
		template = `{{- if eq .level "0" -}}emerg
			{{- else if eq .level "1" -}}crit
			{{- else if eq .level "2" -}}fatal
			{{- else if eq .level "3" -}}error
			{{- else if eq .level "4" -}}warn
			{{- else if eq .level "5" -}}notice
			{{- else if eq .level "6" -}}info
			{{- else if eq .level "7" -}}debug
			{{- else }}trace{{ end -}}`
	}
	// ログの順序を整える
	// ⚠️jsonを生成するため、'{{-'無しでインデント,改行を使用するとjsonにタブスペースが入る。
	stage.template {
		source = "new_line"
		template = `{
	{{- if .computer }}"computer":	{{- toJson .computer -}}{{else}}{{end}}
	{{- if .source }},"source":	{{- toJson .source -}}{{else}}{{end}}
	{{- if .unit }},"unit":	{{- toJson .unit -}}{{else}}{{end}}
	{{- if .message }},"message":	{{- toJson .message -}}{{else}}{{end}}
	{{- if .execution }},"execution":	{{- .execution -}}	{{else}}{{end}}
	{{- if .event_data }},"event_data":	{{- toJson .event_data }}{{else}}{{end}}
	{{- if .Value }},"other":	{{- toJson .Value }}{{else}}{{end -}}	
		}`
	}
	// ログの順序を反映
	stage.output {
		source = "new_line"
	}
	// loki検索用タグ
	stage.labels {
		values = {
			level = "",
			source = "",
			unit = "",
			computer = "",
			// sourceにて宣言したラベルはここでは不要
		}
	}
	forward_to = [loki.write.db.receiver]
}
'@

# {0} に $LOKI_URL を流し込み、UTF-8(BOMなし)で保存
$TEMPLATE = $TEMPLATE.Replace("LOKI_URL", $LOKI_URL)
$TEMPLATE | Set-Content -Path "$env:PROGRAMFILES\GrafanaLabs\Alloy\config.alloy" -Encoding UTF8 -NoNewline

# サービス再起動
net stop "windows_exporter" && net start "windows_exporter"
net stop "Alloy" && net start "Alloy"
Linux用のセットアップ
bash (debian)
PUSHTO="http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
SMARTCTLPATH=/usr/sbin/smartctl	# smartctlのバイナリ位置。デフォルトはここ。
EXPORTERDIR=/usr/local/bin	# Exporterのインストール場所
NODEEX=1.11.1	# Node_exporterのバージョン。 よく更新されているため適宜変更
SMARTEX=0.14.0	# smartctl_exporterのバージョン。 半年程度で更新されている様子
# fedora: 
#ARCH=$(rpm --print-architecture 2>/dev/null || uname -m)
ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)
case "$ARCH" in
  "amd64"|"x86_64")
    BINARY="amd64" ;;  # Intel/AMDの64bit
  " i386"|"i686")
	BINARY="i386" ;;    # Intel/AMDの32bit
  "arm64"|"aarch64")
    BINARY="arm64" ;;  # Pi 3/4の64bit OS, armbian SBCsなど
  "armhf")
    BINARY="armv7" ;; # Pi 3/4の32bit OSなど
  "armv7l")
    BINARY="armv7" ;; # Pi 3/4の32bit OSなど
  "armv6l")
    BINARY="armv6" ;;  # Pi Zero/1など
  "armel")
    BINARY="armv5" ;;  # 古いarmhf系OSなど
  *)
    echo "未知のアーキテクチャ: $ARCH ですわ。手動で選んでくださいな。" && exit 1 ;;
esac
ARCH=$BINARY
# -----
# 未知だった場合は下の#消して、バイナリのアーキテクチャを入れてください。対応してれば入ります。
#ARCH=
# -----

# 取得用のアプリインストール
# Alloy,他必要アプリのインストール
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get -y install alloy ethtool mmc-utils smartmontools lm-sensors i2c-tools moreutils
sudo modprobe drivetemp
sudo modprobe eeprom_93xx46
sudo modprobe eeprom_93cx6
# もしモジュールがあれば
#echo -e "# for sensors\ndrivetemp\neeprom_93xx46\neeprom_93cx6" | sudo tee -a /etc/modules
yes | sudo sensors-detect

# -----
# Node_Exporterのサービスインストール
wget https://github.com/prometheus/node_exporter/releases/download/v$NODEEX/node_exporter-$NODEEX.linux-$ARCH.tar.gz
tar zxf node_exporter-$NODEEX.linux-$ARCH.tar.gz -O node_exporter-$NODEEX.linux-$ARCH/node_exporter > node_exporter
sudo cp node_exporter $EXPORTERDIR/node_exporter
sudo chmod +x $EXPORTERDIR/node_exporter
rm node_exporter
rm node_exporter-$NODEEX.linux-$ARCH.tar.gz
# サービス化
sudo tee /etc/systemd/system/node_exporter.service >/dev/null <<EOF
[Unit]
Description=Node Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/node_exporter.pid
ExecStart=$EXPORTERDIR/node_exporter
Environment="SCRIPT_ARGS=--collector.ethtool.device-include=.* --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+)($|/) --collector.cpu --collector.diskstats --collector.filesystem --collector.loadavg --collector.meminfo --collector.netdev --collector.netstat --collector.stat --collector.uname --collector.vmstat "

User=root
Group=root
SyslogIdentifier=node_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable node_exporter.service
sudo systemctl start node_exporter.service

# -----
# lmsensors_exporterをインストール
sudo wget https://github.com/letwir/lmsensors_exporter/releases/download/0.1.1/lmsensors_exporter-$ARCH -O $EXPORTERDIR/lmsensors_exporter 
sudo chmod +x $EXPORTERDIR/lmsensors_exporter
# サービス化
sudo tee /etc/systemd/system/lmsensors_exporter.service >/dev/null << EOF
[Unit]
Description=Lm_sensors Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/lmsensors_exporter.pid
ExecStart=$EXPORTERDIR/lmsensors_exporter

User=root
Group=root
SyslogIdentifier=lmsensors_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable lmsensors_exporter.service
sudo systemctl start lmsensors_exporter.service
# -----
# smartctl_exporterをインストール
wget https://github.com/prometheus-community/smartctl_exporter/releases/download/v$SMARTEX/smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz
tar zxf smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz -O smartctl_exporter-$SMARTEX.linux-$ARCH/smartctl_exporter > smartctl_exporter
sudo cp smartctl_exporter $EXPORTERDIR/smartctl_exporter
sudo chmod +x $EXPORTERDIR/smartctl_exporter
rm smartctl_exporter
rm smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz
# サービス化
sudo tee /etc/systemd/system/smartctl_exporter.service >/dev/null << EOF
[Unit]
Description=Smartctl_sensors Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/smartctl_exporter.pid
ExecStart=$EXPORTERDIR/smartctl_exporter
Environment="SCRIPT_ARGS=--smartctl.path=$SMARTCTLPATH --smartctl.device-include=/dev/disk/* --smartctl.scan-device-type=by-id "

User=root
Group=root
SyslogIdentifier=smartctl_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable smartctl_exporter.service
sudo systemctl start smartctl_exporter.service

# =====
# Alloyのインストール
CONFIGPATH=/etc/alloy/config.alloy
# ------------------------------
# 上で纏めてインストール済み
# ------------------------------
# alloyユーザに必要グループを付与
sudo usermod -aG docker alloy
sudo usermod -aG adm alloy
sudo usermod -aG systemd-journal alloy

# ------------------------------
#設定ファイルをヒアドキュメントで流し込む
sudo tee $CONFIGPATH > /dev/null << ENDOFFILE
// Linux用 Alloy設定
// docker.sock, journalctlを抽出してlokiへ送る
//========================================
// LOGレベル設定
logging {
	level  = "info"	 	// error, warn, info, debug
	format = "logfmt"	// logfmt, json
}
//========================================
// Lokiへの出力設定 ( push方式 )
loki.write "db" {
	endpoint {
		url = "$PUSHTO/loki/api/v1/push"
	}
}
//========================================
//// dockerログの加工流れ
// [discovery.docker.linux]
// -> [loki.source.docker.docker]
// -> [loki.process.docker.receiver]
// -> [loki.relabel.docker.receiver]
// -> [loki.write.db.receiver]
//========================================
discovery.docker "linux" {
		host = "unix:///var/run/docker.sock"
}
loki.source.docker "docker" {
		host    = "unix:///var/run/docker.sock"
		labels = {
				component = "linux.docker",
				computer  = "container",
				os = "Docker",
		}
		targets = discovery.docker.linux.targets
		forward_to = [loki.process.docker.receiver]
}
//----------------------------------------
loki.process "docker" {
		stage.docker {}
		//log, stream("stderr","stdout"), timeのみ取得可能
		stage.json {
				expressions = {
					message = "log",
					channel = "stream",
					time     = "time",
				}
		}
		// Windows系と合わせた記法に変更
		stage.replace {
				source       = "channel"
				expression = "stdout"
				replace     = "StdOutput"
		}
		stage.replace {
				source         = "channel"
				expression   = "stderr"
				replace       = "StdError"
		}
		stage.timestamp {
				source = "time"
				format = "rfc3339nano"
		}
		stage.labels {
				values = {
					channel = "channel",
					level    = "",
					// sourceにて宣言したラベルはここでは不要
				}
		}
		// クレジットカード値などの自動変換。
		stage.luhn {
					replacement = "****censored****"
		}
		forward_to = [loki.relabel.docker.receiver]
}
//----------------------------------------
loki.relabel "docker" {
	// ラベル名の加工はこちらから
	forward_to = [loki.write.db.receiver]
}

//========================================
//// journalログの加工流れ
// [source.journal "journal"]
// -> [loki.process.journal.receiver]
// -> [loki.relabel.journal.receiver]
// -> [loki.write.db.receiver]
//========================================
// matches に使用できる主な変数
//"_PID=*", // 例: 全てのプロセスIDからログを収集
//"_UID=*", // 例: 全てのユーザーIDからログを収集
//"_COMM=*", // 例: 全てのコマンドからログを収集
//"SYSLOG_IDENTIFIER=*", // 例: 全てのsyslog識別子からログを収集
// PRIORITY: 重要なログのみ収集 (0emerg, 1alert, 2crit, 3err, 4warning, 5notice, 6info)
// _TRANSPORT: トランスポートからログを収集 (syslog, journal, audit, kernel, driver, stdout)
//========================================
// ソースを各チャンネルごとに分けて取得
loki.source.journal "journal_info" {
		format_as_json = true
		matches = "_TRANSPORT=journal"
		labels  = {
			component = "linux.journal",
			channel    = "Journal",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "syslog" {
		format_as_json = true
		matches = "_TRANSPORT=syslog"
		labels  = {
			component = "linux.syslog",
			channel    = "Syslog",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "audit" {
		format_as_json = true
		matches = "_TRANSPORT=audit"
		labels  = {
			component = "linux.audit",
			channel    = "Audit",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "kernel" {
		format_as_json = true
		matches = "_TRANSPORT=kernel"
		labels  = {
			component = "linux.kernel",
			channel    = "Kernel",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "driver" {
		format_as_json = true
		matches = "_TRANSPORT=driver"
		labels  = {
			component = "linux.driver",
			channel    = "Driver",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
//----------------------------------------
loki.process "journal" {
	// 1. JournaldのJSONをパース
	// 変数はWindowsに合わせる
	// "level","computer","source","execution"{"processId","processName"},"message","event_data",
	// 最優先
	stage.json {
		expressions = {
			message       = "MESSAGE",
			level         = "PRIORITY",
			computer     = "_HOSTNAME",
			unit          = "_COMM",
			timeCreated = "_SOURCE_REALTIME_TIMESTAMP",
			// execution用
			pid         = "_PID",
			tid         = "TID",
			comm       = "_COMM",
			exe        = "_EXE",
			uid        = "_UID",
			gid        = "_GID",
			cmdline  = "_CMDLINE",
			cap        = "_CAP_EFFECTIVE",
		}
	}
	// AUDIT用
	stage.json {
		expressions = {
			audit_session   = "_AUDIT_SESSION",
			audit_login_id  = "_AUDIT_LOGINUID",
		}
	}
	// Systemd用
	stage.json {
		expressions = {
			systemd_cgroup        = "_SYSTEMD_CGROUP",
			systemd_slice         = "_SYSTEMD_SLICE",
			systemd_unit          = "_SYSTEMD_UNIT",
			systemd_user_unit     = "_SYSTEMD_USER_UNIT",
			systemd_user_slice    = "_SYSTEMD_USER_SLICE",
			systemd_session       = "_SYSTEMD_SESSION",
			systemd_uid           = "_SYSTEMD_OWNER_UID",
			systemd_invocation_id = "_SYSTEMD_INVOCATION_ID",
		}
	}
	// SELinux用
	stage.json {
		expressions = {
			selinux = "_SELINUX_CONTEXT",
		}
	}
	// Kernel用
	stage.json {
		expressions = {
			device     = "_KERNEL_DEVICE",
			subsystem = "_KERNEL_SUBSYSTEM",
		}
	}
	// Driver用
	stage.json {
		expressions = {
			udev_name = "_UDEV_SYSNAME",
			udev_node = "_UDEV_DEVNODE",
			udev_link = "_UDEV_DEVLINK",
		}
	}
	// その他あれば入るよ。無かったらそのブロック全部消えるよ。
	stage.json {
		expressions = {
			transport = "_TRANSPORT",
			stream     = "_STREAM_ID",
			linebreak = "_LINE_BREAK",
		}
	}
	stage.json {
		expressions = {
			namespace = "_NAMESPACE",
			runtime    = "_RUNTIME_SCOPE",
		}
	}
	// timestamp共通化
	stage.timestamp {
		source = "timeCreated"
		format = "unixmicro"
	}
	// 共通コード: syslog -> loki テンプレコピペ
	stage.template {
		source = "level"
		template = \`{{- if eq .level "0" -}}emerg
			{{- else if eq .level "1" -}}crit
			{{- else if eq .level "2" -}}fatal
			{{- else if eq .level "3" -}}error
			{{- else if eq .level "4" -}}warn
			{{- else if eq .level "5" -}}notice
			{{- else if eq .level "6" -}}info
			{{- else if eq .level "7" -}}debug
			{{- else }}trace{{ end -}}\`
	}
	stage.template {
		source    = "source"
		template = \`{{- if .unit }}{{ .unit -}}
			{{- else if .device }}kernel
			{{- else if .udev_node }}device
			{{- else if .audit_login_id }}audit
			{{- else }}{{ end -}}\`
	}
	stage.template {
		source   = "unit"
		template =\`{{- if .udev_name }}{{ .udev_name -}}
			{{- else if .processName }}{{ .processName -}}
			{{- else if .exe }}{{ .exe -}}
			{{- else if .cmdline }}{{ .cmdline -}}
			{{- else if .comm }}{{ .comm -}}
			{{- else }}{{ end -}}\`
	}
	// ログの順序を整える
	// hostname,processName, binary, execution, message, event_data(etc)
	// ⚠️jsonを生成するため、'{{-'無しでインデント,改行を使用するとjsonにタブスペースが入る。
	stage.template {
    source    = "new_line"
    template = \`{
			{{- if .computer }}"computer":	{{- toJson .computer }}{{ else }}{{ end -}}
			{{- if .source }},"source":	{{- toJson .source -}}{{ else }}{{ end -}}
			{{- if .unit }},"unit":	{{- toJson .unit -}}{{ else }}{{ end -}}
			{{- if .message }},"message":	{{- toJson .message -}}{{ else }}{{ end -}}
			,"execution":{
			{{- if .pid }}"processId":	{{- toJson .pid }}{{ else }}{{ end -}}
			{{- if .uid }},"processUserId":	{{- toJson .uid }}{{ else }}{{ end -}}
			{{- if .gid }},"proocessGroupId":	{{- toJson .gid }}{{ else }}{{ end -}}
			{{- if .tid }},"threadId":	{{- toJson .tid }}{{ else }}{{ end -}}
			{{- if .exe }},"processBin":	{{- toJson .exe }}{{ else }}{{ end -}}
			{{- if .cmdline }},"cmdlineName":	{{- toJson .cmdline }}{{ else }}{{ end -}}
			{{- if .comm }},"processName":	{{- toJson .comm }}{{ else }}{{ end -}}
			}
			{{- if .audit_login_id }},"audit":{
			{{- if .audit_login_id }}"loginId":	{{- toJson .audit_login_id }}{{ else }}{{end -}}
			{{- if .audit_session }},"session":	{{- toJson .audit_session }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .systemd_unit }},"systemd":{
			{{- if .systemd_unit }}"unit":	{{- toJson .systemd_unit }}{{ else }}{{end -}}
			{{- if .systemd_uid }},"uid":	{{- toJson .systemd_uid }}{{ else }}{{end -}}
			{{- if .systemd_user_unit }},"userUnit":	{{- toJson .systemd_user_unit }}{{ else }}{{end -}}
			{{- if .systemd_slice }},"slice":	{{- toJson .systemd_slice }}{{ else }}{{end -}}
			{{- if .systemd_user_slice }},"userSlice":	{{- toJson .systemd_user_slice }}{{ else }}{{end -}}
			{{- if .systemd_session }},"session":	{{- toJson .systemd_session -}}{{ else }}{{end -}}
			{{- if .systemd_invocation_id }},"invocationId":	{{- toJson .systemd_invocation_id }}{{ else }}{{end -}}
			{{- if .systemd_cgroup }},"cgroup":	{{- toJson .systemd_cgroup }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .selinux }},"SELinux":	{{- toJson .selinux -}}{{ else }}{{end -}}
			{{- if .device }},"kernel":{
			{{- if .device }}"device":	{{- toJson .device }}{{ else }}{{end -}}
			{{- if .subsystem }},"subSystem":	{{- toJson .subsystem }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .udev_node }},"udev":{
			{{- if .udev_name }}"name":	{{- toJson .udev_name }}{{ else }}{{end -}}
			{{- if .udev_node }},"node":	{{- toJson .udev_node }}{{ else }}{{end -}}
			{{- if .udev_link }},"link":	{{- toJson .udev_link }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .Value }},"event_data":	{{- toJson .Value -}}{{ else }}{{ end -}}
			}\`
	}
	// ログの順序を反映
	stage.output {
		source = "new_line"
	}
	// loki検索用タグ
	stage.labels {
		values = {
			level = "",
			source = "",
			unit = "",
			computer = "",
			// sourceにて宣言したラベルはここでは不要
		}
	}
	forward_to = [loki.relabel.journal.receiver]
}
//----------------------------------------
// 1. journalのラベル名整形
loki.relabel "journal" {
	// 追加の加工はこちらで
	forward_to = [loki.write.db.receiver]
}
ENDOFFILE

# サービス有効化
sudo systemctl enable alloy
sudo systemctl start alloy


DBコンテナと可視化サーバーの建て方

管理サーバーをDockerで建てるには

🚧🔩保管パス🚧は適宜変更。

お試し用のGrafanaもStackに入れて同時起動するように設定した。
一応Volumesで永続化処置はしてるが、
swarm環境で自動化するとボリュームも消されて永続化になってない。

bash (Dockerホスト)
#データ永続化処理
SAVEDIR=🚧🔩保管パス🚧
URLLOKI=localhost	# LokiのURL
URLVM=localhost	# VictoriaMetricsのURL
cd $SAVEDIR
echo "NatureRemoから取得したAPIキーをそのまま入れてください"
sleep 5
nano .env_remo-api


mkdir -p data/victoriametrics
mkdir -p data/vmagent

tee loki-config.yaml << EOF
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info
  grpc_server_max_concurrent_streams: 1000

common:
  instance_addr: 127.0.0.1
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

limits_config:
  metric_aggregation_enabled: true
  enable_multi_variant_queries: true

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

#pattern_ingester:
#  enabled: true
#  metric_aggregation:
#    loki_address: localhost:3100

#ruler:
#  alertmanager_url: http://localhost:9093

frontend:
  encoding: protobuf
EOF
tee vmagent.yaml <<EOF
global:
  scrape_interval: 1m

scrape_configs:
  # Node Exporter - 既存の全ノード
  - job_name: 'node'
    static_configs:
      - targets:
        - localhost:9100
        - 999.999.999.999:9182
        - ubuntu.hoge-huga.ts.net:9182

  # Remo Exporter
  - job_name: 'remo-exporter'
    metrics_path: /metrics
    static_configs:
      - targets:
        - localhost:3200

  # Navidrome
  - job_name: 'navidrome'
    metrics_path: /metrics
    static_configs:
      - targets:
        - localhost:4533

  # Lm_sensors - 各ノードを別々のjobに記載する
  - job_name: 'lmsensors-1'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - raspi1.hoge-fuga.ts.net:9165

  - job_name: 'lmsensors-2'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - proxmox.hoge-fuga.ts.net:9165

  - job_name: 'windows_exporter'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - 999.999.999.999:9182
        - windows2.hoge-huga.ts.net:9182

  # Docker/Podman監視 - caDvisor
  - job_name: 'docker'
    static_configs:
      - targets:
        - localhost:9104  # docker Main

  # LOG監視 - Loki
  - job_name: 'loki'
    metrics_path: /metrics
    static_configs:
      - targets:
        - localhost:3100  # docker Main
EOF


tee docker-compose.yaml <<EOF
services:
# --------------------------------------------------------------
# 管理用DBとprometheus exporterを収集するエージェント
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
     - GF_PLUGINS_PREINSTALL=grafana-clock-panel
    volumes:
     - grafana-storage:/var/lib/grafana
  # VictoriaMetrics instance, a single process responsible for
  # storing metrics and serve read requests.
  # Prometheus形式のメトリクスを保管する時系列DB
  victoriametrics:
    image: victoriametrics/victoria-metrics:latest
    container_name: victoriametrics
    ports:
      - "8428:8428"
    volumes:
      - $SAVEDIR/data/victoriametrics:/victoria-metrics-data
    command:
      - "--storageDataPath=/victoria-metrics-data"
      - "--httpListenAddr=:8428"
      - "--retentionPeriod=365d"
      - "--loggerLevel=INFO"
    restart: unless-stopped
    logging:
      driver: loki
      options:
        loki-url: "http://$URLLOKI:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # Collector Prometheus
  #  Metrics collector.
  #  It scrapes targets defined in --promscrape.config
  #  And forward them to --remoteWrite.url
  # VictoriaMetricsに送信するための収集エージェント
  vmagent:
    image: victoriametrics/vmagent:latest
    container_name: vmagent
    ports:
      - "8429:8429"  # vmagent自身のメトリクス用
    volumes:
      - $SAVEDIR/vmagent.yml:/etc/vmagent/vmagent.yml:ro
      - $SAVEDIR/data/vmagent:/vmagentdata
    command:
      - "-promscrape.config=/etc/vmagent/vmagent.yml"
      - "-remoteWrite.url=http://$URLVM:8428/api/v1/write"
      - "-httpListenAddr=:8429"
      - "-promscrape.suppressScrapeErrors=true"
      - "-loggerLevel=INFO"
    restart: unless-stopped
    depends_on:
      - victoriametrics
    logging:
      driver: loki
      options:
        loki-url: "http://$URLLOKI:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # LogのDB
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    command:
      - "--config.file=/mnt/config/loki-config.yaml"
    volumes:
      - $SAVEDIR/loki-config.yaml:/mnt/config/loki-config.yaml
    environment:
      - USER_ID=1000
      - GROUP_ID=1000
    logging:
      driver: loki
      options:
        loki-url: "http://$URLLOKI:3100/loki/api/v1/push"
        loki-batch-size: "400"
# --------------------------------------------------------------
# 同梱するエクスポーターコンテナ
  # Exporter1
  # NatureRemoのAPIを叩いてメトリクスを表示するコンテナ
  remo-exporter:
    container_name: remo-exporter
    image: docker.io/kenfdev/remo-exporter:branch-86627f1070d33e64107dd4354880f0097ad1af40
    ports:
      - "3200:9352"
    environment:
      OAUTH_TOKEN_FILE: '/run/secrets/api-keys'
    volumes:
       - "$SAVEDIR/.env_remo-api:/run/secrets/api-keys"
    logging:
      driver: loki
      options:
        loki-url: "http://$URLLOKI:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # Exporter2
  # コンテナやポッドの情報をメトリクスにして表示するコンテナ ( caDvisor )
  docker-exporter-cadvisor:
    container_name: cadvisor
    image: gcr.io/cadvisor/cadvisor:latest-ghcr
    ports:
      - '9104:8080'
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    restart: unless-stopped
    logging:
      driver: loki
      options:
        loki-url: "http://$URLLOKI:3100/loki/api/v1/push"
        loki-batch-size: "400"
volumes:
    grafana-storage: {}
EOF
# Loki用プラグインのインストール
sudo docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
sudo docker stack deploy VictoriaMetrics -c docker-compose.yaml

現在の監視体制

左が監視対象、右が監視サーバー。
左は11台+仮想OSがあり、合計24台程度。

各端末にAlloyとexporterをインストールし、LokiとPrometheus互換のVictoria Metricsに集約。
Grafanaからそれらを参照し可視化している。

環境

物理マシンの内訳は

* x86_64: 5
* ARM64	: 5
* MIPS	: 1

AlloyからLokiへは現在19ノード
8台はVM, LXCを監視している。

MetricsとAlloy両方備えている最低スペックはRaspberry Pi 3B+ rev2
Node_exporterとlmsensors_exporterのみであればSKS8300のOEMであるL3スイッチ(RTL9303 MIPS-34kc 1C 700MHz)。

負荷

監視サーバーマシン負荷

  • CPUはRyzen 5800Uなのでかなりの低負荷と言える。
  • この規模+他のコンテンツストリーミングアプリのコンテナを含めて7GB RAM程度。

監視サーバーIO負荷

  • I/OもSMB上のNASへDB書き込みを行っているが、2%を超えていないのでかなりの低負荷だと思われる。
  • 他コンテナと同居しているものの、Grafanaを除いて
    DB関連は14GB程度/半年
    +DB本体の格納フォルダは約17.5 GB/半年となる
    1年で約61GB越程度の計算となる。

内訳は

  • 全国の気象データを1時間に1回
  • 部屋の温度、湿度、照度、人感センサ、RemoのAPI残り使用量を10分に1回
  • 各CPU温度、電圧、ストレージSMART情報、CPU利用率、RAMの物理仮想残余量等のOS情報を1分ごとに蓄積
  • debug以外の全ログ

を集約している状況となる。

Lokiの見た目。長いので折りたたみ

Lokiの見た目


技術選定

要件

  • Grafanaに対応できて、監視対象にインストールしやすいもの
  • 宅内インフラを極力 圧迫しない構成
    (http GETはPUTより軽い処理のため)
  • 1つの画面で全部眺められると尚良し

選定

  • Prometheus エクスポーター: 大量に存在し使い易い。
    pullなのでインフラを圧迫しない
  • VictoriaMetrics + vmagent: 集計DBとしてのPrometheusは保管期間が短く、動作も遅いためモダンかつ互換性を重視した。
    • ✨️prometheus.yamlをそのまま利用できる✨️
    • メモリ消費が劇的に少なく低スペック下でも快適である。
  • Loki + Alloy: 監視対象のログ収集はpushばかりだった為どれでも良かった
    公式ではPromtailからAlloyへ移行するよう促されている為、Promtailは選定せず。

監視対象へのインストール

流れ

ExporterとAlloyをインストールし、Alloyは設定を流し込む。

  • Exporter: Alloyに集約せず、vmagentからpullでデータを引く。
  • Alloy: info以上のjouranlctl, windowsイベントビューアーの内容をpushで押す。

Windowsは全てwingetで済むが
Linuxはリポジトリの登録が必要。
インストールパッケージ自体はGithubに公開されている為、リポジトリ設定無しでも動作は可能
https://github.com/grafana/alloy/releases


Windows (管理者権限)

Exporterのインストール、設定

https://github.com/prometheus-community/windows_exporter
ここの最新版を利用する。wingetに登録されているためDLの必要はない。

Powershell
winget  install Prometheus.WindowsExporter --interactive
notepad "$env:PROGRAMFILES\windows_exporter\config.yaml"

インストール画面が出るので環境によって調整しながらインストール。
windows_exporter

必要に応じて

  • 2ページ目で自動ファイアウォール穴開け設定も可能
  • ポート、コンフィグファイルの位置はお好きなものへ変更
デフォルト設定
* Port  : tcp, 9182
* Config: "$env:PROGRAMFILES\windows_exporter\config.yaml"
windows_exporter の config.yaml 一例
collectors:
  enabled: cache,cpu,cpu_info,diskdrive,gpu,license,logical_disk,memory,net,os,pagefile,physical_disk,process,remote_fx,scheduled_task,service,smbclient,system,tcp,time,udp,update
collector:
  service:
    include: windows_exporter
log:
  level: warn

Alloy

コンフィグファイルの仕様は以下の通り。

  • #,\は単体では使えない。実質jsonパーサを使用していると思われる。
  • 正規表現のエスケープ文字は\ではなく\\とする必要がある。
PowerShell: 管理者権限
winget  install GrafanaLabs.Alloy
notepad $env:PROGRAMFILES\GrafanaLabs\Alloy\config.alloy
Alloyコンフィグ設定 (Windows)

下記のalloyはURLの指定が必要となる。
https://raw.githubusercontent.com/letwir/config/refs/heads/main/alloy/windows.alloy

以下に自動化したものを記載した。

ヒアドキュメントで流し込む際、PowerShellの環境によってインデントが消滅する。動作に支障はないが、気になる方はGithubのURLからコピーして使用してください。

Windows config.alloy流し込みコマンド
Powershell: Windows管理者権限@設定流し込みコマンド
$LOKI_URL = "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
$CONFIG_PATH = "$env:PROGRAMFILES\GrafanaLabs\Alloy\config.alloy"

# @' ... '@ 内の $ は展開されないが、LOKI_URLは後で置換する
$TEMPLATE = @'
// Windows用 Alloy設定
// イベントログの内容を抽出してlokiへ送る
logging {
	level  = "info"	 	// error, warn, info, debug
	format = "logfmt"	// logfmt, json
}
//========================================
// Lokiへの出力設定 ( push方式 )
loki.write "db" {
	endpoint {
		url = "LOKI_URL"
	}
}
//========================================
//// 加工の流れ
// [各source]
// -> [loki.process.windows.receiver]
// -> [loki.write.db.receiver]
//========================================
// 各チャンネルごとに分けて取得
loki.source.windowsevent "application"  {
	//locale = 1033	// 英語表記で取得する場合はlocaleを10進数で指定する
	eventlog_name = "Application"
	labels = {
		service = "windows.application",
		channel = "Application",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "security"  {
	eventlog_name = "Security"
	labels = {
		service = "windows.security",
		channel  = "Security",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "setup"  {
	eventlog_name = "Setup"
	labels = {
		service = "windows.setup",
		channel  = "Setup",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
loki.source.windowsevent "system"  {
	eventlog_name = "System"
	labels = {
		service = "windows.system",
		channel  = "System",
		os = "Windows",
	}
	forward_to = [loki.process.windows.receiver]
}
//---------------------------------------
loki.process "windows" {
	stage.json{
		expressions = {
			event_data = "",
			event_id = "",
			eventRecordID = "",
			unit = "execution.processName",
			execution = "",
			level = "",
			levelText = "",
			message = "",
			source = "",
			timeCreated = "",
			Overwritten = "",
		}
	}
	// timestamp共通化
	stage.timestamp{
		source = "timeCreated"
		format = "2026-04-07T23:27:31.0174426Z"
	}
	// Windowsのログレベルをsyslogに準拠させる
	stage.template {
		source = "level"
		template = `{{- if eq .level "0" -}}7
			{{- else if eq .level "1" -}}2
			{{- else if eq .level "2" -}}3
			{{- else if eq .level "3" -}}4
			{{- else if eq .level "4" -}}6
			{{- else if eq .level "5" -}}7
			{{- else -}}{{- end -}}`
	}
	// Win,Linux共通コード: syslog -> loki テンプレコピペ
	stage.template {
		source = "level"
		template = `{{- if eq .level "0" -}}emerg
			{{- else if eq .level "1" -}}crit
			{{- else if eq .level "2" -}}fatal
			{{- else if eq .level "3" -}}error
			{{- else if eq .level "4" -}}warn
			{{- else if eq .level "5" -}}notice
			{{- else if eq .level "6" -}}info
			{{- else if eq .level "7" -}}debug
			{{- else }}trace{{ end -}}`
	}
	// ログの順序を整える
	// ⚠️jsonを生成するため、'{{-'無しでインデント,改行を使用するとjsonにタブスペースが入る。
	stage.template {
		source = "new_line"
		template = `{
	{{- if .computer }}"computer":	{{- toJson .computer -}}{{else}}{{end}}
	{{- if .source }},"source":	{{- toJson .source -}}{{else}}{{end}}
	{{- if .unit }},"unit":	{{- toJson .unit -}}{{else}}{{end}}
	{{- if .message }},"message":	{{- toJson .message -}}{{else}}{{end}}
	{{- if .execution }},"execution":	{{- .execution -}}	{{else}}{{end}}
	{{- if .event_data }},"event_data":	{{- toJson .event_data }}{{else}}{{end}}
	{{- if .Value }},"other":	{{- toJson .Value }}{{else}}{{end -}}	
		}`
	}
	// ログの順序を反映
	stage.output {
		source = "new_line"
	}
	// loki検索用タグ
	stage.labels {
		values = {
			level = "",
			source = "",
			unit = "",
			computer = "",
			// sourceにて宣言したラベルはここでは不要
		}
	}
	forward_to = [loki.write.db.receiver]
}
'@

# {0} に $LOKI_URL を流し込み、UTF-8(BOMなし)で保存
$TEMPLATE = $TEMPLATE.Replace("LOKI_URL", $LOKI_URL)
$TEMPLATE | Set-Content -Path $CONFIG_PATH -Encoding UTF8 -NoNewline

# サービス再起動
net stop "Alloy" && net start "Alloy"



Linux

LinuxはExporterのバリエーションが多く、様々なものを監視できる。
Goで書かれているものが多く、一見動かない場合も少々の改修で動く。
Exporterの大半は公式のimportライブラリを使用している為、簡単に作ることが出来る。

Exporter

都会の星の数ほどあるので自宅サーバーで利用している物を紹介する。


node_exporter

ほとんどのLinuxOS情報を引っこ抜くExporter
デフォルトPort: 9100
https://github.com/prometheus/node_exporter

bash(debian)
# 必要に応じてインストール
sudo apt -y install ethtool
bash (debian, RHEL)
# Node_Exporterのサービスインストール
NODEEX=1.11.1	# よく更新されているため適宜変更
ARCH=amd64	# or arm64, mips, mips64, armv5,6,7...
EXPORTERDIR=/usr/local/bin
# -----
wget https://github.com/prometheus/node_exporter/releases/download/v$NODEEX/node_exporter-$NODEEX.linux-$ARCH.tar.gz
sudo tar zxf node_exporter-$NODEEX.linux-$ARCH.tar.gz -O node_exporter-$NODEEX.linux-$ARCH/node_exporter > node_exporter
sudo cp node_exporter $EXPORTERDIR/node_exporter
sudo chmod +x $EXPORTERDIR/node_exporter
rm node_exporter
rm node_exporter-$NODEEX.linux-$ARCH.tar.gz
# サービス化
sudo tee /etc/systemd/system/node_exporter.service >/dev/null <<EOF
[Unit]
Description=Node Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/node_exporter.pid
ExecStart=$EXPORTERDIR/node_exporter
Environment="SCRIPT_ARGS=--collector.ethtool.device-include=.* --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+)($|/) --collector.cpu --collector.diskstats --collector.filesystem --collector.loadavg --collector.meminfo --collector.netdev --collector.netstat --collector.stat --collector.uname --collector.vmstat "

User=root
Group=root
SyslogIdentifier=node_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable node_exporter.service
sudo systemctl start node_exporter.service

lmsensors_exporter

温度、ファン回転速度を引っこ抜くExporter
デフォルトPort: 9165

本家: https://github.com/mdlayher/lmsensors_exporter
本家はARM64未対応だった為、Forkして対応したものはこちら。
https://github.com/letwir/lmsensors_exporter
以下のスクリプトではFork版を使用している。

lmsensors_exporterインストールとサービス化
bash (debian)
# 取得用のアプリインストール
sudo apt -y install lm-sensors i2c-tools moreutils
sudo modprobe drivetemp
sudo modprobe eeprom_93xx46
sudo modprobe eeprom_93cx6
# もしモジュールがあれば
#echo -e "# for sensors\ndrivetemp\neeprom_93xx46\neeprom_93cx6" | sudo tee -a /etc/modules
sudo decode-dimms
yes | sudo sensors-detect
bash
# lmsensors_exporterをインストール
ARCH=amd64	# or arm64
EXPORTERDIR=/usr/local/bin
# -----
sudo wget https://github.com/letwir/lmsensors_exporter/releases/download/0.1.1/lmsensors_exporter-$ARCH -O $EXPORTERDIR/lmsensors_exporter 
sudo chmod +x $EXPORTERDIR/lmsensors_exporter
# サービス化
sudo tee /etc/systemd/system/lmsensors_exporter.service >/dev/null << EOF
[Unit]
Description=Lm_sensors Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/lmsensors_exporter.pid
ExecStart=$EXPORTERDIR/lmsensors_exporter

User=root
Group=root
SyslogIdentifier=lmsensors_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable lmsensors_exporter.service
sudo systemctl start lmsensors_exporter.service

cf.) lm_sensorsの設定

x64ではマザーボードごとに癖があるので
Arch Linuxのトラブルシュートを見ながらモジュールの設定が必要かもしれない。
ASRock製品は別型番でも同じ手法で取得できた。
また、Ryzen系のCPU温度は通常変な値になることが多いため、以下の導入をオススメする。
https://github.com/ocerman/zenpower


smartctl_exporter

ディスクのS.M.A.R.T.情報を引っこ抜くExporter
デフォルトPort: 9633
https://github.com/prometheus-community/smartctl_exporter

smartctl_exporterインストールとサービス化
bash (debian)
# 取得用のアプリインストール
sudo apt -y install mmc-utils smartmontools 
bash
# smartctl_exporterをインストール
SMARTEX=0.14.0	# 半年程度で更新されている様子
ARCH=amd64	# or arm64, mips, mips64, armv5,6,7...
EXPORTERDIR=/usr/local/bin
SMARTCTLPATH=/usr/sbin/smartctl
# -----
wget https://github.com/prometheus-community/smartctl_exporter/releases/download/v$SMARTEX/smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz
tar zxf smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz -O smartctl_exporter-$SMARTEX.linux-$ARCH/smartctl_exporter > smartctl_exporter
sudo cp smartctl_exporter $EXPORTERDIR/smartctl_exporter
sudo chmod +x $EXPORTERDIR/smartctl_exporter
rm smartctl_exporter
rm smartctl_exporter-$SMARTEX.linux-$ARCH.tar.gz
# サービス化
sudo tee /etc/systemd/system/smartctl_exporter.service >/dev/null << EOF
[Unit]
Description=Smartctl_sensors Exporter Service
After=network-online.target

[Service]
Type=simple
PIDFile=/run/smartctl_exporter.pid
ExecStart=$EXPORTERDIR/smartctl_exporter
Environment="SCRIPT_ARGS=--smartctl.path=$SMARTCTLPATH --smartctl.device-include=/dev/disk/* --smartctl.scan-device-type=by-id "

User=root
Group=root
SyslogIdentifier=smartctl_exporter 

Restart=on-failure
RemainAfterExit=no
RestartSec=100ms

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable smartctl_exporter.service
sudo systemctl start smartctl_exporter.service

これ以外にも様々なExporterがある。有名なExporterは下記で探すことも出来る。
https://github.com/orgs/prometheus/repositories?q=_exporter
https://github.com/orgs/prometheus-community/repositories?q=_exporter
https://github.com/orgs/grafana/repositories?q=_exporter

以下は一例。

変わりダネとしてはvShpereの状態、OoklaスピードテストExporterやNature RemoのAPIを叩いてメトリクス形式に変換するコンテナや、
気象庁のデータをメトリクスに変換して公開していただいている有志の方も居る。

Remo Exporterの作者はこちら。
気象庁とお部屋の相違を検知するのにいつもお世話になっている。
寒暖差アレルギーや天気性頭痛の指標としてよく使用している。
https://kenfdev.hateblo.jp/entry/2019/03/16/010805

Alloy

公式のインストール手順そのままである。
Githubに置いてあるパッケージも結局dpkgやrpmを使用するタイプなので、
リポジトリを汚したくない以外は下記手順がオススメだ。

debian系へのインストール
bash: Linuxインストールコマンド (debian系)
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get -y install alloy
# アンインストール時にはリポジトリファイルが残るので削除
#sudo rm -i /etc/apt/sources.list.d/grafana.list
RHEL系へのインストール
bash: Linuxインストールコマンド (RHEL系)
sudo dnf install gpg
wget -q -O gpg.key https://rpm.grafana.com/gpg.key
sudo dnf --import gpg.key
echo -e '[grafana]\nname=grafana\nbaseurl=https://rpm.grafana.com\nrepo_gpgcheck=1\nenabled=1\ngpgcheck=1\ngpgkey=https://rpm.grafana.com/gpg.key\nsslverify=1\nsslcacert=/etc/pki/tls/certs/ca-bundle.crt' | sudo tee /etc/yum.repos.d/grafana.repo
yum update
sudo dnf install alloy
# アンインストール時にはリポジトリファイルが残るので削除
#sudo rm -i /etc/yum.repos.d/grafana.repo
Alloy設定ファイル (Linux)

コンフィグファイルの仕様は以下の通り。

  • #,\は単体では使えない。実質jsonパーサを使用していると思われる。
  • 正規表現のエスケープ文字は\ではなく\\とする必要がある。

下記のalloyはURLの指定が必要となる。
https://raw.githubusercontent.com/letwir/config/refs/heads/main/alloy/linux.alloy

以下に自動化したものを記載した。

以下の設定コマンドではヒアドキュメントを使用するので、
\,`,はエスケープする必要がある。

Linux config.alloy流し込みコマンド
bash: Linux@設定流し込みコマンド
PUSHTO="http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
CONFIGPATH=/etc/alloy/config.alloy
# ------------------------------
# alloyユーザに必要グループを付与
## loki.source.dockerを利用してdocker.sockを読む場合
sudo usermod -aG docker alloy

## loki.source.journalを利用してjournalctlを読む場合
sudo usermod -aG adm alloy
sudo usermod -aG systemd-journal alloy


# ------------------------------
#設定ファイルをヒアドキュメントで流し込む
sudo tee $CONFIGPATH > /dev/null << ENDOFFILE
// Linux用 Alloy設定
// docker.sock, journalctlを抽出してlokiへ送る
//========================================
// LOGレベル設定
logging {
	level  = "info"	 	// error, warn, info, debug
	format = "logfmt"	// logfmt, json
}
//========================================
// Lokiへの出力設定 ( push方式 )
loki.write "db" {
	endpoint {
		url = "$PUSHTO/loki/api/v1/push"
	}
}
//========================================
//// dockerログの加工流れ
// [discovery.docker.linux]
// -> [loki.source.docker.docker]
// -> [loki.process.docker.receiver]
// -> [loki.relabel.docker.receiver]
// -> [loki.write.db.receiver]
//========================================
discovery.docker "linux" {
		host = "unix:///var/run/docker.sock"
}
loki.source.docker "docker" {
		host    = "unix:///var/run/docker.sock"
		labels = {
				component = "linux.docker",
				computer  = "container",
				os = "Docker",
		}
		targets = discovery.docker.linux.targets
		forward_to = [loki.process.docker.receiver]
}
//----------------------------------------
loki.process "docker" {
		stage.docker {}
		//log, stream("stderr","stdout"), timeのみ取得可能
		stage.json {
				expressions = {
					message = "log",
					channel = "stream",
					time     = "time",
				}
		}
		// Windows系と合わせた記法に変更
		stage.replace {
				source       = "channel"
				expression = "stdout"
				replace     = "StdOutput"
		}
		stage.replace {
				source         = "channel"
				expression   = "stderr"
				replace       = "StdError"
		}
		stage.timestamp {
				source = "time"
				format = "rfc3339nano"
		}
		stage.labels {
				values = {
					channel = "channel",
					level    = "",
					// sourceにて宣言したラベルはここでは不要
				}
		}
		// クレジットカード値などの自動変換。
		stage.luhn {
					replacement = "****censored****"
		}
		forward_to = [loki.relabel.docker.receiver]
}
//----------------------------------------
loki.relabel "docker" {
	// ラベル名の加工はこちらから
	forward_to = [loki.write.db.receiver]
}

//========================================
//// journalログの加工流れ
// [source.journal "journal"]
// -> [loki.process.journal.receiver]
// -> [loki.relabel.journal.receiver]
// -> [loki.write.db.receiver]
//========================================
// matches に使用できる主な変数
//"_PID=*", // 例: 全てのプロセスIDからログを収集
//"_UID=*", // 例: 全てのユーザーIDからログを収集
//"_COMM=*", // 例: 全てのコマンドからログを収集
//"SYSLOG_IDENTIFIER=*", // 例: 全てのsyslog識別子からログを収集
// PRIORITY: 重要なログのみ収集 (0emerg, 1alert, 2crit, 3err, 4warning, 5notice, 6info)
// _TRANSPORT: トランスポートからログを収集 (syslog, journal, audit, kernel, driver, stdout)
//========================================
// ソースを各チャンネルごとに分けて取得
loki.source.journal "journal_info" {
		format_as_json = true
		matches = "_TRANSPORT=journal"
		labels  = {
			component = "linux.journal",
			channel    = "Journal",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "syslog" {
		format_as_json = true
		matches = "_TRANSPORT=syslog"
		labels  = {
			component = "linux.syslog",
			channel    = "Syslog",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "audit" {
		format_as_json = true
		matches = "_TRANSPORT=audit"
		labels  = {
			component = "linux.audit",
			channel    = "Audit",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "kernel" {
		format_as_json = true
		matches = "_TRANSPORT=kernel"
		labels  = {
			component = "linux.kernel",
			channel    = "Kernel",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
loki.source.journal "driver" {
		format_as_json = true
		matches = "_TRANSPORT=driver"
		labels  = {
			component = "linux.driver",
			channel    = "Driver",
			os = "Linux",
		}
		forward_to = [loki.process.journal.receiver]
}
//----------------------------------------
loki.process "journal" {
	// 1. JournaldのJSONをパース
	// 変数はWindowsに合わせる
	// "level","computer","source","execution"{"processId","processName"},"message","event_data",
	// 最優先
	stage.json {
		expressions = {
			message       = "MESSAGE",
			level         = "PRIORITY",
			computer     = "_HOSTNAME",
			unit          = "_COMM",
			timeCreated = "_SOURCE_REALTIME_TIMESTAMP",
			// execution用
			pid         = "_PID",
			tid         = "TID",
			comm       = "_COMM",
			exe        = "_EXE",
			uid        = "_UID",
			gid        = "_GID",
			cmdline  = "_CMDLINE",
			cap        = "_CAP_EFFECTIVE",
		}
	}
	// AUDIT用
	stage.json {
		expressions = {
			audit_session   = "_AUDIT_SESSION",
			audit_login_id  = "_AUDIT_LOGINUID",
		}
	}
	// Systemd用
	stage.json {
		expressions = {
			systemd_cgroup        = "_SYSTEMD_CGROUP",
			systemd_slice         = "_SYSTEMD_SLICE",
			systemd_unit          = "_SYSTEMD_UNIT",
			systemd_user_unit     = "_SYSTEMD_USER_UNIT",
			systemd_user_slice    = "_SYSTEMD_USER_SLICE",
			systemd_session       = "_SYSTEMD_SESSION",
			systemd_uid           = "_SYSTEMD_OWNER_UID",
			systemd_invocation_id = "_SYSTEMD_INVOCATION_ID",
		}
	}
	// SELinux用
	stage.json {
		expressions = {
			selinux = "_SELINUX_CONTEXT",
		}
	}
	// Kernel用
	stage.json {
		expressions = {
			device     = "_KERNEL_DEVICE",
			subsystem = "_KERNEL_SUBSYSTEM",
		}
	}
	// Driver用
	stage.json {
		expressions = {
			udev_name = "_UDEV_SYSNAME",
			udev_node = "_UDEV_DEVNODE",
			udev_link = "_UDEV_DEVLINK",
		}
	}
	// その他あれば入るよ。無かったらそのブロック全部消えるよ。
	stage.json {
		expressions = {
			transport = "_TRANSPORT",
			stream     = "_STREAM_ID",
			linebreak = "_LINE_BREAK",
		}
	}
	stage.json {
		expressions = {
			namespace = "_NAMESPACE",
			runtime    = "_RUNTIME_SCOPE",
		}
	}
	// timestamp共通化
	stage.timestamp {
		source = "timeCreated"
		format = "unixmicro"
	}
	// 共通コード: syslog -> loki テンプレコピペ
	stage.template {
		source = "level"
		template = \`{{- if eq .level "0" -}}emerg
			{{- else if eq .level "1" -}}crit
			{{- else if eq .level "2" -}}fatal
			{{- else if eq .level "3" -}}error
			{{- else if eq .level "4" -}}warn
			{{- else if eq .level "5" -}}notice
			{{- else if eq .level "6" -}}info
			{{- else if eq .level "7" -}}debug
			{{- else }}trace{{ end -}}\`
	}
	stage.template {
		source    = "source"
		template = \`{{- if .unit }}{{ .unit -}}
			{{- else if .device }}kernel
			{{- else if .udev_node }}device
			{{- else if .audit_login_id }}audit
			{{- else }}{{ end -}}\`
	}
	stage.template {
		source   = "unit"
		template =\`{{- if .udev_name }}{{ .udev_name -}}
			{{- else if .processName }}{{ .processName -}}
			{{- else if .exe }}{{ .exe -}}
			{{- else if .cmdline }}{{ .cmdline -}}
			{{- else if .comm }}{{ .comm -}}
			{{- else }}{{ end -}}\`
	}
	// ログの順序を整える
	// hostname,processName, binary, execution, message, event_data(etc)
	// ⚠️jsonを生成するため、'{{-'無しでインデント,改行を使用するとjsonにタブスペースが入る。
	stage.template {
    source    = "new_line"
    template = \`{
			{{- if .computer }}"computer":	{{- toJson .computer }}{{ else }}{{ end -}}
			{{- if .source }},"source":	{{- toJson .source -}}{{ else }}{{ end -}}
			{{- if .unit }},"unit":	{{- toJson .unit -}}{{ else }}{{ end -}}
			{{- if .message }},"message":	{{- toJson .message -}}{{ else }}{{ end -}}
			,"execution":{
			{{- if .pid }}"processId":	{{- toJson .pid }}{{ else }}{{ end -}}
			{{- if .uid }},"processUserId":	{{- toJson .uid }}{{ else }}{{ end -}}
			{{- if .gid }},"proocessGroupId":	{{- toJson .gid }}{{ else }}{{ end -}}
			{{- if .tid }},"threadId":	{{- toJson .tid }}{{ else }}{{ end -}}
			{{- if .exe }},"processBin":	{{- toJson .exe }}{{ else }}{{ end -}}
			{{- if .cmdline }},"cmdlineName":	{{- toJson .cmdline }}{{ else }}{{ end -}}
			{{- if .comm }},"processName":	{{- toJson .comm }}{{ else }}{{ end -}}
			}
			{{- if .audit_login_id }},"audit":{
			{{- if .audit_login_id }}"loginId":	{{- toJson .audit_login_id }}{{ else }}{{end -}}
			{{- if .audit_session }},"session":	{{- toJson .audit_session }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .systemd_unit }},"systemd":{
			{{- if .systemd_unit }}"unit":	{{- toJson .systemd_unit }}{{ else }}{{end -}}
			{{- if .systemd_uid }},"uid":	{{- toJson .systemd_uid }}{{ else }}{{end -}}
			{{- if .systemd_user_unit }},"userUnit":	{{- toJson .systemd_user_unit }}{{ else }}{{end -}}
			{{- if .systemd_slice }},"slice":	{{- toJson .systemd_slice }}{{ else }}{{end -}}
			{{- if .systemd_user_slice }},"userSlice":	{{- toJson .systemd_user_slice }}{{ else }}{{end -}}
			{{- if .systemd_session }},"session":	{{- toJson .systemd_session -}}{{ else }}{{end -}}
			{{- if .systemd_invocation_id }},"invocationId":	{{- toJson .systemd_invocation_id }}{{ else }}{{end -}}
			{{- if .systemd_cgroup }},"cgroup":	{{- toJson .systemd_cgroup }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .selinux }},"SELinux":	{{- toJson .selinux -}}{{ else }}{{end -}}
			{{- if .device }},"kernel":{
			{{- if .device }}"device":	{{- toJson .device }}{{ else }}{{end -}}
			{{- if .subsystem }},"subSystem":	{{- toJson .subsystem }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .udev_node }},"udev":{
			{{- if .udev_name }}"name":	{{- toJson .udev_name }}{{ else }}{{end -}}
			{{- if .udev_node }},"node":	{{- toJson .udev_node }}{{ else }}{{end -}}
			{{- if .udev_link }},"link":	{{- toJson .udev_link }}{{ else }}{{end -}}
			}{{ else }}{{end -}}
			{{- if .Value }},"event_data":	{{- toJson .Value -}}{{ else }}{{ end -}}
			}\`
	}
	// ログの順序を反映
	stage.output {
		source = "new_line"
	}
	// loki検索用タグ
	stage.labels {
		values = {
			level = "",
			source = "",
			unit = "",
			computer = "",
			// sourceにて宣言したラベルはここでは不要
		}
	}
	forward_to = [loki.relabel.journal.receiver]
}
//----------------------------------------
// 1. journalのラベル名整形
loki.relabel "journal" {
	// 追加の加工はこちらで
	forward_to = [loki.write.db.receiver]
}
ENDOFFILE

# サービス有効化
sudo systemctl enable alloy
sudo systemctl start alloy


Alloyの設定について

凝ったことをしようとしたらドキュメントが必要になる。
Alloy全体の設定はこちら。
https://grafana.com/docs/alloy/latest/reference/config-blocks/
各データを抽出して加工する部分はこちら。
https://grafana.com/docs/alloy/latest/reference/components/

意外にもGeminiやGPTはGrafana Alloyについてやけに詳しい。
Promtailの頃の知識を披露してくれて2割くらいは壊れるが
十分活用できるので、やりたいことを伝えて自作するのも良い。

cf.) Alloy設定トラブルシュート

設定中に構文が間違っていたりするとWindowsではもちろんのこと、journalctlでも
何が間違っているのかを表示してくれない。
そこで、デバッグ用に使えるコマンドとWindowsで見るべきポイントを紹介していく。

Linux
bash systemdが落ちている時に動かすべきコマンド。
sudo /usr/bin/alloy run --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy

これにより、何行目のどこがどうなってエラーと表示してくれるので、
困ったらこのコマンドを多用しよう(168敗)
もし、送るべきログが無くて動いてるかどうかわからない時は以下のコマンドで意味のないログを送れる。

# journalctl経由
logger -t my-test-tag "Alloy Linux Connection Test: Hello from $(hostname)"
# syslog経由
echo "$(date) Alloy file-source test log" | sudo tee -a /var/log/alloy-test.log
Windows

こちらは特に見づらい所にある。
まずはWinキーを右クリックしてイベントマネージャを起動する。
イベントマネージャの起動
そうしたら左パネルより、Windowsログ -> Applicationと進み、Alloyのエラーから一つ下の情報をクリックすると原因がLinux同様書かれている。
WinEvent画面

Linuxでは

systemd restart alloy

で済むのだが、WindowsはキーボードのWinキーを押して「service」と打つ。
「サービス」が表示されたら押して起動する。
alt text
起動後は名前順で上位にAlloyがある筈なので、右クリックから再起動だ。

管理者権限でターミナルを開く方法もある。

powershell管理者権限
net stop "Alloy" && net start "Alloy"

収集DB用サーバーの設定 (VictoriaMetrics, vmagent, Loki, Grafana)

ここではDockerを用いてVictoriaMetrics, vmagent, Lokiをコンテナとして運用する。

いつの頃からか、Grafanaのコンテナの設定を永続化させようとすると起動に失敗する現象が発生している。

設定をdocker stopしてからexportしてtarに纏めて設定フォルダを抜けばいいのだが、
そんな手間を毎回行うのはコンテナの利点を享受できてないに等しい。

そこで、GrafanaはDockerホストに直接インストールしている。
パッケージ管理システムでアップデートも可能なので現状はコンテナ運用より楽である。

コンテナ永続化準備

bash(データ永続化処理)
cd 🚧🔩保管パス🚧
mkdir -p data/victoriametrics
mkdir -p data/vmagent

touch .env_remo-api	# NatureRemoから取得したAPIキー 
touch loki-config.yaml	# Lokiのコンフィグ
touch vmagent.yaml	# ExporterのURL設定

設定: loki-config.yaml

以下は設定ファイル例。各環境に合わせて変更して使用してほしい。

loki-config.yaml例
loki-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info
  grpc_server_max_concurrent_streams: 1000

common:
  instance_addr: 127.0.0.1
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

limits_config:
  metric_aggregation_enabled: true
  enable_multi_variant_queries: true

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

#pattern_ingester:
#  enabled: true
#  metric_aggregation:
#    loki_address: docker.tigris-tailor.ts.net:3100

#ruler:
#  alertmanager_url: http://localhost:9093

frontend:
  encoding: protobuf


# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration
# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/
#
# Statistics help us better understand how Loki is used, and they show us performance
# levels for most users. This helps us prioritize features and documentation.
# For more information on what's sent, look at
# https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go
# Refer to the buildReport method to see what goes into a report.
#

# If you would like to disable reporting, uncomment the following lines:
#analytics:
#  reporting_enabled: false

設定: vmagent.yaml

実はprometheus.yamlをそのままコピペして良い。

但し、prometheus同様、lmsensors_exporterは何故か複数ターゲットを書いたら認識しない為
job_nameを変えて一個ずつ書くしか無いようだ。

vmagent.yaml例
vmagent.yaml
# vmagent.yml - 既存Prometheus設定からの移行版
global:
  scrape_interval: 1m

scrape_configs:
  # Node Exporter - 既存の全ノード
  - job_name: 'node'
    static_configs:
      - targets:
        # PVE
        - node-host1:9100
        - node-host2:9100

  # Remo Exporter
  - job_name: 'remo-exporter'
    metrics_path: /metrics
    static_configs:
      - targets:
        - remo-container:3200

  # Navidrome
  - job_name: 'navidrome'
    metrics_path: /metrics
    static_configs:
      - targets:
        - navidrome-host:4533

  # Unbound
  - job_name: 'unbound'
    static_configs:
      - targets:
        - unbound_exporter-host:9167

  # Lm_sensors - 各ノード別
  - job_name: 'lmsensors-1'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - sensors-host1:9165

  - job_name: 'lmsensors-2'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - sensors-host2:9165

# SMART Control Exporter
  - job_name: 'smartctl_exporter'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - smart-host1:9633
        - smart-host2:9633

  - job_name: 'windows_exporter'
    scrape_interval: 1m
    metrics_path: /metrics
    static_configs:
      - targets:
        - windows1:9182
        - windows2:9182

  # Docker/Podman監視 - caDvisor
  - job_name: 'docker'
    static_configs:
      - targets:
        - docker-host:9104  # docker Main

  # LOG監視 - Loki
  - job_name: 'loki'
    metrics_path: /metrics
    static_configs:
      - targets:
        - docker-host:3100  # docker Main

  # speedtest_exporter
  - job_name: 'speedtest_exporter'
    metrics_path: /metrics
    scrape_interval: 120m
    scrape_timeout:  240s
    static_configs:
    - targets:
      - speedtest-host:9101

以下の設定にはGrafanaは入っていない。

また、Lokiに永続化処理はしていない。ログは都度読む時だけ保管できれば良いと考えている。
反面、計器情報や統計を取るためのメトリクスはDBで保管している。

target、loki-urlのURL部分は
tailscaleや公開されているWebのURLでも問題なく稼働する。

docker-compose.yaml例
docker-compose.yaml
services:
# --------------------------------------------------------------
# 管理用DBとprometheus exporterを収集するエージェント
  # VictoriaMetrics instance, a single process responsible for
  # storing metrics and serve read requests.
  # Prometheus形式のメトリクスを保管する時系列DB
  victoriametrics:
    image: victoriametrics/victoria-metrics:latest
    container_name: victoriametrics
    ports:
      - "8428:8428"
    volumes:
      - 🚧🔩保管パス🚧/data/victoriametrics:/victoria-metrics-data
    command:
      - "--storageDataPath=/victoria-metrics-data"
      - "--httpListenAddr=:8428"
      - "--retentionPeriod=365d"
      - "--loggerLevel=INFO"
    restart: unless-stopped
    logging:
      driver: loki
      options:
        loki-url: "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # Collector Prometheus
  #  Metrics collector.
  #  It scrapes targets defined in --promscrape.config
  #  And forward them to --remoteWrite.url
  # VictoriaMetricsに送信するための収集エージェント
  vmagent:
    image: victoriametrics/vmagent:latest
    container_name: vmagent
    ports:
      - "8429:8429"  # vmagent自身のメトリクス用
    volumes:
      - 🚧🔩保管パス🚧/vmagent.yml:/etc/vmagent/vmagent.yml:ro
      - 🚧🔩保管パス🚧/data/vmagent:/vmagentdata
    command:
      - "-promscrape.config=/etc/vmagent/vmagent.yml"
      - "-remoteWrite.url=http://$URLVM:8428/api/v1/write"
      - "-httpListenAddr=:8429"
      - "-promscrape.suppressScrapeErrors=true"
      - "-loggerLevel=INFO"
    restart: unless-stopped
    depends_on:
      - victoriametrics
    logging:
      driver: loki
      options:
        loki-url: "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # LogのDB
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    command:
      - "--config.file=/mnt/config/loki-config.yaml"
    volumes:
      - 🚧🔩保管パス🚧/loki-config.yaml:/mnt/config/loki-config.yaml
    environment:
      - USER_ID=1000
      - GROUP_ID=1000
    logging:
      driver: loki
      options:
        loki-url: "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
        loki-batch-size: "400"
# --------------------------------------------------------------
# 同梱するエクスポーターコンテナ
  # Exporter1
  # NatureRemoのAPIを叩いてメトリクスを表示するコンテナ
  remo-exporter:
    container_name: remo-exporter
    image: docker.io/kenfdev/remo-exporter:branch-86627f1070d33e64107dd4354880f0097ad1af40
    ports:
      - "3200:9352"
    environment:
      OAUTH_TOKEN_FILE: '/run/secrets/api-keys'
    volumes:
       - "🚧🔩保管パス🚧/.env_remo-api:/run/secrets/api-keys"
    logging:
      driver: loki
      options:
        loki-url: "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
        loki-batch-size: "400"
  # Exporter2
  # コンテナやポッドの情報をメトリクスにして表示するコンテナ ( caDvisor )
  docker-exporter-cadvisor:
    container_name: cadvisor
    image: gcr.io/cadvisor/cadvisor:latest-ghcr
    ports:
      - '9104:8080'
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    restart: unless-stopped
    logging:
      driver: loki
      options:
        loki-url: "http://🚧🔩lokiのURL🚧:3100/loki/api/v1/push"
        loki-batch-size: "400"

🚧🔩保管パス🚧をSMBやNFSにマウントしたフォルダに設定している場合、
全てのノードが同じフォルダパスで利用できる状況であれば、
Docker Swarmでコンテナを分散利用していても正常稼働する。

但し、冗長構成になっていない(vmagentからの送り先が常に特定のノードになる)為、単独運用か、DBのみバックアップ運用が良いと思われる。

vmagent自体は複数のDBに送信する機能があるし、
公式曰く、シングルノードでも秒間100万サンプルレートに耐えうる。

複数DBへの対応はremote-writeを複数行書くだけである。

bash (dockerコマンド)
# Loki用プラグインのインストール
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions

# デプロイ
docker compose up -d
# or 自動化するなら
docker stack deploy VictoriaMetrics -c docker-compose.yaml

可視化

Grafanaのインストール

公式そのままの方法。

ほとんどのユーザーが試用以外ではLinuxにインストールすると思われる為、
主な2種類について紹介している。
公式にはWindows,Mac,SUSE,Dockerなど多様な方法が載っている。
https://grafana.com/docs/grafana/latest/setup-grafana/installation/
が、Docker版については"$PWD/data:/var/lib/grafana"が機能していない為永続化は不可能に近い。

Grafanaのインストール (debian)
bash (debian)
# Grafanaリポジトリ追加
sudo apt-get install -y apt-transport-https wget gnupg
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc

echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
# or beta
#echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com beta main" | sudo tee -a /etc/apt/sources.list.d/grafana.list

# インストール
sudo apt-get update
sudo apt-get install grafana
# or Enterprise
#sudo apt-get install grafana-enterprise
Grafanaのインストール (RHEL)
bash (RHEL)
# Grafanaリポジトリ追加
wget -q -O gpg.key https://rpm.grafana.com/gpg.key
sudo rpm --import gpg.key

sudo tee /etc/yum.repos.d/grafana.repo << EOF
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF

# インストール
sudo dnf install grafana
# or Enterprise
#sudo dnf install grafana-enterprise

Grafanaの設定

ブラウザで次にアクセス: http://🚧🔩GrafanaのURL🚧:3000/login
Grafana-login
インストール直後にWebGUIにアクセスするとIDとパスワードを求められるが

username: admin
password: admin

でパスワード変更画面へ移行する。

その後右上のprofileからユーザー名、言語設定やタイムゾーンの設定が行える。
ログイン時にずっとadminでは不安だろうので変更をオススメする。
Grafana-login
次にデータソースの追加を行う。左上の->つながり下のデータソースを押す。
Grafana-datasource
右上黄色の+ 新しいデータソースを追加を押し、必要なものを追加していく。
ここでのオススメは Prometheus -> VictoriaMetrics -> Loki の順だ。

最初に追加したソースがデフォルトとなるので、
最もダッシュボードで使われているPrometheusから登録すると使いやすい。

Grafana-dataaddP
各項目について、

  • 名前: 分かれば何でもいいので初回はデフォルトでもいいだろう。
  • Prometheus server URL *: http://🚧🔩VictoriaMetricsのURL🚧:8428
  • Authentication: DBを認証式に設定している場合はここも設定する。
  • Advanced settings: 残り全てデフォルトで問題ない。取得件数でチューニングする際に操作する。
    保存してテストを押してエラーが無ければ登録完了。

同様にVictoriaMetricsのデータソースも追加していく。
http://🚧🔩GrafanaのURL🚧:3000/plugins/victoriametrics-metrics-datasource
にアクセスしてインストールで良い。
Grafana-dataaddV
インストール後は上記Prometheusと同じ手順でVictoriaMetricsを選択し、URLに入力したら保存してテストを押す。

TIPS: オススメのGrafanaプラグイン

Lokiのドリルダウン(自動可視化サンプル)追加: http://🚧🔩GrafanaのURL🚧:3000/plugins/grafana-lokiexplore-app
QRcode生成: http://🚧🔩GrafanaのURL🚧:3000/plugins/betatech-qrcode-panel

ダッシュボードの追加方法

GrafanaBoardADD
/dashboardsをURLに追加すると一覧が表示される。
そこから新規 -> インポートと押す。

https://grafana.com/grafana/dashboards/

ここに人気のダッシュボードがあるので、好きなものをクリックする。

画面右側のCopy ID Clipboardを押してコピーしたら
grafana-import
ダッシュボード一覧画面右上の新規->インポート画面から読み込むを押すだけで追加される。

Githubに公開されているjsonは下部の大きめのウィンドウに貼り付けて読み込むで取り込まれる。
往々にして設定がぐちゃぐちゃなことが多いため、都度jsonのuid部分を手修正する必要がある。

Grafanaダッシュボード紹介

以下は使っている公開ダッシュボード。
https://grafana.com/grafana/dashboards/20763-windows-exporter-dashboard-2024/
https://grafana.com/grafana/dashboards/18267-prometheus-server/
https://grafana.com/grafana/dashboards/22604-smartctl-exporter-dashboard/
https://grafana.com/grafana/dashboards/16656-infrastructures/
https://grafana.com/grafana/dashboards/24397-navidrome-observability/
https://grafana.com/grafana/dashboards/8919-node-exporter-dashboard-20240520-tensuns/
上のNode-exporterを機械翻訳で和訳しました
https://raw.githubusercontent.com/letwir/config/refs/heads/main/Grafana/Dashboard-NodeExporter_和訳mod.json

私が作ったものもあるので試してみても良いと思う。
VictoriaMetrics、Prometheus、Lokiが混在しているので稚拙な作品ではあるが。
総合ダッシュボード

https://raw.githubusercontent.com/letwir/config/refs/heads/main/Grafana/Dashboard-Sougou.json

アメダスのメトリクスを提供してくださってる@miiton様に深く感謝。
当設定では1時間に1回の取得としている。
個人負担のサービスなので頻回取得しないように。

Lokiのクエリ例

可視化の際、迷うのはクエリ方法かと思う。
特にjson形式のkeyからどうやって取り出すかは情報が少なく困った。
のでここに記す。

Loki Query
{ level=~"error|warn|fatal|crit|emerg" }
| json
| line_format "{{.computer}}\t{{.unit}}:\t{{.message}}"

クエリでは改行は無視される。なので視認性の為に改行して記してある。
まずは{}内にprocess部分で指定したstage.labelsや、source部分で指定したlebelsでのkeyから抽出する。
この記事の設定では以下が使用可能。
:::detail 使用可能ラベル

  • OS周り: service, source, unit, host, component, os, job, level, levelText(Windowsのみ), computer, channel
    Dockerのログは"Windows"、"Linux"から切り離して"Docker"という値で入っている。
  • Docker周り: filename, container_name, swarm_stack, swarm_service
    :::
    次に|でjsonへパースすることで、{{.key}}形式で書くことにより値を取り出すことが出来る。
    これをline_format ""内に書くことで表示内容を絞ることが出来る。
    {{}}の部分はgoのテンプレートフォーマットに則っているので、
Loki Query
{{if .computer}} {{else if .source}} {{else}} {{end}}

等の制御記号も記載可能である。
詳しいドキュメントは下へ。
https://grafana.com/docs/loki/latest/query/template_functions/
https://pkg.go.dev/text/template

あとがき

超々長い文章ここまで本当にお疲れ様でした。

ダッシュボードは美的センスが問われてとても辛い。
自分が分かるようにしているが、友人曰く「何がなんやら分かんないっピ!」とのこと。

それはそうかも。最大温度さえ分かればいいかなって気持ちとサイバーな気分になりたいからグラフ化してるところがある。

もっとカッコいいダッシュボードを作れたら是非Github等に上げて頂いてもろて。
私も使用するので是非教えてほしい。

Discussion

YotaHamadaYotaHamada

自宅サーバーの数に驚きました。Grafana かっこいい... Lokiを使えばログも集められるんですね。