Skip to content

Commit

Permalink
all: collect metrics
Browse files Browse the repository at this point in the history
all: collect metrics
  • Loading branch information
seilagamo committed Nov 6, 2023
1 parent 3d37036 commit 2ba18ce
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 39 deletions.
35 changes: 31 additions & 4 deletions cmd/lava/internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ package run
import (
"errors"
"fmt"
"log/slog"
"net/url"
"os"
"path/filepath"
"time"

"github.com/fatih/color"

"github.com/adevinta/lava/cmd/lava/internal/base"
"github.com/adevinta/lava/internal/config"
"github.com/adevinta/lava/internal/engine"
"github.com/adevinta/lava/internal/metrics"
"github.com/adevinta/lava/internal/report"
)

Expand All @@ -38,8 +42,9 @@ output is disabled in the following cases:
}

var (
cfgfile = CmdRun.Flag.String("c", "lava.yaml", "config file")
forceColor = CmdRun.Flag.Bool("forcecolor", false, "force colorized output")
cfgfile = CmdRun.Flag.String("c", "lava.yaml", "config file")
forceColor = CmdRun.Flag.Bool("forcecolor", false, "force colorized output")
metricsFile = CmdRun.Flag.String("m", "", "metrics output file")
)

func init() {
Expand All @@ -56,17 +61,29 @@ func run(args []string) error {
color.NoColor = false
}

executionTime := time.Now()
metrics.Collect("execution_time", executionTime)

cfg, err := config.ParseFile(*cfgfile)
if err != nil {
return fmt.Errorf("parse config file: %w", err)
}

if err := os.Chdir(filepath.Dir(*cfgfile)); err != nil {
if err = os.Chdir(filepath.Dir(*cfgfile)); err != nil {
return fmt.Errorf("change directory: %w", err)
}
metrics.Collect("lava_version", cfg.LavaVersion)
metrics.Collect("targets", cfg.Targets)
if os.Getenv("GITHUB_SERVER_URL") != "" && os.Getenv("GITHUB_REPOSITORY") != "" {
ghRepo, err := url.JoinPath(os.Getenv("GITHUB_SERVER_URL"), os.Getenv("GITHUB_REPOSITORY"))
if err != nil {
slog.Warn("error collecting the GitHub repo URI")
} else {
metrics.Collect("repository_uri", ghRepo)
}
}

base.LogLevel.Set(cfg.LogLevel)

er, err := engine.Run(cfg.ChecktypesURLs, cfg.Targets, cfg.AgentConfig)
if err != nil {
return fmt.Errorf("run: %w", err)
Expand All @@ -83,6 +100,16 @@ func run(args []string) error {
return fmt.Errorf("render report: %w", err)
}

metrics.Collect("exit_code", exitCode)
finishTime := time.Now()
duration := finishTime.Sub(executionTime)
metrics.Collect("duration", duration.String())

if *metricsFile != "" {
if err = metrics.Write(*metricsFile); err != nil {
return fmt.Errorf("write metrics: %w", err)
}
}
os.Exit(int(exitCode))

return nil
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ type ReportConfig struct {
// Exclusions is a list of findings that will be ignored. For
// instance, accepted risks, false positives, etc.
Exclusions []Exclusion `yaml:"exclusions"`

// Metrics decides is a metrics report is printed.
Metrics bool `yaml:"metrics"`
}

// Target represents the target of a scan.
Expand Down
101 changes: 101 additions & 0 deletions internal/metrics/metrics_collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2023 Adevinta

// Package metrics collects all kind of Lava metrics.
package metrics

import (
"encoding/json"
"fmt"
"io"
"os"
"sync"
)

// DefaultCollector is the default [Collector] and is used by [Collect] and [Write].
var DefaultCollector *Collector

// init initializes the DefaultCollector.
func init() {
realInitialization()
}

func realInitialization() {
DefaultCollector = &Collector{
metrics: make(map[string]interface{}),
emitter: NewJSONEmitter(),
}
}

// Collector represents a metrics collector.
type Collector struct {
mutex sync.Mutex
metrics map[string]interface{}
emitter emitter
}

// Collect stores the metric.
func (c *Collector) Collect(metric string, value interface{}) {
c.mutex.Lock()
c.metrics[metric] = value
c.mutex.Unlock()
}

// Write renders the metrics.
func (c *Collector) Write(metricsFile string) error {
if err := c.emitter.Emit(metricsFile, c.metrics); err != nil {
return fmt.Errorf("emit metrics: %w", err)
}
return nil
}

// A emitter emit metrics.
type emitter interface {
Emit(metricsFile string, metrics map[string]interface{}) error
}

// JSONEmitter emits metrics in JSON format.
type JSONEmitter struct {
createWriter func(metricsFile string) (io.WriteCloser, error)
}

// NewJSONEmitter create a metrics emitter.
func NewJSONEmitter() *JSONEmitter {
return &JSONEmitter{
// For lazy file creation.
createWriter: func(metricsFile string) (io.WriteCloser, error) {
f, err := os.Create(metricsFile)
if err != nil {
return nil, fmt.Errorf("create file: %w", err)
}
return f, nil
},
}
}

// Emit renders the metrics in JSON format.
func (c *JSONEmitter) Emit(metricsFile string, metrics map[string]interface{}) error {
w, err := c.createWriter(metricsFile)
if err != nil {
return fmt.Errorf("create writer: %w", err)
}
defer w.Close()
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err = enc.Encode(metrics); err != nil {
return fmt.Errorf("encode report: %w", err)
}
return nil
}

// Collect stores the metric.
func Collect(metric string, value interface{}) {
DefaultCollector.Collect(metric, value)
}

// Write renders the metrics.
func Write(metricsFile string) error {
if err := DefaultCollector.Write(metricsFile); err != nil {

Check warning on line 97 in internal/metrics/metrics_collector.go

View workflow job for this annotation

GitHub Actions / Lint

if-return: redundant if ...; err != nil check, just return error instead. (revive)
return err
}
return nil
}
Loading

0 comments on commit 2ba18ce

Please sign in to comment.