Skip to content

Commit 0523e66

Browse files
committed
[rails] add e2e specs for rails structured logging
1 parent 06ca452 commit 0523e66

File tree

6 files changed

+274
-9
lines changed

6 files changed

+274
-9
lines changed

sentry-ruby/lib/sentry/debug_structured_logger.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ def capture_log_event(level, message, parameters, **attributes)
5151
end
5252

5353
def logged_events
54-
return [] unless File.exist?(log_file)
55-
5654
File.readlines(log_file).map do |line|
5755
JSON.parse(line)
5856
end

spec/apps/rails-mini/Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ source 'https://rubygems.org'
44

55
gem "rake"
66
gem "puma"
7+
78
gem 'railties'
89
gem 'actionpack'
10+
gem 'activerecord'
11+
gem 'activejob'
12+
gem 'sqlite3'
13+
914
gem 'sentry-ruby', path: Pathname(__dir__).join("../../..").realpath
1015
gem 'sentry-rails', path: Pathname(__dir__).join("../../..").realpath

spec/apps/rails-mini/app.rb

Lines changed: 187 additions & 1 deletion
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:tmp/rails_mini_development.sqlite3"
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
@@ -39,10 +42,71 @@ def debug_log_path
3942
config.transport.transport_class = Sentry::DebugTransport
4043
config.sdk_debug_transport_log_file = debug_log_path.join("sentry_debug_events.log")
4144
config.background_worker_threads = 0
45+
46+
config.enable_logs = true
47+
config.structured_logger_class = Sentry::DebugStructuredLogger
48+
config.sdk_debug_structured_logger_log_file = debug_log_path.join("sentry_e2e_tests.log")
49+
50+
config.rails.structured_logging.enabled = true
51+
config.rails.structured_logging.attach_to = [:active_record, :action_controller, :active_job]
4252
end
4353
end
4454
end
4555

56+
class Post < ActiveRecord::Base
57+
end
58+
59+
class User < ActiveRecord::Base
60+
end
61+
62+
class ApplicationJob < ActiveJob::Base
63+
retry_on ActiveRecord::Deadlocked
64+
65+
discard_on ActiveJob::DeserializationError
66+
end
67+
68+
class SampleJob < ApplicationJob
69+
queue_as :default
70+
71+
def perform(message = "Hello from ActiveJob!")
72+
Rails.logger.info("SampleJob executed with message: #{message}")
73+
74+
Post.count
75+
User.count
76+
77+
message
78+
end
79+
end
80+
81+
class DatabaseJob < ApplicationJob
82+
queue_as :default
83+
84+
def perform(post_title = "Test Post")
85+
Rails.logger.info("DatabaseJob creating post: #{post_title}")
86+
87+
post = Post.create!(title: post_title, content: "Content for #{post_title}")
88+
found_post = Post.find(post.id)
89+
90+
Rails.logger.info("DatabaseJob found post: #{found_post.title}")
91+
92+
found_post
93+
end
94+
end
95+
96+
class FailingJob < ApplicationJob
97+
queue_as :default
98+
99+
def perform(should_fail = true)
100+
Rails.logger.info("FailingJob started")
101+
102+
if should_fail
103+
raise StandardError, "Intentional job failure for testing"
104+
end
105+
106+
"Job completed successfully"
107+
end
108+
end
109+
46110
class ErrorController < ActionController::Base
47111
before_action :set_cors_headers
48112

@@ -97,12 +161,134 @@ def set_cors_headers
97161
end
98162
end
99163

