From 13676c0eb1aeb3d8fd17f35fd5dacf5fd048c641 Mon Sep 17 00:00:00 2001 From: Ken'ichiro Oyama Date: Thu, 9 Oct 2025 16:26:07 +0900 Subject: [PATCH] feat: add metrics `pipeline_resolver_graphql_steps_total` and `pipeline_resolver_function_steps_total` --- README.md | 2 + tailor/metrics.go | 23 ++++ tailor/metrics_test.go | 262 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+) diff --git a/README.md b/README.md index de267af..3483004 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,8 @@ The following metrics are collected and displayed: - `pipelines_total` - Total number of pipelines (Unit: count) - `pipeline_resolvers_total` - Total number of pipeline resolvers (Unit: count) - `pipeline_resolver_steps_total` - Total number of pipeline resolver steps (Unit: count) +- `pipeline_resolver_graphql_steps_total` - Total number of Pipeline resolver GraphQL steps (Unit: count) +- `pipeline_resolver_function_steps_total` - Total number of Pipeline resolver Function steps (Unit: count) - `pipeline_resolver_execution_paths_total` - Total number of pipeline resolver execution paths (Unit: count) - Calculation: Based on the number of steps and tests in each resolver (steps \* 2^tests) - Includes overflow detection: Reports error if negative values are encountered diff --git a/tailor/metrics.go b/tailor/metrics.go index de60ff3..7464f6e 100644 --- a/tailor/metrics.go +++ b/tailor/metrics.go @@ -3,6 +3,8 @@ package tailor import ( "errors" "math" + + tailorv1 "buf.build/gen/go/tailor-inc/tailor/protocolbuffers/go/tailor/v1" ) const ( @@ -64,6 +66,8 @@ func (c *Client) Metrics(resources *Resources) ([]Metric, error) { }) resolversTotal := 0 stepsTotal := 0 + graphQLStepsTotal := 0 + functionStepsTotal := 0 executionPathsTotal := 0 for _, p := range resources.Pipelines { resolversTotal += len(p.Resolvers) @@ -74,6 +78,12 @@ func (c *Client) Metrics(resources *Resources) ([]Metric, error) { if s.Operation.Test != "" { testsCount++ } + switch s.Operation.Type { + case tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL: + graphQLStepsTotal++ + case tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION: + functionStepsTotal++ + } } executionPathsTotal += len(r.Steps) * int(math.Pow(2, float64(testsCount))) } @@ -90,6 +100,19 @@ func (c *Client) Metrics(resources *Resources) ([]Metric, error) { Value: float64(stepsTotal), Unit: "", }) + metrics = append(metrics, Metric{ + Key: "pipeline_resolver_graphql_steps_total", + Name: "Total number of Pipeline resolver GraphQL steps", + Value: float64(graphQLStepsTotal), + Unit: "", + }) + metrics = append(metrics, Metric{ + Key: "pipeline_resolver_function_steps_total", + Name: "Total number of Pipeline resolver Function steps", + Value: float64(functionStepsTotal), + Unit: "", + }) + pathsMetic := Metric{ Key: "pipeline_resolver_execution_paths_total", Name: "Total number of Pipeline resolver execution paths", diff --git a/tailor/metrics_test.go b/tailor/metrics_test.go index 9d55e22..febda0b 100644 --- a/tailor/metrics_test.go +++ b/tailor/metrics_test.go @@ -2,6 +2,8 @@ package tailor import ( "testing" + + tailorv1 "buf.build/gen/go/tailor-inc/tailor/protocolbuffers/go/tailor/v1" ) func TestClient_Metrics(t *testing.T) { @@ -24,6 +26,8 @@ func TestClient_Metrics(t *testing.T) { "pipelines_total": 0, "pipeline_resolvers_total": 0, "pipeline_resolver_steps_total": 0, + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths "tailordbs_total": 0, "tailordb_types_total": 0, @@ -70,6 +74,8 @@ func TestClient_Metrics(t *testing.T) { "pipelines_total": 1, "pipeline_resolvers_total": 1, "pipeline_resolver_steps_total": 1, + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, "pipeline_resolver_execution_paths_total": 1, // 1 * 2^0 = 1 (1 step, no tests) "tailordbs_total": 1, "tailordb_types_total": 1, @@ -159,6 +165,8 @@ func TestClient_Metrics(t *testing.T) { "pipelines_total": 2, // ns1, ns2 "pipeline_resolvers_total": 3, // resolver1, resolver2, resolver3 "pipeline_resolver_steps_total": 6, // 2+3+1 steps + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, "pipeline_resolver_execution_paths_total": 6, // 2*2^0 + 3*2^0 + 1*2^0 = 2+3+1 (no tests) "tailordbs_total": 2, // two TailorDB instances "tailordb_types_total": 3, // User, Post, Comment @@ -203,6 +211,8 @@ func TestClient_Metrics(t *testing.T) { "pipelines_total": 0, "pipeline_resolvers_total": 0, "pipeline_resolver_steps_total": 0, + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths "tailordbs_total": 1, "tailordb_types_total": 1, @@ -589,6 +599,8 @@ func TestClient_Metrics_MetricKeys(t *testing.T) { "pipelines_total", "pipeline_resolvers_total", "pipeline_resolver_steps_total", + "pipeline_resolver_graphql_steps_total", + "pipeline_resolver_function_steps_total", "pipeline_resolver_execution_paths_total", "tailordbs_total", "tailordb_types_total", @@ -1033,3 +1045,253 @@ func TestClient_Metrics_ExecutionPaths_EdgeCases(t *testing.T) { } }) } + +func TestClient_Metrics_StepTypes(t *testing.T) { + tests := []struct { + name string + resources *Resources + expectedMetrics map[string]float64 + }{ + { + name: "GraphQL and Function steps counting", + resources: &Resources{ + Pipelines: []*Pipeline{ + { + NamespaceName: "test-namespace", + Resolvers: []*PipelineResolver{ + { + Name: "mixed_resolver", + Steps: []*PipelineStep{ + { + Name: "graphql_step1", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + { + Name: "graphql_step2", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + { + Name: "function_step1", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + }, + }, + }, + }, + }, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 2, + "pipeline_resolver_function_steps_total": 1, + "pipeline_resolver_steps_total": 3, + }, + }, + { + name: "only GraphQL steps", + resources: &Resources{ + Pipelines: []*Pipeline{ + { + NamespaceName: "graphql-namespace", + Resolvers: []*PipelineResolver{ + { + Name: "graphql_resolver", + Steps: []*PipelineStep{ + { + Name: "step1", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + { + Name: "step2", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + }, + }, + }, + }, + }, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 2, + "pipeline_resolver_function_steps_total": 0, + "pipeline_resolver_steps_total": 2, + }, + }, + { + name: "only Function steps", + resources: &Resources{ + Pipelines: []*Pipeline{ + { + NamespaceName: "function-namespace", + Resolvers: []*PipelineResolver{ + { + Name: "function_resolver", + Steps: []*PipelineStep{ + { + Name: "step1", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + { + Name: "step2", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + { + Name: "step3", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + }, + }, + }, + }, + }, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 3, + "pipeline_resolver_steps_total": 3, + }, + }, + { + name: "multiple resolvers with mixed step types", + resources: &Resources{ + Pipelines: []*Pipeline{ + { + NamespaceName: "multi-namespace", + Resolvers: []*PipelineResolver{ + { + Name: "resolver1", + Steps: []*PipelineStep{ + { + Name: "graphql_step", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + { + Name: "function_step", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + }, + }, + { + Name: "resolver2", + Steps: []*PipelineStep{ + { + Name: "graphql_step", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_GRAPHQL, + }, + }, + }, + }, + { + Name: "resolver3", + Steps: []*PipelineStep{ + { + Name: "function_step1", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + { + Name: "function_step2", + Operation: PipelineStepOperation{ + Type: tailorv1.PipelineResolver_OPERATION_TYPE_FUNCTION, + }, + }, + }, + }, + }, + }, + }, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 2, // 1 + 1 + 0 + "pipeline_resolver_function_steps_total": 3, // 1 + 0 + 2 + "pipeline_resolver_steps_total": 5, // 2 + 1 + 2 + }, + }, + { + name: "empty resources - zero counts", + resources: &Resources{ + Pipelines: []*Pipeline{}, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, + "pipeline_resolver_steps_total": 0, + }, + }, + { + name: "resolver with no steps", + resources: &Resources{ + Pipelines: []*Pipeline{ + { + NamespaceName: "empty-resolver-namespace", + Resolvers: []*PipelineResolver{ + { + Name: "empty_resolver", + Steps: []*PipelineStep{}, + }, + }, + }, + }, + }, + expectedMetrics: map[string]float64{ + "pipeline_resolver_graphql_steps_total": 0, + "pipeline_resolver_function_steps_total": 0, + "pipeline_resolver_steps_total": 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := createTestConfig(t) + client, err := New(cfg) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + metrics, err := client.Metrics(tt.resources) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Create a map for easier assertion + metricMap := make(map[string]float64) + for _, m := range metrics { + metricMap[m.Key] = m.Value + } + + // Verify expected metrics + for expectedName, expectedValue := range tt.expectedMetrics { + actualValue, exists := metricMap[expectedName] + if !exists { + t.Errorf("Expected metric %s not found", expectedName) + continue + } + if actualValue != expectedValue { + t.Errorf("Metric %s: expected %.0f, got %.0f", expectedName, expectedValue, actualValue) + } + } + }) + } +}