blob: 2a0257f8a377df81f6992bdc8a6e6294474604ad [file]
// Copyright 2018, OpenCensus 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 stackdriver
import (
"context"
"fmt"
"net"
"sync"
"testing"
"time"
"go.opencensus.io/metric/metricdata"
"google.golang.org/api/option"
"google.golang.org/grpc"
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
"github.com/golang/protobuf/ptypes/empty"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
googlemetricpb "google.golang.org/genproto/googleapis/api/metric"
monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
)
func TestStatsAndMetricsEquivalence(t *testing.T) {
startTime := time.Unix(1000, 0)
startTimePb := &timestamp.Timestamp{Seconds: 1000}
md := metricdata.Descriptor{
Name: "ocagent.io/latency",
Description: "The latency of the various methods",
Unit: "ms",
Type: metricdata.TypeCumulativeInt64,
}
metricDescriptor := &metricspb.MetricDescriptor{
Name: "ocagent.io/latency",
Description: "The latency of the various methods",
Unit: "ms",
Type: metricspb.MetricDescriptor_CUMULATIVE_INT64,
}
seenResources := make(map[*resourcepb.Resource]*monitoredrespb.MonitoredResource)
// Generate some metricdata.Metric and metrics proto.
var metrics []*metricdata.Metric
var metricPbs []*metricspb.Metric
for i := 0; i < 100; i++ {
metric := &metricdata.Metric{
Descriptor: md,
TimeSeries: []*metricdata.TimeSeries{
{
StartTime: startTime,
Points: []metricdata.Point{metricdata.NewInt64Point(startTime.Add(time.Duration(1+i)*time.Second), int64(4*(i+2)))},
},
},
}
metricPb := &metricspb.Metric{
MetricDescriptor: metricDescriptor,
Timeseries: []*metricspb.TimeSeries{
{
StartTimestamp: startTimePb,
Points: []*metricspb.Point{
{
Timestamp: &timestamp.Timestamp{Seconds: int64(1001 + i)},
Value: &metricspb.Point_Int64Value{Int64Value: int64(4 * (i + 2))},
},
},
},
},
}
metrics = append(metrics, metric)
metricPbs = append(metricPbs, metricPb)
}
// Now perform some exporting.
for i, metric := range metrics {
se := &statsExporter{
o: Options{ProjectID: "equivalence", MapResource: defaultMapResource},
}
ctx := context.Background()
sMD, err := se.metricToMpbMetricDescriptor(metric)
if err != nil {
t.Errorf("#%d: Stats.metricToMpbMetricDescriptor: %v", i, err)
}
sMDR := &monitoringpb.CreateMetricDescriptorRequest{
Name: fmt.Sprintf("projects/%s", se.o.ProjectID),
MetricDescriptor: sMD,
}
inMD, err := se.protoToMonitoringMetricDescriptor(metricPbs[i], nil)
if err != nil {
t.Errorf("#%d: Stats.protoMetricDescriptorToMetricDescriptor: %v", i, err)
}
pMDR := &monitoringpb.CreateMetricDescriptorRequest{
Name: fmt.Sprintf("projects/%s", se.o.ProjectID),
MetricDescriptor: inMD,
}
if diff := cmpMDReq(pMDR, sMDR); diff != "" {
t.Fatalf("MetricDescriptor Mismatch -FromMetricsPb +FromMetrics: %s", diff)
}
stss, _ := se.metricToMpbTs(ctx, metric)
sctreql := se.combineTimeSeriesToCreateTimeSeriesRequest(stss)
allTss, _ := protoMetricToTimeSeries(ctx, se, se.getResource(nil, metricPbs[i], seenResources), metricPbs[i])
pctreql := se.combineTimeSeriesToCreateTimeSeriesRequest(allTss)
if diff := cmpTSReqs(pctreql, sctreql); diff != "" {
t.Fatalf("TimeSeries Mismatch -FromMetricsPb +FromMetrics: %s", diff)
}
}
}
// This test creates and uses a "Stackdriver backend" which receives
// CreateTimeSeriesRequest and CreateMetricDescriptor requests
// that the Stackdriver Metrics Proto client then sends to, as it would
// send to Google Stackdriver backends.
//
// This test ensures that the final responses sent by direct stats(metricdata.Metric) exporting
// are exactly equal to those from metricdata.Metric-->OpenCensus-Proto.Metrics exporting.
func TestEquivalenceStatsVsMetricsUploads(t *testing.T) {
server, addr, doneFn := createFakeServer(t)
defer doneFn()
// Now create a gRPC connection to the fake Stackdriver server.
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to make a gRPC connection to the server: %v", err)
}
defer conn.Close()
// Finally create the OpenCensus stats exporter
exporterOptions := Options{
ProjectID: "equivalence",
MonitoringClientOptions: []option.ClientOption{option.WithGRPCConn(conn)},
// Setting this time delay threshold to a very large value
// so that batching is performed deterministically and flushing is
// fully controlled by us.
BundleDelayThreshold: 2 * time.Hour,
MapResource: defaultMapResource,
}
se, err := newStatsExporter(exporterOptions)
if err != nil {
t.Fatalf("Failed to create the statsExporter: %v", err)
}
startTime := time.Unix(1000, 0)
startTimePb := &timestamp.Timestamp{Seconds: 1000}
// Generate the metricdata.Metric.
metrics := []*metricdata.Metric{
{
Descriptor: metricdata.Descriptor{
Name: "ocagent.io/calls",
Description: "The number of the various calls",
Unit: "1",
Type: metricdata.TypeCumulativeInt64,
},
TimeSeries: []*metricdata.TimeSeries{
{
StartTime: startTime,
Points: []metricdata.Point{metricdata.NewInt64Point(startTime.Add(time.Duration(1)*time.Second), 8)},
},
},
},
{
Descriptor: metricdata.Descriptor{
Name: "ocagent.io/latency",
Description: "The latency of the various methods",
Unit: "ms",
Type: metricdata.TypeCumulativeDistribution,
},
TimeSeries: []*metricdata.TimeSeries{
{
StartTime: startTime,
Points: []metricdata.Point{
metricdata.NewDistributionPoint(
startTime.Add(time.Duration(2)*time.Second),
&metricdata.Distribution{
Count: 1,
Sum: 125.9,
Buckets: []metricdata.Bucket{{Count: 0}, {Count: 1}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}},
BucketOptions: &metricdata.BucketOptions{Bounds: []float64{100, 500, 1000, 2000, 4000, 8000, 16000}},
}),
},
},
},
},
{
Descriptor: metricdata.Descriptor{
Name: "ocagent.io/connections",
Description: "The count of various connections instantaneously",
Unit: "1",
Type: metricdata.TypeGaugeInt64,
},
TimeSeries: []*metricdata.TimeSeries{
{
Points: []metricdata.Point{metricdata.NewInt64Point(startTime.Add(time.Duration(3)*time.Second), 99)},
},
},
},
{
Descriptor: metricdata.Descriptor{
Name: "ocagent.io/uptime",
Description: "The total uptime at any instance",
Unit: "ms",
Type: metricdata.TypeCumulativeFloat64,
},
TimeSeries: []*metricdata.TimeSeries{
{
StartTime: startTime,
Points: []metricdata.Point{metricdata.NewFloat64Point(startTime.Add(time.Duration(1)*time.Second), 199903.97)},
},
},
},
}
se.ExportMetrics(context.Background(), metrics)
se.Flush()
// Examining the stackdriver metrics that are available.
var stackdriverTimeSeriesFromMetrics []*monitoringpb.CreateTimeSeriesRequest
server.forEachStackdriverTimeSeries(func(sdt *monitoringpb.CreateTimeSeriesRequest) {
stackdriverTimeSeriesFromMetrics = append(stackdriverTimeSeriesFromMetrics, sdt)
})
var stackdriverMetricDescriptorsFromMetrics []*monitoringpb.CreateMetricDescriptorRequest
server.forEachStackdriverMetricDescriptor(func(sdmd *monitoringpb.CreateMetricDescriptorRequest) {
stackdriverMetricDescriptorsFromMetrics = append(stackdriverMetricDescriptorsFromMetrics, sdmd)
})
// Reset the stackdriverTimeSeries to enable fresh collection
// and then comparison with the results from metrics uploads.
server.resetStackdriverTimeSeries()
server.resetStackdriverMetricDescriptors()
// Generate the proto Metrics.
var metricPbs []*metricspb.Metric
metricPbs = append(metricPbs,
&metricspb.Metric{
MetricDescriptor: &metricspb.MetricDescriptor{
Name: "ocagent.io/calls",
Description: "The number of the various calls",
Unit: "1",
Type: metricspb.MetricDescriptor_CUMULATIVE_INT64,
},
Timeseries: []*metricspb.TimeSeries{
{
StartTimestamp: startTimePb,
Points: []*metricspb.Point{
{
Timestamp: &timestamp.Timestamp{Seconds: int64(1001)},
Value: &metricspb.Point_Int64Value{Int64Value: int64(8)},
},
},
},
},
},
&metricspb.Metric{
MetricDescriptor: &metricspb.MetricDescriptor{
Name: "ocagent.io/latency",
Description: "The latency of the various methods",
Unit: "ms",
Type: metricspb.MetricDescriptor_CUMULATIVE_DISTRIBUTION,
},
Timeseries: []*metricspb.TimeSeries{
{
StartTimestamp: startTimePb,
Points: []*metricspb.Point{
{
Timestamp: &timestamp.Timestamp{Seconds: int64(1002)},
Value: &metricspb.Point_DistributionValue{
DistributionValue: &metricspb.DistributionValue{
Count: 1,
Sum: 125.9,
BucketOptions: &metricspb.DistributionValue_BucketOptions{
Type: &metricspb.DistributionValue_BucketOptions_Explicit_{
Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{Bounds: []float64{100, 500, 1000, 2000, 4000, 8000, 16000}},
},
},
Buckets: []*metricspb.DistributionValue_Bucket{{Count: 0}, {Count: 1}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}},
},
},
},
},
},
},
},
&metricspb.Metric{
MetricDescriptor: &metricspb.MetricDescriptor{
Name: "ocagent.io/connections",
Description: "The count of various connections instantaneously",
Unit: "1",
Type: metricspb.MetricDescriptor_GAUGE_INT64,
},
Timeseries: []*metricspb.TimeSeries{
{
StartTimestamp: startTimePb,
Points: []*metricspb.Point{
{
Timestamp: &timestamp.Timestamp{Seconds: int64(1003)},
Value: &metricspb.Point_Int64Value{Int64Value: 99},
},
},
},
},
},
&metricspb.Metric{
MetricDescriptor: &metricspb.MetricDescriptor{
Name: "ocagent.io/uptime",
Description: "The total uptime at any instance",
Unit: "ms",
Type: metricspb.MetricDescriptor_CUMULATIVE_DOUBLE,
},
Timeseries: []*metricspb.TimeSeries{
{
StartTimestamp: startTimePb,
Points: []*metricspb.Point{
{
Timestamp: &timestamp.Timestamp{Seconds: int64(1001)},
Value: &metricspb.Point_DoubleValue{DoubleValue: 199903.97},
},
},
},
},
})
// Export the proto Metrics to the Stackdriver backend.
se.PushMetricsProto(context.Background(), nil, nil, metricPbs)
se.Flush()
var stackdriverTimeSeriesFromMetricsPb []*monitoringpb.CreateTimeSeriesRequest
server.forEachStackdriverTimeSeries(func(sdt *monitoringpb.CreateTimeSeriesRequest) {
stackdriverTimeSeriesFromMetricsPb = append(stackdriverTimeSeriesFromMetricsPb, sdt)
})
var stackdriverMetricDescriptorsFromMetricsPb []*monitoringpb.CreateMetricDescriptorRequest
server.forEachStackdriverMetricDescriptor(func(sdmd *monitoringpb.CreateMetricDescriptorRequest) {
stackdriverMetricDescriptorsFromMetricsPb = append(stackdriverMetricDescriptorsFromMetricsPb, sdmd)
})
if len(stackdriverTimeSeriesFromMetrics) == 0 {
t.Fatalf("Failed to export timeseries with metrics")
}
if len(stackdriverTimeSeriesFromMetricsPb) == 0 {
t.Fatalf("Failed to export timeseries with metrics pb")
}
// The results should be equal now
if diff := cmpTSReqs(stackdriverTimeSeriesFromMetricsPb, stackdriverTimeSeriesFromMetrics); diff != "" {
t.Fatalf("Unexpected CreateTimeSeriesRequests -FromMetricsPb +FromMetrics: %s", diff)
}
// Examining the metric descriptors too.
if diff := cmpMDReqs(stackdriverMetricDescriptorsFromMetricsPb, stackdriverMetricDescriptorsFromMetrics); diff != "" {
t.Fatalf("Unexpected CreateMetricDescriptorRequests -FromMetricsPb +FromMetrics: %s", diff)
}
}
type fakeMetricsServer struct {
monitoringpb.MetricServiceServer
mu sync.RWMutex
stackdriverTimeSeries []*monitoringpb.CreateTimeSeriesRequest
stackdriverMetricDescriptors []*monitoringpb.CreateMetricDescriptorRequest
}
func createFakeServer(t *testing.T) (*fakeMetricsServer, string, func()) {
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("Failed to bind to an available address: %v", err)
}
server := new(fakeMetricsServer)
srv := grpc.NewServer()
monitoringpb.RegisterMetricServiceServer(srv, server)
go func() {
_ = srv.Serve(ln)
}()
stop := func() {
srv.Stop()
_ = ln.Close()
}
_, agentPortStr, _ := net.SplitHostPort(ln.Addr().String())
return server, "localhost:" + agentPortStr, stop
}
func (server *fakeMetricsServer) forEachStackdriverTimeSeries(fn func(sdt *monitoringpb.CreateTimeSeriesRequest)) {
server.mu.RLock()
defer server.mu.RUnlock()
for _, sdt := range server.stackdriverTimeSeries {
fn(sdt)
}
}
func (server *fakeMetricsServer) forEachStackdriverMetricDescriptor(fn func(sdmd *monitoringpb.CreateMetricDescriptorRequest)) {
server.mu.RLock()
defer server.mu.RUnlock()
for _, sdmd := range server.stackdriverMetricDescriptors {
fn(sdmd)
}
}
func (server *fakeMetricsServer) resetStackdriverTimeSeries() {
server.mu.Lock()
server.stackdriverTimeSeries = server.stackdriverTimeSeries[:0]
server.mu.Unlock()
}
func (server *fakeMetricsServer) resetStackdriverMetricDescriptors() {
server.mu.Lock()
server.stackdriverMetricDescriptors = server.stackdriverMetricDescriptors[:0]
server.mu.Unlock()
}
func (server *fakeMetricsServer) CreateMetricDescriptor(ctx context.Context, req *monitoringpb.CreateMetricDescriptorRequest) (*googlemetricpb.MetricDescriptor, error) {
server.mu.Lock()
server.stackdriverMetricDescriptors = append(server.stackdriverMetricDescriptors, req)
server.mu.Unlock()
return req.MetricDescriptor, nil
}
func (server *fakeMetricsServer) CreateTimeSeries(ctx context.Context, req *monitoringpb.CreateTimeSeriesRequest) (*empty.Empty, error) {
server.mu.Lock()
server.stackdriverTimeSeries = append(server.stackdriverTimeSeries, req)
server.mu.Unlock()
return new(empty.Empty), nil
}