HEX
Server: Apache/2.4.63 (Unix)
System: Linux TOMS-220NAS 4.4.302+ #86009 SMP Wed Nov 26 18:19:17 CST 2025 x86_64
User: flavio87 (1026)
PHP: 8.3.27
Disabled: NONE
Upload Files
File: //usr/local/packages/@appstore/ActiveInsight/collectors/diskstat_collector.py
#!/usr/bin/python

import glob
import json
import os
import subprocess
import time

import flashcache_util

_stat = {}
_time = 0
_prev_stat = {}
_prev_time = 0


class DiskStats:
    def __init__(self, rd_ios, wr_ios, rd_sectors, wr_sectors, rd_ticks, wr_ticks, io_ticks):
        self.rd_ios = rd_ios
        self.wr_ios = wr_ios
        self.rd_sectors = rd_sectors
        self.wr_sectors = wr_sectors
        self.rd_ticks = rd_ticks
        self.wr_ticks = wr_ticks
        self.io_ticks = io_ticks


def disk_slot_mapping(paths):
    try:
        p = subprocess.run(['/usr/syno/bin/syno_mib_disk_mapping'],
                             stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True)
        mapping = json.loads(p.stdout.decode("utf-8").strip())
    except FileNotFoundError:
        return {}
    slot = {}
    for path in paths:
        temp = mapping.get(path)
        if temp:
            slot[path] = temp
    return slot


def parse_stat(raw_stat):
    stats = {}
    for line in raw_stat:
        if not line:
            continue
        temp_stat = line.split()
        stats[temp_stat[2]] = DiskStats(
            float(temp_stat[3]),
            float(temp_stat[7]),
            float(temp_stat[5]),
            float(temp_stat[9]),
            float(temp_stat[6]),
            float(temp_stat[10]),
            float(temp_stat[12]),
        )
    return stats


def get_stat():
    try:
        with open('/proc/diskstats', 'r') as f:
            raw_stat = f.read().split('\n')
            t_now = time.time()
    except Exception as e:
        raise e
    return parse_stat(raw_stat), t_now


def calc_each_metric(stat1, time1, stat2, time2):
    delta_time = time1 - time2

    if not stat2 or delta_time < 1:
        return None

    metric = {}
    delta = {
        'rd_ios':     stat1.rd_ios - stat2.rd_ios,
        'wr_ios':     stat1.wr_ios - stat2.wr_ios,
        'rd_sectors': stat1.rd_sectors - stat2.rd_sectors,
        'wr_sectors': stat1.wr_sectors - stat2.wr_sectors,
        'rd_ticks':   stat1.rd_ticks - stat2.rd_ticks,
        'wr_ticks':   stat1.wr_ticks - stat2.wr_ticks,
        'io_ticks':   stat1.io_ticks - stat2.io_ticks
    }

    for v in delta.values():
        if v < 0:
            return None

    metric['read_iops'] = delta['rd_ios'] / delta_time
    metric['write_iops'] = delta['wr_ios'] / delta_time
    metric['read_throughput'] = delta['rd_sectors'] / delta_time * 512
    metric['write_throughput'] = delta['wr_sectors'] / delta_time * 512
    metric['read_io_latency'] = delta['rd_ticks'] / \
        delta['rd_ios'] * 1000 if delta['rd_ios'] != 0 else 0
    metric['write_io_latency'] = delta['wr_ticks'] / \
        delta['wr_ios'] * 1000 if delta['wr_ios'] != 0 else 0
    metric['utilization'] = delta['io_ticks'] / delta_time / 1000

    if metric['utilization'] > 1:
        metric['utilization'] = 1.0

    return metric


def get_metrics(stat1, time1, stat2, time2):
    metric = {}
    for dev, value in stat1.items():
        metric[dev] = calc_each_metric(value, time1, stat2.get(dev), time2)
    return metric


def get_disk_dev():
    dev = []
    ret = {}
    disks = glob.glob('/run/synostorage/disks/*')
    for disk in disks:
        temp = disk.split('/')[-1]
        if os.path.exists('/dev/'+temp):
            dev.append(temp)
    for path, slot in disk_slot_mapping(dev).items():
        ret[path] = [slot]
    return ret


def get_volume_dev():
    try:
        with open('/etc/fstab', 'r') as f:
            fstab = f.readlines()
    except Exception as e:
        raise e
    # get the device path of all volumes
    # ex: {'cachedev_0': 'volume1', 'md2': 'volume2', 'vg1-volume1': 'volume3', 'vg1000-lv': 'volume4', 'drbd0': 'volume5'}
    dev_path = {}
    for line in fstab:
        if not line:
            continue
        tmp_tab = line.strip().split()
        if tmp_tab[1].startswith('/volume'):
            split_path = tmp_tab[0].split('/')
            if split_path[2].startswith('vg'):
                if split_path[3].startswith('lv'):  # vg100X-lv
                    tmp_key = split_path[2] + '-lv'
                else:  # vgX-volumeX
                    tmp_key = split_path[2] + '-' + split_path[3]
            elif split_path[2].startswith('drbd'):  # drbd-vgX-volumeX
                tmp_key = os.readlink(tmp_tab[0]).split('/')[-1]  # drbdX
            else:
                # cachedev_X, mdX, sdXX
                tmp_key = split_path[-1]
            dev_path[tmp_key] = tmp_tab[1].strip('/')

    dev = {}
    # travere all /sys/block/dm-*/name to get the dm volume mapping
    # ex: {'dm-4': 'volume2', 'dm-3': 'volume1'}
    for dm_name, dev_name in get_dm_name_dev_name_map().items():
        if dev_path.get(dev_name):
            dev[dm_name] = [dev_path[dev_name]]

    # get all non dm volume
    for temp_dev, vol in dev_path.items():
        if not temp_dev.startswith('cryptvol') and not temp_dev.startswith('cachedev') and not temp_dev.startswith('vg'):
            dev[temp_dev] = [vol]
    return dev


def get_dm_name_dev_name_map():
    """e.g. {'dm-1': 'cachedev_0', 'dm-2': 'vg1-volume_1'}, dm_name: e.g. dm-1"""
    info = {}
    dm_paths = glob.glob('/sys/block/dm-*')
    for path in dm_paths:
        try:
            with open(path+'/dm/name', 'r') as f:
                dm_name = path.split('/')[-1]
                info[dm_name] = f.read().rstrip()
        except Exception as e:
            continue
    return info


def get_flashcache_dev():
    if not flashcache_util.is_support_cache():
        return {}

    ret, infos = flashcache_util.get_infos('--active-insight-cache-devs')
    if not ret:
        return {}

    # e.g. {'cachedev_0': 'dm-1'}
    dev_name_dm_name_map = {}
    for dm_name, dev_name in get_dm_name_dev_name_map().items():
        dev_name_dm_name_map[dev_name] = dm_name

    dev = {}
    for info in infos:
        labels = flashcache_util.get_lables(info)
        dm_name = dev_name_dm_name_map.get(info['dev_name'])
        if dm_name is None:
            # when cache is not enabled yet
            continue
        dev[dm_name] = labels

    return dev


def get_raid_dev():
    mdX = glob.glob('/sys/block/md*')
    dev = {}
    for i in mdX:
        temp_dev = i.split('/')[-1]
        dev[temp_dev] = [temp_dev]
    return dev


def _zip_object(list1, list2):
    return {list1[index]: list2[index] for index, _ in enumerate(list1)}


def _add_gauge_metric(app, name, value, label=None):
    if label:
        return {
            'application': app,
            'name': name,
            'type': 'gauge',
            'label': label,
            'value': value,
        }
    else:
        return {
            'application': app,
            'name': name,
            'type': 'gauge',
            'value': value,
        }


def add_metric_family(metrics, app, dev, labels):
    if not dev:
        return []

    result = []
    for dev_name, label in dev.items():
        if dev_name not in metrics:
            raise Exception(
                "Failed to get device diskstat: No such device ({})".format(dev_name))

        result.append(_add_gauge_metric(
            app, 'read_iops', metrics[dev_name]['read_iops'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'write_iops', metrics[dev_name]['write_iops'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'read_io_latency', metrics[dev_name]['read_io_latency'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'write_io_latency', metrics[dev_name]['write_io_latency'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'read_throughput', metrics[dev_name]['read_throughput'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'write_throughput', metrics[dev_name]['write_throughput'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))
        result.append(_add_gauge_metric(
            app, 'utilization', metrics[dev_name]['utilization'] if metrics.get(dev_name) else 0, label=_zip_object(labels, label)))

    return result


def collect_1m():
    global _prev_stat, _prev_time, _stat, _time
    _prev_stat = _stat
    _prev_time = _time
    _stat, _time = get_stat()

    result = []
    metrics = get_metrics(_stat, _time, _prev_stat, _prev_time)
    if not metrics:
        return result
    result.extend(add_metric_family(metrics, 'disk', get_disk_dev(), ['slot']))
    result.extend(add_metric_family(metrics, 'volume', get_volume_dev(), ['volume']))
    result.extend(add_metric_family(metrics, 'flashcache', get_flashcache_dev(), ['volume', 'ssd_id', 'type']))
    result.extend(add_metric_family(metrics, 'raid', get_raid_dev(), ['dev']))
    return result