statscloud 1.0.1

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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './logger_helper'
4
+
5
+ module StatsCloud
6
+ # This helper works to create socket connection.
7
+ module SocketIOHelper
8
+ private
9
+
10
+ include StatsCloud::LoggerHelper
11
+
12
+ def open?
13
+ !@client.nil? && @client.open?
14
+ end
15
+
16
+ def connect_client(url, token)
17
+ eventmachine.run do
18
+ eventmachine.add_periodic_timer(5) do
19
+ try_connect(url, token)
20
+ eventmachine.add_timer(3) { send_tags && eventmachine.stop if connected? }
21
+ end
22
+ eventmachine.add_timer(300.01) { stop_machine unless @client }
23
+ end
24
+ end
25
+
26
+ def try_connect(url, token)
27
+ @client = socketio_client.connect url, path: '/ws', 'auth-token' => token
28
+ listen_connect
29
+ listen_events
30
+ listen_errors
31
+ rescue StandardError => error
32
+ logger.error error
33
+ close
34
+ end
35
+
36
+ def listen_connect
37
+ this = self
38
+
39
+ @client.on :connect do
40
+ this.logger.info 'StatsCloud client has connected to StatsCloud server'
41
+ end
42
+ end
43
+
44
+ def listen_events
45
+ this = self
46
+
47
+ @client.on :events do |names_map|
48
+ this.names_map = names_map
49
+ this.event_name_size_in_bytes = (Math.log(names_map.keys.length) / Math.log(256)).ceil
50
+ this.event_name_size_in_bytes = 1 if this.event_name_size_in_bytes < 1
51
+ end
52
+ end
53
+
54
+ def listen_errors
55
+ this = self
56
+
57
+ @client.on :error do |error|
58
+ this.logger.error error
59
+ end
60
+ end
61
+
62
+ def send_tags
63
+ @client.emit :tags, @tags
64
+ end
65
+
66
+ def stop_machine
67
+ eventmachine.stop
68
+ Thread.stop
69
+ end
70
+
71
+ def eventmachine
72
+ EM
73
+ end
74
+
75
+ def socketio_client
76
+ ::StatsCloudIO::SocketIO::Client::Simple
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'leon'
4
+
5
+ module StatsCloud
6
+ # This helper configures StatsCloud::Client.
7
+ module StatsCloudHelper
8
+ private
9
+
10
+ def config_environment(env)
11
+ @env = env
12
+ end
13
+
14
+ def config_tags(tags)
15
+ @tags = tags
16
+ end
17
+
18
+ def env
19
+ @env ||= @config['environment'] || ENV['RAILS_ENV'] || 'default'
20
+ end
21
+
22
+ def tags
23
+ @tags ||= @config['tags'] || [os.gethostname]
24
+ end
25
+
26
+ def error_message
27
+ "statscloud.io #{@app} cluster have failed to deploy. #{@cluster[:status][:error]}"
28
+ end
29
+
30
+ def successful_message
31
+ "statscloud.io support configured, dashboard URLs are \n #{(@cluster[:grafanaDashboardsUrls] || []).join("\n")}"
32
+ end
33
+
34
+ def os
35
+ Socket
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'leon'
4
+
5
+ module StatsCloud
6
+ # This helper configures StatsmeterClient.
7
+ module StatsmeterHelper
8
+ private
9
+
10
+ def set_config(url, token, tags)
11
+ @url = url
12
+ @token = token
13
+ @tags = tags.map { |t| t.gsub(/[^a-z0-9_-]/, '_') }
14
+ end
15
+
16
+ def set_client_to_nil
17
+ @client = nil
18
+ end
19
+
20
+ def set_pending_values
21
+ @pending_plain_events = buffer.new
22
+ @pending_plain_offset = 0
23
+ @pending_binary_events = buffer.new
24
+ @pending_binary_offset = 0
25
+ end
26
+
27
+ def set_socket_values
28
+ @names_map = {}
29
+ @event_name_size_in_bytes = 0
30
+ end
31
+
32
+ def record_binary_event(name, measurement, binary_length)
33
+ @pending_binary_events.writeInt8(@names_map[name], @pending_binary_offset)
34
+ measurement.zero? ? record_binary_zero : record_binary_measurement(measurement)
35
+ @pending_binary_offset += binary_length
36
+ end
37
+
38
+ def record_binary_measurement(data)
39
+ @pending_binary_events.writeFloatBE(data, (@pending_binary_offset + @event_name_size_in_bytes))
40
+ end
41
+
42
+ def record_plain_measurement(data, name)
43
+ @pending_plain_events.writeFloatBE(data, @pending_plain_offset + name.length + 1)
44
+ end
45
+
46
+ def record_binary_zero
47
+ @pending_binary_events.writeInt32BE(0, (@pending_binary_offset + @event_name_size_in_bytes))
48
+ end
49
+
50
+ def record_plain_zero(name)
51
+ record_zero(@pending_plain_events, @pending_plain_offset + name.length + 1)
52
+ end
53
+
54
+ def record_zero(buffer, offset)
55
+ buffer.writeInt32BE(0, offset)
56
+ end
57
+
58
+ def record_plain_event(name, measurement, plain_length)
59
+ @pending_plain_events.write(name, @pending_plain_offset)
60
+ if measurement
61
+ @pending_plain_events.write(',', @pending_plain_offset + name.length)
62
+ measurement.zero? ? record_plain_zero(name) : record_plain_measurement(measurement, name)
63
+ end
64
+ @pending_plain_events.write(';', @pending_plain_offset + plain_length - 1)
65
+ @pending_plain_offset += plain_length
66
+ end
67
+
68
+ def buffer
69
+ ::LEON::StringBuffer
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'eventmachine'
6
+ require 'singleton'
7
+ require 'socket'
8
+ require_relative 'version'
9
+ require_relative 'statsmeter_client'
10
+ require_relative 'cluster_client'
11
+ require_relative 'helpers/assets_helper'
12
+ require_relative 'helpers/logger_helper'
13
+ require_relative 'helpers/statscloud_helper'
14
+
15
+ module StatsCloud
16
+ # Module which implements StatsCloud framework support.
17
+ class Client
18
+ include Singleton
19
+ include AssetsHelper
20
+ include LoggerHelper
21
+ include StatsCloud::StatsCloudHelper
22
+
23
+ # Configures statsmeter.io support for application and initializes a statscloud.io client.
24
+ #
25
+ # @param [+String] env
26
+ # statsmeter.io cluster environment
27
+ #
28
+ # @param [+Hash+] base_config
29
+ # statsmeter.io configuration
30
+ #
31
+ # @return [Thread]
32
+ #
33
+ # @api public
34
+ def start(base_config = nil)
35
+ initialize_values
36
+ generate_configuration(base_config)
37
+ collect_statscloud_assets(@config, @source_mappings)
38
+ config_values
39
+ clear_data
40
+ configure_cluster
41
+ connect_to_cluster
42
+ end
43
+
44
+ # Configures Statscloud environment.
45
+ #
46
+ # @return StatsCloud::Client
47
+ def with_environment(env)
48
+ config_environment(env)
49
+ self
50
+ end
51
+
52
+ # Configures Statscloud tags.
53
+ #
54
+ # @return StatsCloud::Client
55
+ def with_tags(tags)
56
+ config_tags(tags)
57
+ self
58
+ end
59
+
60
+ # Returns statscloud.io client aka Statsmeter client
61
+ #
62
+ # @return [StatsCloud::StatsmeterClient]
63
+ #
64
+ # @api public
65
+ def meter
66
+ @statsmeter_client
67
+ end
68
+
69
+ # Returns cluster status.
70
+ #
71
+ # @return [Hash]
72
+ #
73
+ # @api public
74
+ def cluster_status
75
+ return unless @cluster_client
76
+ cluster = @cluster_client.get_cluster(@token, @app)&.body
77
+ cluster[:status][:status] if cluster
78
+ end
79
+
80
+ # Stops statscloud.io service.
81
+ #
82
+ # @return [NilClass]
83
+ #
84
+ # @api public
85
+ def stop
86
+ @statsmeter_client&.close
87
+ @statsmeter_client = nil
88
+ @cluster_client&.undeploy_cluster(@token, @app)
89
+ @cluster_client = nil
90
+ end
91
+
92
+ private
93
+
94
+ attr_reader :config, :source_mappings, :statsmeter_client, :cluster, :token, :app, :graphite_url
95
+
96
+ def initialize_values
97
+ @config = {}
98
+ @source_mappings = { metrics: [], admins: [], dashboards: [], alerts: [] }
99
+ end
100
+
101
+ def generate_configuration(base_config)
102
+ if base_config
103
+ join_configs(@config, base_config, @source_mappings, '')
104
+ else
105
+ base_config = get_config_from_file(File.join(__dir__, '.statscloud.yml'))
106
+ join_configs(@config, base_config, @source_mappings, '.statscloud.yml')
107
+ end
108
+ end
109
+
110
+ def config_values
111
+ @config['sourceMaps'] = source_mappings
112
+ @token = @config['token']
113
+ @app = @config['application']
114
+ end
115
+
116
+ def clear_data
117
+ @config.delete('propagateErrors')
118
+ @config.delete('token')
119
+ end
120
+
121
+ def configure_cluster
122
+ @cluster_client = create_cluster_client
123
+ @cluster_client.deploy_cluster(@token, @app, @config)
124
+ @cluster = @cluster_client.get_cluster(@token, @app).body
125
+ @graphite_url = @cluster[:graphiteUrl]
126
+ check_cluster_status
127
+ logger.info successful_message
128
+ end
129
+
130
+ def check_cluster_status
131
+ raise error if @cluster[:status][:status] == 'ERROR'
132
+ end
133
+
134
+ def error
135
+ logger.error error_message
136
+ StandardError.new(error_message)
137
+ end
138
+
139
+ def connect_to_cluster
140
+ @statsmeter_client = create_statsmeter_client(@cluster, @token, tags)
141
+ @statsmeter_client.connect
142
+ end
143
+
144
+ def create_cluster_client
145
+ StatsCloud::ClusterClient.new(env)
146
+ end
147
+
148
+ def create_statsmeter_client(cluster, token, tags)
149
+ host = cluster[:statsmeterUrl]
150
+ StatsCloud::StatsmeterClient.new(host, token, tags)
151
+ rescue StandardError => e
152
+ logger.error e
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './helpers/socketio_helper'
4
+ require_relative './helpers/statsmeter_helper'
5
+ require_relative './helpers/logger_helper'
6
+ require_relative './helpers/event_helper'
7
+ require 'leon'
8
+ require 'eventmachine'
9
+ require 'statscloud.io-ruby-socket.io-client-simple'
10
+ require 'crc32'
11
+
12
+ module StatsCloud
13
+ # Client for Statsmeter.
14
+ class StatsmeterClient
15
+ include StatsCloud::LoggerHelper
16
+ include StatsCloud::SocketIOHelper
17
+ include StatsCloud::StatsmeterHelper
18
+ include StatsCloud::EventHelper
19
+
20
+ # Maximum size of pending binary events buffer.
21
+ BINARY_BUFFER_SIZE = 1024
22
+
23
+ # Maximum size of pending binary events buffer.
24
+ PLAIN_BUFFER_SIZE = 1024 * 100
25
+
26
+ # Statsmeter cluster url is used to connect to cluster.
27
+ #
28
+ # Type: *String*
29
+ attr_reader :url
30
+
31
+ # Socket connection with statscloud cluster which is used to record events.
32
+ #
33
+ # Type: *SocketIO::Client::Simple*
34
+ attr_accessor :client
35
+
36
+ # Metric names.
37
+ #
38
+ # Type: *Hash*
39
+ attr_accessor :names_map
40
+
41
+ # Binary size for metric names.
42
+ #
43
+ # Type: *Integer*
44
+ attr_accessor :event_name_size_in_bytes
45
+
46
+ # Initialize statsmeter client.
47
+ #
48
+ # @param [+String+] url
49
+ # statsmeter url.
50
+ # @param [+String+] token
51
+ # authorization token.
52
+ def initialize(url, token, tags = [])
53
+ set_config(url, token, tags)
54
+ set_client_to_nil
55
+ set_pending_values
56
+ set_socket_values
57
+ end
58
+
59
+ # Connects to the server and starts periodic sending events.
60
+ def connect
61
+ Thread.new do
62
+ connect_client(@url, @token)
63
+ flush_events_loop.join if @client
64
+ end
65
+ end
66
+
67
+ # Records a single event.
68
+ #
69
+ # @param [+String+] name
70
+ # name of the event to record.
71
+ # @param [+Integer+] measurement
72
+ # optional measurement, depending on metrics type.
73
+ #
74
+ # Save event in pending events.
75
+ def record_event(name, measurement = 0)
76
+ record_events(name: name, measurement: measurement)
77
+ end
78
+
79
+ # Records several events at once.
80
+ #
81
+ # @param [+Array+] events
82
+ # events to send (each should have name and optional measurement fields).
83
+ #
84
+ # Save events in pending events.
85
+ def record_events(*events)
86
+ record_events_array(events)
87
+ end
88
+
89
+ # Records an array of events at once.
90
+ #
91
+ # @param [+Array+] events
92
+ # array of events to send (each shoud have name and optional measurement fields).
93
+ #
94
+ # Save events in pending binary and plain arrays. Check flush condition.
95
+ def record_events_array(events)
96
+ events.each do |event|
97
+ name = get_event_name(event)
98
+ measurement = get_event_measurement(event)&.to_f
99
+
100
+ next unless @names_map && @names_map[name]
101
+ binary_length = get_binary_length(@event_name_size_in_bytes)
102
+ plain_length = get_plain_length(name, measurement)
103
+
104
+ flush_events if flush_condition(binary_length, plain_length)
105
+ record_binary_event(name, measurement, binary_length)
106
+ record_plain_event(name, measurement, plain_length)
107
+ end
108
+ end
109
+
110
+ # Sends all pending events to statscloud.
111
+ def flush_events
112
+ return if @pending_binary_offset.zero?
113
+ checksum = crc32.calculate(@pending_plain_events.buffer, @pending_plain_offset, 0)
114
+ return if checksum.zero?
115
+ @pending_binary_events.writeInt32BE(checksum, @pending_binary_offset)
116
+ send_message @pending_binary_events
117
+ set_pending_values
118
+ rescue StandardError => error
119
+ logger.error error
120
+ close
121
+ connect.join
122
+ end
123
+
124
+ # Shows statsmeter state.
125
+ def connected?
126
+ @client&.state == :connect
127
+ end
128
+
129
+ # Stops socket.io connection.
130
+ def close
131
+ client.disconnect if open?
132
+ set_client_to_nil
133
+ end
134
+
135
+ private
136
+
137
+ def flush_events_loop
138
+ Thread.new do
139
+ eventmachine.run do
140
+ eventmachine.add_periodic_timer(0.5) do
141
+ stop_machine unless @client
142
+ flush_events
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def send_message(string_buffer)
149
+ @client.emit :metric, binary_packet(string_buffer.buffer.bytes)
150
+ end
151
+
152
+ def binary_packet(byte_array)
153
+ socketio_client.as_byte_buffer(byte_array)
154
+ end
155
+
156
+ def flush_condition(binary, plain)
157
+ @pending_binary_offset + binary > BINARY_BUFFER_SIZE || @pending_plain_offset + plain > PLAIN_BUFFER_SIZE
158
+ end
159
+
160
+ def crc32
161
+ Crc32
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatsCloud
4
+ # version of statscloud-ruby-client.
5
+ #
6
+ # Type: *String*
7
+ VERSION = '1.0.1'
8
+ end
data/lib/statscloud.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statscloud/statscloud_client'
4
+ require 'statscloud/helpers/logger_helper'
5
+
6
+ # StatsCloud.io ruby client
7
+ module StatsCloud
8
+ class << self
9
+ include LoggerHelper
10
+
11
+ # Configures statsmeter.io support for application and initializes a statscloud.io client.
12
+ #
13
+ # @param [+String+] env
14
+ # statsmeter.io cluster environment
15
+ #
16
+ # @param [+Hash+] base_config
17
+ # statsmeter.io configuration
18
+ #
19
+ # @return [Thread]
20
+ #
21
+ # @api public
22
+ def start(base_config = nil)
23
+ StatsCloud::Client.instance.start(base_config)
24
+ rescue StandardError => error
25
+ logger.error error
26
+ end
27
+
28
+ # Configures Statscloud environment.
29
+ #
30
+ # @return StatsCloud::Client
31
+ def with_environment(env)
32
+ StatsCloud::Client.instance.with_environment(env)
33
+ rescue StandardError => error
34
+ logger.error error
35
+ end
36
+
37
+ # Configures Statscloud tags.
38
+ #
39
+ # @return StatsCloud::Client
40
+ def with_tags(tags)
41
+ StatsCloud::Client.instance.with_tags(tags)
42
+ rescue StandardError => error
43
+ logger.error error
44
+ end
45
+
46
+ # Returns statscloud.io client aka Statsmeter client
47
+ #
48
+ # @return [StatsCloud::StatsmeterClient]
49
+ #
50
+ # @api public
51
+ def meter
52
+ StatsCloud::Client.instance.meter
53
+ rescue StandardError => error
54
+ logger.error error
55
+ end
56
+
57
+ # Returns cluster status.
58
+ #
59
+ # @return [Hash]
60
+ #
61
+ # @api public
62
+ def cluster_status
63
+ StatsCloud::Client.instance.cluster_status
64
+ rescue StandardError => error
65
+ logger.error error
66
+ end
67
+
68
+ # Stops statscloud.io service.
69
+ #
70
+ # @return [NilClass]
71
+ #
72
+ # @api public
73
+ def stop
74
+ StatsCloud::Client.instance.stop
75
+ rescue StandardError => error
76
+ logger.error error
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ ---
2
+ token: place-your-token-here
3
+ passwordProtect: false
4
+ application: your-application-name
5
+ flushIntervalInSeconds: 15
6
+ retention:
7
+ - frequency: 15s
8
+ keep: 1d
9
+ admins:
10
+ admin:
11
+ 'phone': place-your-phone-here
12
+ channels:
13
+ - sms
14
+ - voice
15
+ developer:
16
+ email: place-your-email-here
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ StatsCloud.start
4
+ at_exit { StatsCloud.stop }
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "statscloud/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "statscloud"
9
+ spec.version = StatsCloud::VERSION
10
+ spec.authors = ["Roman Ovcharov"]
11
+ spec.email = ["roman.o.as@agiliumlabs.com"]
12
+
13
+ spec.summary = %q{StatsCloud service.}
14
+ spec.description = %q{PaaS application monitoring system.}
15
+ spec.homepage = "https://statscloud.io"
16
+ spec.license = "MIT"
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against " \
24
+ "public gem pushes."
25
+ end
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_dependency "activesupport", ">= 5.0.0.1"
37
+ spec.add_development_dependency "bundler", "~> 1.16"
38
+ spec.add_dependency "crc32", "~> 1.0.1"
39
+ spec.add_dependency "eventmachine", "~> 1.2"
40
+ spec.add_dependency "fileutils"
41
+ spec.add_dependency "leon", "~> 1.1"
42
+ spec.add_dependency "logger", "~> 1.2"
43
+ spec.add_development_dependency "rake", "~> 10.0"
44
+ spec.add_dependency "rest-client", "~> 2.0.2"
45
+ spec.add_development_dependency "rspec", "~> 3.0"
46
+ spec.add_dependency "statscloud.io-ruby-socket.io-client-simple", "~> 1.2.1.pre.2"
47
+ end