diff --git a/checks/common.go b/checks/common.go index 09628cfa0..34f41e289 100644 --- a/checks/common.go +++ b/checks/common.go @@ -73,37 +73,92 @@ func transform(ctx *context.Context, in *pkg.CheckResult) ([]*pkg.CheckResult, e var transformed []pkg.TransformedCheckResult if err := json.Unmarshal([]byte(out), &transformed); err != nil { - return nil, err + var t pkg.TransformedCheckResult + if errSingle := json.Unmarshal([]byte(out), &t); errSingle != nil { + return nil, err + } + transformed = []pkg.TransformedCheckResult{t} } var results []*pkg.CheckResult - - for _, t := range transformed { - t.Icon = def(t.Icon, in.Check.GetIcon()) - t.Description = def(t.Description, in.Check.GetDescription()) - t.Name = def(t.Name, in.Check.GetName()) - t.Type = def(t.Type, in.Check.GetType()) - t.Endpoint = def(t.Endpoint, in.Check.GetEndpoint()) - t.TransformDeleteStrategy = def(t.TransformDeleteStrategy, in.Check.GetTransformDeleteStrategy()) - r := t.ToCheckResult() - r.Canary = in.Canary - r.Canary.Namespace = def(t.Namespace, r.Canary.Namespace) - if r.Canary.Labels == nil { - r.Canary.Labels = make(map[string]string) - } - - // We use this label to set the transformed column to true - // This label is used and then removed in pkg.FromV1 function - r.Canary.Labels["transformed"] = "true" //nolint:goconst - r.Transformed = true - results = append(results, &r) + if len(transformed) == 0 { + ctx.Tracef("transformation returned empty array") + return nil, nil } - if ctx.IsTrace() { - ctx.Tracef("transformed %s into %v", in, results) + t := transformed[0] + + if t.Name != "" && t.Name != in.Check.GetName() { + // new check result created with a new name + for _, t := range transformed { + t.Icon = def(t.Icon, in.Check.GetIcon()) + t.Description = def(t.Description, in.Check.GetDescription()) + t.Name = def(t.Name, in.Check.GetName()) + t.Type = def(t.Type, in.Check.GetType()) + t.Endpoint = def(t.Endpoint, in.Check.GetEndpoint()) + t.TransformDeleteStrategy = def(t.TransformDeleteStrategy, in.Check.GetTransformDeleteStrategy()) + r := t.ToCheckResult() + r.Canary = in.Canary + r.Canary.Namespace = def(t.Namespace, r.Canary.Namespace) + if r.Canary.Labels == nil { + r.Canary.Labels = make(map[string]string) + } + + // We use this label to set the transformed column to true + // This label is used and then removed in pkg.FromV1 function + r.Canary.Labels["transformed"] = "true" //nolint:goconst + r.Transformed = true + results = append(results, &r) + } + if ctx.IsTrace() { + ctx.Tracef("transformed %s into %v", in, results) + } + return results, nil + + } else if len(transformed) == 1 && t.Name == "" { + if ctx.IsTrace() { + ctx.Tracef("merging %v into %v", t, in) + } + in.Metrics = append(in.Metrics, t.Metrics...) + if t.Start != nil { + in.Start = *t.Start + } + + if t.Pass != nil { + in.Pass = *t.Pass + } + if t.Invalid != nil { + in.Invalid = *t.Invalid + } + if t.Duration != nil { + in.Duration = *t.Duration + } + if t.Message != "" { + in.Message = t.Message + } + if t.Description != "" { + in.Description = t.Description + } + if t.Error != "" { + in.Error = t.Error + } + if t.Detail != nil { + in.Detail = t.Detail + } + if t.DisplayType != "" { + in.DisplayType = t.DisplayType + } + if len(t.Data) > 0 { + for k, v := range t.Data { + in.Data[k] = v + } + } + } else { + return nil, fmt.Errorf("transformation returned more than 1 entry without a name") } - return results, nil + return []*pkg.CheckResult{in}, nil + } func GetJunitReportFromResults(canaryName string, results []*pkg.CheckResult) JunitTestSuite { diff --git a/checks/metrics.go b/checks/metrics.go index a679d491e..bf58fb883 100644 --- a/checks/metrics.go +++ b/checks/metrics.go @@ -60,23 +60,33 @@ func getWithEnvironment(ctx *context.Context, r *pkg.CheckResult) *context.Conte return ctx.New(r.Data) } -func getLabels(ctx *context.Context, metric external.Metrics) (map[string]string, []string, error) { +func getLabels(ctx *context.Context, metric external.Metrics) (map[string]string, error) { var labels = make(map[string]string) - var names = []string{} for _, label := range metric.Labels { val := label.Value if label.ValueExpr != "" { var err error val, err = template(ctx, v1.Template{Expression: label.ValueExpr}) if err != nil { - return nil, nil, err + return nil, err } } labels[label.Name] = val - names = append(names, label.Name) + + } + + return labels, nil +} + +func getLabelNames(labels map[string]string) []string { + var s []string + + for k := range labels { + s = append(s, k) } - sort.Strings(names) - return labels, names, nil + sort.Strings(s) + + return s } func getLabelString(labels map[string]string) string { @@ -98,6 +108,11 @@ func exportCheckMetrics(ctx *context.Context, results pkg.Results) { } for _, r := range results { + for _, metric := range r.Metrics { + if err := exportMetric(ctx, metric); err != nil { + r.ErrorMessage(err) + } + } for _, spec := range r.Check.GetMetricsSpec() { if spec.Name == "" || spec.Value == "" { continue @@ -105,44 +120,59 @@ func exportCheckMetrics(ctx *context.Context, results pkg.Results) { ctx = getWithEnvironment(ctx, r) - var err error - var labels map[string]string - var labelNames []string - if labels, labelNames, err = getLabels(ctx, spec); err != nil { + if metric, err := templateMetrics(ctx, spec); err != nil { + r.ErrorMessage(err) + } else if err := exportMetric(ctx, *metric); err != nil { r.ErrorMessage(err) - continue } + } + } +} - var collector prometheus.Collector - var e any - if collector, e = getOrAddPrometheusMetric(spec.Name, spec.Type, labelNames); e != nil { - r.ErrorMessage(fmt.Errorf("failed to create metric %s (%s) %s: %s", spec.Name, spec.Type, labelNames, e)) - continue - } +func templateMetrics(ctx *context.Context, spec external.Metrics) (*pkg.Metric, error) { + var val float64 + var err error + var labels map[string]string + if val, err = getMetricValue(ctx, spec); err != nil { + return nil, err + } - var val float64 - if val, err = getMetricValue(ctx, spec); err != nil { - r.ErrorMessage(err) - continue - } + if labels, err = getLabels(ctx, spec); err != nil { + return nil, err + } - if ctx.IsDebug() { - ctx.Debugf("%s%v=%0.3f", spec.Name, getLabelString(labels), val) - } + return &pkg.Metric{ + Name: spec.Name, + Type: pkg.MetricType(spec.Type), + Value: val, + Labels: labels, + }, nil +} - switch collector := collector.(type) { - case *prometheus.HistogramVec: - collector.With(labels).Observe(val) - case *prometheus.GaugeVec: - collector.With(labels).Set(val) - case *prometheus.CounterVec: - if val <= 0 { - continue - } - collector.With(labels).Add(val) - } +func exportMetric(ctx *context.Context, spec pkg.Metric) error { + var collector prometheus.Collector + labelNames := getLabelNames(spec.Labels) + var e any + if collector, e = getOrAddPrometheusMetric(spec.Name, string(spec.Type), labelNames); e != nil { + return fmt.Errorf("failed to create metric %s (%s) %s: %s", spec.Name, spec.Type, labelNames, e) + } + + if ctx.IsDebug() { + ctx.Debugf("%s%v=%0.3f", spec.Name, getLabelString(spec.Labels), spec.Value) + } + + switch collector := collector.(type) { + case *prometheus.HistogramVec: + collector.With(spec.Labels).Observe(spec.Value) + case *prometheus.GaugeVec: + collector.With(spec.Labels).Set(spec.Value) + case *prometheus.CounterVec: + if spec.Value <= 0 { + return nil } + collector.With(spec.Labels).Add(spec.Value) } + return nil } func getMetricValue(ctx *context.Context, spec external.Metrics) (float64, error) { diff --git a/fixtures/minimal/metrics-transformed.yaml b/fixtures/minimal/metrics-transformed.yaml new file mode 100644 index 000000000..bc2c5fff2 --- /dev/null +++ b/fixtures/minimal/metrics-transformed.yaml @@ -0,0 +1,28 @@ +apiVersion: canaries.flanksource.com/v1 +kind: Canary +metadata: + name: exchange-rates + annotations: + trace: "true" +spec: + schedule: "every 30 @minutes" + http: + - name: exchange-rates + url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS + transform: + expr: | + { + 'metrics': json.rates.keys().map(k, { + 'name': "exchange_rate", + 'type': "gauge", + 'value': json.rates[k], + 'labels': { + "from": json.base, + "to": k + } + }) + }.toJSON() + metrics: + - name: exchange_rate_api + type: histogram + value: elapsed.getMilliseconds() diff --git a/go.mod b/go.mod index dceccaf3e..ea3d2b025 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/fergusstrange/embedded-postgres v1.24.0 github.com/flanksource/commons v1.12.0 github.com/flanksource/duty v1.0.191 - github.com/flanksource/gomplate/v3 v3.20.14 + github.com/flanksource/gomplate/v3 v3.20.18 github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 github.com/flanksource/kommons v0.31.4 github.com/friendsofgo/errors v0.9.2 diff --git a/go.sum b/go.sum index 91ef3979c..d4ff277d9 100644 --- a/go.sum +++ b/go.sum @@ -824,8 +824,8 @@ github.com/flanksource/commons v1.12.0/go.mod h1:zYEhi6E2+diQ+loVcROUHo/Bgv+Tn61 github.com/flanksource/duty v1.0.191 h1:acnvyTeQlfqmtyXxWprNFGK/vBTUlqkYwxEPLtXSPrk= github.com/flanksource/duty v1.0.191/go.mod h1:ikyl/TcRy6Cc0R5b0wEHT7CecV7gyJvrDGq/4oIZHoc= github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9oZXuxfNFGd/iGSdc= -github.com/flanksource/gomplate/v3 v3.20.14 h1:mtxwRbBaAwGr35T2OxCaXFEqovfyMGZ6vFjDJFk625I= -github.com/flanksource/gomplate/v3 v3.20.14/go.mod h1:1N1aptaAo0XUaGsyU5CWiwn9GMRpbIKX1AdsypfmZYo= +github.com/flanksource/gomplate/v3 v3.20.18 h1:qYiznMxhq+Zau5iWnVzW1yDzA1deHOsmo6yldCN7JhQ= +github.com/flanksource/gomplate/v3 v3.20.18/go.mod h1:2GgHZ2vWmtDspJMBfUIryOuzJSwc8jU7Kw9fDLr0TMA= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc/go.mod h1:4pQhmF+TnVqJroQKY8wSnSp+T18oLson6YQ2M0qPHfQ= github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 h1:s6jf6P1pRfdvksVFjIXFRfnimvEYUR0/Mmla1EIjiRM= github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7/go.mod h1:BH5gh9JyEAuuWVP6Q5y9h43VozS0RfKyjNpM9L4v4hw= diff --git a/pkg/api.go b/pkg/api.go index 29bb3c81f..766ea3dac 100644 --- a/pkg/api.go +++ b/pkg/api.go @@ -461,12 +461,12 @@ func (generic GenericCheck) GetEndpoint() string { } type TransformedCheckResult struct { - Start time.Time `json:"start,omitempty"` - Pass bool `json:"pass,omitempty"` - Invalid bool `json:"invalid,omitempty"` + Start *time.Time `json:"start,omitempty"` + Pass *bool `json:"pass,omitempty"` + Invalid *bool `json:"invalid,omitempty"` Detail interface{} `json:"detail,omitempty"` Data map[string]interface{} `json:"data,omitempty"` - Duration int64 `json:"duration,omitempty"` + Duration *int64 `json:"duration,omitempty"` Description string `json:"description,omitempty"` DisplayType string `json:"displayType,omitempty"` Message string `json:"message,omitempty"` @@ -474,6 +474,7 @@ type TransformedCheckResult struct { Name string `json:"name,omitempty"` Labels map[string]string `json:"labels,omitempty"` Namespace string `json:"namespace,omitempty"` + Metrics []Metric `json:"metrics,omitempty"` Icon string `json:"icon,omitempty"` Type string `json:"type,omitempty"` Endpoint string `json:"endpoint,omitempty"` @@ -486,16 +487,17 @@ func (t TransformedCheckResult) ToCheckResult() CheckResult { labels = make(map[string]string) } return CheckResult{ - Start: t.Start, - Pass: t.Pass, - Invalid: t.Invalid, + Start: utils.Deref(t.Start, time.Now()), + Pass: utils.Deref(t.Pass, false), + Invalid: utils.Deref(t.Invalid, false), Detail: t.Detail, Data: t.Data, - Duration: t.Duration, + Duration: utils.Deref(t.Duration, 0), Description: t.Description, DisplayType: t.DisplayType, Message: t.Message, Error: t.Error, + Metrics: t.Metrics, Check: GenericCheck{ Description: v1.Description{ Description: t.Description, @@ -517,10 +519,10 @@ func (t TransformedCheckResult) GetDescription() string { type MetricType string type Metric struct { - Name string - Type MetricType - Labels map[string]string - Value float64 + Name string `json:"name,omitempty"` + Type MetricType `json:"type,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Value float64 `json:"value,omitempty"` } func (m Metric) String() string { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 95269a23f..b60bea400 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -71,8 +71,11 @@ func Ptr[T any](t T) *T { return &t } -func Deref[T any](v *T) T { +func Deref[T any](v *T, zeroVal ...T) T { if v == nil { + if len(zeroVal) > 0 { + return zeroVal[0] + } var zero T return zero }