Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ genrule(
# gazelle:exclude pkg/model/deployment.pb.validate.go
# gazelle:exclude pkg/model/environment.pb.validate.go
# gazelle:exclude pkg/model/event.pb.validate.go
# gazelle:exclude pkg/model/insight.pb.validate.go
# gazelle:exclude pkg/model/logblock.pb.validate.go
# gazelle:exclude pkg/model/piped.pb.validate.go
# gazelle:exclude pkg/model/piped_stats.pb.validate.go
Expand Down
96 changes: 95 additions & 1 deletion pkg/app/api/grpcapi/web_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,100 @@ func (a *WebAPI) ListAPIKeys(ctx context.Context, req *webservice.ListAPIKeysReq
}

// GetInsightData returns the accumulated insight data.
func (a *WebAPI) GetInsightData(_ context.Context, _ *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) {
func (a *WebAPI) GetInsightData(ctx context.Context, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) {
claims, err := rpcauth.ExtractClaims(ctx)
if err != nil {
a.logger.Error("failed to authenticate the current user", zap.Error(err))
return nil, err
}

switch req.MetricsKind {
case model.InsightMetricsKind_DEPLOYMENT_FREQUENCY:
return a.getInsightDataForDeployFrequency(ctx, claims.Role.ProjectId, req)
}
return nil, status.Error(codes.Unimplemented, "")
}

// getInsightDataForDeployFrequency returns the accumulated insight data for deploy frequency.
func (a *WebAPI) getInsightDataForDeployFrequency(ctx context.Context, projectID string, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) {
counts := make([]*model.InsightDataPoint, req.DataPointCount)

var movePoint func(time.Time, int) time.Time
var start time.Time
// To prevent heavy loading
// - Support only daily
// - DataPointCount needs to be less than or equal to 7
switch req.Step {
case model.InsightStep_DAILY:
if req.DataPointCount > 7 {
return nil, status.Error(codes.InvalidArgument, "DataPointCount needs to be less than or equal to 7")
}
movePoint = func(from time.Time, i int) time.Time {
return from.AddDate(0, 0, i)
}
rangeFrom := time.Unix(req.RangeFrom, 0)
start = time.Date(rangeFrom.Year(), rangeFrom.Month(), rangeFrom.Day(), 0, 0, 0, 0, time.UTC)
default:
return nil, status.Error(codes.InvalidArgument, "Invalid step")
}

for i := 0; i < int(req.DataPointCount); i++ {
Copy link
Member

@nghialv nghialv Dec 4, 2020

Choose a reason for hiding this comment

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

For YEARLY, and MONTHLY we will face a heavy load on our database.
I know that this implementation is just the initial phase before applying the batch strategy in the ops,
But maybe we should limit the requestable range and step. For example, only 7 days for the DAILY is supported at this time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree :)
So, in this time, support the condition only as your example(only 7 days for the DAILY is supported)

target := movePoint(start, i)

filters := []datastore.ListFilter{
{
Field: "ProjectId",
Operator: "==",
Value: projectID,
},
{
Field: "CreatedAt",
Operator: ">=",
Value: target.Unix(),
},
{
Field: "CreatedAt",
Operator: "<",
Value: movePoint(target, 1).Unix(), // target's finish time on unix time
},
}

if req.ApplicationId != "" {
filters = append(filters, datastore.ListFilter{
Field: "ApplicationId",
Operator: "==",
Value: req.ApplicationId,
})
}

pageSize := 50
count := 0
for j := 0; ; j++ {
deployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{
PageSize: pageSize,
Page: j + 1,
Filters: filters,
})
if err != nil {
a.logger.Error("failed to get deployments", zap.Error(err))
return nil, status.Error(codes.Internal, "Failed to get deployments")
}

count += len(deployments)

if len(deployments) != 50 {
break
}
}

counts[i] = &model.InsightDataPoint{
Timestamp: target.Unix(),
Value: float32(count),
}
}

return &webservice.GetInsightDataResponse{
UpdatedAt: time.Now().Unix(),
DataPoints: counts,
}, nil
}
194 changes: 194 additions & 0 deletions pkg/app/api/grpcapi/web_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ package grpcapi
import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"time"

"go.uber.org/zap"

Copy link
Member

Choose a reason for hiding this comment

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

nit: Remove this empty line. (zap is a third-party package so it should be at the same group with gomock and assert.)

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -387,3 +391,193 @@ func TestValidatePipedBelongsToProject(t *testing.T) {
})
}
}

func TestGetInsightDataForDeployFrequency(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

PageSizeForListDeployments := 50
tests := []struct {
name string
pipedID string
projectID string
pipedProjectCache cache.Cache
deploymentStore datastore.DeploymentStore
req *webservice.GetInsightDataRequest
res *webservice.GetInsightDataResponse
wantErr bool
}{
{
name: "valid with InsightStep_DAILY",
pipedID: "pipedID",
projectID: "projectID",
deploymentStore: func() datastore.DeploymentStore {
target := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
targetNextDate := target.AddDate(0, 0, 1)
s := datastoretest.NewMockDeploymentStore(ctrl)
s.EXPECT().
ListDeployments(gomock.Any(), datastore.ListOptions{
PageSize: PageSizeForListDeployments,
Page: 1,
Filters: []datastore.ListFilter{
{
Field: "ProjectId",
Operator: "==",
Value: "projectID",
},
{
Field: "CreatedAt",
Operator: ">=",
Value: target.Unix(),
},
{
Field: "CreatedAt",
Operator: "<",
Value: targetNextDate.Unix(),
},
{
Field: "ApplicationId",
Operator: "==",
Value: "ApplicationId",
},
},
}).Return([]*model.Deployment{
{
Id: "id1",
},
{
Id: "id2",
},
}, nil)

target = time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
targetNextDate = target.AddDate(0, 0, 1)
s.EXPECT().
ListDeployments(gomock.Any(), datastore.ListOptions{
PageSize: PageSizeForListDeployments,
Page: 1,
Filters: []datastore.ListFilter{
{
Field: "ProjectId",
Operator: "==",
Value: "projectID",
},
{
Field: "CreatedAt",
Operator: ">=",
Value: target.Unix(),
},
{
Field: "CreatedAt",
Operator: "<",
Value: targetNextDate.Unix(),
},
{
Field: "ApplicationId",
Operator: "==",
Value: "ApplicationId",
},
},
}).Return([]*model.Deployment{
{
Id: "id1",
},
{
Id: "id2",
},
{
Id: "id3",
},
}, nil)

return s
}(),
req: &webservice.GetInsightDataRequest{
MetricsKind: model.InsightMetricsKind_DEPLOYMENT_FREQUENCY,
Step: model.InsightStep_DAILY,
RangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
DataPointCount: 2,
ApplicationId: "ApplicationId",
},
res: &webservice.GetInsightDataResponse{
UpdatedAt: time.Now().Unix(),
DataPoints: []*model.InsightDataPoint{
{
Value: 2,
Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
},
{
Value: 3,
Timestamp: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(),
},
},
},
wantErr: false,
},
{
name: "return error when something wrong happen on ListDeployments",
pipedID: "pipedID",
projectID: "projectID",
deploymentStore: func() datastore.DeploymentStore {
target := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
targetNextYear := target.AddDate(0, 0, 1)
s := datastoretest.NewMockDeploymentStore(ctrl)
s.EXPECT().
ListDeployments(gomock.Any(), datastore.ListOptions{
PageSize: PageSizeForListDeployments,
Page: 1,
Filters: []datastore.ListFilter{
{
Field: "ProjectId",
Operator: "==",
Value: "projectID",
},
{
Field: "CreatedAt",
Operator: ">=",
Value: target.Unix(),
},
{
Field: "CreatedAt",
Operator: "<",
Value: targetNextYear.Unix(),
},
{
Field: "ApplicationId",
Operator: "==",
Value: "ApplicationId",
},
},
}).Return([]*model.Deployment{}, fmt.Errorf("something wrong happens in ListDeployments"))
return s
}(),
req: &webservice.GetInsightDataRequest{
MetricsKind: model.InsightMetricsKind_DEPLOYMENT_FREQUENCY,
Step: model.InsightStep_DAILY,
RangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
DataPointCount: 2,
ApplicationId: "ApplicationId",
},
res: nil,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
api := &WebAPI{
pipedProjectCache: tt.pipedProjectCache,
deploymentStore: tt.deploymentStore,
logger: zap.NewNop(),
}
res, err := api.getInsightDataForDeployFrequency(ctx, tt.projectID, tt.req)
assert.Equal(t, tt.wantErr, err != nil)
if err == nil {
assert.Equal(t, tt.res.DataPoints, res.DataPoints)
}
})
}
}