diff --git a/README.md b/README.md index 02c32b8..5f9d544 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,56 @@ export TAILOR_TOKEN=your_access_token patterner metrics ``` -The metrics command outputs detailed JSON data about your workspace resources. +The metrics command displays metrics in a table format. Use the `--out-octocov-path` option to output metrics in octocov custom metrics format. + +#### Metrics Options + +- `--since, -s` (default: "30min") - Analyze execution results since the specified time period +- `--out-octocov-path` - Output metrics in octocov custom metrics format to the specified file +- `--with-lint-warnings` (default: false) - Display lint warnings along with metrics output +- `--with-coverage-full-report` (default: false) - Display detailed pipeline resolver step coverage along with metrics output + +##### Option Details + +**--with-coverage-full-report vs --with-lint-warnings:** +- `--with-coverage-full-report`: Displays detailed pipeline resolver step coverage information for execution quality monitoring + - Shows coverage percentage and executed steps count for each resolver + - Format: `Coverage Rate% [Executed Steps/Total Steps] Resolver Name` +- `--with-lint-warnings`: Displays detailed lint warnings list for code quality monitoring + - Shows specific lint issues and recommendations for code improvements + +Both options can be used together to provide comprehensive analysis combining execution quality and code quality insights. + +#### Usage Examples + +```bash +# Basic metrics (last 30 minutes) +patterner metrics + +# Metrics for the past hour +patterner metrics --since 1hour + +# Display metrics with lint warnings +patterner metrics --with-lint-warnings + +# Display metrics with coverage details +patterner metrics --with-coverage-full-report + +# Display metrics with both coverage and lint warnings +patterner metrics --with-coverage-full-report --with-lint-warnings + +# Output metrics to octocov custom metrics file +patterner metrics --out-octocov-path metrics.json + +# Comprehensive analysis with all options +patterner metrics --since 24hours --out-octocov-path metrics.json --with-coverage-full-report --with-lint-warnings +``` + +**Implementation Notes:** +``` + +``` ### View Coverage @@ -141,22 +190,35 @@ The following metrics are collected and displayed: **Pipeline Metrics:** -- `pipelines_total` - Total number of pipelines -- `pipeline_resolvers_total` - Total number of pipeline resolvers -- `pipeline_resolver_steps_total` - Total number of pipeline resolver steps -- `pipeline_resolver_execution_paths_total` - Total number of pipeline resolver execution paths - - Calculated based on the number of steps and tests in each resolver (steps \* 2^tests) +- `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_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 - Used to understand the total number of execution paths based on testable step combinations **TailorDB Metrics:** -- `tailordbs_total` - Total number of TailorDBs -- `tailordb_types_total` - Total number of TailorDB types -- `tailordb_type_fields_total` - Total number of TailorDB type fields +- `tailordbs_total` - Total number of TailorDBs (Unit: count) +- `tailordb_types_total` - Total number of TailorDB types (Unit: count) +- `tailordb_type_fields_total` - Total number of TailorDB type fields (Unit: count) **StateFlow Metrics:** -- `stateflows_total` - Total number of StateFlows +- `stateflows_total` - Total number of StateFlows (Unit: count) + +**Coverage Metrics:** + +- `pipeline_resolver_step_coverage_percentage` - Pipeline resolver step coverage (Unit: %) + - Calculation: (covered steps / total steps) * 100 + - Provides percentage representation of pipeline resolver step execution coverage + +**Lint Metrics:** + +- `lint_warnings_total` - Total number of lint warnings (Unit: count) + - Calculation: Number of warnings returned from the lint function + - Helps monitor code quality and adherence to best practices ## Configuration @@ -247,6 +309,10 @@ lint: - `patterner init` - Initialize configuration file - `patterner lint` - Lint workspace resources - `patterner metrics` - Display workspace metrics + - `--since, -s` (default: "30min") - Analyze execution results since the specified time period + - `--out-octocov-path` - Output metrics in octocov custom metrics format to the specified file + - `--with-lint-warnings` - Display lint warnings along with metrics output + - `--with-coverage-full-report` - Display detailed pipeline resolver step coverage along with metrics output - `patterner coverage` - Display pipeline resolver step coverage --- diff --git a/cmd/coverage.go b/cmd/coverage.go index 2e4078e..039c63b 100644 --- a/cmd/coverage.go +++ b/cmd/coverage.go @@ -77,7 +77,10 @@ var coverageCmd = &cobra.Command{ var total, covered int for _, rc := range coverage { if fullReport { - cover := float64(float64(rc.CoveredSteps)/float64(rc.TotalSteps)) * 100 + var cover float64 + if rc.TotalSteps > 0 { + cover = float64(float64(rc.CoveredSteps)/float64(rc.TotalSteps)) * 100 + } fmt.Printf("%5s%% [%d/%d] %s\n", fmt.Sprintf("%.1f", cover), rc.CoveredSteps, rc.TotalSteps, rc.Name) } total += rc.TotalSteps @@ -86,7 +89,11 @@ var coverageCmd = &cobra.Command{ if fullReport { fmt.Println() } - fmt.Printf("%s %.1f%% [%d/%d]\n", "Pipeline Resolver Step Coverage", float64(float64(covered)/float64(total))*100, covered, total) + var coverTotal float64 + if total > 0 { + coverTotal = float64(float64(covered)/float64(total)) * 100 + } + fmt.Printf("%s %.1f%% [%d/%d]\n", "Pipeline resolver step coverage", coverTotal, covered, total) return nil }, diff --git a/cmd/metrics.go b/cmd/metrics.go index d5de04d..b0f43f4 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -24,12 +24,25 @@ package cmd import ( "encoding/json" "fmt" + "math" + "os" + "time" + "github.com/k1LoW/duration" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/renderer" + "github.com/olekukonko/tablewriter/tw" "github.com/spf13/cobra" "github.com/tailor-platform/patterner/config" "github.com/tailor-platform/patterner/tailor" ) +var ( + outOctocovPath string + withLintWarnings bool + withCoverageFullReport bool +) + var metricsCmd = &cobra.Command{ Use: "metrics", Short: "retrieve and display metrics about the resources in the workspace", @@ -49,24 +62,172 @@ var metricsCmd = &cobra.Command{ if err != nil { return err } - resources, err := c.Resources(cmd.Context()) + d, err := duration.Parse(since) + if err != nil { + return err + } + s := time.Now().Add(-d) + resources, err := c.Resources(cmd.Context(), tailor.WithExecutionResults(&s)) if err != nil { return err } spi.Disable() + + if withLintWarnings { + fmt.Println("Lint warnings") + fmt.Println("============================================================") + warns, err := c.Lint(resources) + if err != nil { + return err + } + for _, w := range warns { + fmt.Printf("[%s] %s: %s\n", w.Type, w.Name, w.Message) + } + fmt.Println() + } + + if withCoverageFullReport { + fmt.Println("Coverage") + fmt.Println("============================================================") + coverages, err := c.Coverage(resources) + if err != nil { + return err + } + for _, rc := range coverages { + var cover float64 + if rc.TotalSteps > 0 { + cover = float64(float64(rc.CoveredSteps)/float64(rc.TotalSteps)) * 100 + } + fmt.Printf("%5s%% [%d/%d] %s\n", fmt.Sprintf("%.1f", cover), rc.CoveredSteps, rc.TotalSteps, rc.Name) + } + fmt.Println() + } + + if withCoverageFullReport || withLintWarnings { + fmt.Println("Metrics") + fmt.Println("============================================================") + } + metrics, err := c.Metrics(resources) if err != nil { return err } - b, err := json.MarshalIndent(metrics, "", " ") - if err != nil { + table := tablewriter.NewTable(os.Stdout, + tablewriter.WithTrimSpace(tw.Off), + tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ + Borders: tw.BorderNone, + Symbols: tw.NewSymbols(tw.StyleNone), + Settings: tw.Settings{ + Lines: tw.Lines{ + ShowTop: tw.Off, + ShowBottom: tw.Off, + ShowHeaderLine: tw.Off, + ShowFooterLine: tw.Off, + }, + Separators: tw.Separators{ + ShowHeader: tw.Off, + ShowFooter: tw.Off, + BetweenRows: tw.Off, + BetweenColumns: tw.Off, + }, + }, + })), + tablewriter.WithHeaderConfig(tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoFormat: tw.Off, + Alignment: tw.AlignLeft, + }, + Padding: tw.CellPadding{ + Global: tw.Padding{Left: tw.Space, Right: tw.Space, Top: tw.Empty, Bottom: tw.Empty}, + }, + }), + tablewriter.WithRowConfig(tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoFormat: tw.Off, + }, + ColumnAligns: []tw.Align{tw.AlignLeft, tw.AlignRight}, + Padding: tw.CellPadding{ + Global: tw.Padding{Left: tw.Space, Right: tw.Space, Top: tw.Empty, Bottom: tw.Empty}, + }, + }), + ) + data := make([][]string, 0, len(metrics)) + for _, m := range metrics { + if m.Error != nil { + data = append(data, []string{m.Name, fmt.Sprintf("Error: %v", m.Error)}) + continue + } + if m.Value == (math.Round(m.Value*10) / 10) { + data = append(data, []string{m.Name, fmt.Sprintf("%.0f%s", m.Value, m.Unit)}) + } else { + data = append(data, []string{m.Name, fmt.Sprintf("%.1f%s", m.Value, m.Unit)}) + } + } + if err := table.Bulk(data); err != nil { + return err + } + if err := table.Render(); err != nil { return err } - fmt.Println(string(b)) + if outOctocovPath != "" { + metricSet := &CustomMetricSet{ + Key: "workspace_metrics", + Name: "Workspace metrics using [Patterner](https://github.com/tailor-platform/patterner)", + Metadata: []*MetadataKV{ + { + Key: "workspace_id", + Value: cfg.WorkspaceID, + }, + }, + } + for _, m := range metrics { + metricSet.Metrics = append(metricSet.Metrics, &CustomMetric{ + Key: m.Key, + Name: m.Name, + Value: m.Value, + Unit: m.Unit, + }) + } + csets := []*CustomMetricSet{metricSet} + b, err := json.MarshalIndent(csets, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(outOctocovPath, b, 0644); err != nil { //nolint:gosec + return err + } + } + return nil }, } func init() { rootCmd.AddCommand(metricsCmd) + metricsCmd.Flags().StringVarP(&since, "since", "s", "30min", "only consider executions since the given duration (e.g., 24hours, 30min, 15sec)") + metricsCmd.Flags().StringVarP(&outOctocovPath, "out-octocov-path", "", "", "output the metrics in octocov custom metrics format to the specified file (e.g., ./metrics.json)") + metricsCmd.Flags().BoolVarP(&withLintWarnings, "with-lint-warnings", "", false, "display the lint warnings along with the metrics") + metricsCmd.Flags().BoolVarP(&withCoverageFullReport, "with-coverage-full-report", "", false, "display the coverage full report along with the metrics") +} + +// copy from github.com/k1LoW/octocov/report +// because octocov use tablewriter v0. +type MetadataKV struct { + Key string `json:"key"` + Name string `json:"name,omitempty"` + Value string `json:"value"` +} + +type CustomMetricSet struct { + Key string `json:"key"` + Name string `json:"name,omitempty"` + Metadata []*MetadataKV `json:"metadata,omitempty"` + Metrics []*CustomMetric `json:"metrics"` +} + +type CustomMetric struct { + Key string `json:"key"` + Name string `json:"name,omitempty"` + Value float64 `json:"value"` + Unit string `json:"unit,omitempty"` } diff --git a/go.mod b/go.mod index e7feed7..de80c6f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/creasty/defaults v1.8.0 github.com/goccy/go-yaml v1.18.0 github.com/k1LoW/duration v1.2.0 + github.com/olekukonko/tablewriter v1.0.9 github.com/spf13/cobra v1.10.1 github.com/vektah/gqlparser/v2 v2.5.30 google.golang.org/protobuf v1.36.9 @@ -17,15 +18,19 @@ require ( require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250717185734-6c6e0d3c608e.1 // indirect - github.com/fatih/color v1.7.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/spf13/pflag v1.0.10 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect ) diff --git a/go.sum b/go.sum index 7e0cceb..17e9289 100644 --- a/go.sum +++ b/go.sum @@ -13,43 +13,62 @@ github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXO github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/k1LoW/duration v1.2.0 h1:qq1gWtPh7YROFyerBufVP+ATR11mOOHDInrcC/Xe/6A= github.com/k1LoW/duration v1.2.0/go.mod h1:qUa0NptIiUl5EUsCc8wIiSaHuNjS4wmpYNMHp0l6pos= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g= @@ -57,5 +76,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go. google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tailor/metrics.go b/tailor/metrics.go index 92634df..04f320f 100644 --- a/tailor/metrics.go +++ b/tailor/metrics.go @@ -1,6 +1,7 @@ package tailor import ( + "errors" "math" ) @@ -9,19 +10,57 @@ const ( ) type Metric struct { - Name string - Description string - Value float64 + Key string + Name string + Value float64 + Unit string + Error error } func (c *Client) Metrics(resources *Resources) ([]Metric, error) { var metrics []Metric + // Coverage Metrics + coverage, err := c.Coverage(resources) + if err != nil { + return nil, err + } + var total, covered int + for _, rc := range coverage { + total += rc.TotalSteps + covered += rc.CoveredSteps + } + var coverTotal float64 + if total > 0 { + coverTotal = float64(float64(covered)/float64(total)) * 100 + } else { + coverTotal = 0 + } + metrics = append(metrics, Metric{ + Key: "pipeline_resolver_step_coverage_percentage", + Name: "Pipeline resolver step coverage", + Value: coverTotal, + Unit: "%", + }) + + // Lint Metrics + warns, err := c.Lint(resources) + if err != nil { + return nil, err + } + metrics = append(metrics, Metric{ + Key: "lint_warnings_total", + Name: "Total number of lint warnings", + Value: float64(len(warns)), + Unit: "", + }) + // Pipeline Metrics metrics = append(metrics, Metric{ - Name: "pipelines_total", - Description: "Total number of pipelines", - Value: float64(len(resources.Pipelines)), + Key: "pipelines_total", + Name: "Total number of pipelines", + Value: float64(len(resources.Pipelines)), + Unit: "", }) resolversTotal := 0 stepsTotal := 0 @@ -40,26 +79,34 @@ func (c *Client) Metrics(resources *Resources) ([]Metric, error) { } } metrics = append(metrics, Metric{ - Name: "pipeline_resolvers_total", - Description: "Total number of pipeline resolvers", - Value: float64(resolversTotal), + Key: "pipeline_resolvers_total", + Name: "Total number of pipeline resolvers", + Value: float64(resolversTotal), + Unit: "", }) metrics = append(metrics, Metric{ - Name: "pipeline_resolver_steps_total", - Description: "Total number of pipeline resolver steps", - Value: float64(stepsTotal), - }) - metrics = append(metrics, Metric{ - Name: "pipeline_resolver_execution_paths_total", - Description: "Total number of pipeline resolver execution paths", - Value: float64(executionPathsTotal), + Key: "pipeline_resolver_steps_total", + Name: "Total number of pipeline resolver steps", + Value: float64(stepsTotal), + Unit: "", }) + pathsMetic := Metric{ + Key: "pipeline_resolver_execution_paths_total", + Name: "Total number of pipeline resolver execution paths", + Value: float64(executionPathsTotal), + Unit: "", + } + if executionPathsTotal < 0 { + pathsMetic.Error = errors.New("overflow detected") + } + metrics = append(metrics, pathsMetic) // TailorDB Metrics metrics = append(metrics, Metric{ - Name: "tailordbs_total", - Description: "Total number of TailorDBs", - Value: float64(len(resources.TailorDBs)), + Key: "tailordbs_total", + Name: "Total number of TailorDBs", + Value: float64(len(resources.TailorDBs)), + Unit: "", }) typesTotal := 0 fieldsTotal := 0 @@ -70,21 +117,24 @@ func (c *Client) Metrics(resources *Resources) ([]Metric, error) { } } metrics = append(metrics, Metric{ - Name: "tailordb_types_total", - Description: "Total number of TailorDB types", - Value: float64(typesTotal), + Key: "tailordb_types_total", + Name: "Total number of TailorDB types", + Value: float64(typesTotal), + Unit: "", }) metrics = append(metrics, Metric{ - Name: "tailordb_type_fields_total", - Description: "Total number of TailorDB type fields", - Value: float64(fieldsTotal), + Key: "tailordb_type_fields_total", + Name: "Total number of TailorDB type fields", + Value: float64(fieldsTotal), + Unit: "", }) // StateFlow Metrics metrics = append(metrics, Metric{ - Name: "stateflows_total", - Description: "Total number of StateFlows", - Value: float64(len(resources.StateFlows)), + Key: "stateflows_total", + Name: "Total number of StateFlows", + Value: float64(len(resources.StateFlows)), + Unit: "", }) return metrics, nil diff --git a/tailor/metrics_test.go b/tailor/metrics_test.go index 2f4257d..9d55e22 100644 --- a/tailor/metrics_test.go +++ b/tailor/metrics_test.go @@ -19,14 +19,16 @@ func TestClient_Metrics(t *testing.T) { StateFlows: []*StateFlow{}, }, expectedMetrics: map[string]float64{ - "pipelines_total": 0, - "pipeline_resolvers_total": 0, - "pipeline_resolver_steps_total": 0, - "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths - "tailordbs_total": 0, - "tailordb_types_total": 0, - "tailordb_type_fields_total": 0, - "stateflows_total": 0, + "pipeline_resolver_step_coverage_percentage": 0, + "lint_warnings_total": 0, + "pipelines_total": 0, + "pipeline_resolvers_total": 0, + "pipeline_resolver_steps_total": 0, + "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths + "tailordbs_total": 0, + "tailordb_types_total": 0, + "tailordb_type_fields_total": 0, + "stateflows_total": 0, }, }, { @@ -63,14 +65,16 @@ func TestClient_Metrics(t *testing.T) { }, }, expectedMetrics: map[string]float64{ - "pipelines_total": 1, - "pipeline_resolvers_total": 1, - "pipeline_resolver_steps_total": 1, - "pipeline_resolver_execution_paths_total": 1, // 1 * 2^0 = 1 (1 step, no tests) - "tailordbs_total": 1, - "tailordb_types_total": 1, - "tailordb_type_fields_total": 2, // id and name fields - "stateflows_total": 1, + "pipeline_resolver_step_coverage_percentage": 0, // no coverage data for test + "lint_warnings_total": 1, + "pipelines_total": 1, + "pipeline_resolvers_total": 1, + "pipeline_resolver_steps_total": 1, + "pipeline_resolver_execution_paths_total": 1, // 1 * 2^0 = 1 (1 step, no tests) + "tailordbs_total": 1, + "tailordb_types_total": 1, + "tailordb_type_fields_total": 2, // id and name fields + "stateflows_total": 1, }, }, { @@ -150,14 +154,16 @@ func TestClient_Metrics(t *testing.T) { }, }, expectedMetrics: map[string]float64{ - "pipelines_total": 2, // ns1, ns2 - "pipeline_resolvers_total": 3, // resolver1, resolver2, resolver3 - "pipeline_resolver_steps_total": 6, // 2+3+1 steps - "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 - "tailordb_type_fields_total": 9, // 3+2+4 fields - "stateflows_total": 3, // flow1, flow2, flow3 + "pipeline_resolver_step_coverage_percentage": 0, // no coverage data for test + "lint_warnings_total": 3, + "pipelines_total": 2, // ns1, ns2 + "pipeline_resolvers_total": 3, // resolver1, resolver2, resolver3 + "pipeline_resolver_steps_total": 6, // 2+3+1 steps + "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 + "tailordb_type_fields_total": 9, // 3+2+4 fields + "stateflows_total": 3, // flow1, flow2, flow3 }, }, { @@ -192,14 +198,16 @@ func TestClient_Metrics(t *testing.T) { }, }, expectedMetrics: map[string]float64{ - "pipelines_total": 0, - "pipeline_resolvers_total": 0, - "pipeline_resolver_steps_total": 0, - "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths - "tailordbs_total": 1, - "tailordb_types_total": 1, - "tailordb_type_fields_total": 2, // Only top-level fields are counted - "stateflows_total": 0, + "pipeline_resolver_step_coverage_percentage": 0, + "lint_warnings_total": 0, + "pipelines_total": 0, + "pipeline_resolvers_total": 0, + "pipeline_resolver_steps_total": 0, + "pipeline_resolver_execution_paths_total": 0, // 0 resolvers = 0 paths + "tailordbs_total": 1, + "tailordb_types_total": 1, + "tailordb_type_fields_total": 2, // Only top-level fields are counted + "stateflows_total": 0, }, }, } @@ -226,7 +234,7 @@ func TestClient_Metrics(t *testing.T) { // Create a map for easier assertion metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // Verify all expected metrics are present @@ -287,11 +295,8 @@ func TestClient_Metrics_MetricStructure(t *testing.T) { if metric.Name == "" { t.Error("Metric name should not be empty") } - if metric.Description == "" { - t.Error("Metric description should not be empty") - } - if metric.Value < 0 { - t.Errorf("Metric value should be non-negative, got %f", metric.Value) + if metric.Key == "" { + t.Error("Metric key should not be empty") } } } @@ -355,7 +360,7 @@ func TestClient_Metrics_SpecificMetricValues(t *testing.T) { // Create metric lookup map metricMap := make(map[string]Metric) for _, m := range metrics { - metricMap[m.Name] = m + metricMap[m.Key] = m } // Test pipeline metrics @@ -419,7 +424,7 @@ func TestClient_Metrics_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // All counts should be 0 @@ -470,7 +475,7 @@ func TestClient_Metrics_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } if metricMap["pipelines_total"] != float64(2) { @@ -506,7 +511,7 @@ func TestClient_Metrics_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } if metricMap["tailordbs_total"] != float64(2) { @@ -545,7 +550,7 @@ func TestClient_Metrics_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } if metricMap["tailordbs_total"] != float64(1) { @@ -560,7 +565,7 @@ func TestClient_Metrics_EdgeCases(t *testing.T) { }) } -func TestClient_Metrics_MetricNames(t *testing.T) { +func TestClient_Metrics_MetricKeys(t *testing.T) { cfg := createTestConfig(t) client, err := New(cfg) if err != nil { @@ -578,7 +583,9 @@ func TestClient_Metrics_MetricNames(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } - expectedMetricNames := []string{ + expectedMetricKeys := []string{ + "pipeline_resolver_step_coverage_percentage", + "lint_warnings_total", "pipelines_total", "pipeline_resolvers_total", "pipeline_resolver_steps_total", @@ -589,86 +596,27 @@ func TestClient_Metrics_MetricNames(t *testing.T) { "stateflows_total", } - actualNames := make([]string, len(metrics)) + actualKeys := make([]string, len(metrics)) for i, m := range metrics { - actualNames[i] = m.Name + actualKeys[i] = m.Key } - // Check that we have all expected metric names - for _, expectedName := range expectedMetricNames { + // Check that we have all expected metric keys + for _, expectedKey := range expectedMetricKeys { found := false - for _, actualName := range actualNames { - if actualName == expectedName { + for _, actualKey := range actualKeys { + if actualKey == expectedKey { found = true break } } if !found { - t.Errorf("Expected metric %s not found", expectedName) + t.Errorf("Expected metric %s not found", expectedKey) } } - if len(expectedMetricNames) != len(actualNames) { - t.Errorf("Expected %d metrics, got %d", len(expectedMetricNames), len(actualNames)) - } -} - -func TestClient_Metrics_MetricDescriptions(t *testing.T) { - cfg := createTestConfig(t) - client, err := New(cfg) - if err != nil { - t.Fatalf("Failed to create client: %v", err) - } - - resources := &Resources{ - Applications: []*Application{}, - Pipelines: []*Pipeline{}, - TailorDBs: []*TailorDB{}, - StateFlows: []*StateFlow{}, - } - metrics, err := client.Metrics(resources) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - expectedDescriptions := map[string]string{ - "pipelines_total": "Total number of pipelines", - "pipeline_resolvers_total": "Total number of pipeline resolvers", - "pipeline_resolver_steps_total": "Total number of pipeline resolver steps", - "pipeline_resolver_execution_paths_total": "Total number of pipeline resolver execution paths", - "tailordbs_total": "Total number of TailorDBs", - "tailordb_types_total": "Total number of TailorDB types", - "tailordb_type_fields_total": "Total number of TailorDB type fields", - "stateflows_total": "Total number of StateFlows", - } - - for _, metric := range metrics { - expectedDesc, exists := expectedDescriptions[metric.Name] - if !exists { - t.Errorf("Unexpected metric: %s", metric.Name) - } - if expectedDesc != metric.Description { - t.Errorf("Wrong description for metric %s: expected '%s', got '%s'", - metric.Name, expectedDesc, metric.Description) - } - } -} - -func TestMetric_Fields(t *testing.T) { - metric := Metric{ - Name: "test_metric", - Description: "Test metric description", - Value: 42.5, - } - - if metric.Name != "test_metric" { - t.Errorf("Expected Name to be 'test_metric', got '%s'", metric.Name) - } - if metric.Description != "Test metric description" { - t.Errorf("Expected Description to be 'Test metric description', got '%s'", metric.Description) - } - if metric.Value != 42.5 { - t.Errorf("Expected Value to be 42.5, got %f", metric.Value) + if len(expectedMetricKeys) != len(actualKeys) { + t.Errorf("Expected %d metrics, got %d", len(expectedMetricKeys), len(actualKeys)) } } @@ -682,11 +630,11 @@ func TestClient_Metrics_LargeNumbers(t *testing.T) { // Create resources with many items pipelines := make([]*Pipeline, 100) - for i := 0; i < 100; i++ { + for i := range 100 { resolvers := make([]*PipelineResolver, 10) - for j := 0; j < 10; j++ { + for j := range 10 { steps := make([]*PipelineStep, 5) - for k := 0; k < 5; k++ { + for k := range 5 { steps[k] = &PipelineStep{Name: "step"} } resolvers[j] = &PipelineResolver{ @@ -712,7 +660,7 @@ func TestClient_Metrics_LargeNumbers(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // Verify calculations @@ -968,7 +916,7 @@ func TestClient_Metrics_ExecutionPaths(t *testing.T) { // Create a map for easier assertion metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // Verify expected metrics @@ -1034,7 +982,7 @@ func TestClient_Metrics_ExecutionPaths_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // Expected: 3 steps, 2 tests (one empty string doesn't count, whitespace does count) @@ -1074,7 +1022,7 @@ func TestClient_Metrics_ExecutionPaths_EdgeCases(t *testing.T) { metricMap := make(map[string]float64) for _, m := range metrics { - metricMap[m.Name] = m.Value + metricMap[m.Key] = m.Value } // Expected: 1 step, 1 test -> 1 * 2^1 = 2