Add support for retrieving Droplet monitoring metrics. (#491)

* Add metrics package with minimal copy of github.com/prometheus/common/model

Package metrics is a minimal copy of github.com/prometheus/common/model
providing types to work with the Prometheus-style results in a DigitalOcean
Monitoring metrics response.

We have copied this here as Prometheus' common packages are considered
internal to Prometheus, without any stability guarantees for external usage.

* Add support for retrieving Droplet monitoring metrics.

* Use pointer receivers.
This commit is contained in:
Andrew Starr-Bochicchio 2021-11-01 15:31:17 -04:00 committed by GitHub
parent 35a70e8f3a
commit 33658a69d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1718 additions and 2 deletions

81
metrics/metrics.go Normal file
View File

@ -0,0 +1,81 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package metrics is a minimal copy of github.com/prometheus/common/model
// providing types to work with the Prometheus-style results in a DigitalOcean
// Monitoring metrics response.
package metrics
import (
"fmt"
"sort"
"strings"
)
const (
// MetricNameLabel is the label name indicating the metric name of a
// timeseries.
MetricNameLabel = "__name__"
)
// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet
// may be fully-qualified down to the point where it may resolve to a single
// Metric in the data store or not. All operations that occur within the realm
// of a LabelSet can emit a vector of Metric entities to which the LabelSet may
// match.
type LabelSet map[LabelName]LabelValue
func (l LabelSet) String() string {
lstrs := make([]string, 0, len(l))
for l, v := range l {
lstrs = append(lstrs, fmt.Sprintf("%s=%q", l, v))
}
sort.Strings(lstrs)
return fmt.Sprintf("{%s}", strings.Join(lstrs, ", "))
}
// A LabelValue is an associated value for a MetricLabelName.
type LabelValue string
// A LabelName is a key for a Metric.
type LabelName string
// A Metric is similar to a LabelSet, but the key difference is that a Metric is
// a singleton and refers to one and only one stream of samples.
type Metric LabelSet
func (m Metric) String() string {
metricName, hasName := m[MetricNameLabel]
numLabels := len(m) - 1
if !hasName {
numLabels = len(m)
}
labelStrings := make([]string, 0, numLabels)
for label, value := range m {
if label != MetricNameLabel {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
}
}
switch numLabels {
case 0:
if hasName {
return string(metricName)
}
return "{}"
default:
sort.Strings(labelStrings)
return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", "))
}
}

