contextualized_logs 0.0.1.pre.alpha → 0.0.2.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.
@@ -1,2 +1,3 @@
1
1
  class ApplicationController < ActionController::API
2
+ include DatadogSetTraceDetails # inject user_id in datadog APM trace
2
3
  end
@@ -0,0 +1,36 @@
1
+ module DatadogSetTraceDetails
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_trace_tags, unless: -> { Rails.env.development? }
6
+ end
7
+
8
+ def set_trace_tags
9
+ begin
10
+ # set log context as (Datadog) APM trace tags
11
+ tracer = Datadog.configuration[:rails][:tracer]
12
+ span = tracer.active_span
13
+ dotted_hash(ContextualizedLogger.config.current_context).each do |k, v|
14
+ span.set_tag(k, v)
15
+ end
16
+ rescue StandardError => e
17
+ Rails.logger.info "Error setting trace tags #{e}"
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # {http: {uuid: 123}} => {http.uuid: 123}
24
+ # could be added to Hash
25
+ def dotted_hash(hash, recursive_key = "")
26
+ hash.each_with_object({}) do |(k, v), ret|
27
+ key = recursive_key + k.to_s
28
+ if v.is_a? Hash
29
+ ret.merge! dotted_hash(v, key + ".")
30
+ else
31
+ ret[key] = v
32
+ end
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,17 @@
1
+ class CustomContextController < ApplicationController
2
+ include ContextualizedLogs::ContextualizedController
3
+
4
+ def contextualize_request(controller)
5
+ super(controller)
6
+
7
+ ContextualizedLogs::ContextualizedController.current_context.custom_attributes = {
8
+ http: {
9
+ service: 'rails'
10
+ }
11
+ }
12
+ end
13
+
14
+ def show
15
+ render json: {}, status: 200
16
+ end
17
+ end
@@ -1,6 +1,6 @@
1
1
  class ModelController < ApplicationController
2
2
  include ContextualizedLogs::ContextualizedController
3
- contextualized_model true
3
+ contextualize_model true
4
4
 
5
5
  def index
6
6
  models = Model.all.map { |model| { id: model.id, value: model.value} }
data/app/models/model.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Model < ActiveRecord::Base
2
2
  include ContextualizedLogs::ContextualizedModel
3
- contextualizable keys: { values: :value }
3
+ contextualizable keys: { model_ids: :id, model_values: :value }
4
4
  end
@@ -1,8 +1,8 @@
1
1
  class ModelWorker
2
2
  include Sidekiq::Worker
3
3
  include ContextualizedLogs::ContextualizedWorker
4
- contextualized_worker true
5
- contextualized_model true
4
+ contextualize_worker true
5
+ contextualize_model true
6
6
  def self.contextualize_args(args)
7
7
  { model_id: args.first, action: args.last }
8
8
  end
@@ -0,0 +1,23 @@
1
+ require 'contextualized_logs'
2
+ require 'json'
3
+
4
+ module ContextualizedLogs
5
+ configure do |config|
6
+ config.log_formatter = proc do |severity, timestamp, progname, msg|
7
+ log = ContextualizedLogger.default_formatter.call(severity, timestamp, progname, msg)
8
+ log = JSON.parse(log)
9
+ # set log <> APM trace correlation
10
+ datadog_correlation = Datadog.tracer.active_correlation
11
+ log.merge(
12
+ dd: {
13
+ trace_id: datadog_correlation.trace_id,
14
+ span_id: datadog_correlation.span_id
15
+ },
16
+ ddsource: ['ruby']
17
+ ).to_json + "\n"
18
+ end
19
+ config.controller_default_contextualizer = proc do |controller|
20
+ ContextualizedController.default_contextualize_request(controller)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ Datadog.configure do |c|
2
+ service_name = "#{Rails.env}-rails-app"
3
+ # https://docs.datadoghq.com/tracing/trace_search_and_analytics/?tab=ruby#automatic-configuration
4
+ # c.analytics_enabled = true
5
+
6
+ # add runtime metrics (runtime.ruby.class_count, runtime.ruby.thread_count, runtime.ruby.gc.*)
7
+ # http://gems.datadoghq.com/trace/docs/#Processing_Pipeline
8
+ # c.runtime_metrics_enabled = true
9
+ #c.tracer debug: true, hostname: '127.0.0.1', log: Logger.new(File.new('log/datadog.log', 'w+'))
10
+ c.tracer hostname: '127.0.0.1'
11
+
12
+ # c.use :rack, service_name: "#{service_name}-rack"
13
+ c.use :rails, service_name: service_name, database_service: "#{service_name}-active_record", analytics_enabled: false
14
+ # c.use :rake, service_name: "#{service_name}-rake"
15
+ # c.use :redis, service_name: "#{service_name}-redis"
16
+ # c.use :sidekiq, client_service_name: "#{service_name}-sidekiq-client", service_name: "#{service_name}-sidekiq-worker"
17
+ end
@@ -8,16 +8,16 @@ require 'sidekiq'
8
8
  # require 'sidekiq/throttled'
