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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -83
  3. data/lib/lognorth/middleware.rb +29 -11
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6facae6811a9d61603077a6c369bfbad5873d9263e7ca2cbee5d4bead5ab3852
4
- data.tar.gz: '08afbfff31cc87bbdd4f15c6e65c5f55f2be5e056589c3b0d46ab6962a33222d'
3
+ metadata.gz: 8ee47ef92f738c9a430483e2f35f55e17155e7519b806017943e69a217649d95
4
+ data.tar.gz: 57995a21f2d38a165604ce4825e1ba4d924b0f13e9c02040c7eee7279997c156
5
5
  SHA512:
6
- metadata.gz: de01d5ef4b8693410fdd5998f79b2a532404b6a0e9f52bfcaa23176195ee8c3864c3d89e36c146a02f0e7ebbd943f132cb9c394a0a9347fd917bfa23e4c06efb
7
- data.tar.gz: c2b6f280ac33dced59879406dfbc9ace7f6f23be12153c164e9edc56937e0ee071388b52f08cb862db4dbf102026485384789d08862bf760ab5ef77f2046a6dd
6
+ metadata.gz: ba3de220f46ffe6a576e2b48619d6a7b664f6bbdc80b5db9f910defb959d0b9940b81a4548bb5bd21631882324edf461f8bf3200f0892d509c0bed09a0a6360c
7
+ data.tar.gz: edc1a9b1795925af3f04c05feef9334a99d5ef4e133b03976f974689a21de952b0664408c3918cdaa03ef150c2c0c76adf3f8e3650af8c1dc69921a2dc01f772
data/README.md CHANGED
@@ -1,76 +1,24 @@
1
- # LogNorth Rails SDK
1
+ # lognorth-sdk-rails
2
2
 
3
- Send errors and logs from Rails to [LogNorth](https://lognorth.com) for monitoring and alerting.
3
+ [![CI](https://github.com/karloscodes/lognorth-sdk-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/karloscodes/lognorth-sdk-rails/actions/workflows/ci.yml) [![Gem Version](https://img.shields.io/gem/v/lognorth)](https://rubygems.org/gems/lognorth) [![License](https://img.shields.io/github/license/karloscodes/lognorth-sdk-rails)](LICENSE)
4
4
 
5
- ## Installation
5
+ Sends errors, logs, and request events from Rails and Rack apps to LogNorth.
6
6
 
7
- ```ruby
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
- ### Options
9
+ ## Install
35
10
 
36
- ```ruby
37
- # config/application.rb
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
- Default: `["/up"]` (Rails 7.1's auto-generated health check — swamped by
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
- ```ruby
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
- # Manual flush (called automatically at exit)
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
- 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:
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, so LogNorth shows how many users an issue hit.
42
+ The request event and its errors carry the user.
96
43
 
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.
44
+ Errors also carry the release. See [Configuration](#configuration) for how the release is set.
98
45
 
99
- ## Batching and delivery
46
+ ## Middleware
100
47
 
101
- Logging calls never block and never raise. They add the event to a queue in memory.
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
- - The thread sends when the queue holds 10 events, when you report an error, or 5 seconds after the first event.
105
- - A request holds at most 500 events and 1 MB of JSON.
106
- - The queue holds at most 10,000 events or 10 MB of JSON.
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
- When a send fails, the SDK keeps the events and tries again later. Events keep their order.
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
- - On a network error, a timeout, or a 5xx, it waits 1 second, then 2, then 4, up to 60.
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
- When the queue is full, the SDK drops the oldest event that is not an error. It keeps errors longest.
117
- After the next successful send, it logs `LogNorth client dropped N events` with the counts.
58
+ ```ruby
59
+ config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :internal_server_error
60
+ ```
118
61
 
119
- At exit (including SIGTERM and SIGINT), the SDK sends what is left in the queue for up to 5 seconds.
120
- It writes the number of events it could not send to stderr.
62
+ ### Without Rails
121
63
 
122
- ## Rack (without Rails)
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).
@@ -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: env["PATH_INFO"], status: status }
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']} #{env['PATH_INFO']} → #{status}",
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: env["PATH_INFO"], status: status }
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']} #{env['PATH_INFO']} → #{status}", context,
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']} #{env['PATH_INFO']}", e,
69
- merge_user_agent!({ method: env["REQUEST_METHOD"], path: env["PATH_INFO"] }, env, 500),
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 named route when available —
97
- # can be pulled straight off the env.
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
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.2.0
4
+ version: 0.3.0.rc.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogNorth