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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbd243e69bd9758c158d11868e091edc0f0c629916cebc8094fdcfb74664b0b3
4
- data.tar.gz: db2c6d4d5e68353da4ecb2757737595ec144c9ae002202bf3cde5abcacdfa978
3
+ metadata.gz: 6facae6811a9d61603077a6c369bfbad5873d9263e7ca2cbee5d4bead5ab3852
4
+ data.tar.gz: '08afbfff31cc87bbdd4f15c6e65c5f55f2be5e056589c3b0d46ab6962a33222d'
5
5
  SHA512:
6
- metadata.gz: 1381eee25cc094217d00d294ee381846cdb0390110e2e727c104459c5199918ede21c6157b1dfbd723b39d8fde5cc45b74998e031b352df058d76676a437a57e
7
- data.tar.gz: 70a5edefba779fd982adcaf91465c34ec89082242428c953485ba4a9855ddb7852f4950c6e01ba21636ac1c2bbfb6963ef66cbbc3208f6594abc85196b1914f7
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", github: "karloscodes/lognorth-sdk-rails"
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.
@@ -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
@@ -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.current_trace_id = nil
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.current_trace_id = nil
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.current_trace_id = nil
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.current_trace_id = nil
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 = {})
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lognorth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogNorth