Skip to content

Commit fada6b8

Browse files
committed
WIP - add e2e specs for rails structured logging
1 parent 3d60aab commit fada6b8

File tree

3 files changed

+351
-4
lines changed

3 files changed

+351
-4
lines changed

spec/apps/rails-mini/Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ gem "rake"
66
gem "puma"
77
gem 'railties', '~> 8.0'
88
gem 'actionpack', '~> 8.0'
9+
gem 'activerecord', '~> 8.0'
10+
gem 'activejob', '~> 8.0'
11+
gem 'sqlite3', '>= 2.1'
912
gem 'sentry-ruby', path: Pathname(__dir__).join("../../..").realpath
1013
gem 'sentry-rails', path: Pathname(__dir__).join("../../..").realpath

spec/apps/rails-mini/app.rb

Lines changed: 242 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
Bundler.require
66

77
ENV["RAILS_ENV"] = "development"
8+
ENV["DATABASE_URL"] = "sqlite3::memory:"
89

9-
require "action_controller"
10+
require "action_controller/railtie"
11+
require "active_record/railtie"
12+
require "active_job/railtie"
1013

1114
class RailsMiniApp < Rails::Application
1215
config.hosts = nil
@@ -17,6 +20,12 @@ class RailsMiniApp < Rails::Application
1720
config.api_only = true
1821
config.force_ssl = false
1922

23+
config.active_record.logger = Logger.new($stdout)
24+
config.active_record.migration_error = :page_load
25+
26+
config.active_job.queue_adapter = :inline
27+
config.active_job.logger = Logger.new($stdout)
28+
2029
initializer :configure_sentry do
2130
Sentry.init do |config|
2231
config.dsn = ENV["SENTRY_DSN"]
@@ -30,9 +39,67 @@ class RailsMiniApp < Rails::Application
3039
config.release = "sentry-ruby-rails-mini-#{Time.now.utc}"
3140

3241
config.transport.transport_class = Sentry::DebugTransport
33-
config.sdk_debug_transport_log_file = "/workspace/sentry/log/sentry_debug_events.log"
3442
config.background_worker_threads = 0
43+
44+
config.enable_logs = true
45+
config.structured_logger_class = Sentry::DebugStructuredLogger
46+
config.rails.structured_logging.enabled = true
47+
config.rails.structured_logging.attach_to = [:active_record, :action_controller, :active_job]
48+
end
49+
end
50+
end
51+
52+
class Post < ActiveRecord::Base
53+
end
54+
55+
class User < ActiveRecord::Base
56+
end
57+
58+
class ApplicationJob < ActiveJob::Base
59+
retry_on ActiveRecord::Deadlocked
60+
61+
discard_on ActiveJob::DeserializationError
62+
end
63+
64+
class SampleJob < ApplicationJob
65+
queue_as :default
66+
67+
def perform(message = "Hello from ActiveJob!")
68+
Rails.logger.info("SampleJob executed with message: #{message}")
69+
70+
Post.count
71+
User.count
72+
73+
message
74+
end
75+
end
76+
77+
class DatabaseJob < ApplicationJob
78+
queue_as :default
79+
80+
def perform(post_title = "Test Post")
81+
Rails.logger.info("DatabaseJob creating post: #{post_title}")
82+
83+
post = Post.create!(title: post_title, content: "Content for #{post_title}")
84+
found_post = Post.find(post.id)
85+
86+
Rails.logger.info("DatabaseJob found post: #{found_post.title}")
87+
88+
found_post
89+
end
90+
end
91+
92+
class FailingJob < ApplicationJob
93+
queue_as :default
94+
95+
def perform(should_fail = true)
96+
Rails.logger.info("FailingJob started")
97+
98+
if should_fail
99+
raise StandardError, "Intentional job failure for testing"
35100
end
101+
102+
"Job completed successfully"
36103
end
37104
end
38105

@@ -61,7 +128,8 @@ def health
61128
status: "ok",
62129
timestamp: Time.now.utc.iso8601,
63130
sentry_initialized: Sentry.initialized?,
64-
log_file_writable: check_log_file_writable
131+
log_file_writable: check_log_file_writable,
132+
structured_log_file_writable: check_structured_log_file_writable
65133
}
66134
end
67135

@@ -70,6 +138,24 @@ def trace_headers
70138
render json: { headers: headers }
71139
end
72140

141+
def logged_events
142+
if Sentry.logger.is_a?(Sentry::DebugStructuredLogger)
143+
events = Sentry.logger.logged_events
144+
render json: { events: events, count: events.length }
145+
else
146+
render json: { events: [], count: 0 }
147+
end
148+
end
149+
150+
def clear_logged_events
151+
if Sentry.logger.is_a?(Sentry::DebugStructuredLogger)
152+
Sentry.logger.clear
153+
render json: { status: "cleared" }
154+
else
155+
render json: { status: "no_debug_logger" }
156+
end
157+
end
158+
73159
private
74160

75161
def check_log_file_writable
@@ -80,6 +166,127 @@ def check_log_file_writable
80166
false
81167
end
82168