9
9
  # Sidekiq::Throttled.setup!
10
10
 
11
- Sidekiq.configure_server do |config|
12
- config.redis = { url: 'redis://127.0.0.1:6379/1' }
13
- config.server_middleware do |chain|
14
- chain.add ContextualizedLogs::Sidekiq::Middleware::Server::RestoreCurrentContext
15
- end
16
- end
17
-
18
- Sidekiq.configure_client do |config|
19
- config.redis = { url: 'redis://127.0.0.1:6379/1' }
20
- config.client_middleware do |chain|
21
- chain.add ContextualizedLogs::Sidekiq::Middleware::Client::InjectCurrentContext
22
- end
23
- end
11
+ # Sidekiq.configure_server do |config|
12
+ # config.redis = { url: 'redis://127.0.0.1:6379/1' }
13
+ # config.server_middleware do |chain|
14
+ # chain.add ContextualizedLogs::Sidekiq::Middleware::Server::RestoreCurrentContext
15
+ # end
16
+ # end
17
+ #
18
+ # Sidekiq.configure_client do |config|
19
+ # config.redis = { url: 'redis://127.0.0.1:6379/1' }
20
+ # config.client_middleware do |chain|
21
+ # chain.add ContextualizedLogs::Sidekiq::Middleware::Client::InjectCurrentContext
22
+ # end
23
+ # end
data/config/routes.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  Rails.application.routes.draw do
2
2
  resources :model
3
+ get '/custom_context' => 'custom_context#show'
3
4
  end
Binary file
@@ -5,7 +5,32 @@ require "contextualized_logs/current_context"
5
5
  require "contextualized_logs/sidekiq/middleware/client/inject_current_context"
6
6
  require "contextualized_logs/sidekiq/middleware/server/restore_current_context"
7
7
  require "contextualized_logs/contextualized_worker"
8
+ require "contextualized_logs/config"
8
9
 
9
10
  module ContextualizedLogs
10
11
  require "contextualized_logs/railtie" if defined?(Rails)
12
+
13
+ class << self
14
+ attr_accessor :config
15
+
16
+ def config
17
+ @config || Config.default
18
+ end
19
+
20
+ def configure(&block)
21
+ config = Config.default
22
+ block.call(config)
23
+ self.config = config
24
+ ContextualizedLogger.formatter = config.log_formatter
25
+ ContextualizedController.default_contextualizer = config.controller_default_contextualizer
26
+ ContextualizedController.default_contextualize_model = config.controller_default_contextualize_model
27
+ ContextualizedWorker.default_contextualize_model = config.worker_default_contextualize_model
28
+ ContextualizedWorker.default_contextualize_worker = config.worker_default_contextualize_worker
29
+ if defined?(Rails) && Rails.logger.is_a?(ContextualizedLogger)
30
+ Rails.logger.formatter = config.log_formatter
31
+ end
32
+ end
33
+ end
11
34
  end
35
+
36
+ ContextualizedLogs.configure { |config| } # set default configuration
@@ -0,0 +1,27 @@
1
+
2
+ module ContextualizedLogs
3
+ class Config
4
+ DEFAULT_CURRENT_CONTEXT = CurrentContext
5
+ DEFAULT_CONTROLLER_CONTEXTUALIZE_MODEL = false
6
+ DEFAULT_WORKER_CONTEXTUALIZE_MODEL = false
7
+ DEFAULT_WORKER_CONTEXTUALIZE_WORKER = true
8
+
9
+ attr_accessor :log_formatter
10
+ attr_accessor :current_context
11
+ attr_accessor :controller_default_contextualizer
12
+ attr_accessor :controller_default_contextualize_model
13
+ attr_accessor :worker_default_contextualize_worker
14
+ attr_accessor :worker_default_contextualize_model
15
+
16
+ class << self
17
+ def default
18
+ config = new
19
+ config.current_context = DEFAULT_CURRENT_CONTEXT
20
+ config.controller_default_contextualize_model = DEFAULT_CONTROLLER_CONTEXTUALIZE_MODEL
21
+ config.worker_default_contextualize_worker = DEFAULT_WORKER_CONTEXTUALIZE_MODEL
22
+ config.worker_default_contextualize_model = DEFAULT_WORKER_CONTEXTUALIZE_WORKER
23
+ config
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,62 +1,76 @@
1
1
  # https://github.com/rails/rails/pull/29180
