rails_semantic_logger 4.15.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bb41dd02df0dc0dc472a66f83a82146878942a309670d4fb10da64bfdae4bc4
4
- data.tar.gz: c1463e8da359c87fffc4e7d4e56919609320473fa0702b6a7f9b9fec1bdc5050
3
+ metadata.gz: 7da18e2122c13f2b4325a6f3000dd74d22517608b3a32355ca3d2bf4e5f08789
4
+ data.tar.gz: b0503c3227cb0bfa5faa1c6f00b408968783c8d09092a960497ab9f004798875
5
5
  SHA512:
6
- metadata.gz: 1604cdbeed7b9bda178517f7cd7f72e4721523cf222aa6b0a2dcce36fc77c62807641ca32aef349f91c9e5aa86d04e9202e15adff71e4b5e4e29798a3c9dde16
7
- data.tar.gz: b0e79372c2111bb823c191595a600c8c4c218e6989249f66c0ec46d66826116abfe2f6c5c761d33ae23f1494e7a74b8299d70b037410570757195115b212dced
6
+ metadata.gz: 30446e6a7553868e37cb56550d0a3e5c15b137991609e2cbb7aee84c7c7f5a75abe0bdd7f5e6263418546645f7a0892472e042ee09e0764be9f0ff9d90311917
7
+ data.tar.gz: 051ff94e24b1d9a4231f01d4148a966333b8d7b57539f7fa15ab953ad0a2fc9c998b61413ae2350716763f323eafa6d7302893b0c7b691e165c2200f7a8f3ef1
data/README.md CHANGED
@@ -3,12 +3,112 @@
3
3
 
