From 43348da3c02af8921db70e1a038ca271877489dc Mon Sep 17 00:00:00 2001 From: Anthony Freyermuth Date: Mon, 12 Jan 2026 17:13:45 +0100 Subject: [PATCH 1/2] :wrench: Add volumeID to outputs --- internal/monitoring/helpers.go | 56 ++++++++++++++++++++++++++++++++++ internal/monitoring/metrics.go | 17 +++++++++++ 2 files changed, 73 insertions(+) diff --git a/internal/monitoring/helpers.go b/internal/monitoring/helpers.go index 9652623..bd6938e 100644 --- a/internal/monitoring/helpers.go +++ b/internal/monitoring/helpers.go @@ -2,8 +2,10 @@ package monitoring import ( "bufio" + "fmt" "os" "os/exec" + "regexp" "strings" ) @@ -109,6 +111,60 @@ func getDiskDevice(diskDevice string) string { return diskDevice } +// getVolumeID extracts the EBS volume ID from a disk device name +// It reads the MODEL and SERIAL from /sys/block/${BASE}/device/ and converts +// the serial to the volume ID format (vol-...) +func getVolumeID(diskDevice string) (string, error) { + // Extract base device name (remove partition suffix like p1, p2, etc.) + base := diskDevice + // Remove partition suffix pattern: p1, p2, etc. (e.g., nvme0n1p1 -> nvme0n1) + re := regexp.MustCompile(`p\d+$`) + base = re.ReplaceAllString(base, "") + + // Read MODEL + modelPath := fmt.Sprintf("/sys/block/%s/device/model", base) + modelBytes, err := os.ReadFile(modelPath) + if err != nil { + return "", fmt.Errorf("failed to read model: %w", err) + } + model := strings.TrimSpace(string(modelBytes)) + + // Validate MODEL contains "Amazon Elastic Block Store" + if !strings.Contains(strings.ToLower(model), "amazon elastic block store") { + return "", fmt.Errorf("%s does not look like an EBS NVMe device (model=%s)", base, model) + } + + // Read SERIAL + serialPath := fmt.Sprintf("/sys/block/%s/device/serial", base) + serialBytes, err := os.ReadFile(serialPath) + if err != nil { + return "", fmt.Errorf("failed to read serial: %w", err) + } + serial := strings.TrimSpace(string(serialBytes)) + // Remove all whitespace + serial = strings.ReplaceAll(serial, " ", "") + serial = strings.ReplaceAll(serial, "\t", "") + serial = strings.ReplaceAll(serial, "\n", "") + + // Convert SERIAL to VOLUME_ID format + // Pattern: vol00bb... -> vol-00bb... + volPattern := regexp.MustCompile(`^vol[0-9a-f]+$`) + volWithDashPattern := regexp.MustCompile(`^vol-[0-9a-f]+$`) + + var volumeID string + if volPattern.MatchString(serial) { + // vol00bb... -> vol-00bb... + volumeID = "vol-" + serial[3:] + } else if volWithDashPattern.MatchString(serial) { + // Already in correct format + volumeID = serial + } else { + return "", fmt.Errorf("unexpected NVMe serial format: %s", serial) + } + + return volumeID, nil +} + // calculateMin returns the minimum value in a slice func calculateMin(data []float64) float64 { if len(data) == 0 { diff --git a/internal/monitoring/metrics.go b/internal/monitoring/metrics.go index 011dfef..29fb5e7 100644 --- a/internal/monitoring/metrics.go +++ b/internal/monitoring/metrics.go @@ -178,11 +178,24 @@ func GenerateMetricsSummary(action *githubactions.Action, metrics []string, form networkInterface = getNetworkInterface(networkInterface) diskDevice = getDiskDevice(diskDevice) + // Get volume ID from disk device + volumeID := "" + if diskDevice != "" { + var err error + volumeID, err = getVolumeID(diskDevice) + if err != nil { + action.Warningf("Failed to get volume ID from disk device %s: %v", diskDevice, err) + } + } + action.Infof("## CloudWatch Metrics Summary\n") action.Infof("Enabled metrics: %s", strings.Join(metrics, ", ")) action.Infof("Namespace: %s", NAMESPACE) action.Infof("Network interface: %s", networkInterface) action.Infof("Disk device: %s", diskDevice) + if volumeID != "" { + action.Infof("Volume ID: %s", volumeID) + } action.Infof("") showLinks(action, metrics) @@ -225,6 +238,10 @@ func GenerateMetricsSummary(action *githubactions.Action, metrics []string, form Name: aws.String("path"), Value: aws.String("/"), }) + dimensions = append(dimensions, types.Dimension{ + Name: aws.String("volume_id"), + Value: aws.String(volumeID), + }) } if metricType == "io" { dimensions = append(dimensions, types.Dimension{ From 78c5865b33ccff9a62074e92f7fcc60f81b859be Mon Sep 17 00:00:00 2001 From: Anthony Freyermuth Date: Mon, 9 Feb 2026 09:21:05 +0100 Subject: [PATCH 2/2] fix: Fixing error --- internal/monitoring/helpers.go | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/internal/monitoring/helpers.go b/internal/monitoring/helpers.go index bd6938e..9a2f1a5 100644 --- a/internal/monitoring/helpers.go +++ b/internal/monitoring/helpers.go @@ -121,20 +121,7 @@ func getVolumeID(diskDevice string) (string, error) { re := regexp.MustCompile(`p\d+$`) base = re.ReplaceAllString(base, "") - // Read MODEL - modelPath := fmt.Sprintf("/sys/block/%s/device/model", base) - modelBytes, err := os.ReadFile(modelPath) - if err != nil { - return "", fmt.Errorf("failed to read model: %w", err) - } - model := strings.TrimSpace(string(modelBytes)) - - // Validate MODEL contains "Amazon Elastic Block Store" - if !strings.Contains(strings.ToLower(model), "amazon elastic block store") { - return "", fmt.Errorf("%s does not look like an EBS NVMe device (model=%s)", base, model) - } - - // Read SERIAL + // Read SERIAL first - this is the definitive way to identify EBS volumes serialPath := fmt.Sprintf("/sys/block/%s/device/serial", base) serialBytes, err := os.ReadFile(serialPath) if err != nil { @@ -159,9 +146,27 @@ func getVolumeID(diskDevice string) (string, error) { // Already in correct format volumeID = serial } else { + // Serial doesn't match volume ID pattern - check model for better error message + modelPath := fmt.Sprintf("/sys/block/%s/device/model", base) + modelBytes, err := os.ReadFile(modelPath) + if err == nil { + model := strings.TrimSpace(string(modelBytes)) + return "", fmt.Errorf("%s does not look like an EBS NVMe device (model=%s, serial=%s)", base, model, serial) + } return "", fmt.Errorf("unexpected NVMe serial format: %s", serial) } + // Optionally validate MODEL (but don't fail if serial already indicates it's a volume) + modelPath := fmt.Sprintf("/sys/block/%s/device/model", base) + modelBytes, err := os.ReadFile(modelPath) + if err == nil { + model := strings.TrimSpace(string(modelBytes)) + if !strings.Contains(strings.ToLower(model), "amazon elastic block store") { + // Model doesn't match, but serial does - this might be instance storage with volume-like serial + // We'll still return the volume ID since the serial pattern matched + } + } + return volumeID, nil }