Skip to content

Commit

Permalink
Merge pull request #17 from tetsuya28/i18y
Browse files Browse the repository at this point in the history
i18y
  • Loading branch information
tetsuya28 authored Jan 1, 2024
2 parents c2f5ada + deb29a6 commit 4c6fec0
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 27 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
## What is aws_cost_usage
Notify daily cost usage of an AWS account to slack channel.

![](./docs/notification.jpg)

## How to use this
You can deploy with Terraform resources on your AWS account.
### Execution
#### Lambda
You can deploy with Terraform resources on your AWS account with AWS Lambda.

```hcl
terraform {
Expand All @@ -28,6 +32,15 @@ module "cost" {
}
```

#### Others
You can download binary from GitHub Release.

### Support multi languages
- English
- Set `LANGUAGE=en`
- Japanese
- Set `LANGUAGE=ja`

## Development
- Init
```
Expand All @@ -38,3 +51,8 @@ cp .env{.sample,}
```
make run
```

## Known issues
### Got daily cost as $0.000
If you set CloudWatch Events Schedule near AM 0:00 in UTC, AWS has not reflect daily cost yet.
So, you need to set the schedule for more later.
17 changes: 17 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package config

import "github.com/kelseyhightower/envconfig"

type Config struct {
SlackToken string `required:"true" envconfig:"SLACK_TOKEN"`
SlackChannel string `required:"true" envconfig:"SLACK_CHANNEL"`
Language string `required:"true" envconfig:"LANGUAGE" default:"ja"`
}

func New() (Config, error) {
config := Config{}
if err := envconfig.Process("", &config); err != nil {
return Config{}, err
}
return config, nil
}
Binary file added docs/notification.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ require (
github.com/slack-go/slack v0.9.3
github.com/stretchr/testify v1.8.4
github.com/ucpr/mongo-streamer v0.0.4
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -48,13 +50,11 @@ require (
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/api v0.128.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.56.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
72 changes: 72 additions & 0 deletions i18y/i18y.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package i18y

import (
"embed"
_ "embed"
"fmt"
"io/fs"
"path/filepath"
"strings"

"golang.org/x/text/language"
"golang.org/x/text/message"
"gopkg.in/yaml.v3"
)

const (
LanguagesDir = "languages"
)

var (
languages = []language.Tag{
language.Japanese,
language.English,
}
//go:embed languages/*.yaml
configByte embed.FS
)

type Messages map[string]map[string]string

func Init() error {
files, err := configByte.ReadDir(LanguagesDir)
if err != nil {
return err
}

for _, file := range files {
data, err := fs.ReadFile(configByte, fmt.Sprintf("%s/%s", LanguagesDir, file.Name()))
if err != nil {
return err
}

var m map[string]string
err = yaml.Unmarshal(data, &m)
if err != nil {
return err
}

// Make language tag from file name
l := language.Make(strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())))
for k, v := range m {
err = message.SetString(l, k, v)
if err != nil {
return err
}
}
}

return nil
}

func Translate(acceptLanguage string, msg string, args ...interface{}) string {
t, _, err := language.ParseAcceptLanguage(acceptLanguage)
if err != nil {
return msg
}

matcher := language.NewMatcher(languages)
tag, _, _ := matcher.Match(t...)
p := message.NewPrinter(tag)
return p.Sprintf(msg, args...)
}
38 changes: 38 additions & 0 deletions i18y/i18y_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package i18y

import (
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
)

func TestTranslate(t *testing.T) {
err := Init()
assert.NoError(t, err)

tests := []struct {
name string
acceptLanguage string
key string
out string
}{
{
name: "japanese",
acceptLanguage: language.Japanese.String(),
key: "cost",
out: "料金",
},
{
name: "english",
acceptLanguage: language.English.String(),
key: "cost",
out: "Cost",
},
}

for _, tt := range tests {
out := Translate(tt.acceptLanguage, tt.key)
assert.Equal(t, tt.out, out)
}
}
3 changes: 3 additions & 0 deletions i18y/languages/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
title: "%s cost of %s is `$%.3f`"
cost: Cost
usage: Usage
3 changes: 3 additions & 0 deletions i18y/languages/ja.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
title: "%s : %s のコスト : `$%.3f`"
cost: 料金
usage: 利用量
60 changes: 36 additions & 24 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import (

"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/service/costexplorer"
"github.com/kelseyhightower/envconfig"
"github.com/slack-go/slack"
"github.com/tetsuya28/aws-cost-report/config"
"github.com/tetsuya28/aws-cost-report/external"
"github.com/tetsuya28/aws-cost-report/i18y"
"github.com/ucpr/mongo-streamer/pkg/log"
)

type Config struct {
SlackToken string `required:"true" envconfig:"SLACK_TOKEN"`
SlackChannel string `required:"true" envconfig:"SLACK_CHANNEL"`
}
var (
Language string
)

type DailyCost struct {
Total float64
Expand All @@ -37,14 +37,25 @@ func main() {
}

func handler() error {
config := Config{}
if err := envconfig.Process("", &config); err != nil {
panic(err)
cfg, err := config.New()
if err != nil {
log.Warn("failed to new config, err=%w", err)
return err
}
slk := external.NewSlack(config.SlackToken)

Language = cfg.Language

err = i18y.Init()
if err != nil {
log.Warn("failed to init i18y, err=%w", err)
return err
}

slk := external.NewSlack(cfg.SlackToken)

result, err := external.GetCost()
if err != nil {
log.Warn("failed to get cost, err=%w", err)
return err
}

Expand Down Expand Up @@ -74,7 +85,7 @@ func handler() error {

dailyCost.Services[serviceName] = c

// 日次合計を計算する
// Sum total daily cost
dailyCost.Total += c.CostAmount
}

Expand All @@ -83,17 +94,19 @@ func handler() error {

fullName, err := external.GetAccountFullName(context.Background())
if err != nil {
log.Warn("failed to get account info, err=%w", err)
return err
}

now := time.Now()
yesterday := now.AddDate(0, 0, -1)
text := fmt.Sprintf("%s の %s コスト\n合計金額: $%.3f", fullName, yesterday.Format("2006-01-02"), cost[0].Total)
text := i18y.Translate(Language, "title", fullName, yesterday.Format("2006-01-02"), cost[1].Total)
option := slack.MsgOptionText(text, false)

attachments := toAttachment(cost)
err = slk.PostMessage(config.SlackChannel, option, slack.MsgOptionAttachments(attachments...))
err = slk.PostMessage(cfg.SlackChannel, option, slack.MsgOptionAttachments(attachments...))
if err != nil {
log.Warn("failed to post message to Slack, err=%w", err)
return err
}

Expand Down Expand Up @@ -146,9 +159,9 @@ func toCost(result *costexplorer.Group) (ServiceDetail, error) {
}

func toAttachment(cost []DailyCost) []slack.Attachment {
// 一昨日、昨日のコスト比較なので 2 つのみ
// [0] : 一昨日、 [1] : 昨日
// Just day before yesterday and yesterday
if len(cost) != 2 {
log.Warn("cost length is not 2")
return nil
}

Expand All @@ -162,30 +175,29 @@ func toAttachment(cost []DailyCost) []slack.Attachment {
diff := (detail.CostAmount / before.CostAmount) * 100

if !math.IsNaN(diff) {
priceDiffStatement += " ( 前日比 : "

// 前日よりも高くなってたら赤色にする
diffMark := ""
// Set red color if diff is over 100%
if diff == 100 {
color = "#ffffff"
priceDiffStatement += ""
} else if diff > 100 {
color = "#ff0000"
priceDiffStatement += "📈 "
diffMark = "📈"
} else {
color = "#0000ff"
priceDiffStatement += "📉 "
diffMark = "📉"
}

priceDiffStatement += fmt.Sprintf(
"%.1f%% )",
priceDiffStatement = fmt.Sprintf(
" ( %s %.1f%% )",
diffMark,
diff,
)
}
}

fields := []slack.AttachmentField{
{
Title: "料金",
Title: i18y.Translate(Language, "cost"),
Value: fmt.Sprintf(
"%.3f%s%s",
detail.CostAmount,
Expand All @@ -195,7 +207,7 @@ func toAttachment(cost []DailyCost) []slack.Attachment {
Short: true,
},
{
Title: "使用量",
Title: i18y.Translate(Language, "usage"),
Value: fmt.Sprintf("%.3f%s", detail.UsageAmount, detail.UsageUnit),
Short: true,
},
Expand Down

0 comments on commit 4c6fec0

Please sign in to comment.