Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/monitor #1792

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 88 additions & 0 deletions api/v1/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2021 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package v1

import (
"bytes"
"time"

"github.com/loadimpact/k6/stats"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
)

func newMetricFamily(m *stats.Metric, t time.Duration) []dto.MetricFamily {
metrics := make([]dto.MetricFamily, 0)

switch m.Type {
case stats.Counter:
counter := m.Sink.(*stats.CounterSink)
metrics = append(metrics, newCounter(m.Name+"_count", counter.Value, m.Name+" cumulative value"))
metrics = append(metrics, newGauge(m.Name+"_rate", counter.Value/(float64(t)/float64(time.Second)),
m.Name+" value per seconds"))
case stats.Gauge:
gauge := m.Sink.(*stats.GaugeSink)
metrics = append(metrics, newGauge(m.Name+"_value", gauge.Value, m.Name+" latest value"))
case stats.Trend:
trend := m.Sink.(*stats.TrendSink)
trend.Calc()
metrics = append(metrics, newGauge(m.Name+"_min", trend.Min, m.Name+" minimum value"))
metrics = append(metrics, newGauge(m.Name+"_max", trend.Max, m.Name+" maximum value"))
metrics = append(metrics, newGauge(m.Name+"_avg", trend.Avg, m.Name+" average value"))
metrics = append(metrics, newGauge(m.Name+"_med", trend.Med, m.Name+" median value"))
metrics = append(metrics, newGauge(m.Name+"_p90", trend.P(0.90), m.Name+" 90 percentile value"))
metrics = append(metrics, newGauge(m.Name+"_p95", trend.P(0.95), m.Name+" 95 percentile value"))
case stats.Rate:
rate := m.Sink.(*stats.RateSink)
metrics = append(metrics, newGauge(m.Name+"_rate", float64(rate.Trues)/float64(rate.Total),
m.Name+" percentage of non-zero values"))
}
Comment on lines +36 to +57
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should just make everything into a GAUGE ;).

I am not familiar with the prometheus API and conventions, but if this is the way things need to be done ... I don't think it will be all that useful ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably counter fits better

return metrics
}

func newGauge(name string, value float64, help string) dto.MetricFamily {
return dto.MetricFamily{
Name: &name,
Help: &help,
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{{Gauge: &dto.Gauge{Value: &value}}},
}
}

func newCounter(name string, value float64, help string) dto.MetricFamily {
return dto.MetricFamily{
Name: &name,
Help: &help,
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{{Counter: &dto.Counter{Value: &value}}},
}
}

func marshallMetricFamily(metrics []dto.MetricFamily) ([]byte, error) {
var b bytes.Buffer
for i := range metrics {
_, err := expfmt.MetricFamilyToText(&b, &metrics[i])
if err != nil {
return nil, err
}
}
return b.Bytes(), nil
}
53 changes: 53 additions & 0 deletions api/v1/monitor_routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2021 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package v1

import (
"net/http"
"strconv"
"time"

"github.com/julienschmidt/httprouter"
"github.com/loadimpact/k6/api/common"
dto "github.com/prometheus/client_model/go"
)

func handleGetMonitor(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
engine := common.GetEngine(r.Context())

var t time.Duration
if engine.ExecutionScheduler != nil {
t = engine.ExecutionScheduler.GetState().GetCurrentTestRunDuration()
}

metrics := make([]dto.MetricFamily, 0)
for _, m := range engine.Metrics {
metrics = append(metrics, newMetricFamily(m, t)...)
}

data, err := marshallMetricFamily(metrics)
Comment on lines +42 to +46
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definetely racy ... there is a reason the Engine has a MetricsLock ;)

You can run go run -race . run script.js and then call the endpoint to get the actual race stacks ... and I would guess there will be others :(.

p.s. the -race flag needs a lot more time to start and it is less performant, but it particularly useful in such situations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I just made it similar to metrics endpoint and didn't spent too much time on it...

if err != nil {
apiError(rw, "Encoding error", err.Error(), http.StatusInternalServerError)
return
}
rw.Header().Add("Content-Length", strconv.Itoa(len(data)))
_, _ = rw.Write(data)
}
78 changes: 78 additions & 0 deletions api/v1/monitor_routes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2021 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package v1

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"

"github.com/loadimpact/k6/core"
"github.com/loadimpact/k6/core/local"
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/lib/testutils/minirunner"
"github.com/loadimpact/k6/stats"

"github.com/prometheus/common/expfmt"
)

func TestGetMonitor(t *testing.T) {
logger := logrus.New()
logger.SetOutput(testutils.NewTestOutput(t))
execScheduler, err := local.NewExecutionScheduler(&minirunner.MiniRunner{}, logger)
require.NoError(t, err)
engine, err := core.NewEngine(execScheduler, lib.Options{}, logger)
require.NoError(t, err)

engine.Metrics = map[string]*stats.Metric{
"my_trend": stats.New("my_trend", stats.Trend, stats.Time),
}
engine.Metrics["my_trend"].Tainted = null.BoolFrom(true)

rw := httptest.NewRecorder()
NewHandler().ServeHTTP(rw, newRequestWithEngine(engine, "GET", "/v1/monitor", nil))
res := rw.Result()

assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-Type"))
assert.NotEmpty(t, res.Header.Get("Content-Length"))

t.Run("metrics", func(t *testing.T) {
parser := expfmt.TextParser{}
metrics, err := parser.TextToMetricFamilies(rw.Body)
assert.NoError(t, err)
assert.NotNil(t, metrics)
assert.Len(t, metrics, 6)
suffixes := []string{"_min", "_max", "_avg", "_med", "_p90", "_p95"}
for _, suffix := range suffixes {
name := "my_trend" + suffix
assert.Equal(t, name, metrics[name].GetName())
}
})

assert.NoError(t, res.Body.Close())
}
95 changes: 95 additions & 0 deletions api/v1/monitor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2021 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package v1

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/loadimpact/k6/stats"
)

