Complete rewrite: SnapRAID SMART logger with BackBlaze algorithm

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Wes
2025-12-07 06:55:39 -05:00
co-authored by Claude Sonnet 4.5
commit 27afedb32e
25 changed files with 3207 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Explore Scrutiny's InfluxDB schema to understand how SMART data is stored.
Run this first to understand the data structure before running the main logger.
"""
import os
from influxdb_client import InfluxDBClient
from dotenv import load_dotenv
load_dotenv()
def explore_influxdb():
"""Connect to InfluxDB and explore the schema."""
url = os.getenv('INFLUXDB_URL')
token = os.getenv('INFLUXDB_TOKEN')
org = os.getenv('INFLUXDB_ORG', 'scrutiny')
bucket = os.getenv('INFLUXDB_BUCKET', 'metrics')
print(f"Connecting to InfluxDB at {url}")
print(f"Organization: {org}")
print(f"Bucket: {bucket}\n")
client = InfluxDBClient(url=url, token=token, org=org)
query_api = client.query_api()
# 1. List all measurements in the bucket
print("=" * 80)
print("STEP 1: Discovering measurements in bucket")
print("=" * 80)
query_measurements = f'''
import "influxdata/influxdb/schema"
schema.measurements(bucket: "{bucket}")
'''
try:
result = query_api.query(query_measurements)
measurements = []
for table in result:
for record in table.records:
measurement = record.get_value()
measurements.append(measurement)
print(f" - {measurement}")
if not measurements:
print(" No measurements found!")
return
print(f"\nFound {len(measurements)} measurement(s)\n")
# 2. For each measurement, show field keys
print("=" * 80)
print("STEP 2: Discovering fields for each measurement")
print("=" * 80)
for measurement in measurements[:5]: # Limit to first 5 measurements
print(f"\nMeasurement: {measurement}")
query_fields = f'''
import "influxdata/influxdb/schema"
schema.measurementFieldKeys(
bucket: "{bucket}",
measurement: "{measurement}"
)
'''
result = query_api.query(query_fields)
for table in result:
for record in table.records:
field = record.get_value()
print(f" Field: {field}")
# 3. Show sample data from the first measurement
print("\n" + "=" * 80)
print("STEP 3: Sample data from first measurement")
print("=" * 80)
first_measurement = measurements[0]
query_sample = f'''
from(bucket: "{bucket}")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "{first_measurement}")
|> limit(n: 10)
'''
print(f"\nSample records from '{first_measurement}':\n")
result = query_api.query(query_sample)
for table in result:
for record in table.records:
print(f"Time: {record.get_time()}")
print(f" Measurement: {record.get_measurement()}")
print(f" Field: {record.get_field()}")
print(f" Value: {record.get_value()}")
print(f" Tags: {record.values}")
print()
except Exception as e:
print(f"Error querying InfluxDB: {e}")
import traceback
traceback.print_exc()
finally:
client.close()
if __name__ == '__main__':
if not os.getenv('INFLUXDB_URL'):
print("ERROR: Please create a .env file with your InfluxDB configuration")
print("Copy .env.example to .env and fill in your details")
exit(1)
explore_influxdb()