Skip to content

Commit 8b34dfc

Browse files
author
Stancu Florin
committed
[FEAT] Add Support for Human-Readable Data Units in NVMe Attributes
1 parent a58f944 commit 8b34dfc

File tree

8 files changed

+130
-6
lines changed

8 files changed

+130
-6
lines changed

docker-compose.dev.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: '3.5'
2+
3+
services:
4+
scrutiny-dev:
5+
build:
6+
context: .
7+
dockerfile: docker/Dockerfile
8+
container_name: scrutiny-dev
9+
cap_add:
10+
- SYS_RAWIO
11+
- SYS_ADMIN # Required for NVMe drives
12+
privileged: true # Gives access to all devices on the host
13+
ports:
14+
- "8080:8080" # Web UI
15+
- "8086:8086" # InfluxDB admin (if needed)
16+
volumes:
17+
- scrutiny-config:/opt/scrutiny/config
18+
- scrutiny-influxdb:/opt/scrutiny/influxdb
19+
- go-cache:/go # Cache Go modules
20+
- node-cache:/root/.npm # Cache npm packages
21+
# Uncomment to enable debug mode
22+
# environment:
23+
# - DEBUG=true
24+
# - SCRUTINY_LOG_FILE=/tmp/web.log
25+
# - COLLECTOR_LOG_FILE=/tmp/collector.log
26+
27+
volumes:
28+
scrutiny-config:
29+
scrutiny-influxdb:
30+
go-cache: # Persistent cache for Go modules
31+
node-cache: # Persistent cache for npm packages

docs/DEVELOPER.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Scrutiny Developer Guide
2+
3+
This guide is designed for developers who want to make and test changes to Scrutiny using Docker.
4+
5+
## Quick Start with Docker
6+
7+
### Making and Testing Changes
8+
9+
#### 1. Make Your Changes
10+
11+
Make the necessary code changes to implement your feature or fix.
12+
13+
#### 2. Use the Development Docker Compose File
14+
15+
We've provided a unified Docker Compose file that works on all platforms (Linux, macOS, Windows):
16+
17+
```bash
18+
docker-compose -f docker-compose.dev.yml up --build
19+
```
20+
21+
This command:
22+
- Builds a new Docker image with your changes
23+
- Starts the container
24+
- Maps the necessary ports and volumes
25+
26+
27+
#### 3. Access the Application
28+
29+
Open your browser and navigate to:
30+
- Web UI: http://localhost:8080
31+
32+
#### 4. Test Your Changes
33+
34+
The collector will run automatically on container startup. To manually trigger the collector:
35+
36+
```bash
37+
docker exec scrutiny-dev /opt/scrutiny/bin/scrutiny-collector-metrics run
38+
```

webapp/backend/pkg/models/measurements/smart.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,26 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
163163
"temperature": (&SmartNvmeAttribute{AttributeId: "temperature", Value: nvmeSmartHealthInformationLog.Temperature, Threshold: -1}).PopulateAttributeStatus(),
164164
"available_spare": (&SmartNvmeAttribute{AttributeId: "available_spare", Value: nvmeSmartHealthInformationLog.AvailableSpare, Threshold: nvmeSmartHealthInformationLog.AvailableSpareThreshold}).PopulateAttributeStatus(),
165165
"percentage_used": (&SmartNvmeAttribute{AttributeId: "percentage_used", Value: nvmeSmartHealthInformationLog.PercentageUsed, Threshold: 100}).PopulateAttributeStatus(),
166-
"data_units_read": (&SmartNvmeAttribute{AttributeId: "data_units_read", Value: nvmeSmartHealthInformationLog.DataUnitsRead, Threshold: -1}).PopulateAttributeStatus(),
167-
"data_units_written": (&SmartNvmeAttribute{AttributeId: "data_units_written", Value: nvmeSmartHealthInformationLog.DataUnitsWritten, Threshold: -1}).PopulateAttributeStatus(),
166+
"data_units_read": func() SmartAttribute {
167+
transformedValue, valueUnit := TransformDataUnits(nvmeSmartHealthInformationLog.DataUnitsRead)
168+
return (&SmartNvmeAttribute{
169+
AttributeId: "data_units_read",
170+
Value: nvmeSmartHealthInformationLog.DataUnitsRead,
171+
Threshold: -1,
172+
TransformedValue: transformedValue,
173+
ValueUnit: valueUnit,
174+
}).PopulateAttributeStatus()
175+
}(),
176+
"data_units_written": func() SmartAttribute {
177+
transformedValue, valueUnit := TransformDataUnits(nvmeSmartHealthInformationLog.DataUnitsWritten)
178+
return (&SmartNvmeAttribute{
179+
AttributeId: "data_units_written",
180+
Value: nvmeSmartHealthInformationLog.DataUnitsWritten,
181+
Threshold: -1,
182+
TransformedValue: transformedValue,
183+
ValueUnit: valueUnit,
184+
}).PopulateAttributeStatus()
185+
}(),
168186
"host_reads": (&SmartNvmeAttribute{AttributeId: "host_reads", Value: nvmeSmartHealthInformationLog.HostReads, Threshold: -1}).PopulateAttributeStatus(),
169187
"host_writes": (&SmartNvmeAttribute{AttributeId: "host_writes", Value: nvmeSmartHealthInformationLog.HostWrites, Threshold: -1}).PopulateAttributeStatus(),
170188
"controller_busy_time": (&SmartNvmeAttribute{AttributeId: "controller_busy_time", Value: nvmeSmartHealthInformationLog.ControllerBusyTime, Threshold: -1}).PopulateAttributeStatus(),