66
metrics/metrics_test.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metrics
import (
"testing"
)
func TestMetricToString(t *testing.T) {
scenarios := []struct {
name string
input Metric
expected string
}{
{
name: "valid metric without __name__ label",
input: Metric{
"first_name": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
expected: `{first_name="electro", manufacturer="westinghouse", occupation="robot"}`,
},
{
name: "valid metric with __name__ label",
input: Metric{
"__name__": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
expected: `electro{manufacturer="westinghouse", occupation="robot"}`,
},
{
name: "empty metric with __name__ label",
input: Metric{
"__name__": "fooname",
},
expected: "fooname",
},
{
name: "empty metric",
input: Metric{},
expected: "{}",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
actual := scenario.input.String()
if actual != scenario.expected {
t.Errorf("expected string output %s but got %s", actual, scenario.expected)
}
})
}
}

164
metrics/time.go Normal file
View File

@ -0,0 +1,164 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metrics
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
const (
// MinimumTick is the minimum supported time resolution. This has to be
// at least time.Second in order for the code below to work.
minimumTick = time.Millisecond
// second is the Time duration equivalent to one second.
second = int64(time.Second / minimumTick)
// The number of nanoseconds per minimum tick.
nanosPerTick = int64(minimumTick / time.Nanosecond)
// Earliest is the earliest Time representable. Handy for
// initializing a high watermark.
Earliest = Time(math.MinInt64)
// Latest is the latest Time representable. Handy for initializing
// a low watermark.
Latest = Time(math.MaxInt64)
)
// Time is the number of milliseconds since the epoch
// (1970-01-01 00:00 UTC) excluding leap seconds.
type Time int64
// Interval describes an interval between two timestamps.
type Interval struct {
Start, End Time
}
// Now returns the current time as a Time.
func Now() Time {
return TimeFromUnixNano(time.Now().UnixNano())
}
// TimeFromUnix returns the Time equivalent to the Unix Time t
// provided in seconds.
func TimeFromUnix(t int64) Time {
return Time(t * second)
}
// TimeFromUnixNano returns the Time equivalent to the Unix Time
// t provided in nanoseconds.
func TimeFromUnixNano(t int64) Time {
return Time(t / nanosPerTick)
}
// Equal reports whether two Times represent the same instant.
func (t Time) Equal(o Time) bool {
return t == o
}
// Before reports whether the Time t is before o.
func (t Time) Before(o Time) bool {
return t < o
}
// After reports whether the Time t is after o.
func (t Time) After(o Time) bool {
return t > o
}
// Add returns the Time t + d.
func (t Time) Add(d time.Duration) Time {
return t + Time(d/minimumTick)
}
// Sub returns the Duration t - o.
func (t Time) Sub(o Time) time.Duration {
return time.Duration(t-o) * minimumTick
}
// Time returns the time.Time representation of t.
func (t Time) Time() time.Time {
return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick)
}
// Unix returns t as a Unix time, the number of seconds elapsed
// since January 1, 1970 UTC.
func (t Time) Unix() int64 {
return int64(t) / second
}
// UnixNano returns t as a Unix time, the number of nanoseconds elapsed
// since January 1, 1970 UTC.
func (t Time) UnixNano() int64 {
return int64(t) * nanosPerTick
}
// The number of digits after the dot.
var dotPrecision = int(math.Log10(float64(second)))
// String returns a string representation of the Time.
func (t Time) String() string {
return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64)
}
// MarshalJSON implements the json.Marshaler interface.
func (t Time) MarshalJSON() ([]byte, error) {
return []byte(t.String()), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (t *Time) UnmarshalJSON(b []byte) error {
p := strings.Split(string(b), ".")
switch len(p) {
case 1:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
if err != nil {
return err
}
*t = Time(v * second)
case 2:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
if err != nil {
return err
}
v *= second
prec := dotPrecision - len(p[1])
if prec < 0 {
p[1] = p[1][:dotPrecision]
} else if prec > 0 {
p[1] = p[1] + strings.Repeat("0", prec)
}
va, err := strconv.ParseInt(p[1], 10, 32)
if err != nil {
return err
}
// If the value was something like -0.1 the negative is lost in the
// parsing because of the leading zero, this ensures that we capture it.
if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 {
*t = Time(v+va) * -1
} else {
*t = Time(v + va)
}
default:
return fmt.Errorf("invalid time %q", string(b))
}
return nil
}

121
metrics/time_test.go Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metrics
import (
"strconv"
"testing"
"time"
)
func TestComparators(t *testing.T) {
t1a := TimeFromUnix(0)
t1b := TimeFromUnix(0)
t2 := TimeFromUnix(2*second - 1)
if !t1a.Equal(t1b) {
t.Fatalf("Expected %s to be equal to %s", t1a, t1b)
}
if t1a.Equal(t2) {
t.Fatalf("Expected %s to not be equal to %s", t1a, t2)
}
if !t1a.Before(t2) {
t.Fatalf("Expected %s to be before %s", t1a, t2)
}
if t1a.Before(t1b) {
t.Fatalf("Expected %s to not be before %s", t1a, t1b)
}
if !t2.After(t1a) {
t.Fatalf("Expected %s to be after %s", t2, t1a)
}
if t1b.After(t1a) {
t.Fatalf("Expected %s to not be after %s", t1b, t1a)
}
}
func TestTimeConversions(t *testing.T) {
unixSecs := int64(1136239445)
unixNsecs := int64(123456789)
unixNano := unixSecs*1e9 + unixNsecs
t1 := time.Unix(unixSecs, unixNsecs-unixNsecs%nanosPerTick)
t2 := time.Unix(unixSecs, unixNsecs)
ts := TimeFromUnixNano(unixNano)
if !ts.Time().Equal(t1) {
t.Fatalf("Expected %s, got %s", t1, ts.Time())
}
// Test available precision.
ts = TimeFromUnixNano(t2.UnixNano())
if !ts.Time().Equal(t1) {
t.Fatalf("Expected %s, got %s", t1, ts.Time())
}
if ts.UnixNano() != unixNano-unixNano%nanosPerTick {
t.Fatalf("Expected %d, got %d", unixNano, ts.UnixNano())
}
}
func TestDuration(t *testing.T) {
duration := time.Second + time.Minute + time.Hour
goTime := time.Unix(1136239445, 0)
ts := TimeFromUnix(goTime.Unix())
if !goTime.Add(duration).Equal(ts.Add(duration).Time()) {
t.Fatalf("Expected %s to be equal to %s", goTime.Add(duration), ts.Add(duration))
}
earlier := ts.Add(-duration)
delta := ts.Sub(earlier)
if delta != duration {
t.Fatalf("Expected %s to be equal to %s", delta, duration)
}
}
func TestTimeJSON(t *testing.T) {
tests := []struct {
in Time
out string
}{
{Time(1), `0.001`},
{Time(-1), `-0.001`},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
b, err := test.in.MarshalJSON()
if err != nil {
t.Fatalf("Error marshaling time: %v", err)
}
if string(b) != test.out {
t.Errorf("Mismatch in marshal expected=%s actual=%s", test.out, b)
}
var tm Time
if err := tm.UnmarshalJSON(b); err != nil {
t.Fatalf("Error Unmarshaling time: %v", err)
}
if !test.in.Equal(tm) {
t.Fatalf("Mismatch after Unmarshal expected=%v actual=%v", test.in, tm)
}
})
}
}

