contextualized_logs 0.0.1.pre.alpha

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +480 -0
  4. data/Rakefile +28 -0
  5. data/app/assets/config/manifest.js +3 -0
  6. data/app/assets/javascripts/application.js +16 -0
  7. data/app/assets/javascripts/cable.js +13 -0
  8. data/app/assets/stylesheets/application.css +15 -0
  9. data/app/channels/application_cable/channel.rb +4 -0
  10. data/app/channels/application_cable/connection.rb +4 -0
  11. data/app/controllers/application_controller.rb +2 -0
  12. data/app/controllers/model_controller.rb +26 -0
  13. data/app/helpers/application_helper.rb +2 -0
  14. data/app/jobs/application_job.rb +2 -0
  15. data/app/mailers/application_mailer.rb +4 -0
  16. data/app/models/application_record.rb +3 -0
  17. data/app/models/model.rb +4 -0
  18. data/app/views/layouts/application.html.erb +15 -0
  19. data/app/views/layouts/mailer.html.erb +13 -0
  20. data/app/views/layouts/mailer.text.erb +1 -0
  21. data/app/workers/model_worker.rb +13 -0
  22. data/config/application.rb +19 -0
  23. data/config/boot.rb +4 -0
  24. data/config/cable.yml +10 -0
  25. data/config/credentials.yml.enc +1 -0
  26. data/config/database.yml +25 -0
  27. data/config/environment.rb +5 -0
  28. data/config/environments/development.rb +62 -0
  29. data/config/environments/production.rb +94 -0
  30. data/config/environments/test.rb +47 -0
  31. data/config/initializers/application_controller_renderer.rb +8 -0
  32. data/config/initializers/assets.rb +14 -0
  33. data/config/initializers/backtrace_silencers.rb +7 -0
  34. data/config/initializers/content_security_policy.rb +25 -0
  35. data/config/initializers/cookies_serializer.rb +5 -0
  36. data/config/initializers/filter_parameter_logging.rb +4 -0
  37. data/config/initializers/inflections.rb +16 -0
  38. data/config/initializers/mime_types.rb +4 -0
  39. data/config/initializers/sidekiq.rb +23 -0
  40. data/config/initializers/wrap_parameters.rb +14 -0
  41. data/config/locales/en.yml +33 -0
  42. data/config/master.key +1 -0
  43. data/config/puma.rb +37 -0
  44. data/config/routes.rb +3 -0
  45. data/config/spring.rb +6 -0
  46. data/config/storage.yml +34 -0
  47. data/db/development.sqlite3 +0 -0
  48. data/db/migrate/20200424081113_create_model.rb +7 -0
  49. data/db/schema.rb +19 -0
  50. data/db/seeds.rb +7 -0
  51. data/db/test.sqlite3 +0 -0
  52. data/lib/contextualized_logs/contextualized_controller.rb +63 -0
  53. data/lib/contextualized_logs/contextualized_logger.rb +90 -0
  54. data/lib/contextualized_logs/contextualized_model.rb +54 -0
  55. data/lib/contextualized_logs/contextualized_worker.rb +41 -0
  56. data/lib/contextualized_logs/current_context.rb +99 -0
  57. data/lib/contextualized_logs/railtie.rb +9 -0
  58. data/lib/contextualized_logs/sidekiq/middleware/client/inject_current_context.rb +38 -0
  59. data/lib/contextualized_logs/sidekiq/middleware/server/restore_current_context.rb +43 -0
  60. data/lib/contextualized_logs/version.rb +3 -0
  61. data/lib/contextualized_logs.rb +11 -0
  62. data/lib/tasks/contextualized_logs_tasks.rake +4 -0
  63. metadata +123 -0
