Skip to content

Commit

Permalink
Added API method and test for getting app stats independently
Browse files Browse the repository at this point in the history
of the instances call. Modified app command so that it uses the
stats request instead of stats+instances requests.

[#70701706]
  • Loading branch information
Anand Gaitonde and Devin Fallak authored and Anand Gaitonde and Devin Fallak committed Jul 15, 2014
1 parent 417c7ee commit 87702ed
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 72 deletions.
11 changes: 8 additions & 3 deletions cf/api/app_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package api

import (
"fmt"
"github.com/cloudfoundry/cli/cf/configuration"
"github.com/cloudfoundry/cli/cf/models"
"github.com/cloudfoundry/cli/cf/net"
"strconv"
"strings"
"time"

"github.com/cloudfoundry/cli/cf/configuration"
"github.com/cloudfoundry/cli/cf/models"
"github.com/cloudfoundry/cli/cf/net"
)

type InstancesApiResponse map[string]InstanceApiResponse
Expand All @@ -18,15 +19,19 @@ type InstanceApiResponse struct {
}

type StatsApiResponse map[string]InstanceStatsApiResponse
type StatsApiResponse2 map[string]models.AppStatsFields

type InstanceStatsApiResponse struct {
State string
Stats struct {
DiskQuota uint64 `json:"disk_quota"`
MemQuota uint64 `json:"mem_quota"`
Uptime uint64
Usage struct {
Cpu float64
Disk uint64
Mem uint64
Time string
}
}
}
Expand Down
29 changes: 0 additions & 29 deletions cf/api/app_instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,6 @@ var _ = Describe("AppInstancesRepo", func() {
})
})

var appStatsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{
Method: "GET",
Path: "/v2/apps/my-cool-app-guid/stats",
Response: testnet.TestResponse{Status: http.StatusOK, Body: `
{
"1":{
"stats": {
"disk_quota": 10000,
"mem_quota": 1024,
"usage": {
"cpu": 0.3,
"disk": 10000,
"mem": 1024
}
}
},
"0":{
"stats": {
"disk_quota": 1073741824,
"mem_quota": 67108864,
"usage": {
"cpu": 3.659571249238058e-05,
"disk": 56037376,
"mem": 19218432
}
}
}
}`}})

var appInstancesRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{
Method: "GET",
Path: "/v2/apps/my-cool-app-guid/instances",
Expand Down
45 changes: 45 additions & 0 deletions cf/api/app_stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"fmt"
"strconv"

"github.com/cloudfoundry/cli/cf/configuration"
"github.com/cloudfoundry/cli/cf/models"
"github.com/cloudfoundry/cli/cf/net"
)

type AppStatsRepository interface {
GetStats(appGuid string) (stats []models.AppStatsFields, apiErr error)
}

type CloudControllerAppStatsRepository struct {
config configuration.Reader
gateway net.Gateway
}

func NewCloudControllerAppStatsRepository(config configuration.Reader, gateway net.Gateway) (repo CloudControllerAppStatsRepository) {
repo.config = config
repo.gateway = gateway
return
}

func (repo CloudControllerAppStatsRepository) GetStats(guid string) (stats []models.AppStatsFields, apiErr error) {
path := fmt.Sprintf("%s/v2/apps/%s/stats", repo.config.ApiEndpoint(), guid)
statsResponse := map[string]models.AppStatsFields{}
apiErr = repo.gateway.GetResource(path, &statsResponse)
if apiErr != nil {
return
}

stats = make([]models.AppStatsFields, len(statsResponse), len(statsResponse))
for key, value := range statsResponse {
index, err := strconv.Atoi(key)
if err != nil {
continue
}

stats[index] = value
}
return
}
88 changes: 88 additions & 0 deletions cf/api/app_stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package api_test

import (
"net/http"
"net/http/httptest"
"time"

testapi "github.com/cloudfoundry/cli/cf/api/fakes"
"github.com/cloudfoundry/cli/cf/models"
"github.com/cloudfoundry/cli/cf/net"
testconfig "github.com/cloudfoundry/cli/testhelpers/configuration"
testnet "github.com/cloudfoundry/cli/testhelpers/net"

. "github.com/cloudfoundry/cli/cf/api"
. "github.com/cloudfoundry/cli/testhelpers/matchers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("AppStatsRepo", func() {
It("returns stats for the app, given a guid", func() {
ts, handler, repo := createAppStatsRepo([]testnet.TestRequest{
appStatsRequest,
})
defer ts.Close()
appGuid := "my-cool-app-guid"

stats, err := repo.GetStats(appGuid)
Expect(err).NotTo(HaveOccurred())
Expect(handler).To(HaveAllRequestsCalled())

Expect(len(stats)).To(Equal(2))

Expect(stats[1].State).To(Equal(models.InstanceRunning))
Expect(stats[0].State).To(Equal(models.InstanceFlapping))

stats1 := stats[1]
Expect(stats1.Stats.DiskQuota).To(Equal(uint64(10000)))
Expect(stats1.Stats.Usage.Disk).To(Equal(uint64(10000)))
Expect(stats1.Stats.MemQuota).To(Equal(uint64(1024)))
Expect(stats1.Stats.Usage.Mem).To(Equal(uint64(1024)))
Expect(stats1.Stats.Usage.Cpu).To(Equal(0.3))
})
})

var appStatsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{
Method: "GET",
Path: "/v2/apps/my-cool-app-guid/stats",
Response: testnet.TestResponse{Status: http.StatusOK, Body: `
{
"1":{
"state": "running",
"stats": {
"disk_quota": 10000,
"mem_quota": 1024,
"usage": {
"cpu": 0.3,
"disk": 10000,
"mem": 1024,
"time": "2014-07-14 23:33:55 +0000"
}
}
},
"0":{
"state": "flapping",
"stats": {
"disk_quota": 1073741824,
"mem_quota": 67108864,
"usage": {
"cpu": 3.659571249238058e-05,
"disk": 56037376,
"mem": 19218432,
"time": "2014-07-14 23:33:55 +0000"
}
}
}
}`}})

func createAppStatsRepo(requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo AppStatsRepository) {
ts, handler = testnet.NewServer(requests)
space := models.SpaceFields{}
space.Guid = "my-space-guid"
configRepo := testconfig.NewRepositoryWithDefaults()
configRepo.SetApiEndpoint(ts.URL)
gateway := net.NewCloudControllerGateway(configRepo, time.Now)
repo = NewCloudControllerAppStatsRepository(configRepo, gateway)
return
}
39 changes: 39 additions & 0 deletions cf/api/fakes/fake_app_stats_repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package fakes

import (
"github.com/cloudfoundry/cli/cf/errors"
"github.com/cloudfoundry/cli/cf/models"
)

type FakeAppStatsRepo struct {
GetStatsAppGuid string
GetStatsResponses [][]models.AppStatsFields
GetStatsErrorCodes []string
}

