timber 1.0.3

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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +6 -0
  5. data/Appraisals +41 -0
  6. data/Gemfile +30 -0
  7. data/LICENSE.md +15 -0
  8. data/README.md +194 -0
  9. data/circle.yml +33 -0
  10. data/lib/timber/config.rb +18 -0
  11. data/lib/timber/context.rb +17 -0
  12. data/lib/timber/contexts/custom.rb +27 -0
  13. data/lib/timber/contexts/http.rb +28 -0
  14. data/lib/timber/contexts/organization.rb +35 -0
  15. data/lib/timber/contexts/user.rb +36 -0
  16. data/lib/timber/contexts.rb +10 -0
  17. data/lib/timber/current_context.rb +43 -0
  18. data/lib/timber/event.rb +17 -0
  19. data/lib/timber/events/controller_call.rb +40 -0
  20. data/lib/timber/events/custom.rb +42 -0
  21. data/lib/timber/events/exception.rb +35 -0
  22. data/lib/timber/events/http_request.rb +50 -0
  23. data/lib/timber/events/http_response.rb +36 -0
  24. data/lib/timber/events/sql_query.rb +26 -0
  25. data/lib/timber/events/template_render.rb +26 -0
  26. data/lib/timber/events.rb +37 -0
  27. data/lib/timber/frameworks/rails.rb +13 -0
  28. data/lib/timber/frameworks.rb +19 -0
  29. data/lib/timber/log_devices/http.rb +87 -0
  30. data/lib/timber/log_devices.rb +8 -0
  31. data/lib/timber/log_entry.rb +59 -0
  32. data/lib/timber/logger.rb +142 -0
  33. data/lib/timber/probe.rb +23 -0
  34. data/lib/timber/probes/action_controller_log_subscriber/log_subscriber.rb +64 -0
  35. data/lib/timber/probes/action_controller_log_subscriber.rb +20 -0
  36. data/lib/timber/probes/action_dispatch_debug_exceptions.rb +80 -0
  37. data/lib/timber/probes/action_view_log_subscriber/log_subscriber.rb +62 -0
  38. data/lib/timber/probes/action_view_log_subscriber.rb +20 -0
  39. data/lib/timber/probes/active_record_log_subscriber/log_subscriber.rb +29 -0
  40. data/lib/timber/probes/active_record_log_subscriber.rb +20 -0
  41. data/lib/timber/probes/rack_http_context.rb +51 -0
  42. data/lib/timber/probes/rails_rack_logger.rb +76 -0
  43. data/lib/timber/probes.rb +21 -0
  44. data/lib/timber/util/active_support_log_subscriber.rb +33 -0
  45. data/lib/timber/util/hash.rb +14 -0
  46. data/lib/timber/util.rb +8 -0
  47. data/lib/timber/version.rb +3 -0
  48. data/lib/timber.rb +22 -0
  49. data/spec/spec_helper.rb +30 -0
  50. data/spec/support/action_controller.rb +4 -0
  51. data/spec/support/action_view.rb +4 -0
  52. data/spec/support/active_record.rb +28 -0
  53. data/spec/support/coveralls.rb +2 -0
  54. data/spec/support/rails/templates/_partial.html +1 -0
  55. data/spec/support/rails/templates/template.html +1 -0
  56. data/spec/support/rails.rb +37 -0
  57. data/spec/support/simplecov.rb +9 -0
  58. data/spec/support/socket_hostname.rb +12 -0
  59. data/spec/support/timber.rb +4 -0
  60. data/spec/support/timecop.rb +3 -0
  61. data/spec/support/webmock.rb +2 -0
  62. data/spec/timber/events_spec.rb +55 -0
  63. data/spec/timber/log_devices/http_spec.rb +62 -0
  64. data/spec/timber/logger_spec.rb +89 -0
  65. data/spec/timber/probes/action_controller_log_subscriber_spec.rb +70 -0
  66. data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +51 -0
  67. data/spec/timber/probes/action_view_log_subscriber_spec.rb +61 -0
  68. data/spec/timber/probes/active_record_log_subscriber_spec.rb +52 -0
  69. data/spec/timber/probes/rack_http_context_spec.rb +54 -0
  70. data/spec/timber/probes/rails_rack_logger_spec.rb +46 -0
  71. data/timber.gemspec +22 -0
  72. metadata +149 -0
