supergood 0.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: 665ca96dbcbfec2b928ff9dd1828ad464d63b23048346ee0324cbeb2b2f22977
4
+ data.tar.gz: e29262ab2a84f91991bb912223acf3d23e2e9332cbd0a33d6ec03db6b8388be0
5
+ SHA512:
6
+ metadata.gz: a1b3f9b9f987bc19cf9e596126830576374cd75d3eb5179d1a9615f137191440bba6999a376a207847626ad2ee1041662fd2ca45be5c9d8ce88aff1c093cb28d
7
+ data.tar.gz: e96124b5501e72db95ac4e6c9e76c69896f6c8d0542b33c085d060d43e73d9da16f998fcd0a880dfc5d847f39b8d30c27b81963442965d26e3a86649a629063c
@@ -0,0 +1,48 @@
1
+ require 'faraday'
2
+
3
+ module Supergood
4
+ class Api
5
+ DEFAULT_SUPERGOOD_BASE_URL = 'https://dashboard.supergood.ai'
6
+ DEFAULT_SUPERGOOD_CONFIG = {
7
+ flush_interval: 1,
8
+ event_sink_endpoint: DEFAULT_SUPERGOOD_BASE_URL + '/api/events',
9
+ error_sink_endpoint: DEFAULT_SUPERGOOD_BASE_URL + '/api/errors',
10
+ keys_to_hash: ['request.body', 'response.body'],
11
+ ignored_domains: []
12
+ }
13
+
14
+ def initialize(header_options, base_url)
15
+ @header_options = header_options
16
+ @config_fetch_url = base_url + '/api/config'
17
+ end
18
+
19
+ def log
20
+ @log
21
+ end
22
+
23
+ def set_logger(logger)
24
+ @log = logger
25
+ end
26
+
27
+ def set_event_sink_url(url)
28
+ @event_sink_url = url
29
+ end
30
+
31
+ def set_error_sink_url(url)
32
+ @error_sink_url = url
33
+ end
34
+
35
+ def post_events(payload)
36
+ Faraday.post(@event_sink_url, payload, @header_options)
37
+ end
38
+
39
+ def post_errors(payload)
40
+ Faraday.post(@event_sink_url, payload, @header_options)
41
+ end
42
+
43
+ def fetch_config
44
+ # Faraday.get(config_fetch_url, @header_options)
45
+ DEFAULT_SUPERGOOD_CONFIG
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,205 @@
1
+ require 'webmock'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'securerandom'
6
+ require 'dotenv'
7
+ require 'base64'
8
+
9
+ Dotenv.load
10
+
11
+ module Supergood
12
+
13
+ DEFAULT_SUPERGOOD_BASE_URL = 'https://dashboard.supergood.ai'
14
+
15
+ class << self
16
+ def init(supergood_client_id=nil, supergood_client_secret=nil, base_url=nil)
17
+
18
+ supergood_client_id = supergood_client_id || ENV['SUPERGOOD_CLIENT_ID']
19
+ supergood_client_secret = supergood_client_secret || ENV['SUPERGOOD_CLIENT_SECRET']
20
+ base_url = base_url || ENV['SUPERGOOD_BASE_URL'] || DEFAULT_SUPERGOOD_BASE_URL
21
+ puts "Initializing Supergood with client_id: #{supergood_client_id}, client_secret: #{supergood_client_secret}, base_url: #{base_url}"
22
+ header_options = {
23
+ 'headers' => {
24
+ 'Content-Type' => 'application/json',
25
+ 'Authorization' => 'Basic ' + Base64.encode64(supergood_client_id + ':' + supergood_client_secret)
26
+ }
27
+ }
28
+
29
+ @api = Supergood::Api.new(header_options, base_url)
30
+ @logger = Supergood::Logger.new(@api, header_options, base_url)
31
+
32
+ config = api.fetch_config()
33
+
34
+ @ignored_domains = config[:ignored_domains]
35
+ @keys_to_hash = config[:keys_to_hash]
36
+
37
+ api.set_error_sink_url(base_url + config[:error_sink_endpoint])
38
+ api.set_event_sink_url(base_url + config[:event_sink_endpoint])
39
+ api.set_logger(@logger)
40
+
41
+ @request_cache = {}
42
+ @response_cache = {}
43
+
44
+ set_interval(config[:flush_interval]) { flush_cache }
45
+ end
46
+
47
+ def log
48
+ @logger
49
+ end
50
+
51
+ def api
52
+ @api
53
+ end
54
+
55
+ def flush_cache(force = false)
56
+ # If there's notthing in the response cache, and we're not forcing a flush, then return
57
+ if @response_cache.empty? && !force
58
+ return
59
+ elsif force && @response_cache.empty? && @request_cache.empty?
60
+ return
61
+ end
62
+
63
+ data = @response_cache.values
64
+
65
+ if force
66
+ data += @request_cache.values
67
+ end
68
+
69
+ begin
70
+ log.debug(data)
71
+ # api.post_events(data)
72
+ rescue => e
73
+ #TODO Add error posting to Supergood
74
+ puts "Error posting events: #{e}"
75
+ api.post_errors(e)
76
+ ensure
77
+ @response_cache.clear
78
+ @request_cache.clear if force
79
+ end
80
+
81
+ end
82
+
83
+ def set_interval(delay)
84
+ Thread.new do
85
+ loop do
86
+ sleep delay
87
+ yield # call passed block
88
+ end
89
+ end
90
+ end
91
+
92
+ def self.intercept(*args, &block)
93
+ instance.intercept(*args, &block)
94
+ end
95
+
96
+ def self.instance
97
+ @instance ||= Supergood.new
98
+ end
99
+
100
+ def intercept(http, request, request_body)
101
+ request_id = SecureRandom.uuid
102
+ start_time = Time.now
103
+ response = yield
104
+ ensure
105
+ if log_event?(http, request)
106
+ time = ((Time.now - start_time) * 1000)
107
+ url_payload = parse_url(http, request)
108
+ @request_cache[request_id] = {
109
+ request: {
110
+ id: request_id,
111
+ method: request.method,
112
+ url: url_payload[:url],
113
+ protocol: url_payload[:protocol],
114
+ domain: url_payload[:domain],
115
+ path: url_payload[:path],
116
+ search: url_payload[:search],
117
+ body: hash_specified_keys(request.body),
118
+ header: hash_specified_keys(get_header(request)),
119
+ }
120
+ }
121
+ if defined?(response) && response
122
+ request_payload = @request_cache[request_id]
123
+ @response_cache[request_id] = {
124
+ request: request_payload,
125
+ response: {
126
+ status: response.code,
127
+ status_text: response.message,
128
+ header: hash_specified_keys(get_header(response)),
129
+ body: hash_specified_keys(response.body),
130
+ }
131
+ }
132
+ @request_cache.delete(request_id)
133
+ end
134
+ end
135
+ end
136
+
137
+ def log_event?(http, request)
138
+ !ignored?(http, request) && (http.started? || webmock?(http, request))
139
+ end
140
+
141
+ def ignored?(http, request)
142
+ url = request_url(http, request)
143
+ @ignored_domains.any? do |pattern|
144
+ url =~ pattern
145
+ end
146
+ end
147
+
148
+ def webmock?(http, request)
149
+ return false unless defined?(::WebMock)
150
+ uri = request_uri_as_string(http, request)
151
+ method = request.method.downcase.to_sym
152
+ signature = WebMock::RequestSignature.new(method, uri)
153
+ ::WebMock.registered_request?(signature)
154
+ end
155
+
156
+ # TODO: Hash keys, move to Utils?
157
+ def hash_specified_keys(body_or_header)
158
+ body_or_header
159
+ end
160
+
161
+ def get_header(request_or_response)
162
+ header = {}
163
+ request_or_response.each_header do |k,v|
164
+ header[k] = v
165
+ end
166
+ header
167
+ end
168
+
169
+ def parse_url(http, request)
170
+ url = request_url(http, request)
171
+ uri = URI.parse(url)
172
+ {
173
+ url: url,
174
+ protocol: uri.scheme,
175
+ domain: uri.host,
176
+ path: uri.path,
177
+ search: uri.query,
178
+ }
179
+ end
180
+
181
+ def request_url(http, request)
182
+ URI::DEFAULT_PARSER.unescape("http#{"s" if http.use_ssl?}://#{http.address}:#{http.port}#{request.path}")
183
+ end
184
+ end
185
+ end
186
+
187
+
188
+ # Attach library to Net::HTTP and WebMock
189
+ block = lambda do |a|
190
+ # raise instance_methods.inspect
191
+ alias request_without_net_http_logger request
192
+ def request(request, body = nil, &block)
193
+ Supergood.intercept(self, request, body) do
194
+ request_without_net_http_logger(request, body, &block)
195
+ end
196
+ end
197
+ end
198
+
199
+ if defined?(::WebMock)
200
+ klass = WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get("@webMockNetHTTP")
201
+ # raise klass.instance_methods.inspect
202
+ klass.class_eval(&block)
203
+ end
204
+
205
+ Net::HTTP.class_eval(&block)
@@ -0,0 +1,31 @@
1
+ require 'logger'
2
+ require 'dotenv'
3
+
4
+ Dotenv.load
5
+
6
+ module Supergood
7
+ class Logger < Logger
8
+ def initialize(post_errors, config, header_options)
9
+ super(STDOUT)
10
+ @post_errors = post_errors
11
+ @config = config
12
+ @header_options = header_options
13
+ end
14
+
15
+ def post_errors
16
+ @post_errors
17
+ end
18
+
19
+ def warn(payload, error, msg)
20
+ super
21
+ post_errors({ error: error, message: msg, payload: payload })
22
+ end
23
+
24
+ def debug(payload)
25
+ if(ENV['SUPERGOOD_LOG_LEVEL'] == 'debug')
26
+ super
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ require 'rudash'
2
+
3
+ module Supergood
4
+ module Utils
5
+ def hash_specified_keys
6
+ end
7
+ end
8
+ end
data/lib/supergood.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Supergood
2
+ require_relative 'supergood/api'
3
+ require_relative 'supergood/logger'
4
+ require_relative 'supergood/client'
5
+ require_relative 'supergood/utils'
6
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Supergood
2
+ VERSION = '0.0.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: supergood
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Klarfeld
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: alex@supergood.ai
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/supergood.rb
20
+ - lib/supergood/api.rb
21
+ - lib/supergood/client.rb
22
+ - lib/supergood/logger.rb
23
+ - lib/supergood/utils.rb
24
+ - lib/version.rb
25
+ homepage: https://supergood.ai
26
+ licenses:
27
+ - Business Source License 1.1
28
+ metadata:
29
+ source_code_uri: https://github.com/supergoodsystems/supergood-rb
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 2.1.0
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.0.3.1
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Supergood - API monitoring
49
+ test_files: []