100
metrics/values.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metrics
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
)
// A SampleValue is a representation of a value for a given sample at a given time.
type SampleValue float64
// UnmarshalJSON implements json.Unmarshaler.
func (v *SampleValue) UnmarshalJSON(b []byte) error {
if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
return fmt.Errorf("sample value must be a quoted string")
}
f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
if err != nil {
return err
}
*v = SampleValue(f)
return nil
}
// MarshalJSON implements json.Marshaler.
func (v SampleValue) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
func (v SampleValue) String() string {
return strconv.FormatFloat(float64(v), 'f', -1, 64)
}
// Equal returns true if the value of v and o is equal or if both are NaN. Note
// that v==o is false if both are NaN. If you want the conventional float
// behavior, use == to compare two SampleValues.
func (v SampleValue) Equal(o SampleValue) bool {
if v == o {
return true
}
return math.IsNaN(float64(v)) && math.IsNaN(float64(o))
}
// SamplePair pairs a SampleValue with a Timestamp.
type SamplePair struct {
Timestamp Time
Value SampleValue
}
func (s SamplePair) String() string {
return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp)
}
// UnmarshalJSON implements json.Unmarshaler.
func (s *SamplePair) UnmarshalJSON(b []byte) error {
v := [...]json.Unmarshaler{&s.Timestamp, &s.Value}
return json.Unmarshal(b, &v)
}
// MarshalJSON implements json.Marshaler.
func (s SamplePair) MarshalJSON() ([]byte, error) {
t, err := json.Marshal(s.Timestamp)
if err != nil {
return nil, err
}
v, err := json.Marshal(s.Value)
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
}
// SampleStream is a stream of Values belonging to an attached COWMetric.
type SampleStream struct {
Metric Metric `json:"metric"`
Values []SamplePair `json:"values"`
}
func (ss SampleStream) String() string {
vals := make([]string, len(ss.Values))
for i, v := range ss.Values {
vals[i] = v.String()
}
return fmt.Sprintf("%s =>\n%s", ss.Metric, strings.Join(vals, "\n"))
}