4
4
  Rails Semantic Logger replaces the Rails default logger with [Semantic Logger](https://logger.rocketjob.io/)
5
5
 
6
+ When any large Rails application is deployed to production one of the first steps is to move to centralized logging, so that logs can be viewed and searched from a central location.
7
+
8
+ Centralized logging quickly falls apart when trying to consume the current human readable log files:
9
+ - Log entries often span multiple lines, resulting in unrelated log lines in the centralized logging system. For example, stack traces.
10
+ - Complex Regular Expressions are needed to parse the text lines and make them machine readable. For example to build queries, or alerts that are looking for specific elements in the message.
11
+ - Writing searches, alerts, or dashboards based on text logs is incredibly brittle, since a small change to the text logged can often break the parsing of those logs.
12
+ - Every log entry often has a completely different format, making it difficult to make consistent searches against the data.
13
+
14
+ For these and many other reasons switching to structured logging, or logs in JSON format, in testing and production makes centralized logging incredibly powerful.
15
+
16
+ For example, adding these lines to `config/application.rb` and removing any other log overrides from other environments, will switch automatically to structured logging when running inside Kubernetes:
17
+ ~~~ruby
18
+ # Setup structured logging
19
+ config.semantic_logger.application = "my_application"
20
+ config.semantic_logger.environment = ENV["STACK_NAME"] || Rails.env
21
+ config.log_level = ENV["LOG_LEVEL"] || :info
22
+
23
+ # Switch to JSON Logging output to stdout when running on Kubernetes
24
+ if ENV["LOG_TO_CONSOLE"] || ENV["KUBERNETES_SERVICE_HOST"]
25
+ config.rails_semantic_logger.add_file_appender = false
26
+ config.semantic_logger.add_appender(io: $stdout, formatter: :json)
27
+ end
28
+ ~~~
29
+
30
+ Then configure the centralized logging system to tell it that the data is in JSON format, so that it will parse it for you into a hierarchy.
31
+
32
+ For example, the following will instruct [Observe](https://www.observeinc.com/) to parse the JSON data and create machine readable data from it:
33
+ ~~~ruby
34
+ interface "log", "log":log
35
+
36
+ make_col event:parse_json(log)
37
+
38
+ make_col
39
+ time:parse_isotime(event.timestamp),
40
+ application:string(event.application),
41
+ environment:string(event.environment),
42
+ duration:duration_ms(event.duration_ms),
43
+ level:string(event.level),
44
+ name:string(event.name),
45
+ message:string(event.message),
46
+ named_tags:event.named_tags,
47
+ payload:event.payload,
48
+ metric:string(event.metric),
49
+ metric_amount:float64(event.metric_amount),
50
+ tags:array(event.tags),
51
+ exception:event.exception,
52
+ host:string(event.host),
53
+ pid:int64(event.pid),
54
+ thread:string(event.thread),
55
+ file:string(event.file),
56
+ line:int64(event.line),
57
+ dimensions:event.dimensions,
58
+ backtrace:array(event.backtrace),
59
+ level_index:int64(event.level_index)
60
+
61
+ set_valid_from(time)
62
+ drop_col timestamp, log, event, stream
63
+ rename_col timestamp:time
64
+ ~~~
65
+
66
+ Now queries can be built to drill down into each of these fields, including `payload` which is a nested object.
67
+
68
+ For example to find all failed Sidekiq job calls where the causing exception class name is `NoMethodError`:
69
+ ~~~ruby
70
+ filter environment = "uat2"
71
+ filter level = "error"
72
+ filter metric = "sidekiq.job.perform"
73
+ filter (string(exception.cause.name) = "NoMethodError")
74
+ ~~~
75
+
76
+ Example: create a dashboard showing the duration of all successful Sidekiq jobs:
77
+ ~~~ruby
78
+ filter environment = "production"
79
+ filter level = "info"
80
+ filter metric = "sidekiq.job.perform"
81
+ timechart duration:avg(duration), group_by(name)
82
+ ~~~
83
+
84
+ Example: create a dashboard showing the queue latency of all Sidekiq jobs.
85
+ The queue latency is the time between when the job was enqueued and when it was started:
86
+ ~~~ruby
87
+ filter environment = "production"
88
+ filter level = "info"
89
+ filter metric = "sidekiq.queue.latency"
90
+ timechart duration:avg(duration), group_by(name)
91
+ ~~~
92
+
6
93
  * http://github.com/reidmorrison/rails_semantic_logger
7
94
 
8
95
  ## Documentation
9
96
 
10
97
  For complete documentation see: https://logger.rocketjob.io/rails
11
98
 
99
+ ## Upgrading to Semantic Logger V4.16 - Sidekiq Metrics Support
100
+
101
+ Rails Semantic Logger now supports Sidekiq metrics.
102
+ Below are the metrics that are now available when the JSON logging format is used:
103
+ - `sidekiq.job.perform` - The duration of each Sidekiq job.
104
+ - `sidekiq.queue.latency` - The time between when a Sidekiq job was enqueued and when it was started.
105
+
106
+ ## Upgrading to Semantic Logger v4.15 & V4.16 - Sidekiq Support
107
+
108
+ Rails Semantic Logger introduces direct support for Sidekiq v4, v5, v6, and v7.
109
+ Please remove any previous custom patches or configurations to make Sidekiq work with Semantic Logger.
110
+ To see the complete list of patches being made, and to contribute your own changes, see: [Sidekiq Patches](https://github.com/reidmorrison/rails_semantic_logger/blob/master/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb)
111
+
12
112
  ## Upgrading to Semantic Logger v4.4
13
113
 
14
114
  With some forking frameworks it is necessary to call `reopen` after the fork. With v4.4 the
@@ -19,7 +119,18 @@ I.e. Please remove the following line if being called anywhere:
19
119
  SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new)
20
120
  ~~~
21
121
 
22
- ## Supports
122
+ ## New Versions of Rails, etc.
123
+
124
+ The primary purpose of the Rails Semantic Logger gem is to patch other gems, primarily Rails, to make them support structured logging though Semantic Logger.
125
+
126
+ When new versions of Rails and other gems are published they often make changes to the internals, so the existing patches stop working.
127
+
128
+ Rails Semantic Logger survives only when someone in the community upgrades to a newer Rails or other supported libraries, runs into problems,
129
+ and then contributes the fix back to the community by means of a pull request.
130
+
131
+ Additionally, when new popular gems come out, we rely only the community to supply the necessary patches in Rails Semantic Logger to make those gems support structured logging.
132
+
133
+ ## Supported Platforms
23
134
 
24
135
  For the complete list of supported Ruby and Rails versions, see the [Testing file](https://github.com/reidmorrison/rails_semantic_logger/blob/master/.github/workflows/ci.yml).
25
136
 
@@ -33,18 +33,26 @@ module Sidekiq
33
33
  if defined?(::Sidekiq::JobLogger)
34
34
  # Let Semantic Logger handle duration logging
35
35
  class JobLogger
36
- def call(item, queue)
36
+ def call(item, queue, &block)
37
37
  klass = item["wrapped"] || item["class"]
38
- metric = "Sidekiq/#{klass}/perform" if klass
39
38
  logger = klass ? SemanticLogger[klass] : Sidekiq.logger
40
- logger.info("Start #perform")
41
- logger.measure_info(
42
- "Completed #perform",
43
- on_exception_level: :error,
44
- log_exception: :full,
45
- metric: metric
46
- ) do
47
- yield
39
+
40
+ SemanticLogger.tagged(queue: queue) do
41
+ # Latency is the time between when the job was enqueued and when it started executing.
42
+ logger.info(
43
+ "Start #perform",
44
+ metric: "sidekiq.queue.latency",
45
+ metric_amount: job_latency_ms(item)
46
+ )
47
+
48
+ # Measure the duration of running the job
49
+ logger.measure_info(
50
+ "Completed #perform",
51
+ on_exception_level: :error,
52
+ log_exception: :full,
53
+ metric: "sidekiq.job.perform",
54
+ &block
55
+ )
48
56
  end
49
57
  end
50
58
 
@@ -60,14 +68,18 @@ module Sidekiq
60
68
  end
61
69
 
62
70
  def job_hash_context(job_hash)
63
- h = {
64
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
65
- jid: job_hash["jid"]
66
- }
67
- h[:bid] = job_hash["bid"] if job_hash["bid"]
68
- h[:tags] = job_hash["tags"] if job_hash["tags"]
71
+ h = {jid: job_hash["jid"]}
72
+ h[:bid] = job_hash["bid"] if job_hash["bid"]
73
+ h[:tags] = job_hash["tags"] if job_hash["tags"]
74
+ h[:queue] = job_hash["queue"] if job_hash["queue"]
69
75
  h
70
76
  end
77
+
78
+ def job_latency_ms(job)
79
+ return unless job && job["enqueued_at"]
80
+
81
+ (Time.now.to_f - job["enqueued_at"].to_f) * 1000
82
+ end
71
83
  end
72
84
  end
73
85
 
@@ -80,48 +92,47 @@ module Sidekiq
80
92
  end
81
93
 
82
94
  def self.job_hash_context(job_hash)
83
- klass = job_hash["wrapped"] || job_hash["class"]
84
- event = { class: klass, jid: job_hash["jid"] }
85
- event[:bid] = job_hash["bid"] if job_hash["bid"]
86
- event
95
+ h = {jid: job_hash["jid"]}
96
+ h[:bid] = job_hash["bid"] if job_hash["bid"]
97
+ h[:queue] = job_hash["queue"] if job_hash["queue"]
98
+ h
87
99
  end
88
100
  end
89
101
  end
90
102
 
91
103
  # Exception is already logged by Semantic Logger during the perform call
92
- # Sidekiq <= v6.5
93
104
  if defined?(::Sidekiq::ExceptionHandler)
105
+ # Sidekiq <= v6.5
94
106
  module ExceptionHandler
95
107
  class Logger
96
- def call(ex, ctx)
97
- unless ctx.empty?
98
- job_hash = ctx[:job] || {}
99
- klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
100
- logger = klass ? SemanticLogger[klass] : Sidekiq.logger
101
- ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx)
102
- end
108
+ def call(_exception, ctx)
109
+ return if ctx.empty?
110
+
111
+ job_hash = ctx[:job] || {}
112
+ klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
113
+ logger = klass ? SemanticLogger[klass] : Sidekiq.logger
114
+ ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx)
103
115
  end
104
116
  end
105
117
  end
106
- # Sidekiq >= v7
107
118
  elsif defined?(::Sidekiq::Config)
119
+ # Sidekiq >= v7
108
120
  class Config
109
121
  remove_const :ERROR_HANDLER
110
122
 
111
- ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
123
+ ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) do
112
124
  unless ctx.empty?