@@ -0,0 +1,35 @@
1
+ module Timber
2
+ module Events
3
+ # The exception event is used to track exceptions.
4
+ #
5
+ # @note This event should be installed automatically through probes,
6
+ # such as the {Probes::ActionDispatchDebugExceptions} probe.
7
+ class Exception < Timber::Event
8
+ attr_reader :name, :exception_message, :backtrace
9
+
10
+ def initialize(attributes)
11
+ @name = attributes[:name] || raise(ArgumentError.new(":name is required"))
12
+ @exception_message = attributes[:exception_message] || raise(ArgumentError.new(":exception_message is required"))
13
+ @backtrace = attributes[:backtrace]
14
+ end
15
+
16
+ def to_hash
17
+ {name: name, message: exception_message, backtrace: backtrace}
18
+ end
19
+ alias to_h to_hash
20
+
21
+ def as_json(_options = {})
22
+ {:exception => to_hash}
23
+ end
24
+
25
+ def message
26
+ message = "#{name} (#{exception_message}):"
27
+ if backtrace.is_a?(Array) && backtrace.length > 0
28
+ message << "\n\n"
29
+ message << backtrace.join("\n")
30
+ end
31
+ message
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,50 @@
1
+ module Timber
2
+ module Events
3
+ # The HTTP request event tracks incoming HTTP requests.
4
+ #
5
+ # @note This event should be installed automatically through probes,
6
+ # such as the {Probes::ActionControllerLogSubscriber} probe.
7
+ class HTTPRequest < Timber::Event
8
+ attr_reader :host, :method, :path, :port, :query_params, :content_type,
9
+ :remote_addr, :referrer, :request_id, :user_agent
10
+
11
+ def initialize(attributes)
12
+ @host = attributes[:host] || raise(ArgumentError.new(":host is required"))
13
+ @method = attributes[:method] || raise(ArgumentError.new(":method is required"))
14
+ @path = attributes[:path] || raise(ArgumentError.new(":path is required"))
15
+ @port = attributes[:port]
16
+ @query_params = attributes[:query_params]
17
+ @content_type = attributes[:content_type]
18
+ @remote_addr = attributes[:remote_addr]
19
+ @referrer = attributes[:referrer]
20
+ @request_id = attributes[:request_id]
21
+ @user_agent = attributes[:user_agent]
22
+ end
23
+
24
+ def to_hash
25
+ {host: host, method: method, path: path, port: port, query_params: query_params,
26
+ headers: {content_type: content_type, remote_addr: remote_addr, referrer: referrer,
27
+ request_id: request_id, user_agent: user_agent}}
28
+ end
29
+ alias to_h to_hash
30
+
31
+ def as_json(_options = {})
32
+ hash = to_hash
33
+ hash[:headers] = Util::Hash.compact(hash[:headers])
34
+ hash = Util::Hash.compact(hash)
35
+ {:http_request => hash}
36
+ end
37
+
38
+ def message
39
+ 'Started %s "%s" for %s' % [
40
+ method,
41
+ path,
42
+ remote_addr]
43
+ end
44
+
45
+ def status_description
46
+ Rack::Utils::HTTP_STATUS_CODES[status]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ module Timber
2
+ module Events
3
+ # The HTTP response event tracks outgoing HTTP request responses.
4
+ #
5
+ # @note This event should be installed automatically through probes,
6
+ # such as the {Probes::ActionControllerLogSubscriber} probe.
7
+ class HTTPResponse < Timber::Event
8
+ attr_reader :status, :time_ms, :additions
9
+
10
+ def initialize(attributes)
11
+ @status = attributes[:status] || raise(ArgumentError.new(":status is required"))
12
+ @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
13
+ @additions = attributes[:additions]
14
+ end
15
+
16
+ def to_hash
17
+ {status: status, time_ms: time_ms}
18
+ end
19
+ alias to_h to_hash
20
+
21
+ def as_json(_options = {})
22
+ {:http_response => to_hash}
23
+ end
24
+
25
+ def message
26
+ message = "Completed #{status} #{status_description} in #{time_ms}ms"
27
+ message << " (#{additions.join(" | ".freeze)})" unless additions.empty?
28
+ message
29
+ end
30
+
31
+ def status_description
32
+ Rack::Utils::HTTP_STATUS_CODES[status]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ module Timber
2
+ module Events
3
+ # The SQL query event tracks sql queries to your database.
4
+ #
5
+ # @note This event should be installed automatically through probes,
6
+ # such as the {Probes::ActiveRecordLogSubscriber} probe.
7
+ class SQLQuery < Timber::Event
8
+ attr_reader :sql, :time_ms, :message
9
+
10
+ def initialize(attributes)
11
+ @sql = attributes[:sql] || raise(ArgumentError.new(":sql is required"))
12
+ @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
13
+ @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
14
+ end
15
+
16
+ def to_hash
17
+ {sql: sql, time_ms: time_ms}
18
+ end
19
+ alias to_h to_hash
20
+
21
+ def as_json(_options = {})
22
+ {:sql_query => to_hash}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Timber
2
+ module Events
3
+ # The template render event track template renderings and their performance.
4
+ #
5
+ # @note This event should be installed automatically through probes,
6
+ # such as the {Probes::ActionViewLogSubscriber} probe.
7
+ class TemplateRender < Timber::Event
8
+ attr_reader :message, :name, :time_ms
9
+
10
+ def initialize(attributes)
11
+ @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
12
+ @name = attributes[:name] || raise(ArgumentError.new(":name is required"))
13
+ @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
14
+ end
15
+
16
+ def to_hash
17
+ {name: name, time_ms: time_ms}
18
+ end
19
+ alias to_h to_hash
20
+
21
+ def as_json(_options = {})
22
+ {:template_render => to_hash}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require "timber/events/controller_call"
2
+ require "timber/events/custom"
3
+ require "timber/events/exception"
4
+ require "timber/events/http_request"
5
+ require "timber/events/http_response"
6
+ require "timber/events/sql_query"
7
+ require "timber/events/template_render"
8
+
9
+ module Timber
10
+ module Events
11
+ # Protocol for casting objects into a `Timber::Event`.
12
+ #
13
+ # @example Casting a hash
14
+ # Timber::Events.build({type: :custom_event, message: "My log message", data: {my: "data"}})
15
+ def self.build(obj)
16
+ if obj.is_a?(::Timber::Event)
17
+ obj
18
+ elsif obj.respond_to?(:to_timber_event)
19
+ obj.to_timber_event
20
+ elsif obj.is_a?(Hash) && obj.key?(:message) && obj.key?(:type) && obj.key?(:data)
21
+ Events::Custom.new(
22
+ type: obj[:type],
23
+ message: obj[:message],
24
+ data: obj[:data]
25
+ )
26
+ elsif obj.is_a?(Struct) && obj.respond_to?(:message) && obj.respond_to?(:type)
27
+ Events::Custom.new(
28
+ type: obj.type,
29
+ message: obj.message,
30
+ data: obj.respond_to?(:hash) ? obj.hash : obj.to_h # ruby 1.9.3 does not have to_h
31
+ )
32
+ else
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ module Timber
2
+ module Frameworks
3
+ module Rails
4
+ # Installs Timber into your Rails app automatically.
5
+ class Railtie < ::Rails::Railtie
6
+ config.timber = Config.instance
7
+ config.before_initialize do
8
+ Probes.insert!(config.app_middleware, ::Rails::Rack::Logger)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require "logger"
2
+
3
+ # Attempt to require Rails. We can not list it as a gem
4
+ # dependency because we want to support multiple frameworks.
5
+ begin
6
+ require("rails")
7
+ rescue LoadError
8
+ end
9
+
10
+ if defined?(::Rails) && defined?(::Rails::Railtie)
11
+ require 'timber/frameworks/rails'
12
+ end
13
+
14
+ module Timber
15
+ # Namespace for installing Timber into frameworks
16
+ # @private
17
+ module Frameworks
18
+ end
19
+ end
@@ -0,0 +1,87 @@
1
+ require "monitor"
2
+ require "msgpack"
3
+
4
+ module Timber
5
+ module LogDevices
6
+ # A log device that buffers and sends logs to the Timber API over HTTP in intervals. The buffer
7
+ # uses MessagePack::Buffer, which is fast, efficient with memory, and reduces
8
+ # the payload size sent to Timber.
9
+ class HTTP
10
+ class DeliveryError < StandardError; end
11
+
12
+ API_URI = URI.parse("https://api.timber.io/http_frames")
13
+ CONTENT_TYPE = "application/json".freeze
14
+ CONNECTION_HEADER = "keep-alive".freeze
15
+ USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
16
+
17
+ HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
18
+ https.use_ssl = true
19
+ https.read_timeout = 30
20
+ https.ssl_timeout = 10
21
+ if https.respond_to?(:keep_alive_timeout=)
22
+ https.keep_alive_timeout = 60
23
+ end
24
+ https.open_timeout = 10
25
+ end
26
+
27
+ DEFAULT_DELIVERY_FREQUENCY = 2.freeze
28
+
29
+ # Instantiates a new HTTP log device.
30
+ #
31
+ # @param api_key [String] The API key provided to you after you add your application to
32
+ # [Timber](https://timber.io).
33
+ # @param [Hash] options the options to create a HTTP log device with.
34
+ # @option attributes [Symbol] :frequency_seconds (2) How often the client should
35
+ # attempt to deliver logs to the Timber API. The HTTP client buffers logs between calls.
36
+ def initialize(api_key, options = {})
37
+ @api_key = api_key
38
+ @buffer = []
39
+ @monitor = Monitor.new
40
+ @delivery_thread = Thread.new do
41
+ at_exit { deliver }
42
+ loop do
43
+ sleep options[:frequency_seconds] || DEFAULT_DELIVERY_FREQUENCY
44
+ deliver
45
+ end
46
+ end
47
+ end
48
+
49
+ def write(msg)
50
+ @monitor.synchronize {
51
+ @buffer << msg
52
+ }
53
+ end
54
+
55
+ def close
56
+ @delivery_thread.kill
57
+ end
58
+
59
+ private
60
+ def deliver
61
+ body = @buffer.read
62
+
63
+ request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
64
+ req['Authorization'] = authorization_payload
65
+ req['Connection'] = CONNECTION_HEADER
66
+ req['Content-Type'] = CONTENT_TYPE
67
+ req['User-Agent'] = USER_AGENT
68
+ req.body = body
69
+ end
70
+
71
+ HTTPS.request(request).tap do |res|
72
+ code = res.code.to_i
73
+ if code < 200 || code >= 300
74
+ raise DeliveryError.new("Bad response from Timber API - #{res.code}: #{res.body}")
75
+ end
76
+ Config.instance.logger.debug("Success! #{code}: #{res.body}")
77
+ end
78
+
79
+ @buffer.clear
80
+ end
81
+
82
+ def authorization_payload
83
+ @authorization_payload ||= "Basic #{Base64.strict_encode64(@api_key).chomp}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,8 @@
1
+ require "timber/log_devices/http"
2
+
3
+ module Timber
4
+ # Namespace for all log devices.
5
+ # @private
6
+ module LogDevices
7
+ end
8
+ end
@@ -0,0 +1,59 @@
1
+ module Timber
2
+ # Represents a new log entry into the log. This is an intermediary class between
3
+ # `Logger` and the log device that you set it up with.
4
+ class LogEntry #:nodoc:
5
+ DT_PRECISION = 6.freeze
6
+
7
+ attr_reader :level, :time, :progname, :message, :context, :event
8
+
9
+ # Creates a log entry suitable to be sent to the Timber API.
10
+ # @param severity [Integer] the log level / severity
11
+ # @param time [Time] the exact time the log message was written
12
+ # @param progname [String] the progname scope for the log message
13
+ # @param message [#to_json] structured data representing the log line event, this can
14
+ # be anything that responds to #to_json
15
+ # @return [LogEntry] the resulting LogEntry object
16
+ def initialize(level, time, progname, message, context, event)
17
+ @level = level
18
+ @time = time.utc
19
+ @progname = progname
20
+ @message = message
21
+ @context = context
22
+ @event = event
23
+ end
24
+
25
+ def as_json(options = {})
26
+ options ||= {}
27
+ hash = {level: level, dt: formatted_dt, message: message}
28
+
29
+ if !event.nil?
30
+ hash[:event] = event
31
+ end
32
+
33
+ if !context.nil? && context.length > 0
34
+ hash[:context] = context
35
+ end
36
+
37
+ if options[:only]
38
+ hash.select do |key, _value|
39
+ options[:only].include?(key)
40
+ end
41
+ elsif options[:except]
42
+ hash.select do |key, _value|
43
+ !options[:except].include?(key)
44
+ end
45
+ else
46
+ hash
47
+ end
48
+ end
49
+
50
+ def to_json(options = {})
51
+ as_json(options).to_json
52
+ end
53
+
54
+ private
55
+ def formatted_dt
56
+ @formatted_dt ||= time.iso8601(DT_PRECISION)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,142 @@
1
+ require "logger"
2
+
3
+ module Timber
4
+ # The Timber Logger behaves exactly like `::Logger`, except that it supports a transparent API
5
+ # for logging structured messages. It ensures your log messages are communicated properly
6
+ # with the Timber.io API.
7
+ #
8
+ # To adhere to our no code debt / no lock-in promise, the Timber Logger will *never* deviate
9
+ # from the `::Logger` interface. That is, it will *never* add methods, or alter any
10
+ # method signatures. This ensures Timber can be removed without consequence.
11
+ #
12
+ # @example Basic example (the original ::Logger interface remains untouched):
13
+ # logger.info "Payment rejected for customer #{customer_id}"
14
+ #
15
+ # @example Using a map
16
+ # # The :message, :type, and :data keys are required
17
+ # logger.info message: "Payment rejected", type: :payment_rejected, data: {customer_id: customer_id, amount: 100}
18
+ #
19
+ # @example Using a Struct (a simple, more structured way, to define events)
20
+ # PaymentRejectedEvent = Struct.new(:customer_id, :amount, :reason) do
21
+ # def message; "Payment rejected for #{customer_id}"; end
22
+ # def type; :payment_rejected; end
23
+ # end
24
+ # Logger.info PaymentRejectedEvent.new("abcd1234", 100, "Card expired")
25
+ #
26
+ # @example Using typed Event classes
27
+ # # Event implementation is left to you. Events should be simple classes.
28
+ # # The only requirement is that it responds to #to_timber_event and return the
29
+ # # appropriate Timber::Events::* type.
30
+ # class Event
31
+ # def to_hash
32
+ # hash = {}
33
+ # instance_variables.each { |var| hash[var.to_s.delete("@")] = instance_variable_get(var) }
34
+ # hash
35
+ # end
36
+ # alias to_h to_hash
37
+ #
38
+ # def to_timber_event
39
+ # Timber::Events::Custom.new(type: type, message: message, data: to_hash)
40
+ # end
41
+ #
42
+ # def message; raise NotImplementedError.new; end
43
+ # def type; raise NotImplementedError.new; end
44
+ # end
45
+ #
46
+ # class PaymentRejectedEvent < Event
47
+ # attr_accessor :customer_id, :amount
48
+ # def initialize(customer_id, amount)
49
+ # @customer_id = customer_id
50
+ # @amount = amount
51
+ # end
52
+ # def message; "Payment rejected for customer #{customer_id}"; end
53
+ # def type; :payment_rejected_event; end
54
+ # end
55
+ #
56
+ # Logger.info PymentRejectedEvent.new("abcd1234", 100)
57
+ #
58
+ class Logger < ::Logger
59
+ # @private
60
+ class Formatter
61
+ # Formatters get the formatted level from the logger.
62
+ SEVERITY_MAP = {
63
+ "DEBUG" => :debug,
64
+ "INFO" => :info,
65
+ "WARN" => :warn,
66
+ "ERROR" => :error,
67
+ "FATAL" => :datal,
68
+ "UNKNOWN" => :unknown
69
+ }
70
+
71
+ private
72
+ def build_log_entry(severity, time, progname, msg)
73
+ level = SEVERITY_MAP.fetch(severity)
74
+ context = CurrentContext.instance.snapshot
75
+ event = Events.build(msg)
76
+ if event
77
+ LogEntry.new(level, time, progname, event.message, context, event)
78
+ else
79
+ LogEntry.new(level, time, progname, msg, context, nil)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Structures your log messages into JSON.
85
+ #
86
+ # logger = Timber::Logger.new(STDOUT)
87
+ # logger.formatter = Timber::JSONFormatter.new
88
+ #
89
+ # Example message:
90
+ #
91
+ # {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00","message":"My log message"}
92
+ #
93
+ class JSONFormatter < Formatter
94
+ def call(severity, time, progname, msg)
95
+ # use << for concatenation for performance reasons
96
+ build_log_entry(severity, time, progname, msg).to_json() << "\n"
97
+ end
98
+ end
99
+
100
+ # Structures your log messages into Timber's hybrid format, which makes
101
+ # it easy to read while also appending the appropriate metadata.
102
+ #
103
+ # logger = Timber::Logger.new(STDOUT)
104
+ # logger.formatter = Timber::JSONFormatter.new
105
+ #
106
+ # Example message:
107
+ #
108
+ # My log message @timber.io {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
109
+ #
110
+ class HybridFormatter < Formatter
111
+ METADATA_CALLOUT = "@timber.io".freeze
112
+
113
+ def call(severity, time, progname, msg)
114
+ log_entry = build_log_entry(severity, time, progname, msg)
115
+ metadata = log_entry.to_json(:except => [:message])
116
+ # use << for concatenation for performance reasons
117
+ log_entry.message.gsub("\n", "\\n") << " " << METADATA_CALLOUT << " " << metadata << "\n"
118
+ end
119
+ end
120
+
121
+ # Creates a new Timber::Logger instances. Accepts the same arguments as `::Logger.new`.
122
+ # The only difference is that it default the formatter to {HybridFormatter}. Using
123
+ # a different formatter is easy. For example, if you prefer your logs in JSON.
124
+ #
125
+ # @example Changing your formatter
126
+ # logger = Timber::Logger.new(STDOUT)
127
+ # logger.formatter = Timber::Logger::JSONFormatter.new
128
+ def initialize(*args)
129
+ super(*args)
130
+ self.formatter = HybridFormatter.new
131
+ end
132
+
133
+ # Backwards compatibility with older ActiveSupport::Logger versions
134
+ Logger::Severity.constants.each do |severity|
135
+ class_eval(<<-EOT, __FILE__, __LINE__ + 1)
136
+ def #{severity.downcase}? # def debug?
137
+ Logger::#{severity} >= level # DEBUG >= level
138
+ end # end
139
+ EOT
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,23 @@
1
+ module Timber
2
+ # Base class for `Timber::Probes::*`.
3
+ # @private
4
+ class Probe
5
+ class RequirementNotMetError < StandardError; end
6
+
7
+ class << self
8
+ def insert!(*args)
9
+ new(*args).insert!
10
+ Config.instance.logger.debug("Inserted probe #{name}")
11
+ true
12
+ # RequirementUnsatisfiedError is the only silent failure we support
13
+ rescue RequirementNotMetError => e
14
+ Config.instance.logger.debug("Failed inserting probe #{name}: #{e.message}")
15
+ false
16
+ end
17
+ end
18
+
19
+ def insert!
20
+ raise NotImplementedError.new
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,64 @@
1
+ module Timber
2
+ module Probes
3
+ class ActionControllerLogSubscriber < Probe
4
+ # The log subscriber that replaces the default `ActionController::LogSubscriber`.
5
+ # The intent of this subscriber is to, as transparently as possible, properly
6
+ # track events that are being logged here. This LogSubscriber will never change
7
+ # default behavior / log messages.
8
+ class LogSubscriber < ::ActionController::LogSubscriber
9
+ def start_processing(event)
10
+ info do
11
+ payload = event.payload
12
+ params = payload[:params].except(*INTERNAL_PARAMS)
13
+ format = extract_format(payload)
14
+ format = format.to_s.upcase if format.is_a?(Symbol)
15
+
16
+ Events::ControllerCall.new(
17
+ controller: payload[:controller],
18
+ action: payload[:action],
19
+ format: format,
20
+ params: params
21
+ )
22
+ end
23
+ end
24
+
25
+ def process_action(event)
26
+ info do
27
+ payload = event.payload
28
+ additions = ActionController::Base.log_process_action(payload)
29
+
30
+ status = payload[:status]
31
+ if status.nil? && payload[:exception].present?
32
+ exception_class_name = payload[:exception].first
33
+ status = extract_status(exception_class_name)
34
+ end
35
+
36
+ Events::HTTPResponse.new(
37
+ status: status,
38
+ time_ms: event.duration,
39
+ additions: additions
40
+ )
41
+ end
42
+ end
43
+
44
+ private
45
+ def extract_format(payload)
46
+ if payload.key?(:format)
47
+ payload[:format] # rails > 4.X
48
+ elsif payload.key?(:formats)
49
+ payload[:formats].first # rails 3.X
50
+ end
51
+ end
52
+
53
+ def extract_status(exception_class_name)
54
+ if defined?(ActionDispatch::ExceptionWrapper)
55
+ ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
56
+ else
57
+ # Rails 3.X
58
+ Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[exception_class_name]) rescue nil
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ module Timber
2
+ module Probes
3
+ # Responsible for automatically tracking controller call and http response events
4
+ # for applications that use `ActionController`.
5
+ class ActionControllerLogSubscriber < Probe
6
+ def initialize
7
+ require "action_controller/log_subscriber"
8
+ require "timber/probes/action_controller_log_subscriber/log_subscriber"
9
+ rescue LoadError => e
10
+ raise RequirementNotMetError.new(e.message)
11
+ end
12
+
13
+ def insert!
14
+ return true if Util::ActiveSupportLogSubscriber.subscribed?(:action_controller, LogSubscriber)
15
+ Util::ActiveSupportLogSubscriber.unsubscribe(:action_controller, ::ActionController::LogSubscriber)
16
+ LogSubscriber.attach_to(:action_controller)
17
+ end
18
+ end
19
+ end
20
+ end