sillydev 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/exe/sillydev +6 -0
- data/lib/sillydev/cli.rb +295 -0
- data/lib/sillydev/client.rb +199 -0
- data/lib/sillydev/console.rb +1131 -0
- data/lib/sillydev/envelope.rb +84 -0
- data/lib/sillydev/errors.rb +160 -0
- data/lib/sillydev/mcp_server.rb +193 -0
- data/lib/sillydev/monitoring/aggregator.rb +107 -0
- data/lib/sillydev/monitoring/client.rb +674 -0
- data/lib/sillydev/monitoring/discordrb.rb +131 -0
- data/lib/sillydev/monitoring/http.rb +31 -0
- data/lib/sillydev/monitoring/rack.rb +114 -0
- data/lib/sillydev/monitoring/rails.rb +41 -0
- data/lib/sillydev/monitoring/runtime.rb +96 -0
- data/lib/sillydev/monitoring/serialize.rb +168 -0
- data/lib/sillydev/monitoring.rb +227 -0
- data/lib/sillydev/resources.rb +844 -0
- data/lib/sillydev/transport.rb +71 -0
- data/lib/sillydev/version.rb +5 -0
- data/lib/sillydev/websocket.rb +248 -0
- data/lib/sillydev.rb +20 -0
- metadata +67 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
require_relative "monitoring/client"
|
|
5
|
+
|
|
6
|
+
module SillyDev
|
|
7
|
+
# Opt-in application monitoring for Silly Development.
|
|
8
|
+
#
|
|
9
|
+
# Nothing is sent, no thread is started, no hook is installed and no environment variable is read
|
|
10
|
+
# until you call SillyDev::Monitoring.init with a monitoring key (or with SILLYDEV_MONITORING_KEY
|
|
11
|
+
# set):
|
|
12
|
+
#
|
|
13
|
+
# require "sillydev/monitoring"
|
|
14
|
+
#
|
|
15
|
+
# SillyDev::Monitoring.init # reads SILLYDEV_MONITORING_KEY; does nothing without a key
|
|
16
|
+
# SillyDev::Monitoring.increment("orders.created", tags: { plan: "free" })
|
|
17
|
+
#
|
|
18
|
+
# Until then every method here does nothing and never raises. require "sillydev" does not load
|
|
19
|
+
# this file. Docs: https://sillydev.co.uk/docs/monitoring
|
|
20
|
+
module Monitoring
|
|
21
|
+
KEY_RE = /\Asdevm_([1-9][0-9]*)_/
|
|
22
|
+
|
|
23
|
+
@current = nil
|
|
24
|
+
@init_lock = Mutex.new
|
|
25
|
+
@collectors = []
|
|
26
|
+
@hooks_installed = false
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Called with the client once per flush window, on its thread (the Discord integration adds
|
|
30
|
+
# one for its gauges).
|
|
31
|
+
attr_reader :collectors
|
|
32
|
+
|
|
33
|
+
# "sdevm_<N>_..." gives "https://ingest<N>.sillydev.co.uk" (nil for anything else).
|
|
34
|
+
def endpoint_for_key(key)
|
|
35
|
+
match = KEY_RE.match(key.to_s)
|
|
36
|
+
match ? "https://ingest#{match[1]}.sillydev.co.uk" : nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Starts monitoring. Without a key (argument or SILLYDEV_MONITORING_KEY) nothing happens.
|
|
40
|
+
#
|
|
41
|
+
# Options left as nil fall back to SILLYDEV_MONITORING_KEY, SILLYDEV_MONITORING_URL,
|
|
42
|
+
# SILLYDEV_MONITORING_SERVICE, SILLYDEV_MONITORING_ENVIRONMENT and SILLYDEV_MONITORING_RELEASE.
|
|
43
|
+
# SILLYDEV_MONITORING_DISABLED=1 (or enabled: false) turns it off. Calling init again shuts the
|
|
44
|
+
# previous client down and starts a new one. Returns the client; client.enabled? says whether
|
|
45
|
+
# it is sending.
|
|
46
|
+
def init(key = nil, endpoint: nil, service: nil, environment: nil, release: nil, runtime_metrics: true,
|
|
47
|
+
capture_unhandled: true, sample_rate: 1.0, before_send: nil, flush_interval: 10.0,
|
|
48
|
+
max_buffer_bytes: 1_000_000, slow_threshold_ms: 1000, logger: nil, debug: false, enabled: true)
|
|
49
|
+
@init_lock.synchronize do
|
|
50
|
+
previous = @current
|
|
51
|
+
@current = nil
|
|
52
|
+
previous&.shutdown(2.0)
|
|
53
|
+
|
|
54
|
+
key ||= env("SILLYDEV_MONITORING_KEY")
|
|
55
|
+
disabled = %w[1 true yes on].include?(env("SILLYDEV_MONITORING_DISABLED").to_s.downcase)
|
|
56
|
+
endpoint ||= env("SILLYDEV_MONITORING_URL")
|
|
57
|
+
active = enabled && !disabled && key ? true : false
|
|
58
|
+
if active && endpoint.nil?
|
|
59
|
+
endpoint = endpoint_for_key(key)
|
|
60
|
+
if endpoint.nil?
|
|
61
|
+
warn "sillydev.monitoring: the key is not a server key (sdevm_<n>_...) and no endpoint was given; " \
|
|
62
|
+
"monitoring is off"
|
|
63
|
+
active = false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
client = Client.new(
|
|
68
|
+
key, endpoint: endpoint, service: service || env("SILLYDEV_MONITORING_SERVICE"),
|
|
69
|
+
environment: environment || env("SILLYDEV_MONITORING_ENVIRONMENT"),
|
|
70
|
+
release: release || env("SILLYDEV_MONITORING_RELEASE"), runtime_metrics: runtime_metrics,
|
|
71
|
+
capture_unhandled: capture_unhandled, sample_rate: sample_rate, before_send: before_send,
|
|
72
|
+
flush_interval: flush_interval, max_buffer_bytes: max_buffer_bytes,
|
|
73
|
+
slow_threshold_ms: slow_threshold_ms, logger: logger, debug: debug, active: active
|
|
74
|
+
)
|
|
75
|
+
@current = client
|
|
76
|
+
if client.active?
|
|
77
|
+
install_process_hooks
|
|
78
|
+
client.start
|
|
79
|
+
end
|
|
80
|
+
client
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The client made by the last init, if any.
|
|
85
|
+
def client
|
|
86
|
+
@current
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# True while a client with a key is running and has not been stopped.
|
|
90
|
+
def enabled?
|
|
91
|
+
client = @current
|
|
92
|
+
client ? client.enabled? : false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Adds value to a counter.
|
|
96
|
+
def increment(name, value = 1, tags: nil)
|
|
97
|
+
@current&.increment(name, value, tags)
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Records the latest value of a gauge.
|
|
102
|
+
def gauge(name, value, tags: nil)
|
|
103
|
+
@current&.gauge(name, value, tags)
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Records a duration in milliseconds in a histogram.
|
|
108
|
+
def timing(name, ms, tags: nil)
|
|
109
|
+
@current&.timing(name, ms, tags)
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Times the block into the name histogram (and as a slow operation over the threshold), and
|
|
114
|
+
# returns the block's value.
|
|
115
|
+
def time(name, tags: nil)
|
|
116
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
117
|
+
yield
|
|
118
|
+
ensure
|
|
119
|
+
finish_timer(name, start, tags) if start
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Records something that happened (a deploy, a sign-up). Subject to sample_rate.
|
|
123
|
+
def event(name, attributes = nil)
|
|
124
|
+
@current&.event(name, attributes)
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Reports an exception (the one being handled, $!, when none is given).
|
|
129
|
+
def capture_exception(exception = $!, tags: nil, context: nil, fingerprint: nil, handled: true)
|
|
130
|
+
@current&.capture_exception(exception, tags: tags, context: context, fingerprint: fingerprint, handled: handled)
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Reports a message as an error without an exception.
|
|
135
|
+
def capture_message(message, level: "error", tags: nil, context: nil)
|
|
136
|
+
@current&.capture_message(message, level: level, tags: tags, context: context)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Reports a slow operation (kind is "http", "discord", "task" or "custom").
|
|
141
|
+
def record_slow(name, duration_ms, kind: "custom", tags: nil, attributes: nil)
|
|
142
|
+
@current&.record_slow(name, duration_ms, kind: kind, tags: tags, attributes: attributes)
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Marks this instance alive now (sent straight away by the background thread).
|
|
147
|
+
def heartbeat
|
|
148
|
+
@current&.heartbeat
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Sends everything buffered now. True when nothing is left to send.
|
|
153
|
+
def flush(timeout = 2.0)
|
|
154
|
+
client = @current
|
|
155
|
+
client ? client.flush(timeout) : false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Flushes and stops. Later calls do nothing until init is called again.
|
|
159
|
+
def shutdown(timeout = 2.0)
|
|
160
|
+
client = @init_lock.synchronize do
|
|
161
|
+
c = @current
|
|
162
|
+
@current = nil
|
|
163
|
+
c
|
|
164
|
+
end
|
|
165
|
+
client&.shutdown(timeout)
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def env(name)
|
|
172
|
+
value = ENV.fetch(name, nil)
|
|
173
|
+
value.nil? || value.strip.empty? ? nil : value.strip
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def finish_timer(name, start, tags)
|
|
177
|
+
client = @current
|
|
178
|
+
return unless client&.enabled?
|
|
179
|
+
|
|
180
|
+
ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
181
|
+
client.timing(name, ms, tags)
|
|
182
|
+
client.maybe_slow(name, ms, "task", tags)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def install_process_hooks
|
|
186
|
+
return if @hooks_installed
|
|
187
|
+
|
|
188
|
+
@hooks_installed = true
|
|
189
|
+
at_exit { Monitoring.send(:at_exit_hook, $!) }
|
|
190
|
+
Process.singleton_class.prepend(ForkHook) if Process.respond_to?(:_fork)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Reports the exception that is ending the program (unless it is a normal exit or Ctrl+C),
|
|
194
|
+
# then flushes and stops.
|
|
195
|
+
def at_exit_hook(error)
|
|
196
|
+
client = @current
|
|
197
|
+
return if client.nil?
|
|
198
|
+
|
|
199
|
+
if error && client.capture_unhandled? && !error.is_a?(SystemExit) && !error.is_a?(Interrupt) &&
|
|
200
|
+
!error.is_a?(SignalException)
|
|
201
|
+
client.capture_exception(error, handled: false)
|
|
202
|
+
end
|
|
203
|
+
client.shutdown(2.0)
|
|
204
|
+
rescue StandardError
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def after_fork
|
|
209
|
+
@init_lock = Mutex.new
|
|
210
|
+
client = @current
|
|
211
|
+
client.after_fork if client&.enabled?
|
|
212
|
+
rescue StandardError
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Restarts the client's thread in a child process (Ruby 3.1 and later call Process._fork for
|
|
218
|
+
# every fork).
|
|
219
|
+
module ForkHook
|
|
220
|
+
def _fork
|
|
221
|
+
pid = super
|
|
222
|
+
Monitoring.send(:after_fork) if pid.zero?
|
|
223
|
+
pid
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|