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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.yardopts +6 -0
- data/Appraisals +41 -0
- data/Gemfile +30 -0
- data/LICENSE.md +15 -0
- data/README.md +194 -0
- data/circle.yml +33 -0
- data/lib/timber/config.rb +18 -0
- data/lib/timber/context.rb +17 -0
- data/lib/timber/contexts/custom.rb +27 -0
- data/lib/timber/contexts/http.rb +28 -0
- data/lib/timber/contexts/organization.rb +35 -0
- data/lib/timber/contexts/user.rb +36 -0
- data/lib/timber/contexts.rb +10 -0
- data/lib/timber/current_context.rb +43 -0
- data/lib/timber/event.rb +17 -0
- data/lib/timber/events/controller_call.rb +40 -0
- data/lib/timber/events/custom.rb +42 -0
- data/lib/timber/events/exception.rb +35 -0
- data/lib/timber/events/http_request.rb +50 -0
- data/lib/timber/events/http_response.rb +36 -0
- data/lib/timber/events/sql_query.rb +26 -0
- data/lib/timber/events/template_render.rb +26 -0
- data/lib/timber/events.rb +37 -0
- data/lib/timber/frameworks/rails.rb +13 -0
- data/lib/timber/frameworks.rb +19 -0
- data/lib/timber/log_devices/http.rb +87 -0
- data/lib/timber/log_devices.rb +8 -0
- data/lib/timber/log_entry.rb +59 -0
- data/lib/timber/logger.rb +142 -0
- data/lib/timber/probe.rb +23 -0
- data/lib/timber/probes/action_controller_log_subscriber/log_subscriber.rb +64 -0
- data/lib/timber/probes/action_controller_log_subscriber.rb +20 -0
- data/lib/timber/probes/action_dispatch_debug_exceptions.rb +80 -0
- data/lib/timber/probes/action_view_log_subscriber/log_subscriber.rb +62 -0
- data/lib/timber/probes/action_view_log_subscriber.rb +20 -0
- data/lib/timber/probes/active_record_log_subscriber/log_subscriber.rb +29 -0
- data/lib/timber/probes/active_record_log_subscriber.rb +20 -0
- data/lib/timber/probes/rack_http_context.rb +51 -0
- data/lib/timber/probes/rails_rack_logger.rb +76 -0
- data/lib/timber/probes.rb +21 -0
- data/lib/timber/util/active_support_log_subscriber.rb +33 -0
- data/lib/timber/util/hash.rb +14 -0
- data/lib/timber/util.rb +8 -0
- data/lib/timber/version.rb +3 -0
- data/lib/timber.rb +22 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/action_controller.rb +4 -0
- data/spec/support/action_view.rb +4 -0
- data/spec/support/active_record.rb +28 -0
- data/spec/support/coveralls.rb +2 -0
- data/spec/support/rails/templates/_partial.html +1 -0
- data/spec/support/rails/templates/template.html +1 -0
- data/spec/support/rails.rb +37 -0
- data/spec/support/simplecov.rb +9 -0
- data/spec/support/socket_hostname.rb +12 -0
- data/spec/support/timber.rb +4 -0
- data/spec/support/timecop.rb +3 -0
- data/spec/support/webmock.rb +2 -0
- data/spec/timber/events_spec.rb +55 -0
- data/spec/timber/log_devices/http_spec.rb +62 -0
- data/spec/timber/logger_spec.rb +89 -0
- data/spec/timber/probes/action_controller_log_subscriber_spec.rb +70 -0
- data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +51 -0
- data/spec/timber/probes/action_view_log_subscriber_spec.rb +61 -0
- data/spec/timber/probes/active_record_log_subscriber_spec.rb +52 -0
- data/spec/timber/probes/rack_http_context_spec.rb +54 -0
- data/spec/timber/probes/rails_rack_logger_spec.rb +46 -0
- data/timber.gemspec +22 -0
- 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,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
|
data/lib/timber/probe.rb
ADDED
@@ -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
|