webapp/backend/pkg/models/measurements/smart_nvme_attribute.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type SmartNvmeAttribute struct {
1414
Threshold int64 `json:"thresh"`
1515

1616
TransformedValue int64 `json:"transformed_value"`
17+
ValueUnit string `json:"value_unit,omitempty"` // Unit type (MB/GB/TB/PB)
1718
Status pkg.AttributeStatus `json:"status"`
1819
StatusReason string `json:"status_reason,omitempty"`
1920
FailureRate float64 `json:"failure_rate,omitempty"`
@@ -35,6 +36,7 @@ func (sa *SmartNvmeAttribute) Flatten() map[string]interface{} {
3536

3637
//Generated Data
3738
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
39+
fmt.Sprintf("attr.%s.value_unit", sa.AttributeId): sa.ValueUnit,
3840
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
3941
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
4042
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
@@ -58,6 +60,8 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
5860
//generated
5961
case "transformed_value":
6062
sa.TransformedValue = val.(int64)
63+
case "value_unit":
64+
sa.ValueUnit = val.(string)
6165
case "status":
6266
sa.Status = pkg.AttributeStatus(val.(int64))
6367
case "status_reason":
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package measurements
2+
3+
// TransformDataUnits converts data units to bytes and determines the appropriate unit (MB, GB, TB, PB)
4+
// Returns the transformed value and the unit as a string
5+
func TransformDataUnits(value int64) (int64, string) {
6+
// Convert to bytes: value * 1000 * 512 (1000 units of 512 byte data units)
7+
// According to NVMe spec: "This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read)"
8+
bytes := float64(value) * 1000 * 512
9+
10+
if bytes < 1000*1000*1000 {
11+
// Less than 1 GB, show in MB
12+
return int64(bytes / 1000 / 1000), "MB"
13+
} else if bytes < 1000*1000*1000*1000 {
14+
// Less than 1 TB, show in GB
15+
return int64(bytes / 1000 / 1000 / 1000), "GB"
16+
} else if bytes < 1000*1000*1000*1000*1000 {
17+
// Less than 1 PB, show in TB
18+
return int64(bytes / 1000 / 1000 / 1000 / 1000), "TB"
19+
} else {
20+
// Show in PB
21+
return int64(bytes / 1000 / 1000 / 1000 / 1000 / 1000), "PB"
22+
}
23+
}

webapp/backend/pkg/thresholds/nvme_attribute_metadata.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ var NmveMetadata = map[string]NvmeAttributeMetadata{
5252
"data_units_read": {
5353
ID: "data_units_read",
5454
DisplayName: "Data Units Read",
55-
DisplayType: "",
55+
DisplayType: "transformed_with_unit",
5656
Ideal: "",
5757
Critical: false,
5858
Description: "Contains the number of 512 byte data units the host has read from the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data read to 512 byte units.",
5959
},
6060
"data_units_written": {
6161
ID: "data_units_written",
6262
DisplayName: "Data Units Written",
63-
DisplayType: "",
63+
DisplayType: "transformed_with_unit",
6464
Ideal: "",
6565
Critical: false,
6666
Description: "Contains the number of 512 byte data units the host has written to the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes written) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data written to 512 byte units.",

webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface SmartAttributeModel {
1111
when_failed?: string
1212

1313
transformed_value: number
14+
value_unit?: string
1415
status: number
1516
status_reason?: string
1617
failure_rate?: number

webapp/frontend/src/app/modules/detail/detail.component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
204204
}
205205
}
206206

207-
getAttributeValue(attributeData: SmartAttributeModel): number {
207+
getAttributeValue(attributeData: SmartAttributeModel): number | string {
208+
// Get attribute metadata for any device type
209+
const attributeMetadata = this.metadata[attributeData.attribute_id]
210+
211+
// Check for transformed_with_unit display type first (for any device type)
212+
if (attributeMetadata?.display_type === 'transformed_with_unit' &&
213+
attributeData.transformed_value && attributeData.value_unit) {
214+
return `${attributeData.transformed_value} ${attributeData.value_unit}`
215+
}
216+
217+
// Then handle device-specific logic
208218
if (this.isAta()) {
209-
const attributeMetadata = this.metadata[attributeData.attribute_id]
210219
if (!attributeMetadata) {
211220
return attributeData.value
212221
} else if (attributeMetadata.display_type === 'raw') {

0 commit comments

Comments
 (0)