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