Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[azure] Improve cost management integration #275

Merged
merged 1 commit into from
Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from all 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#263](https://github.com/kobsio/kobs/pull/263): [core] :warning: _Breaking change:_ :warning: Refactor `cluster` and `clusters` package.
- [#265](https://github.com/kobsio/kobs/pull/265): [applications] Improve tags support by allow users to filter applications by tags and showing tags on application page.
- [#269](https://github.com/kobsio/kobs/pull/269): [applications] :warning: _Breaking change:_ :warning: Improve topology graph, by allowing custom styles for applications.
- [#275](https://github.com/kobsio/kobs/pull/275): [azure] Improve cost management integration by adjusting the chart style and allowing the usage in dashboard panels.

## [v0.7.0](https://github.com/kobsio/kobs/releases/tag/v0.7.0) (2021-11-19)

Expand Down
10 changes: 9 additions & 1 deletion docs/plugins/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ The following options can be used for a panel with the Azure plugin:

| Field | Type | Description | Required |
| ----- | ---- | ----------- | -------- |
| type | string | The service type which should be used for the panel Currently `containerinstances`, `kubernetesservices` and `virtualmachinescalesets` are supported values. | Yes |
| type | string | The service type which should be used for the panel Currently `containerinstances`, `costmanagement`, `kubernetesservices` and `virtualmachinescalesets` are supported values. | Yes |
| containerinstances | [Container Instances](#container-instances) | The configuration for the panel if the type is `containerinstances`. | No |
| costmanagement | [Cost Management](#cost-management) | The configuration for the panel if the type is `costmanagement`. | No |
| kubernetesservices | [Kubernetes Services](#kubernetes-services) | The configuration for the panel if the type is `kubernetesservices`. | No |
| virtualmachinescalesets | [Virtual Machine Scale Sets](#virtual-machine-scale-sets) | The configuration for the panel if the type is `virtualmachinescalesets`. | No |

Expand All @@ -63,6 +64,13 @@ The following options can be used for a panel with the Azure plugin:
| metricNames | string | The name of the metric for which the data should be displayed. Supported values are `CPUUsage`, `MemoryUsage`, `NetworkBytesReceivedPerSecond` and `NetworkBytesTransmittedPerSecond`. This is only required if the type is `metrics`. | No |
| aggregationType | string | The aggregation type for the metric. Supported values are `Average`, `Minimum`, `Maximum`, `Total` and `Count`. This is only required if the type is `metrics`. | No |

### Cost Management

| Field | Type | Description | Required |
| ----- | ---- | ----------- | -------- |
| type | string | The type of the panel for which the Cost Management data should be displayed. Currently only `actualcosts` is supported. | Yes |
| scope | string | The scope for the costs data. This could be the name of a resource group or `All`. The default value is `All`. | No |

### Kubernetes Services

| Field | Type | Description | Required |
Expand Down
28 changes: 18 additions & 10 deletions plugins/azure/costmanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,11 @@ import (

func (router *Router) getActualCosts(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
timeframe := r.URL.Query().Get("timeframe")
scope := r.URL.Query().Get("scope")
timeStart := r.URL.Query().Get("timeStart")
timeEnd := r.URL.Query().Get("timeEnd")

log.Debug(r.Context(), "Get actual costs parameters.", zap.String("name", name), zap.String("timeframe", timeframe), zap.String("scope", scope))

timeframeParsed, err := strconv.Atoi(timeframe)
if err != nil {
log.Error(r.Context(), "Invalid timeframe parameter.", zap.Error(err))
errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid timeframe parameter")
return
}
log.Debug(r.Context(), "Get actual costs parameters.", zap.String("name", name), zap.String("scope", scope), zap.String("timeStart", timeStart), zap.String("timeEnd", timeEnd))

i := router.getInstance(name)
if i == nil {
Expand All @@ -33,7 +27,21 @@ func (router *Router) getActualCosts(w http.ResponseWriter, r *http.Request) {
return
}

costUsage, err := i.CostManagementClient().GetActualCost(r.Context(), timeframeParsed, scope)
parsedTimeStart, err := strconv.ParseInt(timeStart, 10, 64)
if err != nil {
log.Error(r.Context(), "Could not parse start time.", zap.Error(err))
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse start time")
return
}

parsedTimeEnd, err := strconv.ParseInt(timeEnd, 10, 64)
if err != nil {
log.Error(r.Context(), "Could not parse end time.", zap.Error(err))
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse end time")
return
}

costUsage, err := i.CostManagementClient().GetActualCost(r.Context(), scope, parsedTimeStart, parsedTimeEnd)
if err != nil {
log.Error(r.Context(), "Could not query cost usage.", zap.Error(err))
errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not query cost usage")
Expand Down
26 changes: 19 additions & 7 deletions plugins/azure/costmanagement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,22 @@ func TestGetActualCosts(t *testing.T) {
do func(router Router, w *httptest.ResponseRecorder, req *http.Request)
}{
{
name: "invalid timeframe parameter",
name: "invalid instance name",
url: "/invalidname/costmanagement/actualcosts",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Could not find instance name\"}\n",
prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("azure")
},
do: func(router Router, w *httptest.ResponseRecorder, req *http.Request) {
router.getActualCosts(w, req)
},
},
{
name: "invalid start time",
url: "/azure/costmanagement/actualcosts",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Invalid timeframe parameter\"}\n",
expectedBody: "{\"error\":\"Could not parse start time: strconv.ParseInt: parsing \\\"\\\": invalid syntax\"}\n",
prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("azure")
},
Expand All @@ -36,10 +48,10 @@ func TestGetActualCosts(t *testing.T) {
},
},
{
name: "invalid instance name",
url: "/invalidname/costmanagement/actualcosts?timeframe=3600",
name: "invalid end time",
url: "/azure/costmanagement/actualcosts?timeStart=1",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Could not find instance name\"}\n",
expectedBody: "{\"error\":\"Could not parse end time: strconv.ParseInt: parsing \\\"\\\": invalid syntax\"}\n",
prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("azure")
},
Expand All @@ -51,11 +63,11 @@ func TestGetActualCosts(t *testing.T) {
// panic: interface conversion: *costmanagement.MockClient is not costmanagement.Client: missing method GetActualCost
// {
// name: "could not get actual costs",
// url: "/azure/costmanagement/actualcosts?timeframe=3600",
// url: "/azure/costmanagement/actualcosts?timeStart=1&timeEnd=1",
// expectedStatusCode: http.StatusBadRequest,
// expectedBody: "{\"error\":\"Could not find instance name\"}\n",
// prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) {
// mockClient.On("GetActualCost", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("could not get costs"))
// mockClient.On("GetActualCost", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("could not get costs"))

// mockInstance.On("GetName").Return("azure")
// mockInstance.On("CostManagementClient").Return(mockClient)
Expand Down
57 changes: 3 additions & 54 deletions plugins/azure/pkg/instance/costmanagement/costmanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ package costmanagement
import (
"context"
"fmt"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/date"
)

// Client is the interface for a client to interact with the Azure cost management api.
type Client interface {
GetActualCost(ctx context.Context, timeframe int, scope string) (costmanagement.QueryResult, error)
GetActualCost(ctx context.Context, scope string, timeStart, timeEnd int64) (costmanagement.QueryResult, error)
}

type client struct {
Expand All @@ -22,7 +19,7 @@ type client struct {
}

// GetActualCost query the actual costs for the configured subscription and given timeframe grouped by resourceGroup
func (c *client) GetActualCost(ctx context.Context, timeframe int, scope string) (costmanagement.QueryResult, error) {
func (c *client) GetActualCost(ctx context.Context, scope string, timeStart, timeEnd int64) (costmanagement.QueryResult, error) {
var queryScope string
var subscriptionScope bool

Expand All @@ -33,55 +30,7 @@ func (c *client) GetActualCost(ctx context.Context, timeframe int, scope string)
queryScope = fmt.Sprintf("subscriptions/%s/resourceGroups/%s", c.subscriptionID, scope)
}

return c.queryClient.Usage(ctx, queryScope, buildQueryParams(timeframe, subscriptionScope))
}

func buildQueryParams(timeframe int, subscriptionScope bool) costmanagement.QueryDefinition {
agg := make(map[string]*costmanagement.QueryAggregation)
tc := costmanagement.QueryAggregation{
Name: to.StringPtr("Cost"),
Function: costmanagement.FunctionTypeSum,
}
agg["totalCost"] = &tc

var grouping []costmanagement.QueryGrouping
if subscriptionScope {
grouping = []costmanagement.QueryGrouping{
{
Type: costmanagement.QueryColumnTypeDimension,
Name: to.StringPtr("resourceGroup"),
},
}
} else {
grouping = []costmanagement.QueryGrouping{
{
Type: costmanagement.QueryColumnTypeDimension,
Name: to.StringPtr("ServiceName"),
},
}
}

ds := costmanagement.QueryDataset{
Granularity: "None",
Configuration: nil,
Aggregation: agg,
Grouping: &grouping,
Filter: nil,
}

now := date.Time{Time: time.Now()}
from := date.Time{Time: now.AddDate(0, 0, timeframe*-1)}
tp := costmanagement.QueryTimePeriod{
From: &from,
To: &now,
}

return costmanagement.QueryDefinition{
Type: costmanagement.ExportTypeActualCost,
Timeframe: costmanagement.TimeframeTypeCustom,
TimePeriod: &tp,
Dataset: &ds,
}
return c.queryClient.Usage(ctx, queryScope, buildQueryParams(subscriptionScope, timeStart, timeEnd))
}

// New returns a new client to interact with the cost management API.
Expand Down
14 changes: 7 additions & 7 deletions plugins/azure/pkg/instance/costmanagement/costmanagement_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions plugins/azure/pkg/instance/costmanagement/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package costmanagement

import (
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement"
"github.com/Azure/go-autorest/autorest/date"
)

func buildQueryParams(subscriptionScope bool, timeStart, timeEnd int64) costmanagement.QueryDefinition {
agg := make(map[string]*costmanagement.QueryAggregation)
tc := costmanagement.QueryAggregation{
Name: to.StringPtr("Cost"),
Function: costmanagement.FunctionTypeSum,
}
agg["totalCost"] = &tc

var grouping []costmanagement.QueryGrouping
if subscriptionScope {
grouping = []costmanagement.QueryGrouping{
{
Type: costmanagement.QueryColumnTypeDimension,
Name: to.StringPtr("resourceGroup"),
},
}
} else {
grouping = []costmanagement.QueryGrouping{
{
Type: costmanagement.QueryColumnTypeDimension,
Name: to.StringPtr("ServiceName"),
},
}
}

ds := costmanagement.QueryDataset{
Granularity: "None",
Configuration: nil,
Aggregation: agg,
Grouping: &grouping,
Filter: nil,
}

now := date.Time{Time: time.Unix(timeEnd, 0)}
from := date.Time{Time: time.Unix(timeStart, 0)}
tp := costmanagement.QueryTimePeriod{
From: &from,
To: &now,
}

return costmanagement.QueryDefinition{
Type: costmanagement.ExportTypeActualCost,
Timeframe: costmanagement.TimeframeTypeCustom,
TimePeriod: &tp,
Dataset: &ds,
}
}
28 changes: 28 additions & 0 deletions plugins/azure/pkg/instance/costmanagement/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package costmanagement

import (
"testing"
"time"

"github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement"
"github.com/Azure/go-autorest/autorest/date"
"github.com/stretchr/testify/require"
)

func TestBuildQueryParams(t *testing.T) {
t.Run("subscription scope", func(t *testing.T) {
actualQueryParams := buildQueryParams(true, 0, 1)

require.Equal(t, costmanagement.ExportTypeActualCost, actualQueryParams.Type)
require.Equal(t, costmanagement.TimeframeTypeCustom, actualQueryParams.Timeframe)
require.Equal(t, costmanagement.QueryTimePeriod{From: &date.Time{Time: time.Unix(0, 0)}, To: &date.Time{Time: time.Unix(1, 0)}}, *actualQueryParams.TimePeriod)
})

t.Run("not subscription scope", func(t *testing.T) {
actualQueryParams := buildQueryParams(false, 0, 1)

require.Equal(t, costmanagement.ExportTypeActualCost, actualQueryParams.Type)
require.Equal(t, costmanagement.TimeframeTypeCustom, actualQueryParams.Timeframe)
require.Equal(t, costmanagement.QueryTimePeriod{From: &date.Time{Time: time.Unix(0, 0)}, To: &date.Time{Time: time.Unix(1, 0)}}, *actualQueryParams.TimePeriod)
})
}
Loading