soracom_summary 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/soracom_summary +5 -0
- data/lib/soracom_summary.rb +8 -0
- data/lib/soracom_summary/api_client.rb +218 -0
- data/lib/soracom_summary/billing.rb +62 -0
- data/lib/soracom_summary/cli.rb +145 -0
- data/lib/soracom_summary/const.rb +6 -0
- data/lib/soracom_summary/http_client.rb +100 -0
- data/lib/soracom_summary/subscriber.rb +88 -0
- data/lib/soracom_summary/traffic.rb +90 -0
- data/lib/soracom_summary/version.rb +3 -0
- data/soracom_summary.gemspec +28 -0
- metadata +138 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: e0fdd3af68a2563ceb97e1276db7a114c1d51c93
|
|
4
|
+
data.tar.gz: 1b70ced95d1f6adeee8dc4975af46f3f514d5fc2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d3fe57db7e72cb1eb2bed0f1e4c324ef44534a35f762819492d6ed14537f4fe8a4598c0bdb71bb5814ed12ccd968f674c4944752d8b40ce69880b18ca58c8a7a
|
|
7
|
+
data.tar.gz: f8ec6b884d6305a7228544d67005b7fc17f4c854972f7f4e0f4b09ad5b26e7a9591d4f5b37bb6d27a624c9709c27d7407ed7aa2fe25b8ab89d69e62da0fdcc95
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at funahara@hioki.co.jp. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
soracom_summary (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
debase (0.2.4.1)
|
|
10
|
+
debase-ruby_core_source (>= 0.10.2)
|
|
11
|
+
debase-ruby_core_source (0.10.7)
|
|
12
|
+
diff-lcs (1.3)
|
|
13
|
+
rake (10.5.0)
|
|
14
|
+
rspec (3.5.0)
|
|
15
|
+
rspec-core (~> 3.5.0)
|
|
16
|
+
rspec-expectations (~> 3.5.0)
|
|
17
|
+
rspec-mocks (~> 3.5.0)
|
|
18
|
+
rspec-core (3.5.4)
|
|
19
|
+
rspec-support (~> 3.5.0)
|
|
20
|
+
rspec-expectations (3.5.0)
|
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
22
|
+
rspec-support (~> 3.5.0)
|
|
23
|
+
rspec-mocks (3.5.0)
|
|
24
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
25
|
+
rspec-support (~> 3.5.0)
|
|
26
|
+
rspec-support (3.5.0)
|
|
27
|
+
ruby-debug-ide (0.7.0)
|
|
28
|
+
rake (>= 0.8.1)
|
|
29
|
+
|
|
30
|
+
PLATFORMS
|
|
31
|
+
ruby
|
|
32
|
+
|
|
33
|
+
DEPENDENCIES
|
|
34
|
+
bundler (~> 2.0)
|
|
35
|
+
debase
|
|
36
|
+
rake (~> 10.0)
|
|
37
|
+
rspec (~> 3.0)
|
|
38
|
+
ruby-debug-ide
|
|
39
|
+
soracom_summary!
|
|
40
|
+
|
|
41
|
+
BUNDLED WITH
|
|
42
|
+
2.0.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Ippei Funahara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SORACOM Summary
|
|
2
|
+
SORACOM Summary は、ソラコム株式会社の提供する回線サービスSORACOMの使用状況をWeb APIを用いてスクレイピングし、
|
|
3
|
+
取得した使用状況をSORACOM Harvestにアップロードすることにより可視化するツールです。
|
|
4
|
+
|
|
5
|
+
## インストール
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$ gem install soracom_summary
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
ライブラリ及びsoracom_summaryコマンドがインストールされます。
|
|
12
|
+
|
|
13
|
+
## 使用方法
|
|
14
|
+
|
|
15
|
+
### ユーザー認証情報の設定
|
|
16
|
+
環境変数にSAMユーザーの認証キーIDと認証キーシークレットを設定します。
|
|
17
|
+
SAMユーザーの説明はこちら
|
|
18
|
+
https://dev.soracom.io/jp/start/sam/
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
$ export SORACOM_AUTH_KEY_ID='keyId-XXXX'
|
|
22
|
+
$ export SORACOM_AUTH_KEY='secret-XXXX'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
SAMユーザーには、以下の権限が必要です。上記ドキュメントを参照して、権限を設定します。
|
|
26
|
+
- Subscriber:listSubscribers
|
|
27
|
+
- Subscriber:listSessionEvents
|
|
28
|
+
- Log:getLogs
|
|
29
|
+
- Billing:exportBilling
|
|
30
|
+
- Stats:exportAirStats
|
|
31
|
+
|
|
32
|
+
### デバイス認証情報の設定
|
|
33
|
+
|
|
34
|
+
環境変数にHarvestにアップロードするためのSORACOM InventoryのデバイスIDとデバイスシークレットを設定します。
|
|
35
|
+
デバイスIDとデバイスシークレットについての説明はこちら
|
|
36
|
+
https://dev.soracom.io/jp/start/inventory_harvest_with_keys/
|
|
37
|
+
|
|
38
|
+
集計情報用(必須)
|
|
39
|
+
```
|
|
40
|
+
$ export SORACOM_SUMMARY_DEVICE_ID='d-XXXX'
|
|
41
|
+
$ export SORACOM_SUMMARY_DEVICE_SECRET='XXXX'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
セッション情報の分布用(オプション)
|
|
45
|
+
```
|
|
46
|
+
$ export SORACOM_SESSION_DEVICE_ID='d-XXXX'
|
|
47
|
+
$ export SORACOM_SESSION_DEVICE_SECRET='XXXX'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
請求情報の分布用(オプション)
|
|
51
|
+
```
|
|
52
|
+
$ export SORACOM_BILLING_DEVICE_ID='d-XXXX'
|
|
53
|
+
$ export SORACOM_BILLING_DEVICE_SECRET='XXXX'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
通信量の分布用(オプション)
|
|
57
|
+
```
|
|
58
|
+
$ export SORACOM_TRAFFIC_DEVICE_ID='d-XXXX'
|
|
59
|
+
$ export SORACOM_TRAFFIC_DEVICE_SECRET='XXXX'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 使用方法
|
|
63
|
+
以下のコマンドを実行すると1日前の集計をします
|
|
64
|
+
```
|
|
65
|
+
$ soracom_summary
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
セッション情報を取得するには時間がかかるため、--sessionオプションで明示的に指定する必要があります
|
|
69
|
+
```
|
|
70
|
+
$ soracom_summary --session
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
指定したタグごとにSIMの枚数を集計する場合、--tagオプションを使用します
|
|
74
|
+
```
|
|
75
|
+
$ soracom_summary --tag environment
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
過去にさかのぼって集計する場合、--from、--toオプションを使用します
|
|
79
|
+
```
|
|
80
|
+
$ soracom_summary --from 2019-11-22 --to 2019-11-24
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
実行後、データはSORACOM Harvestにアップロードされ、SORACOM Lagoonにて可視化することが出来ます。
|
|
84
|
+
|
|
85
|
+
Lagoonでの可視化は以下を参照して設定します。
|
|
86
|
+
https://dev.soracom.io/jp/start/lagoon-panel/
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "soracom_summary"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/soracom_summary
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require "soracom_summary/version"
|
|
2
|
+
require "soracom_summary/const"
|
|
3
|
+
require "soracom_summary/cli"
|
|
4
|
+
require 'soracom_summary/api_client'
|
|
5
|
+
require 'soracom_summary/http_client'
|
|
6
|
+
require 'soracom_summary/subscriber'
|
|
7
|
+
require 'soracom_summary/billing'
|
|
8
|
+
require 'soracom_summary/traffic'
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'csv'
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
module SoracomSummary
|
|
7
|
+
class ApiClient
|
|
8
|
+
BASE_URL = 'https://api.soracom.io/v1/'
|
|
9
|
+
def initialize(
|
|
10
|
+
auth_key_id: ENV['SORACOM_AUTH_KEY_ID'],
|
|
11
|
+
auth_key: ENV['SORACOM_AUTH_KEY']
|
|
12
|
+
)
|
|
13
|
+
@logger = Logger.new(STDERR)
|
|
14
|
+
@logger.level = ENV['SORACOM_SUMMARY_DEBUG'] ? Logger::DEBUG : Logger::WARN
|
|
15
|
+
@http_client = HttpClient.new(base_url: BASE_URL)
|
|
16
|
+
@token = authenticate(auth_key_id, auth_key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_subscribers
|
|
20
|
+
@logger.debug('start get subscribers')
|
|
21
|
+
subscribers = []
|
|
22
|
+
path = 'subscribers'
|
|
23
|
+
loop do
|
|
24
|
+
response = http_access_with_token('GET', path)
|
|
25
|
+
response_subscribers = response['body'].map do |subscriber|
|
|
26
|
+
status = subscriber['status']
|
|
27
|
+
if status == 'active'
|
|
28
|
+
status = subscriber['sessionStatus']['online'] ? 'online' : 'offline'
|
|
29
|
+
end
|
|
30
|
+
Subscriber.new(
|
|
31
|
+
imsi: subscriber['imsi'],
|
|
32
|
+
created_at: subscriber['createdAt'],
|
|
33
|
+
tags: subscriber['tags'],
|
|
34
|
+
status: status
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
subscribers.concat(response_subscribers)
|
|
38
|
+
next_link = get_next_link(response['headers'])
|
|
39
|
+
break if next_link.nil?
|
|
40
|
+
path = next_link
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@logger.debug('finish get subscribers')
|
|
44
|
+
subscribers
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_sessions(imsi, from, to)
|
|
48
|
+
@logger.debug("start get sessions #{imsi}")
|
|
49
|
+
from_timestamp_ms = from.to_i * 1000
|
|
50
|
+
to_timestamp_ms = (to.to_i + Const::ONE_DAY_SEC) * 1000
|
|
51
|
+
sessions = []
|
|
52
|
+
path = "subscribers/#{imsi}/events/sessions"
|
|
53
|
+
loop do
|
|
54
|
+
response = http_access_with_token('GET', path)
|
|
55
|
+
break if response['body'].empty?
|
|
56
|
+
|
|
57
|
+
# Modifiedなど接続/切断以外のイベントは排除
|
|
58
|
+
response_sessions = response['body'].select { |session| session['event'] == 'Created' || session['event'] == 'Deleted' }
|
|
59
|
+
sessions.concat(response_sessions)
|
|
60
|
+
|
|
61
|
+
# fromの時間に入る直前までのセッションが取得できていたら終了する
|
|
62
|
+
break if sessions.length > 0 && sessions.last['time'] < from_timestamp_ms
|
|
63
|
+
|
|
64
|
+
next_link = get_next_link(response['headers'])
|
|
65
|
+
break if next_link.nil?
|
|
66
|
+
path = next_link
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# to以降のセッションは除外
|
|
70
|
+
sessions = sessions.reject { |session| session['time'] >= to_timestamp_ms }
|
|
71
|
+
|
|
72
|
+
# from以前のセッションは最後のイベントをfromの開始時のイベントとして残し、他は除外する
|
|
73
|
+
before_from_sessions = sessions.select { |session| session['time'] < from_timestamp_ms }
|
|
74
|
+
sessions = sessions.reject { |session| session['time'] < from_timestamp_ms }
|
|
75
|
+
if before_from_sessions.length > 0
|
|
76
|
+
sessions.push(before_from_sessions.first.merge({'time' => from_timestamp_ms }))
|
|
77
|
+
else
|
|
78
|
+
sessions.push({'event' => 'Deleted', 'time' => from_timestamp_ms} )
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@logger.debug("finish get sessions #{imsi}")
|
|
82
|
+
sessions
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# エラーログを取得する
|
|
86
|
+
# from、toの指定が効いていない?
|
|
87
|
+
def get_logs(from, to)
|
|
88
|
+
@logger.debug("start get logs")
|
|
89
|
+
|
|
90
|
+
from_timestamp = from.to_i
|
|
91
|
+
to_timestamp = to.to_i + Const::ONE_DAY_SEC
|
|
92
|
+
logs = []
|
|
93
|
+
path = "logs?from=#{from_timestamp}&to=#{to_timestamp}"
|
|
94
|
+
|
|
95
|
+
loop do
|
|
96
|
+
response = http_access_with_token('GET', path)
|
|
97
|
+
break if response['body'].empty?
|
|
98
|
+
logs.concat(response['body'])
|
|
99
|
+
next_link = get_next_link(response['headers'])
|
|
100
|
+
break if next_link.nil?
|
|
101
|
+
path = next_link
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@logger.debug("finish get logs")
|
|
105
|
+
logs = logs.select { |log| log['time'] >= from_timestamp * 1000 && log['time'] < to_timestamp * 1000 }
|
|
106
|
+
logs
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def get_billing(target_months)
|
|
110
|
+
@logger.debug("start get billings")
|
|
111
|
+
billings = []
|
|
112
|
+
|
|
113
|
+
target_months.each do |month|
|
|
114
|
+
export_response = http_access_with_token('POST', "bills/#{month}/export?export_mode=sync")
|
|
115
|
+
export_url = export_response['body']['url']
|
|
116
|
+
|
|
117
|
+
# 1文字目はBOMのため除外する
|
|
118
|
+
csv_text = HttpClient.download(export_url)
|
|
119
|
+
csv_text.slice!(0)
|
|
120
|
+
CSV.parse(csv_text, headers: true ) do |ln|
|
|
121
|
+
billing = Billing.new(
|
|
122
|
+
imsi: ln['imsi'],
|
|
123
|
+
device_id: ln['deviceId'],
|
|
124
|
+
date: ln['date'],
|
|
125
|
+
bill_item_name: ln['billItemName'],
|
|
126
|
+
amount: ln['amount']
|
|
127
|
+
)
|
|
128
|
+
billings.push(billing)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@logger.debug("finish get billings")
|
|
133
|
+
billings
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_traffic(target_months)
|
|
137
|
+
@logger.debug("start get traffics")
|
|
138
|
+
|
|
139
|
+
traffics = []
|
|
140
|
+
|
|
141
|
+
target_months.each do |month|
|
|
142
|
+
from = Time.parse(month + '01')
|
|
143
|
+
# 翌月の1日 - 1
|
|
144
|
+
to = from + (32 - (from + 31 * Const::ONE_DAY_SEC).mday) * Const::ONE_DAY_SEC - 1
|
|
145
|
+
body = { 'from' => from.to_i, 'to' => to.to_i, 'period' => 'day'}
|
|
146
|
+
export_response = http_access_with_token('POST', "stats/air/operators/#{@token['operatorId']}/export?export_mode=sync", nil, body)
|
|
147
|
+
export_url = export_response['body']['url']
|
|
148
|
+
|
|
149
|
+
# 1文字目はBOMのため除外する
|
|
150
|
+
csv_text = HttpClient.download(export_url)
|
|
151
|
+
csv_text.slice!(0)
|
|
152
|
+
CSV.parse(csv_text, headers: true ) do |ln|
|
|
153
|
+
traffic = Traffic.new(
|
|
154
|
+
imsi: ln['imsi'],
|
|
155
|
+
date: ln['date'],
|
|
156
|
+
type: ln['type'],
|
|
157
|
+
upload_byte_size_total: ln['uploadByteSizeTotal'],
|
|
158
|
+
download_byte_size_total: ln['downloadByteSizeTotal'],
|
|
159
|
+
)
|
|
160
|
+
traffics.push(traffic)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@logger.debug("finish get traffics")
|
|
165
|
+
traffics
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def upload_harvest(device_id, device_secret, time, data)
|
|
169
|
+
@logger.debug("start upload harvest #{device_id}")
|
|
170
|
+
|
|
171
|
+
# device_secretはURLセーフは文字列ではないためエンコードが必要
|
|
172
|
+
path = "devices/#{device_id}/publish?device_secret=#{URI.encode_www_form_component(device_secret)}"
|
|
173
|
+
headers = nil
|
|
174
|
+
unless time.nil?
|
|
175
|
+
headers = { 'X-SORACOM-TIMESTAMP' => (time.to_i * 1000).to_s }
|
|
176
|
+
end
|
|
177
|
+
result = @http_client.post(path, headers, data)
|
|
178
|
+
|
|
179
|
+
@logger.debug("finish upload harvest #{device_id}")
|
|
180
|
+
result
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# 認証IDと認証キーで認証する
|
|
186
|
+
def authenticate(auth_key_id, auth_key)
|
|
187
|
+
@logger.debug("start authenticate")
|
|
188
|
+
|
|
189
|
+
request_body = { authKeyId: auth_key_id, authKey: auth_key }
|
|
190
|
+
token_response = @http_client.post("auth", {}, request_body)
|
|
191
|
+
|
|
192
|
+
@logger.debug("finish authenticate")
|
|
193
|
+
token_response['body']
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# トークン情報付でアクセスする
|
|
197
|
+
def http_access_with_token(method, path, headers = nil, body = nil)
|
|
198
|
+
headers = {} if headers.nil?
|
|
199
|
+
headers.merge!({
|
|
200
|
+
'X-SORACOM-API-KEY' => @token['apiKey'],
|
|
201
|
+
'X-SORACOM-TOKEN' => @token['token']
|
|
202
|
+
})
|
|
203
|
+
case method
|
|
204
|
+
when 'GET' then @http_client.get(path, headers)
|
|
205
|
+
when 'POST' then @http_client.post(path, headers, body)
|
|
206
|
+
else nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def get_next_link(headers)
|
|
211
|
+
return nil unless headers.key?('link')
|
|
212
|
+
link_array = headers['link'].split(/\s*,\s*/)
|
|
213
|
+
next_link_text = link_array.find { |link| link.include?('rel=next') }
|
|
214
|
+
return nil if next_link_text.nil?
|
|
215
|
+
next_link_text[(next_link_text.index('<') + 1)...(next_link_text.index('>'))]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SoracomSummary
|
|
2
|
+
class Billing
|
|
3
|
+
attr_accessor :imsi, :device_id, :date, :bill_item_name, :amount
|
|
4
|
+
class << self
|
|
5
|
+
def summary(billings, time)
|
|
6
|
+
month_text = time.strftime('%Y%m')
|
|
7
|
+
day = time.strftime('%Y%m%d').to_i
|
|
8
|
+
|
|
9
|
+
# 対象期間の請求をフィルタする
|
|
10
|
+
month_billings = billings.select { |billing| billing.date[0, 6] == month_text && billing.date.to_i <= day }
|
|
11
|
+
day_billings = billings.select { |billing| billing.date.to_i == day }
|
|
12
|
+
billing_summary = { 'billings-month-total' => sum(month_billings), 'billings-day-total' => sum(day_billings) }
|
|
13
|
+
|
|
14
|
+
# 請求を項目ごとにグループ化する
|
|
15
|
+
month_billings_by_item = month_billings
|
|
16
|
+
.group_by { |billing| billing.bill_item_name }
|
|
17
|
+
.map { |item, group| ["billings-month-item-#{item}", sum(group) ]}
|
|
18
|
+
.to_h
|
|
19
|
+
billing_summary.merge!(month_billings_by_item)
|
|
20
|
+
|
|
21
|
+
day_billings_by_item = day_billings
|
|
22
|
+
.group_by { |billing| billing.bill_item_name }
|
|
23
|
+
.map { |item, group| ["billings-day-item-#{item}", sum(group) ]}
|
|
24
|
+
.to_h
|
|
25
|
+
billing_summary.merge!(day_billings_by_item)
|
|
26
|
+
|
|
27
|
+
billing_summary
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def group_by_origin(billings, time)
|
|
31
|
+
day = time.strftime('%Y%m%d')
|
|
32
|
+
|
|
33
|
+
# 対象期間の請求をフィルタする
|
|
34
|
+
day_billings = billings.select { |billing| billing.date == day }
|
|
35
|
+
|
|
36
|
+
day_billings_by_origin = day_billings
|
|
37
|
+
.group_by { |billing| billing.imsi || billing.device_id || billing.bill_item_name }
|
|
38
|
+
.map { |origin, group| [origin, sum(group)] }
|
|
39
|
+
.to_h
|
|
40
|
+
|
|
41
|
+
day_billings_by_origin
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sum(billings)
|
|
45
|
+
billings.inject(0.0) { |sum, billing| sum + billing.amount.to_f }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(
|
|
50
|
+
imsi:,
|
|
51
|
+
device_id:,
|
|
52
|
+
date:,
|
|
53
|
+
bill_item_name:,
|
|
54
|
+
amount:)
|
|
55
|
+
@imsi = imsi
|
|
56
|
+
@device_id = device_id
|
|
57
|
+
@date = date
|
|
58
|
+
@bill_item_name = bill_item_name
|
|
59
|
+
@amount = amount
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'date'
|
|
3
|
+
|
|
4
|
+
module SoracomSummary
|
|
5
|
+
class CLI
|
|
6
|
+
class << self
|
|
7
|
+
def run(argv)
|
|
8
|
+
# ソラコム内では時間はUTCで表現されるため、タイムゾーンをUTCにしておく
|
|
9
|
+
ENV['TZ'] = 'UTC'
|
|
10
|
+
parse!(argv)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def parse!(argv)
|
|
16
|
+
options = {}
|
|
17
|
+
parser = create_parser(options)
|
|
18
|
+
parser.order!(argv)
|
|
19
|
+
|
|
20
|
+
if options.key?('from')
|
|
21
|
+
@from = Time.parse(options['from'])
|
|
22
|
+
else
|
|
23
|
+
@from = Date.today.to_time - Const::ONE_DAY_SEC
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if options.key?('to')
|
|
27
|
+
@to = Time.parse(options['to']) + Const::ONE_DAY_SEC
|
|
28
|
+
else
|
|
29
|
+
@to = Date.today.to_time
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if @from >= @to
|
|
33
|
+
raise ArgumentError, 'from must be less or equal to'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 分類に使用するタグの設定
|
|
37
|
+
if options.key?('category_tag')
|
|
38
|
+
@category_tag = options['category_tag']
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# セッション分析を実施するか
|
|
42
|
+
# 取得に時間を要するためオプションとする
|
|
43
|
+
if options.key?('session')
|
|
44
|
+
@session_analyze_enable = options['session']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
scrape
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create_parser(options)
|
|
51
|
+
OptionParser.new do |opt|
|
|
52
|
+
opt.on_head('-v', '--version', 'display version') do
|
|
53
|
+
puts "soracom_summary #{VERSION}"
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
opt.on('--from from_date', 'set from_date to scrape(default: today)') { |v| options['from'] = v }
|
|
57
|
+
opt.on('--to to_date', 'set to_date to scrape(default: today)') { |v| options['to'] = v }
|
|
58
|
+
opt.on('--tag category_tag', 'select tag to categorize(default: nil)') { |v| options['category_tag'] = v }
|
|
59
|
+
opt.on('--session', 'enable session analyze(default: false)') { |v| options['session'] = v }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scrape
|
|
64
|
+
raise ArgumentError, 'need SORACOM_AUTH_KEY_ID environment' unless ENV.key?('SORACOM_AUTH_KEY_ID')
|
|
65
|
+
raise ArgumentError, 'need SORACOM_AUTH_KEY environment' unless ENV.key?('SORACOM_AUTH_KEY')
|
|
66
|
+
raise ArgumentError, 'need SORACOM_SUMMARY_DEVICE_ID environment' unless ENV.key?('SORACOM_SUMMARY_DEVICE_ID')
|
|
67
|
+
raise ArgumentError, 'need SORACOM_SUMMARY_DEVICE_SECRET environment' unless ENV.key?('SORACOM_SUMMARY_DEVICE_SECRET')
|
|
68
|
+
|
|
69
|
+
client = SoracomSummary::ApiClient.new(
|
|
70
|
+
auth_key_id: ENV['SORACOM_AUTH_KEY_ID'],
|
|
71
|
+
auth_key: ENV['SORACOM_AUTH_KEY'])
|
|
72
|
+
|
|
73
|
+
subscribers = client.get_subscribers
|
|
74
|
+
|
|
75
|
+
# セッション分析を実行する場合
|
|
76
|
+
if @session_analyze_enable == true
|
|
77
|
+
subscribers.each do |subscriber|
|
|
78
|
+
sessions = client.get_sessions(subscriber.imsi, @from, @to )
|
|
79
|
+
subscriber.sessions = sessions
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
target_months = get_target_months(@from, @to)
|
|
84
|
+
billings = client.get_billing(target_months)
|
|
85
|
+
traffics = client.get_traffic(target_months)
|
|
86
|
+
logs = client.get_logs(@from, @to)
|
|
87
|
+
|
|
88
|
+
time = @from
|
|
89
|
+
loop do
|
|
90
|
+
summary = get_summary(subscribers, billings, traffics, logs, time)
|
|
91
|
+
client.upload_harvest(ENV['SORACOM_SUMMARY_DEVICE_ID'], ENV['SORACOM_SUMMARY_DEVICE_SECRET'], time, summary)
|
|
92
|
+
|
|
93
|
+
if @session_analyze_enable == true && ENV.key?('SORACOM_SESSION_DEVICE_ID') && ENV.key?('SORACOM_SESSION_DEVICE_SECRET')
|
|
94
|
+
sessions_count_by_imsi = Subscriber.sessions_count_by_imsi(subscribers, time)
|
|
95
|
+
client.upload_harvest(ENV['SORACOM_SESSION_DEVICE_ID'], ENV['SORACOM_SESSION_DEVICE_SECRET'], time, sessions_count_by_imsi)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if ENV.key?('SORACOM_BILLING_DEVICE_ID') && ENV.key?('SORACOM_BILLING_DEVICE_SECRET')
|
|
99
|
+
billing_by_origin = Billing.group_by_origin(billings, time)
|
|
100
|
+
client.upload_harvest(ENV['SORACOM_BILLING_DEVICE_ID'], ENV['SORACOM_BILLING_DEVICE_SECRET'], time, billing_by_origin)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if ENV.key?('SORACOM_TRAFFIC_DEVICE_ID') && ENV.key?('SORACOM_TRAFFIC_DEVICE_SECRET')
|
|
104
|
+
traffic_by_imsi = Traffic.group_by_imsi(traffics, time)
|
|
105
|
+
client.upload_harvest(ENV['SORACOM_TRAFFIC_DEVICE_ID'], ENV['SORACOM_TRAFFIC_DEVICE_SECRET'], time, traffic_by_imsi)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
time += Const::ONE_DAY_SEC
|
|
109
|
+
break if time >= @to
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def get_summary(subscribers, billings, traffics, logs, time)
|
|
114
|
+
subscribers_summary = Subscriber.summary(subscribers, time, @category_tag, @session_analyze_enable)
|
|
115
|
+
billings_summary = Billing.summary(billings, time)
|
|
116
|
+
traffics_summary = Traffic.summary(traffics, time)
|
|
117
|
+
logs_summary = summary_log(logs, time)
|
|
118
|
+
|
|
119
|
+
result = subscribers_summary
|
|
120
|
+
.merge(billings_summary)
|
|
121
|
+
.merge(traffics_summary)
|
|
122
|
+
.merge(logs_summary)
|
|
123
|
+
result
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_target_months(from, to)
|
|
127
|
+
target_months = []
|
|
128
|
+
time = from
|
|
129
|
+
loop do
|
|
130
|
+
target_months.push(time.strftime('%Y%m'))
|
|
131
|
+
time += Const::ONE_DAY_SEC
|
|
132
|
+
break if time >= to
|
|
133
|
+
end
|
|
134
|
+
target_months.uniq
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def summary_log(logs, time)
|
|
138
|
+
target_logs = logs.select do |log|
|
|
139
|
+
log['time'] >= time.to_i * 1000 && log['time'] < time.to_i * 1000 + Const::ONE_DAY_MSEC
|
|
140
|
+
end
|
|
141
|
+
{ 'error-log-count' => target_logs.length }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
module SoracomSummary
|
|
7
|
+
class HttpClient
|
|
8
|
+
class << self
|
|
9
|
+
def download(url)
|
|
10
|
+
uri = URI.parse(url)
|
|
11
|
+
request = Net::HTTP::Get.new(uri)
|
|
12
|
+
response = Net::HTTP.start(uri.hostname, uri.port, { use_ssl: true }) do |http|
|
|
13
|
+
http.request(request)
|
|
14
|
+
end
|
|
15
|
+
if (response.code.start_with?('2'))
|
|
16
|
+
return response.body.force_encoding('UTF-8')
|
|
17
|
+
else
|
|
18
|
+
Logger.new(STDERR).warn("Cannot download csv")
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(base_url: , interval_second: 1, retry_limit: 5)
|
|
25
|
+
@logger = Logger.new(STDERR)
|
|
26
|
+
@logger.level = ENV['SORACOM_SUMMARY_DEBUG'] ? Logger::DEBUG : Logger::WARN
|
|
27
|
+
@base_url = base_url
|
|
28
|
+
@interval_second = interval_second
|
|
29
|
+
@retry_limit = retry_limit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(path, headers)
|
|
33
|
+
uri = URI.join(@base_url, path)
|
|
34
|
+
request = Net::HTTP::Get.new(uri)
|
|
35
|
+
|
|
36
|
+
if headers.class == Hash
|
|
37
|
+
headers.each do |key, value|
|
|
38
|
+
request[key] = value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
access(uri, request)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def post(path, headers, request_body)
|
|
46
|
+
uri = URI.join(@base_url, path)
|
|
47
|
+
request = Net::HTTP::Post.new(uri)
|
|
48
|
+
request.content_type = 'application/json'
|
|
49
|
+
|
|
50
|
+
if headers.class == Hash
|
|
51
|
+
headers.each do |key, value|
|
|
52
|
+
request[key] = value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if !request_body.nil?
|
|
57
|
+
request.body = JSON.generate(request_body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
access(uri, request)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def access(uri, request, retry_count = 0)
|
|
66
|
+
response = Net::HTTP.start(uri.hostname, uri.port, { use_ssl: true }) do |http|
|
|
67
|
+
http.request(request)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if (response.code.start_with?('2'))
|
|
71
|
+
# 成功した場合はヘッダーとボディをそれぞれ返す
|
|
72
|
+
# ヘッダーを返すのはlinkヘッダーの取得が必要となるため
|
|
73
|
+
# ボディは基本的にはJSONでパースできるが、空の場合は出来ないため空文字列で返す
|
|
74
|
+
# 連続してアクセスしすぎないよう規定時間スリープしてから応答する
|
|
75
|
+
sleep @interval_second
|
|
76
|
+
return { 'headers' => response.header, 'body' => response.body.empty? ? '' : JSON.parse(response.body)}
|
|
77
|
+
elsif response.code == '429'
|
|
78
|
+
# 429はAPIレート制限のため、1分待ってからエラーを発生させる
|
|
79
|
+
@logger.debug("#{response.code}: #{response.body}")
|
|
80
|
+
sleep 60
|
|
81
|
+
raise
|
|
82
|
+
else
|
|
83
|
+
# その他不明なコードが返った場合はエラーを発生させる
|
|
84
|
+
@logger.warn("#{response.code}: #{response.body}")
|
|
85
|
+
raise
|
|
86
|
+
end
|
|
87
|
+
rescue => e
|
|
88
|
+
@logger.warn e
|
|
89
|
+
# エラーが発生した場合はリトライ回数が規定値未満であればリトライする
|
|
90
|
+
retry_count += 1
|
|
91
|
+
if retry_count < @retry_limit
|
|
92
|
+
sleep (2 ** retry_count)
|
|
93
|
+
retry
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# リトライ回数が規定値を超えるとエラーを上位に報告する
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module SoracomSummary
|
|
2
|
+
class Subscriber
|
|
3
|
+
attr_accessor :imsi, :created_at, :tags, :status, :sessions
|
|
4
|
+
class << self
|
|
5
|
+
def summary(subscribers, time, category_tag = nil, session_analyze_enable = false)
|
|
6
|
+
# 指定期間内に登録されていたSIMのみ対象とする
|
|
7
|
+
target_subscribers = subscribers.select do |subscriber|
|
|
8
|
+
subscriber.created_at < time.to_i * 1000 + Const::ONE_DAY_MSEC
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
subscribers_summary = {
|
|
12
|
+
'subscribers-total' => target_subscribers.length
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# ステータスでの分類
|
|
16
|
+
subscribers_count_by_status = target_subscribers
|
|
17
|
+
.group_by { |subscriber| subscriber.status }
|
|
18
|
+
.map { |status, group| ["subscribers-status-#{status}", group.length ]}
|
|
19
|
+
.to_h
|
|
20
|
+
subscribers_summary.merge!(subscribers_count_by_status)
|
|
21
|
+
|
|
22
|
+
# 分類対象のタグがセットされていたらタグで分類する
|
|
23
|
+
if !category_tag.nil?
|
|
24
|
+
subscribers_count_by_tag = target_subscribers
|
|
25
|
+
.group_by { |subscriber| subscriber.tags[category_tag] }
|
|
26
|
+
.map { |tag, group| ["subscribers-#{category_tag}-#{(tag.nil? ? "none" : tag)}", group.length ]}
|
|
27
|
+
.to_h
|
|
28
|
+
subscribers_summary.merge!(subscribers_count_by_tag)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# セッション状態での分類
|
|
32
|
+
if session_analyze_enable
|
|
33
|
+
subscribers_count_by_active = target_subscribers
|
|
34
|
+
.group_by { |subscriber| subscriber.active?(time) }
|
|
35
|
+
.map { |active, group| ["subscribers-#{active ? 'active' : 'inactive'}", group.length ]}
|
|
36
|
+
.to_h
|
|
37
|
+
subscribers_summary.merge!(subscribers_count_by_active)
|
|
38
|
+
end
|
|
39
|
+
subscribers_summary
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# imsiごとのセッション数を取得する
|
|
43
|
+
def sessions_count_by_imsi(subscribers, time)
|
|
44
|
+
subscribers
|
|
45
|
+
.map { |subscriber| [ subscriber.imsi, subscriber.sessions_count(time) ]}
|
|
46
|
+
.to_h
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(
|
|
51
|
+
imsi:,
|
|
52
|
+
created_at:,
|
|
53
|
+
tags:,
|
|
54
|
+
status:)
|
|
55
|
+
@imsi = imsi
|
|
56
|
+
@created_at = created_at
|
|
57
|
+
@tags = tags
|
|
58
|
+
@status = status
|
|
59
|
+
@sessions = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def active?(time)
|
|
63
|
+
from_timestamp_ms = time.to_i * 1000
|
|
64
|
+
to_timestamp_ms = time.to_i * 1000 + Const::ONE_DAY_MSEC
|
|
65
|
+
|
|
66
|
+
# 対象期間内にCreatedのイベントがあればアクティブ
|
|
67
|
+
judge = @sessions.any? do |session|
|
|
68
|
+
session['time'] >= from_timestamp_ms && session['time'] < to_timestamp_ms && session['event'] == 'Created'
|
|
69
|
+
end
|
|
70
|
+
return true if judge == true
|
|
71
|
+
|
|
72
|
+
# 対象期間内の直前のイベントがCreatedであればアクティブ
|
|
73
|
+
sessions_before_from = @sessions.select { |session| session['time'] < from_timestamp_ms }
|
|
74
|
+
return false if sessions_before_from.length == 0
|
|
75
|
+
sessions_before_from.first['event'] == 'Created'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sessions_count(time)
|
|
79
|
+
from_timestamp_ms = time.to_i * 1000
|
|
80
|
+
to_timestamp_ms = time.to_i * 1000 + Const::ONE_DAY_MSEC
|
|
81
|
+
|
|
82
|
+
created_sessions = @sessions.select do |session|
|
|
83
|
+
session['time'] >= from_timestamp_ms && session['time'] < to_timestamp_ms && session['event'] == 'Created'
|
|
84
|
+
end
|
|
85
|
+
created_sessions.length
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module SoracomSummary
|
|
2
|
+
class Traffic
|
|
3
|
+
attr_accessor :imsi, :date, :type, :upload_byte_size_total, :download_byte_size_total
|
|
4
|
+
class << self
|
|
5
|
+
def summary(traffics, time)
|
|
6
|
+
month_text = time.strftime('%Y%m')
|
|
7
|
+
day = time.strftime('%Y%m%d').to_i
|
|
8
|
+
|
|
9
|
+
# 対象期間の請求をフィルタする
|
|
10
|
+
month_traffics = traffics.select { |traffic| traffic.date[0, 6] == month_text && traffic.date.to_i <= day }
|
|
11
|
+
day_traffics = traffics.select { |traffic| traffic.date.to_i == day }
|
|
12
|
+
|
|
13
|
+
traffic_summary = {
|
|
14
|
+
'traffics-month-total' => sum(month_traffics),
|
|
15
|
+
'traffics-month-upload' => upload_sum(month_traffics),
|
|
16
|
+
'traffics-month-download' => download_sum(month_traffics),
|
|
17
|
+
'traffics-day-total' => sum(day_traffics),
|
|
18
|
+
'traffics-day-upload' => upload_sum(day_traffics),
|
|
19
|
+
'traffics-day-download' => download_sum(day_traffics)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# 通信量を種類ごとにグループ化する
|
|
23
|
+
month_traffics_by_type = month_traffics
|
|
24
|
+
.group_by { |traffic| traffic.type }
|
|
25
|
+
.map do |type, group|
|
|
26
|
+
[
|
|
27
|
+
"traffics-month-total-#{type}", sum(group),
|
|
28
|
+
"traffics-month-upload-#{type}", upload_sum(group),
|
|
29
|
+
"traffics-month-download-#{type}", download_sum(group)
|
|
30
|
+
]
|
|
31
|
+
end.flatten
|
|
32
|
+
traffic_summary.merge!(Hash[*month_traffics_by_type])
|
|
33
|
+
|
|
34
|
+
day_traffics_by_type = day_traffics
|
|
35
|
+
.group_by { |traffic| traffic.type }
|
|
36
|
+
.map do |type, group|
|
|
37
|
+
[
|
|
38
|
+
"traffics-day-total-#{type}", sum(group),
|
|
39
|
+
"traffics-day-upload-#{type}", upload_sum(group),
|
|
40
|
+
"traffics-day-download-#{type}", download_sum(group)
|
|
41
|
+
]
|
|
42
|
+
end.flatten
|
|
43
|
+
traffic_summary.merge!(Hash[*day_traffics_by_type])
|
|
44
|
+
|
|
45
|
+
traffic_summary
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def group_by_imsi(traffics, time)
|
|
49
|
+
day = time.strftime('%Y%m%d')
|
|
50
|
+
|
|
51
|
+
# 対象期間の請求をフィルタする
|
|
52
|
+
day_traffics = traffics.select { |traffic| traffic.date == day }
|
|
53
|
+
|
|
54
|
+
day_traffics_by_imsi = day_traffics
|
|
55
|
+
.group_by { |traffic| traffic.imsi }
|
|
56
|
+
.map { |imsi, group| [imsi, sum(group)] }
|
|
57
|
+
.to_h
|
|
58
|
+
|
|
59
|
+
day_traffics_by_imsi
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sum(traffics)
|
|
63
|
+
traffics.inject(0) do |sum, traffic|
|
|
64
|
+
sum + traffic.upload_byte_size_total.to_i + traffic.download_byte_size_total.to_i
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def upload_sum(traffics)
|
|
69
|
+
traffics.inject(0) { |sum, traffic| sum + traffic.upload_byte_size_total.to_i }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def download_sum(traffics)
|
|
73
|
+
traffics.inject(0) { |sum, traffic| sum + traffic.download_byte_size_total.to_i }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def initialize(
|
|
78
|
+
imsi:,
|
|
79
|
+
date:,
|
|
80
|
+
type:,
|
|
81
|
+
upload_byte_size_total:,
|
|
82
|
+
download_byte_size_total:)
|
|
83
|
+
@imsi = imsi
|
|
84
|
+
@date = date
|
|
85
|
+
@type = type
|
|
86
|
+
@upload_byte_size_total = upload_byte_size_total
|
|
87
|
+
@download_byte_size_total = download_byte_size_total
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require "soracom_summary/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "soracom_summary"
|
|
7
|
+
spec.version = SoracomSummary::VERSION
|
|
8
|
+
spec.authors = ["1stship"]
|
|
9
|
+
spec.email = ["1peifunyaq@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = 'tool to create SORACOM usage summary'
|
|
12
|
+
spec.description = 'Scraping SORACOM usage via SORACOM API and uploading these data to SORACOM Harvest'
|
|
13
|
+
spec.homepage = 'https://github.com/1stship/soracom-summary/'
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
18
|
+
end
|
|
19
|
+
spec.bindir = "exe"
|
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
|
+
spec.require_paths = ["lib"]
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
26
|
+
spec.add_development_dependency 'ruby-debug-ide'
|
|
27
|
+
spec.add_development_dependency 'debase'
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: soracom_summary
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 1stship
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-11-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: ruby-debug-ide
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: debase
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
description: Scraping SORACOM usage via SORACOM API and uploading these data to SORACOM
|
|
84
|
+
Harvest
|
|
85
|
+
email:
|
|
86
|
+
- 1peifunyaq@gmail.com
|
|
87
|
+
executables:
|
|
88
|
+
- soracom_summary
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- ".gitignore"
|
|
93
|
+
- ".rspec"
|
|
94
|
+
- ".travis.yml"
|
|
95
|
+
- CODE_OF_CONDUCT.md
|
|
96
|
+
- Gemfile
|
|
97
|
+
- Gemfile.lock
|
|
98
|
+
- LICENSE.txt
|
|
99
|
+
- README.md
|
|
100
|
+
- Rakefile
|
|
101
|
+
- bin/console
|
|
102
|
+
- bin/setup
|
|
103
|
+
- exe/soracom_summary
|
|
104
|
+
- lib/soracom_summary.rb
|
|
105
|
+
- lib/soracom_summary/api_client.rb
|
|
106
|
+
- lib/soracom_summary/billing.rb
|
|
107
|
+
- lib/soracom_summary/cli.rb
|
|
108
|
+
- lib/soracom_summary/const.rb
|
|
109
|
+
- lib/soracom_summary/http_client.rb
|
|
110
|
+
- lib/soracom_summary/subscriber.rb
|
|
111
|
+
- lib/soracom_summary/traffic.rb
|
|
112
|
+
- lib/soracom_summary/version.rb
|
|
113
|
+
- soracom_summary.gemspec
|
|
114
|
+
homepage: https://github.com/1stship/soracom-summary/
|
|
115
|
+
licenses:
|
|
116
|
+
- MIT
|
|
117
|
+
metadata: {}
|
|
118
|
+
post_install_message:
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubyforge_project:
|
|
134
|
+
rubygems_version: 2.6.12
|
|
135
|
+
signing_key:
|
|
136
|
+
specification_version: 4
|
|
137
|
+
summary: tool to create SORACOM usage summary
|
|
138
|
+
test_files: []
|