diff --git a/pkg/azurefile/controllerserver.go b/pkg/azurefile/controllerserver.go index cc0e5dbde4..ada4d7714f 100644 --- a/pkg/azurefile/controllerserver.go +++ b/pkg/azurefile/controllerserver.go @@ -43,7 +43,6 @@ import ( "k8s.io/utils/ptr" csiMetrics "sigs.k8s.io/azurefile-csi-driver/pkg/metrics" azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache" - "sigs.k8s.io/cloud-provider-azure/pkg/metrics" "sigs.k8s.io/cloud-provider-azure/pkg/provider/storage" ) @@ -517,14 +516,6 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) } } - csiMC := csiMetrics.NewCSIMetricContext(requestName) - isOperationSucceeded := false - defer func() { - csiMC.ObserveWithLabels(isOperationSucceeded, - "protocol", string(shareProtocol), - "storage_account_type", sku) - }() - if sourceID != "" { _, srcAccountName, _, _, _, _, err = GetFileShareInfo(sourceID) //nolint:dogsled if err != nil { @@ -567,9 +558,10 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) SourceAccountName: srcAccountName, } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, d.cloud.ResourceGroup, subsID, d.Name) + mc := csiMetrics.NewCSIMetricContext(requestName).WithBasicVolumeInfo(d.cloud.ResourceGroup, subsID, d.Name) + isOperationSucceeded := false defer func() { - mc.ObserveOperationWithResult(isOperationSucceeded, VolumeID, volumeID) + mc.WithAdditionalVolumeInfo(VolumeID, volumeID).ObserveWithLabels(isOperationSucceeded, csiMetrics.Protocol, string(shareProtocol), csiMetrics.StorageAccountType, sku) }() var accountKey, lockKey string @@ -791,12 +783,6 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) // DeleteVolume delete an azure file func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (resp *csi.DeleteVolumeResponse, returnedErr error) { - requestName := "controller_delete_volume" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - defer func() { - csiMC.Observe(returnedErr == nil) - }() - volumeID := req.GetVolumeId() if len(volumeID) == 0 { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") @@ -841,9 +827,9 @@ func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) secret = createStorageAccountSecret(accountName, accountKey) } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, resourceGroupName, subsID, d.Name) + mc := csiMetrics.NewCSIMetricContext("controller_delete_volume").WithBasicVolumeInfo(resourceGroupName, subsID, d.Name) defer func() { - mc.ObserveOperationWithResult(returnedErr == nil, VolumeID, volumeID) + mc.WithAdditionalVolumeInfo(VolumeID, volumeID).Observe(returnedErr == nil) }() if err := d.DeleteFileShare(ctx, subsID, resourceGroupName, accountName, fileShareName, secret, useDataPlaneAPI); err != nil { @@ -950,12 +936,6 @@ func (d *Driver) ControllerUnpublishVolume(_ context.Context, _ *csi.ControllerU // CreateSnapshot create a snapshot func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (resp *csi.CreateSnapshotResponse, returnedErr error) { - requestName := "controller_create_snapshot" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - defer func() { - csiMC.Observe(returnedErr == nil) - }() - sourceVolumeID := req.GetSourceVolumeId() snapshotName := req.Name if len(snapshotName) == 0 { @@ -993,9 +973,9 @@ func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequ useDataPlaneAPI = d.useDataPlaneAPI(ctx, sourceVolumeID, accountName) } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, rgName, subsID, d.Name) + mc := csiMetrics.NewCSIMetricContext("controller_create_snapshot").WithBasicVolumeInfo(rgName, subsID, d.Name) defer func() { - mc.ObserveOperationWithResult(returnedErr == nil, SourceResourceID, sourceVolumeID, SnapshotName, snapshotName) + mc.WithAdditionalVolumeInfo(SourceResourceID, sourceVolumeID, SnapshotName, snapshotName).Observe(returnedErr == nil) }() exists, itemSnapshot, itemSnapshotTime, itemSnapshotQuota, err := d.snapshotExists(ctx, sourceVolumeID, snapshotName, req.GetSecrets(), useDataPlaneAPI) @@ -1105,13 +1085,6 @@ func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequ // DeleteSnapshot delete a snapshot (todo) func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { - requestName := "controller_delete_snapshot" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - isOperationSucceeded := false - defer func() { - csiMC.Observe(isOperationSucceeded) - }() - if len(req.SnapshotId) == 0 { return nil, status.Error(codes.InvalidArgument, "Snapshot ID must be provided") } @@ -1133,9 +1106,10 @@ func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequ subsID = d.cloud.SubscriptionID } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, rgName, subsID, d.Name) + mc := csiMetrics.NewCSIMetricContext("controller_delete_snapshot").WithBasicVolumeInfo(rgName, subsID, d.Name) + isOperationSucceeded := false defer func() { - mc.ObserveOperationWithResult(isOperationSucceeded, SnapshotID, req.SnapshotId) + mc.WithAdditionalVolumeInfo(SnapshotID, req.SnapshotId).Observe(isOperationSucceeded) }() var deleteErr error @@ -1303,13 +1277,6 @@ func (d *Driver) execAzcopyCopy(srcPath, dstPath string, azcopyCopyOptions, auth // ControllerExpandVolume controller expand volume func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { - requestName := "controller_expand_volume" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - isOperationSucceeded := false - defer func() { - csiMC.Observe(isOperationSucceeded) - }() - volumeID := req.GetVolumeId() if len(volumeID) == 0 { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") @@ -1348,9 +1315,10 @@ func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.Controller } } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, resourceGroupName, subsID, d.Name) + mc := csiMetrics.NewCSIMetricContext("controller_expand_volume").WithBasicVolumeInfo(resourceGroupName, subsID, d.Name) + isOperationSucceeded := false defer func() { - mc.ObserveOperationWithResult(isOperationSucceeded, VolumeID, volumeID) + mc.WithAdditionalVolumeInfo(VolumeID, volumeID).Observe(isOperationSucceeded) }() secrets := req.GetSecrets() diff --git a/pkg/azurefile/nodeserver.go b/pkg/azurefile/nodeserver.go index 0d782ea945..4ee1b92805 100644 --- a/pkg/azurefile/nodeserver.go +++ b/pkg/azurefile/nodeserver.go @@ -60,9 +60,9 @@ func NewMountClient(cc *grpc.ClientConn) *MountClient { // NodePublishVolume mount the volume from staging to target path func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (resp *csi.NodePublishVolumeResponse, returnedErr error) { - csiMC := csiMetrics.NewCSIMetricContext("node_publish_volume") + mc := csiMetrics.NewCSIMetricContext("node_publish_volume") defer func() { - csiMC.Observe(returnedErr == nil) + mc.Observe(returnedErr == nil) }() volCap := req.GetVolumeCapability() @@ -204,9 +204,9 @@ func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolu // NodeUnpublishVolume unmount the volume from the target path func (d *Driver) NodeUnpublishVolume(_ context.Context, req *csi.NodeUnpublishVolumeRequest) (resp *csi.NodeUnpublishVolumeResponse, returnedErr error) { - csiMC := csiMetrics.NewCSIMetricContext("node_unpublish_volume") + mc := csiMetrics.NewCSIMetricContext("node_unpublish_volume") defer func() { - csiMC.Observe(returnedErr == nil) + mc.Observe(returnedErr == nil) }() if len(req.GetVolumeId()) == 0 { @@ -242,12 +242,6 @@ func (d *Driver) NodeUnpublishVolume(_ context.Context, req *csi.NodeUnpublishVo // NodeStageVolume mount the volume to a staging path func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (resp *csi.NodeStageVolumeResponse, returnedErr error) { - requestName := "node_stage_volume" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - defer func() { - csiMC.Observe(returnedErr == nil) - }() - if len(req.GetVolumeId()) == 0 { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") } @@ -277,9 +271,9 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe klog.V(2).Infof("CSI volume is read-only, mounting with extra option ro") } - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, d.cloud.ResourceGroup, "", d.Name) + mc := csiMetrics.NewCSIMetricContext("node_stage_volume").WithBasicVolumeInfo(d.cloud.ResourceGroup, "", d.Name) defer func() { - mc.ObserveOperationWithResult(returnedErr == nil, VolumeID, volumeID) + mc.WithAdditionalVolumeInfo(VolumeID, volumeID).Observe(returnedErr == nil) }() _, accountName, accountKey, fileShareName, diskName, _, err := d.GetAccountInfo(ctx, volumeID, req.GetSecrets(), context) @@ -593,13 +587,6 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe // NodeUnstageVolume unmount the volume from the staging path func (d *Driver) NodeUnstageVolume(_ context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { - requestName := "node_unstage_volume" - csiMC := csiMetrics.NewCSIMetricContext(requestName) - isOperationSucceeded := false - defer func() { - csiMC.Observe(isOperationSucceeded) - }() - volumeID := req.GetVolumeId() if len(volumeID) == 0 { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") @@ -615,9 +602,10 @@ func (d *Driver) NodeUnstageVolume(_ context.Context, req *csi.NodeUnstageVolume } defer d.volumeLocks.Release(lockKey) - mc := metrics.NewMetricContext(azureFileCSIDriverName, requestName, d.cloud.ResourceGroup, "", d.Name) + mc := csiMetrics.NewCSIMetricContext("node_unstage_volume").WithBasicVolumeInfo(d.cloud.ResourceGroup, "", d.Name) + isOperationSucceeded := false defer func() { - mc.ObserveOperationWithResult(isOperationSucceeded, VolumeID, volumeID) + mc.WithAdditionalVolumeInfo(VolumeID, volumeID).Observe(isOperationSucceeded) }() klog.V(2).Infof("NodeUnstageVolume: unmount volume %s on %s", volumeID, stagingTargetPath) @@ -715,11 +703,11 @@ func (d *Driver) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeS klog.V(6).Infof("NodeGetVolumeStats: begin to get VolumeStats on volume %s path %s", req.VolumeId, req.VolumePath) } - mc := metrics.NewMetricContext(azureFileCSIDriverName, "node_get_volume_stats", d.cloud.ResourceGroup, "", d.Name) - mc.LogLevel = 6 // change log level + azureMC := metrics.NewMetricContext(azureFileCSIDriverName, "node_get_volume_stats", d.cloud.ResourceGroup, "", d.Name) + azureMC.LogLevel = 6 // change log level isOperationSucceeded := false defer func() { - mc.ObserveOperationWithResult(isOperationSucceeded, VolumeID, req.VolumeId) + azureMC.ObserveOperationWithResult(isOperationSucceeded, VolumeID, req.VolumeId) }() resp, err := GetVolumeStats(req.VolumePath, d.enableWindowsHostProcess) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index fa42483c27..da71791bc6 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -17,14 +17,20 @@ limitations under the License. package metrics import ( + "strings" "time" "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" + klog "k8s.io/klog/v2" ) const ( subSystem = "azurefile_csi_driver" + + // Label keys for metrics + Protocol = "protocol" + StorageAccountType = "storage_account_type" ) var ( @@ -47,7 +53,7 @@ var ( Buckets: []float64{0.1, 0.2, 0.5, 1, 5, 10, 15, 20, 30, 40, 50, 60, 100, 200, 300}, StabilityLevel: metrics.ALPHA, }, - []string{"operation", "success", "protocol", "storage_account_type"}, + []string{"operation", "success", Protocol, StorageAccountType}, ) operationTotal = metrics.NewCounterVec( @@ -69,18 +75,48 @@ func init() { // CSIMetricContext represents the context for CSI operation metrics type CSIMetricContext struct { - operation string - start time.Time - labels map[string]string + operation string + volumeContext []interface{} + start time.Time + labels map[string]string + logLevel int32 } // NewCSIMetricContext creates a new CSI metric context func NewCSIMetricContext(operation string) *CSIMetricContext { return &CSIMetricContext{ - operation: operation, - start: time.Now(), - labels: make(map[string]string), + operation: operation, + volumeContext: []interface{}{}, + start: time.Now(), + labels: make(map[string]string), + logLevel: 3, + } +} + +// WithBasicVolumeInfo adds the standard volume-related context to the metric context +func (mc *CSIMetricContext) WithBasicVolumeInfo(resourceGroup, subscriptionID, source string) *CSIMetricContext { + if resourceGroup != "" { + mc.volumeContext = append(mc.volumeContext, "resource_group", strings.ToLower(resourceGroup)) + } + if subscriptionID != "" { + mc.volumeContext = append(mc.volumeContext, "subscription_id", subscriptionID) } + if source != "" { + mc.volumeContext = append(mc.volumeContext, "source", source) + } + return mc +} + +// WithAdditionalVolumeInfo adds additional volume-related context as key-value pairs +// e.g., WithAdditionalVolumeInfo("volumeid", "vol-123") +func (mc *CSIMetricContext) WithAdditionalVolumeInfo(keyValuePairs ...string) *CSIMetricContext { + if len(keyValuePairs)%2 != 0 { + return mc + } + for i := 0; i < len(keyValuePairs); i += 2 { + mc.volumeContext = append(mc.volumeContext, keyValuePairs[i], keyValuePairs[i+1]) + } + return mc } // WithLabel adds a label to the metric context @@ -92,6 +128,12 @@ func (mc *CSIMetricContext) WithLabel(key, value string) *CSIMetricContext { return mc } +// WithLogLevel sets the log level for the metric context +func (mc *CSIMetricContext) WithLogLevel(level int32) *CSIMetricContext { + mc.logLevel = level + return mc +} + // Observe records the operation result and duration func (mc *CSIMetricContext) Observe(success bool) { duration := time.Since(mc.start).Seconds() @@ -106,8 +148,8 @@ func (mc *CSIMetricContext) Observe(success bool) { // Record detailed metrics if labels are present if len(mc.labels) > 0 { - protocol := mc.labels["protocol"] - storageAccountType := mc.labels["storage_account_type"] + protocol := mc.labels[Protocol] + storageAccountType := mc.labels[StorageAccountType] operationDurationWithLabels.WithLabelValues( mc.operation, @@ -116,6 +158,14 @@ func (mc *CSIMetricContext) Observe(success bool) { storageAccountType, ).Observe(duration) } + + logger := klog.Background().WithName("logLatency").V(int(mc.logLevel)) + if !logger.Enabled() { + return + } + + keysAndValues := []interface{}{"latency_seconds", duration, "request", subSystem + "_" + mc.operation, "success", successStr} + logger.Info("Observed Request Latency", append(keysAndValues, mc.volumeContext...)...) } // ObserveWithLabels records the operation with provided label pairs diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index f817a3cda5..cd19dd8333 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -25,7 +25,7 @@ import ( func TestCSIMetricContext_NewCSIMetricContext(t *testing.T) { operation := "test_operation" - mc := NewCSIMetricContext(operation) + mc := NewCSIMetricContext(operation).WithBasicVolumeInfo("test-rg", "test-sub", "test-source") if mc.operation != operation { t.Errorf("expected operation %s, got %s", operation, mc.operation) @@ -41,7 +41,7 @@ func TestCSIMetricContext_NewCSIMetricContext(t *testing.T) { } func TestCSIMetricContext_WithLabel(t *testing.T) { - mc := NewCSIMetricContext("test_operation") + mc := NewCSIMetricContext("test_operation").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") mc.WithLabel("key1", "value1") mc.WithLabel("key2", "value2") @@ -60,7 +60,7 @@ func TestCSIMetricContext_Observe(t *testing.T) { operationDuration.Reset() operationTotal.Reset() - mc := NewCSIMetricContext("node_stage_volume") + mc := NewCSIMetricContext("node_stage_volume").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test basic observation (success) mc.Observe(true) @@ -101,7 +101,7 @@ func TestCSIMetricContext_ObserveWithFailure(t *testing.T) { // Reset metrics before test operationTotal.Reset() - mc := NewCSIMetricContext("node_publish_volume") + mc := NewCSIMetricContext("node_publish_volume").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test observation with failure mc.Observe(false) @@ -147,12 +147,12 @@ func TestCSIMetricContext_ObserveWithLabels(t *testing.T) { operationTotal.Reset() operationDurationWithLabels.Reset() - mc := NewCSIMetricContext("controller_create_volume") + mc := NewCSIMetricContext("controller_create_volume").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test observation with labels mc.ObserveWithLabels(true, - "protocol", "smb", - "storage_account_type", "Standard_LRS") + Protocol, "smb", + StorageAccountType, "Standard_LRS") // Verify that both basic and labeled metrics were recorded families, err := legacyregistry.DefaultGatherer.Gather() @@ -182,8 +182,8 @@ func TestCSIMetricContext_ObserveWithLabels(t *testing.T) { labelMap[label.GetName()] = label.GetValue() } - if labelMap["protocol"] != "smb" || - labelMap["storage_account_type"] != "Standard_LRS" { + if labelMap[Protocol] != "smb" || + labelMap[StorageAccountType] != "Standard_LRS" { t.Errorf("expected labeled metric with correct labels, got: %v", labelMap) } } @@ -203,10 +203,10 @@ func TestCSIMetricContext_ObserveWithInvalidLabels(t *testing.T) { operationTotal.Reset() operationDurationWithLabels.Reset() - mc := NewCSIMetricContext("test_operation") + mc := NewCSIMetricContext("test_operation").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test with odd number of label parameters (should fallback to basic observe) - mc.ObserveWithLabels(true, "protocol", "smb", "orphan_key") + mc.ObserveWithLabels(true, Protocol, "smb", "orphan_key") // Should still record basic metrics but not labeled metrics families, err := legacyregistry.DefaultGatherer.Gather() @@ -235,25 +235,29 @@ func TestCSIMetricContext_ObserveWithInvalidLabels(t *testing.T) { } func TestCSIMetricContext_TimingAccuracy(t *testing.T) { - mc := NewCSIMetricContext("timing_test") + mc := NewCSIMetricContext("timing_test").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") + + // Set start to a fixed time in the past so the test is deterministic + // and doesn't depend on wall-clock timing or scheduler delays. + fixedDuration := 500 * time.Millisecond + mc.start = time.Now().Add(-fixedDuration) - // Simulate that the operation started some time ago by setting a fixed start time. - expectedDuration := 50 * time.Millisecond - mc.start = time.Now().Add(-expectedDuration) mc.Observe(true) duration := time.Since(mc.start) - // The duration should be at least the expected duration (allowing for minimal overhead). - if duration < expectedDuration { - t.Errorf("expected duration to be at least %v, got %v", expectedDuration, duration) + + // The duration should be at least the fixed offset we set + if duration < fixedDuration { + t.Errorf("expected duration to be at least %v, got %v", fixedDuration, duration) } - // But not too much more (allowing for some variance in execution). - if duration > expectedDuration+50*time.Millisecond { - t.Errorf("expected duration to be less than %v, got %v", expectedDuration+50*time.Millisecond, duration) + + // Should be very close to fixedDuration (allow a small margin for the few instructions between) + if duration > fixedDuration+10*time.Millisecond { + t.Errorf("expected duration to be close to %v, got %v", fixedDuration, duration) } } func TestCSIMetricContext_ChainedLabels(t *testing.T) { - mc := NewCSIMetricContext("test_operation") + mc := NewCSIMetricContext("test_operation").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test method chaining mc.WithLabel("key1", "value1").WithLabel("key2", "value2").WithLabel("key3", "value3") @@ -264,7 +268,7 @@ func TestCSIMetricContext_ChainedLabels(t *testing.T) { } func TestCSIMetricContext_EmptyLabels(t *testing.T) { - mc := NewCSIMetricContext("test_operation") + mc := NewCSIMetricContext("test_operation").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") // Test observing without any labels mc.Observe(true) @@ -274,22 +278,145 @@ func TestCSIMetricContext_EmptyLabels(t *testing.T) { } } +func TestCSIMetricContext_WithAdditionalVolumeInfo(t *testing.T) { + mc := NewCSIMetricContext("test_operation") + + // Test adding additional volume info key-value pairs + mc.WithAdditionalVolumeInfo("volumeid", "vol-123", "container", "my-container") + + // volumeContext is now []interface{}, check order and values + expected := []interface{}{"volumeid", "vol-123", "container", "my-container"} + if len(mc.volumeContext) != len(expected) { + t.Errorf("expected %d elements, got %d", len(expected), len(mc.volumeContext)) + } + for i, v := range expected { + if mc.volumeContext[i] != v { + t.Errorf("expected volumeContext[%d]=%v, got %v", i, v, mc.volumeContext[i]) + } + } +} + +func TestCSIMetricContext_WithAdditionalVolumeInfo_Chaining(t *testing.T) { + mc := NewCSIMetricContext("test_operation"). + WithBasicVolumeInfo("test-rg", "test-sub", "test-source"). + WithAdditionalVolumeInfo("volumeid", "vol-456", "storageaccount", "mystorageaccount") + + // volumeContext preserves insertion order + expected := []interface{}{ + "resource_group", "test-rg", + "subscription_id", "test-sub", + "source", "test-source", + "volumeid", "vol-456", + "storageaccount", "mystorageaccount", + } + if len(mc.volumeContext) != len(expected) { + t.Errorf("expected %d elements, got %d", len(expected), len(mc.volumeContext)) + } + for i, v := range expected { + if mc.volumeContext[i] != v { + t.Errorf("expected volumeContext[%d]=%v, got %v", i, v, mc.volumeContext[i]) + } + } +} + +func TestCSIMetricContext_WithAdditionalVolumeInfo_OddParameters(t *testing.T) { + mc := NewCSIMetricContext("test_operation") + + // Test odd number of parameters - should silently skip adding to volumeContext + mc.WithAdditionalVolumeInfo("key1", "value1", "orphan_key") + + // Should not have added anything due to odd number of parameters + if len(mc.volumeContext) != 0 { + t.Errorf("expected empty volumeContext with odd parameters, got: %v", mc.volumeContext) + } + + // Now add valid pairs + mc.WithAdditionalVolumeInfo("key2", "value2") + + // Should work for valid pairs + if len(mc.volumeContext) != 2 || mc.volumeContext[0] != "key2" || mc.volumeContext[1] != "value2" { + t.Errorf("expected [key2, value2] after valid call, got %v", mc.volumeContext) + } +} + +func TestCSIMetricContext_WithLogLevel(t *testing.T) { + tests := []struct { + name string + logLevel int32 + expectedLogLevel int32 + }{ + { + name: "set log level to 0", + logLevel: 0, + expectedLogLevel: 0, + }, + { + name: "set log level to 5", + logLevel: 5, + expectedLogLevel: 5, + }, + { + name: "set high log level", + logLevel: 10, + expectedLogLevel: 10, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := NewCSIMetricContext("test_operation").WithLogLevel(tt.logLevel) + + if mc.logLevel != tt.expectedLogLevel { + t.Errorf("expected logLevel %d, got %d", tt.expectedLogLevel, mc.logLevel) + } + }) + } +} + +func TestCSIMetricContext_WithLogLevel_DefaultValue(t *testing.T) { + mc := NewCSIMetricContext("test_operation") + + // Default log level should be 3 + if mc.logLevel != 3 { + t.Errorf("expected default logLevel 3, got %d", mc.logLevel) + } +} + +func TestCSIMetricContext_WithLogLevel_Chaining(t *testing.T) { + mc := NewCSIMetricContext("test_operation"). + WithBasicVolumeInfo("test-rg", "test-sub", "test-source"). + WithLogLevel(5). + WithLabel("key", "value") + + if mc.logLevel != 5 { + t.Errorf("expected logLevel 5 after chaining, got %d", mc.logLevel) + } + if mc.labels["key"] != "value" { + t.Errorf("expected label key=value after chaining, got %s", mc.labels["key"]) + } + // volumeContext is []interface{}, check first two elements + if len(mc.volumeContext) < 2 || mc.volumeContext[0] != "resource_group" || mc.volumeContext[1] != "test-rg" { + t.Errorf("expected resource_group=test-rg after chaining, got %v", mc.volumeContext) + } +} + func BenchmarkCSIMetricContext_Observe(b *testing.B) { - mc := NewCSIMetricContext("benchmark_test") + mc := NewCSIMetricContext("benchmark_test").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") b.ResetTimer() for i := 0; i < b.N; i++ { - mc.start = time.Now() // Reset start time each iteration to simulate fresh observation + mc.start = time.Now() mc.Observe(true) } } func BenchmarkCSIMetricContext_ObserveWithLabels(b *testing.B) { + mc := NewCSIMetricContext("benchmark_test").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") b.ResetTimer() for i := 0; i < b.N; i++ { - mc := NewCSIMetricContext("benchmark_test") + mc.start = time.Now() mc.ObserveWithLabels(true, - "protocol", "smb", - "storage_account_type", "Standard_LRS") + Protocol, "smb", + StorageAccountType, "Standard_LRS") } } @@ -306,7 +433,7 @@ func BenchmarkMetricsRecordingOnly(b *testing.B) { func BenchmarkCSIMetricContext_NewAndObserve(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - mc := NewCSIMetricContext("benchmark_test") + mc := NewCSIMetricContext("benchmark_test").WithBasicVolumeInfo("test-rg", "test-sub", "test-source") mc.Observe(true) } }