lognorth 0.1.0 → 0.2.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/README.md +15 -1
- data/lib/lognorth/client.rb +71 -6
- data/lib/lognorth/middleware.rb +14 -5
- data/lib/lognorth.rb +11 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6facae6811a9d61603077a6c369bfbad5873d9263e7ca2cbee5d4bead5ab3852
|
|
4
|
+
data.tar.gz: '08afbfff31cc87bbdd4f15c6e65c5f55f2be5e056589c3b0d46ab6962a33222d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de01d5ef4b8693410fdd5998f79b2a532404b6a0e9f52bfcaa23176195ee8c3864c3d89e36c146a02f0e7ebbd943f132cb9c394a0a9347fd917bfa23e4c06efb
|
|
7
|
+
data.tar.gz: c2b6f280ac33dced59879406dfbc9ace7f6f23be12153c164e9edc56937e0ee071388b52f08cb862db4dbf102026485384789d08862bf760ab5ef77f2046a6dd
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Send errors and logs from Rails to [LogNorth](https://lognorth.com) for monitori
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
gem "lognorth"
|
|
8
|
+
gem "lognorth"
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Rails Setup
|
|
@@ -82,6 +82,20 @@ end
|
|
|
82
82
|
LogNorth.flush
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Who hit the error, and in which release
|
|
86
|
+
|
|
87
|
+
LogNorth reads `Current.user` when your app defines it, as the Rails authentication generator does. Otherwise, name the user yourself. Use an ID, not an email:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class ApplicationController < ActionController::Base
|
|
91
|
+
before_action { LogNorth.user = current_user&.id }
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The request event and its errors carry the user, so LogNorth shows how many users an issue hit.
|
|
96
|
+
|
|
97
|
+
Errors also carry the release. The gem reads `KAMAL_VERSION`, `LOGNORTH_RELEASE`, `GIT_SHA`, or the commit variable of Render, Heroku, Railway, Vercel, or Coolify. When the release is set, the gem logs `Release <version> started` once at boot. LogNorth marks each release's first start on its charts. A failed request (5xx) also carries its user agent, so you can tell a bot from a browser.
|
|
98
|
+
|
|
85
99
|
## Batching and delivery
|
|
86
100
|
|
|
87
101
|
Logging calls never block and never raise. They add the event to a queue in memory.
|
data/lib/lognorth/client.rb
CHANGED
|
@@ -15,7 +15,12 @@ module LogNorth
|
|
|
15
15
|
MAX_STACK_TRACE_BYTES = 16 * 1024
|
|
16
16
|
MAX_STRING_BYTES = 8 * 1024
|
|
17
17
|
# The context keys an event keeps when it is still too big after trimming.
|
|
18
|
-
ESSENTIAL_KEYS = %w[error error_class error_file error_line method path status environment].freeze
|
|
18
|
+
ESSENTIAL_KEYS = %w[error error_class error_file error_line method path status environment release user].freeze
|
|
19
|
+
# Where deploy tools put the version that runs. The first one set wins.
|
|
20
|
+
RELEASE_ENV = %w[
|
|
21
|
+
LOGNORTH_RELEASE GIT_SHA GIT_COMMIT SOURCE_COMMIT KAMAL_VERSION
|
|
22
|
+
RENDER_GIT_COMMIT HEROKU_SLUG_COMMIT SOURCE_VERSION RAILWAY_GIT_COMMIT_SHA VERCEL_GIT_COMMIT_SHA
|
|
23
|
+
].freeze
|
|
19
24
|
|
|
20
25
|
module Client
|
|
21
26
|
# An event in the queue, with its JSON size and whether it is an error.
|
|
@@ -39,6 +44,8 @@ module LogNorth
|
|
|
39
44
|
@endpoint = nil
|
|
40
45
|
@api_key = nil
|
|
41
46
|
@environment = nil
|
|
47
|
+
@release = nil
|
|
48
|
+
@announced = nil # the release this process last said it started
|
|
42
49
|
|
|
43
50
|
# Wait times in seconds. Tests set them lower.
|
|
44
51
|
@flush_interval = 5
|
|
@@ -53,13 +60,15 @@ module LogNorth
|
|
|
53
60
|
class << self
|
|
54
61
|
attr_accessor :debug
|
|
55
62
|
|
|
56
|
-
def config(url, key, environment: nil)
|
|
63
|
+
def config(url, key, environment: nil, release: nil)
|
|
57
64
|
@mutex.synchronize do
|
|
58
65
|
@endpoint = url.chomp("/")
|
|
59
66
|
@api_key = key
|
|
60
67
|
@environment = environment
|
|
68
|
+
@release = release || release_from_env
|
|
61
69
|
end
|
|
62
70
|
log_debug("configured with url=#{url} env=#{environment.inspect}")
|
|
71
|
+
announce_release
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
def configured?
|
|
@@ -82,6 +91,22 @@ module LogNorth
|
|
|
82
91
|
Thread.current[:lognorth_trace_id] = id
|
|
83
92
|
end
|
|
84
93
|
|
|
94
|
+
# The user of the request on this thread: the one set with LogNorth.user=,
|
|
95
|
+
# or else Current.user, which the Rails authentication generator defines.
|
|
96
|
+
def current_user
|
|
97
|
+
Thread.current[:lognorth_user] || user_from_current_attributes
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def current_user=(id)
|
|
101
|
+
Thread.current[:lognorth_user] = id&.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Forgets the trace ID and the user when a request ends.
|
|
105
|
+
def end_request
|
|
106
|
+
Thread.current[:lognorth_trace_id] = nil
|
|
107
|
+
Thread.current[:lognorth_user] = nil
|
|
108
|
+
end
|
|
109
|
+
|
|
85
110
|
def send_event(message, context = {}, trace_id: nil, duration_ms: nil, timestamp: nil)
|
|
86
111
|
return unless configured?
|
|
87
112
|
|
|
@@ -89,7 +114,7 @@ module LogNorth
|
|
|
89
114
|
event = {
|
|
90
115
|
message: message,
|
|
91
116
|
timestamp: (timestamp || Time.now).utc.iso8601(3),
|
|
92
|
-
context: stamp_environment(context)
|
|
117
|
+
context: stamp_user(stamp_environment(context))
|
|
93
118
|
}
|
|
94
119
|
event[:trace_id] = trace_id if trace_id
|
|
95
120
|
event[:duration_ms] = duration_ms if duration_ms
|
|
@@ -115,14 +140,14 @@ module LogNorth
|
|
|
115
140
|
event = {
|
|
116
141
|
message: message,
|
|
117
142
|
timestamp: (timestamp || Time.now).utc.iso8601(3),
|
|
118
|
-
context: stamp_environment(context.merge(
|
|
143
|
+
context: stamp_user(stamp_environment(context.merge(
|
|
119
144
|
error: exception.message,
|
|
120
145
|
error_class: exception.class.name,
|
|
121
146
|
error_file: error_file,
|
|
122
147
|
error_line: error_line,
|
|
123
148
|
error_caller: error_caller,
|
|
124
149
|
stack_trace: exception.backtrace&.first(20)&.join("\n")
|
|
125
|
-
))
|
|
150
|
+
)))
|
|
126
151
|
}
|
|
127
152
|
event[:trace_id] = trace_id if trace_id
|
|
128
153
|
event[:duration_ms] = duration_ms if duration_ms
|
|
@@ -169,8 +194,48 @@ module LogNorth
|
|
|
169
194
|
context.merge(environment: env)
|
|
170
195
|
end
|
|
171
196
|
|
|
197
|
+
# Logs "Release <version> started" once per release, when the client is
|
|
198
|
+
# configured. LogNorth takes the first start of a release as its deploy
|
|
199
|
+
# time and marks it on its charts.
|
|
200
|
+
def announce_release
|
|
201
|
+
release = @mutex.synchronize do
|
|
202
|
+
next if @release.nil? || @release == @announced
|
|
203
|
+
|
|
204
|
+
@announced = @release
|
|
205
|
+
end
|
|
206
|
+
send_event("Release #{release} started", { release: release }) if release
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def stamp_user(context)
|
|
210
|
+
user = current_user
|
|
211
|
+
return context if user.nil? || fetch(context, :user)
|
|
212
|
+
|
|
213
|
+
context.merge(user: user)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def user_from_current_attributes
|
|
217
|
+
return unless defined?(::Current) && ::Current.respond_to?(:user)
|
|
218
|
+
|
|
219
|
+
::Current.user&.id&.to_s
|
|
220
|
+
rescue StandardError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Errors carry the release: that is where it answers which deploy broke
|
|
225
|
+
# something. Other events stay small.
|
|
226
|
+
def stamp_release(event)
|
|
227
|
+
release = @mutex.synchronize { @release }
|
|
228
|
+
return event if release.nil? || !error_event?(event[:context]) || fetch(event[:context], :release)
|
|
229
|
+
|
|
230
|
+
event.merge(context: event[:context].merge(release: release))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def release_from_env
|
|
234
|
+
RELEASE_ENV.lazy.map { |k| ENV[k].to_s.strip }.find { |v| !v.empty? }
|
|
235
|
+
end
|
|
236
|
+
|
|
172
237
|
def enqueue(event, urgent: false)
|
|
173
|
-
entry = new_entry(trim(event))
|
|
238
|
+
entry = new_entry(trim(stamp_release(event)))
|
|
174
239
|
@mutex.synchronize do
|
|
175
240
|
push(entry)
|
|
176
241
|
@urgent = true if urgent
|
data/lib/lognorth/middleware.rb
CHANGED
|
@@ -26,7 +26,7 @@ module LogNorth
|
|
|
26
26
|
|
|
27
27
|
# Don't track requests that didn't match any route (scanner noise)
|
|
28
28
|
if status == 404 && !env["action_controller.instance"]
|
|
29
|
-
LogNorth::Client.
|
|
29
|
+
LogNorth::Client.end_request
|
|
30
30
|
return [status, headers, response]
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -35,6 +35,7 @@ module LogNorth
|
|
|
35
35
|
|
|
36
36
|
context = { method: env["REQUEST_METHOD"], path: env["PATH_INFO"], status: status }
|
|
37
37
|
merge_route_info!(context, env)
|
|
38
|
+
merge_user_agent!(context, env, status)
|
|
38
39
|
|
|
39
40
|
LogNorth::Client.send_event(
|
|
40
41
|
"#{env['REQUEST_METHOD']} #{env['PATH_INFO']} → #{status}",
|
|
@@ -46,7 +47,7 @@ module LogNorth
|
|
|
46
47
|
|
|
47
48
|
LogNorth.flush if status >= 500
|
|
48
49
|
|
|
49
|
-
LogNorth::Client.
|
|
50
|
+
LogNorth::Client.end_request
|
|
50
51
|
[status, headers, response]
|
|
51
52
|
rescue StandardError => e
|
|
52
53
|
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
@@ -60,17 +61,17 @@ module LogNorth
|
|
|
60
61
|
"#{env['REQUEST_METHOD']} #{env['PATH_INFO']} → #{status}", context,
|
|
61
62
|
trace_id: trace_id, duration_ms: duration_ms, timestamp: start_time
|
|
62
63
|
)
|
|
63
|
-
LogNorth::Client.
|
|
64
|
+
LogNorth::Client.end_request
|
|
64
65
|
raise
|
|
65
66
|
end
|
|
66
67
|
LogNorth::Client.send_error_event(
|
|
67
68
|
"Request failed: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}", e,
|
|
68
|
-
{ method: env["REQUEST_METHOD"], path: env["PATH_INFO"] },
|
|
69
|
+
merge_user_agent!({ method: env["REQUEST_METHOD"], path: env["PATH_INFO"] }, env, 500),
|
|
69
70
|
trace_id: trace_id,
|
|
70
71
|
duration_ms: duration_ms,
|
|
71
72
|
timestamp: start_time
|
|
72
73
|
)
|
|
73
|
-
LogNorth::Client.
|
|
74
|
+
LogNorth::Client.end_request
|
|
74
75
|
raise
|
|
75
76
|
end
|
|
76
77
|
|
|
@@ -83,6 +84,14 @@ module LogNorth
|
|
|
83
84
|
ignored_paths.any? { |p| path == p || path.start_with?("#{p}/") }
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
# The user agent tells a bot from a browser on a failed request. Only
|
|
88
|
+
# failed ones carry it, to keep every other event small.
|
|
89
|
+
def merge_user_agent!(context, env, status)
|
|
90
|
+
agent = env["HTTP_USER_AGENT"].to_s
|
|
91
|
+
context[:user_agent] = agent if status.to_i >= 500 && !agent.empty?
|
|
92
|
+
context
|
|
93
|
+
end
|
|
94
|
+
|
|
86
95
|
# Rails populates action_controller.instance after dispatch so the
|
|
87
96
|
# controller/action names — and the named route when available —
|
|
88
97
|
# can be pulled straight off the env.
|
data/lib/lognorth.rb
CHANGED
|
@@ -7,8 +7,17 @@ require_relative "lognorth/railtie" if defined?(Rails::Railtie)
|
|
|
7
7
|
|
|
8
8
|
module LogNorth
|
|
9
9
|
class << self
|
|
10
|
-
def config(url, key, environment: nil)
|
|
11
|
-
Client.config(url, key, environment: environment)
|
|
10
|
+
def config(url, key, environment: nil, release: nil)
|
|
11
|
+
Client.config(url, key, environment: environment, release: release)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Names the user of the current request: an ID, not an email. The request
|
|
15
|
+
# event and its errors carry it, so an issue shows how many users it hit.
|
|
16
|
+
# Without it, LogNorth reads Current.user when the app defines one.
|
|
17
|
+
#
|
|
18
|
+
# before_action { LogNorth.user = current_user&.id }
|
|
19
|
+
def user=(id)
|
|
20
|
+
Client.current_user = id
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
def log(message, context = {})
|