Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing raw metrics for sat devices. Resolves #25 #38

Merged
merged 1 commit into from
Jul 29, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 44 additions & 52 deletions smartprom.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import time
import json
from typing import List
from prometheus_client import start_http_server, Gauge

import prometheus_client

DRIVES = {}
METRICS = {}
LABELS = ['drive']


def run(args: List[str]):
def run_smartctl_cmd(args: list):
"""
Runs the smartctl command on the system
"""
Expand All @@ -22,12 +26,12 @@ def run(args: List[str]):
return stdout.decode("utf-8")


def get_drives():
def get_drives() -> dict:
"""
returns a dictionary of devices and its types
Returns a dictionary of devices and its types
"""
disks = {}
result = run(['smartctl', '--scan-open', '--json=c'])
result = run_smartctl_cmd(['smartctl', '--scan-open', '--json=c'])
result_json = json.loads(result)
if 'devices' in result_json:
devices = result_json['devices']
Expand All @@ -39,68 +43,49 @@ def get_drives():
return disks


DRIVES = get_drives()
HEADER = 'ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE'
METRICS = {}
LABELS = ['drive']


def smart_sat(dev: str) -> List[str]:
def smart_sat(dev: str) -> dict:
"""
Runs the smartctl command on a "sat" device
and processes its attributes
"""
results = run(['smartctl', '-A', '-d', 'sat', dev])
results = run_smartctl_cmd(['smartctl', '-A', '-d', 'sat', '--json=c', dev])

attributes = {}
got_header = False
for result in results.split('\n'):
if not result:
continue

if result == HEADER:
got_header = True
continue

if got_header:
tokens = result.split()
if len(tokens) > 3:
raw = None
try:
raw = int(tokens[9])
except:
pass

attributes[tokens[1]] = (int(tokens[0]), int(tokens[3]))
if raw is not None:
attributes[f'{tokens[1]}_raw'] = (int(tokens[0]), raw)
data = json.loads(results)['ata_smart_attributes']['table']
for metric in data:
code = metric['id']
name = metric['name']
value = metric['value']
value_raw = metric['raw']['value']
attributes[name] = (int(code), value)
attributes[f'{name}_raw'] = (int(code), value_raw)
return attributes


def smart_nvme(dev: str) -> List[str]:
def smart_nvme(dev: str) -> dict:
"""
Runs the smartctl command on a "nvme" device
and processes its attributes
"""
results = run(['smartctl', '-A', '-d', 'nvme', '--json=c', dev])
results = run_smartctl_cmd(['smartctl', '-A', '-d', 'nvme', '--json=c', dev])
attributes = {}

health_info = json.loads(results)['nvme_smart_health_information_log']
for k, v in health_info.items():
if k == 'temperature_sensors':
for i, value in enumerate(v, start=1):
attributes['temperature_sensor{i}'.format(i=i)] = value
continue
attributes[k] = v

data = json.loads(results)['nvme_smart_health_information_log']
for key, value in data.items():
if key == 'temperature_sensors':
for i, _value in enumerate(value, start=1):
attributes[f'temperature_sensor{i}'] = _value
else:
attributes[key] = value
return attributes


def smart_scsi(dev: str) -> List[str]:
def smart_scsi(dev: str) -> dict:
"""
Runs the smartctl command on a "scsi" device
and processes its attributes
"""
results = run(['smartctl', '-A', '-d', 'scsi', '--json=c', dev])
results = run_smartctl_cmd(['smartctl', '-A', '-d', 'scsi', '--json=c', dev])
attributes = {}
data = json.loads(results)
for key, value in data.items():
Expand All @@ -117,7 +102,7 @@ def collect():
"""
Collect all drive metrics and save them as Gauge type
"""
global METRICS
global DRIVES, METRICS, LABELS

for drive, typ in DRIVES.items():
try:
Expand All @@ -132,15 +117,15 @@ def collect():

for key, values in attrs.items():
# Metric name in lower case
metric = 'smartprom_' + key.replace('-', '_').replace(' ', '_').replace('.', '').replace('/', '_')\
metric = 'smartprom_' + key.replace('-', '_').replace(' ', '_').replace('.', '').replace('/', '_') \
.lower()

# Create metric if it does not exist
if metric not in METRICS:
desc = key.replace('_', ' ')
code = hex(values[0]) if typ == 'sat' else hex(values)
print(f'Adding new gauge {metric} ({code})')
METRICS[metric] = Gauge(metric, f'({code}) {desc}', LABELS)
METRICS[metric] = prometheus_client.Gauge(metric, f'({code}) {desc}', LABELS)

# Update metric
metric_val = values[1] if typ == 'sat' else values
Expand All @@ -155,11 +140,18 @@ def main():
"""
Starts a server and exposes the metrics
"""
global DRIVES

# Validate configuration
exporter_address = os.environ.get("SMARTCTL_EXPORTER_ADDRESS", "0.0.0.0")
exporter_port = int(os.environ.get("SMARTCTL_EXPORTER_PORT", 9902))
refresh_interval = int(os.environ.get("SMARTCTL_REFRESH_INTERVAL", 60))

start_http_server(exporter_port, exporter_address)
# Get drives (test smartctl)
DRIVES = get_drives()

# Start Prometheus server
prometheus_client.start_http_server(exporter_port, exporter_address)
print(f"Server listening in http://{exporter_address}:{exporter_port}/metrics")

while True:
Expand Down