func TestNewMetricFamilyTrend(t *testing.T) {
trend := stats.New("name", stats.Trend, stats.Time)
sink := trend.Sink.(*stats.TrendSink)
sink.Min = 1
sink.Max = 10
sink.Avg = 5
sink.Med = 4

m := newMetricFamily(trend, 0)
assert.Len(t, m, 6)

assert.Equal(t, "name_min", *m[0].Name)
assert.Equal(t, float64(1), *m[0].GetMetric()[0].Gauge.Value)

assert.Equal(t, "name_max", *m[1].Name)
assert.Equal(t, float64(10), *m[1].GetMetric()[0].Gauge.Value)

assert.Equal(t, "name_avg", *m[2].Name)
assert.Equal(t, float64(5), *m[2].GetMetric()[0].Gauge.Value)

assert.Equal(t, "name_med", *m[3].Name)
assert.Equal(t, float64(4), *m[3].GetMetric()[0].Gauge.Value)

assert.Equal(t, "name_p90", *m[4].Name)
assert.Equal(t, "name_p95", *m[5].Name)
}

func TestNewMetricFamilyCounter(t *testing.T) {
counter := stats.New("name", stats.Counter, stats.Time)
sink := counter.Sink.(*stats.CounterSink)
sink.Value = 42

m := newMetricFamily(counter, 0)
assert.Len(t, m, 2)

assert.Equal(t, "name_count", *m[0].Name)
assert.Equal(t, float64(42), *m[0].GetMetric()[0].Counter.Value)

assert.Equal(t, "name_rate", *m[1].Name)
}

func TestNewMetricFamilyGauge(t *testing.T) {
gauge := stats.New("name", stats.Gauge, stats.Time)
sink := gauge.Sink.(*stats.GaugeSink)
sink.Value = 42

m := newMetricFamily(gauge, 0)
assert.Len(t, m, 1)

assert.Equal(t, "name_value", *m[0].Name)
assert.Equal(t, float64(42), *m[0].GetMetric()[0].Gauge.Value)
}

func TestNewMetricFamilyRate(t *testing.T) {
rate := stats.New("name", stats.Rate, stats.Time)
sink := rate.Sink.(*stats.RateSink)
sink.Total = 42
sink.Trues = 42 * 42

m := newMetricFamily(rate, 0)
assert.Len(t, m, 1)

assert.Equal(t, "name_rate", *m[0].Name)
assert.Equal(t, float64(42), *m[0].GetMetric()[0].Gauge.Value)
}
2 changes: 2 additions & 0 deletions api/v1/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@ func NewHandler() http.Handler {

router.POST("/v1/teardown", HandleRunTeardown)

router.GET("/v1/monitor", handleGetMonitor)

return router
}
24 changes: 10 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/DataDog/datadog-go v0.0.0-20180330214955-e67964b4021a
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da
github.com/PuerkitoBio/goquery v1.3.0
github.com/Shopify/sarama v1.16.0
github.com/Shopify/sarama v1.19.0
github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect
github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c
Expand All @@ -17,24 +17,20 @@ require (
github.com/dop251/goja v0.0.0-20201107160812-7545ac6de48a
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4
github.com/eapache/go-resiliency v1.1.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20160609142408-bb955e01b934 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/fatih/color v1.5.0
github.com/fatih/color v1.7.0
github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-gonic/gin v1.1.5-0.20170702092826-d459835d2b07 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/protobuf v1.4.2
github.com/golang/snappy v0.0.0-20170215233205-553a64147049 // indirect
github.com/google/go-cmp v0.5.1 // indirect
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
github.com/gorilla/mux v1.6.1 // indirect
github.com/gorilla/websocket v1.4.2
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/influxdata/influxdb1-client v0.0.0-20190402204710-8ff2fc3824fc
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
github.com/jhump/protoreflect v1.7.0
github.com/julienschmidt/httprouter v1.1.1-0.20180222160526-d18983907793
github.com/julienschmidt/httprouter v1.3.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kelseyhightower/envconfig v1.4.0
github.com/klauspost/compress v1.7.2
Expand All @@ -47,21 +43,21 @@ require (
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.4
github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238
github.com/mitchellh/mapstructure v1.1.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/onsi/ginkgo v1.14.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2
github.com/pierrec/lz4 v1.0.2-0.20171218195038-2fcda4cb7018 // indirect
github.com/pierrec/xxHash v0.1.1 // indirect
github.com/pkg/errors v0.8.0
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.0
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 // indirect
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.15.0
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516
github.com/sirupsen/logrus v1.6.0
github.com/spf13/afero v1.1.1
github.com/spf13/cobra v0.0.4-0.20180629152535-a114f312e075
github.com/spf13/pflag v1.0.1
github.com/stretchr/testify v1.2.2
github.com/stretchr/testify v1.4.0
github.com/tidwall/gjson v1.6.1
github.com/tidwall/pretty v1.0.2
github.com/ugorji/go v1.1.7 // indirect
Expand All @@ -73,7 +69,7 @@ require (
golang.org/x/net v0.0.0-20201008223702-a5fa9d4b7c91
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
golang.org/x/text v0.3.3
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20200903010400-9bfcb5116336 // indirect
google.golang.org/grpc v1.31.1
Expand Down