114
metrics/values_test.go Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metrics
import (
"encoding/json"
"math"
"testing"
)
func TestEqualValues(t *testing.T) {
tests := map[string]struct {
in1, in2 SampleValue
want bool
}{
"equal floats": {
in1: 3.14,
in2: 3.14,
want: true,
},
"unequal floats": {
in1: 3.14,
in2: 3.1415,
want: false,
},
"positive inifinities": {
in1: SampleValue(math.Inf(+1)),
in2: SampleValue(math.Inf(+1)),
want: true,
},
"negative inifinities": {
in1: SampleValue(math.Inf(-1)),
in2: SampleValue(math.Inf(-1)),
want: true,
},
"different inifinities": {
in1: SampleValue(math.Inf(+1)),
in2: SampleValue(math.Inf(-1)),
want: false,
},
"number and infinity": {
in1: 42,
in2: SampleValue(math.Inf(+1)),
want: false,
},
"number and NaN": {
in1: 42,
in2: SampleValue(math.NaN()),
want: false,
},
"NaNs": {
in1: SampleValue(math.NaN()),
in2: SampleValue(math.NaN()),
want: true, // !!!
},
}
for name, test := range tests {
got := test.in1.Equal(test.in2)
if got != test.want {
t.Errorf("Comparing %s, %f and %f: got %t, want %t", name, test.in1, test.in2, got, test.want)
}
}
}
func TestSamplePairJSON(t *testing.T) {
input := []struct {
plain string
value SamplePair
}{
{
plain: `[1234.567,"123.1"]`,
value: SamplePair{
Value: 123.1,
Timestamp: 1234567,
},
},
}
for _, test := range input {
b, err := json.Marshal(test.value)
if err != nil {
t.Error(err)
continue
}
if string(b) != test.plain {
t.Errorf("encoding error: expected %q, got %q", test.plain, b)
continue
}
var sp SamplePair
err = json.Unmarshal(b, &sp)
if err != nil {
t.Error(err)
continue
}
if sp != test.value {
t.Errorf("decoding error: expected %v, got %v", test.value, sp)
}
}
}

View File