func (repo *FakeAppStatsRepo) GetStats(appGuid string) (stats []models.AppStatsFields, apiErr error) {
repo.GetStatsAppGuid = appGuid

if len(repo.GetStatsResponses) > 0 {
stats = repo.GetStatsResponses[0]

if len(repo.GetStatsResponses) > 1 {
repo.GetStatsResponses = repo.GetStatsResponses[1:]
}
}

if len(repo.GetStatsErrorCodes) > 0 {
errorCode := repo.GetStatsErrorCodes[0]

// don't slice away the last one if this is all we have
if len(repo.GetStatsErrorCodes) > 1 {
repo.GetStatsErrorCodes = repo.GetStatsErrorCodes[1:]
}

if errorCode != "" {
apiErr = errors.NewHttpError(400, errorCode, "Error staging app")
}
}

return
}
6 changes: 6 additions & 0 deletions cf/api/repository_locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type RepositoryLocator struct {
appBitsRepo CloudControllerApplicationBitsRepository
appSummaryRepo CloudControllerAppSummaryRepository
appInstancesRepo CloudControllerAppInstancesRepository
appStatsRepo CloudControllerAppStatsRepository
appEventsRepo CloudControllerAppEventsRepository
appFilesRepo CloudControllerAppFilesRepository
domainRepo CloudControllerDomainRepository
Expand Down Expand Up @@ -71,6 +72,7 @@ func NewRepositoryLocator(config configuration.ReadWriter, gatewaysByName map[st
loc.appRepo = NewCloudControllerApplicationRepository(config, cloudControllerGateway)
loc.appSummaryRepo = NewCloudControllerAppSummaryRepository(config, cloudControllerGateway)
loc.appInstancesRepo = NewCloudControllerAppInstancesRepository(config, cloudControllerGateway)
loc.appStatsRepo = NewCloudControllerAppStatsRepository(config, cloudControllerGateway)
loc.authTokenRepo = NewCloudControllerServiceAuthTokenRepository(config, cloudControllerGateway)
loc.curlRepo = NewCloudControllerCurlRepository(config, cloudControllerGateway)
loc.domainRepo = NewCloudControllerDomainRepository(config, cloudControllerGateway, strategy)
Expand Down Expand Up @@ -138,6 +140,10 @@ func (locator RepositoryLocator) GetAppInstancesRepository() AppInstancesReposit
return locator.appInstancesRepo
}

func (locator RepositoryLocator) GetAppStatsRepository() AppStatsRepository {
return locator.appStatsRepo
}

func (locator RepositoryLocator) GetAppEventsRepository() AppEventsRepository {
return locator.appEventsRepo
}
Expand Down
2 changes: 1 addition & 1 deletion cf/command_factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func NewFactory(ui terminal.UI, config configuration.ReadWriter, manifestRepo ma
factory.cmdsByName["map-route"] = route.NewMapRoute(ui, config, repoLocator.GetRouteRepository(), createRoute)
factory.cmdsByName["unmap-route"] = route.NewUnmapRoute(ui, config, repoLocator.GetRouteRepository())

displayApp := application.NewShowApp(ui, config, repoLocator.GetAppSummaryRepository(), repoLocator.GetAppInstancesRepository())
displayApp := application.NewShowApp(ui, config, repoLocator.GetAppSummaryRepository(), repoLocator.GetAppStatsRepository())
start := application.NewStart(ui, config, displayApp, repoLocator.GetApplicationRepository(), repoLocator.GetAppInstancesRepository(), repoLocator.GetLogsRepository())
stop := application.NewStop(ui, config, repoLocator.GetApplicationRepository())
restart := application.NewRestart(ui, start, stop)
Expand Down
45 changes: 28 additions & 17 deletions cf/commands/application/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package application
import (
"fmt"
"strings"
"time"

"github.com/cloudfoundry/cli/cf/api"
"github.com/cloudfoundry/cli/cf/command_metadata"
Expand All @@ -17,23 +18,23 @@ import (
)

type ShowApp struct {
ui terminal.UI
config configuration.Reader
appSummaryRepo api.AppSummaryRepository
appInstancesRepo api.AppInstancesRepository
appReq requirements.ApplicationRequirement
ui terminal.UI
config configuration.Reader
appSummaryRepo api.AppSummaryRepository
appStatsRepo api.AppStatsRepository
appReq requirements.ApplicationRequirement
}

type ApplicationDisplayer interface {
ShowApp(app models.Application)
}

func NewShowApp(ui terminal.UI, config configuration.Reader, appSummaryRepo api.AppSummaryRepository, appInstancesRepo api.AppInstancesRepository) (cmd *ShowApp) {
func NewShowApp(ui terminal.UI, config configuration.Reader, appSummaryRepo api.AppSummaryRepository, appStatsRepo api.AppStatsRepository) (cmd *ShowApp) {
cmd = new(ShowApp)
cmd.ui = ui
cmd.config = config
cmd.appSummaryRepo = appSummaryRepo
cmd.appInstancesRepo = appInstancesRepo
cmd.appStatsRepo = appStatsRepo
return
}

Expand Down Expand Up @@ -88,8 +89,8 @@ func (cmd *ShowApp) ShowApp(app models.Application) {
return
}

var instances []models.AppInstanceFields
instances, apiErr = cmd.appInstancesRepo.GetInstances(app.Guid)
var stats []models.AppStatsFields
stats, apiErr = cmd.appStatsRepo.GetStats(app.Guid)
if apiErr != nil && !appIsStopped {
cmd.ui.Failed(apiErr.Error())
return
Expand Down Expand Up @@ -118,20 +119,30 @@ func (cmd *ShowApp) ShowApp(app models.Application) {

table := terminal.NewTable(cmd.ui, []string{"", T("state"), T("since"), T("cpu"), T("memory"), T("disk")})

for index, instance := range instances {
for index, stat := range stats {
since, err := time.Parse("2006-01-02 15:04:05 +0000", stat.Stats.Usage.Time)
if err != nil {
cmd.ui.Failed(err.Error())
return
}

uptime := time.Duration(stat.Stats.Uptime) * time.Second

since = since.Add(-uptime)

table.Add(
fmt.Sprintf("#%d", index),
ui_helpers.ColoredInstanceState(instance),
instance.Since.Format("2006-01-02 03:04:05 PM"),
fmt.Sprintf("%.1f%%", instance.CpuUsage*100),
ui_helpers.ColoredInstanceState(stat.State),
since.Format("2006-01-02 03:04:05 PM"),
fmt.Sprintf("%.1f%%", stat.Stats.Usage.Cpu*100),
fmt.Sprintf(T("{{.MemUsage}} of {{.MemQuota}}",
map[string]interface{}{
"MemUsage": formatters.ByteSize(instance.MemUsage),
"MemQuota": formatters.ByteSize(instance.MemQuota)})),
"MemUsage": formatters.ByteSize(stat.Stats.Usage.Mem),
"MemQuota": formatters.ByteSize(stat.Stats.MemQuota)})),
fmt.Sprintf(T("{{.DiskUsage}} of {{.DiskQuota}}",
map[string]interface{}{
"DiskUsage": formatters.ByteSize(instance.DiskUsage),
"DiskQuota": formatters.ByteSize(instance.DiskQuota)})),
"DiskUsage": formatters.ByteSize(stat.Stats.Usage.Disk),
"DiskQuota": formatters.ByteSize(stat.Stats.DiskQuota)})),
)
}

Expand Down

0 comments on commit 87702ed

Please sign in to comment.