raises 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +15 -0
- data/lib/raises/delivery.rb +50 -0
- data/lib/raises/railtie.rb +1 -1
- data/lib/raises/spool.rb +133 -0
- data/lib/raises/spool_storage.rb +140 -0
- data/lib/raises/subscriber.rb +59 -7
- data/lib/raises/version.rb +1 -1
- data/lib/raises.rb +12 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 826d161b194d0bb9f3d3b1e00f3326dc986da4efb420e9ae1c8ff0dccf59708e
|
|
4
|
+
data.tar.gz: b0b9b1dbe0e03571e0585c6982e59091495337e14118765a0ff1274427a1b956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee447f476885abbda2516cbe5b92600b824b879507954f23c8f276225adb718f78652f2cc53bf79e1bf55fe11a6203bad9519b4adffbbcb64e4eae20ca9b653a
|
|
7
|
+
data.tar.gz: 15d056b1826db28175a104359215f1a3eebd7497358a8c260057b0811e5a3758bef330c9412372393f7de077a81ea7741bcd8a82a20a9e0f2a0e0898c14c6098
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Add `Raises.notify` for informational, warning, and error-level events.
|
|
6
|
+
- Preserve event and exception routes in the optional disk spool.
|
|
7
|
+
- Keep informational events separate from exception grouping and GitHub issues.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
- Add opt-in, bounded disk spooling through `RAISES_SPOOL_DIR`.
|
|
12
|
+
- Retry transient delivery failures safely across Rails processes and restarts.
|
|
13
|
+
- Keep ingestion credentials out of spool files.
|
|
14
|
+
|
|
3
15
|
## 0.1.0
|
|
4
16
|
|
|
5
17
|
- Initial Rails 7.1+ `Rails.error` subscriber.
|
data/README.md
CHANGED
|
@@ -8,6 +8,21 @@ gem "raises"
|
|
|
8
8
|
|
|
9
9
|
Set `RAISES_TOKEN` to the ingestion credential created for the project. Production errors are reported to `https://raises.dev` automatically through `Rails.error`. Set `RAISES_REPORT=1` to exercise the integration outside production.
|
|
10
10
|
|
|
11
|
+
Send a one-off operational notice without raising an exception:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
Raises.notify(
|
|
15
|
+
"Import finished",
|
|
16
|
+
level: :info,
|
|
17
|
+
source: "nightly-import",
|
|
18
|
+
context: { imported: 412, skipped: 3 }
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Valid levels are `:info`, `:warning`, and `:error`. Notices are retained and delivered to configured outbound integrations, but they do not create error groups, acknowledgements, or GitHub issues.
|
|
23
|
+
|
|
11
24
|
Optional settings are `RAISES_URL`, `RAISES_ENV`, `RAISES_REVISION`, `RAISES_OPEN_TIMEOUT`, and `RAISES_READ_TIMEOUT`.
|
|
12
25
|
|
|
26
|
+
Set `RAISES_SPOOL_DIR` to add restart-safe, disk-backed delivery retries. The directory must be on persistent storage if payloads should survive a deploy. Raises stores event or exception JSON there with private permissions, never stores the ingestion token, retries network errors, HTTP 408/429, and 5xx responses, and bounds the spool at 1,000 payloads or 100 MB. Leave it unset to preserve synchronous best-effort delivery.
|
|
27
|
+
|
|
13
28
|
The reporter fails open: network or serialization failures never replace the application exception. Rails filtered parameters are included; headers, raw bodies, query strings, and the request object are not.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Raises
|
|
6
|
+
class Delivery
|
|
7
|
+
def initialize(post:, spool:, warn:)
|
|
8
|
+
@post = post
|
|
9
|
+
@spool = spool
|
|
10
|
+
@warn = warn
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(payload, path: "v1/notices")
|
|
14
|
+
item = { "path" => path, "payload" => payload }
|
|
15
|
+
response = @post.call(path, payload)
|
|
16
|
+
return true unless response.respond_to?(:code)
|
|
17
|
+
return true if response.is_a?(Net::HTTPSuccess) || response.code.to_i.between?(200, 299)
|
|
18
|
+
|
|
19
|
+
response_accepted?(item, response.code.to_i)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
exception_accepted?(item || { "path" => path, "payload" => payload }, e)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def response_accepted?(item, status)
|
|
27
|
+
if retryable_status?(status) && @spool&.enqueue(item)
|
|
28
|
+
@warn.call("raises queued notice after HTTP #{status}")
|
|
29
|
+
true
|
|
30
|
+
else
|
|
31
|
+
@warn.call("raises rejected notice: HTTP #{status}")
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def exception_accepted?(item, error)
|
|
37
|
+
if @spool&.enqueue(item)
|
|
38
|
+
@warn.call("raises queued notice after #{error.class}")
|
|
39
|
+
true
|
|
40
|
+
else
|
|
41
|
+
@warn.call("raises subscriber failed: #{error.class}: #{error.message}")
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def retryable_status?(status)
|
|
47
|
+
status == 408 || status == 429 || status >= 500
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/raises/railtie.rb
CHANGED
data/lib/raises/spool.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spool_storage"
|
|
4
|
+
|
|
5
|
+
module Raises
|
|
6
|
+
class Spool
|
|
7
|
+
RETRYABLE_STATUS = [408, 429].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(directory, deliver:, warn:, **options)
|
|
10
|
+
@deliver = deliver
|
|
11
|
+
@warn = warn
|
|
12
|
+
@now = options.fetch(:now, -> { Time.now })
|
|
13
|
+
@random = options.fetch(:random, Random.new)
|
|
14
|
+
@start_on_enqueue = options.fetch(:start_on_enqueue, true)
|
|
15
|
+
@storage = SpoolStorage.new(directory, warn: warn, now: @now)
|
|
16
|
+
reset_process_state
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
reset_process_state if @pid != Process.pid
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
return if @thread&.alive?
|
|
23
|
+
|
|
24
|
+
@thread = Thread.new { run }
|
|
25
|
+
@thread.name = "raises-spool" if @thread.respond_to?(:name=)
|
|
26
|
+
@thread.report_on_exception = false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stop
|
|
31
|
+
return if @pid != Process.pid
|
|
32
|
+
|
|
33
|
+
thread = @mutex.synchronize do
|
|
34
|
+
@stopping = true
|
|
35
|
+
@condition.broadcast
|
|
36
|
+
@thread
|
|
37
|
+
end
|
|
38
|
+
thread&.join(1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enqueue(item)
|
|
42
|
+
accepted = @storage.store(item) == :stored
|
|
43
|
+
if accepted
|
|
44
|
+
start if @start_on_enqueue
|
|
45
|
+
wake if @start_on_enqueue
|
|
46
|
+
else
|
|
47
|
+
@warn.call("raises spool is full; notice was not queued")
|
|
48
|
+
end
|
|
49
|
+
accepted
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
@warn.call("raises could not queue notice: #{e.class}: #{e.message}")
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def drain_once(limit: 20)
|
|
56
|
+
@storage.each_due(limit: limit) do |path, envelope|
|
|
57
|
+
deliver(path, envelope)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def reset_process_state
|
|
64
|
+
@pid = Process.pid
|
|
65
|
+
@mutex = Mutex.new
|
|
66
|
+
@condition = ConditionVariable.new
|
|
67
|
+
@thread = nil
|
|
68
|
+
@stopping = false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def deliver(path, envelope)
|
|
72
|
+
response = @deliver.call(envelope.fetch("path"), envelope.fetch("payload"))
|
|
73
|
+
status = response_code(response)
|
|
74
|
+
if status.between?(200, 299)
|
|
75
|
+
@storage.delete(path)
|
|
76
|
+
elsif retryable_status?(status)
|
|
77
|
+
reschedule(path, envelope, retry_after(response))
|
|
78
|
+
else
|
|
79
|
+
@storage.delete(path)
|
|
80
|
+
@warn.call("raises rejected queued notice: HTTP #{status}")
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
reschedule(path, envelope)
|
|
84
|
+
@warn.call("raises queued notice retry failed: #{e.class}: #{e.message}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def response_code(response)
|
|
88
|
+
Integer(response.respond_to?(:code) ? response.code : response)
|
|
89
|
+
rescue ArgumentError, TypeError
|
|
90
|
+
500
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def retryable_status?(status)
|
|
94
|
+
RETRYABLE_STATUS.include?(status) || status >= 500
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def retry_after(response)
|
|
98
|
+
return unless response.respond_to?(:[])
|
|
99
|
+
|
|
100
|
+
seconds = Integer(response["Retry-After"], exception: false)
|
|
101
|
+
seconds&.clamp(1, 3_600)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def reschedule(path, envelope, requested_delay = nil)
|
|
105
|
+
attempts = envelope.fetch("attempts", 0).to_i + 1
|
|
106
|
+
base = [2**[attempts, 8].min, 300].min
|
|
107
|
+
delay = requested_delay || (base + (@random.rand * base * 0.2))
|
|
108
|
+
envelope["attempts"] = attempts
|
|
109
|
+
envelope["next_attempt_at"] = @now.call.to_f + delay
|
|
110
|
+
@storage.rewrite(path, envelope)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run
|
|
114
|
+
loop do
|
|
115
|
+
drain_once
|
|
116
|
+
@mutex.synchronize do
|
|
117
|
+
break if @stopping
|
|
118
|
+
|
|
119
|
+
@condition.wait(@mutex, 5)
|
|
120
|
+
break if @stopping
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
@warn.call("raises spool worker stopped: #{e.class}: #{e.message}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def wake
|
|
128
|
+
return if @pid != Process.pid
|
|
129
|
+
|
|
130
|
+
@mutex.synchronize { @condition.broadcast }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Raises
|
|
8
|
+
class SpoolStorage
|
|
9
|
+
MAX_NOTICES = 1_000
|
|
10
|
+
MAX_BYTES = 100 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
def initialize(directory, warn:, now:)
|
|
13
|
+
@directory = File.expand_path(directory)
|
|
14
|
+
@warn = warn
|
|
15
|
+
@now = now
|
|
16
|
+
prepare
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def store(item)
|
|
20
|
+
prepare
|
|
21
|
+
raw = JSON.generate(
|
|
22
|
+
"version" => 2,
|
|
23
|
+
"attempts" => 0,
|
|
24
|
+
"next_attempt_at" => @now.call.to_f,
|
|
25
|
+
"path" => item.fetch("path"),
|
|
26
|
+
"payload" => item.fetch("payload")
|
|
27
|
+
)
|
|
28
|
+
with_directory_lock do
|
|
29
|
+
return :full unless capacity_for?(raw)
|
|
30
|
+
|
|
31
|
+
atomic_write(File.join(@directory, filename), raw)
|
|
32
|
+
end
|
|
33
|
+
:stored
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each_due(limit:)
|
|
37
|
+
processed = 0
|
|
38
|
+
files.each do |path|
|
|
39
|
+
break if processed >= limit
|
|
40
|
+
|
|
41
|
+
File.open(path, File::RDWR) do |file|
|
|
42
|
+
next unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
43
|
+
|
|
44
|
+
envelope = parse(file.read, path)
|
|
45
|
+
next unless envelope
|
|
46
|
+
next if envelope.fetch("next_attempt_at", 0).to_f > @now.call.to_f
|
|
47
|
+
|
|
48
|
+
processed += 1
|
|
49
|
+
yield path, envelope
|
|
50
|
+
end
|
|
51
|
+
rescue Errno::ENOENT
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
processed
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete(path)
|
|
58
|
+
File.delete(path)
|
|
59
|
+
rescue Errno::ENOENT
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rewrite(path, envelope)
|
|
64
|
+
atomic_write(path, JSON.generate(envelope))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def prepare
|
|
70
|
+
FileUtils.mkdir_p(@directory, mode: 0o700)
|
|
71
|
+
File.chmod(0o700, @directory)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def files
|
|
75
|
+
Dir.glob(File.join(@directory, "*.json"))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def capacity_for?(raw)
|
|
79
|
+
paths = files
|
|
80
|
+
return false if paths.length >= MAX_NOTICES
|
|
81
|
+
|
|
82
|
+
paths.sum { |path| file_size(path) } + raw.bytesize <= MAX_BYTES
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def file_size(path)
|
|
86
|
+
File.size(path)
|
|
87
|
+
rescue Errno::ENOENT
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def filename
|
|
92
|
+
format("%<time>020d-%<random>s.json", time: (@now.call.to_f * 1_000_000).to_i, random: SecureRandom.hex(8))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def with_directory_lock
|
|
96
|
+
File.open(File.join(@directory, ".lock"), File::WRONLY | File::CREAT, 0o600) do |lock|
|
|
97
|
+
lock.flock(File::LOCK_EX)
|
|
98
|
+
yield
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def atomic_write(path, raw)
|
|
103
|
+
temporary = "#{path}.tmp-#{Process.pid}-#{SecureRandom.hex(4)}"
|
|
104
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
105
|
+
file.write(raw)
|
|
106
|
+
file.flush
|
|
107
|
+
file.fsync
|
|
108
|
+
end
|
|
109
|
+
File.rename(temporary, path)
|
|
110
|
+
ensure
|
|
111
|
+
File.delete(temporary) if defined?(temporary) && File.exist?(temporary)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse(raw, path)
|
|
115
|
+
envelope = JSON.parse(raw)
|
|
116
|
+
return envelope if envelope.is_a?(Hash) && envelope["path"].is_a?(String) && envelope["payload"].is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
if envelope.is_a?(Hash) && envelope["notice"].is_a?(Hash)
|
|
119
|
+
envelope["version"] = 2
|
|
120
|
+
envelope["path"] = "v1/notices"
|
|
121
|
+
envelope["payload"] = envelope.delete("notice")
|
|
122
|
+
return envelope
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
raise JSON::ParserError, "invalid envelope"
|
|
126
|
+
rescue JSON::ParserError, TypeError => e
|
|
127
|
+
quarantine(path)
|
|
128
|
+
@warn.call("raises quarantined corrupt spool entry: #{e.message}")
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def quarantine(path)
|
|
133
|
+
target = path.sub(/\.json\z/, ".corrupt")
|
|
134
|
+
File.rename(path, target)
|
|
135
|
+
File.chmod(0o600, target)
|
|
136
|
+
rescue Errno::ENOENT
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/raises/subscriber.rb
CHANGED
|
@@ -5,10 +5,21 @@ require "net/http"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
|
|
7
7
|
module Raises
|
|
8
|
+
# Rails.error integration and explicit application notices share one delivery path.
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
10
|
class Subscriber
|
|
11
|
+
def initialize
|
|
12
|
+
@spool = build_spool
|
|
13
|
+
@delivery = Delivery.new(post: method(:post), spool: @spool, warn: ->(message) { warn(message) })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start = tap { @spool&.start }
|
|
17
|
+
|
|
9
18
|
def report(error, handled:, severity:, context:, source: nil)
|
|
10
19
|
return unless report?
|
|
11
20
|
|
|
21
|
+
start
|
|
22
|
+
|
|
12
23
|
notice = {
|
|
13
24
|
env: env_name,
|
|
14
25
|
revision: revision,
|
|
@@ -22,13 +33,55 @@ module Raises
|
|
|
22
33
|
request: request_from(context)
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
@delivery.call(notice, path: "v1/notices")
|
|
26
37
|
rescue StandardError => e
|
|
27
38
|
warn("raises subscriber failed: #{e.class}: #{e.message}")
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def notify(message, level: :info, context: {}, source: nil)
|
|
43
|
+
message = message.to_s.strip
|
|
44
|
+
level = level.to_s
|
|
45
|
+
validate_event!(message, level, context, source)
|
|
46
|
+
return false unless report?
|
|
47
|
+
|
|
48
|
+
start
|
|
49
|
+
event = {
|
|
50
|
+
env: env_name,
|
|
51
|
+
revision: revision,
|
|
52
|
+
level: level,
|
|
53
|
+
message: message,
|
|
54
|
+
source: source,
|
|
55
|
+
context: json_safe(context)
|
|
56
|
+
}
|
|
57
|
+
@delivery.call(event, path: "v1/events")
|
|
58
|
+
rescue ArgumentError
|
|
59
|
+
raise
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
warn("raises notify failed: #{e.class}: #{e.message}")
|
|
62
|
+
false
|
|
28
63
|
end
|
|
29
64
|
|
|
30
65
|
private
|
|
31
66
|
|
|
67
|
+
def validate_event!(message, level, context, source)
|
|
68
|
+
raise ArgumentError, "message is required" if message.empty?
|
|
69
|
+
raise ArgumentError, "message is too long" if message.length > 2_000
|
|
70
|
+
raise ArgumentError, "level must be info, warning, or error" unless %w[info warning error].include?(level)
|
|
71
|
+
raise ArgumentError, "source is too long" if source.to_s.length > 120
|
|
72
|
+
raise ArgumentError, "context must be a hash" unless context.respond_to?(:each_pair)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_spool
|
|
76
|
+
directory = ENV["RAISES_SPOOL_DIR"].to_s
|
|
77
|
+
return if directory.empty?
|
|
78
|
+
|
|
79
|
+
Spool.new(directory, deliver: method(:post), warn: ->(message) { warn(message) })
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
warn("raises spool unavailable: #{e.class}: #{e.message}")
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
32
85
|
def report?
|
|
33
86
|
return false if url.to_s.empty? || token.to_s.empty?
|
|
34
87
|
return true if ENV["RAISES_REPORT"] == "1"
|
|
@@ -113,8 +166,8 @@ module Raises
|
|
|
113
166
|
end
|
|
114
167
|
end
|
|
115
168
|
|
|
116
|
-
def post(
|
|
117
|
-
uri = URI.join(url.end_with?("/") ? url : "#{url}/",
|
|
169
|
+
def post(path, payload)
|
|
170
|
+
uri = URI.join(url.end_with?("/") ? url : "#{url}/", path)
|
|
118
171
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
119
172
|
http.use_ssl = uri.scheme == "https"
|
|
120
173
|
http.open_timeout = timeout("RAISES_OPEN_TIMEOUT", 1)
|
|
@@ -123,10 +176,8 @@ module Raises
|
|
|
123
176
|
request["Authorization"] = "Bearer #{token}"
|
|
124
177
|
request["Content-Type"] = "application/json"
|
|
125
178
|
request["User-Agent"] = "raises-ruby/#{Raises::VERSION}"
|
|
126
|
-
request.body = JSON.generate(
|
|
127
|
-
|
|
128
|
-
warn("raises rejected notice: HTTP #{response.code}") unless response.is_a?(Net::HTTPSuccess)
|
|
129
|
-
response
|
|
179
|
+
request.body = JSON.generate(payload)
|
|
180
|
+
http.request(request)
|
|
130
181
|
end
|
|
131
182
|
|
|
132
183
|
def timeout(name, fallback)
|
|
@@ -136,4 +187,5 @@ module Raises
|
|
|
136
187
|
fallback
|
|
137
188
|
end
|
|
138
189
|
end
|
|
190
|
+
# rubocop:enable Metrics/ClassLength
|
|
139
191
|
end
|
data/lib/raises/version.rb
CHANGED
data/lib/raises.rb
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "raises/version"
|
|
4
|
+
require "raises/spool_storage"
|
|
5
|
+
require "raises/spool"
|
|
6
|
+
require "raises/delivery"
|
|
4
7
|
require "raises/subscriber"
|
|
5
8
|
require "raises/railtie" if defined?(Rails::Railtie)
|
|
6
9
|
|
|
7
10
|
module Raises
|
|
11
|
+
class << self
|
|
12
|
+
def notify(message, level: :info, context: {}, source: nil)
|
|
13
|
+
subscriber.notify(message, level: level, context: context, source: source)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def subscriber
|
|
17
|
+
@subscriber ||= Subscriber.new.start
|
|
18
|
+
end
|
|
19
|
+
end
|
|
8
20
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raises
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clayton Lengel-Zigich
|
|
@@ -32,7 +32,10 @@ files:
|
|
|
32
32
|
- LICENSE.txt
|
|
33
33
|
- README.md
|
|
34
34
|
- lib/raises.rb
|
|
35
|
+
- lib/raises/delivery.rb
|
|
35
36
|
- lib/raises/railtie.rb
|
|
37
|
+
- lib/raises/spool.rb
|
|
38
|
+
- lib/raises/spool_storage.rb
|
|
36
39
|
- lib/raises/subscriber.rb
|
|
37
40
|
- lib/raises/version.rb
|
|
38
41
|
homepage: https://raises.dev
|