113
125
  job_hash = ctx[:job] || {}
114
- klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
115
- logger = klass ? SemanticLogger[klass] : Sidekiq.logger
126
+ klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
127
+ logger = klass ? SemanticLogger[klass] : Sidekiq.logger
116
128
  ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx)
117
129
  end
118
- }
130
+ end
119
131
  end
120
132
  else
121
133
  # Sidekiq >= 6.5
122
- # TODO: Not taking effect. See test/sidekiq_test.rb
123
- def self.default_error_handler(ex, ctx)
124
- binding.irb
134
+ Sidekiq.error_handlers.delete(Sidekiq::DEFAULT_ERROR_HANDLER)
135
+ Sidekiq.error_handlers << ->(ex, ctx) do
125
136
  unless ctx.empty?
126
137
  job_hash = ctx[:job] || {}
127
138
  klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
@@ -132,10 +143,13 @@ module Sidekiq
132
143
  end
133
144
 
134
145
  # Logging within each worker should use its own logger
135
- if Sidekiq::VERSION.to_i == 4
146
+ case Sidekiq::VERSION.to_i
147
+ when 4
136
148
  module Worker
137
149
  def self.included(base)
138
- raise ArgumentError, "You cannot include Sidekiq::Worker in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
150
+ if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
151
+ raise ArgumentError, "You cannot include Sidekiq::Worker in an ActiveJob: #{base.name}"
152
+ end
139
153
 
140
154
  base.extend(ClassMethods)
141
155
  base.include(SemanticLogger::Loggable)
@@ -144,10 +158,12 @@ module Sidekiq
144
158
  base.class_attribute :sidekiq_retries_exhausted_block
145
159
  end
146
160
  end