164+
class PostsController < ActionController::Base
165+
before_action :set_cors_headers
166+
def index
167+
posts = Post.all.to_a
168+
169+
Sentry.logger.info("Posts index accessed", posts_count: posts.length)
170+
171+
render json: {
172+
posts: posts.map { |p| { id: p.id, title: p.title, content: p.content } }
173+
}
174+
end
175+
176+
def create
177+
post = Post.create!(post_params)
178+
179+
Sentry.logger.info("Post created", post_id: post.id, title: post.title)
180+
181+
render json: { post: { id: post.id, title: post.title, content: post.content } }, status: :created
182+
rescue ActiveRecord::RecordInvalid => e
183+
render json: { error: e.message }, status: :unprocessable_entity
184+
end
185+
186+
def show
187+
post = Post.find(params[:id])
188+
render json: { post: { id: post.id, title: post.title, content: post.content } }
189+
rescue ActiveRecord::RecordNotFound
190+
render json: { error: "Post not found" }, status: :not_found
191+
end
192+
193+
private
194+
195+
def post_params
196+
params.require(:post).permit(:title, :content)
197+
end
198+
199+
def set_cors_headers
200+
response.headers['Access-Control-Allow-Origin'] = '*'
201+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
202+
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, sentry-trace, baggage'
203+
end
204+
end
205+
206+
class JobsController < ActionController::Base
207+
before_action :set_cors_headers
208+
209+
def sample_job
210+
job = SampleJob.perform_later("Hello from Rails mini app!")
211+
212+
Sentry.logger.info("SampleJob enqueued", job_id: job.job_id)
213+
214+
render json: {
215+
message: "SampleJob enqueued successfully",
216+
job_id: job.job_id,
217+
job_class: job.class.name
218+
}
219+
end
220+
221+
def database_job
222+
title = params[:title] || "Test Post from Job"
223+
job = DatabaseJob.perform_later(title)
224+
225+
Sentry.logger.info("DatabaseJob enqueued", job_id: job.job_id, post_title: title)
226+
227+
render json: {
228+
message: "DatabaseJob enqueued successfully",
229+
job_id: job.job_id,
230+
job_class: job.class.name,
231+
post_title: title
232+
}
233+
end
234+
235+
def failing_job
236+
should_fail = params[:should_fail] != "false"
237+
job = FailingJob.perform_later(should_fail)
238+
239+
Sentry.logger.info("FailingJob enqueued", job_id: job.job_id, should_fail: should_fail)
240+
241+
render json: {
242+
message: "FailingJob enqueued successfully",
243+
job_id: job.job_id,
244+
job_class: job.class.name,
245+
should_fail: should_fail
246+
}
247+
end
248+
249+
private
250+
251+
def set_cors_headers
252+
response.headers['Access-Control-Allow-Origin'] = '*'
253+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
254+
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, sentry-trace, baggage'
255+
end
256+
end
257+
100258
RailsMiniApp.initialize!
101259

260+
ActiveRecord::Schema.define do
261+
create_table :posts, force: true do |t|
262+
t.string :title, null: false
263+
t.text :content
264+
t.timestamps
265+
end
266+
267+
create_table :users, force: true do |t|
268+
t.string :name, null: false
269+
t.string :email
270+
t.timestamps
271+
end
272+
end
273+
274+
Post.create!(title: "Welcome Post", content: "Welcome to the Rails mini app!")
275+
Post.create!(title: "Sample Post", content: "This is a sample post for testing.")
276+
User.create!(name: "Test User", email: "test@example.com")
277+
102278
RailsMiniApp.routes.draw do
103279
get '/health', to: 'events#health'
104280
get '/error', to: 'error#error'
105281
get '/trace_headers', to: 'events#trace_headers'
282+
get '/logged_events', to: 'events#logged_events'
283+
post '/clear_logged_events', to: 'events#clear_logged_events'
284+
285+
get '/posts', to: 'posts#index'
286+
post '/posts', to: 'posts#create'
287+
get '/posts/:id', to: 'posts#show'
288+
289+
post '/jobs/sample', to: 'jobs#sample_job'
290+
post '/jobs/database', to: 'jobs#database_job'
291+
post '/jobs/failing', to: 'jobs#failing_job'
106292

