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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cfc4b9f7ca3ff6dfc74d26c74805c53562ef11ae1842c043ff76fa802025867e
4
+ data.tar.gz: 8bd02234c81c1537ed5b03b5eb507d31e23cf0140ebf03343445af1d143904c5
5
+ SHA512:
6
+ metadata.gz: 1036da28708e37ced51549361dc0f200ba3ab9a4386ad9b5ce6b0228d77ed56e25b3ff491ff9d80f449ce9806c42be70da2655ababfd629b93066fd06a13889d
7
+ data.tar.gz: 09800276336b3e3087c332d27facf0e57118ba3175c141af87b5467a21a7c434ae4d3d9a9b2fa38acb9186bf0f0d957109bd1439556eb6a0a2f819e580226e98
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.idea
2
+ /.bundle/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /log/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,72 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
4
+ Layout/ExtraSpacing:
5
+ Exclude:
6
+ - 'statscloud.gemspec'
7
+
8
+ Layout/LeadingBlankLines:
9
+ Exclude:
10
+ - 'statscloud.gemspec'
11
+
12
+ Layout/SpaceAroundOperators:
13
+ Exclude:
14
+ - 'statscloud.gemspec'
15
+
16
+ Layout/SpaceInsideBlockBraces:
17
+ Exclude:
18
+ - 'Gemfile'
19
+
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - 'spec/**/*'
23
+ - 'test/**/*'
24
+ - 'statscloud.gemspec'
25
+
26
+ Metrics/AbcSize:
27
+ Exclude:
28
+ - 'lib/statscloud/helpers/socketio_helper.rb'
29
+
30
+ Metrics/MethodLength:
31
+ Exclude:
32
+ - 'lib/statscloud/helpers/socketio_helper.rb'
33
+
34
+ Style/Documentation:
35
+ Exclude:
36
+ - 'spec/**/*'
37
+ - 'test/**/*'
38
+
39
+ Style/ExpandPathArguments:
40
+ Exclude:
41
+ - 'statscloud.gemspec'
42
+
43
+ Style/HashSyntax:
44
+ EnforcedStyle: ruby19
45
+
46
+ Style/MutableConstant:
47
+ Exclude:
48
+ - 'lib/statscloud/version.rb'
49
+
50
+ Style/PercentLiteralDelimiters:
51
+ Exclude:
52
+ - 'statscloud.gemspec'
53
+
54
+ Style/StringLiterals:
55
+ Exclude:
56
+ - 'Gemfile'
57
+ - 'Rakefile'
58
+ - 'bin/console'
59
+ - 'lib/statscloud/version.rb'
60
+ - 'spec/spec_helper.rb'
61
+ - 'spec/statscloud_client_spec.rb'
62
+ - 'statscloud.gemspec'
63
+
64
+ Style/FrozenStringLiteralComment:
65
+ Exclude:
66
+ - 'lib/templates/statscloud/statscloud.rb'
67
+ Style/UnneededPercentQ:
68
+ Exclude:
69
+ - 'statscloud.gemspec'
70
+
71
+ Metrics/LineLength:
72
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in statscloud.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ statscloud (1.0.0)
5
+ activesupport (>= 5.0.0.1)
6
+ crc32 (~> 1.0.1)
7
+ eventmachine (~> 1.2)
8
+ fileutils
9
+ leon (~> 1.1)
10
+ logger (~> 1.2)
11
+ rest-client (~> 2.0.2)
12
+ statscloud.io-ruby-socket.io-client-simple (~> 1.2.1.pre.2)
13
+
14
+ GEM
15
+ remote: https://rubygems.org/
16
+ specs:
17
+ activesupport (5.2.1)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 0.7, < 2)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+ concurrent-ruby (1.0.5)
23
+ crc32 (1.0.1)
24
+ diff-lcs (1.3)
25
+ domain_name (0.5.20180417)
26
+ unf (>= 0.0.5, < 1.0.0)
27
+ event_emitter (0.2.6)
28
+ eventmachine (1.2.7)
29
+ fileutils (1.1.0)
30
+ http-cookie (1.0.3)
31
+ domain_name (~> 0.5)
32
+ httparty (0.16.2)
33
+ multi_xml (>= 0.5.2)
34
+ i18n (1.1.1)
35
+ concurrent-ruby (~> 1.0)
36
+ json (2.1.0)
37
+ leon (1.1.2)
38
+ logger (1.2.8)
39
+ mime-types (3.2.2)
40
+ mime-types-data (~> 3.2015)
41
+ mime-types-data (3.2018.0812)
42
+ minitest (5.11.3)
43
+ multi_xml (0.6.0)
44
+ netrc (0.11.0)
45
+ rake (10.5.0)
46
+ rest-client (2.0.2)
47
+ http-cookie (>= 1.0.2, < 2.0)
48
+ mime-types (>= 1.16, < 4.0)
49
+ netrc (~> 0.8)
50
+ rspec (3.8.0)
51
+ rspec-core (~> 3.8.0)
52
+ rspec-expectations (~> 3.8.0)
53
+ rspec-mocks (~> 3.8.0)
54
+ rspec-core (3.8.0)
55
+ rspec-support (~> 3.8.0)
56
+ rspec-expectations (3.8.2)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.8.0)
59
+ rspec-mocks (3.8.0)
60
+ diff-lcs (>= 1.2.0, < 2.0)
61
+ rspec-support (~> 3.8.0)
62
+ rspec-support (3.8.0)
63
+ statscloud.io-ruby-socket.io-client-simple (1.2.1.pre.2)
64
+ event_emitter
65
+ httparty
66
+ json
67
+ websocket-client-simple (~> 0.3.0)
68
+ thread_safe (0.3.6)
69
+ tzinfo (1.2.5)
70
+ thread_safe (~> 0.1)
71
+ unf (0.1.4)
72
+ unf_ext
73
+ unf_ext (0.0.7.5)
74
+ websocket (1.2.8)
75
+ websocket-client-simple (0.3.0)
76
+ event_emitter
77
+ websocket
78
+
79
+ PLATFORMS
80
+ ruby
81
+
82
+ DEPENDENCIES
83
+ bundler (~> 1.16)
84
+ rake (~> 10.0)
85
+ rspec (~> 3.0)
86
+ statscloud!
87
+
88
+ BUNDLED WITH
89
+ 1.16.3
data/LICENSE.txt ADDED
@@ -0,0 +1 @@
1
+ (c) Copyright 2018 Agilium Labs LLC, all rights reserved
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # StatsCloud
2
+ StatsCloud module
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'statscloud'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install statscloud
18
+
19
+ ## Usage
20
+
21
+ ### Rails
22
+
23
+ 1. Run generator for creatinig initializer file, statscloud.io configuration file `.statscloud.yml` and directory for saving your metrics configurations.
24
+ ```ruby
25
+ rails g stats_cloud:install
26
+ ```
27
+ 2. Set up your configuration files (you can read more about StatsCloud configuration [here](https://medium.com/@roman.kisilenko/software-application-monitoring-how-to-or-how-not-to-let-your-production-fail-9481dd0ef6de)).
28
+
29
+ 3. You can change `statscloud.rb` intializer file if you want to start StatsCloud service with a different environment or add some tags to your StatsmeterClient.
30
+
31
+ #### `with_environment('env')`
32
+ This method allows you to record metrics from different environments (like `test`, `development`, `production`) separately, so
33
+ ```ruby
34
+ StatsCloud.with_environment('production').start
35
+ ```
36
+ would start StatsCloud service at `production` environment. Without this method, the environment would be chosen from the configuration file. If you don't configure it service would try to find env in the RAILS_ENV and if it is also missing it would set the value as the `default`.
37
+
38
+ #### `with_tags(array[string])`
39
+ Also, you can start service with sending some tags to the cluster. Tags are used to group metrics. By default, the host name is used as a tag, which allows you to track metrics for both the application as a whole and for each host individually. In the cloud, an array of tags `['region', 'server_name']` can be used, which allows you to track metrics for the application as a whole, separate regions or separate servers.
40
+ ```ruby
41
+ StatsCloud.with_tags(['region', 'server_name']).start
42
+ ```
43
+ You can combine these methods calling them as a chain.
44
+
45
+ 4. Use StatsCLoud.meter with `record_event` method for recording one event or `record_events` to send multiple events to cluster from any place of you application.
46
+
47
+ ```ruby
48
+ StatsCloud.meter.record_event('event', 1)
49
+ StatsCloud.meter.record_events({name: 'gauge', measurement: 123}, {name: 'counter'})
50
+ ```
51
+ ### Ruby
52
+ 1. Install `statscloud` gem:
53
+
54
+ ```ruby
55
+ gem install statscloud
56
+ ```
57
+
58
+ 2. Require `statscloud` in the project and set up your application structure, with `.statscloud.yml` configuration file (at the root of the project) and metrics configs (read more about StatsCloud configuration [here](https://medium.com/@roman.kisilenko/software-application-monitoring-how-to-or-how-not-to-let-your-production-fail-9481dd0ef6de)).
59
+
60
+ 3. Customize statscloud run with `with_environment` and `with_tags` methods and start it where you need it.
61
+
62
+ ```ruby
63
+ StatsCloud.with_environment('test').with_tags(['usa', 'server_1']).start
64
+ ```
65
+ 4. Send metrics via StatsCloud.meter
66
+
67
+ ```ruby
68
+ meter = StatsCloud.meter
69
+ meter.record_event('event', 1)
70
+ meter.record_events({name: 'gauge', measurement: 123}, {name: 'counter'});
71
+ ```
72
+
73
+ 5. Stop work of StatsCloud service when 'time is over'.
74
+ ```ruby
75
+ StatsCloud.stop
76
+ ```
77
+
78
+ The full version of pure ruby example takes the form:
79
+ ```ruby
80
+ require 'statscloud' # import gem
81
+
82
+ # start the module, returns a thread
83
+ StatsCloud.start
84
+
85
+ # track events (remember that you have to wait for the socket connection to the cluster before record any metrics)
86
+ StatsCloud.meter.record_event('numeric', 123)
87
+ StatsCloud.meter.record_events({name: 'gauge', measurement: 123}, {name: 'counter'})
88
+
89
+ # stop the module
90
+ StatsCloud.stop
91
+
92
+ ```
93
+
94
+ ## License
95
+
96
+ (c) Copyright 2018 Agilium Labs LLC, all rights reserved.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "statscloud"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module StatsCloud
6
+ # Rails generator for statscloud configuration.
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('../../templates/statscloud', __dir__)
9
+
10
+ # Creates rails initializer for simultaneous start with rails server.
11
+ def create_initializer_file
12
+ copy_file 'statscloud_template.rb', File.join('config', 'initializers', 'statscloud.rb')
13
+ end
14
+
15
+ # Creates directory for saving metrics config files.
16
+ def create_config_statscloud_directory
17
+ empty_directory File.join('config', 'statscloud')
18
+ end
19
+
20
+ # Creates statscloud configuration file.
21
+ def create_config_file
22
+ copy_file '.statscloud_template.yml', '.statscloud.yml'
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './helpers/parsed_response_helper'
4
+ require_relative './helpers/logger_helper'
5
+ require 'rest-client'
6
+
7
+ module StatsCloud
8
+ # statscloud.io cluster-api service client.
9
+ class ClusterClient
10
+ include StatsCloud::LoggerHelper
11
+
12
+ # Constructs cluster-api service client instance.
13
+ #
14
+ # @param [+String+] env
15
+ # cluster api environment
16
+ #
17
+ # @param [+String+] host
18
+ # cluster api host.
19
+ #
20
+ # @api public
21
+ def initialize(env, host = 'https://cluster-api-v1.statscloud.statscloud.io')
22
+ @environment = env
23
+ @host = host
24
+ end
25
+
26
+ # Schedules cluster instance to be retrieved.
27
+ #
28
+ # @param [+String+] token
29
+ # authorization token.
30
+ # @param [+String+] app
31
+ # cluster name.
32
+ #
33
+ # @api public
34
+ def get_cluster(token, app)
35
+ url = "#{host}/users/current/clusters/#{app}/#{environment}"
36
+ response = http_client.get(url, headers(token))
37
+ get_parsed_response(response)
38
+ rescue StandardError => error
39
+ logger.error error
40
+ end
41
+
42
+ # Schedules cluster to be (re)deployed.
43
+ #
44
+ # @param [+String+] token
45
+ # authorization token.
46
+ # @param [+String+] app
47
+ # cluster name.
48
+ # @param [+String+] configuration
49
+ # cluster configuration
50
+ #
51
+ # @api public
52
+ def deploy_cluster(token, app, configuration)
53
+ url = "#{host}/users/current/clusters/#{app}/#{environment}"
54
+ body = body_hash_parameters(configuration)
55
+ response = http_client.put(url, body.to_json, headers(token))
56
+ get_parsed_response(response)
57
+ rescue StandardError => error
58
+ logger.error error
59
+ end
60
+
61
+ # Schedules cluster to be undeployed.
62
+ # @param [+String+] token
63
+ # authorization token.
64
+ # @param [+String+] app
65
+ # cluster name.
66
+ #
67
+ # @api public
68
+ def undeploy_cluster(token, app)
69
+ url = "#{host}/users/current/clusters/#{app}/#{environment}/undeploy"
70
+ response = http_client.post(url, nil, headers(token))
71
+ get_parsed_response(response)
72
+ rescue StandardError => error
73
+ logger.error error
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :host, :environment
79
+
80
+ def headers(token)
81
+ default_headers.merge(auth_headers(token))
82
+ end
83
+
84
+ def default_headers
85
+ {
86
+ content_type: :json,
87
+ accept: :json
88
+ }
89
+ end
90
+
91
+ def auth_headers(token)
92
+ {
93
+ 'auth-token' => token
94
+ }
95
+ end
96
+
97
+ def body_hash_parameters(configuration)
98
+ {
99
+ configuration: configuration.to_json
100
+ }
101
+ end
102
+
103
+ def get_parsed_response(response)
104
+ StatsCloud::ParsedResponseHelper.new(response)
105
+ end
106
+
107
+ def http_client
108
+ RestClient
109
+ end
110
+
111
+ def logger
112
+ @logger ||= ::StatsCloud.logger
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatsCloud
4
+ # This helper works to gather and merge the configuration from files.
5
+ module AssetsHelper
6
+ private
7
+
8
+ def asset_selector_matches(filename, asset_selector)
9
+ includes = asset_selector[:include]
10
+ excludes = asset_selector[:exclude]
11
+ process_match(includes, filename) && !process_match(excludes, filename)
12
+ end
13
+
14
+ def collect_statscloud_assets(config, source_mappings, dir = '.')
15
+ asset_selector = get_checked_asset_selector(config)
16
+ process_files(dir, config, source_mappings, asset_selector)
17
+ end
18
+
19
+ def join_configs(config, extra_config, source_mappings, file_name)
20
+ process_array_fields(config, extra_config, source_mappings, file_name)
21
+ process_object_fields(config, extra_config, source_mappings, file_name)
22
+ process_value_fields(config, extra_config)
23
+ end
24
+
25
+ def get_checked_asset_selector(config)
26
+ asset_selector = config[:assetSelector] || {
27
+ include: ['.*\.statscloud.yml'], exclude: []
28
+ }
29
+ asset_selector[:include] = put_to_array_if_needed(asset_selector[:include])
30
+ asset_selector[:exclude] = put_to_array_if_needed(asset_selector[:exclude])
31
+ asset_selector
32
+ end
33
+
34
+ def get_config_from_file(file)
35
+ return ::YAML.load_file(file) if File.exist?(file)
36
+ {}
37
+ end
38
+
39
+ def process_files(dir, config, source_mappings, asset_selector)
40
+ entries = Dir.entries(dir)
41
+ entries.reduce({}) do |_config, entry|
42
+ file_name = File.join(dir, entry)
43
+ if it_directory?(file_name, entry)
44
+ collect_statscloud_assets(config, source_mappings, file_name)
45
+ elsif it_config_file?(file_name, asset_selector)
46
+ extra_config = get_config_from_file(file_name)
47
+ join_configs(config, extra_config, source_mappings, file_name)
48
+ end
49
+ end
50
+ end
51
+
52
+ def process_match(list, filename)
53
+ list.reduce(false) do |match, pattern|
54
+ match || Regexp.new(pattern).match(filename)
55
+ end
56
+ end
57
+
58
+ def process_array_fields(config, extra_config, source_mappings, file_name)
59
+ array_fields.each do |array_field|
60
+ next unless it_not_empty_array?(extra_config, array_field)
61
+ config[array_field] ||= []
62
+ source_mappings[array_field.to_sym].push(source_map_index(config, extra_config, array_field, file_name))
63
+ config[array_field] = config[array_field].concat(extra_config[array_field])
64
+ end
65
+ end
66
+
67
+ def process_object_fields(config, extra_config, source_mappings, file_name)
68
+ object_fields.each do |object_field|
69
+ next unless extra_config[object_field]&.kind_of?(Hash)
70
+ source_mappings[object_field.to_sym].push(
71
+ fields: extra_config[object_field].keys,
72
+ sourceFile: file_name
73
+ )
74
+ config[object_field] = (config[object_field] || {}).merge(extra_config[object_field])
75
+ end
76
+ end
77
+
78
+ def process_value_fields(config, extra_config)
79
+ value_fields.each do |field|
80
+ config[field] = extra_config[field] if config[field].nil? && !extra_config[field].nil?
81
+ end
82
+ end
83
+
84
+ def put_to_array_if_needed(object)
85
+ object.is_a?(Array) ? object : [object]
86
+ end
87
+
88
+ def it_directory?(file_name, entry)
89
+ File.directory?(file_name) && not_required_directory.exclude?(entry)
90
+ end
91
+
92
+ def it_config_file?(file_name, asset_selector)
93
+ File.stat(file_name).file? && asset_selector_matches(file_name, asset_selector)
94
+ end
95
+
96
+ def it_not_empty_array?(extra_config, field)
97
+ extra_config[field]&.kind_of?(Array) && extra_config[field].length.positive?
98
+ end
99
+
100
+ def source_map_index(config, extra_config, array_field, file_name)
101
+ start_index = config[array_field].length
102
+ {
103
+ startIndex: start_index,
104
+ endIndex: start_index + extra_config[array_field].length - 1,
105
+ sourceFile: file_name
106
+ }
107
+ end
108
+
109
+ def not_required_directory
110
+ %w[. ..]
111
+ end
112
+
113
+ def array_fields
114
+ %w[alerts dashboards]
115
+ end
116
+
117
+ def object_fields
118
+ %w[metrics admins]
119
+ end
120
+
121
+ def value_fields
122
+ %w[token application environment endpoint assetSelector propagateErrors
123
+ passwordProtect retention flushIntervalInSeconds diskSize]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatsCloud
4
+ # This helper works with events.
5
+ module EventHelper
6
+ private
7
+
8
+ # Byte size for measurement.
9
+ MEASUREMENT_SIZE = 4
10
+
11
+ def get_binary_length(size)
12
+ size + MEASUREMENT_SIZE
13
+ end
14
+
15
+ def get_plain_length(name, measurement)
16
+ name.length + (measurement ? MEASUREMENT_SIZE + 1 : 0) + 1
17
+ end
18
+
19
+ def get_event_name(event)
20
+ event[:name].to_s
21
+ end
22
+
23
+ def get_event_measurement(event)
24
+ event[:measurement]&.to_s
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module StatsCloud
6
+ # This helper works to help log errors and info about StatsCloud service work.
7
+ module LoggerHelper
8
+ # Log information about work to standart ouput
9
+ def logger
10
+ file_utils.mkdir_p 'log'
11
+ @logger ||= logger_servise.new(File.join('log', 'statscloud.log')).tap do |log|
12
+ log.progname = 'StatsCloud'
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def logger_servise
19
+ Logger
20
+ end
21
+
22
+ def file_utils
23
+ FileUtils
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support/all'
5
+
6
+ module StatsCloud
7
+ # This helper works to parse response from the server to convenient object.
8
+ class ParsedResponseHelper
9
+ # RestClient json-parsed response body.
10
+ #
11
+ # Type: *Hash*
12
+ attr_reader :body
13
+
14
+ # RestClient response code.
15
+ #
16
+ # Type: *Integer*
17
+ attr_reader :code
18
+
19
+ # Creates new helper object with parsed response body as json.
20
+ #
21
+ # @param [+RestClient::Response+] response
22
+ # response from the cluster.
23
+ def initialize(response)
24
+ @code = response&.code
25
+ @body = parse_json(response.body)
26
+ end
27
+
28
+ private
29
+
30
+ def parse_json(json, default = nil)
31
+ JSON.parse(json).with_indifferent_access
32
+ rescue JSON::ParserError
33
+ default
34
+ end
35
+ end
36
+ end