@ -4,11 +4,15 @@ import (
"context"
"fmt"
"net/http"
"time"
"github.com/digitalocean/godo/metrics"
)
const (
monitoringBasePath = "v2/monitoring"
alertPolicyBasePath = monitoringBasePath + "/alerts"
monitoringBasePath = "v2/monitoring"
alertPolicyBasePath = monitoringBasePath + "/alerts"
dropletMetricsBasePath = monitoringBasePath + "/metrics/droplet"
DropletCPUUtilizationPercent = "v1/insights/droplet/cpu"
DropletMemoryUtilizationPercent = "v1/insights/droplet/memory_utilization_percent"
@ -33,6 +37,18 @@ type MonitoringService interface {
CreateAlertPolicy(context.Context, *AlertPolicyCreateRequest) (*AlertPolicy, *Response, error)
UpdateAlertPolicy(context.Context, string, *AlertPolicyUpdateRequest) (*AlertPolicy, *Response, error)
DeleteAlertPolicy(context.Context, string) (*Response, error)
GetDropletBandwidth(context.Context, *DropletBandwidthMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletAvailableMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletCPU(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletFilesystemFree(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletFilesystemSize(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletLoad1(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletLoad5(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletLoad15(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletCachedMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletFreeMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
GetDropletTotalMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error)
}
// MonitoringServiceOp handles communication with monitoring related methods of the
@ -115,6 +131,32 @@ type alertPolicyRoot struct {
AlertPolicy *AlertPolicy `json:"policy,omitempty"`
}
// DropletMetricsRequest holds the information needed to retrieve Droplet various metrics.
type DropletMetricsRequest struct {
HostID string
Start time.Time
End time.Time
}
// DropletBandwidthMetricsRequest holds the information needed to retrieve Droplet bandwidth metrics.
type DropletBandwidthMetricsRequest struct {
DropletMetricsRequest
Interface string
Direction string
}
// MetricsResponse holds a Metrics query response.
type MetricsResponse struct {
Status string `json:"status"`
Data MetricsData `json:"data"`
}
// MetricsData holds the data portion of a Metrics response.
type MetricsData struct {
ResultType string `json:"resultType"`
Result []metrics.SampleStream `json:"result"`
}
// ListAlertPolicies all alert policies
func (s *MonitoringServiceOp) ListAlertPolicies(ctx context.Context, opt *ListOptions) ([]AlertPolicy, *Response, error) {
path := alertPolicyBasePath
@ -221,3 +263,94 @@ func (s *MonitoringServiceOp) DeleteAlertPolicy(ctx context.Context, uuid string
return resp, err
}
// GetDropletBandwidth retrieves Droplet bandwidth metrics.
func (s *MonitoringServiceOp) GetDropletBandwidth(ctx context.Context, args *DropletBandwidthMetricsRequest) (*MetricsResponse, *Response, error) {
path := dropletMetricsBasePath + "/bandwidth"
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
q := req.URL.Query()
q.Add("host_id", args.HostID)
q.Add("interface", args.Interface)
q.Add("direction", args.Direction)
q.Add("start", fmt.Sprintf("%d", args.Start.Unix()))
q.Add("end", fmt.Sprintf("%d", args.End.Unix()))
req.URL.RawQuery = q.Encode()
root := new(MetricsResponse)
resp, err := s.client.Do(ctx, req, root)
return root, resp, err
}
// GetDropletCPU retrieves Droplet CPU metrics.
func (s *MonitoringServiceOp) GetDropletCPU(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/cpu", args)
}
// GetDropletFilesystemFree retrieves Droplet filesystem free metrics.
func (s *MonitoringServiceOp) GetDropletFilesystemFree(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/filesystem_free", args)
}
// GetDropletFilesystemSize retrieves Droplet filesystem size metrics.
func (s *MonitoringServiceOp) GetDropletFilesystemSize(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/filesystem_size", args)
}
// GetDropletLoad1 retrieves Droplet load 1 metrics.
func (s *MonitoringServiceOp) GetDropletLoad1(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/load_1", args)
}
// GetDropletLoad5 retrieves Droplet load 5 metrics.
func (s *MonitoringServiceOp) GetDropletLoad5(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/load_5", args)
}
// GetDropletLoad15 retrieves Droplet load 15 metrics.
func (s *MonitoringServiceOp) GetDropletLoad15(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/load_15", args)
}
// GetDropletCachedMemory retrieves Droplet cached memory metrics.
func (s *MonitoringServiceOp) GetDropletCachedMemory(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/memory_cached", args)
}
// GetDropletFreeMemory retrieves Droplet free memory metrics.
func (s *MonitoringServiceOp) GetDropletFreeMemory(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/memory_free", args)
}
// GetDropletTotalMemory retrieves Droplet total memory metrics.
func (s *MonitoringServiceOp) GetDropletTotalMemory(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/memory_total", args)
}
// GetDropletAvailableMemory retrieves Droplet available memory metrics.
func (s *MonitoringServiceOp) GetDropletAvailableMemory(ctx context.Context, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
return s.getDropletMetrics(ctx, "/memory_available", args)
}
func (s *MonitoringServiceOp) getDropletMetrics(ctx context.Context, path string, args *DropletMetricsRequest) (*MetricsResponse, *Response, error) {
fullPath := dropletMetricsBasePath + path
req, err := s.client.NewRequest(ctx, http.MethodGet, fullPath, nil)
if err != nil {
return nil, nil, err
}
q := req.URL.Query()
q.Add("host_id", args.HostID)
q.Add("start", fmt.Sprintf("%d", args.Start.Unix()))
q.Add("end", fmt.Sprintf("%d", args.End.Unix()))
req.URL.RawQuery = q.Encode()
root := new(MetricsResponse)
resp, err := s.client.Do(ctx, req, root)
return root, resp, err
}

View File

@ -6,6 +6,10 @@ import (
"net/http"
"reflect"
"testing"
"time"
"github.com/digitalocean/godo/metrics"
"github.com/stretchr/testify/assert"
)
var (
@ -170,6 +174,557 @@ var (
}
}
`
bandwidthRespJSON = `
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"direction": "inbound",
"host_id": "222651441",
"interface": "private"
},
"values": [
[
1634052360,
"0.016600450090265357"
],
[
1634052480,
"0.015085955677299055"
],
[
1634052600,
"0.014941163855322308"
],
[
1634052720,
"0.016214285714285712"
]
]
}
]
}
}`
memoryRespJSON = `
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"host_id": "123"
},
"values": [
[
1635386880,
"1028956160"
],
[
1635387000,
"1028956160"
],
[
1635387120,
"1028956160"
]
]
}
]
}
}`
filesystemRespJSON = `
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"device": "/dev/vda1",
"fstype": "ext4",
"host_id": "123",
"mountpoint": "/"
},
"values": [
[
1635386880,
"25832407040"
],
[
1635387000,
"25832407040"
],
[
1635387120,
"25832407040"
]
]
}
]
}
}`
loadRespJSON = `
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"host_id": "123"
},
"values": [
[
1635386880,
"0.04"
],
[
1635387000,
"0.03"
],
[
1635387120,
"0.01"
]
]
}
]
}
}`
cpuRespJSON = `
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"host_id": "123",
"mode": "idle"
},
"values": [
[
1635386880,
"122901.18"
],
[
1635387000,
"123020.92"
],
[
1635387120,
"123140.8"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "iowait"
},
"values": [
[
1635386880,
"14.99"
],
[
1635387000,
"15.01"
],
[
1635387120,
"15.01"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "irq"
},
"values": [
[
1635386880,
"0"
],
[
1635387000,
"0"
],
[
1635387120,
"0"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "nice"
},
"values": [
[
1635386880,
"66.35"
],
[
1635387000,
"66.35"
],
[
1635387120,
"66.35"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "softirq"
},
"values": [
[
1635386880,
"2.13"
],
[
1635387000,
"2.13"
],
[
1635387120,
"2.13"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "steal"
},
"values": [
[
1635386880,
"7.89"
],
[
1635387000,
"7.9"
],
[
1635387120,
"7.91"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "system"
},
"values": [
[
1635386880,
"140.09"
],
[
1635387000,
"140.2"
],
[
1635387120,
"140.23"
]
]
},
{
"metric": {
"host_id": "123",
"mode": "user"
},
"values": [
[
1635386880,
"278.57"
],
[
1635387000,
"278.65"
],
[
1635387120,
"278.69"
]
]
}
]
}
}`
testCPUResponse = &MetricsResponse{
Status: "success",
Data: MetricsData{
ResultType: "matrix",
Result: []metrics.SampleStream{
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "idle",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 122901.18,
},
{
Timestamp: 1635387000000,
Value: 123020.92,
},
{
Timestamp: 1635387120000,
Value: 123140.8,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "iowait",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 14.99,
},
{
Timestamp: 1635387000000,
Value: 15.01,
},
{
Timestamp: 1635387120000,
Value: 15.01,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "irq",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 0,
},
{
Timestamp: 1635387000000,
Value: 0,
},
{
Timestamp: 1635387120000,
Value: 0,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "nice",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 66.35,
},
{
Timestamp: 1635387000000,
Value: 66.35,
},
{
Timestamp: 1635387120000,
Value: 66.35,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "softirq",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 2.13,
},
{
Timestamp: 1635387000000,
Value: 2.13,
},
{
Timestamp: 1635387120000,
Value: 2.13,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "steal",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 7.89,
},
{
Timestamp: 1635387000000,
Value: 7.9,
},
{
Timestamp: 1635387120000,
Value: 7.91,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "system",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 140.09,
},
{
Timestamp: 1635387000000,
Value: 140.2,
},
{
Timestamp: 1635387120000,
Value: 140.23,
},
},
},
{
Metric: metrics.Metric{
"host_id": "123",
"mode": "user",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 278.57,
},
{
Timestamp: 1635387000000,
Value: 278.65,
},
{
Timestamp: 1635387120000,
Value: 278.69,
},
},
},
},
},
}
testLoadResponse = &MetricsResponse{
Status: "success",
Data: MetricsData{
ResultType: "matrix",
Result: []metrics.SampleStream{
{
Metric: metrics.Metric{
"host_id": "123",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 0.04,
},
{
Timestamp: 1635387000000,
Value: 0.03,
},
{
Timestamp: 1635387120000,
Value: 0.01,
},
},
},
},
},
}
testFilesystemResponse = &MetricsResponse{
Status: "success",
Data: MetricsData{
ResultType: "matrix",
Result: []metrics.SampleStream{
{
Metric: metrics.Metric{
"device": "/dev/vda1",
"fstype": "ext4",
"host_id": "123",
"mountpoint": "/",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 25832407040,
},
{
Timestamp: 1635387000000,
Value: 25832407040,
},
{
Timestamp: 1635387120000,
Value: 25832407040,
},
},
},
},
},
}
testMemoryResponse = &MetricsResponse{
Status: "success",
Data: MetricsData{
ResultType: "matrix",
Result: []metrics.SampleStream{
{
Metric: metrics.Metric{
"host_id": "123",
},
Values: []metrics.SamplePair{
{
Timestamp: 1635386880000,
Value: 1.02895616e+09,
},
{
Timestamp: 1635387000000,
Value: 1.02895616e+09,
},
{
Timestamp: 1635387120000,
Value: 1.02895616e+09,
},
},
},
},
},
}
)
func TestAlertPolicies_List(t *testing.T) {
@ -368,3 +923,385 @@ func TestAlertPolicy_Update(t *testing.T) {
t.Errorf("Monitoring.UpdateAlertPolicy returned %+v, expected %+v", policy, expected)
}
}
func TestGetDropletBandwidth(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletBandwidthMetricsRequest{
DropletMetricsRequest: DropletMetricsRequest{HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
},
Interface: "private",
Direction: "inbound",
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/bandwidth", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
inter := r.URL.Query().Get("interface")
direction := r.URL.Query().Get("direction")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, metricReq.Interface, inter)
assert.Equal(t, metricReq.Direction, direction)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, bandwidthRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletBandwidth(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletBandwidthMetrics returned error: %v", err)
}
expected := &MetricsResponse{
Status: "success",
Data: MetricsData{
ResultType: "matrix",
Result: []metrics.SampleStream{
{
Metric: metrics.Metric{
"host_id": "222651441",
"direction": "inbound",
"interface": "private",
},
Values: []metrics.SamplePair{
{
Timestamp: 1634052360000,
Value: 0.016600450090265357,
},
{
Timestamp: 1634052480000,
Value: 0.015085955677299055,
},
{
Timestamp: 1634052600000,
Value: 0.014941163855322308,
},
{
Timestamp: 1634052720000,
Value: 0.016214285714285712,
},
},
},
},
},
}
assert.Equal(t, expected, metricsResp)
}
func TestGetDropletTotalMemory(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/memory_total", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, memoryRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletTotalMemory(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletTotalMemory returned error: %v", err)
}
assert.Equal(t, testMemoryResponse, metricsResp)
}
func TestGetDropletFreeMemory(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/memory_free", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, memoryRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletFreeMemory(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletFreeMemory returned error: %v", err)
}
assert.Equal(t, testMemoryResponse, metricsResp)
}
func TestGetDropletAvailableMemory(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/memory_available", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, memoryRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletAvailableMemory(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletAvailableMemory returned error: %v", err)
}
assert.Equal(t, testMemoryResponse, metricsResp)
}
func TestGetDropletCachedMemory(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/memory_cached", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, memoryRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletCachedMemory(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletCachedMemory returned error: %v", err)
}
assert.Equal(t, testMemoryResponse, metricsResp)
}
func TestGetDropletFilesystemFree(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/filesystem_free", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, filesystemRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletFilesystemFree(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletFilesystemFree returned error: %v", err)
}
assert.Equal(t, testFilesystemResponse, metricsResp)
}
func TestGetDropletFilesystemSize(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/filesystem_size", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, filesystemRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletFilesystemSize(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletFilesystemSize returned error: %v", err)
}
assert.Equal(t, testFilesystemResponse, metricsResp)
}
func TestGetDropletLoad1(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/load_1", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, loadRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletLoad1(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletLoad1 returned error: %v", err)
}
assert.Equal(t, testLoadResponse, metricsResp)
}
func TestGetDropletLoad5(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/load_5", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, loadRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletLoad5(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletLoad5 returned error: %v", err)
}
assert.Equal(t, testLoadResponse, metricsResp)
}
func TestGetDropletLoad15(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/load_15", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, loadRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletLoad15(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletLoad15 returned error: %v", err)
}
assert.Equal(t, testLoadResponse, metricsResp)
}
func TestGetDropletCPU(t *testing.T) {
setup()
defer teardown()
now := time.Now()
metricReq := &DropletMetricsRequest{
HostID: "123",
Start: now.Add(-300 * time.Second),
End: now,
}
mux.HandleFunc("/v2/monitoring/metrics/droplet/cpu", func(w http.ResponseWriter, r *http.Request) {
hostID := r.URL.Query().Get("host_id")
start := r.URL.Query().Get("start")
end := r.URL.Query().Get("end")
assert.Equal(t, metricReq.HostID, hostID)
assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start)
assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end)
testMethod(t, r, http.MethodGet)
fmt.Fprintf(w, cpuRespJSON)
})
metricsResp, _, err := client.Monitoring.GetDropletCPU(ctx, metricReq)
if err != nil {
t.Errorf("Monitoring.GetDropletCPU returned error: %v", err)
}
assert.Equal(t, testCPUResponse, metricsResp)
}