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:
+321
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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';
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
Executable
+85
@@ -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()
|
||||||
Executable
+79
@@ -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()
|
||||||
@@ -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]
|
||||||
Executable
+112
@@ -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()
|
||||||
Executable
+89
@@ -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()
|
||||||
@@ -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
|
||||||
|
-- );
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
influxdb-client>=1.38.0
|
||||||
|
psycopg[binary]>=3.1.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
requests>=2.31.0
|
||||||
+58
@@ -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';
|
||||||
@@ -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
@@ -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';
|
||||||
Executable
+87
@@ -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"
|
||||||
Executable
+255
@@ -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()
|
||||||
Executable
+335
@@ -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()
|
||||||
Executable
+387
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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}%")
|
||||||
Reference in New Issue
Block a user