feat(smartctl): add smartctl exporter

This commit is contained in:
il
2026-06-30 15:57:29 +09:00
parent 88e85fff56
commit 3c8469e604
19 changed files with 713 additions and 114 deletions
@@ -0,0 +1,11 @@
[Unit]
Description=SMART Metric generator
[Service]
Type=oneshot
StandardOutput=journal
StandardError=journal
# Run the script
ExecStart=/usr/local/sbin/smart_metric_generator.sh
@@ -0,0 +1,352 @@
#!/bin/bash
set -euo pipefail
out_dir="${OUT_DIR:-/etc/smartctl-exporter}"
smartctl_bin="${SMARTCTL:-/usr/sbin/smartctl}"
tmp="$(mktemp "${out_dir}/smartctl.prom.XXXXXX")"
out="${out_dir}/smartctl.prom"
cleanup() {
rm -f "$tmp"
}
trap cleanup EXIT
discover_devices() {
for sys in /sys/class/block/*; do
dev="${sys##*/}"
[ -e "$sys/device" ] || continue
[ ! -e "$sys/partition" ] || continue
case "$dev" in
loop*|ram*|zram*|dm-*|md*|nbd*|sr*|fd*)
continue
;;
esac
sys_path="$(readlink -f "$sys" 2>/dev/null || true)"
case "$sys_path" in
*usb*)
continue
;;
*/virtual/*)
continue
;;
esac
[ -b "/dev/$dev" ] || continue
printf '%s\n' "/dev/$dev"
done | sort -V
}
write_header() {
cat <<'HDR'
# HELP smartctl_version smartctl version
# TYPE smartctl_version gauge
# HELP smartctl_device Device info
# TYPE smartctl_device gauge
# HELP smartctl_devices Number of devices configured or dynamically discovered
# TYPE smartctl_devices gauge
# HELP smartctl_device_capacity_blocks Device capacity in blocks
# TYPE smartctl_device_capacity_blocks gauge
# HELP smartctl_device_capacity_bytes Device capacity in bytes
# TYPE smartctl_device_capacity_bytes gauge
# HELP smartctl_device_nvme_capacity_bytes NVMe device total capacity bytes
# TYPE smartctl_device_nvme_capacity_bytes gauge
# HELP smartctl_device_block_size Device block size
# TYPE smartctl_device_block_size gauge
# HELP smartctl_device_interface_speed Device interface speed, bits per second
# TYPE smartctl_device_interface_speed gauge
# HELP smartctl_device_attribute Device attributes
# TYPE smartctl_device_attribute gauge
# HELP smartctl_device_power_on_seconds Device power on seconds
# TYPE smartctl_device_power_on_seconds counter
# HELP smartctl_device_rotation_rate Device rotation rate
# TYPE smartctl_device_rotation_rate gauge
# HELP smartctl_device_temperature Device temperature celsius
# TYPE smartctl_device_temperature gauge
# HELP smartctl_device_power_cycle_count Device power cycle count
# TYPE smartctl_device_power_cycle_count counter
# HELP smartctl_device_percentage_used Device write percentage used
# TYPE smartctl_device_percentage_used counter
# HELP smartctl_device_available_spare Normalized percentage of the remaining spare capacity available
# TYPE smartctl_device_available_spare counter
# HELP smartctl_device_available_spare_threshold Available spare threshold
# TYPE smartctl_device_available_spare_threshold counter
# HELP smartctl_device_critical_warning Critical warnings for the state of the controller
# TYPE smartctl_device_critical_warning counter
# HELP smartctl_device_media_errors Media/data integrity errors
# TYPE smartctl_device_media_errors counter
# HELP smartctl_device_num_err_log_entries Number of error information log entries
# TYPE smartctl_device_num_err_log_entries counter
# HELP smartctl_device_bytes_read Device bytes read
# TYPE smartctl_device_bytes_read counter
# HELP smartctl_device_bytes_written Device bytes written
# TYPE smartctl_device_bytes_written counter
# HELP smartctl_device_smart_status General smart status
# TYPE smartctl_device_smart_status gauge
# HELP smartctl_device_smartctl_exit_status Exit status of smartctl on device
# TYPE smartctl_device_smartctl_exit_status gauge
# HELP smartctl_device_state Device state
# TYPE smartctl_device_state gauge
# HELP smartctl_device_statistics Device statistics
# TYPE smartctl_device_statistics gauge
# HELP smartctl_device_error_log_count Device SMART error log count
# TYPE smartctl_device_error_log_count gauge
# HELP smartctl_device_self_test_log_count Device SMART self test log count
# TYPE smartctl_device_self_test_log_count gauge
# HELP smartctl_device_self_test_log_error_count Device SMART self test log error count
# TYPE smartctl_device_self_test_log_error_count gauge
# HELP smartctl_device_erc_seconds Device SMART Error Recovery Control Seconds
# TYPE smartctl_device_erc_seconds gauge
# HELP smartctl_scsi_grown_defect_list Device SCSI grown defect list counter
# TYPE smartctl_scsi_grown_defect_list gauge
# HELP smartctl_read_errors_corrected_by_rereads_rewrites Read Errors Corrected by ReReads/ReWrites
# TYPE smartctl_read_errors_corrected_by_rereads_rewrites gauge
# HELP smartctl_read_errors_corrected_by_eccfast Read Errors Corrected by ECC Fast
# TYPE smartctl_read_errors_corrected_by_eccfast gauge
# HELP smartctl_read_errors_corrected_by_eccdelayed Read Errors Corrected by ECC Delayed
# TYPE smartctl_read_errors_corrected_by_eccdelayed gauge
# HELP smartctl_read_total_uncorrected_errors Read Total Uncorrected Errors
# TYPE smartctl_read_total_uncorrected_errors gauge
# HELP smartctl_write_errors_corrected_by_rereads_rewrites Write Errors Corrected by ReReads/ReWrites
# TYPE smartctl_write_errors_corrected_by_rereads_rewrites gauge
# HELP smartctl_write_errors_corrected_by_eccfast Write Errors Corrected by ECC Fast
# TYPE smartctl_write_errors_corrected_by_eccfast gauge
# HELP smartctl_write_errors_corrected_by_eccdelayed Write Errors Corrected by ECC Delayed
# TYPE smartctl_write_errors_corrected_by_eccdelayed gauge
# HELP smartctl_write_total_uncorrected_errors Write Total Uncorrected Errors
# TYPE smartctl_write_total_uncorrected_errors gauge
HDR
}
emit_version() {
jq -r '
def esc: tostring | gsub("\\\\";"\\\\") | gsub("\"";"\\\"");
(.json_format_version // [0,0]) as $jv
| (.smartctl.version // [0,0]) as $sv
| "smartctl_version{json_format_version=\"\($jv[0]).\($jv[1])\",smartctl_version=\"\($sv[0]).\($sv[1])\",svn_revision=\"\((.smartctl.svn_revision // "")|esc)\",build_info=\"\((.smartctl.build_info // "")|esc)\"} 1"
'
}
emit_metrics() {
jq -r '
def esc: tostring | gsub("\\\\";"\\\\") | gsub("\"";"\\\"");
def lbl($pairs): $pairs | map("\(.[0])=\"\(.[1]|esc)\"") | join(",");
def n($x):
if $x == true then 1
elif $x == false then 0
elif $x == null then 0
else $x
end;
def emit($name; $pairs; $value):
"\($name){\(lbl($pairs))} \(n($value))";
def emit_if_exists($name; $pairs; $value):
if $value == null then empty else emit($name; $pairs; $value) end;
def trimstr: tostring | gsub("^\\s+|\\s+$";"");
def build_device_label($name; $type):
($name | sub("^/dev/(disk/by-id/|disk/by-path/|)"; "") | gsub("/"; "_")) as $base
| if ($type | contains(",")) then
"\($base)_\($type | gsub(","; "_"))"
else
$base
end;
def longflags($obj; $names):
[$names[] as $k | select(($obj[$k] // false) == true) | $k] | join(",");
. as $j
| build_device_label(($j.device.name // ""); ($j.device.type // "")) as $device
| ($j.device.type // "") as $interface
| ($j.device.protocol // "") as $protocol
| [
emit("smartctl_device_smartctl_exit_status"; [["device", $device]]; $j.smartctl.exit_status),
emit("smartctl_device"; [
["device", $device],
["interface", $interface],
["protocol", $protocol],
["model_family", (($j.model_family // "unknown") | trimstr)],
["model_name", (($j.model_name // $j.scsi_model_name // "unknown") | if . == "" then "unknown" else . end | trimstr)],
["serial_number", (($j.serial_number // "") | trimstr)],
["ata_additional_product_id", ($j.ata_additional_product_id // "unknown")],
["firmware_version", ($j.firmware_version // "")],
["ata_version", ($j.ata_version.string // "")],
["sata_version", ($j.sata_version.string // "")],
["form_factor", ($j.form_factor.name // "")],
["scsi_vendor", ($j.scsi_vendor // "")],
["scsi_product", ($j.scsi_product // "")],
["scsi_revision", ($j.scsi_revision // "")],
["scsi_version", ($j.scsi_version // "")]
]; 1),
emit("smartctl_device_capacity_blocks"; [["device", $device]]; $j.user_capacity.blocks),
emit("smartctl_device_capacity_bytes"; [["device", $device]]; $j.user_capacity.bytes),
emit_if_exists("smartctl_device_nvme_capacity_bytes"; [["device", $device]]; $j.nvme_total_capacity),
emit("smartctl_device_block_size"; [["device", $device], ["blocks_type", "logical"]]; $j.logical_block_size),
emit("smartctl_device_block_size"; [["device", $device], ["blocks_type", "physical"]]; $j.physical_block_size),
(if $j.interface_speed != null then
($j.interface_speed
| to_entries[]
| select(.key == "max" or .key == "current")
| emit("smartctl_device_interface_speed";
[["device", $device], ["speed_type", .key]];
n(.value.units_per_second) * n(.value.bits_per_unit)))
else empty end),
($j.ata_smart_attributes.table[]?
| . as $a
| longflags(($a.flags // {}); ["prefailure", "updated_online", "performance", "error_rate", "event_count", "auto_keep"]) as $lf
| [
["value", $a.value],
["worst", $a.worst],
["thresh", $a.thresh],
["raw", $a.raw.value]
][]
| emit("smartctl_device_attribute"; [
["device", $device],
["attribute_name", (($a.name // "") | trimstr)],
["attribute_flags_short", (($a.flags.string // "") | trimstr)],
["attribute_flags_long", $lf],
["attribute_value_type", .[0]],
["attribute_id", (($a.id // "") | tostring)]
]; .[1])
),
(if $j.power_on_time != null then
emit("smartctl_device_power_on_seconds"; [["device", $device]];
n($j.power_on_time.hours) * 3600 + n($j.power_on_time.minutes) * 60)
else empty end),
(if n($j.rotation_rate) > 0 then
emit("smartctl_device_rotation_rate"; [["device", $device]]; $j.rotation_rate)
else empty end),
($j.temperature? // {}
| to_entries[]?
| emit("smartctl_device_temperature"; [["device", $device], ["temperature_type", .key]]; .value)
),
(if $j.power_cycle_count != null then
emit("smartctl_device_power_cycle_count"; [["device", $device]]; $j.power_cycle_count)
elif $j.scsi_start_stop_cycle_counter.accumulated_start_stop_cycles != null then
emit("smartctl_device_power_cycle_count"; [["device", $device]]; $j.scsi_start_stop_cycle_counter.accumulated_start_stop_cycles)
else empty end),
(if $j.ata_sct_status != null then
emit("smartctl_device_state"; [["device", $device]]; $j.ata_sct_status.device_state)
else empty end),
(if $j.smart_status != null then
emit("smartctl_device_smart_status"; [["device", $device]];
if $j.smart_status.passed == true then 1
elif $j.smart_status.passed == false then 0
else $j.smart_status.passed
end)
else empty end),
($j.ata_device_statistics.pages[]?
| select(((.name // "") | trimstr) != "Vendor Specific Statistics") as $page
| $page.table[]?
| emit("smartctl_device_statistics"; [
["device", $device],
["statistic_table", (($page.name // "") | trimstr)],
["statistic_name", ((.name // "") | trimstr)],
["statistic_flags_short", ((.flags.string // "") | trimstr)],
["statistic_flags_long", longflags((.flags // {}); ["valid", "normalized", "supports_dsn", "monitored_condition_met"])]
]; .value)
),
($j.sata_phy_event_counters.table[]?
| emit("smartctl_device_statistics"; [
["device", $device],
["statistic_table", "SATA PHY Event Counters"],
["statistic_name", ((.name // "") | trimstr)],
["statistic_flags_short", "V---"],
["statistic_flags_long", "valid"]
]; .value)
),
($j.ata_smart_error_log? // {}
| to_entries[]?
| emit("smartctl_device_error_log_count"; [["device", $device], ["error_log_type", .key]]; .value.count)
),
($j.ata_smart_self_test_log? // {}
| to_entries[]?
| emit("smartctl_device_self_test_log_count"; [["device", $device], ["self_test_log_type", .key]]; .value.count)
),
($j.ata_smart_self_test_log? // {}
| to_entries[]?
| emit("smartctl_device_self_test_log_error_count"; [["device", $device], ["self_test_log_type", .key]]; .value.error_count_total)
),
($j.ata_sct_erc? // {}
| to_entries[]?
| emit("smartctl_device_erc_seconds"; [["device", $device], ["op_type", .key]]; n(.value.deciseconds) / 10.0)
),
(if $interface == "nvme" then
emit("smartctl_device_percentage_used"; [["device", $device]]; $j.nvme_smart_health_information_log.percentage_used),
emit("smartctl_device_available_spare"; [["device", $device]]; $j.nvme_smart_health_information_log.available_spare),
emit("smartctl_device_available_spare_threshold"; [["device", $device]]; $j.nvme_smart_health_information_log.available_spare_threshold),
emit("smartctl_device_critical_warning"; [["device", $device]]; $j.nvme_smart_health_information_log.critical_warning),
emit("smartctl_device_media_errors"; [["device", $device]]; $j.nvme_smart_health_information_log.media_errors),
emit("smartctl_device_num_err_log_entries"; [["device", $device]]; $j.nvme_smart_health_information_log.num_err_log_entries),
(if $j.nvme_smart_health_information_log.data_units_read != null and n($j.nvme_smart_health_information_log.data_units_read) != 0 then
emit("smartctl_device_bytes_read"; [["device", $device]]; n($j.nvme_smart_health_information_log.data_units_read) * 1000 * 512)
else empty end),
(if $j.nvme_smart_health_information_log.data_units_written != null and n($j.nvme_smart_health_information_log.data_units_written) != 0 then
emit("smartctl_device_bytes_written"; [["device", $device]]; n($j.nvme_smart_health_information_log.data_units_written) * 1000 * 512)
else empty end)
else empty end),
(if $interface == "scsi" then
emit_if_exists("smartctl_scsi_grown_defect_list"; [["device", $device]]; $j.scsi_grown_defect_list),
(if $j.scsi_error_counter_log != null then
emit("smartctl_device_bytes_read"; [["device", $device]]; n($j.scsi_error_counter_log.read.gigabytes_processed) * 1000000000),
emit("smartctl_device_bytes_written"; [["device", $device]]; n($j.scsi_error_counter_log.write.gigabytes_processed) * 1000000000),
emit("smartctl_read_errors_corrected_by_rereads_rewrites"; [["device", $device]]; $j.scsi_error_counter_log.read.errors_corrected_by_rereads_rewrites),
emit("smartctl_read_errors_corrected_by_eccfast"; [["device", $device]]; $j.scsi_error_counter_log.read.errors_corrected_by_eccfast),
emit("smartctl_read_errors_corrected_by_eccdelayed"; [["device", $device]]; $j.scsi_error_counter_log.read.errors_corrected_by_eccdelayed),
emit("smartctl_read_total_uncorrected_errors"; [["device", $device]]; $j.scsi_error_counter_log.read.total_uncorrected_errors),
emit("smartctl_write_errors_corrected_by_rereads_rewrites"; [["device", $device]]; $j.scsi_error_counter_log.write.errors_corrected_by_rereads_rewrites),
emit("smartctl_write_errors_corrected_by_eccfast"; [["device", $device]]; $j.scsi_error_counter_log.write.errors_corrected_by_eccfast),
emit("smartctl_write_errors_corrected_by_eccdelayed"; [["device", $device]]; $j.scsi_error_counter_log.write.errors_corrected_by_eccdelayed),
emit("smartctl_write_total_uncorrected_errors"; [["device", $device]]; $j.scsi_error_counter_log.write.total_uncorrected_errors)
else empty end)
else empty end)
]
| .[]
'
}
write_header > "$tmp"
version_emitted=0
device_count=0
while IFS= read -r dev; do
json="$("$smartctl_bin" -a -j "$dev" 2>/dev/null || true)"
printf '%s' "$json" | jq -e . >/dev/null 2>&1 || continue
if [ "$version_emitted" -eq 0 ]; then
printf '%s' "$json" | emit_version >> "$tmp"
version_emitted=1
fi
if printf '%s' "$json" | emit_metrics >> "$tmp"; then
device_count=$((device_count + 1))
fi
done < <(discover_devices)
printf 'smartctl_devices %s\n' "$device_count" >> "$tmp"
chmod 0644 "$tmp"
mv "$tmp" "$out"
trap - EXIT
@@ -0,0 +1,10 @@
[Unit]
Description=Run SMART Metric generator service every 5 minutes
[Timer]
OnBootSec=1min
OnUnitInactiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target