lognorth 0.2.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 +110 -83
- data/lib/lognorth/middleware.rb +29 -11
- 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,76 +1,24 @@
|
|
|
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"
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Rails Setup
|
|
12
|
-
|
|
13
|
-
Add credentials:
|
|
14
|
-
|
|
15
|
-
```yaml
|
|
16
|
-
# config/credentials.yml.enc
|
|
17
|
-
lognorth:
|
|
18
|
-
url: https://your-lognorth-instance.com
|
|
19
|
-
api_key: your_api_key
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
That's it. The gem auto-configures via Railtie.
|
|
23
|
-
|
|
24
|
-
### Manual Configuration
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
# config/initializers/lognorth.rb
|
|
28
|
-
LogNorth.config(
|
|
29
|
-
ENV["LOGNORTH_URL"],
|
|
30
|
-
ENV["LOGNORTH_API_KEY"]
|
|
31
|
-
)
|
|
32
|
-
```
|
|
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/).
|
|
33
8
|
|
|
34
|
-
|
|
9
|
+
## Install
|
|
35
10
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
config.lognorth.enabled = true # Default: on everywhere except development and test
|
|
39
|
-
config.lognorth.middleware = true # Log HTTP requests
|
|
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"]
|
|
11
|
+
```sh
|
|
12
|
+
bundle add lognorth
|
|
46
13
|
```
|
|
47
14
|
|
|
48
|
-
|
|
49
|
-
kamal-proxy and load-balancer pings otherwise). Setting `ignored_paths =
|
|
50
|
-
[]` disables ignoring entirely. Matching is exact path or `path/…`
|
|
51
|
-
prefix, so `/up` also covers `/up/detail`.
|
|
52
|
-
|
|
53
|
-
### Client errors are not errors
|
|
54
|
-
|
|
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.
|
|
59
|
-
|
|
60
|
-
Rails decides the status from `config.action_dispatch.rescue_responses`, and the SDK
|
|
61
|
-
asks the same map. To report one of them as an error, map it to a 5xx:
|
|
15
|
+
## Use
|
|
62
16
|
|
|
63
|
-
|
|
64
|
-
config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :internal_server_error
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Usage
|
|
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:
|
|
68
18
|
|
|
69
19
|
```ruby
|
|
70
|
-
# Log messages (batched, sent after 5s or at 10 events)
|
|
71
20
|
LogNorth.log("User signed up", { user_id: 123 })
|
|
72
21
|
|
|
73
|
-
# Report errors (sent at once)
|
|
74
22
|
begin
|
|
75
23
|
risky_operation
|
|
76
24
|
rescue => e
|
|
@@ -78,13 +26,12 @@ rescue => e
|
|
|
78
26
|
raise
|
|
79
27
|
end
|
|
80
28
|
|
|
81
|
-
#
|
|
82
|
-
LogNorth.flush
|
|
29
|
+
LogNorth.flush # sends the queue now, for up to 5 seconds
|
|
83
30
|
```
|
|
84
31
|
|
|
85
32
|
### Who hit the error, and in which release
|
|
86
33
|
|
|
87
|
-
|
|
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:
|
|
88
35
|
|
|
89
36
|
```ruby
|
|
90
37
|
class ApplicationController < ActionController::Base
|
|
@@ -92,34 +39,29 @@ class ApplicationController < ActionController::Base
|
|
|
92
39
|
end
|
|
93
40
|
```
|
|
94
41
|
|
|
95
|
-
The request event and its errors carry the user
|
|
42
|
+
The request event and its errors carry the user.
|
|
96
43
|
|
|
97
|
-
Errors also carry the release.
|
|
44
|
+
Errors also carry the release. See [Configuration](#configuration) for how the release is set.
|
|
98
45
|
|
|
99
|
-
##
|
|
46
|
+
## Middleware
|
|
100
47
|
|
|
101
|
-
|
|
102
|
-
One background thread sends the queue, one request at a time.
|
|
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.
|
|
103
49
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
- The SDK trims an event to at most 64 KB before it enters the queue. It marks a trimmed event with `context.truncated = true`.
|
|
50
|
+
Requests that match no route are not logged. A 5xx request also carries its user agent, and it sends the queue at once.
|
|
51
|
+
|
|
52
|
+
### Client errors are not errors
|
|
108
53
|
|
|
109
|
-
|
|
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.
|
|
110
55
|
|
|
111
|
-
|
|
112
|
-
- On a 429 or 503, it waits as long as the `Retry-After` header says.
|
|
113
|
-
- 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.
|
|
114
|
-
- On a 413 or other 4xx, it splits the batch in two and sends again. It drops a single event the server still refuses.
|
|
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:
|
|
115
57
|
|
|
116
|
-
|
|
117
|
-
|
|
58
|
+
```ruby
|
|
59
|
+
config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :internal_server_error
|
|
60
|
+
```
|
|
118
61
|
|
|
119
|
-
|
|
120
|
-
It writes the number of events it could not send to stderr.
|
|
62
|
+
### Without Rails
|
|
121
63
|
|
|
122
|
-
|
|
64
|
+
Use the middleware in any Rack app:
|
|
123
65
|
|
|
124
66
|
```ruby
|
|
125
67
|
require "lognorth"
|
|
@@ -127,3 +69,88 @@ require "lognorth"
|
|
|
127
69
|
LogNorth.config("https://lognorth.example.com", "api_key")
|
|
128
70
|
use LogNorth::Middleware
|
|
129
71
|
```
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
### Batching
|
|
76
|
+
|
|
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.
|
|
80
|
+
- A request holds at most 500 events and 1 MB of JSON.
|
|
81
|
+
- The queue holds at most 10,000 events or 10 MB of JSON.
|
|
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
|
|
97
|
+
|
|
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.
|
|
99
|
+
|
|
100
|
+
### Errors and jobs
|
|
101
|
+
|
|
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
|
+
```
|
|
115
|
+
|
|
116
|
+
Without a URL and an API key, the SDK logs a warning at boot and sends nothing.
|
|
117
|
+
|
|
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:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
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
|
+
```
|
|
129
|
+
|
|
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
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT. See [LICENSE](LICENSE).
|
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)
|
|
@@ -33,12 +36,12 @@ module LogNorth
|
|
|
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)
|
|
38
41
|
merge_user_agent!(context, env, status)
|
|
39
42
|
|
|
40
43
|
LogNorth::Client.send_event(
|
|
41
|
-
"#{env['REQUEST_METHOD']} #{
|
|
44
|
+
"#{env['REQUEST_METHOD']} #{path} → #{status}",
|
|
42
45
|
context,
|
|
43
46
|
trace_id: trace_id,
|
|
44
47
|
duration_ms: duration_ms,
|
|
@@ -55,18 +58,18 @@ module LogNorth
|
|
|
55
58
|
# Rails turns this into a 4xx further out. Log the request with that
|
|
56
59
|
# status, like any other request, instead of an error.
|
|
57
60
|
status = LogNorth.response_status_for(e)
|
|
58
|
-
context = { method: env["REQUEST_METHOD"], path:
|
|
59
|
-
merge_route_info!(context, env)
|
|
61
|
+
context = { method: env["REQUEST_METHOD"], path: path, status: status }
|
|
62
|
+
merge_route_info!(context, env, script_name)
|
|
60
63
|
LogNorth::Client.send_event(
|
|
61
|
-
"#{env['REQUEST_METHOD']} #{
|
|
64
|
+
"#{env['REQUEST_METHOD']} #{path} → #{status}", context,
|
|
62
65
|
trace_id: trace_id, duration_ms: duration_ms, timestamp: start_time
|
|
63
66
|
)
|
|
64
67
|
LogNorth::Client.end_request
|
|
65
68
|
raise
|
|
66
69
|
end
|
|
67
70
|
LogNorth::Client.send_error_event(
|
|
68
|
-
"Request failed: #{env['REQUEST_METHOD']} #{
|
|
69
|
-
merge_user_agent!({ 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),
|
|
70
73
|
trace_id: trace_id,
|
|
71
74
|
duration_ms: duration_ms,
|
|
72
75
|
timestamp: start_time
|
|
@@ -93,9 +96,9 @@ module LogNorth
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
# Rails populates action_controller.instance after dispatch so the
|
|
96
|
-
# controller/action names — and the
|
|
97
|
-
#
|
|
98
|
-
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)
|
|
99
102
|
controller = env["action_controller.instance"]
|
|
100
103
|
return unless controller
|
|
101
104
|
|
|
@@ -103,6 +106,21 @@ module LogNorth
|
|
|
103
106
|
action = controller.respond_to?(:action_name) ? controller.action_name : nil
|
|
104
107
|
context[:controller] = klass if klass
|
|
105
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)")
|
|
106
124
|
end
|
|
107
125
|
end
|
|
108
126
|
end
|