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

agent/metrics: allow preinitializing supported metrics to zero #312

Merged
merged 1 commit into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkg/agent/protocol/grpc/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ func NewProxy(c ProxyConfig, d Disruption) (protocol.Proxy, error) {
return &proxy{
disruption: d,
config: c,
metrics: &protocol.MetricMap{},
metrics: protocol.NewMetricMap(
protocol.MetricRequests,
protocol.MetricRequestsExcluded,
protocol.MetricRequestsDisrupted,
),
}, nil
}

Expand Down
48 changes: 35 additions & 13 deletions pkg/agent/protocol/grpc/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,28 @@ func Test_ProxyMetrics(t *testing.T) {
type TestCase struct {
title string
disruption Disruption
skipRequest bool
expectedMetrics map[string]uint
}

// TODO: Add test for excluded endpoints
testCases := []TestCase{
{
title: "no requests",
disruption: Disruption{
AverageDelay: 0,
DelayVariation: 0,
ErrorRate: 0.0,
StatusCode: 0,
StatusMessage: "",
},
skipRequest: true,
expectedMetrics: map[string]uint{
protocol.MetricRequests: 0,
protocol.MetricRequestsDisrupted: 0,
protocol.MetricRequestsExcluded: 0,
},
},
{
title: "passthrough",
disruption: Disruption{
Expand All @@ -356,7 +373,9 @@ func Test_ProxyMetrics(t *testing.T) {
StatusMessage: "",
},
expectedMetrics: map[string]uint{
protocol.MetricRequests: 1,
protocol.MetricRequests: 1,
protocol.MetricRequestsDisrupted: 0,
protocol.MetricRequestsExcluded: 0,
},
},
{
Expand All @@ -371,6 +390,7 @@ func Test_ProxyMetrics(t *testing.T) {
expectedMetrics: map[string]uint{
protocol.MetricRequests: 1,
protocol.MetricRequestsDisrupted: 1,
protocol.MetricRequestsExcluded: 0,
},
},
}
Expand Down Expand Up @@ -435,18 +455,20 @@ func Test_ProxyMetrics(t *testing.T) {
_ = conn.Close()
}()

client := ping.NewPingServiceClient(conn)

var headers metadata.MD
_, _ = client.Ping(
context.TODO(),
&ping.PingRequest{
Error: 0,
Message: "ping",
},
grpc.Header(&headers),
grpc.WaitForReady(true),
)
if !tc.skipRequest {
client := ping.NewPingServiceClient(conn)

var headers metadata.MD
_, _ = client.Ping(
context.TODO(),
&ping.PingRequest{
Error: 0,
Message: "ping",
},
grpc.Header(&headers),
grpc.WaitForReady(true),
)
}

metrics := proxy.Metrics()

Expand Down
16 changes: 14 additions & 2 deletions pkg/agent/protocol/http/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type proxy struct {
config ProxyConfig
disruption Disruption
srv *http.Server
metrics protocol.MetricMap
metrics *protocol.MetricMap
}

// NewProxy return a new Proxy for HTTP requests
Expand Down Expand Up @@ -72,6 +72,7 @@ func NewProxy(c ProxyConfig, d Disruption) (protocol.Proxy, error) {
return &proxy{
disruption: d,
config: c,
metrics: protocol.NewMetricMap(supportedMetrics()...),
}, nil
}

Expand Down Expand Up @@ -176,7 +177,7 @@ func (p *proxy) Start() error {
handler := &httpHandler{
upstreamURL: *upstreamURL,
disruption: p.disruption,
metrics: &p.metrics,
metrics: p.metrics,
}

p.srv = &http.Server{
Expand Down Expand Up @@ -211,3 +212,14 @@ func (p *proxy) Force() error {
}
return nil
}

// supportedMetrics is a helper function that returns the metrics that the http proxy supports and thus should be
// pre-initialized to zero. This function is defined due to the testing limitations mentioned in
// https://github.com/grafana/xk6-disruptor/issues/314, as httpHandler tests currently need this information.
func supportedMetrics() []string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I do not agree with this comment. Moreover, I think this method should be public as part of the contract of the Proxy.

Copy link
Collaborator Author

@roobre roobre Aug 23, 2023

Choose a reason for hiding this comment

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

I like more declaring which metrics are supported by making them available in the map returned by Metrics, rather than exposing that list directly.

In my view, consumers of the Proxy interface should not be interested on the "list of metrics this proxy supports", which would be this method, but rather on "is this metric supported?". I believe this question is answered better by the Metrics method containing or not a particular metric.

As an example, the way a user would ask the proxy if it has a metric would be this:

requests, hasMetric := d.proxy.Metrics()[MetricRequests]
if hasMetric && requests == 0 {
    return fmt.Errorf("disruptor did not receive any request")
}

If the supported list of metrics was part of the contract, I think the code would look more verbose:

supportedMetrics := d.proxy.SupportedMetrics()
if slices.contains(supportedMetrics, MetricRequests) && d.proxy.Metrics()[MetricRequests] == 0 {
    return fmt.Errorf("disruptor did not receive any request")
}

I think the first way leverages the built-in existence check on maps to convey this meaning, while the latter reimplements is as a is-contained-in-separate slice which I like a bit less.

Copy link
Collaborator

@pablochacin pablochacin Aug 23, 2023

Choose a reason for hiding this comment

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

I don't see why having the list of metrics available should change the code for checking the metric, therefore I don't think the "verbosity" argument is valid.

In any case, my point was that having the proxy to know the supported list of metrics is not in my opinion a hack around the limitations of testing but something we expect it to know. The hack, in any case, is testing this at the handler level and not the proxy level. Maybe this should be stated more clearly in the comment.

Regarding exposing it or not in the API, it is secondary. But I think it is valid because it is not immediately evident that the proxy.Metrics() method will return all supported metrics regardless of whether they have a non-zero value or not. We should make this more explicit in the documentation:

// Metrics returns a map of counter-type metrics. Implementations may return zero or more of the metrics defined
// below, as well as any number of implementation-defined metrics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These are good points, I've clarified both comments to state more clearly how supported metrics are exposed and the purpose of supportedMetrics. LMK if these look better to you 🙂

return []string{
protocol.MetricRequests,
protocol.MetricRequestsExcluded,
protocol.MetricRequestsDisrupted,
}
}
14 changes: 9 additions & 5 deletions pkg/agent/protocol/http/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func Test_ProxyHandler(t *testing.T) {
handler := &httpHandler{
upstreamURL: *upstreamURL,
disruption: tc.disruption,
metrics: &protocol.MetricMap{},
metrics: protocol.NewMetricMap(supportedMetrics()...),
}

proxyServer := httptest.NewServer(handler)
Expand Down Expand Up @@ -388,8 +388,12 @@ func Test_Metrics(t *testing.T) {
expectedMetrics map[string]uint
}{
{
name: "no requests",
expectedMetrics: map[string]uint{},
name: "no requests",
expectedMetrics: map[string]uint{
protocol.MetricRequests: 0,
protocol.MetricRequestsExcluded: 0,
protocol.MetricRequestsDisrupted: 0,
},
},
{
name: "requests",
Expand Down Expand Up @@ -419,12 +423,12 @@ func Test_Metrics(t *testing.T) {
t.Fatalf("error parsing httptest url")
}

metrics := protocol.MetricMap{}
metrics := protocol.NewMetricMap(supportedMetrics()...)

handler := &httpHandler{
upstreamURL: *upstreamURL,
disruption: tc.config,
metrics: &metrics,
metrics: metrics,
}

proxyServer := httptest.NewServer(handler)
Expand Down
20 changes: 15 additions & 5 deletions pkg/agent/protocol/metricmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ type MetricMap struct {
mutex sync.RWMutex
}

// Inc increases the value of the specified counter by one.
// NewMetricMap returns a MetricMap with the specified metrics initialized to zero.
func NewMetricMap(metrics ...string) *MetricMap {
mm := &MetricMap{
metrics: map[string]uint{},
}

for _, metric := range metrics {
mm.metrics[metric] = 0
}

return mm
}

// Inc increases the value of the specified counter by one. If the metric hasn't been initialized or incremented before,
// it is set to 1.
func (m *MetricMap) Inc(name string) {
m.mutex.Lock()
defer m.mutex.Unlock()

if m.metrics == nil {
m.metrics = make(map[string]uint)
}

m.metrics[name]++
}

Expand Down
28 changes: 22 additions & 6 deletions pkg/agent/protocol/metricmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,34 @@ import (
func TestMetricMap(t *testing.T) {
t.Parallel()

t.Run("initializes metrics", func(t *testing.T) {
t.Parallel()

const foo = "foo_metric"

mm := protocol.NewMetricMap(foo)
fooMetric, hasFoo := mm.Map()[foo]
if !hasFoo {
t.Fatalf("foo should exist in the output map")
}

if fooMetric != 0 {
t.Fatalf("foo should be zero")
}
})

t.Run("increases counters", func(t *testing.T) {
t.Parallel()

const name = "foo_metric"
const foo = "foo_metric"

mm := protocol.MetricMap{}
if current := mm.Map(); current[name] != 0 {
t.Fatalf("map should start containing zero")
mm := protocol.NewMetricMap()
if current := mm.Map(); current[foo] != 0 {
t.Fatalf("uninitialized foo should be zero")
}

mm.Inc(name)
if updated := mm.Map(); updated[name] != 1 {
mm.Inc(foo)
if updated := mm.Map(); updated[foo] != 1 {
t.Fatalf("metric was not incremented")
}
})
Expand Down
3 changes: 2 additions & 1 deletion pkg/agent/protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ type Proxy interface {
Start() error
Stop() error
// Metrics returns a map of counter-type metrics. Implementations may return zero or more of the metrics defined
// below, as well as any number of implementation-defined metrics.
// below, as well as any number of implementation-defined metrics. Callers can check if a metric exists in the map
// returned by Metrics to distinguish a counter with a value of zero from an unsupported metric.
Metrics() map[string]uint
Force() error
}
Expand Down
Loading