diff --git a/monitoring/helpers.go b/monitoring/helpers.go index 4cee6f9..136769a 100644 --- a/monitoring/helpers.go +++ b/monitoring/helpers.go @@ -3,6 +3,7 @@ package libpack_monitoring import ( "fmt" "os" + "sort" "strings" "unicode" @@ -10,37 +11,53 @@ import ( ) func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) (complete_name string) { - if labels == nil { - labels = make(map[string]string) - } - - // Adding default labels - labels["microservice"] = libpack_config.PKG_NAME - if podName, err := os.Hostname(); err == nil { - labels["pod"] = podName - } else { - labels["pod"] = "unknown" - } - + const unknownPodName = "unknown" var sb strings.Builder + + // Prepare default labels without initializing a new map + podName := unknownPodName + if hn, err := os.Hostname(); err == nil { + podName = hn + } + if labels == nil { + labels = map[string]string{ + "microservice": libpack_config.PKG_NAME, + "pod": podName, + } + } else { + if _, exists := labels["microservice"]; !exists { + labels["microservice"] = libpack_config.PKG_NAME + } + if _, exists := labels["pod"]; !exists { + labels["pod"] = podName + } + } + + // Prefix handling if ms.metrics_prefix != "" { sb.WriteString(ms.metrics_prefix) sb.WriteString("_") } sb.WriteString(name) + // Append labels if any if len(labels) > 0 { sb.WriteString("{") - first := true - for k, v := range labels { - if !first { + + keys := make([]string, 0, len(labels)) + for k := range labels { + keys = append(keys, k) + } + sort.Strings(keys) + + for i, k := range keys { + if i > 0 { sb.WriteString(",") } sb.WriteString(k) sb.WriteString("=\"") - sb.WriteString(v) + sb.WriteString(labels[k]) sb.WriteString("\"") - first = false } sb.WriteString("}") } @@ -82,9 +99,31 @@ func validate_metrics_name(name string) error { } func compile_metrics_with_labels(name string, labels map[string]string) string { - metric_name := name + var totalLength int + totalLength += len(name) for k, v := range labels { - metric_name += "_" + k + "_" + v + totalLength += len(k) + len(v) + 2 } - return metric_name + + var sb strings.Builder + sb.Grow(totalLength + 1) + + sb.WriteString(name) + + // Collect keys and sort them + keys := make([]string, 0, len(labels)) + for k := range labels { + keys = append(keys, k) + } + sort.Strings(keys) + + // Append sorted key-value pairs to the builder + for _, k := range keys { + sb.WriteString("_") + sb.WriteString(k) + sb.WriteString("_") + sb.WriteString(labels[k]) + } + + return sb.String() } diff --git a/monitoring/helpers_bench_test.go b/monitoring/helpers_bench_test.go new file mode 100644 index 0000000..84791f5 --- /dev/null +++ b/monitoring/helpers_bench_test.go @@ -0,0 +1,44 @@ +package libpack_monitoring + +import ( + "testing" + + libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" +) + +func BenchmarkGetMetricsName(b *testing.B) { + // Setup environment + libpack_config.PKG_NAME = "test_service" + + ms := &MetricsSetup{metrics_prefix: "test_prefix"} + + labels := map[string]string{ + "env": "production", + "region": "us-west-2", + } + + // Run the benchmark + for n := 0; n < b.N; n++ { + ms.get_metrics_name("request_count", labels) + } +} + +func BenchmarkCompileMetricsWithLabels(b *testing.B) { + labels := map[string]string{ + "env": "production", + "region": "us-west-2", + "app": "api-server", + } + + for n := 0; n < b.N; n++ { + compile_metrics_with_labels("request_count", labels) + } +} + +func BenchmarkValidateMetricsName(b *testing.B) { + input := "valid metric name with special chars @#! and underscores__" + + for n := 0; n < b.N; n++ { + validate_metrics_name(input) + } +} diff --git a/monitoring/helpers_test.go b/monitoring/helpers_test.go new file mode 100644 index 0000000..7530118 --- /dev/null +++ b/monitoring/helpers_test.go @@ -0,0 +1,143 @@ +package libpack_monitoring + +import ( + "os" + "testing" + + libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" + "github.com/stretchr/testify/assert" +) + +func TestGetMetricsName(t *testing.T) { + ms := &MetricsSetup{metrics_prefix: "prefix"} + libpack_config.PKG_NAME = "example_microservice" + + tests := []struct { + name string + metricName string + labels map[string]string + expectedOutput string + }{ + { + name: "No labels", + metricName: "test_metric", + labels: nil, + expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "With labels", + metricName: "test_metric", + labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Alphabetical order labels", + metricName: "test_metric", + labels: map[string]string{ + "label2": "value2", + "label1": "value1", + }, + expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Empty metric name", + metricName: "", + labels: nil, + expectedOutput: "prefix_{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Empty labels map", + metricName: "test_metric", + labels: map[string]string{}, + expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Single label", + metricName: "test_metric", + labels: map[string]string{ + "label1": "value1", + }, + expectedOutput: "prefix_test_metric{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Multiple labels with special characters", + metricName: "test_metric", + labels: map[string]string{ + "label-2": "value-2", + "label_1": "value_1", + }, + expectedOutput: "prefix_test_metric{label-2=\"value-2\",label_1=\"value_1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + { + name: "Prefix only", + metricName: "", + labels: map[string]string{ + "label1": "value1", + }, + expectedOutput: "prefix_{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ms.get_metrics_name(tt.metricName, tt.labels) + assert.Equal(t, tt.expectedOutput, result) + }) + } +} + +func TestCompileMetricsWithLabels(t *testing.T) { + tests := []struct { + name string + labels map[string]string + want string + }{ + {"request_count", map[string]string{"env": "production", "region": "us-west-2"}, "request_count_env_production_region_us-west-2"}, + {"metric_name", map[string]string{}, "metric_name"}, + {"metric_name", nil, "metric_name"}, + {"metric_name", map[string]string{"key1": "value1"}, "metric_name_key1_value1"}, + {"metric_name", map[string]string{"k": "v", "key2": "value2"}, "metric_name_k_v_key2_value2"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := compile_metrics_with_labels(tt.name, tt.labels); got != tt.want { + t.Errorf("compile_metrics_with_labels() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValidateMetricsName(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + {"Valid name", "valid_metric_name", false}, + {"Name with spaces", "valid metric name", true}, + {"Name with special chars", "valid@metric#name!", true}, + {"Name with leading underscore", "_valid_metric_name", true}, + {"Name with trailing underscore", "valid_metric_name_", true}, + {"Name with consecutive underscores", "valid__metric__name", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validate_metrics_name(tt.input); (err != nil) != tt.wantErr { + t.Errorf("validate_metrics_name() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func getPodName() string { + podName, err := os.Hostname() + if err != nil { + return "unknown" + } + return podName +}