Skip to content
This repository was archived by the owner on Oct 26, 2022. It is now read-only.

Commit ec66394

Browse files
Merge pull request #41 from googleinterns/ericwang/time_series_validation
Add validation for CreateTimeSeries
2 parents f6fb5ae + 55d35e1 commit ec66394

File tree

6 files changed

+202
-26
lines changed

6 files changed

+202
-26
lines changed

internal/validation/mock_metric_validation.go

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@ package validation
1717
import (
1818
"fmt"
1919
"reflect"
20+
"time"
21+
22+
"github.com/golang/protobuf/ptypes"
2023

2124
"google.golang.org/genproto/googleapis/api/metric"
2225
"google.golang.org/genproto/googleapis/monitoring/v3"
2326
"google.golang.org/genproto/googleapis/rpc/errdetails"
2427
)
2528

26-
// IsValidRequest verifies that the given request is valid.
27-
// This means required fields are present and all fields semantically make sense.
28-
func IsValidRequest(req interface{}) error {
29+
const (
30+
// Time series constraints can be found at https://cloud.google.com/monitoring/quotas#custom_metrics_quotas.
31+
maxTimeSeriesPerRequest = 200
32+
maxTimeSeriesLabelKeyBytes = 100
33+
maxTimeSeriesLabelValueBytes = 1024
34+
)
35+
36+
// ValidRequiredFields verifies that the given request contains the required fields.
37+
func ValidRequiredFields(req interface{}) error {
2938
reqReflect := reflect.ValueOf(req)
3039
requiredFields := []string{"Name"}
3140
requestName := ""
@@ -105,3 +114,116 @@ func RemoveMetricDescriptor(uploadedMetricDescriptors map[string]*metric.MetricD
105114

106115
return nil
107116
}
117+
118+
// ValidateCreateTimeSeries checks that the given TimeSeries conform to the API requirements.
119+
func ValidateCreateTimeSeries(timeSeries []*monitoring.TimeSeries, descriptors map[string]*metric.MetricDescriptor) error {
120+
if len(timeSeries) > maxTimeSeriesPerRequest {
121+
return statusTooManyTimeSeries
122+
}
123+
124+
for _, ts := range timeSeries {
125+
// Check that required fields for time series are present.
126+
if ts.Metric == nil || len(ts.Points) != 1 || ts.Resource == nil {
127+
return statusInvalidTimeSeries
128+
}
129+
130+
// Check that the metric labels follow the constraints.
131+
for k, v := range ts.Metric.Labels {
132+
if len(k) > maxTimeSeriesLabelKeyBytes {
133+
return statusInvalidTimeSeriesLabelKey
134+
}
135+
136+
if len(v) > maxTimeSeriesLabelValueBytes {
137+
return statusInvalidTimeSeriesLabelValue
138+
}
139+
}
140+
141+
if err := validateMetricKind(ts, descriptors); err != nil {
142+
return err
143+
}
144+
145+
if err := validateValueType(ts.ValueType, ts.Points[0]); err != nil {
146+
return err
147+
}
148+
149+
if err := validatePoint(ts.MetricKind, ts.Points[0]); err != nil {
150+
return err
151+
}
152+
}
153+
154+
return nil
155+
}
156+
157+
// validateMetricKind check that if metric_kind is present,
158+
// it is the same as the metricKind of the associated metric.
159+
func validateMetricKind(timeSeries *monitoring.TimeSeries, descriptors map[string]*metric.MetricDescriptor) error {
160+
descriptor := descriptors[timeSeries.Metric.Type]
161+
if descriptor == nil {
162+
return statusMissingMetricDescriptor
163+
}
164+
if descriptor.MetricKind != timeSeries.MetricKind {
165+
return statusInvalidTimeSeriesMetricKind
166+
}
167+
168+
return nil
169+
}
170+
171+
// validateValueType checks that if TimeSeries' "value_type" is present,
172+
// it is the same as the type of the data in the "points" field.
173+
func validateValueType(valueType metric.MetricDescriptor_ValueType, point *monitoring.Point) error {
174+
if valueType == metric.MetricDescriptor_BOOL {
175+
if _, ok := point.Value.Value.(*monitoring.TypedValue_BoolValue); !ok {
176+
return statusInvalidTimeSeriesValueType
177+
}
178+
}
179+
if valueType == metric.MetricDescriptor_INT64 {
180+
if _, ok := point.Value.Value.(*monitoring.TypedValue_Int64Value); !ok {
181+
return statusInvalidTimeSeriesValueType
182+
}
183+
}
184+
if valueType == metric.MetricDescriptor_DOUBLE {
185+
if _, ok := point.Value.Value.(*monitoring.TypedValue_DoubleValue); !ok {
186+
return statusInvalidTimeSeriesValueType
187+
}
188+
}
189+
if valueType == metric.MetricDescriptor_STRING {
190+
if _, ok := point.Value.Value.(*monitoring.TypedValue_StringValue); !ok {
191+
return statusInvalidTimeSeriesValueType
192+
}
193+
}
194+
if valueType == metric.MetricDescriptor_DISTRIBUTION {
195+
if _, ok := point.Value.Value.(*monitoring.TypedValue_DistributionValue); !ok {
196+
return statusInvalidTimeSeriesValueType
197+
}
198+
}
199+
200+
return nil
201+
}
202+
203+
// validatePoint checks that the data for the point is valid.
204+
// The specifics checks depend on the metric_kind.
205+
func validatePoint(metricKind metric.MetricDescriptor_MetricKind, point *monitoring.Point) error {
206+
startTime, err := ptypes.Timestamp(point.Interval.StartTime)
207+
// If metric_kind is GAUGE, the startTime is optional.
208+
if err != nil && metricKind != metric.MetricDescriptor_GAUGE {
209+
return statusMalformedTimestamp
210+
}
211+
endTime, err := ptypes.Timestamp(point.Interval.EndTime)
212+
if err != nil {
213+
return statusMalformedTimestamp
214+
}
215+
216+
// If metric_kind is GAUGE, if start time is supplied, it must equal the end time.
217+
if metricKind == metric.MetricDescriptor_GAUGE {
218+
// If start time is nil, ptypes.Timestamp will return time.Unix(0, 0).UTC().
219+
if startTime != time.Unix(0, 0).UTC() && !startTime.Equal(endTime) {
220+
return statusInvalidTimeSeriesPointGauge
221+
}
222+
}
223+
224+
// TODO (ejwang): also need checks for metric.MetricDescriptor_DELTA and metric.MetricDescriptor_CUMULATIVE,
225+
// however they involve comparing against previous time series, so we'll need to store time series in memory.
226+
// Leaving this for another PR since this PR is already quite large.
227+
228+
return nil
229+
}

internal/validation/mock_trace_validation.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,9 @@ var (
5151
"http.route": {},
5252
"http.status_code": {},
5353
}
54-
requiredFields = []string{"Name", "SpanId", "DisplayName", "StartTime", "EndTime"}
55-
spanNameRegex = regexp.MustCompile("^projects/[^/]+/traces/[a-fA-F0-9]{32}/spans/[a-fA-F0-9]{16}$")
56-
projectNameRegex = regexp.MustCompile("^projects/[^/]+$")
57-
agentRegex = regexp.MustCompile(`^opentelemetry-[a-zA-Z]+ [0-9]+\.[0-9]+\.[0-9]+; google-cloud-trace-exporter [0-9]+\.[0-9]+\.[0-9]+$`)
54+
requiredFields = []string{"Name", "SpanId", "DisplayName", "StartTime", "EndTime"}
55+
spanNameRegex = regexp.MustCompile("^projects/[^/]+/traces/[a-fA-F0-9]{32}/spans/[a-fA-F0-9]{16}$")
56+
agentRegex = regexp.MustCompile(`^opentelemetry-[a-zA-Z]+ [0-9]+\.[0-9]+\.[0-9]+; google-cloud-trace-exporter [0-9]+\.[0-9]+\.[0-9]+$`)
5857
)
5958

6059
// ValidateSpans checks that the spans conform to the API requirements.
@@ -130,15 +129,6 @@ func AccessSpan(index int, uploadedSpans []*cloudtrace.Span) *cloudtrace.Span {
130129
return uploadedSpans[index]
131130
}
132131

133-
// ValidateProjectName verifies that the project name from the BatchWriteSpans request
134-
// is of the form projects/[PROJECT_ID]
135-
func ValidateProjectName(projectName string) error {
136-
if !projectNameRegex.MatchString(projectName) {
137-
return statusInvalidProjectName
138-
}
139-
return nil
140-
}
141-
142132
// validateDisplayName verifies that the display name has at most 128 bytes.
143133
func validateDisplayName(displayName *cloudtrace.TruncatableString) error {
144134
if len(displayName.Value) > maxDisplayNameBytes {

internal/validation/shared_validation.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package validation
1717
import (
1818
"fmt"
1919
"reflect"
20+
"regexp"
2021

2122
"google.golang.org/genproto/googleapis/rpc/errdetails"
2223
"google.golang.org/grpc/status"
@@ -26,6 +27,10 @@ const (
2627
missingFieldMsg = "%v must contain the required %v field"
2728
)
2829

30+
var (
31+
projectNameRegex = regexp.MustCompile("^projects/[^/]+$")
32+
)
33+
2934
// CheckForRequiredFields verifies that the required fields for the given request are present.
3035
func CheckForRequiredFields(requiredFields []string, reqReflect reflect.Value, requestName string) error {
3136
br := &errdetails.BadRequest{}
@@ -78,3 +83,12 @@ func ValidateDuplicateErrDetails(err error, duplicateName string) bool {
7883
}
7984
return true
8085
}
86+
87+
// ValidateProjectName verifies that the project name from the BatchWriteSpans request
88+
// is of the form projects/[PROJECT_ID]
89+
func ValidateProjectName(projectName string) error {
90+
if !projectNameRegex.MatchString(projectName) {
91+
return statusInvalidProjectName
92+
}
93+
return nil
94+
}

internal/validation/statuses.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ var (
6161
// Metric statuses.
6262
statusDuplicateMetricDescriptorType = status.New(codes.AlreadyExists, "metric descriptor of same type already exists")
6363
statusMetricDescriptorNotFound = status.New(codes.NotFound, "metric descriptor of given type does not exist")
64+
statusTooManyTimeSeries = status.Error(codes.InvalidArgument,
65+
fmt.Sprintf("maximum number of time series per request is %v", maxTimeSeriesPerRequest))
66+
statusInvalidTimeSeries = status.Error(codes.InvalidArgument,
67+
"time series require fields metric, resource, exactly one point")
68+
statusInvalidTimeSeriesLabelKey = status.Error(codes.InvalidArgument,
69+
fmt.Sprintf("time series metric label keys have a max length of %v bytes", maxTimeSeriesLabelKeyBytes))
70+
statusInvalidTimeSeriesLabelValue = status.Error(codes.InvalidArgument,
71+
fmt.Sprintf("time series metric label values have a max length of %v bytes", maxTimeSeriesLabelValueBytes))
72+
statusInvalidTimeSeriesValueType = status.Error(codes.InvalidArgument,
73+
"time series' value_type field must be the same as the type of the data in the points field")
74+
statusMissingMetricDescriptor = status.Error(codes.InvalidArgument,
75+
"corresponding metric descriptor for given time series does not exist")
76+
statusInvalidTimeSeriesMetricKind = status.Error(codes.InvalidArgument,
77+
"metric kind must be the same as the metric kind of the associated metric")
78+
statusInvalidTimeSeriesPointGauge = status.Error(codes.InvalidArgument,
79+
"for a GAUGE metric kind, the point's start time must equal the end time")
6480

6581
// Shared statuses.
6682
statusMissingField = status.New(codes.InvalidArgument, "missing required field(s)")

server/metric/mock_metric.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func NewMockMetricServer() *MockMetricServer {
4444
// GetMonitoredResourceDescriptor returns the requested monitored resource descriptor if it exists.
4545
func (s *MockMetricServer) GetMonitoredResourceDescriptor(ctx context.Context, req *monitoring.GetMonitoredResourceDescriptorRequest,
4646
) (*monitoredres.MonitoredResourceDescriptor, error) {
47-
if err := validation.IsValidRequest(req); err != nil {
47+
if err := validation.ValidRequiredFields(req); err != nil {
4848
return nil, err
4949
}
5050
return &monitoredres.MonitoredResourceDescriptor{}, nil
@@ -54,7 +54,7 @@ func (s *MockMetricServer) GetMonitoredResourceDescriptor(ctx context.Context, r
5454
// that are picked up by the given query.
5555
func (s *MockMetricServer) ListMonitoredResourceDescriptors(ctx context.Context, req *monitoring.ListMonitoredResourceDescriptorsRequest,
5656
) (*monitoring.ListMonitoredResourceDescriptorsResponse, error) {
57-
if err := validation.IsValidRequest(req); err != nil {
57+
if err := validation.ValidRequiredFields(req); err != nil {
5858
return nil, err
5959
}
6060
return &monitoring.ListMonitoredResourceDescriptorsResponse{
@@ -67,7 +67,7 @@ func (s *MockMetricServer) ListMonitoredResourceDescriptors(ctx context.Context,
6767
// If it doesn't esxist, an error is returned.
6868
func (s *MockMetricServer) GetMetricDescriptor(ctx context.Context, req *monitoring.GetMetricDescriptorRequest,
6969
) (*metric.MetricDescriptor, error) {
70-
if err := validation.IsValidRequest(req); err != nil {
70+
if err := validation.ValidRequiredFields(req); err != nil {
7171
return nil, err
7272
}
7373

@@ -85,7 +85,7 @@ func (s *MockMetricServer) GetMetricDescriptor(ctx context.Context, req *monitor
8585
// If it already exists, an error is returned.
8686
func (s *MockMetricServer) CreateMetricDescriptor(ctx context.Context, req *monitoring.CreateMetricDescriptorRequest,
8787
) (*metric.MetricDescriptor, error) {
88-
if err := validation.IsValidRequest(req); err != nil {
88+
if err := validation.ValidRequiredFields(req); err != nil {
8989
return nil, err
9090
}
9191
s.uploadedMetricDescriptorsLock.Lock()
@@ -101,7 +101,7 @@ func (s *MockMetricServer) CreateMetricDescriptor(ctx context.Context, req *moni
101101
// If it doesn't exist, an error is returned.
102102
func (s *MockMetricServer) DeleteMetricDescriptor(ctx context.Context, req *monitoring.DeleteMetricDescriptorRequest,
103103
) (*empty.Empty, error) {
104-
if err := validation.IsValidRequest(req); err != nil {
104+
if err := validation.ValidRequiredFields(req); err != nil {
105105
return nil, err
106106
}
107107

@@ -117,7 +117,7 @@ func (s *MockMetricServer) DeleteMetricDescriptor(ctx context.Context, req *moni
117117
// ListMetricDescriptors lists all the metric descriptors that are picked up by the given query.
118118
func (s *MockMetricServer) ListMetricDescriptors(ctx context.Context, req *monitoring.ListMetricDescriptorsRequest,
119119
) (*monitoring.ListMetricDescriptorsResponse, error) {
120-
if err := validation.IsValidRequest(req); err != nil {
120+
if err := validation.ValidRequiredFields(req); err != nil {
121121
return nil, err
122122
}
123123
return &monitoring.ListMetricDescriptorsResponse{
@@ -130,7 +130,13 @@ func (s *MockMetricServer) ListMetricDescriptors(ctx context.Context, req *monit
130130
// If it already exists, an error is returned.
131131
func (s *MockMetricServer) CreateTimeSeries(ctx context.Context, req *monitoring.CreateTimeSeriesRequest,
132132
) (*empty.Empty, error) {
133-
if err := validation.IsValidRequest(req); err != nil {
133+
if err := validation.ValidRequiredFields(req); err != nil {
134+
return nil, err
135+
}
136+
if err := validation.ValidateProjectName(req.Name); err != nil {
137+
return nil, err
138+
}
139+
if err := validation.ValidateCreateTimeSeries(req.TimeSeries, s.uploadedMetricDescriptors); err != nil {
134140
return nil, err
135141
}
136142
return &empty.Empty{}, nil
@@ -139,7 +145,7 @@ func (s *MockMetricServer) CreateTimeSeries(ctx context.Context, req *monitoring
139145
// ListTimeSeries lists all time series that are picked up by the given query.
140146
func (s *MockMetricServer) ListTimeSeries(ctx context.Context, req *monitoring.ListTimeSeriesRequest,
141147
) (*monitoring.ListTimeSeriesResponse, error) {
142-
if err := validation.IsValidRequest(req); err != nil {
148+
if err := validation.ValidRequiredFields(req); err != nil {
143149
return nil, err
144150
}
145151
return &monitoring.ListTimeSeriesResponse{

server/metric/mock_metric_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"net"
2121
"testing"
2222

23+
"github.com/golang/protobuf/ptypes"
24+
2325
"github.com/golang/protobuf/ptypes/empty"
2426
"github.com/googleinterns/cloud-operations-api-mock/internal/validation"
2527

@@ -78,9 +80,35 @@ func TestMockMetricServer_CreateTimeSeries(t *testing.T) {
7880
setup()
7981
defer tearDown()
8082

83+
// Create the corresponding MetricDescriptor.
84+
_, err := client.CreateMetricDescriptor(ctx, &monitoring.CreateMetricDescriptorRequest{
85+
Name: "projects/test-project",
86+
MetricDescriptor: &metric.MetricDescriptor{
87+
Type: "test-metric-type",
88+
MetricKind: metric.MetricDescriptor_GAUGE,
89+
},
90+
})
91+
92+
// Create the TimeSeries.
93+
gaugeTime := ptypes.TimestampNow()
94+
if err != nil {
95+
log.Fatalf("failed to create span with error: %v", err)
96+
}
97+
8198
in := &monitoring.CreateTimeSeriesRequest{
82-
Name: "projects/test-project",
83-
TimeSeries: []*monitoring.TimeSeries{{}},
99+
Name: "projects/test-project",
100+
TimeSeries: []*monitoring.TimeSeries{{
101+
Metric: &metric.Metric{Type: "test-metric-type"},
102+
Resource: &monitoredres.MonitoredResource{Type: "test-monitored-resource"},
103+
MetricKind: metric.MetricDescriptor_GAUGE,
104+
Points: []*monitoring.Point{
105+
{
106+
Interval: &monitoring.TimeInterval{
107+
StartTime: gaugeTime, EndTime: gaugeTime,
108+
},
109+
},
110+
},
111+
}},
84112
}
85113
want := &empty.Empty{}
86114
response, err := client.CreateTimeSeries(ctx, in)

0 commit comments

Comments
 (0)