timber 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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