2
2
  require 'active_support'
3
- require_relative 'current_context'
3
+ require 'action_controller'
4
4
 
5
5
  module ContextualizedLogs
6
6
  module ContextualizedController
7
7
  extend ActiveSupport::Concern
8
8
 
9
- DEFAULT_CONTEXTUALIZED_MODEL_ENABLED = false
10
- DEFAULT_CURRENT_CONTEXT = ContextualizedLogs::CurrentContext
9
+ class << self
10
+ attr_accessor :default_contextualizer
11
+ attr_writer :default_contextualize_model
12
+ attr_writer :current_context
11
13
 
12
- included do
13
- before_action :contextualize_requests
14
- end
14
+ def current_context
15
+ @current_context || ContextualizedLogs.config.current_context
16
+ end
15
17
 
16
- def contextualize_requests
17
- # Rails.logger.debug "contextualize_requests"
18
- # store request && user info in CurrentContext ActiveSupport attribute
19
- # which can then be read from anywhere
20
- self.class.current_context.contextualized_model_enabled = self.class.contextualized_model_enabled?
21
- begin
22
- self.class.current_context.resource_name = "#{self.class.name.downcase}_#{action_name.downcase}" rescue nil
23
- self.class.current_context.request_uuid = request.uuid
24
- self.class.current_context.request_origin = request.origin
25
- self.class.current_context.request_user_agent = request.user_agent
26
- self.class.current_context.request_referer = request.referer&.to_s
27
- self.class.current_context.request_ip = request.ip
28
- self.class.current_context.request_remote_ip = request.remote_ip
29
- self.class.current_context.request_remote_addr = request.remote_addr
30
- self.class.current_context.request_x_forwarded_for = request.x_forwarded_for
31
- self.class.current_context.request_xhr = request.xhr? ? 'true' : 'false'
32
- rescue StandardError => e
33
- Rails.logger.dump_error('error setting context', e)
34
- end
35
- end
18
+ def default_contextualize_model
19
+ @default_contextualize_model || ContextualizedLogs.config.controller_default_contextualize_model
20
+ end
21
+
22
+ def included(base)
23
+ if !base.ancestors.include?(ActionController::Base) && !base.ancestors.include?(ActionController::API)
24
+ raise ArgumentError, "ContextualizedLogs::ContextualizedController can only be included in a ActionController::Base or ActionController::API"
25
+ end
26
+
27
+ base.class_eval do
28
+ before_action do |controller|
29
+ contextualize_request(controller)
30
+ end
31
+ end
36
32
 
37
- def self.included(base)
38
- if !base.ancestors.include?(ActionController::Base) && !base.ancestors.include?(ActionController::API)
39
- raise ArgumentError, "ContextualizedLogs::ContextualizedController can only be included in a ActionController::Base or ActionController::API"
33
+ base.extend(ClassMethods)
40
34
  end
41
35
 
42
- base.extend(ClassMethods)
36
+ def default_contextualize_request(controller)
37
+ # Rails.logger.debug "contextualize_request"
38
+ # store request && user info in CurrentContext ActiveSupport attribute
39
+ # which can then be read from anywhere
40
+ ContextualizedController.current_context.contextualize_model_enabled = controller.class.contextualize_model_enabled?
41
+ ContextualizedController.current_context.resource_name = "#{controller.class.name.downcase}_#{controller.action_name.downcase}" rescue nil
42
+ ContextualizedController.current_context.request_uuid = controller.request.uuid
43
+ ContextualizedController.current_context.request_origin = controller.request.origin
44
+ ContextualizedController.current_context.request_user_agent = controller.request.user_agent
45
+ ContextualizedController.current_context.request_referer = controller.request.referer&.to_s
46
+ ContextualizedController.current_context.request_ip = controller.request.ip
47
+ ContextualizedController.current_context.request_remote_ip = controller.request.remote_ip
48
+ ContextualizedController.current_context.request_remote_addr = controller.request.remote_addr
49
+ ContextualizedController.current_context.request_x_forwarded_for = controller.request.x_forwarded_for
50
+ ContextualizedController.current_context.request_xhr = controller.request.xhr? ? 'true' : 'false'
51
+ end
43
52
  end