107293
match '*path', to: proc { |env|
108294
[200, {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe "Structured Logging", type: :e2e do
4+
it "captures Rails application logs using structured logging" do
5+
response = make_request("/posts")
6+
expect(response.code).to eq("200")
7+
8+
logged_events = Sentry.logger.logged_events
9+
expect(logged_events).not_to be_empty
10+
11+
log_event = logged_events.first
12+
expect(log_event).to have_key("timestamp")
13+
expect(log_event).to have_key("level")
14+
expect(log_event).to have_key("message")
15+
expect(log_event).to have_key("attributes")
16+
expect(log_event["timestamp"]).to be_a(String)
17+
end
18+
19+
it "captures logs from Rails mini app" do
20+
response = make_request("/posts")
21+
expect(response.code).to eq("200")
22+
23+
logged_events = Sentry.logger.logged_events
24+
expect(logged_events).not_to be_empty
25+
26+
posts_log = logged_events.find { |log| log["message"] == "Posts index accessed" }
27+
expect(posts_log).not_to be_nil
28+
expect(posts_log["level"]).to eq("info")
29+
expect(posts_log["attributes"]["posts_count"]).to eq(2)
30+
end
31+
32+
it "captures structured logs with proper format" do
33+
response = make_request("/posts")
34+
expect(response.code).to eq("200")
35+
36+
logged_events = Sentry.logger.logged_events
37+
expect(logged_events).not_to be_empty
38+
39+
log_event = logged_events.first
40+
expect(log_event).to have_key("timestamp")
41+
expect(log_event).to have_key("level")
42+
expect(log_event).to have_key("message")
43+
expect(log_event).to have_key("attributes")
44+
expect(log_event["timestamp"]).to be_a(String)
45+
expect(log_event["level"]).to be_a(String)
46+
expect(log_event["message"]).to be_a(String)
47+
expect(log_event["attributes"]).to be_a(Hash)
48+
end
49+
end

spec/spec_helper.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
require "capybara"
99
require "capybara/rspec"
10+
require "debug"
1011

1112
require_relative "support/test_helper"
1213

@@ -31,12 +32,14 @@
3132

3233
RSpec.configure do |config|
3334
config.include(Capybara::DSL, type: :e2e)
34-
3535
config.include(Test::Helper)
3636

3737
config.before(:suite) do
3838
Test::Helper.perform_basic_setup do |config|
3939
config.transport.transport_class = Sentry::DebugTransport
40+
config.structured_logger_class = Sentry::DebugStructuredLogger
41+
config.sdk_debug_structured_logger_log_file = Test::Helper.debug_log_path.join("sentry_e2e_tests.log")
42+
config.enable_logs = true
4043
end
4144

4245
Test::Helper.clear_logged_events
@@ -45,4 +48,8 @@
4548
config.after(:each) do
4649
Test::Helper.clear_logged_events
4750
end
51+
52+
config.after(:each) do
53+
Test::Helper.clear_logs
54+
end
4855
end

spec/support/test_helper.rb

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# frozen_string_literal: true
22

3+
require 'net/http'
4+
require 'uri'
5+
36
module Test
47
module Helper
58
module_function
69

10+
def rails_app_url
11+
ENV.fetch("SENTRY_E2E_RAILS_APP_URL")
12+
end
13+
14+
def make_request(path)
15+
Net::HTTP.get_response(URI("#{rails_app_url}#{path}"))
16+
end
17+
718
def logged_events
819
@logged_events ||= begin
920
extracted_events = []
@@ -40,11 +51,7 @@ def logged_envelopes
4051
end
4152

4253
def logged_structured_events
43-
if Sentry.logger.is_a?(Sentry::DebugStructuredLogger)
44-
Sentry.logger.logged_events
45-
else
46-
[]
47-
end
54+
Sentry.logger.logged_events
4855
end
4956

5057
# TODO: move this to a shared helper for all gems
@@ -58,5 +65,18 @@ def perform_basic_setup
5865
yield(config) if block_given?
5966
end
6067
end
68+
69+
def debug_log_path
70+
@log_path ||= begin
71+
path = Pathname(__dir__).join("../../log")
72+
FileUtils.mkdir_p(path) unless path.exist?
73+
path.realpath
74+
end
75+
end
76+
77+
def clear_logs
78+
Sentry.get_current_client.transport.clear
79+
Sentry.logger.clear
80+
end
6181
end
6282
end

0 commit comments

Comments
 (0)