147
- elsif Sidekiq::VERSION.to_i == 5
161
+ when 5
148
162
  module Worker
149
163
  def self.included(base)
150
- raise ArgumentError, "You cannot include Sidekiq::Worker in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
164
+ if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
165
+ raise ArgumentError, "You cannot include Sidekiq::Worker in an ActiveJob: #{base.name}"
166
+ end
151
167
 
152
168
  base.extend(ClassMethods)
153
169
  base.include(SemanticLogger::Loggable)
@@ -156,10 +172,12 @@ module Sidekiq
156
172
  base.sidekiq_class_attribute :sidekiq_retries_exhausted_block
157
173
  end
158
174
  end
159
- elsif Sidekiq::VERSION.to_i == 6
175
+ when 6
160
176
  module Worker
161
177
  def self.included(base)
162
- raise ArgumentError, "Sidekiq::Worker cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
178
+ if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
179
+ raise ArgumentError, "Sidekiq::Worker cannot be included in an ActiveJob: #{base.name}"
180
+ end
163
181
 
164
182
  base.include(Options)
165
183
  base.extend(ClassMethods)
@@ -169,7 +187,9 @@ module Sidekiq
169
187
  else
170
188
  module Job
171
189
  def self.included(base)
172
- raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
190
+ if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
191
+ raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}"
192
+ end
173
193
 
174
194
  base.include(Options)
175
195
  base.extend(ClassMethods)
@@ -178,14 +198,15 @@ module Sidekiq
178
198
  end
179
199
  end
180
200
 
181
- if Sidekiq::VERSION.to_i == 4
201
+ if defined?(::Sidekiq::Middleware::Server::Logging)
202
+ # Sidekiq v4
182
203
  # Convert string to machine readable format
183
204
  class Processor
184
205
  def log_context(job_hash)
185
- klass = job_hash["wrapped"] || job_hash["class"]
186
- event = { class: klass, jid: job_hash["jid"] }
187
- event[:bid] = job_hash["bid"] if job_hash["bid"]
188
- event
206
+ h = {jid: job_hash["jid"]}
207
+ h[:bid] = job_hash["bid"] if job_hash["bid"]
208
+ h[:queue] = job_hash["queue"] if job_hash["queue"]
209
+ h
189
210
  end
190
211
  end
191
212
 
@@ -194,16 +215,26 @@ module Sidekiq
194
215
  module Server
195
216
  class Logging
196
217
  def call(worker, item, queue)
197
- worker.logger.info("Start #perform")
198
- worker.logger.measure_info(
199
- "Completed #perform",
200
- on_exception_level: :error,
201
- log_exception: :full,
202
- metric: "Sidekiq/#{worker.class.name}/perform"
203
- ) do
204
- yield
218
+ SemanticLogger.tagged(queue: queue) do
219
+ worker.logger.info(
220
+ "Start #perform",
221
+ metric: "sidekiq.queue.latency",
222
+ metric_amount: job_latency_ms(item)
223
+ )
224
+ worker.logger.measure_info(
225
+ "Completed #perform",
226
+ on_exception_level: :error,
227
+ log_exception: :full,
228
+ metric: "sidekiq.job.perform"
229
+ ) { yield }
205
230
  end
206
231
  end
232
+
233
+ def job_latency_ms(job)
234
+ return unless job && job["enqueued_at"]
235
+
236
+ (Time.now.to_f - job["enqueued_at"].to_f) * 1000
237
+ end
207
238
  end
208
239
  end
209
240
  end
@@ -1,3 +1,3 @@
1
1
  module RailsSemanticLogger
2
- VERSION = "4.15.0".freeze
2
+ VERSION = "4.16.0".freeze
3
3
  end
@@ -69,7 +69,7 @@ require("rails_semantic_logger/extensions/active_support/logger") if defined?(Ac
69
69
  require("rails_semantic_logger/extensions/active_support/log_subscriber") if defined?(ActiveSupport::LogSubscriber)
70
70
 
71
71
  begin
72
- require 'rackup'
72
+ require "rackup"
73
73
  rescue LoadError
74
74
  # No need to do anything, will fall back to Rack
75
75
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.15.0
4
+ version: 4.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-27 00:00:00.000000000 Z
11
+ date: 2024-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -92,7 +92,7 @@ licenses:
92
92
  metadata:
93
93
  bug_tracker_uri: https://github.com/reidmorrison/rails_semantic_logger/issues
94
94
  documentation_uri: https://logger.rocketjob.io
95
- source_code_uri: https://github.com/reidmorrison/rails_semantic_logger/tree/v4.15.0
95
+ source_code_uri: https://github.com/reidmorrison/rails_semantic_logger/tree/v4.16.0
96
96
  rubygems_mfa_required: 'true'
97
97
  post_install_message:
98
98
  rdoc_options: []