statscloud 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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