lognorth 0.1.0 → 0.3.0.rc.2
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 +113 -72
- data/lib/lognorth/client.rb +71 -6
- data/lib/lognorth/middleware.rb +42 -15
- 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: 8ee47ef92f738c9a430483e2f35f55e17155e7519b806017943e69a217649d95
|
|
4
|
+
data.tar.gz: 57995a21f2d38a165604ce4825e1ba4d924b0f13e9c02040c7eee7279997c156
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba3de220f46ffe6a576e2b48619d6a7b664f6bbdc80b5db9f910defb959d0b9940b81a4548bb5bd21631882324edf461f8bf3200f0892d509c0bed09a0a6360c
|
|
7
|
+
data.tar.gz: edc1a9b1795925af3f04c05feef9334a99d5ef4e133b03976f974689a21de952b0664408c3918cdaa03ef150c2c0c76adf3f8e3650af8c1dc69921a2dc01f772
|
data/README.md
CHANGED
|
@@ -1,115 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lognorth-sdk-rails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/karloscodes/lognorth-sdk-rails/actions/workflows/ci.yml) [](https://rubygems.org/gems/lognorth) [](LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Sends errors, logs, and request events from Rails and Rack apps to LogNorth.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
gem "lognorth", github: "karloscodes/lognorth-sdk-rails"
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Rails Setup
|
|
7
|
+
[LogNorth](https://lognorth.com) monitors your app and alerts you when it fails. See the [LogNorth Rails docs](https://lognorth.com/docs/integrations/rails/).
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
## Install
|
|
14
10
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
lognorth:
|
|
18
|
-
url: https://your-lognorth-instance.com
|
|
19
|
-
api_key: your_api_key
|
|
11
|
+
```sh
|
|
12
|
+
bundle add lognorth
|
|
20
13
|
```
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
## Use
|
|
23
16
|
|
|
24
|
-
|
|
17
|
+
With credentials set (see [Configuration](#configuration)), Rails sends request events, reported exceptions, and job events on its own. Call the SDK to send your own events:
|
|
25
18
|
|
|
26
19
|
```ruby
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
LogNorth.log("User signed up", { user_id: 123 })
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
risky_operation
|
|
24
|
+
rescue => e
|
|
25
|
+
LogNorth.error("Payment failed", e, { order_id: 456 })
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
LogNorth.flush # sends the queue now, for up to 5 seconds
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
###
|
|
32
|
+
### Who hit the error, and in which release
|
|
33
|
+
|
|
34
|
+
The SDK reads `Current.user` when your app defines it, as the Rails authentication generator does. Otherwise, set the user yourself. Use an ID, not an email:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
config.lognorth.error_subscriber = true # Report exceptions (Rails 7+)
|
|
41
|
-
|
|
42
|
-
# Paths to skip from request logging. Defaults to Rails' built-in
|
|
43
|
-
# health-check endpoint; set to [] to log everything or replace with
|
|
44
|
-
# your own list.
|
|
45
|
-
config.lognorth.ignored_paths = ["/up", "/healthz"]
|
|
37
|
+
class ApplicationController < ActionController::Base
|
|
38
|
+
before_action { LogNorth.user = current_user&.id }
|
|
39
|
+
end
|
|
46
40
|
```
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
The request event and its errors carry the user.
|
|
43
|
+
|
|
44
|
+
Errors also carry the release. See [Configuration](#configuration) for how the release is set.
|
|
45
|
+
|
|
46
|
+
## Middleware
|
|
47
|
+
|
|
48
|
+
The middleware logs each request as one event. The event has the method, path, status, duration, controller, action, and route (`/users/:id`, Rails 7.1 and later). LogNorth groups requests into endpoints by the route. It also sets an `X-Trace-ID` response header. It reuses an incoming `X-Trace-ID` request header when one is set.
|
|
49
|
+
|
|
50
|
+
Requests that match no route are not logged. A 5xx request also carries its user agent, and it sends the queue at once.
|
|
52
51
|
|
|
53
52
|
### Client errors are not errors
|
|
54
53
|
|
|
55
|
-
An exception that Rails answers with a 4xx is the request's fault, not the app's
|
|
56
|
-
`ActiveRecord::RecordNotFound` (404), `ActionController::InvalidAuthenticityToken`
|
|
57
|
-
(422), `ActionController::ParameterMissing` (400). The SDK logs the request with
|
|
58
|
-
that status and does not report an error, so these never become issues or alerts.
|
|
54
|
+
An exception that Rails answers with a 4xx is the request's fault, not the app's. Examples are `ActiveRecord::RecordNotFound` (404), `ActionController::InvalidAuthenticityToken` (422), and `ActionController::ParameterMissing` (400). The SDK logs the request with that status. It does not report an error, so these never become issues or alerts.
|
|
59
55
|
|
|
60
|
-
Rails
|
|
61
|
-
asks the same map. To report one of them as an error, map it to a 5xx:
|
|
56
|
+
Rails sets the status in `config.action_dispatch.rescue_responses`. The SDK reads the same map. To report one of them as an error, map it to a 5xx:
|
|
62
57
|
|
|
63
58
|
```ruby
|
|
64
59
|
config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :internal_server_error
|
|
65
60
|
```
|
|
66
61
|
|
|
67
|
-
|
|
62
|
+
### Without Rails
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
# Log messages (batched, sent after 5s or at 10 events)
|
|
71
|
-
LogNorth.log("User signed up", { user_id: 123 })
|
|
64
|
+
Use the middleware in any Rack app:
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
risky_operation
|
|
76
|
-
rescue => e
|
|
77
|
-
LogNorth.error("Payment failed", e, { order_id: 456 })
|
|
78
|
-
raise
|
|
79
|
-
end
|
|
66
|
+
```ruby
|
|
67
|
+
require "lognorth"
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
LogNorth
|
|
69
|
+
LogNorth.config("https://lognorth.example.com", "api_key")
|
|
70
|
+
use LogNorth::Middleware
|
|
83
71
|
```
|
|
84
72
|
|
|
85
|
-
##
|
|
73
|
+
## How it works
|
|
86
74
|
|
|
87
|
-
|
|
88
|
-
One background thread sends the queue, one request at a time.
|
|
75
|
+
### Batching
|
|
89
76
|
|
|
90
|
-
|
|
77
|
+
Logging calls never wait for the network. Each call adds the event to a queue in memory. One background thread sends the queue, one request at a time.
|
|
78
|
+
|
|
79
|
+
- The thread sends when the queue holds 10 events, when you report an error, or 5 seconds after the first queued event.
|
|
91
80
|
- A request holds at most 500 events and 1 MB of JSON.
|
|
92
81
|
- The queue holds at most 10,000 events or 10 MB of JSON.
|
|
93
|
-
-
|
|
82
|
+
- A message over 1,000 characters is cut. A long string in the context is cut.
|
|
83
|
+
- An event still over 64 KB keeps only its essential context keys. The SDK sets `context.truncated` to `true` on any cut event.
|
|
84
|
+
|
|
85
|
+
### Failed sends
|
|
86
|
+
|
|
87
|
+
The SDK keeps the events and sends them again later. Events keep their order.
|
|
88
|
+
|
|
89
|
+
- A network error, a timeout, a 408, or a 5xx waits 1, 2, 4 seconds, and so on, up to 60 seconds. Each wait varies by up to 20%.
|
|
90
|
+
- A 429 or 503 waits as long as the `Retry-After` header says, from 1 to 300 seconds. Without the header, the SDK uses the normal wait.
|
|
91
|
+
- A 401, 403, 404, or other unexpected status waits 60 seconds, then 120, then 240, up to 300 seconds. It writes a line to stderr when delivery starts failing. Check the URL and the API key.
|
|
92
|
+
- Another 4xx, such as 400, 413, or 422, splits the batch in two and sends each half. A single event that the server still refuses is dropped.
|
|
93
|
+
|
|
94
|
+
When the queue is full, the SDK drops the oldest event that is not an error. After the next successful send, it logs `LogNorth client dropped N events`. The context holds the counts of dropped events and dropped errors.
|
|
95
|
+
|
|
96
|
+
### Exit
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
At exit, the SDK sends the queue for up to 5 seconds. Ruby runs this exit hook on SIGTERM and SIGINT too, unless the app traps them. The SDK writes the number of unsent events to stderr.
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
- On a 429 or 503, it waits as long as the `Retry-After` header says.
|
|
99
|
-
- On a 401, 403, or 404, it waits 60 seconds, then up to 5 minutes. It writes one line to stderr, so you can fix the url or the key.
|
|
100
|
-
- On a 413 or other 4xx, it splits the batch in two and sends again. It drops a single event the server still refuses.
|
|
100
|
+
### Errors and jobs
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
- `Rails.error` reports go to LogNorth. Each event adds `handled`, `severity`, and `source` to the context.
|
|
103
|
+
- With `config.lognorth.active_job` on, each job logs `<Job> completed` or `<Job> failed`. A job carries the trace ID of the request that enqueued it.
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
The SDK reads the URL and API key from Rails credentials:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
# config/credentials.yml.enc
|
|
111
|
+
lognorth:
|
|
112
|
+
url: https://your-lognorth-instance.com
|
|
113
|
+
api_key: your_api_key
|
|
114
|
+
```
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
It writes the number of events it could not send to stderr.
|
|
116
|
+
Without a URL and an API key, the SDK logs a warning at boot and sends nothing.
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
Rails sets the environment name for you. Set `release:` yourself, or let the SDK read it from the environment. To configure the SDK by hand, call `LogNorth.config` in an initializer:
|
|
109
119
|
|
|
110
120
|
```ruby
|
|
111
|
-
|
|
121
|
+
# config/initializers/lognorth.rb
|
|
122
|
+
LogNorth.config(
|
|
123
|
+
ENV["LOGNORTH_URL"],
|
|
124
|
+
ENV["LOGNORTH_API_KEY"],
|
|
125
|
+
environment: "production", # optional
|
|
126
|
+
release: "1.2.3" # optional
|
|
127
|
+
)
|
|
128
|
+
```
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
When no `release:` is set, the SDK reads the first of these variables that is set: `LOGNORTH_RELEASE`, `GIT_SHA`, `GIT_COMMIT`, `SOURCE_COMMIT`, `KAMAL_VERSION`, `RENDER_GIT_COMMIT`, `HEROKU_SLUG_COMMIT`, `SOURCE_VERSION`, `RAILWAY_GIT_COMMIT_SHA`, `VERCEL_GIT_COMMIT_SHA`. When a release is set, the SDK logs `Release <version> started` once, when it configures.
|
|
131
|
+
|
|
132
|
+
Options, in `config/application.rb`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
config.lognorth.enabled = true # Default: on, except in development and test
|
|
136
|
+
config.lognorth.middleware = true # Request events
|
|
137
|
+
config.lognorth.error_subscriber = true # Rails.error reports (Rails 7.0+)
|
|
138
|
+
config.lognorth.active_job = true # Job events
|
|
139
|
+
|
|
140
|
+
# Requests to skip. Default: Rails' health check, which load balancers call often.
|
|
141
|
+
# Set [] to log everything.
|
|
142
|
+
config.lognorth.ignored_paths = ["/up"]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
An ignored path matches the exact path, or the path followed by `/`. So `/up` also skips `/up/detail`.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
bundle install
|
|
151
|
+
bundle exec rake test
|
|
115
152
|
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT. See [LICENSE](LICENSE).
|
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
|
@@ -9,7 +9,10 @@ module LogNorth
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call(env)
|
|
12
|
+
# A mounted engine or Rack app leaves PATH_INFO and SCRIPT_NAME as it
|
|
13
|
+
# saw them, so read both before the call.
|
|
12
14
|
path = env["PATH_INFO"]
|
|
15
|
+
script_name = env["SCRIPT_NAME"].to_s
|
|
13
16
|
|
|
14
17
|
# Skip ignored paths (health checks, metrics, etc.)
|
|
15
18
|
if ignored_path?(path)
|
|
@@ -26,18 +29,19 @@ module LogNorth
|
|
|
26
29
|
|
|
27
30
|
# Don't track requests that didn't match any route (scanner noise)
|
|
28
31
|
if status == 404 && !env["action_controller.instance"]
|
|
29
|
-
LogNorth::Client.
|
|
32
|
+
LogNorth::Client.end_request
|
|
30
33
|
return [status, headers, response]
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
34
37
|
headers["X-Trace-ID"] = trace_id
|
|
35
38
|
|
|
36
|
-
context = { method: env["REQUEST_METHOD"], path:
|
|
37
|
-
merge_route_info!(context, env)
|
|
39
|
+
context = { method: env["REQUEST_METHOD"], path: path, status: status }
|
|
40
|
+
merge_route_info!(context, env, script_name)
|
|
41
|
+
merge_user_agent!(context, env, status)
|
|
38
42
|
|
|
39
43
|
LogNorth::Client.send_event(
|
|
40
|
-
"#{env['REQUEST_METHOD']} #{
|
|
44
|
+
"#{env['REQUEST_METHOD']} #{path} → #{status}",
|
|
41
45
|
context,
|
|
42
46
|
trace_id: trace_id,
|
|
43
47
|
duration_ms: duration_ms,
|
|
@@ -46,7 +50,7 @@ module LogNorth
|
|
|
46
50
|
|
|
47
51
|
LogNorth.flush if status >= 500
|
|
48
52
|
|
|
49
|
-
LogNorth::Client.
|
|
53
|
+
LogNorth::Client.end_request
|
|
50
54
|
[status, headers, response]
|
|
51
55
|
rescue StandardError => e
|
|
52
56
|
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
@@ -54,23 +58,23 @@ module LogNorth
|
|
|
54
58
|
# Rails turns this into a 4xx further out. Log the request with that
|
|
55
59
|
# status, like any other request, instead of an error.
|
|
56
60
|
status = LogNorth.response_status_for(e)
|
|
57
|
-
context = { method: env["REQUEST_METHOD"], path:
|
|
58
|
-
merge_route_info!(context, env)
|
|
61
|
+
context = { method: env["REQUEST_METHOD"], path: path, status: status }
|
|
62
|
+
merge_route_info!(context, env, script_name)
|
|
59
63
|
LogNorth::Client.send_event(
|
|
60
|
-
"#{env['REQUEST_METHOD']} #{
|
|
64
|
+
"#{env['REQUEST_METHOD']} #{path} → #{status}", context,
|
|
61
65
|
trace_id: trace_id, duration_ms: duration_ms, timestamp: start_time
|
|
62
66
|
)
|
|
63
|
-
LogNorth::Client.
|
|
67
|
+
LogNorth::Client.end_request
|
|
64
68
|
raise
|
|
65
69
|
end
|
|
66
70
|
LogNorth::Client.send_error_event(
|
|
67
|
-
"Request failed: #{env['REQUEST_METHOD']} #{
|
|
68
|
-
{ method: env["REQUEST_METHOD"], path:
|
|
71
|
+
"Request failed: #{env['REQUEST_METHOD']} #{path}", e,
|
|
72
|
+
merge_user_agent!({ method: env["REQUEST_METHOD"], path: path }, env, 500),
|
|
69
73
|
trace_id: trace_id,
|
|
70
74
|
duration_ms: duration_ms,
|
|
71
75
|
timestamp: start_time
|
|
72
76
|
)
|
|
73
|
-
LogNorth::Client.
|
|
77
|
+
LogNorth::Client.end_request
|
|
74
78
|
raise
|
|
75
79
|
end
|
|
76
80
|
|
|
@@ -83,10 +87,18 @@ module LogNorth
|
|
|
83
87
|
ignored_paths.any? { |p| path == p || path.start_with?("#{p}/") }
|
|
84
88
|
end
|
|
85
89
|
|
|
90
|
+
# The user agent tells a bot from a browser on a failed request. Only
|
|
91
|
+
# failed ones carry it, to keep every other event small.
|
|
92
|
+
def merge_user_agent!(context, env, status)
|
|
93
|
+
agent = env["HTTP_USER_AGENT"].to_s
|
|
94
|
+
context[:user_agent] = agent if status.to_i >= 500 && !agent.empty?
|
|
95
|
+
context
|
|
96
|
+
end
|
|
97
|
+
|
|
86
98
|
# Rails populates action_controller.instance after dispatch so the
|
|
87
|
-
# controller/action names — and the
|
|
88
|
-
#
|
|
89
|
-
def merge_route_info!(context, env)
|
|
99
|
+
# controller/action names — and the route — can be pulled straight off
|
|
100
|
+
# the env.
|
|
101
|
+
def merge_route_info!(context, env, script_name)
|
|
90
102
|
controller = env["action_controller.instance"]
|
|
91
103
|
return unless controller
|
|
92
104
|
|
|
@@ -94,6 +106,21 @@ module LogNorth
|
|
|
94
106
|
action = controller.respond_to?(:action_name) ? controller.action_name : nil
|
|
95
107
|
context[:controller] = klass if klass
|
|
96
108
|
context[:action] = action if action
|
|
109
|
+
route = route_of(env, script_name)
|
|
110
|
+
context[:route] = route if route
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The route as `bin/rails routes` prints it (Rails 7.1+), without the
|
|
114
|
+
# optional format: "/users/:id". An engine's route is relative to its
|
|
115
|
+
# mount point, which Rails added to SCRIPT_NAME.
|
|
116
|
+
def route_of(env, script_name)
|
|
117
|
+
return unless defined?(ActionDispatch::Request) && ActionDispatch::Request.method_defined?(:route_uri_pattern)
|
|
118
|
+
|
|
119
|
+
pattern = ActionDispatch::Request.new(env).route_uri_pattern
|
|
120
|
+
return unless pattern
|
|
121
|
+
|
|
122
|
+
mount = env["SCRIPT_NAME"].to_s.delete_prefix(script_name)
|
|
123
|
+
mount + pattern.delete_suffix("(.:format)")
|
|
97
124
|
end
|
|
98
125
|
end
|
|
99
126
|
end
|
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 = {})
|