169+
def check_structured_log_file_writable
170+
if Sentry.logger.is_a?(Sentry::DebugStructuredLogger)
171+
log_file_path = Sentry.logger.log_file
172+
File.writable?(File.dirname(log_file_path)) &&
173+
(!File.exist?(log_file_path) || File.writable?(log_file_path))
174+
else
175+
false
176+
end
177+
rescue
178+
false
179+
end
180+
181+
def set_cors_headers
182+
response.headers['Access-Control-Allow-Origin'] = '*'
183+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
184+
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, sentry-trace, baggage'
185+
end
186+
end
187+
188+
class PostsController < ActionController::Base
189+
before_action :set_cors_headers
190+
before_action :ensure_database_setup
191+
192+
def index
193+
posts = Post.all.to_a
194+
195+
Sentry.logger.info("Posts index accessed", posts_count: posts.length)
196+
197+
render json: {
198+
posts: posts.map { |p| { id: p.id, title: p.title, content: p.content } }
199+
}
200+
end
201+
202+
def create
203+
post = Post.create!(post_params)
204+
205+
Sentry.logger.info("Post created", post_id: post.id, title: post.title)
206+
207+
render json: { post: { id: post.id, title: post.title, content: post.content } }, status: :created
208+
rescue ActiveRecord::RecordInvalid => e
209+
render json: { error: e.message }, status: :unprocessable_entity
210+
end
211+
212+
def show
213+
post = Post.find(params[:id])
214+
render json: { post: { id: post.id, title: post.title, content: post.content } }
215+
rescue ActiveRecord::RecordNotFound
216+
render json: { error: "Post not found" }, status: :not_found
217+
end
218+
219+
private
220+
221+
def post_params
222+
params.require(:post).permit(:title, :content)
223+
end
224+
225+
def ensure_database_setup
226+
unless ActiveRecord::Base.connection.table_exists?('posts')
227+
setup_database
228+
end
229+
end
230+
231+
def set_cors_headers
232+
response.headers['Access-Control-Allow-Origin'] = '*'
233+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
234+
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, sentry-trace, baggage'
235+
end
236+
end
237+
238+
class JobsController < ActionController::Base
239+
before_action :set_cors_headers
240+
before_action :ensure_database_setup
241+
242+
def sample_job
243+
job = SampleJob.perform_later("Hello from Rails mini app!")
244+
245+
Sentry.logger.info("SampleJob enqueued", job_id: job.job_id)
246+
247+
render json: {
248+
message: "SampleJob enqueued successfully",
249+
job_id: job.job_id,
250+
job_class: job.class.name
251+
}
252+
end
253+
254+
def database_job
255+
title = params[:title] || "Test Post from Job"
256+
job = DatabaseJob.perform_later(title)
257+
258+
Sentry.logger.info("DatabaseJob enqueued", job_id: job.job_id, post_title: title)
259+
260+
render json: {
261+
message: "DatabaseJob enqueued successfully",
262+
job_id: job.job_id,
263+
job_class: job.class.name,
264+
post_title: title
265+
}
266+
end
267+
268+
def failing_job
269+
should_fail = params[:should_fail] != "false"
270+
job = FailingJob.perform_later(should_fail)
271+
272+
Sentry.logger.info("FailingJob enqueued", job_id: job.job_id, should_fail: should_fail)
273+
274+
render json: {
275+
message: "FailingJob enqueued successfully",
276+
job_id: job.job_id,
277+
job_class: job.class.name,
278+
should_fail: should_fail
279+
}
280+
end
281+
282+
private
283+
284+
def ensure_database_setup
285+
unless ActiveRecord::Base.connection.table_exists?('posts')
286+
setup_database
287+
end
288+
end
289+
83290
def set_cors_headers
84291
response.headers['Access-Control-Allow-Origin'] = '*'
85292
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
@@ -89,12 +296,43 @@ def set_cors_headers
89296

90297
RailsMiniApp.initialize!
91298

299+
def setup_database
300+
ActiveRecord::Schema.define do
301+
create_table :posts, force: true do |t|
302+
t.string :title, null: false
303+
t.text :content
304+
t.timestamps
305+
end
306+
307+
create_table :users, force: true do |t|
308+
t.string :name, null: false
309+
t.string :email
310+
t.timestamps
311+
end
312+
end
313+
314+
Post.create!(title: "Welcome Post", content: "Welcome to the Rails mini app!")
315+
Post.create!(title: "Sample Post", content: "This is a sample post for testing.")
316+
User.create!(name: "Test User", email: "[email protected]")
317+
end
318+
319+
setup_database
320+
92321
RailsMiniApp.routes.draw do
93322
get '/health', to: 'events#health'
94323
get '/error', to: 'error#error'
95324
get '/trace_headers', to: 'events#trace_headers'
325+
get '/logged_events', to: 'events#logged_events'
326+
post '/clear_logged_events', to: 'events#clear_logged_events'
327+
328+
get '/posts', to: 'posts#index'
329+
post '/posts', to: 'posts#create'
330+
get '/posts/:id', to: 'posts#show'
331+
332+
post '/jobs/sample', to: 'jobs#sample_job'
333+
post '/jobs/database', to: 'jobs#database_job'
334+
post '/jobs/failing', to: 'jobs#failing_job'
96335

97-
# Add CORS headers for cross-origin requests from JS app
98336
match '*path', to: proc { |env|
99337
[200, {
100338
'Access-Control-Allow-Origin' => '*',

0 commit comments

Comments
 (0)