Complete rewrite: SnapRAID SMART logger with BackBlaze algorithm

Fetches SMART data from Scrutiny's InfluxDB and calculates failure
probabilities using the exact BackBlaze tables from SnapRAID source.

Key features:
- Calculates both v12 (with attr 193) and v13 (without attr 193) algorithms
- v12 matches SnapRAID pre-v13.0 (includes Load Cycle Count)
- v13 matches modern SnapRAID v13.0+ (excludes Load Cycle Count)
- Stores both values for trending and comparison
- Correctly applies bit masks (16-bit for 187/188, 32-bit for others)
- Annualizes monthly rates and applies Poisson distribution
- Logs to PostgreSQL with device metadata

Solved the mystery: SnapRAID v12 uses attribute 193 which was removed
in v13.0. Load cycle count has massive impact on failure predictions
for some drives (80% vs 4% difference).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Wes
2025-12-07 06:55:39 -05:00
co-authored by Claude Sonnet 4.5
commit 27afedb32e
25 changed files with 3207 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
# SnapRAID SMART Logger - Investigation Notes
## Project Goal
Create a Python utility that:
1. Fetches SMART data from Scrutiny's InfluxDB
2. Calculates failure probability **exactly like SnapRAID does** (using BackBlaze algorithm)
3. Logs enhanced data to PostgreSQL
4. Eventually add alerts and automation
## Current Status: BLOCKED - Algorithm Mismatch
We successfully built the infrastructure but **cannot match SnapRAID's failure probability calculations**.
---
## The Mystery: 81% vs 4.63%
### What SnapRAID Reports for /dev/sdc (ZLW0A3QJ)
```
Temp Power Error FP Size
C OnDays Count TB Serial Device Disk
-----------------------------------------------------------------------
24 1968 0 81% 12.0 ZLW0A3QJ /dev/sdc d1
```
**Failure Probability: 81%**
### What smartctl -A Shows
```
ID# ATTRIBUTE_NAME VALUE WORST THRESH RAW_VALUE
5 Reallocated_Sector_Ct 100 100 010 0
187 Reported_Uncorrect 100 100 000 0
188 Command_Timeout 100 100 000 0
197 Current_Pending_Sector 100 100 000 0
198 Offline_Uncorrectable 100 100 000 0
```
**All critical attributes are PERFECT** (VALUE=100, RAW_VALUE=0)
### What Our BackBlaze Calculation Gives
Using the exact same BackBlaze tables extracted from SnapRAID source:
- With RAW_VALUE = 0: **Probability = 4.63%**
- With VALUE = 100: **Probability = 100%** (saturated, wrong)
**Neither matches SnapRAID's 81%!**
---
## What We Know About SnapRAID's Algorithm
### Source Code Analysis
From `cmdline/device.c`:
```c
static double smart_afr(uint64_t* smart, const char* model)
{
double afr = 0;
uint64_t mask32 = 0xffffffffU;
uint64_t mask16 = 0xffffU;
// Attribute 5: Reallocated Sectors (32-bit mask)
if (smart[5] != SMART_UNASSIGNED) {
double r = smart_afr_value(SMART_5_R, SMART_5_STEP, smart[5] & mask32);
if (afr < r) afr = r;
}
// Attribute 187: Reported Uncorrectable (16-bit mask)
if (smart[187] != SMART_UNASSIGNED) {
double r = smart_afr_value(SMART_187_R, SMART_187_STEP, smart[187] & mask16);
if (afr < r) afr = r;
}
// Attribute 188: Command Timeout (16-bit, skipped for Seagate)
if (strncmp(model, "ST", 2) != 0 && smart[188] != SMART_UNASSIGNED) {
double r = smart_afr_value(SMART_188_R, SMART_188_STEP, smart[188] & mask16);
if (afr < r) afr = r;
}
// Attribute 197: Current Pending Sectors (32-bit)
if (smart[197] != SMART_UNASSIGNED) {
double r = smart_afr_value(SMART_197_R, SMART_197_STEP, smart[197] & mask32);
if (afr < r) afr = r;
}
// Attribute 198: Offline Uncorrectable (32-bit)
if (smart[198] != SMART_UNASSIGNED) {
double r = smart_afr_value(SMART_198_R, SMART_198_STEP, smart[198] & mask32);
if (afr < r) afr = r;
}
return afr;
}
```
### Table Lookup Function
```c
static double smart_afr_value(double* tab, unsigned step, uint64_t value)
{
value /= step;
if (value >= SMART_MEASURES) value = SMART_MEASURES - 1;
return 365.0 / 30.0 * tab[value]; // Annualize monthly rate
}
```
### Final Probability Calculation
```c
// Poisson distribution: P(at least 1 failure) = 1 - e^(-AFR)
poisson_prob_n_or_more_failures(afr, 1) * 100
```
**Formula:** `P = (1 - e^(-AFR)) × 100`
---
## Key Findings
### 1. BackBlaze Tables Are Correct
We extracted all 5 tables (256 values each) from SnapRAID source:
- `SMART_5_R` (Reallocated Sectors)
- `SMART_187_R` (Reported Uncorrectable)
- `SMART_188_R` (Command Timeout)
- `SMART_197_R` (Current Pending Sectors)
- `SMART_198_R` (Offline Uncorrectable)
Tables match exactly - verified byte-for-byte.
### 2. Algorithm Is Correct
Our implementation:
1. ✅ Uses correct bit masks (16-bit for 187/188, 32-bit for 5/197/198)
2. ✅ Applies correct step size (all = 1)
3. ✅ Takes maximum AFR across attributes
4. ✅ Scales monthly to annual (365/30)
5. ✅ Applies Poisson: `1 - e^(-AFR)`
### 3. The Math Works Backwards
**If SnapRAID reports 81%, then:**
```
0.81 = 1 - e^(-AFR)
e^(-AFR) = 0.19
AFR = -ln(0.19) = 1.66
Monthly rate = 1.66 × (30/365) = 0.136
```
**Looking at SMART_187_R table:**
- `table[1] = 0.1287` → AFR = 1.565 → **P = 79%**
- `table[2] = 0.1579` → AFR = 1.920 → **P = 85%**
**Conclusion: SnapRAID is using an attribute value of 1 or 2 for one of the attributes!**
### 4. But We Can't Find That Value!
From both smartctl AND InfluxDB:
- RAW_VALUE = 0 for all 5 attributes
- VALUE (normalized) = 100 for all 5 attributes
- WORST = 100 for all 5 attributes
- No errors, no degradation visible
---
## Current Hypothesis
SnapRAID must be:
1. **Extracting a specific byte** from RAW_VALUE that contains 1-2?
2. **Using a different field** we haven't checked (FLAGS? WHEN_FAILED?)?
3. **Calculating a derived value** from multiple fields?
4. **Using a different version** of the algorithm than GitHub master?
---
## What We Need
### Immediate Next Step
Check if RAW_VALUE has byte-level data we're missing:
```bash
smartctl -A /dev/sdc | grep -E "^( 5|187|188|197|198)"
```
Need to see if there are parenthetical byte breakdowns like:
```
194 Temperature_Celsius 24 (0 15 0 0 0)
^^^^^^^^^^^^^ Individual bytes
```
### Alternative Approaches
1. **Run SnapRAID with debug/verbose mode** to see what values it reads
2. **Find the smartctl parsing code** in SnapRAID source
3. **Check SnapRAID version** - might be using different algorithm
4. **Contact SnapRAID author** if needed
---
## Project Files
### Core Implementation
- `smart_logger_v3.py` - Main logger (currently uses Scrutiny's failure rates)
- `backblaze_tables.py` - BackBlaze failure rate tables (256 values × 5 attributes)
- `schema_v2.sql` - PostgreSQL schema with devices + smart_metrics tables
- `populate_devices_final.sql` - Your 9 drives' metadata
### Debug Scripts
- `debug_smart_values.py` - Check InfluxDB raw SMART values
- `debug_scrutiny_failure_rate.py` - Check Scrutiny's calculated rates
- `debug_value_field.py` - Compare VALUE vs RAW_VALUE fields
- `test_backblaze_calc.py` - Test BackBlaze calculation with different inputs
### Setup
- `setup_database.sh` - PostgreSQL database creation
- `get_device_info.py` - Extract device WWNs from InfluxDB
- `requirements.txt` - Python dependencies
---
## Database Schema
### devices (manually populated)
```sql
device_wwn, device_path, serial_number, model, manufacturer,
capacity_bytes, size_tb, disk_role, notes
```
### smart_metrics (auto-populated by logger)
```sql
device_wwn, timestamp, temperature_celsius, power_on_days,
error_count, failure_probability_pct, smart_attributes (JSONB)
```
### Views
- `smart_latest` - Latest metrics per device with metadata
- `smart_high_risk` - Devices >50% failure probability
- `smart_summary` - SnapRAID-style summary table
---
## Database Connection Info
- **InfluxDB**: cyrion.c0smere.net:8086
- Org: scrutiny
- Bucket: metrics
- Token: (in .env)
- **PostgreSQL**: phlegethon.c0smere.net:5432
- Database: smart_monitoring
- User: smart_logger
- Password: saddog095
---
## Next Steps (After Solving Algorithm)
### Option 2 Features (After Option 1 Works)
1. **Alerts**
- Email/webhook when failure probability crosses thresholds
- Trend detection (rapidly increasing failure rate)
- Integration with monitoring systems
2. **SnapRAID Integration**
- Trigger scrub when high-risk drives detected
- Pre-scrub notifications
- Post-scrub verification
3. **Historical Analysis**
- Failure probability trends over time
- Predict when drives will cross risk thresholds
- Identify patterns across drive models/manufacturers
4. **Custom Queries**
- Drive health reports
- Lifetime statistics
- Comparison across drive families
---
## Comparison: SnapRAID vs Scrutiny vs Our Calculator
| Drive | SnapRAID | Scrutiny | Our Calc (raw=0) | Our Calc (val=100) |
|-------|----------|----------|------------------|---------------------|
| /dev/sdc (d1) | **81%** | 11% | 4.63% | 100% |
| /dev/sdb (d2) | 22% | 19% | 4.63% | 100% |
| /dev/sdd (d5) | 51% | 11% | 4.63% | 100% |
| /dev/sda (parity) | 49% | 16% | 4.63% | 100% |
| /dev/sdg (2-parity) | 30% | 12% | 4.63% | 100% |
| /dev/sdh (2-parity) | 4% | 5% | 4.63% | 100% |
| /dev/sde | 4% | 5% | 4.63% | 100% |
| /dev/sdf | 84% | 11% | 4.63% | 100% |
| /dev/sdi | 84% | 11% | 4.63% | 100% |
**Pattern:** SnapRAID shows high variance (4-84%), we show either 4.63% or 100%.
---
## Questions to Answer
1. **What exact value does SnapRAID read for attribute 5/187/188/197/198?**
2. **Is SnapRAID using RAW_VALUE, VALUE, WORST, or something else?**
3. **Could there be byte-level parsing we're missing?**
4. **What version of SnapRAID is running, and does it match GitHub master?**
5. **Is there a verbose mode to see what SnapRAID actually reads?**
---
## References
- [SnapRAID GitHub](https://github.com/amadvance/snapraid)
- [SnapRAID device.c](https://github.com/amadvance/snapraid/blob/master/cmdline/device.c)
- [Scrutiny GitHub](https://github.com/AnalogJ/scrutiny)
- [BackBlaze Hard Drive Stats](https://www.backblaze.com/cloud-storage/resources/hard-drive-test-data)
---
**Last Updated:** 2025-12-05
**Status:** 🔴 BLOCKED - Cannot match SnapRAID algorithm
**Blocker:** Unknown how SnapRAID calculates 81% with all attributes = 0/100
+15
View File
@@ -0,0 +1,15 @@
# InfluxDB Configuration
INFLUXDB_URL=http://your-docker-host:8086
INFLUXDB_TOKEN=your-scrutiny-token
INFLUXDB_ORG=scrutiny
INFLUXDB_BUCKET=metrics
# Scrutiny API Configuration (defaults to INFLUXDB_URL with port 8080)
SCRUTINY_API_URL=http://your-docker-host:8080
# PostgreSQL Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=smart_monitoring
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-password
+24
View File
@@ -0,0 +1,24 @@
# Python
venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Environment
.env
# IDE
.vscode/
.idea/
*.swp
*.swo
# Output files
explore_output.txt
*.log
# OS
.DS_Store
Thumbs.db
+182
View File
@@ -0,0 +1,182 @@
# SnapRAID SMART Logger
A Python utility that collects SMART data from Scrutiny's InfluxDB, calculates BackBlaze-based failure probabilities (like SnapRAID does), and logs the enhanced data to PostgreSQL.
## Features
- ✅ Connects to Scrutiny's InfluxDB to fetch SMART attributes
- ✅ Implements SnapRAID's BackBlaze failure probability algorithm
- ✅ Calculates annual failure rate (AFR) for each disk
- ✅ Stores historical data in PostgreSQL with time-series support
- ✅ Provides views for latest metrics and high-risk devices
## Quick Start
### 1. Install Dependencies
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### 2. Configure Environment
```bash
cp .env.example .env
# Edit .env with your actual values
```
Required configuration:
- `INFLUXDB_URL`: Your Scrutiny InfluxDB URL (e.g., `http://192.168.1.100:8086`)
- `INFLUXDB_TOKEN`: Your Scrutiny InfluxDB token
- `POSTGRES_*`: Your PostgreSQL connection details
### 3. Set Up PostgreSQL Schema
```bash
psql -h <host> -U <user> -d <database> -f schema.sql
```
### 4. Explore Scrutiny's InfluxDB Schema
**IMPORTANT**: Run this first to understand how Scrutiny stores SMART data:
```bash
python explore_influx.py
```
This will show you:
- Available measurements in the `metrics` bucket
- Field names for each measurement
- Sample data structure
- Tags used for device identification
### 5. Customize the Logger
Based on the output from `explore_influx.py`, update `smart_logger.py`:
1. **Update the InfluxDB query** (`fetch_smart_data` method):
- Set correct `_measurement` name
- Map field names to device properties (temp, serial, model, etc.)
- Map SMART attribute fields to IDs (5, 187, 188, 197, 198)
2. **Adjust device detection**:
- Update how device path, serial, and model are extracted
- Add disk role mapping if you want to match SnapRAID roles
### 6. Run the Logger
```bash
python smart_logger.py
```
## Output Format
The logger will display a summary like SnapRAID:
```
Device: /dev/sda
Serial: ZX20AAMV
Model: WDC WD200EFAX-68FB5N0
Temp: 25°C
Power-On: 636 days
Failure Probability: 49.00%
================================================================================
Summary:
================================================================================
🔴 HIGH RISK /dev/sdc: 81.00% failure probability
🟢 OK /dev/sdb: 22.00% failure probability
```
## PostgreSQL Schema
### Tables
- **`smart_metrics`**: Main table storing all SMART data snapshots
- Includes temperature, power-on time, error counts
- Stores calculated failure probability
- Raw SMART attributes as JSONB for flexibility
### Views
- **`smart_latest`**: Latest metrics for each device
- **`smart_high_risk`**: Devices with >50% annual failure probability
### Example Queries
```sql
-- Get latest status for all devices
SELECT device_path, serial_number, temperature_celsius,
failure_probability_pct, timestamp
FROM smart_latest
ORDER BY failure_probability_pct DESC;
-- Historical trend for a specific disk
SELECT timestamp, temperature_celsius, failure_probability_pct
FROM smart_metrics
WHERE serial_number = 'ZX20AAMV'
ORDER BY timestamp DESC
LIMIT 100;
-- Check specific SMART attribute over time
SELECT timestamp,
(smart_attributes->>'5')::int as reallocated_sectors,
(smart_attributes->>'197')::int as pending_sectors
FROM smart_metrics
WHERE device_path = '/dev/sda'
ORDER BY timestamp DESC;
```
## BackBlaze Algorithm
The failure probability calculation is based on SnapRAID's implementation, which uses BackBlaze's 2014 dataset (47,322 disk observations) to create lookup tables for these SMART attributes:
- **Attribute 5**: Reallocated Sectors Count
- **Attribute 187**: Reported Uncorrectable Errors
- **Attribute 188**: Command Timeout (excluded for Seagate disks)
- **Attribute 197**: Current Pending Sector Count
- **Attribute 198**: Offline Uncorrectable Sector Count
The algorithm returns the **maximum** AFR across all attributes (since they're correlated, not independent).
## Automation
To run this periodically, add a cron job:
```bash
# Run every hour
0 * * * * /path/to/venv/bin/python /path/to/smart_logger.py >> /var/log/smart_logger.log 2>&1
```
Or use systemd timer, or your preferred scheduler.
## Troubleshooting
### InfluxDB Connection Issues
- Verify the URL and port are correct
- Check that the token has read permissions for the `metrics` bucket
- Ensure InfluxDB is accessible from the machine running this script
### No Data Returned
- Run `explore_influx.py` to check the data structure
- Verify Scrutiny is actively collecting data
- Check the time range in the query (`-5m` might be too short)
### PostgreSQL Insert Failures
- Ensure the schema was created (`schema.sql`)
- Check that the database user has INSERT permissions
- Verify data types match (especially JSONB for smart_attributes)
## License
MIT License - feel free to modify and use as needed!
## Credits
- BackBlaze failure probability tables extracted from [SnapRAID](https://github.com/amadvance/snapraid)
- Based on BackBlaze's 2014 hard drive reliability dataset
+95
View File
@@ -0,0 +1,95 @@
# Scrutiny InfluxDB Schema Notes
Based on analysis of your `explore_output.txt`, here's what I discovered:
## InfluxDB Structure
### Measurements
- `smart` - SMART attribute data
- `temp` - Temperature data (simpler format)
### Tags (Device Identifiers)
- `device_wwn` - World Wide Name, unique identifier (e.g., `0x5000c500744487c5`)
- `device_protocol` - Protocol type (`ATA`, `NVME`, etc.)
### Fields in `smart` Measurement
**Basic Metrics:**
- `temp` - Temperature in Celsius
- `power_on_hours` - Total power-on hours
- `power_cycle_count` - Number of power cycles
**SMART Attributes** (pattern: `attr.{id}.{property}`):
For each SMART attribute ID (e.g., 5, 187, 188, 194, 197, 198), Scrutiny stores:
- `attr.{id}.attribute_id` - The attribute ID itself
- `attr.{id}.raw_value` - **Raw value (what we need for calculations!)**
- `attr.{id}.raw_string` - String representation
- `attr.{id}.transformed_value` - Normalized value (0-100)
- `attr.{id}.value` - Current value
- `attr.{id}.thresh` - Threshold
- `attr.{id}.worst` - Worst value seen
- `attr.{id}.status` - Status indicator
- `attr.{id}.status_reason` - Status explanation
- `attr.{id}.failure_rate` - Scrutiny's own failure rate calculation
- `attr.{id}.when_failed` - Failure timestamp (if applicable)
## Key SMART Attributes for Failure Prediction
BackBlaze analysis (used by SnapRAID) focuses on these attributes:
| ID | Name | Description |
|-----|------|-------------|
| 5 | Reallocated Sectors Count | Sectors remapped due to errors |
| 187 | Reported Uncorrectable Errors | Errors that couldn't be corrected |
| 188 | Command Timeout | Commands that timed out |
| 197 | Current Pending Sector Count | Sectors waiting for remapping |
| 198 | Offline Uncorrectable Sector Count | Errors found during offline scan |
## Your Devices (from explore_output.txt)
Found 5 devices with these WWNs:
1. `0x5000c500744487c5`
2. `0x5000c500c455c45e`
3. `0x5000c500c46b0832`
4. `0x5000c500c570663e`
5. `0x5000c500e584b9c8`
## Device Metadata Location
**Important:** Serial number, model, device path (`/dev/sdX`) are **NOT** stored in InfluxDB!
They are stored in:
1. **Scrutiny's SQLite database** (`/opt/scrutiny/config/scrutiny.db`)
2. **Scrutiny's REST API** endpoints:
- `/api/summary` - All devices with metadata
- `/api/device/{wwn}/smart` - Individual device data
## Solution Used
The `smart_logger_v2.py` script:
1. **Fetches device metadata** from Scrutiny's `/api/summary` endpoint
2. **Fetches SMART data** from InfluxDB (faster than API for historical queries)
3. **Calculates failure probability** using BackBlaze tables
4. **Exports to PostgreSQL** with full metadata
## Example InfluxDB Query
```flux
from(bucket: "metrics")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) =>
r._field == "temp" or
r._field == "power_on_hours" or
r._field == "attr.5.raw_value" or
r._field == "attr.187.raw_value" or
r._field == "attr.188.raw_value" or
r._field == "attr.197.raw_value" or
r._field == "attr.198.raw_value"
)
|> last()
|> pivot(rowKey:["device_wwn"], columnKey: ["_field"], valueColumn: "_value")
```
This groups all fields by device WWN, making it easy to get all SMART data per device.
+170
View File
@@ -0,0 +1,170 @@
# Setup Guide - Smart Logger V3
This version uses a **PostgreSQL devices table** for metadata instead of the Scrutiny API. Much simpler!
## Quick Setup
### 1. Create the Database Schema
```bash
# Run the new schema (includes devices table)
psql -h <your-pg-host> -U <user> -d smart_monitoring -f schema_v2.sql
```
### 2. Get Your Device WWNs
Run the helper script to see all devices in InfluxDB:
```bash
python get_device_info.py
```
This will show:
```
Device 1:
WWN: 0x5000c500744487c5
Protocol: ATA
Temperature: 24°C
Power-On Time: 1968 days (47232 hours)
Device 2:
...
```
### 3. Populate Device Metadata
Now match each WWN with its actual device info from `snapraid smart`:
```
Temp Power Error FP Size
C OnDays Count TB Serial Device Disk
-----------------------------------------------------------------------
24 1968 0 81% 12.0 ZLW0A3QJ /dev/sdc d1
25 637 0 22% 20.0 ZX22EJ3Y /dev/sdb d2
21 1014 0 51% 12.0 ZTN1BNEZ /dev/sdd d5
25 636 0 49% 20.0 ZX20AAMV /dev/sda parity
25 2540 0 30% 10.0 2TJ97M6D /dev/sdg 2-parity
28 2354 1 4% 10.0 7JJVJ65C /dev/sdh 2-parity
32 1983 0 4% 3.0 YVGGG6EC /dev/sde -
30 2072 0 84% 8.0 ZA1GX9F4 /dev/sdf -
- - 0 - - 25243X801438 /dev/nvme0n1 -
32 2074 0 84% 8.0 ZA1GW0XM /dev/sdi -
```
Match the power-on days from `get_device_info.py` with the power-on days from `snapraid smart` to identify each WWN.
For example:
- **WWN 0x5000c500744487c5** has **1968 days** → matches **/dev/sdc** (ZLW0A3QJ, d1)
### 4. Edit populate_devices.sql
Update `populate_devices.sql` with your actual device information:
```sql
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500744487c5',
'/dev/sdc',
'ZLW0A3QJ',
'WDC WD120EFAX-68FB5N0', -- Get from smartctl -i /dev/sdc
'Western Digital',
12000000000000,
12.0,
'd1',
'Data drive 1'
);
```
Repeat for all 9 drives.
### 5. Load the Data
```bash
psql -h <your-pg-host> -U <user> -d smart_monitoring -f populate_devices.sql
```
### 6. Run the Logger!
```bash
python smart_logger_v3.py
```
## Expected Output
```
Connecting to InfluxDB at http://cyrion.c0smere.net:8086...
✓ Connected to InfluxDB
Connecting to PostgreSQL...
✓ Connected to PostgreSQL
Loading device metadata from database...
✓ Loaded metadata for 9 device(s)
Querying InfluxDB for SMART data...
✓ Found data for 9 device(s)
/dev/sdc
WWN: 0x5000c500744487c5
Serial: ZLW0A3QJ
Model: WDC WD120EFAX
Role: d1
Temp: 24°C
Power-On: 1968 days
Size: 12.0 TB
SMART Attrs: [5, 187, 197, 198]
Failure Probability: 81.00%
...
================================================================================
SnapRAID-style SMART Summary
================================================================================
Temp Power Error FP Size
C OnDays Count TB Serial Device Disk
--------------------------------------------------------------------------------
24 1968 0 81% 12.0 ZLW0A3QJ /dev/sdc d1
25 637 0 22% 20.0 ZX22EJ3Y /dev/sdb d2
21 1014 0 51% 12.0 ZTN1BNEZ /dev/sdd d5
25 636 0 49% 20.0 ZX20AAMV /dev/sda parity
25 2540 0 30% 10.0 2TJ97M6D /dev/sdg 2-parity
```
## Querying the Data
```sql
-- Latest status for all drives
SELECT * FROM smart_summary;
-- Historical data for a specific drive
SELECT timestamp, temperature_celsius, failure_probability_pct
FROM smart_metrics sm
JOIN devices d ON sm.device_wwn = d.device_wwn
WHERE d.device_path = '/dev/sdc'
ORDER BY timestamp DESC
LIMIT 100;
-- High-risk drives
SELECT * FROM smart_high_risk;
```
## Automation
Add to cron to run hourly:
```cron
0 * * * * cd /home/wes/projects/snapraid_smart_logger && /home/wes/projects/snapraid_smart_logger/venv/bin/python smart_logger_v3.py >> /var/log/smart_logger.log 2>&1
```
## Updating Device Info
If you add/remove drives, just update the `devices` table:
```sql
-- Add a new drive
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role)
VALUES ('0xNEW_WWN', '/dev/sdj', 'NEWSERIAL', 'MODEL', 'MANUFACTURER', 12000000000000, 12.0, 'd6');
-- Remove a drive
DELETE FROM devices WHERE device_wwn = '0xOLD_WWN';
-- Update drive role
UPDATE devices SET disk_role = 'parity' WHERE device_path = '/dev/sda';
```
+283
View File
@@ -0,0 +1,283 @@
"""
BackBlaze SMART failure rate tables extracted from SnapRAID source code.
Based on BackBlaze's 2014 dataset (47,322 disk observations).
Tables contain 256 monthly failure rates, scaled to annual rates using: AFR = (365/30) * monthly_rate
"""
import math
# SMART Attribute 5: Reallocated Sectors Count
SMART_5_STEP = 1
SMART_5_R = [
0.0026, 0.0748, 0.0919, 0.1013, 0.1079, 0.1137, 0.1194, 0.1235, 0.1301, 0.1398,
0.1453, 0.1490, 0.1528, 0.1566, 0.1595, 0.1635, 0.1656, 0.1701, 0.1718, 0.1740,
0.1762, 0.1787, 0.1808, 0.1833, 0.1858, 0.1885, 0.1901, 0.1915, 0.1934, 0.1958,
0.1975, 0.1993, 0.2014, 0.2048, 0.2068, 0.2088, 0.2109, 0.2120, 0.2137, 0.2160,
0.2173, 0.2214, 0.2226, 0.2237, 0.2262, 0.2277, 0.2292, 0.2304, 0.2338, 0.2369,
0.2381, 0.2396, 0.2411, 0.2427, 0.2445, 0.2462, 0.2472, 0.2488, 0.2496, 0.2504,
0.2514, 0.2525, 0.2535, 0.2544, 0.2554, 0.2571, 0.2583, 0.2601, 0.2622, 0.2631,
0.2635, 0.2644, 0.2659, 0.2675, 0.2682, 0.2692, 0.2701, 0.2707, 0.2712, 0.2726,
0.2745, 0.2767, 0.2778, 0.2784, 0.2800, 0.2814, 0.2834, 0.2839, 0.2851, 0.2877,
0.2883, 0.2891, 0.2900, 0.2907, 0.2916, 0.2934, 0.2950, 0.2969, 0.2975, 0.2983,
0.2999, 0.3006, 0.3013, 0.3021, 0.3033, 0.3054, 0.3066, 0.3074, 0.3082, 0.3094,
0.3106, 0.3112, 0.3120, 0.3137, 0.3141, 0.3145, 0.3151, 0.3159, 0.3169, 0.3174,
0.3181, 0.3194, 0.3215, 0.3219, 0.3231, 0.3234, 0.3237, 0.3242, 0.3255, 0.3270,
0.3283, 0.3286, 0.3289, 0.3304, 0.3315, 0.3322, 0.3347, 0.3361, 0.3382, 0.3384,
0.3395, 0.3398, 0.3401, 0.3405, 0.3411, 0.3431, 0.3435, 0.3442, 0.3447, 0.3450,
0.3455, 0.3464, 0.3472, 0.3486, 0.3497, 0.3501, 0.3509, 0.3517, 0.3531, 0.3535,
0.3540, 0.3565, 0.3569, 0.3576, 0.3579, 0.3584, 0.3590, 0.3594, 0.3599, 0.3621,
0.3627, 0.3642, 0.3649, 0.3655, 0.3658, 0.3667, 0.3683, 0.3699, 0.3704, 0.3707,
0.3711, 0.3715, 0.3718, 0.3721, 0.3727, 0.3740, 0.3744, 0.3748, 0.3753, 0.3756,
0.3761, 0.3766, 0.3775, 0.3794, 0.3801, 0.3804, 0.3813, 0.3817, 0.3823, 0.3831,
0.3847, 0.3875, 0.3881, 0.3886, 0.3890, 0.3893, 0.3896, 0.3900, 0.3907, 0.3923,
0.3925, 0.3933, 0.3936, 0.3961, 0.3971, 0.3981, 0.3989, 0.4007, 0.4012, 0.4018,
0.4023, 0.4027, 0.4041, 0.4048, 0.4056, 0.4073, 0.4079, 0.4086, 0.4104, 0.4107,
0.4109, 0.4112, 0.4118, 0.4133, 0.4139, 0.4144, 0.4146, 0.4148, 0.4164, 0.4165,
0.4174, 0.4191, 0.4197, 0.4201, 0.4204, 0.4210, 0.4213, 0.4216, 0.4221, 0.4231,
0.4235, 0.4237, 0.4239, 0.4241, 0.4244, 0.4249,
]
# SMART Attribute 187: Reported Uncorrectable Errors
SMART_187_STEP = 1
SMART_187_R = [
0.0039, 0.1287, 0.1579, 0.1776, 0.1905, 0.2013, 0.2226, 0.3263, 0.3612, 0.3869,
0.4086, 0.4292, 0.4559, 0.5278, 0.5593, 0.5847, 0.6124, 0.6345, 0.6517, 0.6995,
0.7308, 0.7541, 0.7814, 0.8122, 0.8306, 0.8839, 0.9100, 0.9505, 0.9906, 1.0254,
1.0483, 1.1060, 1.1280, 1.1624, 1.1895, 1.2138, 1.2452, 1.2864, 1.3120, 1.3369,
1.3705, 1.3894, 1.4055, 1.4218, 1.4434, 1.4670, 1.4834, 1.4993, 1.5174, 1.5400,
1.5572, 1.5689, 1.5808, 1.6198, 1.6346, 1.6405, 1.6570, 1.6618, 1.6755, 1.6877,
1.7100, 1.7258, 1.7347, 1.7814, 1.7992, 1.8126, 1.8225, 1.8269, 1.8341, 1.8463,
1.8765, 1.8850, 1.9005, 1.9281, 1.9398, 1.9618, 1.9702, 1.9905, 2.0099, 2.0480,
2.0565, 2.0611, 2.0709, 2.0846, 2.0895, 2.0958, 2.1008, 2.1055, 2.1097, 2.1235,
2.1564, 2.1737, 2.1956, 2.1989, 2.2015, 2.2148, 2.2355, 2.2769, 2.2940, 2.3045,
2.3096, 2.3139, 2.3344, 2.3669, 2.3779, 2.3941, 2.4036, 2.4396, 2.4473, 2.4525,
2.4656, 2.4762, 2.4787, 2.5672, 2.5732, 2.5755, 2.5794, 2.5886, 2.6100, 2.6144,
2.6341, 2.6614, 2.6679, 2.6796, 2.6847, 2.6872, 2.6910, 2.6934, 2.6995, 2.7110,
2.7179, 2.7204, 2.7232, 2.7282, 2.7355, 2.7375, 2.7422, 2.7558, 2.7580, 2.7643,
2.7767, 2.7770, 2.8016, 2.9292, 2.9294, 2.9337, 2.9364, 2.9409, 2.9436, 2.9457,
2.9466, 2.9498, 2.9543, 2.9570, 2.9573, 2.9663, 2.9708, 2.9833, 2.9859, 2.9895,
2.9907, 2.9932, 2.9935, 3.0021, 3.0035, 3.0079, 3.0103, 3.0126, 3.0151, 3.0266,
3.0288, 3.0320, 3.0330, 3.0343, 3.0373, 3.0387, 3.0438, 3.0570, 3.0579, 3.0616,
3.0655, 3.0728, 3.0771, 3.0794, 3.0799, 3.0812, 3.1769, 3.1805, 3.1819, 3.1860,
3.1869, 3.2004, 3.2016, 3.2025, 3.2070, 3.2129, 3.2173, 3.2205, 3.2254, 3.2263,
3.2300, 3.2413, 3.2543, 3.2580, 3.2595, 3.2611, 3.2624, 3.2787, 3.2798, 3.2809,
3.2823, 3.2833, 3.2834, 3.2853, 3.2866, 3.3332, 3.3580, 3.3595, 3.3625, 3.3631,
3.3667, 3.3702, 3.3737, 3.3742, 3.3747, 3.3769, 3.3775, 3.3791, 3.3809, 3.3813,
3.3814, 3.3822, 3.3827, 3.3828, 3.3833, 3.3833, 3.3843, 3.3882, 3.3963, 3.4047,
3.4057, 3.4213, 3.4218, 3.4230, 3.4231, 3.4240, 3.4262, 3.4283, 3.4283, 3.4288,
3.4293, 3.4302, 3.4317, 3.4478, 3.4486, 3.4520,
]
# SMART Attribute 188: Command Timeout
SMART_188_STEP = 1
SMART_188_R = [
0.0025, 0.0129, 0.0182, 0.0215, 0.0236, 0.0257, 0.0279, 0.0308, 0.0341, 0.0382,
0.0430, 0.0491, 0.0565, 0.0658, 0.0770, 0.0906, 0.1037, 0.1197, 0.1355, 0.1525,
0.1686, 0.1864, 0.2011, 0.2157, 0.2281, 0.2404, 0.2505, 0.2591, 0.2676, 0.2766,
0.2827, 0.2913, 0.2999, 0.3100, 0.3185, 0.3298, 0.3361, 0.3446, 0.3506, 0.3665,
0.3699, 0.3820, 0.3890, 0.4059, 0.4108, 0.4255, 0.4290, 0.4424, 0.4473, 0.4617,
0.4667, 0.4770, 0.4829, 0.4977, 0.4997, 0.5102, 0.5137, 0.5283, 0.5316, 0.5428,
0.5480, 0.5597, 0.5634, 0.5791, 0.5826, 0.5929, 0.5945, 0.6025, 0.6102, 0.6175,
0.6245, 0.6313, 0.6421, 0.6468, 0.6497, 0.6557, 0.6570, 0.6647, 0.6698, 0.6769,
0.6849, 0.6884, 0.6925, 0.7025, 0.7073, 0.7161, 0.7223, 0.7256, 0.7280, 0.7411,
0.7445, 0.7530, 0.7628, 0.7755, 0.7900, 0.8006, 0.8050, 0.8098, 0.8132, 0.8192,
0.8230, 0.8293, 0.8356, 0.8440, 0.8491, 0.8672, 0.8766, 0.8907, 0.8934, 0.8992,
0.9062, 0.9111, 0.9209, 0.9290, 0.9329, 0.9378, 0.9385, 0.9402, 0.9427, 0.9448,
0.9459, 0.9568, 0.9626, 0.9628, 0.9730, 0.9765, 0.9797, 0.9825, 0.9873, 0.9902,
0.9926, 0.9991, 1.0031, 1.0044, 1.0062, 1.0120, 1.0148, 1.0188, 1.0218, 1.0231,
1.0249, 1.0277, 1.0335, 1.0355, 1.0417, 1.0467, 1.0474, 1.0510, 1.0529, 1.0532,
1.0562, 1.0610, 1.0702, 1.0708, 1.0800, 1.0804, 1.0845, 1.1120, 1.1191, 1.1225,
1.1264, 1.1265, 1.1335, 1.1347, 1.1479, 1.1479, 1.1519, 1.1545, 1.1645, 1.1646,
1.1647, 1.1649, 1.1678, 1.1713, 1.1723, 1.1733, 1.1736, 1.1736, 1.1738, 1.1739,
1.1739, 1.1741, 1.1741, 1.1746, 1.1746, 1.1748, 1.1750, 1.1760, 1.1794, 1.1854,
1.1908, 1.1912, 1.1912, 1.1971, 1.2033, 1.2033, 1.2120, 1.2166, 1.2185, 1.2185,
1.2189, 1.2211, 1.2226, 1.2234, 1.2320, 1.2345, 1.2345, 1.2347, 1.2350, 1.2350,
1.2407, 1.2408, 1.2408, 1.2408, 1.2409, 1.2460, 1.2518, 1.2519, 1.2519, 1.2519,
1.2520, 1.2520, 1.2521, 1.2521, 1.2521, 1.2593, 1.2745, 1.2760, 1.2772, 1.2831,
1.2833, 1.2890, 1.2906, 1.3166, 1.3201, 1.3202, 1.3202, 1.3202, 1.3204, 1.3204,
1.3314, 1.3422, 1.3423, 1.3441, 1.3491, 1.3583, 1.3602, 1.3606, 1.3636, 1.3650,
1.3661, 1.3703, 1.3708, 1.3716, 1.3730, 1.3731,
]
# SMART Attribute 197: Current Pending Sector Count
SMART_197_STEP = 1
SMART_197_R = [
0.0028, 0.2972, 0.3883, 0.4363, 0.4644, 0.4813, 0.4948, 0.5051, 0.5499, 0.8535,
0.8678, 0.8767, 0.8882, 0.8933, 0.9012, 0.9076, 0.9368, 1.1946, 1.2000, 1.2110,
1.2177, 1.2305, 1.2385, 1.2447, 1.2699, 1.4713, 1.4771, 1.4802, 1.4887, 1.5292,
1.5384, 1.5442, 1.5645, 1.7700, 1.7755, 1.7778, 1.7899, 1.7912, 1.7991, 1.7998,
1.8090, 1.9974, 1.9992, 2.0088, 2.0132, 2.0146, 2.0161, 2.0171, 2.0273, 2.1845,
2.1866, 2.1877, 2.1900, 2.1922, 2.1944, 2.1974, 2.2091, 2.3432, 2.3459, 2.3463,
2.3468, 2.3496, 2.3503, 2.3533, 2.3593, 2.4604, 2.4606, 2.4609, 2.4612, 2.4620,
2.4626, 2.4638, 2.4689, 2.5575, 2.5581, 2.5586, 2.5586, 2.5588, 2.5602, 2.5602,
2.5648, 2.6769, 2.6769, 2.6769, 2.6794, 2.6805, 2.6811, 2.6814, 2.6862, 2.7742,
2.7755, 2.7771, 2.7780, 2.7790, 2.7797, 2.7807, 2.7871, 2.9466, 2.9478, 2.9492,
2.9612, 2.9618, 2.9624, 2.9628, 2.9669, 3.1467, 3.1481, 3.1494, 3.1499, 3.1504,
3.1507, 3.1509, 3.1532, 3.2675, 3.2681, 3.2703, 3.2712, 3.2714, 3.2726, 3.2726,
3.2743, 3.3376, 3.3379, 3.3382, 3.3397, 3.3403, 3.3410, 3.3410, 3.3429, 3.4052,
3.4052, 3.4052, 3.4052, 3.4052, 3.4053, 3.4053, 3.4075, 3.4616, 3.4616, 3.4616,
3.4616, 3.4616, 3.4616, 3.4620, 3.4634, 3.4975, 3.4975, 3.4975, 3.4975, 3.4979,
3.4979, 3.4979, 3.4998, 3.5489, 3.5489, 3.5489, 3.5489, 3.5489, 3.5493, 3.5497,
3.5512, 3.5827, 3.5828, 3.5828, 3.5828, 3.5828, 3.5828, 3.5828, 3.5844, 3.6251,
3.6251, 3.6251, 3.6267, 3.6267, 3.6271, 3.6271, 3.6279, 3.6562, 3.6562, 3.6563,
3.7206, 3.7242, 3.7332, 3.7332, 3.7346, 3.7548, 3.7548, 3.7553, 3.7576, 3.7581,
3.7586, 3.7587, 3.7600, 3.7773, 3.7812, 3.7836, 3.7841, 3.7842, 3.7851, 3.7856,
3.7876, 3.8890, 3.8890, 3.8890, 3.8890, 3.8890, 3.8890, 3.8890, 3.8897, 3.9111,
3.9114, 3.9114, 3.9114, 3.9114, 3.9114, 3.9114, 3.9126, 3.9440, 3.9440, 3.9440,
3.9440, 3.9440, 3.9498, 3.9498, 3.9509, 3.9783, 3.9783, 3.9784, 3.9784, 3.9784,
3.9784, 4.0012, 4.0019, 4.0406, 4.0413, 4.0413, 4.0413, 4.0413, 4.0414, 4.0414,
4.0421, 4.0552, 4.0552, 4.0558, 4.0558, 4.0558, 4.0558, 4.0558, 4.0563, 4.0753,
4.0753, 4.0760, 4.1131, 4.1131, 4.1131, 4.1131,
]
# SMART Attribute 193: Load Cycle Count (removed in SnapRAID v13.0)
# Included for compatibility with older SnapRAID versions
SMART_193_STEP = 649
SMART_193_R = [
0.0000, 0.0016, 0.0032, 0.0036, 0.0039,
0.0042, 0.0046, 0.0049, 0.0052, 0.0054,
0.0057, 0.0060, 0.0062, 0.0065, 0.0068,
0.0071, 0.0074, 0.0077, 0.0080, 0.0083,
0.0086, 0.0091, 0.0094, 0.0098, 0.0101,
0.0104, 0.0108, 0.0111, 0.0119, 0.0122,
0.0127, 0.0130, 0.0134, 0.0137, 0.0141,
0.0144, 0.0146, 0.0152, 0.0155, 0.0159,
0.0163, 0.0165, 0.0168, 0.0172, 0.0176,
0.0179, 0.0184, 0.0188, 0.0190, 0.0194,
0.0197, 0.0201, 0.0204, 0.0207, 0.0209,
0.0213, 0.0215, 0.0219, 0.0221, 0.0225,
0.0229, 0.0234, 0.0241, 0.0246, 0.0253,
0.0263, 0.0278, 0.0286, 0.0293, 0.0298,
0.0302, 0.0306, 0.0311, 0.0315, 0.0319,
0.0322, 0.0329, 0.0334, 0.0338, 0.0343,
0.0348, 0.0352, 0.0358, 0.0362, 0.0367,
0.0371, 0.0374, 0.0378, 0.0383, 0.0388,
0.0393, 0.0397, 0.0401, 0.0404, 0.0410,
0.0416, 0.0422, 0.0428, 0.0436, 0.0443,
0.0449, 0.0454, 0.0457, 0.0462, 0.0468,
0.0473, 0.0479, 0.0483, 0.0488, 0.0491,
0.0493, 0.0497, 0.0500, 0.0504, 0.0507,
0.0510, 0.0514, 0.0519, 0.0523, 0.0528,
0.0533, 0.0538, 0.0542, 0.0547, 0.0551,
0.0556, 0.0560, 0.0565, 0.0572, 0.0577,
0.0584, 0.0590, 0.0594, 0.0599, 0.0603,
0.0607, 0.0611, 0.0616, 0.0621, 0.0626,
0.0632, 0.0639, 0.0647, 0.0655, 0.0661,
0.0669, 0.0676, 0.0683, 0.0691, 0.0699,
0.0708, 0.0713, 0.0719, 0.0724, 0.0730,
0.0736, 0.0745, 0.0751, 0.0759, 0.0769,
0.0779, 0.0787, 0.0796, 0.0804, 0.0815,
0.0825, 0.0833, 0.0840, 0.0847, 0.0854,
0.0859, 0.0865, 0.0873, 0.0881, 0.0890,
0.0900, 0.0912, 0.0919, 0.0929, 0.0942,
0.0956, 0.0965, 0.0976, 0.0986, 0.0995,
0.1006, 0.1019, 0.1031, 0.1038, 0.1045,
0.1051, 0.1058, 0.1066, 0.1072, 0.1077,
0.1084, 0.1091, 0.1099, 0.1104, 0.1111,
0.1118, 0.1127, 0.1135, 0.1142, 0.1149,
0.1157, 0.1163, 0.1168, 0.1173, 0.1179,
0.1184, 0.1189, 0.1195, 0.1203, 0.1208,
0.1213, 0.1223, 0.1231, 0.1240, 0.1246,
0.1252, 0.1260, 0.1269, 0.1276, 0.1287,
0.1303, 0.1311, 0.1319, 0.1328, 0.1335,
0.1341, 0.1348, 0.1362, 0.1373, 0.1380,
0.1387, 0.1392, 0.1398, 0.1403, 0.1408,
0.1412, 0.1418, 0.1422, 0.1428, 0.1434,
0.1439, 0.1445, 0.1451, 0.1457, 0.1464,
0.1469, 0.1475, 0.1480, 0.1486, 0.1491,
0.1498,
]
# SMART Attribute 198: Offline Uncorrectable Sector Count
SMART_198_STEP = 1
SMART_198_R = [
0.0030, 0.5479, 0.5807, 0.5949, 0.6046, 0.6086, 0.6139, 0.6224, 0.6639, 1.0308,
1.0329, 1.0364, 1.0371, 1.0387, 1.0399, 1.0421, 1.0675, 1.3730, 1.3733, 1.3741,
1.3741, 1.3752, 1.3794, 1.3800, 1.3985, 1.6291, 1.6303, 1.6309, 1.6352, 1.6384,
1.6448, 1.6464, 1.6645, 1.8949, 1.8951, 1.8962, 1.9073, 1.9073, 1.9152, 1.9161,
1.9240, 2.1308, 2.1315, 2.1328, 2.1328, 2.1328, 2.1328, 2.1329, 2.1439, 2.3203,
2.3205, 2.3205, 2.3205, 2.3205, 2.3205, 2.3205, 2.3265, 2.4729, 2.4729, 2.4729,
2.4729, 2.4729, 2.4729, 2.4729, 2.4778, 2.5900, 2.5900, 2.5901, 2.5901, 2.5901,
2.5901, 2.5901, 2.5949, 2.6964, 2.6965, 2.6965, 2.6965, 2.6965, 2.6965, 2.6965,
2.7010, 2.8328, 2.8328, 2.8328, 2.8329, 2.8329, 2.8329, 2.8329, 2.8366, 2.9405,
2.9405, 2.9405, 2.9405, 2.9405, 2.9405, 2.9405, 2.9442, 3.1344, 3.1344, 3.1346,
3.1463, 3.1463, 3.1463, 3.1463, 3.1493, 3.3076, 3.3076, 3.3076, 3.3076, 3.3076,
3.3077, 3.3077, 3.3097, 3.4456, 3.4456, 3.4456, 3.4456, 3.4456, 3.4456, 3.4456,
3.4473, 3.5236, 3.5236, 3.5236, 3.5236, 3.5236, 3.5236, 3.5236, 3.5249, 3.6004,
3.6004, 3.6004, 3.6004, 3.6004, 3.6004, 3.6004, 3.6026, 3.6684, 3.6684, 3.6684,
3.6684, 3.6684, 3.6684, 3.6684, 3.6697, 3.7121, 3.7121, 3.7121, 3.7121, 3.7121,
3.7121, 3.7121, 3.7136, 3.7744, 3.7744, 3.7744, 3.7744, 3.7744, 3.7745, 3.7745,
3.7756, 3.8151, 3.8151, 3.8151, 3.8151, 3.8151, 3.8151, 3.8151, 3.8163, 3.8673,
3.8673, 3.8673, 3.8673, 3.8673, 3.8673, 3.8673, 3.8680, 3.9044, 3.9044, 3.9044,
3.9044, 3.9044, 3.9044, 3.9044, 3.9056, 3.9297, 3.9297, 3.9297, 3.9297, 3.9297,
3.9297, 3.9297, 3.9305, 3.9494, 3.9494, 3.9494, 3.9494, 3.9494, 3.9494, 3.9494,
3.9514, 4.0725, 4.0725, 4.0725, 4.0725, 4.0725, 4.0725, 4.0725, 4.0731, 4.0990,
4.0993, 4.0993, 4.0993, 4.0993, 4.0993, 4.0993, 4.1004, 4.1385, 4.1385, 4.1385,
4.1386, 4.1386, 4.1387, 4.1387, 4.1398, 4.1732, 4.2284, 4.2284, 4.2284, 4.2284,
4.2284, 4.2284, 4.2290, 4.2781, 4.2781, 4.2963, 4.2963, 4.2963, 4.2963, 4.2963,
4.2971, 4.3141, 4.3141, 4.3141, 4.3141, 4.3141, 4.3141, 4.3141, 4.3146, 4.3393,
4.3393, 4.3393, 4.3393, 4.3393, 4.3393, 4.3393,
]
def calculate_afr(smart_attributes: dict, is_seagate: bool = False, include_193: bool = False) -> float:
"""
Calculate Annual Failure Rate (AFR) based on SMART attributes.
Uses BackBlaze dataset to predict 1-year failure probability.
Returns the maximum AFR across all relevant SMART attributes.
Args:
smart_attributes: Dict mapping SMART attribute IDs to their values
is_seagate: Whether this is a Seagate disk (affects attr 188 handling)
include_193: Include attribute 193 (Load Cycle Count) for SnapRAID pre-v13.0 compatibility
Returns:
AFR as a decimal (e.g., 0.81 for 81%)
"""
afr_values = []
# Helper function to get AFR from a table
def get_afr_from_table(attr_id: int, table: list, step: int) -> float:
if attr_id not in smart_attributes:
return 0.0
value = smart_attributes[attr_id]
index = min(value // step, len(table) - 1)
monthly_rate = table[index]
# Annualize: convert monthly to annual (like SnapRAID does)
return (365.0 / 30.0) * monthly_rate
# Check each attribute
afr_values.append(get_afr_from_table(5, SMART_5_R, SMART_5_STEP))
afr_values.append(get_afr_from_table(187, SMART_187_R, SMART_187_STEP))
# Exclude attribute 188 for Seagate disks (false positives on SMR/IronWolf)
if not is_seagate:
afr_values.append(get_afr_from_table(188, SMART_188_R, SMART_188_STEP))
# Attribute 193 (Load Cycle Count) - removed in SnapRAID v13.0
# Include for compatibility with older SnapRAID versions
if include_193:
afr_values.append(get_afr_from_table(193, SMART_193_R, SMART_193_STEP))
afr_values.append(get_afr_from_table(197, SMART_197_R, SMART_197_STEP))
afr_values.append(get_afr_from_table(198, SMART_198_R, SMART_198_STEP))
# Return the maximum AFR (attributes are correlated, not independent)
return max(afr_values) if afr_values else 0.0
def afr_to_failure_probability(afr: float) -> float:
"""
Convert Annual Failure Rate to failure probability using Poisson distribution.
SnapRAID uses: P(at least 1 failure) = 1 - e^(-AFR)
Args:
afr: Annual Failure Rate (e.g., 1.64 for high risk)
Returns:
Failure probability as a decimal (e.g., 0.806 for 80.6%)
"""
return 1.0 - math.exp(-afr)
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
Check if Scrutiny has failure_rate fields we should be using
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
def check_scrutiny_failure_rates():
"""Check Scrutiny's own failure rate calculations."""
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# Get failure_rate fields
query = f'''
from(bucket: "{bucket}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) => r._field =~ /failure_rate/)
|> last()
'''
print("=" * 80)
print("Scrutiny's Failure Rate Calculations")
print("=" * 80)
result = query_api.query(query)
devices = {}
for table in result:
for record in table.records:
wwn = record.values.get('device_wwn')
field = record.get_field()
value = record.get_value()
if wwn not in devices:
devices[wwn] = {}
# Extract attribute ID
parts = field.split('.')
if len(parts) >= 2:
attr_id = parts[1]
devices[wwn][attr_id] = value
# Show per device
for wwn, attrs in sorted(devices.items()):
print(f"\nDevice {wwn}:")
max_rate = 0
for attr_id, rate in sorted(attrs.items(), key=lambda x: int(x[0])):
if rate and rate > 0:
print(f" Attr {attr_id:3s}: {rate:.4f}")
max_rate = max(max_rate, rate)
if max_rate > 0:
print(f" MAX: {max_rate:.4f} ({max_rate*100:.2f}%)")
client.close()
if __name__ == '__main__':
check_scrutiny_failure_rates()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Debug script to see actual SMART attribute values from InfluxDB
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
def debug_smart_values():
"""Fetch and display raw SMART values for debugging."""
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# Get all SMART attribute fields for one device
query = f'''
from(bucket: "{bucket}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) => r.device_wwn == "0x5000c500c570663e")
|> filter(fn: (r) =>
r._field =~ /attr\\.5\\.(raw_value|transformed_value|value)/ or
r._field =~ /attr\\.187\\.(raw_value|transformed_value|value)/ or
r._field =~ /attr\\.188\\.(raw_value|transformed_value|value)/ or
r._field =~ /attr\\.197\\.(raw_value|transformed_value|value)/ or
r._field =~ /attr\\.198\\.(raw_value|transformed_value|value)/ or
r._field =~ /attr\\.9\\.(raw_value|transformed_value|value)/
)
|> last()
'''
print("=" * 80)
print("SMART Attribute Values for /dev/sdc (ZLW0A3QJ) - Should be 81% failure")
print("=" * 80)
result = query_api.query(query)
values = {}
for table in result:
for record in table.records:
field = record.get_field()
value = record.get_value()
values[field] = value
# Group by attribute
attrs = {}
for field, value in sorted(values.items()):
parts = field.split('.')
if len(parts) >= 3:
attr_id = parts[1]
field_type = parts[2]
if attr_id not in attrs:
attrs[attr_id] = {}
attrs[attr_id][field_type] = value
# Display
for attr_id in sorted(attrs.keys(), key=int):
print(f"\nAttribute {attr_id}:")
attr_data = attrs[attr_id]
raw = attr_data.get('raw_value', 'N/A')
transformed = attr_data.get('transformed_value', 'N/A')
value = attr_data.get('value', 'N/A')
print(f" raw_value: {raw}")
if isinstance(raw, int):
print(f" hex: 0x{raw:012x}")
print(f" low byte: {raw & 0xFF}")
print(f" low word (16-bit): {raw & 0xFFFF}")
print(f" low 32-bit: {raw & 0xFFFFFFFF}")
print(f" transformed_value: {transformed}")
print(f" value: {value}")
client.close()
if __name__ == '__main__':
debug_smart_values()
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
Check the VALUE field (not raw_value) for SMART attributes
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# Get VALUE field (normalized 0-100) for critical attributes
query = f'''
from(bucket: "{bucket}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) => r.device_wwn == "0x5000c500c570663e")
|> filter(fn: (r) =>
r._field == "attr.5.value" or
r._field == "attr.187.value" or
r._field == "attr.188.value" or
r._field == "attr.197.value" or
r._field == "attr.198.value" or
r._field == "attr.5.raw_value" or
r._field == "attr.187.raw_value" or
r._field == "attr.188.raw_value" or
r._field == "attr.197.raw_value" or
r._field == "attr.198.raw_value"
)
|> last()
'''
print("=" * 80)
print("/dev/sdc (ZLW0A3QJ) - should show 81% failure")
print("Comparing InfluxDB values to smartctl output")
print("=" * 80)
result = query_api.query(query)
attrs = {}
for table in result:
for record in table.records:
field = record.get_field()
value = record.get_value()
parts = field.split('.')
if len(parts) >= 3:
attr_id = parts[1]
field_type = parts[2]
if attr_id not in attrs:
attrs[attr_id] = {}
attrs[attr_id][field_type] = value
print("\nInfluxDB Data:")
print("-" * 80)
for attr_id in sorted(attrs.keys(), key=int):
print(f"\nAttribute {attr_id}:")
print(f" value (normalized): {attrs[attr_id].get('value', 'N/A')}")
print(f" raw_value: {attrs[attr_id].get('raw_value', 'N/A')}")
print("\n" + "=" * 80)
print("From smartctl -A /dev/sdc:")
print("-" * 80)
print(" 5 Reallocated_Sector_Ct: VALUE=100, RAW_VALUE=0")
print("187 Reported_Uncorrect: VALUE=100, RAW_VALUE=0")
print("188 Command_Timeout: VALUE=100, RAW_VALUE=0")
print("197 Current_Pending_Sector: VALUE=100, RAW_VALUE=0")
print("198 Offline_Uncorrectable: VALUE=100, RAW_VALUE=0")
client.close()
+10
View File
@@ -0,0 +1,10 @@
0x5000c500744487c5 = /dev/sda [parity]
0x5000c500c455c45e = /dev/sdf
0x5000c500c46b0832 = /dev/sdi
0x5000c500c570663e = /dev/sdc [d1]
0x5000c500e584b9c8 = /dev/sdd [d5]
0x5000c500e7a6a520 = /dev/sdb [d2]
0x5000cca234c69287 = /dev/sde
0x5000cca266e83d77 = /dev/sdh [2-parity]
0x5000cca26ae062f3 = /dev/sdg [2-parity]
25243x801438 = /dev/nvme0n1 [os]
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Explore Scrutiny's InfluxDB schema to understand how SMART data is stored.
Run this first to understand the data structure before running the main logger.
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
def explore_influxdb():
"""Connect to InfluxDB and explore the schema."""
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
print(f"Connecting to InfluxDB at {url}")
print(f"Organization: {org}")
print(f"Bucket: {bucket}\n")
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# 1. List all measurements in the bucket
print("=" * 80)
print("STEP 1: Discovering measurements in bucket")
print("=" * 80)
query_measurements = f'''
import "influxdata/influxdb/schema"
schema.measurements(bucket: "{bucket}")
'''
try:
result = query_api.query(query_measurements)
measurements = []
for table in result:
for record in table.records:
measurement = record.get_value()
measurements.append(measurement)
print(f" - {measurement}")
if not measurements:
print(" No measurements found!")
return
print(f"\nFound {len(measurements)} measurement(s)\n")
# 2. For each measurement, show field keys
print("=" * 80)
print("STEP 2: Discovering fields for each measurement")
print("=" * 80)
for measurement in measurements[:5]: # Limit to first 5 measurements
print(f"\nMeasurement: {measurement}")
query_fields = f'''
import "influxdata/influxdb/schema"
schema.measurementFieldKeys(
bucket: "{bucket}",
measurement: "{measurement}"
)
'''
result = query_api.query(query_fields)
for table in result:
for record in table.records:
field = record.get_value()
print(f" Field: {field}")
# 3. Show sample data from the first measurement
print("\n" + "=" * 80)
print("STEP 3: Sample data from first measurement")
print("=" * 80)
first_measurement = measurements[0]
query_sample = f'''
from(bucket: "{bucket}")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "{first_measurement}")
|> limit(n: 10)
'''
print(f"\nSample records from '{first_measurement}':\n")
result = query_api.query(query_sample)
for table in result:
for record in table.records:
print(f"Time: {record.get_time()}")
print(f" Measurement: {record.get_measurement()}")
print(f" Field: {record.get_field()}")
print(f" Value: {record.get_value()}")
print(f" Tags: {record.values}")
print()
except Exception as e:
print(f"Error querying InfluxDB: {e}")
import traceback
traceback.print_exc()
finally:
client.close()
if __name__ == '__main__':
if not os.getenv('INFLUXDB_URL'):
print("ERROR: Please create a .env file with your InfluxDB configuration")
print("Copy .env.example to .env and fill in your details")
exit(1)
explore_influxdb()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Helper script to extract WWN to device mappings from InfluxDB.
Run this to help populate the devices table.
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
def get_device_info():
"""Extract unique device WWNs and their latest data from InfluxDB."""
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
print(f"Connecting to InfluxDB at {url}...")
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# Get unique device WWNs with their latest data
query = f'''
from(bucket: "{bucket}")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) =>
r._field == "temp" or
r._field == "power_on_hours"
)
|> last()
|> group(columns: ["device_wwn", "device_protocol"])
'''
print("\nDiscovered devices:\n")
print("=" * 80)
result = query_api.query(query)
devices = {}
for table in result:
for record in table.records:
wwn = record.values.get('device_wwn')
protocol = record.values.get('device_protocol')
if wwn not in devices:
devices[wwn] = {
'wwn': wwn,
'protocol': protocol,
'temp': None,
'power_on_hours': None
}
field = record.get_field()
if field == 'temp':
devices[wwn]['temp'] = record.get_value()
elif field == 'power_on_hours':
devices[wwn]['power_on_hours'] = record.get_value()
# Print devices
for i, (wwn, info) in enumerate(sorted(devices.items()), 1):
power_on_days = int(info['power_on_hours']) // 24 if info['power_on_hours'] else 0
print(f"Device {i}:")
print(f" WWN: {wwn}")
print(f" Protocol: {info['protocol']}")
print(f" Temperature: {info['temp']}°C")
print(f" Power-On Time: {power_on_days} days ({info['power_on_hours']} hours)")
print()
print("=" * 80)
print("\nNow run 'snapraid smart' or 'smartctl -a /dev/sdX' on each device to get:")
print(" - Serial number")
print(" - Model name")
print(" - Capacity")
print(" - Device path (/dev/sdX)")
print("\nThen update populate_devices.sql with this information.")
client.close()
if __name__ == '__main__':
if not os.getenv('INFLUXDB_URL'):
print("ERROR: Please create a .env file with your InfluxDB configuration")
exit(1)
get_device_info()
+87
View File
@@ -0,0 +1,87 @@
-- Populate devices table with your drive metadata
-- Replace the placeholder values with actual information from your drives
-- Device 1: WWN 0x5000c500744487c5
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500744487c5',
'/dev/sdc', -- Update with actual device path
'ZLW0A3QJ', -- Update with actual serial
'WDC WD120EFAX', -- Update with actual model
'Western Digital', -- Update with manufacturer
12000000000000, -- Update with capacity in bytes
12.0, -- Size in TB
'd1', -- SnapRAID role (d1, d2, parity, 2-parity, or '-' for unused)
'Data drive 1' -- Optional notes
);
-- Device 2: WWN 0x5000c500c455c45e
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c455c45e',
'/dev/sdb',
'ZX22EJ3Y',
'WDC WD200EFAX',
'Western Digital',
20000000000000,
20.0,
'd2',
'Data drive 2'
);
-- Device 3: WWN 0x5000c500c46b0832
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c46b0832',
'/dev/sdd',
'ZTN1BNEZ',
'WDC WD120EFAX',
'Western Digital',
12000000000000,
12.0,
'd5',
'Data drive 5'
);
-- Device 4: WWN 0x5000c500c570663e
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c570663e',
'/dev/sda',
'ZX20AAMV',
'WDC WD200EFAX',
'Western Digital',
20000000000000,
20.0,
'parity',
'Parity drive'
);
-- Device 5: WWN 0x5000c500e584b9c8
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500e584b9c8',
'/dev/sdg',
'2TJ97M6D',
'WDC WD100EFAX',
'Western Digital',
10000000000000,
10.0,
'2-parity',
'Second parity drive'
);
-- Add remaining 4 devices following the same pattern
-- Template:
-- INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
-- VALUES (
-- '0xXXXXXXXXXXXXXXXX', -- WWN from InfluxDB
-- '/dev/sdX', -- Device path
-- 'SERIAL', -- Serial number from snapraid smart or smartctl
-- 'MODEL', -- Model number
-- 'MANUFACTURER', -- Manufacturer name
-- 0, -- Capacity in bytes
-- 0.0, -- Size in TB
-- '-', -- SnapRAID role
-- '' -- Notes
-- );
+149
View File
@@ -0,0 +1,149 @@
-- Populate devices table with actual drive data
-- Based on device_mappings.txt and snapraid smart output
-- Device 1: /dev/sda [parity]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500744487c5',
'/dev/sda',
'ZX20AAMV',
'WDC WD200EFAX',
'Western Digital',
20000000000000,
20.0,
'parity',
'Parity drive - 636 days old'
);
-- Device 2: /dev/sdb [d2]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500e7a6a520',
'/dev/sdb',
'ZX22EJ3Y',
'WDC WD200EFAX',
'Western Digital',
20000000000000,
20.0,
'd2',
'Data drive 2 - 637 days old'
);
-- Device 3: /dev/sdc [d1]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c570663e',
'/dev/sdc',
'ZLW0A3QJ',
'WDC WD120EFAX',
'Western Digital',
12000000000000,
12.0,
'd1',
'Data drive 1 - 1968 days old - HIGH RISK 81%'
);
-- Device 4: /dev/sdd [d5]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500e584b9c8',
'/dev/sdd',
'ZTN1BNEZ',
'WDC WD120EFAX',
'Western Digital',
12000000000000,
12.0,
'd5',
'Data drive 5 - 1014 days old'
);
-- Device 5: /dev/sde (unused)
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000cca234c69287',
'/dev/sde',
'YVGGG6EC',
'WDC WD30EFRX',
'Western Digital',
3000000000000,
3.0,
'-',
'Unused drive - 1983 days old'
);
-- Device 6: /dev/sdf (unused)
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c455c45e',
'/dev/sdf',
'ZA1GX9F4',
'WDC WD80EFAX',
'Western Digital',
8000000000000,
8.0,
'-',
'Unused drive - 2072 days old - HIGH RISK 84%'
);
-- Device 7: /dev/sdg [2-parity]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000cca26ae062f3',
'/dev/sdg',
'2TJ97M6D',
'WDC WD100EFAX',
'Western Digital',
10000000000000,
10.0,
'2-parity',
'Second parity drive - 2540 days old'
);
-- Device 8: /dev/sdh [2-parity]
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000cca266e83d77',
'/dev/sdh',
'7JJVJ65C',
'WDC WD100EFAX',
'Western Digital',
10000000000000,
10.0,
'2-parity',
'Second parity drive - 2354 days old - 1 error reported'
);
-- Device 9: /dev/sdi (unused)
INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
VALUES (
'0x5000c500c46b0832',
'/dev/sdi',
'ZA1GW0XM',
'WDC WD80EFAX',
'Western Digital',
8000000000000,
8.0,
'-',
'Unused drive - 2074 days old - HIGH RISK 84%'
);
-- Device 10: /dev/nvme0n1 [OS drive] - Note: WWN format different for NVMe
-- NVMe devices may not report WWN in the same format, skipping for now
-- You can add this manually if needed after checking actual WWN from InfluxDB
-- INSERT INTO devices (device_wwn, device_path, serial_number, model, manufacturer, capacity_bytes, size_tb, disk_role, notes)
-- VALUES (
-- '25243x801438', -- May need different format
-- '/dev/nvme0n1',
-- '25243X801438',
-- 'NVMe SSD',
-- 'Unknown',
-- NULL,
-- NULL,
-- 'os',
-- 'Operating system drive'
-- );
-- Verify the data
SELECT device_path, serial_number, size_tb, disk_role, notes
FROM devices
ORDER BY device_path;
+4
View File
@@ -0,0 +1,4 @@
influxdb-client>=1.38.0
psycopg[binary]>=3.1.0
python-dotenv>=1.0.0
requests>=2.31.0
+58
View File
@@ -0,0 +1,58 @@
-- PostgreSQL schema for SMART data logging
-- Main table for SMART metrics snapshots
CREATE TABLE IF NOT EXISTS smart_metrics (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
device_path VARCHAR(255) NOT NULL,
serial_number VARCHAR(255) NOT NULL,
model VARCHAR(255),
disk_role VARCHAR(50), -- e.g., 'd1', 'd2', 'parity', '2-parity', or '-' for unused
-- Basic metrics (from SnapRAID smart output)
temperature_celsius INTEGER,
power_on_days INTEGER,
error_count INTEGER,
size_tb DECIMAL(10, 2),
-- Calculated failure probability
failure_probability_pct DECIMAL(5, 2), -- 0.00 to 100.00
-- Raw SMART attributes (key-value pairs as JSONB)
smart_attributes JSONB,
-- Indexes for efficient querying
CONSTRAINT unique_device_timestamp UNIQUE (device_path, timestamp)
);
-- Index on serial number for device history queries
CREATE INDEX IF NOT EXISTS idx_smart_metrics_serial ON smart_metrics(serial_number);
-- Index on timestamp for time-range queries
CREATE INDEX IF NOT EXISTS idx_smart_metrics_timestamp ON smart_metrics(timestamp DESC);
-- Index on device_path for per-device queries
CREATE INDEX IF NOT EXISTS idx_smart_metrics_device ON smart_metrics(device_path);
-- Index on JSONB attributes for querying specific SMART values
CREATE INDEX IF NOT EXISTS idx_smart_attributes_gin ON smart_metrics USING gin (smart_attributes);
-- View for latest metrics per device
CREATE OR REPLACE VIEW smart_latest AS
SELECT DISTINCT ON (device_path)
*
FROM smart_metrics
ORDER BY device_path, timestamp DESC;
-- View for high-risk devices (failure probability > 50%)
CREATE OR REPLACE VIEW smart_high_risk AS
SELECT *
FROM smart_latest
WHERE failure_probability_pct > 50.0
ORDER BY failure_probability_pct DESC;
COMMENT ON TABLE smart_metrics IS 'Historical SMART metrics collected from Scrutiny InfluxDB';
COMMENT ON COLUMN smart_metrics.failure_probability_pct IS 'BackBlaze-based 1-year failure probability (0-100%)';
COMMENT ON COLUMN smart_metrics.smart_attributes IS 'Raw SMART attributes as JSON (attribute_id -> value)';
COMMENT ON VIEW smart_latest IS 'Latest SMART metrics for each device';
COMMENT ON VIEW smart_high_risk IS 'Devices with >50% annual failure probability';
+72
View File
@@ -0,0 +1,72 @@
-- Migration: Add v13.0 failure probability column
-- Adds a second failure probability column for the modern algorithm (without attr 193)
-- Add new column for v13.0+ algorithm (without attribute 193)
ALTER TABLE smart_metrics
ADD COLUMN IF NOT EXISTS failure_probability_v13_pct DECIMAL(5, 2);
-- Rename existing column to clarify it includes attribute 193 (pre-v13.0)
ALTER TABLE smart_metrics
RENAME COLUMN failure_probability_pct TO failure_probability_v12_pct;
-- Update views to include both columns
DROP VIEW IF EXISTS smart_latest CASCADE;
CREATE VIEW smart_latest AS
SELECT
m.id,
m.timestamp,
m.device_wwn,
d.device_path,
d.serial_number,
d.model,
d.manufacturer,
d.capacity_bytes,
d.size_tb,
d.disk_role,
m.temperature_celsius,
m.power_on_days,
m.error_count,
m.failure_probability_v12_pct, -- With attr 193 (matches your current SnapRAID)
m.failure_probability_v13_pct, -- Without attr 193 (modern SnapRAID v13.0+)
m.smart_attributes
FROM smart_metrics m
INNER JOIN devices d ON m.device_wwn = d.device_wwn
WHERE m.timestamp = (
SELECT MAX(m2.timestamp)
FROM smart_metrics m2
WHERE m2.device_wwn = m.device_wwn
);
DROP VIEW IF EXISTS smart_high_risk;
CREATE VIEW smart_high_risk AS
SELECT *
FROM smart_latest
WHERE failure_probability_v12_pct > 50 -- Using v12 since that matches your SnapRAID
ORDER BY failure_probability_v12_pct DESC;
DROP VIEW IF EXISTS smart_summary;
CREATE VIEW smart_summary AS
SELECT
device_path,
serial_number,
temperature_celsius,
power_on_days,
error_count,
failure_probability_v12_pct, -- With attr 193
failure_probability_v13_pct, -- Without attr 193
size_tb,
disk_role
FROM smart_latest
ORDER BY
CASE disk_role
WHEN 'parity' THEN 1
WHEN '2-parity' THEN 2
ELSE 3
END,
device_path;
COMMENT ON COLUMN smart_metrics.failure_probability_v12_pct IS
'Failure probability using SnapRAID <v13.0 algorithm (includes attribute 193 Load Cycle Count)';
COMMENT ON COLUMN smart_metrics.failure_probability_v13_pct IS
'Failure probability using SnapRAID v13.0+ algorithm (excludes attribute 193)';
+106
View File
@@ -0,0 +1,106 @@
-- PostgreSQL schema for SMART data logging (Version 2)
-- Now includes a devices metadata table
-- Device metadata table (populated manually)
CREATE TABLE IF NOT EXISTS devices (
device_wwn VARCHAR(50) PRIMARY KEY, -- e.g., '0x5000c500744487c5'
device_path VARCHAR(255) NOT NULL, -- e.g., '/dev/sda'
serial_number VARCHAR(255) NOT NULL,
model VARCHAR(255),
manufacturer VARCHAR(100),
capacity_bytes BIGINT,
size_tb DECIMAL(10, 2),
disk_role VARCHAR(50), -- e.g., 'd1', 'd2', 'parity', '2-parity', or '-' for unused
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Main table for SMART metrics snapshots
CREATE TABLE IF NOT EXISTS smart_metrics (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
device_wwn VARCHAR(50) NOT NULL, -- Foreign key to devices table
-- Basic metrics (from SnapRAID smart output)
temperature_celsius INTEGER,
power_on_days INTEGER,
error_count INTEGER,
-- Calculated failure probability
failure_probability_pct DECIMAL(5, 2), -- 0.00 to 100.00
-- Raw SMART attributes (key-value pairs as JSONB)
smart_attributes JSONB,
-- Foreign key constraint
CONSTRAINT fk_device FOREIGN KEY (device_wwn) REFERENCES devices(device_wwn),
-- Unique constraint
CONSTRAINT unique_device_timestamp UNIQUE (device_wwn, timestamp)
);
-- Indexes for efficient querying
CREATE INDEX IF NOT EXISTS idx_smart_metrics_device_wwn ON smart_metrics(device_wwn);
CREATE INDEX IF NOT EXISTS idx_smart_metrics_timestamp ON smart_metrics(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_smart_attributes_gin ON smart_metrics USING gin (smart_attributes);
CREATE INDEX IF NOT EXISTS idx_devices_serial ON devices(serial_number);
CREATE INDEX IF NOT EXISTS idx_devices_path ON devices(device_path);
-- View for latest metrics per device with metadata
CREATE OR REPLACE VIEW smart_latest AS
SELECT DISTINCT ON (d.device_wwn)
d.device_wwn,
d.device_path,
d.serial_number,
d.model,
d.manufacturer,
d.size_tb,
d.disk_role,
sm.timestamp,
sm.temperature_celsius,
sm.power_on_days,
sm.error_count,
sm.failure_probability_pct,
sm.smart_attributes
FROM devices d
LEFT JOIN smart_metrics sm ON d.device_wwn = sm.device_wwn
ORDER BY d.device_wwn, sm.timestamp DESC NULLS LAST;
-- View for high-risk devices (failure probability > 50%)
CREATE OR REPLACE VIEW smart_high_risk AS
SELECT *
FROM smart_latest
WHERE failure_probability_pct > 50.0
ORDER BY failure_probability_pct DESC;
-- View for summary (like snapraid smart output)
CREATE OR REPLACE VIEW smart_summary AS
SELECT
device_path,
serial_number,
temperature_celsius as temp_c,
power_on_days,
error_count,
failure_probability_pct as fp_pct,
size_tb,
disk_role,
model,
timestamp as last_updated
FROM smart_latest
ORDER BY
CASE disk_role
WHEN 'parity' THEN 1
WHEN '2-parity' THEN 2
ELSE 3
END,
device_path;
-- Comments
COMMENT ON TABLE devices IS 'Device metadata (WWN, serial, model, etc.) - manually populated';
COMMENT ON TABLE smart_metrics IS 'Historical SMART metrics collected from Scrutiny InfluxDB';
COMMENT ON COLUMN smart_metrics.failure_probability_pct IS 'BackBlaze-based 1-year failure probability (0-100%)';
COMMENT ON COLUMN smart_metrics.smart_attributes IS 'Raw SMART attributes as JSON (attribute_id -> value)';
COMMENT ON VIEW smart_latest IS 'Latest SMART metrics for each device with metadata';
COMMENT ON VIEW smart_high_risk IS 'Devices with >50% annual failure probability';
COMMENT ON VIEW smart_summary IS 'Summary view matching snapraid smart output format';
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# Setup script for PostgreSQL database and user
set -e # Exit on error
# Configuration
PG_HOST="${1:-localhost}"
PG_ADMIN_USER="postgres"
PG_ADMIN_PASS="Lkxdjfhl1d"
DB_NAME="smart_monitoring"
DB_USER="smart_logger"
DB_PASS="${2:-change_me_123}" # Pass as second argument or use default
echo "PostgreSQL Setup for Smart Logger"
echo "=================================="
echo "Host: $PG_HOST"
echo "Database: $DB_NAME"
echo "User: $DB_USER"
echo ""
# Check if psql is installed
if ! command -v psql &> /dev/null; then
echo "ERROR: psql is not installed"
echo "Install with: sudo pacman -S postgresql"
exit 1
fi
echo "Step 1: Creating database..."
PGPASSWORD="$PG_ADMIN_PASS" psql -h "$PG_HOST" -U "$PG_ADMIN_USER" -d postgres <<EOF
-- Create database if it doesn't exist
SELECT 'CREATE DATABASE $DB_NAME'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec
-- Create user if doesn't exist
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$DB_USER') THEN
CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
END IF;
END
\$\$;
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
EOF
echo "✓ Database and user created"
echo ""
echo "Step 2: Creating schema..."
PGPASSWORD="$PG_ADMIN_PASS" psql -h "$PG_HOST" -U "$PG_ADMIN_USER" -d "$DB_NAME" -f schema_v2.sql
echo "✓ Schema created"
echo ""
echo "Step 3: Granting permissions to $DB_USER..."
PGPASSWORD="$PG_ADMIN_PASS" psql -h "$PG_HOST" -U "$PG_ADMIN_USER" -d "$DB_NAME" <<EOF
-- Grant schema usage
GRANT USAGE ON SCHEMA public TO $DB_USER;
-- Grant table permissions
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;
-- Grant default privileges for future objects
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;
EOF
echo "✓ Permissions granted"
echo ""
echo "=========================================="
echo "Setup Complete!"
echo "=========================================="
echo ""
echo "Connection details for your .env file:"
echo ""
echo "POSTGRES_HOST=$PG_HOST"
echo "POSTGRES_PORT=5432"
echo "POSTGRES_DB=$DB_NAME"
echo "POSTGRES_USER=$DB_USER"
echo "POSTGRES_PASSWORD=$DB_PASS"
echo ""
echo "Test connection with:"
echo "psql -h $PG_HOST -U $DB_USER -d $DB_NAME"
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""
SMART Logger - Collect SMART data from Scrutiny InfluxDB,
calculate BackBlaze failure probability, and log to PostgreSQL.
"""
import os
import sys
from datetime import datetime, timezone
from typing import Dict, List, Optional
import json
from influxdb_client import InfluxDBClient
import psycopg
from psycopg.rows import dict_row
from dotenv import load_dotenv
from backblaze_tables import calculate_afr
load_dotenv()
class SmartLogger:
def __init__(self):
# InfluxDB configuration
self.influx_url = os.getenv('INFLUXDB_URL')
self.influx_token = os.getenv('INFLUXDB_TOKEN')
self.influx_org = os.getenv('INFLUXDB_ORG', 'scrutiny')
self.influx_bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
# PostgreSQL configuration
self.pg_host = os.getenv('POSTGRES_HOST', 'localhost')
self.pg_port = int(os.getenv('POSTGRES_PORT', '5432'))
self.pg_db = os.getenv('POSTGRES_DB', 'smart_monitoring')
self.pg_user = os.getenv('POSTGRES_USER', 'postgres')
self.pg_password = os.getenv('POSTGRES_PASSWORD')
self.influx_client = None
self.pg_conn = None
def connect_influxdb(self):
"""Connect to InfluxDB."""
print(f"Connecting to InfluxDB at {self.influx_url}...")
self.influx_client = InfluxDBClient(
url=self.influx_url,
token=self.influx_token,
org=self.influx_org
)
print("✓ Connected to InfluxDB")
def connect_postgres(self):
"""Connect to PostgreSQL."""
print(f"Connecting to PostgreSQL at {self.pg_host}:{self.pg_port}...")
self.pg_conn = psycopg.connect(
host=self.pg_host,
port=self.pg_port,
dbname=self.pg_db,
user=self.pg_user,
password=self.pg_password,
row_factory=dict_row
)
print("✓ Connected to PostgreSQL")
def fetch_smart_data(self) -> List[Dict]:
"""
Fetch latest SMART data from InfluxDB.
NOTE: This query needs to be customized based on your Scrutiny schema.
Run explore_influx.py first to understand the measurement and field names.
Returns:
List of device data dictionaries
"""
query_api = self.influx_client.query_api()
# TODO: Customize this query based on your Scrutiny InfluxDB schema
# This is a template - you'll need to adjust field names and tags
query = f'''
from(bucket: "{self.influx_bucket}")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "smart")
|> last()
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
'''
print("Querying InfluxDB for SMART data...")
result = query_api.query(query)
devices = []
for table in result:
for record in table.records:
# Extract device information
# TODO: Adjust these field names based on your schema
device_data = {
'timestamp': record.get_time(),
'device_path': record.values.get('device', 'unknown'),
'serial_number': record.values.get('serial', 'unknown'),
'model': record.values.get('model', 'unknown'),
'temperature': record.values.get('temp', None),
'power_on_hours': record.values.get('power_on_hours', None),
'smart_attributes': {}
}
# Collect SMART attributes (IDs 5, 187, 188, 197, 198)
# TODO: Map your InfluxDB field names to SMART attribute IDs
for attr_id in [5, 187, 188, 197, 198]:
field_name = f'attr_{attr_id}' # Adjust based on your schema
if field_name in record.values:
device_data['smart_attributes'][attr_id] = record.values[field_name]
devices.append(device_data)
print(f"✓ Found {len(devices)} device(s)")
return devices
def process_device_data(self, devices: List[Dict]) -> List[Dict]:
"""
Process device data and calculate failure probabilities.
Args:
devices: Raw device data from InfluxDB
Returns:
Processed device data with calculated AFR
"""
processed = []
for device in devices:
# Determine if this is a Seagate disk
model = device.get('model', '').lower()
is_seagate = 'seagate' in model
# Calculate failure probability
smart_attrs = device.get('smart_attributes', {})
afr = calculate_afr(smart_attrs, is_seagate=is_seagate)
failure_probability_pct = afr * 100 # Convert to percentage
# Calculate power-on days
power_on_hours = device.get('power_on_hours')
power_on_days = power_on_hours // 24 if power_on_hours else None
# Get error count (SMART attribute 199 or similar)
error_count = smart_attrs.get(199, 0) # TODO: Adjust based on your needs
processed_device = {
'timestamp': device.get('timestamp', datetime.now(timezone.utc)),
'device_path': device.get('device_path'),
'serial_number': device.get('serial_number'),
'model': device.get('model'),
'disk_role': None, # TODO: Map device to snapraid role if needed
'temperature_celsius': device.get('temperature'),
'power_on_days': power_on_days,
'error_count': error_count,
'size_tb': None, # TODO: Get disk size if available
'failure_probability_pct': round(failure_probability_pct, 2),
'smart_attributes': json.dumps(smart_attrs)
}
processed.append(processed_device)
# Print summary
print(f"\nDevice: {processed_device['device_path']}")
print(f" Serial: {processed_device['serial_number']}")
print(f" Model: {processed_device['model']}")
print(f" Temp: {processed_device['temperature_celsius']}°C")
print(f" Power-On: {processed_device['power_on_days']} days")
print(f" Failure Probability: {processed_device['failure_probability_pct']:.2f}%")
return processed
def save_to_postgres(self, devices: List[Dict]):
"""
Save processed device data to PostgreSQL.
Args:
devices: Processed device data
"""
if not devices:
print("No devices to save")
return
print(f"\nSaving {len(devices)} device record(s) to PostgreSQL...")
insert_query = '''
INSERT INTO smart_metrics (
timestamp, device_path, serial_number, model, disk_role,
temperature_celsius, power_on_days, error_count, size_tb,
failure_probability_pct, smart_attributes
) VALUES (
%(timestamp)s, %(device_path)s, %(serial_number)s, %(model)s, %(disk_role)s,
%(temperature_celsius)s, %(power_on_days)s, %(error_count)s, %(size_tb)s,
%(failure_probability_pct)s, %(smart_attributes)s::jsonb
)
ON CONFLICT (device_path, timestamp) DO UPDATE SET
temperature_celsius = EXCLUDED.temperature_celsius,
power_on_days = EXCLUDED.power_on_days,
error_count = EXCLUDED.error_count,
failure_probability_pct = EXCLUDED.failure_probability_pct,
smart_attributes = EXCLUDED.smart_attributes
'''
with self.pg_conn.cursor() as cur:
for device in devices:
cur.execute(insert_query, device)
self.pg_conn.commit()
print("✓ Data saved to PostgreSQL")
def run(self):
"""Main execution flow."""
try:
self.connect_influxdb()
self.connect_postgres()
devices = self.fetch_smart_data()
processed = self.process_device_data(devices)
self.save_to_postgres(processed)
print("\n" + "=" * 80)
print("Summary:")
print("=" * 80)
for device in processed:
fp = device['failure_probability_pct']
status = "🔴 HIGH RISK" if fp > 50 else "🟡 MEDIUM" if fp > 20 else "🟢 OK"
print(f"{status} {device['device_path']}: {fp:.2f}% failure probability")
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
if self.influx_client:
self.influx_client.close()
if self.pg_conn:
self.pg_conn.close()
def main():
# Validate environment variables
required_vars = ['INFLUXDB_URL', 'INFLUXDB_TOKEN', 'POSTGRES_PASSWORD']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}")
print("Please create a .env file based on .env.example")
sys.exit(1)
logger = SmartLogger()
logger.run()
if __name__ == '__main__':
main()
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
SMART Logger V2 - Optimized for Scrutiny's actual InfluxDB schema.
Fetches SMART data from Scrutiny's InfluxDB and device metadata from Scrutiny's API,
calculates BackBlaze failure probability, and logs to PostgreSQL.
"""
import os
import sys
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional
import json
import requests
from influxdb_client import InfluxDBClient
import psycopg
from psycopg.rows import dict_row
from dotenv import load_dotenv
from backblaze_tables import calculate_afr
load_dotenv()
class SmartLogger:
def __init__(self):
# InfluxDB configuration
self.influx_url = os.getenv('INFLUXDB_URL')
self.influx_token = os.getenv('INFLUXDB_TOKEN')
self.influx_org = os.getenv('INFLUXDB_ORG', 'scrutiny')
self.influx_bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
# Scrutiny API configuration
self.scrutiny_api_url = os.getenv('SCRUTINY_API_URL',
self.influx_url.replace(':8086', ':8080'))
# PostgreSQL configuration
self.pg_host = os.getenv('POSTGRES_HOST', 'localhost')
self.pg_port = int(os.getenv('POSTGRES_PORT', '5432'))
self.pg_db = os.getenv('POSTGRES_DB', 'smart_monitoring')
self.pg_user = os.getenv('POSTGRES_USER', 'postgres')
self.pg_password = os.getenv('POSTGRES_PASSWORD')
self.influx_client = None
self.pg_conn = None
self.device_metadata = {} # WWN -> device info mapping
def connect_influxdb(self):
"""Connect to InfluxDB."""
print(f"Connecting to InfluxDB at {self.influx_url}...")
self.influx_client = InfluxDBClient(
url=self.influx_url,
token=self.influx_token,
org=self.influx_org
)
print("✓ Connected to InfluxDB")
def connect_postgres(self):
"""Connect to PostgreSQL."""
print(f"Connecting to PostgreSQL at {self.pg_host}:{self.pg_port}...")
self.pg_conn = psycopg.connect(
host=self.pg_host,
port=self.pg_port,
dbname=self.pg_db,
user=self.pg_user,
password=self.pg_password,
row_factory=dict_row
)
print("✓ Connected to PostgreSQL")
def fetch_device_metadata(self):
"""
Fetch device metadata from Scrutiny API.
This gets serial, model, device path, etc.
"""
print("Fetching device metadata from Scrutiny API...")
try:
response = requests.get(f"{self.scrutiny_api_url}/api/summary")
response.raise_for_status()
data = response.json()
# Parse the summary response
if 'data' in data and 'summary' in data['data']:
for wwn, device_info in data['data']['summary'].items():
device = device_info.get('device', {})
self.device_metadata[wwn] = {
'serial_number': device.get('serial_number', 'unknown'),
'model': device.get('model_name', 'unknown'),
'device_name': device.get('device_name', 'unknown'),
'device_type': device.get('device_type', 'unknown'),
'manufacturer': device.get('manufacturer', 'unknown'),
'capacity': device.get('capacity', 0)
}
print(f"✓ Found metadata for {len(self.device_metadata)} device(s)")
except Exception as e:
print(f"⚠ Warning: Could not fetch device metadata from API: {e}")
print(" Will use WWN as identifier")
def fetch_smart_data_from_influx(self) -> Dict[str, Dict]:
"""
Fetch latest SMART data from InfluxDB for all devices.
Returns:
Dict mapping WWN to device data
"""
query_api = self.influx_client.query_api()
# Query to get latest data for each device
query = f'''
from(bucket: "{self.influx_bucket}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) =>
r._field == "temp" or
r._field == "power_on_hours" or
r._field == "attr.5.raw_value" or
r._field == "attr.187.raw_value" or
r._field == "attr.188.raw_value" or
r._field == "attr.197.raw_value" or
r._field == "attr.198.raw_value" or
r._field == "attr.9.raw_value"
)
|> last()
'''
print("Querying InfluxDB for SMART data...")
result = query_api.query(query)
devices_data = {}
for table in result:
for record in table.records:
wwn = record.values.get('device_wwn')
if not wwn:
continue
if wwn not in devices_data:
devices_data[wwn] = {
'timestamp': record.get_time(),
'wwn': wwn,
'protocol': record.values.get('device_protocol'),
'smart_attributes': {},
'temp': None,
'power_on_hours': None
}
field = record.get_field()
value = record.get_value()
# Map fields to device data
if field == 'temp':
devices_data[wwn]['temp'] = int(value) if value else None
elif field == 'power_on_hours':
devices_data[wwn]['power_on_hours'] = int(value) if value else None
elif field.startswith('attr.'):
# Extract attribute ID from field name like "attr.5.raw_value"
parts = field.split('.')
if len(parts) >= 2:
attr_id = int(parts[1])
# We want raw_value for calculations
if parts[-1] == 'raw_value':
# For most attributes, we want the raw value modulo 2^32
# (InfluxDB stores as int64, but SMART is typically 48-bit)
raw_val = int(value) if value else 0
# Mask to handle potential large values
devices_data[wwn]['smart_attributes'][attr_id] = raw_val & 0xFFFF
print(f"✓ Found data for {len(devices_data)} device(s)")
return devices_data
def process_device_data(self, devices_data: Dict[str, Dict]) -> List[Dict]:
"""
Process device data and calculate failure probabilities.
Args:
devices_data: Dict mapping WWN to device data from InfluxDB
Returns:
Processed device data with calculated AFR
"""
processed = []
for wwn, data in devices_data.items():
# Get device metadata
metadata = self.device_metadata.get(wwn, {})
# Determine if this is a Seagate disk
model = metadata.get('model', '').lower()
manufacturer = metadata.get('manufacturer', '').lower()
is_seagate = 'seagate' in model or 'seagate' in manufacturer
# Calculate failure probability
smart_attrs = data.get('smart_attributes', {})
afr = calculate_afr(smart_attrs, is_seagate=is_seagate)
failure_probability_pct = afr * 100 # Convert to percentage
# Calculate power-on days
power_on_hours = data.get('power_on_hours')
power_on_days = power_on_hours // 24 if power_on_hours else None
# Calculate disk size in TB
capacity_bytes = metadata.get('capacity', 0)
size_tb = round(capacity_bytes / (1000**4), 1) if capacity_bytes else None
# Get device path/name
device_name = metadata.get('device_name', wwn)
processed_device = {
'timestamp': data.get('timestamp', datetime.now(timezone.utc)),
'device_path': device_name,
'serial_number': metadata.get('serial_number', wwn),
'model': metadata.get('model', 'unknown'),
'disk_role': None, # TODO: Map to snapraid role if needed
'temperature_celsius': data.get('temp'),
'power_on_days': power_on_days,
'error_count': smart_attrs.get(199, smart_attrs.get(187, 0)),
'size_tb': size_tb,
'failure_probability_pct': round(failure_probability_pct, 2),
'smart_attributes': json.dumps(smart_attrs)
}
processed.append(processed_device)
# Print summary
print(f"\nDevice: {processed_device['device_path']}")
print(f" Serial: {processed_device['serial_number']}")
print(f" Model: {processed_device['model']}")
print(f" Temp: {processed_device['temperature_celsius']}°C")
print(f" Power-On: {processed_device['power_on_days']} days")
print(f" Size: {processed_device['size_tb']} TB")
print(f" SMART Attrs: {list(smart_attrs.keys())}")
print(f" Failure Probability: {processed_device['failure_probability_pct']:.2f}%")
return processed
def save_to_postgres(self, devices: List[Dict]):
"""
Save processed device data to PostgreSQL.
Args:
devices: Processed device data
"""
if not devices:
print("No devices to save")
return
print(f"\nSaving {len(devices)} device record(s) to PostgreSQL...")
insert_query = '''
INSERT INTO smart_metrics (
timestamp, device_path, serial_number, model, disk_role,
temperature_celsius, power_on_days, error_count, size_tb,
failure_probability_pct, smart_attributes
) VALUES (
%(timestamp)s, %(device_path)s, %(serial_number)s, %(model)s, %(disk_role)s,
%(temperature_celsius)s, %(power_on_days)s, %(error_count)s, %(size_tb)s,
%(failure_probability_pct)s, %(smart_attributes)s::jsonb
)
ON CONFLICT (device_path, timestamp) DO UPDATE SET
temperature_celsius = EXCLUDED.temperature_celsius,
power_on_days = EXCLUDED.power_on_days,
error_count = EXCLUDED.error_count,
failure_probability_pct = EXCLUDED.failure_probability_pct,
smart_attributes = EXCLUDED.smart_attributes
'''
with self.pg_conn.cursor() as cur:
for device in devices:
cur.execute(insert_query, device)
self.pg_conn.commit()
print("✓ Data saved to PostgreSQL")
def run(self):
"""Main execution flow."""
try:
self.connect_influxdb()
self.connect_postgres()
# Fetch device metadata from Scrutiny API
self.fetch_device_metadata()
# Fetch SMART data from InfluxDB
devices_data = self.fetch_smart_data_from_influx()
# Process and calculate failure probabilities
processed = self.process_device_data(devices_data)
# Save to PostgreSQL
self.save_to_postgres(processed)
# Print summary
print("\n" + "=" * 80)
print("Summary:")
print("=" * 80)
for device in sorted(processed, key=lambda x: x['failure_probability_pct'], reverse=True):
fp = device['failure_probability_pct']
status = "🔴 HIGH RISK" if fp > 50 else "🟡 MEDIUM" if fp > 20 else "🟢 OK"
print(f"{status} {device['device_path']:20s} {fp:5.2f}% | "
f"Temp: {device['temperature_celsius']:2d}°C | "
f"Age: {device['power_on_days']:5d} days | "
f"Size: {device['size_tb']:4.1f} TB")
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
if self.influx_client:
self.influx_client.close()
if self.pg_conn:
self.pg_conn.close()
def main():
# Validate environment variables
required_vars = ['INFLUXDB_URL', 'INFLUXDB_TOKEN', 'POSTGRES_PASSWORD']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}")
print("Please create a .env file based on .env.example")
sys.exit(1)
logger = SmartLogger()
logger.run()
if __name__ == '__main__':
main()
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env python3
"""
SMART Logger V3 - Uses PostgreSQL devices table for metadata.
Fetches SMART data from Scrutiny's InfluxDB, looks up device metadata from
PostgreSQL devices table, calculates BackBlaze failure probability, and logs
the enhanced data to PostgreSQL.
"""
import os
import sys
from datetime import datetime, timezone
from typing import Dict, List, Optional
import json
from influxdb_client import InfluxDBClient
import psycopg
from psycopg.rows import dict_row
from dotenv import load_dotenv
from backblaze_tables import calculate_afr, afr_to_failure_probability
load_dotenv()
class SmartLogger:
def __init__(self):
# InfluxDB configuration
self.influx_url = os.getenv('INFLUXDB_URL')
self.influx_token = os.getenv('INFLUXDB_TOKEN')
self.influx_org = os.getenv('INFLUXDB_ORG', 'scrutiny')
self.influx_bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
# PostgreSQL configuration
self.pg_host = os.getenv('POSTGRES_HOST', 'localhost')
self.pg_port = int(os.getenv('POSTGRES_PORT', '5432'))
self.pg_db = os.getenv('POSTGRES_DB', 'smart_monitoring')
self.pg_user = os.getenv('POSTGRES_USER', 'postgres')
self.pg_password = os.getenv('POSTGRES_PASSWORD')
self.influx_client = None
self.pg_conn = None
self.device_metadata = {} # WWN -> device info mapping
def connect_influxdb(self):
"""Connect to InfluxDB."""
print(f"Connecting to InfluxDB at {self.influx_url}...")
self.influx_client = InfluxDBClient(
url=self.influx_url,
token=self.influx_token,
org=self.influx_org
)
print("✓ Connected to InfluxDB")
def connect_postgres(self):
"""Connect to PostgreSQL."""
print(f"Connecting to PostgreSQL at {self.pg_host}:{self.pg_port}...")
self.pg_conn = psycopg.connect(
host=self.pg_host,
port=self.pg_port,
dbname=self.pg_db,
user=self.pg_user,
password=self.pg_password,
row_factory=dict_row
)
print("✓ Connected to PostgreSQL")
def load_device_metadata(self):
"""Load device metadata from PostgreSQL devices table."""
print("Loading device metadata from database...")
query = """
SELECT device_wwn, device_path, serial_number, model, manufacturer,
capacity_bytes, size_tb, disk_role, notes
FROM devices
"""
with self.pg_conn.cursor() as cur:
cur.execute(query)
for row in cur.fetchall():
wwn = row['device_wwn']
self.device_metadata[wwn] = dict(row)
print(f"✓ Loaded metadata for {len(self.device_metadata)} device(s)")
if not self.device_metadata:
print("\n⚠ WARNING: No devices found in the 'devices' table!")
print(" Please populate the devices table using populate_devices.sql")
print(" Run: psql -h <host> -U <user> -d <db> -f populate_devices.sql\n")
def fetch_smart_data_from_influx(self) -> Dict[str, Dict]:
"""
Fetch latest SMART data from InfluxDB for all devices.
Returns:
Dict mapping WWN to device data
"""
query_api = self.influx_client.query_api()
# Query to get latest data for each device
# Include attribute 193 for SnapRAID pre-v13.0 compatibility
query = f'''
from(bucket: "{self.influx_bucket}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "smart")
|> filter(fn: (r) =>
r._field == "temp" or
r._field == "power_on_hours" or
r._field =~ /failure_rate$/ or
r._field == "attr.5.raw_value" or
r._field == "attr.187.raw_value" or
r._field == "attr.188.raw_value" or
r._field == "attr.193.raw_value" or
r._field == "attr.197.raw_value" or
r._field == "attr.198.raw_value" or
r._field == "attr.9.raw_value"
)
|> last()
'''
print("Querying InfluxDB for SMART data...")
result = query_api.query(query)
devices_data = {}
for table in result:
for record in table.records:
wwn = record.values.get('device_wwn')
if not wwn:
continue
if wwn not in devices_data:
devices_data[wwn] = {
'timestamp': record.get_time(),
'wwn': wwn,
'protocol': record.values.get('device_protocol'),
'smart_attributes': {},
'failure_rates': {}, # Scrutiny's pre-calculated rates
'temp': None,
'power_on_hours': None
}
field = record.get_field()
value = record.get_value()
# Map fields to device data
if field == 'temp':
devices_data[wwn]['temp'] = int(value) if value else None
elif field == 'power_on_hours':
devices_data[wwn]['power_on_hours'] = int(value) if value else None
elif field.startswith('attr.'):
# Extract attribute ID from field name like "attr.5.raw_value"
parts = field.split('.')
if len(parts) >= 2:
# Try to parse as integer, skip if not numeric (e.g., NVMe attributes)
try:
attr_id = int(parts[1])
except ValueError:
continue # Skip non-numeric attribute IDs (NVMe, etc.)
# Collect raw_value for logging
if parts[-1] == 'raw_value':
raw_val = int(value) if value else 0
# Apply correct bit mask per attribute (like SnapRAID does)
# 16-bit mask for: 187 (Uncorrectable), 188 (Timeout)
# 32-bit mask for: 5 (Reallocated), 193 (Load Cycle), 197 (Pending), 198 (Offline)
if attr_id in [187, 188]:
devices_data[wwn]['smart_attributes'][attr_id] = raw_val & 0xFFFF
else:
devices_data[wwn]['smart_attributes'][attr_id] = raw_val & 0xFFFFFFFF
# Collect Scrutiny's failure_rate for each attribute
elif parts[-1] == 'failure_rate':
devices_data[wwn]['failure_rates'][attr_id] = float(value) if value else 0.0
print(f"✓ Found data for {len(devices_data)} device(s)")
return devices_data
def process_device_data(self, devices_data: Dict[str, Dict]) -> List[Dict]:
"""
Process device data and calculate failure probabilities.
Args:
devices_data: Dict mapping WWN to device data from InfluxDB
Returns:
Processed device data with calculated AFR
"""
processed = []
for wwn, data in devices_data.items():
# Get device metadata from database
metadata = self.device_metadata.get(wwn, {})
if not metadata:
print(f"\n⚠ Warning: No metadata found for device {wwn}")
print(f" Add this device to the devices table using populate_devices.sql")
continue
# Get SMART attributes
smart_attrs = data.get('smart_attributes', {})
# Determine if Seagate (for attribute 188 handling)
model = metadata.get('model', '').lower()
manufacturer = metadata.get('manufacturer', '').lower()
is_seagate = 'seagate' in model or 'seagate' in manufacturer
# Calculate failure probability BOTH ways for trending
# v12 (with attr 193) - matches your current SnapRAID
afr_v12 = calculate_afr(smart_attrs, is_seagate=is_seagate, include_193=True)
prob_v12 = afr_to_failure_probability(afr_v12)
failure_probability_v12_pct = prob_v12 * 100
# v13 (without attr 193) - modern SnapRAID v13.0+
afr_v13 = calculate_afr(smart_attrs, is_seagate=is_seagate, include_193=False)
prob_v13 = afr_to_failure_probability(afr_v13)
failure_probability_v13_pct = prob_v13 * 100
# Calculate power-on days
power_on_hours = data.get('power_on_hours')
power_on_days = power_on_hours // 24 if power_on_hours else None
processed_device = {
'timestamp': data.get('timestamp', datetime.now(timezone.utc)),
'device_wwn': wwn,
'temperature_celsius': data.get('temp'),
'power_on_days': power_on_days,
'error_count': smart_attrs.get(199, smart_attrs.get(187, 0)),
'failure_probability_v12_pct': round(failure_probability_v12_pct, 2),
'failure_probability_v13_pct': round(failure_probability_v13_pct, 2),
'smart_attributes': json.dumps(smart_attrs)
}
processed.append(processed_device)
# Print summary with metadata
print(f"\n{metadata.get('device_path', wwn)}")
print(f" WWN: {wwn}")
print(f" Serial: {metadata.get('serial_number', 'unknown')}")
print(f" Model: {metadata.get('model', 'unknown')}")
print(f" Role: {metadata.get('disk_role', '-')}")
print(f" Temp: {processed_device['temperature_celsius']}°C")
print(f" Power-On: {processed_device['power_on_days']} days")
print(f" Size: {metadata.get('size_tb', '?')} TB")
print(f" SMART Attrs: {list(smart_attrs.keys())}")
print(f" Failure Probability (v12 w/193): {processed_device['failure_probability_v12_pct']:.2f}%")
print(f" Failure Probability (v13 no193): {processed_device['failure_probability_v13_pct']:.2f}%")
return processed
def save_to_postgres(self, devices: List[Dict]):
"""
Save processed device data to PostgreSQL.
Args:
devices: Processed device data
"""
if not devices:
print("\nNo devices to save")
return
print(f"\nSaving {len(devices)} device record(s) to PostgreSQL...")
insert_query = '''
INSERT INTO smart_metrics (
timestamp, device_wwn,
temperature_celsius, power_on_days, error_count,
failure_probability_v12_pct, failure_probability_v13_pct, smart_attributes
) VALUES (
%(timestamp)s, %(device_wwn)s,
%(temperature_celsius)s, %(power_on_days)s, %(error_count)s,
%(failure_probability_v12_pct)s, %(failure_probability_v13_pct)s, %(smart_attributes)s::jsonb
)
ON CONFLICT (device_wwn, timestamp) DO UPDATE SET
temperature_celsius = EXCLUDED.temperature_celsius,
power_on_days = EXCLUDED.power_on_days,
error_count = EXCLUDED.error_count,
failure_probability_v12_pct = EXCLUDED.failure_probability_v12_pct,
failure_probability_v13_pct = EXCLUDED.failure_probability_v13_pct,
smart_attributes = EXCLUDED.smart_attributes
'''
with self.pg_conn.cursor() as cur:
for device in devices:
cur.execute(insert_query, device)
self.pg_conn.commit()
print("✓ Data saved to PostgreSQL")
def print_summary(self):
"""Print a summary with both v12 and v13 failure calculations."""
print("\n" + "=" * 95)
print("SMART Summary (v12=with attr 193, v13=without attr 193)")
print("=" * 95)
print(f"{'Temp':>5} {'Power':>6} {'Error':>7} {'v12':>4} {'v13':>4} {'Size':>5}")
print(f"{'C':>5} {'OnDays':>6} {'Count':>7} {'FP':>4} {'FP':>4} {'TB':>5} "
f"{'Serial':<16} {'Device':<16} {'Disk':<10}")
print("-" * 95)
query = """
SELECT
device_path,
serial_number,
temperature_celsius,
power_on_days,
error_count,
failure_probability_v12_pct,
failure_probability_v13_pct,
size_tb,
disk_role
FROM smart_summary
ORDER BY
CASE disk_role
WHEN 'parity' THEN 1
WHEN '2-parity' THEN 2
ELSE 3
END,
device_path
"""
with self.pg_conn.cursor() as cur:
cur.execute(query)
for row in cur.fetchall():
temp = row['temperature_celsius'] or 0
days = row['power_on_days'] or 0
errors = row['error_count'] or 0
fp_v12 = int(row['failure_probability_v12_pct'] or 0)
fp_v13 = int(row['failure_probability_v13_pct'] or 0)
size = row['size_tb'] or 0
serial = (row['serial_number'] or '')[:16]
device = (row['device_path'] or '')[:16]
role = row['disk_role'] or '-'
print(f"{temp:5d} {days:6d} {errors:7d} {fp_v12:3d}% {fp_v13:3d}% {size:5.1f} "
f"{serial:<16} {device:<16} {role:<10}")
print()
def run(self):
"""Main execution flow."""
try:
self.connect_influxdb()
self.connect_postgres()
# Load device metadata from PostgreSQL
self.load_device_metadata()
# Fetch SMART data from InfluxDB
devices_data = self.fetch_smart_data_from_influx()
# Process and calculate failure probabilities
processed = self.process_device_data(devices_data)
# Save to PostgreSQL
self.save_to_postgres(processed)
# Print SnapRAID-style summary
self.print_summary()
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
if self.influx_client:
self.influx_client.close()
if self.pg_conn:
self.pg_conn.close()
def main():
# Validate environment variables
required_vars = ['INFLUXDB_URL', 'INFLUXDB_TOKEN', 'POSTGRES_PASSWORD']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}")
print("Please create a .env file based on .env.example")
sys.exit(1)
logger = SmartLogger()
logger.run()
if __name__ == '__main__':
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Test attribute 193 calculation"""
import math
SMART_193_R = [
0.0000, 0.0016, 0.0032, 0.0036, 0.0039,
0.0042, 0.0046, 0.0049, 0.0052, 0.0054,
0.0057, 0.0060, 0.0062, 0.0065, 0.0068,
0.0071, 0.0074, 0.0077, 0.0080, 0.0083,
0.0086, 0.0091, 0.0094, 0.0098, 0.0101,
0.0104, 0.0108, 0.0111, 0.0119, 0.0122,
0.0127, 0.0130, 0.0134, 0.0137, 0.0141,
0.0144, 0.0146, 0.0152, 0.0155, 0.0159,
0.0163, 0.0165, 0.0168, 0.0172, 0.0176,
0.0179, 0.0184, 0.0188, 0.0190, 0.0194,
0.0197, 0.0201, 0.0204, 0.0207, 0.0209,
0.0213, 0.0215, 0.0219, 0.0221, 0.0225,
0.0229, 0.0234, 0.0241, 0.0246, 0.0253,
0.0263, 0.0278, 0.0286, 0.0293, 0.0298,
0.0302, 0.0306, 0.0311, 0.0315, 0.0319,
0.0322, 0.0329, 0.0334, 0.0338, 0.0343,
0.0348, 0.0352, 0.0358, 0.0362, 0.0367,
0.0371, 0.0374, 0.0378, 0.0383, 0.0388,
0.0393, 0.0397, 0.0401, 0.0404, 0.0410,
0.0416, 0.0422, 0.0428, 0.0436, 0.0443,
0.0449, 0.0454, 0.0457, 0.0462, 0.0468,
0.0473, 0.0479, 0.0483, 0.0488, 0.0491,
0.0493, 0.0497, 0.0500, 0.0504, 0.0507,
0.0510, 0.0514, 0.0519, 0.0523, 0.0528,
0.0533, 0.0538, 0.0542, 0.0547, 0.0551,
0.0556, 0.0560, 0.0565, 0.0572, 0.0577,
0.0584, 0.0590, 0.0594, 0.0599, 0.0603,
0.0607, 0.0611, 0.0616, 0.0621, 0.0626,
0.0632, 0.0639, 0.0647, 0.0655, 0.0661,
0.0669, 0.0676, 0.0683, 0.0691, 0.0699,
0.0708, 0.0713, 0.0719, 0.0724, 0.0730,
0.0736, 0.0745, 0.0751, 0.0759, 0.0769,
0.0779, 0.0787, 0.0796, 0.0804, 0.0815,
0.0825, 0.0833, 0.0840, 0.0847, 0.0854,
0.0859, 0.0865, 0.0873, 0.0881, 0.0890,
0.0900, 0.0912, 0.0919, 0.0929, 0.0942,
0.0956, 0.0965, 0.0976, 0.0986, 0.0995,
0.1006, 0.1019, 0.1031, 0.1038, 0.1045,
0.1051, 0.1058, 0.1066, 0.1072, 0.1077,
0.1084, 0.1091, 0.1099, 0.1104, 0.1111,
0.1118, 0.1127, 0.1135, 0.1142, 0.1149,
0.1157, 0.1163, 0.1168, 0.1173, 0.1179,
0.1184, 0.1189, 0.1195, 0.1203, 0.1208,
0.1213, 0.1223, 0.1231, 0.1240, 0.1246,
0.1252, 0.1260, 0.1269, 0.1276, 0.1287,
0.1303, 0.1311, 0.1319, 0.1328, 0.1335,
0.1341, 0.1348, 0.1362, 0.1373, 0.1380,
0.1387, 0.1392, 0.1398, 0.1403, 0.1408,
0.1412, 0.1418, 0.1422, 0.1428, 0.1434,
0.1439, 0.1445, 0.1451, 0.1457, 0.1464,
0.1469, 0.1475, 0.1480, 0.1486, 0.1491,
0.1498,
]
SMART_193_STEP = 649
# User's drive data
raw_value = 150091
# Calculate table index
table_index = raw_value // SMART_193_STEP
print(f"Attribute 193 RAW_VALUE: {raw_value}")
print(f"STEP: {SMART_193_STEP}")
print(f"Table index: {raw_value} // {SMART_193_STEP} = {table_index}")
# Get monthly rate
if table_index >= len(SMART_193_R):
table_index = len(SMART_193_R) - 1
monthly_rate = SMART_193_R[table_index]
print(f"Monthly failure rate (SMART_193_R[{table_index}]): {monthly_rate}")
# Annualize
afr = (365.0 / 30.0) * monthly_rate
print(f"Annualized: (365/30) * {monthly_rate} = {afr:.4f}")
# Apply Poisson
prob = (1 - math.exp(-afr)) * 100
print(f"Poisson P(failure): (1 - e^(-{afr:.4f})) * 100 = {prob:.2f}%")
print("\n" + "="*60)
print(f"SnapRAID reports: 81%")
print(f"Our calculation: {prob:.2f}%")
print("="*60)
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Test BackBlaze calculation with different values"""
import math
from backblaze_tables import SMART_5_R, SMART_187_R, SMART_188_R, SMART_197_R, SMART_198_R
def poisson_prob(rate):
"""Calculate P(at least 1 failure) = 1 - e^(-rate)"""
return 1 - math.exp(-rate)
def test_calculation(attr_value):
"""Test with a given attribute value"""
# Scale monthly to annual (like snapraid does)
afr_5 = (365.0 / 30.0) * SMART_5_R[min(attr_value, 255)]
afr_187 = (365.0 / 30.0) * SMART_187_R[min(attr_value, 255)]
afr_188 = (365.0 / 30.0) * SMART_188_R[min(attr_value, 255)]
afr_197 = (365.0 / 30.0) * SMART_197_R[min(attr_value, 255)]
afr_198 = (365.0 / 30.0) * SMART_198_R[min(attr_value, 255)]
# Take maximum (like snapraid)
max_afr = max(afr_5, afr_187, afr_188, afr_197, afr_198)
# Apply Poisson
prob = poisson_prob(max_afr) * 100
return max_afr, prob
print("Testing different attribute values:")
print("=" * 80)
for test_val in [0, 50, 100, 150, 200, 255]:
afr, prob = test_calculation(test_val)
print(f"Attribute value {test_val:3d}: AFR={afr:.4f}, Probability={prob:.2f}%")
print("\n" + "=" * 80)
print("What we're seeing from /dev/sdc:")
print(" All critical attributes: RAW_VALUE=0, VALUE=100")
print(" SnapRAID reports: 81%")
print(" Our calculation with value=0: ", end="")
afr0, prob0 = test_calculation(0)
print(f"{prob0:.2f}%")
print(" Our calculation with value=100:", end="")
afr100, prob100 = test_calculation(100)
print(f"{prob100:.2f}%")