@@ -0,0 +1,90 @@
1
+ require 'active_support'
2
+
3
+ module ContextualizedLogs
4
+ # custom logger for
5
+ # logging in json format with log enrichment
6
+ # support Rails.logger.dump('msg', hash)
7
+
8
+ class ContextualizedLogger < ActiveSupport::Logger
9
+ def initialize(*args)
10
+ super(*args)
11
+ @formatter = formatter
12
+ end
13
+
14
+ def dump(msg, attributes, severity = :info)
15
+ # log message with attributes as structured dump attributes
16
+ send(severity, { msg: msg, attributes: attributes })
17
+ end
18
+
19
+ def dump_error(msg, attributes)
20
+ dump(msg, attributes, :error)
21
+ end
22
+
23
+ def current_context
24
+ CurrentContext.context
25
+ end
26
+
27
+ def formatter
28
+ Proc.new{|severity, timestamp, progname, msg|
29
+ # format (and enrich) log in JSON format (-> )
30
+ # https://docs.hq.com/logs/processing/attributes_naming_convention/#source-code
31
+ # correlation = Datadog.tracer.active_correlation
32
+ data = {
33
+ # dd: {
34
+ # trace_id: correlation.trace_id,
35
+ # span_id: correlation.span_id
36
+ # },
37
+ # ddsource: ['ruby'],
38
+ syslog: { env: Rails.env, host: Socket.gethostname },
39
+ type: severity.to_s,
40
+ time: timestamp
41
+ }
42
+ data[:stack] = Kernel.caller.
43
+ map { |caller| caller.gsub(/#{Rails.root}/, '') }.
44
+ reject { |caller| caller.start_with?('/usr/local') || caller.include?('/shared/bundle/') || caller.start_with?('/Users/') }.
45
+ first(15)
46
+ data[:log_type] = 'log'
47
+ data.merge!(parse_msg(msg)) # parse message (string, hash, error, ...)
48
+ data.merge!(current_context) # merge current request context
49
+ data.to_json + "\n"
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def parse_msg(msg)
56
+ data = {}
57
+ case msg
58
+ when Hash
59
+ # used by logger.dump(msg|error, attributes = {})
60
+ if msg.include?(:attributes)
61
+ # adding message as log attributes if is a hash
62
+ data.merge!(parse_error(msg[:msg]))
63
+ data[:attributes] = msg[:attributes]
64
+ else
65
+ data.merge!(parse_error(msg))
66
+ end
67
+ else
68
+ data.merge!(parse_error(msg))
69
+ end
70
+ data
71
+ end
72
+
73
+ def parse_error(msg)
74
+ data = {}
75
+ case msg
76
+ when ::Exception
77
+ # format data to be interpreted as an error logs by
78
+ data[:error] = {
79
+ kind: msg.class.to_s,
80
+ message: msg.message,
81
+ stack: (msg.backtrace || []).join('; ')
82
+ }
83
+ else
84
+ data[:message] = msg.to_s
85
+ end
86
+ data
87
+ end
88
+ end
89
+
90
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support'
2
+ require_relative 'current_context'
3
+
4
+ module ContextualizedLogs
5
+ module ContextualizedModel
6
+ extend ActiveSupport::Concern
7
+
8
+ DEFAULT_CURRENT_CONTEXT = ContextualizedLogs::CurrentContext
9
+
10
+ class_methods do
11
+ attr_reader :contextualizable_keys
12
+
13
+ def current_context
14
+ @current_context || DEFAULT_CURRENT_CONTEXT
15
+ end
16
+
17
+ private
18
+
19
+ def contextualizable(keys: {})
20
+ @contextualizable_keys = keys
21
+ end
22
+
23
+ def set_current_context(current_context)
24
+ @current_context = current_context
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def contextualize(model, keys, context)
30
+ # Rails.logger.debug "model: #{model}"
31
+ # Rails.logger.debug "keys: #{keys}"
32
+ # Rails.logger.debug "context.context: #{context}"
33
+ # Rails.logger.debug "context.contextualized_model_enabled: #{context.contextualized_model_enabled}"
34
+ return unless context.contextualized_model_enabled
35
+ keys&.each do |k, v|
36
+ v = model.try(v.to_sym)
37
+ context.add_context(k, v) if v
38
+ end
39
+ end
40
+ end
41
+
42
+ included do
43
+ after_find do |object|
44
+ # Rails.logger.debug "after_find #{object}"
45
+ ContextualizedModel.contextualize(object, self.class.contextualizable_keys, self.class.current_context)
46
+ end
47
+
48
+ after_create do
49
+ # Rails.logger.debug "after_create #{self}"
50
+ ContextualizedModel.contextualize(self, self.class.contextualizable_keys, self.class.current_context)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ require 'active_support'
2
+
3
+ module ContextualizedLogs
4
+ module ContextualizedWorker
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_CURRENT_CONTEXT = ContextualizedLogs::CurrentContext
8
+ DEFAULT_CONTEXTUALIZED_WORKER_ENABLED = false
9
+ DEFAULT_CONTEXTUALIZED_MODEL_ENABLED = false
10
+
11
+ class_methods do
12
+ # contextualize_args
13
+
14
+ def current_context
15
+ @current_context || DEFAULT_CURRENT_CONTEXT
16
+ end
17
+
18
+ def contextualized_worker_enabled
19
+ @contextualized_worker_enabled || DEFAULT_CONTEXTUALIZED_WORKER_ENABLED
20
+ end
21
+
22
+ def contextualized_model_enabled
23
+ @contextualized_model_enabled || DEFAULT_CONTEXTUALIZED_MODEL_ENABLED
24
+ end
25
+
26
+ private
27
+
28
+ def set_current_context(current_context)
29
+ @current_context = current_context
30
+ end
31
+
32
+ def contextualized_worker(enabled)
33
+ @contextualized_worker_enabled = enabled
34
+ end
35
+
36
+ def contextualized_model(enabled)
37
+ @contextualized_model_enabled = enabled
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,99 @@
1
+ # https://github.com/rails/rails/pull/29180
2
+ # storing global request info
3
+ require 'active_support'
4
+
5
+ module ContextualizedLogs
6
+ class CurrentContext < ActiveSupport::CurrentAttributes
7
+ # ⚠️ do not use this class to store any controller specific info..
8
+
9
+ attribute \
10
+ :request_uuid, :request_user_agent, :request_origin, :request_referer, :request_xhr, # request
11
+ :current_job_id, :enqueued_jobs_ids, :worker, :worker_args, # sidekiq
12
+ :request_remote_ip, :request_ip, :request_remote_addr, :request_x_forwarded_for, # ips
13
+ :errors,
14
+ :contextualized_model_enabled, # enable model context values
15
+ :context_values, :context_values_count, # context values
16
+ :resource_name # controller_action to correlate APM metrics
17
+
18
+ MAX_CONTEXT_VALUES = 100
19
+
20
+ def self.context
21
+ # https://docs.hq.com/logs/processing/attributes_naming_convention/#source-code
22
+
23
+ data = {}
24
+
25
+ data[:resource_name] = resource_name unless resource_name.nil?
26
+
27
+ # normalized
28
+ data[:http] = {}
29
+ data[:http][:referer] = request_referer unless request_referer.nil?
30
+ data[:http][:request_id] = request_uuid unless request_uuid.nil?
31
+ data[:http][:useragent] = request_user_agent unless request_user_agent.nil?
32
+ data[:http][:origin] = request_origin unless request_origin.nil?
33
+ data.delete(:http) if data[:http].empty?
34
+
35
+ # normalized
36
+ data[:network] = { client: {} }
37
+ data[:network][:client][:ip] = request_ip unless request_ip.nil?
38
+ data[:network][:client][:remote_addr] = request_remote_addr unless request_remote_addr.nil?
39
+ data[:network][:client][:remote_ip] = request_remote_ip unless request_remote_ip.nil?
40
+ data[:network][:client][:x_forwarded_for] = request_x_forwarded_for unless request_x_forwarded_for.nil?
41
+ data.delete(:network) if data[:network][:client].empty?
42
+
43
+ # eventual error response
44
+ # normalized
45
+ data[:errors] = errors unless errors.nil?
46
+
47
+ # context_values
48
+ unless context_values.nil?
49
+ if context_values.is_a?(Hash) && !context_values.empty?
50
+ data[:context_values] = {}
51
+ context_values.each { |k, v| data[:context_values][k.to_sym] = v }
52
+ end
53
+ end
54
+
55
+ unless current_job_id.nil? && worker.nil?
56
+ data[:job] = { id: current_job_id, worker: worker }
57
+ data[:job][:args] = worker_args if worker_args
58
+ end
59
+
60
+ data
61
+ end
62
+
63
+ def self.to_json
64
+ attributes.to_json
65
+ end
66
+
67
+ def self.add_context(key, value)
68
+ self.context_values_count ||= 0
69
+ self.context_values_count += 1
70
+ if self.context_values_count >= MAX_CONTEXT_VALUES
71
+ Rails.logger.warn('high number of context values') if self.context_values_count == MAX_CONTEXT_VALUES
72
+ return
73
+
74
+ end
75
+ self.context_values ||= {}
76
+ self.context_values[key] ||= []
77
+ unless self.context_values[key].include?(value)
78
+ self.context_values[key] << value
79
+ end
80
+ end
81
+
82
+ def self.from_json(json)
83
+ return unless json
84
+
85
+ begin
86
+ values = JSON.parse(json).deep_symbolize_keys
87
+ values.each { |k, v| send("#{k}=", v) }
88
+ rescue
89
+ end
90
+ end
91
+
92
+ def self.add_error(error)
93
+ return if error.nil?
94
+
95
+ self.errors ||= []
96
+ self.errors << error
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,9 @@
1
+
2
+ require "rails/railtie"
3
+ require 'rails'
4
+
5
+ module ContextualizedLogs
6
+ class Railtie < ::Rails::Railtie
7
+ railtie_name :contextualized_logs
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ module ContextualizedLogs
2
+ # https://github.com/mperham/sidekiq/wiki/Middleware
3
+ module Sidekiq
4
+ module Middleware
5
+ module Client
6
+ class InjectCurrentContext
7
+ # @param [String, Class] worker_class the string or class of the worker class being enqueued
8
+ # @param [Hash] job the full job payload
9
+ # * @see https://github.com/mperham/sidekiq/wiki/Job-Format
10
+ # @param [String] queue the name of the queue the job was pulled from
11
+ # @param [ConnectionPool] redis_pool the redis pool
12
+ # @return [Hash, FalseClass, nil] if false or nil is returned,
13
+ # the job is not to be enqueued into redis, otherwise the block's
14
+ # return value is returned
15
+ # @yield the next middleware in the chain or the enqueuing of the job
16
+ def call(worker_class, job, queue, redis_pool)
17
+ # https://github.com/rails/rails/issues/37526
18
+ # current attribute should be clear between jobs
19
+ # no need to `Current.reset`
20
+ worker_klass = worker_class.is_a?(String) ? worker_class.constantize : worker_class
21
+ if worker_klass.include?(ContextualizedWorker)
22
+ current_context = worker_klass.current_context
23
+ current_context.enqueued_jobs_ids ||= []
24
+ current_context.enqueued_jobs_ids << job['jid']
25
+ current_context.contextualized_model_enabled = worker_klass.contextualized_model_enabled
26
+ if worker_klass.contextualized_worker_enabled
27
+ job['context'] = current_context.to_json
28
+ Rails.logger.info "sidekiq: enqueing job #{worker_class}: #{job['jid']}, on queue: #{queue}"
29
+ Rails.logger.dump('Injecting context', JSON.parse(current_context.to_json), :debug)
30
+ end
31
+ end
32
+ yield
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module ContextualizedLogs
2
+ # https://github.com/mperham/sidekiq/wiki/Middleware
3
+ module Sidekiq
4
+ module Middleware
5
+ module Server
6
+ # https://github.com/mperham/sidekiq/wiki/Middleware
7
+ class RestoreCurrentContext
8
+ # @param [Object] worker the worker instance
9
+ # @param [Hash] job the full job payload
10
+ # * @see https://github.com/mperham/sidekiq/wiki/Job-Format
11
+ # @param [String] queue the name of the queue the job was pulled from
12
+ # @yield the next middleware in the chain or worker `perform` method
13
+ # @return [Void]
14
+ def call(worker, job, queue)
15
+ worker_klass = worker.class
16
+ if worker_klass.include?(ContextualizedWorker)
17
+ job_context_json = job['context']
18
+ current_context = worker_klass.current_context
19
+ current_context.from_json(job_context_json) if job_context_json
20
+ current_context.current_job_id = job['jid']
21
+ current_context.worker = worker.class.to_s
22
+ # https://github.com/mperham/sidekiq/wiki/Job-Format
23
+ current_context.worker_args = worker_klass.contextualize_args(job['args']) if worker_klass.respond_to?(:contextualize_args) && job['args']
24
+ current_context.contextualized_model_enabled = worker_klass.contextualized_model_enabled
25
+ if worker_klass.contextualized_worker_enabled
26
+ Rails.logger.info "sidekiq: performing job #{worker_klass}: #{job['jid']}, on queue #{queue}"
27
+ yield
28
+ Rails.logger.info "sidekiq: completing job #{worker_klass}: #{job['jid']}, on queue #{queue}"
29
+ end
30
+ else
31
+ yield
32
+ end
33
+ rescue StandardError => e
34
+ if worker_klass.include?(ContextualizedWorker)
35
+ Rails.logger.error "sidekiq: failure job #{worker.class}: #{job['jid']}, on queue #{queue}: #{e}"
36
+ end
37
+ raise e
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module ContextualizedLogs
2
+ VERSION = '0.0.1-alpha'
3
+ end
@@ -0,0 +1,11 @@
1
+ require "contextualized_logs/contextualized_logger"
2
+ require "contextualized_logs/contextualized_controller"
3
+ require "contextualized_logs/contextualized_model"
4
+ require "contextualized_logs/current_context"
5
+ require "contextualized_logs/sidekiq/middleware/client/inject_current_context"
6
+ require "contextualized_logs/sidekiq/middleware/server/restore_current_context"
7
+ require "contextualized_logs/contextualized_worker"
8
+
9
+ module ContextualizedLogs
10
+ require "contextualized_logs/railtie" if defined?(Rails)
11
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :contextualized_logs do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: contextualized_logs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Hugues Bernet-Rollande
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Online logging solution (like [Datadog](https://www.datadoghq.com)) have drastically transform the way we log.
15
+
16
+ An app will nowdays logs dozen (hundred) of logs per requests.
17
+
18
+ The issue is often to correlate this logs, with the initiating request (or job) and add shared metadata on this logs.
19
+
20
+ Here come `ContextualizedLogs`.
21
+
22
+ The main idea is to enhance your logs from your controller (including `ContextualizedController`, which use a before action), which will add the params to your logs (and some metadata about the request itself, like `request.uuid`).
23
+
24
+ This metadata are stored in a `ActiveSupport::CurrentAttributes` which is a singleton (reset per request).
25
+
26
+ Each subsequent logs in this thread (request) will also be enriched with this metadata, making it easier to find all the logs associated with a request (`uuid`, `ip`, `params.xxx`).
27
+
28
+ On top of this, logs can also be enriched by the ActiveRecord model they use (`create` or `find`) (models including `ContextualizedModel`). So any time a contextualized model is created or find, some metadata related to the model (`id`, ...) will also be added to the logs.
29
+
30
+ Allowing you to find all logs which "touched" this models.
31
+ email:
32
+ - hugues@xdev.fr
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - MIT-LICENSE
38
+ - README.md
39
+ - Rakefile
40
+ - app/assets/config/manifest.js
41
+ - app/assets/javascripts/application.js
42
+ - app/assets/javascripts/cable.js
43
+ - app/assets/stylesheets/application.css
44
+ - app/channels/application_cable/channel.rb
45
+ - app/channels/application_cable/connection.rb
46
+ - app/controllers/application_controller.rb
47
+ - app/controllers/model_controller.rb
48
+ - app/helpers/application_helper.rb
49
+ - app/jobs/application_job.rb
50
+ - app/mailers/application_mailer.rb
51
+ - app/models/application_record.rb
52
+ - app/models/model.rb
53
+ - app/views/layouts/application.html.erb
54
+ - app/views/layouts/mailer.html.erb
55
+ - app/views/layouts/mailer.text.erb
56
+ - app/workers/model_worker.rb
57
+ - config/application.rb
58
+ - config/boot.rb
59
+ - config/cable.yml
60
+ - config/credentials.yml.enc
61
+ - config/database.yml
62
+ - config/environment.rb
63
+ - config/environments/development.rb
64
+ - config/environments/production.rb
65
+ - config/environments/test.rb
66
+ - config/initializers/application_controller_renderer.rb
67
+ - config/initializers/assets.rb
68
+ - config/initializers/backtrace_silencers.rb
69
+ - config/initializers/content_security_policy.rb
70
+ - config/initializers/cookies_serializer.rb
71
+ - config/initializers/filter_parameter_logging.rb
72
+ - config/initializers/inflections.rb
73
+ - config/initializers/mime_types.rb
74
+ - config/initializers/sidekiq.rb
75
+ - config/initializers/wrap_parameters.rb
76
+ - config/locales/en.yml
77
+ - config/master.key
78
+ - config/puma.rb
79
+ - config/routes.rb
80
+ - config/spring.rb
81
+ - config/storage.yml
82
+ - db/development.sqlite3
83
+ - db/migrate/20200424081113_create_model.rb
84
+ - db/schema.rb
85
+ - db/seeds.rb
86
+ - db/test.sqlite3
87
+ - lib/contextualized_logs.rb
88
+ - lib/contextualized_logs/contextualized_controller.rb
89
+ - lib/contextualized_logs/contextualized_logger.rb
90
+ - lib/contextualized_logs/contextualized_model.rb
91
+ - lib/contextualized_logs/contextualized_worker.rb
92
+ - lib/contextualized_logs/current_context.rb
93
+ - lib/contextualized_logs/railtie.rb
94
+ - lib/contextualized_logs/sidekiq/middleware/client/inject_current_context.rb
95
+ - lib/contextualized_logs/sidekiq/middleware/server/restore_current_context.rb
96
+ - lib/contextualized_logs/version.rb
97
+ - lib/tasks/contextualized_logs_tasks.rake
98
+ homepage: https://github.com/hugues/contextualized_logs
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">"
114
+ - !ruby/object:Gem::Version
115
+ version: 1.3.1
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.7.7
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Contextualize your logs (requests params, found/created model metadata, workers,
122
+ ...)
123
+ test_files: []