44
53
 
45
- module ClassMethods
46
- def contextualized_model_enabled?
47
- @contextualized_model_enabled || DEFAULT_CONTEXTUALIZED_MODEL_ENABLED
54
+ def contextualize_request(controller)
55
+ if ContextualizedController.default_contextualizer
56
+ ContextualizedController.default_contextualizer.call(controller)
57
+ return
48
58
  end
49
59
 
50
- def contextualized_model(enable)
51
- @contextualized_model_enabled = enable
52
- end
60
+ ContextualizedController.default_contextualize_request(controller)
61
+ rescue StandardError => e
62
+ Rails.logger.dump_error('error setting controller context', e)
63
+ end
53
64
 
54
- def current_context
55
- @current_context || DEFAULT_CURRENT_CONTEXT
65
+ module ClassMethods
66
+ def contextualize_model_enabled?
67
+ return @contextualize_model_enabled if defined?(@contextualize_model_enabled)
68
+
69
+ ContextualizedController.default_contextualize_model
56
70
  end
57
71
 
58
- def set_current_context(current_context)
59
- @current_context = current_context
72
+ def contextualize_model(enable)
73
+ @contextualize_model_enabled = enable
60
74
  end
61
75
  end
62
76
  end
@@ -6,9 +6,82 @@ module ContextualizedLogs
6
6
  # support Rails.logger.dump('msg', hash)
7
7
 
8
8
  class ContextualizedLogger < ActiveSupport::Logger
9
+ class << self
10
+ attr_accessor :formatter
11
+ attr_writer :current_context
12
+
13
+ def current_context
14
+ @current_context || ContextualizedLogs.config.current_context
15
+ end
16
+
17
+ def default_formatter
18
+ proc do |severity, timestamp, progname, msg|
19
+ # format (and enrich) log in JSON format (-> )
20
+ # https://docs.hq.com/logs/processing/attributes_naming_convention/#source-code
21
+ # correlation = Datadog.tracer.active_correlation
22
+ data = {
23
+ # dd: {
24
+ # trace_id: correlation.trace_id,
25
+ # span_id: correlation.span_id
26
+ # },
27
+ # ddsource: ['ruby'],
28
+ syslog: { env: Rails.env, host: Socket.gethostname },
29
+ type: severity.to_s,
30
+ time: timestamp
31
+ }
32
+ data[:stack] = Kernel.caller.
33
+ map { |caller| caller.gsub(/#{Rails.root}/, '') }.
34
+ reject { |caller| caller.start_with?('/usr/local') || caller.include?('/shared/bundle/') || caller.start_with?('/Users/') }.
35
+ first(15)
36
+ data[:log_type] = 'log'
37
+ data.merge!(parse_msg(msg)) # parse message (string, hash, error, ...)
38
+ data.merge!(current_context.context) # merge current request context
39
+ data.to_json + "\n"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def parse_msg(msg)
46
+ data = {}
47
+ case msg
48
+ when Hash
49
+ # used by logger.dump(msg|error, attributes = {})
50
+ if msg.include?(:attributes)
51
+ # adding message as log attributes if is a hash
52
+ data.merge!(parse_error(msg[:msg]))
53
+ data[:attributes] = msg[:attributes]
54
+ else
55
+ data.merge!(parse_error(msg))
56
+ end
57
+ else
58
+ data.merge!(parse_error(msg))
59
+ end
60
+ data
61
+ end
62
+
63
+ def parse_error(msg)
64
+ data = {}
65
+ case msg
66
+ when ::Exception
67
+ # format data to be interpreted as an error logs by
68
+ data[:error] = {
69
+ kind: msg.class.to_s,
70
+ message: msg.message,
71
+ stack: (msg.backtrace || []).join('; ')
72
+ }
73
+ else
74
+ data[:message] = msg.to_s
75
+ end
76
+ data
77
+ end
78
+ end
79
+
80
+ attr_accessor :formatter
81
+
9
82
  def initialize(*args)
10
83
  super(*args)
11
- @formatter = formatter
84
+ @formatter = self.class.formatter || self.class.default_formatter
12
85
  end
13
86
 
14
87
  def dump(msg, attributes, severity = :info)
@@ -19,72 +92,6 @@ module ContextualizedLogs
19
92
  def dump_error(msg, attributes)
20
93
  dump(msg, attributes, :error)
21
94
  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
95
  end
89
96
 
90
97
  end