koderift-rails 1.3.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e49d9d7f56f87dde732a6465a3ced955d8960b692408f6818cf686e30965c8c
4
+ data.tar.gz: 299cb7491b8211baa559aebbca51b7759929891caefe5048d562791431759202
5
+ SHA512:
6
+ metadata.gz: e01be29d2c18193b54c04d93ae80ce6bca85d58b8c3c57e92227d5e706c4e033d8d13cb103c285a61d90a19e14431f80eec95e2ace8ed38fe2bbb8f92cd9fabf
7
+ data.tar.gz: b620b59cbe817e063ef7f862be7ed1fc0a36390b6465b4f52b20d59afe51f34de53a47dc72371f7ba14a0079c579a3946c16dbfa1bb3d7cd316c7ad381afe53f
data/CHANGELOG.md ADDED
@@ -0,0 +1,82 @@
1
+ # Changelog
2
+
3
+ All notable changes to `koderift-rails` are documented here.
4
+
5
+ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+ Releases before 1.3.4 were installed from git and were never published to
7
+ RubyGems; they are listed for continuity.
8
+
9
+ ## [1.3.4] - 2026-08-27
10
+
11
+ First release published to RubyGems.
12
+
13
+ ### Fixed
14
+ - An external HTTP call could be recorded twice on a single request, inflating
15
+ the external-call count and the time attributed to it.
16
+
17
+ ### Changed
18
+ - Packaging only: added a LICENSE file, gem metadata (source, changelog and
19
+ issue links, MFA required for publishing) and this changelog. No change to
20
+ what the gem captures or emits.
21
+
22
+ ## [1.3.3] - 2026-05-25
23
+ ### Added
24
+ - The current Rails environment is emitted with each request, so traces can be
25
+ filtered by environment rather than inferring it from the host.
26
+
27
+ ## [1.3.2] - 2026-05-25
28
+ ### Changed
29
+ - Improved internal logging.
30
+
31
+ ## [1.3.1] - 2026-05-25
32
+ ### Added
33
+ - External HTTP calls made from Sidekiq jobs are tracked, not just those made
34
+ during a web request.
35
+
36
+ ## [1.3.0] - 2026-05-25
37
+ ### Changed
38
+ - Removed the per-request limits on captured spans; the full set is emitted and
39
+ trimmed downstream by the ingest agent.
40
+
41
+ ## [1.2.2] - 2026-05-25
42
+ ### Fixed
43
+ - Maintenance release.
44
+
45
+ ## [1.2.1] - 2026-05-24
46
+ ### Fixed
47
+ - Searchkick instrumentation issue reported by a client.
48
+
49
+ ## [1.2.0] - 2026-05-24
50
+ ### Added
51
+ - Search query instrumentation (`search.searchkick` and
52
+ `multi_search.searchkick`).
53
+
54
+ ## [1.0.6] - 2026-05-24
55
+ ### Added
56
+ - RUM token support.
57
+
58
+ ## [1.0.5] - 2026-05-24
59
+ ### Added
60
+ - Browser RUM instrumentation.
61
+
62
+ ## [1.0.4] - 2026-05-24
63
+ ### Added
64
+ - Distributed tracing: a trace id is propagated across services via the
65
+ `X-Koderift-Trace-ID` header.
66
+
67
+ ## [1.0.3] - 2026-05-23
68
+ ### Added
69
+ - External HTTP call tracking via a `Net::HTTP` patch.
70
+
71
+ ## [1.0.2] - 2026-05-22
72
+ ### Fixed
73
+ - Maintenance release.
74
+
75
+ ## [1.0.1] - 2025-12-10
76
+ ### Fixed
77
+ - Load-order issue when the gem initialised before Lograge.
78
+
79
+ ## [1.0.0] - 2025-08-11
80
+ ### Added
81
+ - Initial release: request instrumentation, partial and SQL span capture,
82
+ breadcrumbs, and automatic Lograge configuration.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Koderift
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # koderift-rails
2
+
3
+ Rails instrumentation gem for [Koderift](https://koderift.com) log analytics.
4
+
5
+ Automatically captures request context, performance data, and error
6
+ information — no manual `ApplicationController` changes needed.
7
+
8
+ ## Installation
9
+
10
+ Requires Ruby >= 2.7 and Rails 7.1-8.x.
11
+
12
+ Add to your `Gemfile`:
13
+
14
+ ```ruby
15
+ gem 'koderift-rails', '~> 1.3'
16
+ ```
17
+
18
+ Run `bundle install`. Lograge is a dependency of this gem and is installed
19
+ with it - you do not need to add it yourself.
20
+
21
+ ## Configuration
22
+
23
+ Create `config/initializers/koderift.rb`:
24
+
25
+ ```ruby
26
+ Koderift::Rails.configure do |config|
27
+ config.project_token = ENV['KODERIFT_PROJECT_TOKEN']
28
+
29
+ # Optional: customise how the current user is resolved
30
+ # config.current_user = ->(controller) { controller.current_user }
31
+ end
32
+ ```
33
+
34
+ Set `KODERIFT_PROJECT_TOKEN` in your environment to the token from your
35
+ project's Log Analytics settings page in Koderift.
36
+
37
+ ## What it captures
38
+
39
+ * Controller, action, path, method, status, duration
40
+ * DB runtime, view runtime, allocations
41
+ * Current user ID
42
+ * Request ID, IP address, user agent, referer
43
+ * Request params (filtered — passwords, tokens etc removed)
44
+ * Top 5 slowest partials with render times
45
+ * Top 5 slowest SQL queries
46
+ * Last 20 breadcrumbs (ordered partial renders + SQL queries)
47
+ * Exception class, message, and cause chain
48
+ * Stack traces (requires `keep_original_rails_log: true`)
49
+
50
+ ## Requirements
51
+
52
+ * Ruby >= 2.7
53
+ * Rails >= 6.1
54
+ * lograge >= 0.11
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koderift
4
+ module Rails
5
+ class Configuration
6
+ # Required: identifies which project this app reports to.
7
+ # Set from an environment variable in the initializer.
8
+ attr_accessor :project_token
9
+
10
+ # Lambda called with the controller instance to get the current user.
11
+ # Default tries common patterns. Override if your app is different.
12
+ # Example: ->(controller) { controller.current_user }
13
+ attr_accessor :current_user
14
+
15
+ # Parameters to filter from captured request params.
16
+ attr_accessor :filter_params
17
+
18
+ # Maximum number of breadcrumbs to capture per request.
19
+ attr_accessor :max_breadcrumbs
20
+
21
+ # Hosts to exclude from external call tracking (e.g. internal services,
22
+ # localhost, known noise). Always excludes 127.0.0.1 and localhost.
23
+ attr_accessor :external_call_ignore_hosts
24
+
25
+ # Whether to enable the instrumentation. Set to false to disable
26
+ # without removing the gem.
27
+ attr_accessor :enabled
28
+
29
+ # Whether to enable Browser RUM. Set to false for API-only apps.
30
+ # Defaults to true — the meta tag is harmless if rum.js is not loaded.
31
+ attr_accessor :rum_enabled
32
+
33
+ # Fraction of sessions to capture (0.0–1.0). Default 0.1 (10%).
34
+ # Passed to rum.js so sampling happens client-side.
35
+ attr_accessor :rum_sampling_rate
36
+
37
+ # Threshold in milliseconds above which a Sidekiq job is marked 'slow'
38
+ # in the emitted log payload. Default 30_000 (30 seconds).
39
+ attr_accessor :slow_job_threshold_ms
40
+
41
+ def initialize
42
+ @project_token = nil
43
+ @current_user = default_current_user
44
+ @filter_params = %w[
45
+ password password_confirmation
46
+ token secret authenticity_token
47
+ credit_card cvv ssn
48
+ ]
49
+ @max_breadcrumbs = 20
50
+ @external_call_ignore_hosts = []
51
+ @enabled = true
52
+ @rum_enabled = true
53
+ @rum_sampling_rate = 0.1
54
+ @slow_job_threshold_ms = 30_000
55
+ end
56
+
57
+ def valid?
58
+ project_token.present?
59
+ end
60
+
61
+ private
62
+
63
+ def default_current_user
64
+ lambda do |controller|
65
+ # Try common patterns — override in initializer if different
66
+ if controller.respond_to?(:current_user, true)
67
+ controller.send(:current_user)
68
+ end
69
+ rescue StandardError
70
+ nil
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'securerandom'
5
+ require 'koderift/rails/instrumentation'
6
+
7
+ module Koderift
8
+ module Rails
9
+ module Controller
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ around_action :koderift_instrument_request
14
+ end
15
+
16
+ private
17
+
18
+ def koderift_instrument_request
19
+ trace_id = request.headers['X-Koderift-Trace-ID'].presence ||
20
+ SecureRandom.uuid
21
+
22
+ Thread.current[:koderift_trace_id] = trace_id
23
+ request.env['koderift.trace_id'] = trace_id
24
+
25
+ result = Koderift::Rails::Instrumentation.capture do
26
+ yield
27
+ end
28
+
29
+ request.env['koderift.spans'] = result[:spans]
30
+ request.env['koderift.query_count'] = result[:query_count]
31
+ request.env['koderift.partial_count'] = result[:partial_count]
32
+ request.env['koderift.search_count'] = result[:search_count]
33
+ request.env['koderift.breadcrumbs'] = result[:breadcrumbs]
34
+ ensure
35
+ Thread.current[:koderift_trace_id] = nil
36
+ end
37
+
38
+ def append_info_to_payload(payload)
39
+ super
40
+ config = Koderift::Rails.configuration
41
+
42
+ user = begin
43
+ config.current_user.call(self)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+
48
+ filtered_params = request.filtered_parameters
49
+ .except('controller', 'action', 'format',
50
+ 'utf8', '_method')
51
+ .reject do |k, _|
52
+ config.filter_params.any? do |f|
53
+ k.to_s.match?(/#{Regexp.escape(f)}/i)
54
+ end
55
+ end
56
+
57
+ payload[:koderift_user_id] = user&.id
58
+ payload[:koderift_host] = request.host
59
+ payload[:koderift_ip] = request.remote_ip
60
+ payload[:koderift_user_agent] = request.user_agent
61
+ payload[:koderift_request_id] = request.request_id
62
+ payload[:koderift_trace_id] = request.env['koderift.trace_id']
63
+ payload[:koderift_referer] = request.referer
64
+ payload[:koderift_params] = filtered_params.to_h
65
+ payload[:koderift_spans] = request.env['koderift.spans']
66
+ payload[:koderift_query_count] = request.env['koderift.query_count']
67
+ payload[:koderift_partial_count] = request.env['koderift.partial_count']
68
+ payload[:koderift_search_count] = request.env['koderift.search_count']
69
+ payload[:koderift_breadcrumbs] = request.env['koderift.breadcrumbs']
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'active_support/core_ext/string/filters'
5
+
6
+ module Koderift
7
+ module Rails
8
+ module Instrumentation
9
+ # Subscribes to Rails notifications during a request to capture
10
+ # partial render times, SQL query times, and breadcrumbs.
11
+ # Returns a hash of captured data to be merged into the log payload.
12
+ def self.capture
13
+ partials = []
14
+ queries = []
15
+ search_queries = []
16
+ breadcrumbs = []
17
+ config = Koderift::Rails.configuration
18
+
19
+ partial_sub = ActiveSupport::Notifications.subscribe('render_partial.action_view') do |*args|
20
+ event = ActiveSupport::Notifications::Event.new(*args)
21
+ payload = event.payload
22
+ duration = (payload[:duration] || event.duration).round
23
+ identifier = payload[:identifier].to_s
24
+ .sub(rails_root_prefix, '')
25
+ .sub(%r{\A/app/views/}, '')
26
+
27
+ partials << { partial: identifier, duration_ms: duration }
28
+ breadcrumbs << {
29
+ type: 'render',
30
+ message: identifier,
31
+ duration_ms: duration,
32
+ timestamp: Time.now.utc.iso8601
33
+ }
34
+ end
35
+
36
+ query_sub = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
37
+ event = ActiveSupport::Notifications::Event.new(*args)
38
+ payload = event.payload
39
+ next if payload[:name].to_s.match?(/\A(CACHE|SCHEMA)/)
40
+
41
+ duration = (payload[:duration] || event.duration).round
42
+ sql = payload[:sql].to_s.squish.truncate(300)
43
+
44
+ queries << { sql: sql, duration_ms: duration }
45
+ breadcrumbs << {
46
+ type: 'sql',
47
+ message: sql,
48
+ duration_ms: duration,
49
+ timestamp: Time.now.utc.iso8601
50
+ }
51
+ end
52
+
53
+ # Single-model search: fired by Model.search(...)
54
+ # payload: { name: "Admin10 Search", query: {...} }
55
+ search_sub = ActiveSupport::Notifications.subscribe('search.searchkick') do |*args|
56
+ event = ActiveSupport::Notifications::Event.new(*args)
57
+ payload = event.payload
58
+ duration = (payload[:duration] || event.duration).round
59
+
60
+ index = payload[:name].to_s.sub(/\s*Search\z/i, '').strip
61
+ index = 'unknown' if index.blank?
62
+
63
+ search_queries << { index: index, duration_ms: duration }
64
+ end
65
+
66
+ # Multi search: fired by Searchkick.multi_search([...])
67
+ # payload: { name: "Multi Search", body: "..." }
68
+ # Duration covers the entire batch — record as a single entry.
69
+ multi_search_sub = ActiveSupport::Notifications.subscribe('multi_search.searchkick') do |*args|
70
+ event = ActiveSupport::Notifications::Event.new(*args)
71
+ duration = event.duration.round
72
+
73
+ search_queries << { index: 'multi_search', duration_ms: duration }
74
+ end
75
+
76
+ external_calls = []
77
+ Thread.current[:koderift_external_calls] = external_calls
78
+ Thread.current[:koderift_external_config] = config
79
+
80
+ begin
81
+ yield
82
+ ensure
83
+ ActiveSupport::Notifications.unsubscribe(partial_sub)
84
+ ActiveSupport::Notifications.unsubscribe(query_sub)
85
+ ActiveSupport::Notifications.unsubscribe(search_sub)
86
+ ActiveSupport::Notifications.unsubscribe(multi_search_sub)
87
+ Thread.current[:koderift_external_calls] = nil
88
+ Thread.current[:koderift_external_config] = nil
89
+ end
90
+
91
+ spans = []
92
+
93
+ partials.each_with_index do |p, i|
94
+ spans << { type: 'partial', name: p[:partial], duration_ms: p[:duration_ms], sequence: i }
95
+ end
96
+
97
+ queries.each_with_index do |q, i|
98
+ spans << { type: 'sql', name: q[:sql], duration_ms: q[:duration_ms], sequence: i }
99
+ end
100
+
101
+ search_queries.each_with_index do |s, i|
102
+ spans << { type: 'search', name: s[:index], duration_ms: s[:duration_ms], sequence: i }
103
+ end
104
+
105
+ external_calls.each_with_index do |c, i|
106
+ spans << {
107
+ type: 'http',
108
+ name: "#{c[:host]}#{c[:endpoint]}",
109
+ duration_ms: c[:duration_ms],
110
+ detail: { method: c[:method], status: c[:status], trace_id: c[:trace_id] }.compact.to_json,
111
+ sequence: i
112
+ }
113
+ end
114
+
115
+ {
116
+ spans: spans,
117
+ query_count: queries.size,
118
+ partial_count: partials.size,
119
+ search_count: search_queries.size,
120
+ breadcrumbs: breadcrumbs.last(config.max_breadcrumbs)
121
+ }
122
+ end
123
+
124
+ def self.rails_root_prefix
125
+ defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root ? ::Rails.root.to_s : ''
126
+ end
127
+ end
128
+
129
+ # Installed once when the gem loads (see the prepend at the bottom of this
130
+ # file). Writes to the thread-local external_calls array set by
131
+ # Instrumentation.capture; outside of a capture block it is a pass-through.
132
+ #
133
+ # CONTRACT: exactly one entry per LOGICAL outbound call. Net::HTTP#request
134
+ # re-enters itself whenever it is called on a session that is not started:
135
+ #
136
+ # def request(req, body = nil, &block)
137
+ # unless started?
138
+ # start { req['connection'] ||= 'close'; return request(req, body, &block) }
139
+ # end
140
+ #
141
+ # (net-http-0.9.1 lib/net/http.rb:2399-2405). Both frames run this prepended
142
+ # patch, so one call was recorded twice. That branch also forces
143
+ # `Connection: close` and start's ensure closes the socket, so an affected
144
+ # call site double-counts on EVERY call, not intermittently.
145
+ module NetHttpPatch
146
+ def request(req, *args, &block)
147
+ calls = Thread.current[:koderift_external_calls]
148
+ config = Thread.current[:koderift_external_config]
149
+ trace_id = Thread.current[:koderift_trace_id]
150
+
151
+ req['X-Koderift-Trace-ID'] = trace_id if trace_id
152
+
153
+ # Nothing below this line may run before the pass-through return: the
154
+ # spec stand-in defines only #request, and the header injection must
155
+ # still happen when instrumentation is not capturing.
156
+ return super(req, *args, &block) unless calls && config
157
+
158
+ # The stdlib recursion re-invokes with the SAME req object, so request
159
+ # identity separates "my own re-entry" from a genuinely different call
160
+ # issued while this one is in flight. Do NOT use `started?` (it keeps
161
+ # the inner frame, which cannot see connection setup, and the stand-in
162
+ # does not define it) and do NOT use an object ivar (one Net::HTTP
163
+ # shared across threads would silently lose most of its spans).
164
+ in_flight = (Thread.current[:koderift_http_in_flight] ||= [])
165
+ return super(req, *args, &block) if in_flight.any? { |r| r.equal?(req) }
166
+
167
+ in_flight << req
168
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
169
+ begin
170
+ response = super(req, *args, &block)
171
+ ensure
172
+ # Strictly LIFO: the inner frame returns above without pushing, and
173
+ # push/pop are paired in one begin/ensure. The ensure also covers a
174
+ # raise (Net::OpenTimeout, ECONNREFUSED, a raising response block) --
175
+ # without it one failure would silence every later call on this thread.
176
+ in_flight.pop
177
+ end
178
+ duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
179
+
180
+ host = address.to_s
181
+ ignored = ['127.0.0.1', 'localhost', '[::1]'].include?(host) ||
182
+ config.external_call_ignore_hosts.any? { |h| host.include?(h) }
183
+
184
+ unless ignored
185
+ raw_path = req.path.to_s.split('?').first.to_s
186
+ segments = raw_path.split('/').reject(&:empty?).first(2)
187
+ endpoint = segments.empty? ? '/' : '/' + segments.join('/')
188
+
189
+ calls << {
190
+ host: host, endpoint: endpoint, method: req.method.to_s.upcase,
191
+ status: response.code.to_i, duration_ms: duration, trace_id: trace_id
192
+ }
193
+ end
194
+
195
+ response
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ require 'net/http'
202
+ Net::HTTP.prepend(Koderift::Rails::NetHttpPatch)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'lograge'
5
+ rescue LoadError
6
+ # lograge not available — instrumentation will be skipped
7
+ end
8
+
9
+ module Koderift
10
+ module Rails
11
+ module LogrageConfig
12
+ MAX_FRAMES = 50
13
+ LEADING_FRAMES = 10
14
+
15
+ def self.apply(app)
16
+ return unless defined?(Lograge)
17
+
18
+ app.config.lograge.enabled = true
19
+ app.config.lograge.keep_original_rails_log = true
20
+ app.config.lograge.formatter = Lograge::Formatters::Json.new
21
+
22
+ app.config.lograge.custom_options = lambda do |event|
23
+ exceptions = event.payload[:exception_object]
24
+
25
+ {
26
+ user_id: event.payload[:koderift_user_id],
27
+ host: event.payload[:koderift_host],
28
+ db: event.payload[:db_runtime]&.round(2),
29
+ exception: event.payload[:exception]&.join(': '),
30
+ request_id: event.payload[:koderift_request_id],
31
+ trace_id: event.payload[:koderift_trace_id],
32
+ ip: event.payload[:koderift_ip],
33
+ user_agent: event.payload[:koderift_user_agent],
34
+ allocations: event.payload[:allocations],
35
+ error_cause: exceptions&.cause&.message,
36
+ stack_trace: format_backtrace(exceptions),
37
+ referer: event.payload[:koderift_referer],
38
+ breadcrumbs: event.payload[:koderift_breadcrumbs],
39
+ params: event.payload[:koderift_params],
40
+ spans: event.payload[:koderift_spans],
41
+ query_count: event.payload[:koderift_query_count],
42
+ partial_count: event.payload[:koderift_partial_count],
43
+ search_count: event.payload[:koderift_search_count]
44
+ }.compact
45
+ end
46
+ end
47
+
48
+ def self.format_backtrace(exception)
49
+ return nil unless exception&.backtrace&.any?
50
+
51
+ backtrace = exception.backtrace
52
+
53
+ leading = backtrace.first(LEADING_FRAMES)
54
+
55
+ rest = backtrace[LEADING_FRAMES..]&.select { |l| l.include?('/app/') } || []
56
+
57
+ frames = (leading + rest).first(MAX_FRAMES)
58
+
59
+ if frames.size < MAX_FRAMES
60
+ non_app_rest = backtrace[LEADING_FRAMES..]&.reject { |l| l.include?('/app/') } || []
61
+ frames = (frames + non_app_rest).first(MAX_FRAMES)
62
+ end
63
+
64
+ frames.join("\n").presence
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Koderift
6
+ module Rails
7
+ class Railtie < ::Rails::Railtie
8
+ # Configure Lograge after the app initializers have run
9
+ # (so the user's initializer sets up Koderift.configure first)
10
+ initializer 'koderift.configure_lograge', after: :load_config_initializers do |app|
11
+ next unless Koderift::Rails.configuration.enabled
12
+
13
+ require 'koderift/rails/lograge_config'
14
+ Koderift::Rails::LogrageConfig.apply(app)
15
+ end
16
+
17
+ # Include the controller concern into ActionController::Base
18
+ initializer 'koderift.include_controller', after: :load_config_initializers do
19
+ next unless Koderift::Rails.configuration.enabled
20
+
21
+ ActiveSupport.on_load(:action_controller_base) do
22
+ require 'koderift/rails/controller'
23
+ include Koderift::Rails::Controller
24
+ end
25
+ end
26
+
27
+ # Make koderift_rum_meta_tag available in all views
28
+ initializer 'koderift.helpers', after: :load_config_initializers do
29
+ next unless Koderift::Rails.configuration.enabled
30
+
31
+ ActiveSupport.on_load(:action_view) do
32
+ require 'koderift/rails/rum_helper'
33
+ include Koderift::Rails::RumHelper
34
+ end
35
+ end
36
+
37
+ # Register Sidekiq server middleware lazily when Sidekiq is present.
38
+ initializer 'koderift.sidekiq_middleware', after: :load_config_initializers do
39
+ next unless Koderift::Rails.configuration.enabled
40
+
41
+ ActiveSupport.on_load(:after_initialize) do
42
+ if defined?(Sidekiq) && Sidekiq.respond_to?(:configure_server)
43
+ require 'koderift/rails/sidekiq_middleware'
44
+ Sidekiq.configure_server do |sidekiq_config|
45
+ sidekiq_config.server_middleware do |chain|
46
+ chain.add Koderift::Rails::SidekiqMiddleware
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koderift
4
+ module Rails
5
+ module RumHelper
6
+ # Renders the meta tag that rum.js reads for initialisation.
7
+ # Place once inside <head> in your application layout.
8
+ #
9
+ # Example:
10
+ # <%= koderift_rum_meta_tag %>
11
+ #
12
+ # Renders:
13
+ # <meta name="koderift-rum-token"
14
+ # content="abc123"
15
+ # data-rum-sampling-rate="0.1"
16
+ # data-environment="production">
17
+ #
18
+ # Returns an empty string when RUM is disabled or no token is configured.
19
+ def koderift_rum_meta_tag
20
+ config = Koderift::Rails.configuration
21
+ return ''.html_safe unless config.rum_enabled && config.project_token.present?
22
+
23
+ tag.meta(
24
+ name: 'koderift-rum-token',
25
+ content: config.project_token,
26
+ data: {
27
+ rum_sampling_rate: config.rum_sampling_rate.to_f,
28
+ environment: (defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env.to_s : 'production')
29
+ }
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'koderift/rails/instrumentation'
6
+
7
+ module Koderift
8
+ module Rails
9
+ # Sidekiq server middleware that wraps each job in Instrumentation.capture
10
+ # and writes a single JSON line to Rails.logger. The koderift-agent picks
11
+ # this up via the existing Rails log tailer — the "job_class" key
12
+ # distinguishes it from Lograge request lines.
13
+ class SidekiqMiddleware
14
+ def call(worker, job, queue)
15
+ config = Koderift::Rails.configuration
16
+ return yield unless config.enabled && config.project_token.present?
17
+
18
+ trace_id = job['koderift_trace_id'].to_s
19
+ trace_id = SecureRandom.uuid if trace_id.empty?
20
+ Thread.current[:koderift_trace_id] = trace_id
21
+
22
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+
24
+ begin
25
+ result = Instrumentation.capture { yield }
26
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
27
+ emit_job_log(worker, job, queue, result, duration_ms, trace_id, error: nil)
28
+ result
29
+ rescue StandardError => e
30
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
31
+ empty_result = { spans: [], query_count: 0, partial_count: 0,
32
+ search_count: 0, breadcrumbs: [] }
33
+ emit_job_log(worker, job, queue, empty_result, duration_ms, trace_id, error: e)
34
+ raise
35
+ ensure
36
+ Thread.current[:koderift_trace_id] = nil
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def emit_job_log(_worker, job, queue, result, duration_ms, trace_id, error:)
43
+ config = Koderift::Rails.configuration
44
+
45
+ level = if error
46
+ 'error'
47
+ elsif duration_ms >= config.slow_job_threshold_ms
48
+ 'slow'
49
+ else
50
+ 'info'
51
+ end
52
+
53
+ payload = {
54
+ job_class: job['class'].to_s,
55
+ jid: job['jid'].to_s,
56
+ queue: queue.to_s,
57
+ status: error ? 500 : 200,
58
+ duration: duration_ms,
59
+ level: level,
60
+ trace_id: trace_id,
61
+ spans: result[:spans],
62
+ query_count: result[:query_count],
63
+ search_count: result[:search_count],
64
+ breadcrumbs: result[:breadcrumbs]
65
+ }
66
+
67
+ if error
68
+ payload[:exception] = "#{error.class}: #{error.message}"
69
+ bt = error.backtrace&.first(20)&.join("\n")
70
+ payload[:stack_trace] = bt if bt && !bt.empty?
71
+ end
72
+
73
+ ::Rails.logger.info(payload.compact.to_json)
74
+ rescue StandardError => e
75
+ ::Rails.logger.warn("[KoderiftSidekiqMiddleware] emit failed: #{e.message}")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koderift
4
+ module Rails
5
+ VERSION = '1.3.4'
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'koderift/rails/version'
4
+ require 'koderift/rails/configuration'
5
+ require 'koderift/rails/instrumentation'
6
+ require 'koderift/rails/lograge_config'
7
+ require 'koderift/rails/controller'
8
+ require 'koderift/rails/rum_helper'
9
+ require 'koderift/rails/railtie' if defined?(::Rails::Railtie)
10
+
11
+ module Koderift
12
+ module Rails
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def configure
19
+ yield configuration
20
+ end
21
+
22
+ def reset!
23
+ @configuration = Configuration.new
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'koderift/rails'
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: koderift-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.4
5
+ platform: ruby
6
+ authors:
7
+ - Koderift
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: lograge
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0.11'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0.11'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '3.12'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '3.12'
66
+ - !ruby/object:Gem::Dependency
67
+ name: rspec-rails
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '7.1'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '7.1'
80
+ - !ruby/object:Gem::Dependency
81
+ name: rails
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '7.1'
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: '9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '7.1'
97
+ - - "<"
98
+ - !ruby/object:Gem::Version
99
+ version: '9'
100
+ description: Automatically captures request context, performance data, and error information
101
+ for Koderift log analytics.
102
+ email:
103
+ - support@koderift.com
104
+ executables: []
105
+ extensions: []
106
+ extra_rdoc_files: []
107
+ files:
108
+ - CHANGELOG.md
109
+ - LICENSE
110
+ - README.md
111
+ - lib/koderift-rails.rb
112
+ - lib/koderift/rails.rb
113
+ - lib/koderift/rails/configuration.rb
114
+ - lib/koderift/rails/controller.rb
115
+ - lib/koderift/rails/instrumentation.rb
116
+ - lib/koderift/rails/lograge_config.rb
117
+ - lib/koderift/rails/railtie.rb
118
+ - lib/koderift/rails/rum_helper.rb
119
+ - lib/koderift/rails/sidekiq_middleware.rb
120
+ - lib/koderift/rails/version.rb
121
+ homepage: https://github.com/koderift/koderift-rails
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ source_code_uri: https://github.com/koderift/koderift-rails
126
+ changelog_uri: https://github.com/koderift/koderift-rails/blob/main/CHANGELOG.md
127
+ bug_tracker_uri: https://github.com/koderift/koderift-rails/issues
128
+ rubygems_mfa_required: 'true'
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '2.7'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.6.9
144
+ specification_version: 4
145
+ summary: Rails instrumentation for Koderift log analytics
146
+ test_files: []