tinymonrb 0.2.0 → 0.4.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/lib/tinymon/client.rb +27 -5
- data/lib/tinymon/event_builder.rb +11 -2
- data/lib/tinymon/rack.rb +56 -0
- data/lib/tinymon/railtie.rb +13 -0
- data/lib/tinymon/transport.rb +99 -34
- data/lib/tinymon/version.rb +1 -1
- data/lib/tinymon.rb +5 -62
- data/lib/tinymonrb.rb +15 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 721bdc7f477b8281a78914201aadf4367fe02206248735b91dc6c564459c4050
|
|
4
|
+
data.tar.gz: 5b119d9cd4b65d8cef7648b779be6a84f93ffca1c78c7fd0560e63529ca31560
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 911fac08481f040b354966c3baf7d14db96d9a5deab26b86bd077b432c430eca3dcda7c9dc84e0fc600efd344c4c8354c31137651e3b22cada233e976b142a47
|
|
7
|
+
data.tar.gz: b3cf2caffb694ba16700b9f69d758d2f025d87bae3a4c1849bc10d0899118905a096082b79a29aa87f23b786b69ed48f82fb7231c419c3b2d02bf40e8ef032a2
|
data/lib/tinymon/client.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Tinymon
|
|
|
37
37
|
return if rand > @sample_rate
|
|
38
38
|
event = _prepare(exception)
|
|
39
39
|
return if event.nil?
|
|
40
|
-
@transport.
|
|
40
|
+
@transport.send_event(event)
|
|
41
41
|
rescue StandardError
|
|
42
42
|
# SWALLOW. The SDK must never throw into the host app.
|
|
43
43
|
nil
|
|
@@ -48,22 +48,44 @@ module Tinymon
|
|
|
48
48
|
return if event.nil?
|
|
49
49
|
event["level"] = level
|
|
50
50
|
event["exception"]["type"] = "Message"
|
|
51
|
-
@transport.
|
|
51
|
+
@transport.send_event(event)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Per-event context capture. Use from Rack middleware; do NOT use
|
|
57
|
+
# Tinymon.set_user/set_tag for per-request data — they race under
|
|
58
|
+
# concurrency.
|
|
59
|
+
def capture_exception_with_context(exception, request: nil, user: nil, tags: nil)
|
|
60
|
+
return if rand > @sample_rate
|
|
61
|
+
event = _prepare(exception, ctx: { request: request, user: user, tags: tags })
|
|
62
|
+
return if event.nil?
|
|
63
|
+
@transport.send_event(event)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Block until queued events are delivered (or timeout). Call before a
|
|
69
|
+
# short-lived process exits so events aren't dropped.
|
|
70
|
+
def flush(timeout = 2.0)
|
|
71
|
+
@transport.flush(timeout)
|
|
52
72
|
rescue StandardError
|
|
53
73
|
nil
|
|
54
74
|
end
|
|
55
75
|
|
|
56
76
|
private
|
|
57
77
|
|
|
58
|
-
def _prepare(exception)
|
|
78
|
+
def _prepare(exception, ctx: nil)
|
|
59
79
|
snap = SCOPE.snapshot
|
|
80
|
+
ctx ||= {}
|
|
60
81
|
event = EventBuilder.build(
|
|
61
82
|
exception,
|
|
62
83
|
release: @release,
|
|
63
84
|
environment: @environment,
|
|
64
|
-
user: snap[:user],
|
|
65
|
-
tags: snap[:tags],
|
|
85
|
+
user: ctx[:user] || snap[:user],
|
|
86
|
+
tags: ctx[:tags] || snap[:tags],
|
|
66
87
|
breadcrumbs: snap[:breadcrumbs],
|
|
88
|
+
request: ctx[:request],
|
|
67
89
|
)
|
|
68
90
|
# Default scrub BEFORE before_send.
|
|
69
91
|
Scrub.scrub_event(event, scrub_url_query: @scrub_url_query, scrub_patterns: @scrub_patterns)
|
|
@@ -12,7 +12,7 @@ module Tinymon
|
|
|
12
12
|
|
|
13
13
|
SENSITIVE = /password|token|secret|auth|card|cvv|ssn/i.freeze
|
|
14
14
|
|
|
15
|
-
def build(exception, release: nil, environment: nil, user: nil, tags: nil, breadcrumbs: nil, url: nil)
|
|
15
|
+
def build(exception, release: nil, environment: nil, user: nil, tags: nil, breadcrumbs: nil, url: nil, request: nil)
|
|
16
16
|
event = {
|
|
17
17
|
"event_id" => SecureRandom.uuid,
|
|
18
18
|
"timestamp" => Time.now.to_f,
|
|
@@ -30,7 +30,16 @@ module Tinymon
|
|
|
30
30
|
}
|
|
31
31
|
event["release"] = release if release
|
|
32
32
|
event["environment"] = environment if environment
|
|
33
|
-
|
|
33
|
+
if request && (request[:method] || request["method"] || request[:url] || request["url"])
|
|
34
|
+
req = {}
|
|
35
|
+
m = request[:method] || request["method"]
|
|
36
|
+
u = request[:url] || request["url"]
|
|
37
|
+
req["method"] = m if m
|
|
38
|
+
req["url"] = u if u
|
|
39
|
+
event["request"] = req
|
|
40
|
+
elsif url
|
|
41
|
+
event["request"] = { "url" => url }
|
|
42
|
+
end
|
|
34
43
|
event
|
|
35
44
|
end
|
|
36
45
|
|
data/lib/tinymon/rack.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rack middleware. Insert into ANY Rack app (Rails, Sinatra, Hanami,
|
|
4
|
+
# plain Rack) to capture in-request exceptions. Captures with per-event
|
|
5
|
+
# request context (method + URL) and RE-RAISES so the framework's own
|
|
6
|
+
# error handling still fires.
|
|
7
|
+
#
|
|
8
|
+
# Manual insert:
|
|
9
|
+
# require "tinymonrb"
|
|
10
|
+
# use Tinymon::Rack # Sinatra / plain Rack
|
|
11
|
+
# # Rails:
|
|
12
|
+
# config.middleware.use Tinymon::Rack
|
|
13
|
+
#
|
|
14
|
+
# Or use the Railtie for auto-insert in Rails apps (loaded automatically
|
|
15
|
+
# when Rails is detected at runtime).
|
|
16
|
+
|
|
17
|
+
module Tinymon
|
|
18
|
+
class Rack
|
|
19
|
+
def initialize(app)
|
|
20
|
+
@app = app
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(env)
|
|
24
|
+
@app.call(env)
|
|
25
|
+
rescue StandardError, ScriptError => e
|
|
26
|
+
capture(e, env)
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def capture(exc, env)
|
|
33
|
+
mod = ::Tinymon
|
|
34
|
+
client = mod.instance_variable_get(:@client)
|
|
35
|
+
return unless client
|
|
36
|
+
|
|
37
|
+
url = build_url(env)
|
|
38
|
+
method = env["REQUEST_METHOD"]
|
|
39
|
+
client.capture_exception_with_context(
|
|
40
|
+
exc,
|
|
41
|
+
request: { method: method, url: url }
|
|
42
|
+
)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
# never let the SDK break the request
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_url(env)
|
|
49
|
+
scheme = env["rack.url_scheme"] || "http"
|
|
50
|
+
host = env["HTTP_HOST"] || env["SERVER_NAME"] || ""
|
|
51
|
+
path = env["REQUEST_URI"] ||
|
|
52
|
+
"#{env['SCRIPT_NAME']}#{env['PATH_INFO']}#{env['QUERY_STRING'] && !env['QUERY_STRING'].empty? ? "?#{env['QUERY_STRING']}" : ''}"
|
|
53
|
+
host.empty? ? path : "#{scheme}://#{host}#{path}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Auto-insert Tinymon::Rack into the Rails middleware stack. Loaded only
|
|
4
|
+
# when Rails is present at require time — no hard Rails dependency.
|
|
5
|
+
require_relative "rack"
|
|
6
|
+
|
|
7
|
+
module Tinymon
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
initializer "tinymon.middleware" do |app|
|
|
10
|
+
app.middleware.use Tinymon::Rack
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/tinymon/transport.rb
CHANGED
|
@@ -5,46 +5,55 @@ require "net/http"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
|
|
7
7
|
module Tinymon
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# HTTP transport. stdlib-only. Sends each event immediately on a background
|
|
9
|
+
# worker thread; events are only queued when the network fails, and that
|
|
10
|
+
# retry queue is drained with exponential backoff. This matches how
|
|
11
|
+
# production error SDKs behave — the error lands in the dashboard now, not
|
|
12
|
+
# on a 5-second timer. at_exit drains anything still pending on shutdown.
|
|
10
13
|
class Transport
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
MAX_QUEUE = 30 # cap on retry buffer (dead-network memory guard)
|
|
15
|
+
REQUEST_TIMEOUT = 5 # seconds
|
|
16
|
+
BACKOFF_START = 1.0 # seconds
|
|
17
|
+
BACKOFF_MAX = 30.0 # seconds
|
|
15
18
|
|
|
16
19
|
def initialize(endpoint, dsn)
|
|
17
20
|
@endpoint = endpoint
|
|
18
21
|
@dsn = dsn
|
|
19
|
-
@
|
|
22
|
+
@pending = [] # fresh events — sent immediately
|
|
23
|
+
@retry = [] # failed events — retried with backoff
|
|
20
24
|
@mutex = Mutex.new
|
|
25
|
+
@cond = ConditionVariable.new
|
|
26
|
+
@backoff = BACKOFF_START
|
|
21
27
|
@stop = false
|
|
22
28
|
@thread = Thread.new { run_loop }
|
|
23
29
|
@thread.name = "tinymon-transport" if @thread.respond_to?(:name=)
|
|
24
30
|
at_exit { shutdown }
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
# Queue for immediate send on the worker thread (non-blocking).
|
|
34
|
+
def send_event(event)
|
|
29
35
|
@mutex.synchronize do
|
|
30
|
-
@
|
|
31
|
-
@
|
|
32
|
-
should_flush = @queue.length >= MAX_BATCH
|
|
36
|
+
@pending.push(event)
|
|
37
|
+
@cond.signal
|
|
33
38
|
end
|
|
34
|
-
flush if should_flush
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
# Block until pending + retry events are sent, or timeout elapses.
|
|
42
|
+
def flush(timeout = 2.0)
|
|
43
|
+
deadline = monotonic + timeout
|
|
44
|
+
loop do
|
|
45
|
+
empty = @mutex.synchronize { @pending.empty? && @retry.empty? }
|
|
46
|
+
return if empty
|
|
47
|
+
break if monotonic >= deadline
|
|
48
|
+
@mutex.synchronize { @cond.signal }
|
|
49
|
+
sleep(0.02)
|
|
41
50
|
end
|
|
42
|
-
return if batch.nil? || batch.empty?
|
|
43
|
-
batch.each { |event| send_one(event) }
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
private
|
|
47
54
|
|
|
55
|
+
# Returns true on success OR an unrecoverable 4xx (retrying a client bug
|
|
56
|
+
# won't help); false on 5xx / network failure.
|
|
48
57
|
def send_one(event)
|
|
49
58
|
uri = URI.parse(@endpoint)
|
|
50
59
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
@@ -57,33 +66,89 @@ module Tinymon
|
|
|
57
66
|
})
|
|
58
67
|
req.body = JSON.dump(event)
|
|
59
68
|
begin
|
|
60
|
-
http.request(req)
|
|
69
|
+
res = http.request(req)
|
|
70
|
+
code = res.code.to_i
|
|
71
|
+
return true if code >= 200 && code < 300
|
|
72
|
+
return true if code >= 400 && code < 500 # client bug — drop, don't retry
|
|
73
|
+
false
|
|
61
74
|
rescue StandardError
|
|
62
|
-
|
|
63
|
-
@mutex.synchronize do
|
|
64
|
-
@queue.unshift(event) if @queue.length < MAX_QUEUE
|
|
65
|
-
end
|
|
75
|
+
false
|
|
66
76
|
end
|
|
67
77
|
end
|
|
68
78
|
|
|
69
79
|
def run_loop
|
|
70
80
|
until @stop
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
event = next_pending
|
|
82
|
+
if event
|
|
83
|
+
queue_retry(event) unless send_one(event)
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
next if drain_retry
|
|
88
|
+
|
|
89
|
+
# Idle — wait for new work or the next retry window.
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
wait = @retry.empty? ? nil : @backoff
|
|
92
|
+
@cond.wait(@mutex, wait) unless @stop
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def next_pending
|
|
98
|
+
@mutex.synchronize { @pending.shift }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def queue_retry(event)
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
@retry.shift while @retry.length >= MAX_QUEUE
|
|
104
|
+
@retry.push(event)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def drain_retry
|
|
109
|
+
batch = @mutex.synchronize do
|
|
110
|
+
next nil if @retry.empty?
|
|
111
|
+
b = @retry.dup
|
|
112
|
+
@retry.clear
|
|
113
|
+
b
|
|
114
|
+
end
|
|
115
|
+
return false if batch.nil?
|
|
116
|
+
|
|
117
|
+
any_failed = false
|
|
118
|
+
batch.each do |event|
|
|
119
|
+
unless send_one(event)
|
|
120
|
+
any_failed = true
|
|
121
|
+
queue_retry(event)
|
|
76
122
|
end
|
|
77
123
|
end
|
|
124
|
+
if any_failed
|
|
125
|
+
@backoff = [@backoff * 2, BACKOFF_MAX].min
|
|
126
|
+
sleep(@backoff)
|
|
127
|
+
else
|
|
128
|
+
@backoff = BACKOFF_START
|
|
129
|
+
end
|
|
130
|
+
true
|
|
78
131
|
end
|
|
79
132
|
|
|
80
133
|
def shutdown
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
# Best-effort: deliver anything still pending before exit.
|
|
135
|
+
deadline = monotonic + 2.0
|
|
136
|
+
while monotonic < deadline
|
|
137
|
+
event = next_pending
|
|
138
|
+
event ||= @mutex.synchronize { @retry.shift }
|
|
139
|
+
break if event.nil?
|
|
140
|
+
send_one(event)
|
|
141
|
+
end
|
|
142
|
+
@mutex.synchronize do
|
|
143
|
+
@stop = true
|
|
144
|
+
@cond.broadcast
|
|
86
145
|
end
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def monotonic
|
|
151
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
87
152
|
end
|
|
88
153
|
end
|
|
89
154
|
end
|
data/lib/tinymon/version.rb
CHANGED
data/lib/tinymon.rb
CHANGED
|
@@ -1,64 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "tinymon/client"
|
|
9
|
-
require_relative "tinymon/integrations"
|
|
10
|
-
|
|
11
|
-
# Public API:
|
|
12
|
-
#
|
|
13
|
-
# require "tinymon"
|
|
14
|
-
# Tinymon.init(dsn: "tm_pub_xxx", environment: "production", release: "1.0.0")
|
|
15
|
-
#
|
|
16
|
-
# begin
|
|
17
|
-
# risky_thing
|
|
18
|
-
# rescue => e
|
|
19
|
-
# Tinymon.capture_exception(e)
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
module Tinymon
|
|
23
|
-
@client = nil
|
|
24
|
-
|
|
25
|
-
class << self
|
|
26
|
-
def init(dsn:, endpoint: nil, environment: nil, release: nil, sample_rate: 1.0, before_send: nil)
|
|
27
|
-
@client = Client.new(
|
|
28
|
-
dsn: dsn,
|
|
29
|
-
endpoint: endpoint,
|
|
30
|
-
environment: environment,
|
|
31
|
-
release: release,
|
|
32
|
-
sample_rate: sample_rate,
|
|
33
|
-
before_send: before_send,
|
|
34
|
-
)
|
|
35
|
-
Integrations.install(@client)
|
|
36
|
-
@client
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def capture_exception(exception)
|
|
40
|
-
@client&.capture_exception(exception)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def capture_message(message, level: "info")
|
|
44
|
-
@client&.capture_message(message, level: level)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def set_user(user)
|
|
48
|
-
SCOPE.set_user(user)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def set_tag(key, value)
|
|
52
|
-
SCOPE.set_tag(key, value)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def add_breadcrumb(crumb)
|
|
56
|
-
SCOPE.add_breadcrumb(crumb)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Exposed for the cross-language contract test in test/test_contract.rb.
|
|
60
|
-
def _build_event(exception, **kwargs)
|
|
61
|
-
EventBuilder.build(exception, **kwargs)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
3
|
+
# Back-compat shim. The gem is published as `tinymonrb`, so the canonical
|
|
4
|
+
# entry point is `require "tinymonrb"`. This file only exists so that older
|
|
5
|
+
# code doing `require "tinymon"` keeps working — it loads the full SDK
|
|
6
|
+
# (context capture, Rack middleware, Rails railtie) rather than a partial copy.
|
|
7
|
+
require_relative "tinymonrb"
|
data/lib/tinymonrb.rb
CHANGED
|
@@ -7,6 +7,9 @@ require_relative "tinymon/scope"
|
|
|
7
7
|
require_relative "tinymon/transport"
|
|
8
8
|
require_relative "tinymon/client"
|
|
9
9
|
require_relative "tinymon/integrations"
|
|
10
|
+
require_relative "tinymon/rack"
|
|
11
|
+
# Rails auto-insert — only loaded when Rails is present at require time.
|
|
12
|
+
require_relative "tinymon/railtie" if defined?(::Rails::Railtie)
|
|
10
13
|
|
|
11
14
|
# Public API:
|
|
12
15
|
#
|
|
@@ -53,10 +56,22 @@ module Tinymon
|
|
|
53
56
|
@client&.capture_exception(exception)
|
|
54
57
|
end
|
|
55
58
|
|
|
59
|
+
# Per-event context capture. Use from Rack middleware; do NOT use
|
|
60
|
+
# set_user/set_tag for per-request data (they race under concurrency).
|
|
61
|
+
def capture_exception_with_context(exception, request: nil, user: nil, tags: nil)
|
|
62
|
+
@client&.capture_exception_with_context(exception, request: request, user: user, tags: tags)
|
|
63
|
+
end
|
|
64
|
+
|
|
56
65
|
def capture_message(message, level: "info")
|
|
57
66
|
@client&.capture_message(message, level: level)
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
# Block until queued events are delivered (or timeout). Call before a
|
|
70
|
+
# short-lived process exits so events aren't dropped.
|
|
71
|
+
def flush(timeout = 2.0)
|
|
72
|
+
@client&.flush(timeout)
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
def set_user(user)
|
|
61
76
|
SCOPE.set_user(user)
|
|
62
77
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tinymonrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tinymon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json_schemer
|
|
@@ -48,6 +48,8 @@ files:
|
|
|
48
48
|
- lib/tinymon/client.rb
|
|
49
49
|
- lib/tinymon/event_builder.rb
|
|
50
50
|
- lib/tinymon/integrations.rb
|
|
51
|
+
- lib/tinymon/rack.rb
|
|
52
|
+
- lib/tinymon/railtie.rb
|
|
51
53
|
- lib/tinymon/scope.rb
|
|
52
54
|
- lib/tinymon/scrub.rb
|
|
53
55
|
- lib/tinymon/stacktrace.rb
|