timberio 1.0.0.beta1
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/.codeclimate.yml +34 -0
- data/.gitignore +14 -0
- data/Appraisals +37 -0
- data/Gemfile +22 -0
- data/LICENSE +38 -0
- data/README.md +22 -0
- data/Rakefile +4 -0
- data/TODO +4 -0
- data/benchmark/README.md +26 -0
- data/benchmark/rails_request.rb +68 -0
- data/benchmark/support/rails.rb +69 -0
- data/circle.yml +27 -0
- data/docs/installation/rails_on_heroku.md +31 -0
- data/docs/installation/rails_over_http.md +22 -0
- data/gemfiles/rails_3.0.X.gemfile +25 -0
- data/gemfiles/rails_3.1.X.gemfile +25 -0
- data/gemfiles/rails_3.2.X.gemfile +25 -0
- data/gemfiles/rails_4.0.X.gemfile +26 -0
- data/gemfiles/rails_4.1.X.gemfile +26 -0
- data/gemfiles/rails_4.2.X.gemfile +26 -0
- data/gemfiles/rails_5.0.X.gemfile +26 -0
- data/gemfiles/rails_edge.gemfile +27 -0
- data/lib/timber/api_settings.rb +17 -0
- data/lib/timber/bootstrap.rb +45 -0
- data/lib/timber/config.rb +25 -0
- data/lib/timber/context.rb +76 -0
- data/lib/timber/context_snapshot.rb +64 -0
- data/lib/timber/contexts/dynamic_values.rb +59 -0
- data/lib/timber/contexts/exception.rb +40 -0
- data/lib/timber/contexts/http_request.rb +22 -0
- data/lib/timber/contexts/http_requests/action_controller_specific.rb +48 -0
- data/lib/timber/contexts/http_requests/rack/params.rb +26 -0
- data/lib/timber/contexts/http_requests/rack.rb +105 -0
- data/lib/timber/contexts/http_response.rb +19 -0
- data/lib/timber/contexts/http_responses/action_controller.rb +76 -0
- data/lib/timber/contexts/logger.rb +33 -0
- data/lib/timber/contexts/organization.rb +33 -0
- data/lib/timber/contexts/organizations/action_controller.rb +34 -0
- data/lib/timber/contexts/server.rb +21 -0
- data/lib/timber/contexts/servers/heroku_specific.rb +48 -0
- data/lib/timber/contexts/sql_queries/active_record.rb +30 -0
- data/lib/timber/contexts/sql_queries/active_record_specific/binds.rb +37 -0
- data/lib/timber/contexts/sql_queries/active_record_specific.rb +59 -0
- data/lib/timber/contexts/sql_query.rb +18 -0
- data/lib/timber/contexts/template_render.rb +17 -0
- data/lib/timber/contexts/template_renders/action_view.rb +29 -0
- data/lib/timber/contexts/template_renders/action_view_specific.rb +51 -0
- data/lib/timber/contexts/user.rb +39 -0
- data/lib/timber/contexts/users/action_controller.rb +34 -0
- data/lib/timber/contexts.rb +23 -0
- data/lib/timber/current_context.rb +58 -0
- data/lib/timber/current_line_indexes.rb +35 -0
- data/lib/timber/frameworks/rails.rb +24 -0
- data/lib/timber/frameworks.rb +21 -0
- data/lib/timber/internal_logger.rb +35 -0
- data/lib/timber/log_device.rb +40 -0
- data/lib/timber/log_devices/heroku_logplex/hybrid_formatter.rb +14 -0
- data/lib/timber/log_devices/heroku_logplex.rb +14 -0
- data/lib/timber/log_devices/http/log_pile.rb +86 -0
- data/lib/timber/log_devices/http/log_truck/delivery.rb +116 -0
- data/lib/timber/log_devices/http/log_truck.rb +87 -0
- data/lib/timber/log_devices/http.rb +28 -0
- data/lib/timber/log_devices/io/formatter.rb +46 -0
- data/lib/timber/log_devices/io/hybrid_formatter.rb +41 -0
- data/lib/timber/log_devices/io/hybrid_hidden_formatter.rb +36 -0
- data/lib/timber/log_devices/io/json_formatter.rb +11 -0
- data/lib/timber/log_devices/io/logfmt_formatter.rb +11 -0
- data/lib/timber/log_devices/io.rb +41 -0
- data/lib/timber/log_devices.rb +4 -0
- data/lib/timber/log_line.rb +33 -0
- data/lib/timber/logger.rb +20 -0
- data/lib/timber/macros/compactor.rb +16 -0
- data/lib/timber/macros/date_formatter.rb +9 -0
- data/lib/timber/macros/deep_merger.rb +11 -0
- data/lib/timber/macros/logfmt_encoder.rb +77 -0
- data/lib/timber/macros.rb +4 -0
- data/lib/timber/patterns/delegated_singleton.rb +21 -0
- data/lib/timber/patterns/to_json.rb +22 -0
- data/lib/timber/patterns/to_logfmt.rb +9 -0
- data/lib/timber/patterns.rb +3 -0
- data/lib/timber/probe.rb +21 -0
- data/lib/timber/probes/action_controller_base.rb +31 -0
- data/lib/timber/probes/action_dispatch_debug_exceptions.rb +57 -0
- data/lib/timber/probes/active_support_log_subscriber/action_controller.rb +15 -0
- data/lib/timber/probes/active_support_log_subscriber/action_view.rb +26 -0
- data/lib/timber/probes/active_support_log_subscriber/active_record.rb +13 -0
- data/lib/timber/probes/active_support_log_subscriber.rb +62 -0
- data/lib/timber/probes/heroku.rb +30 -0
- data/lib/timber/probes/logger.rb +31 -0
- data/lib/timber/probes/rack.rb +36 -0
- data/lib/timber/probes/server.rb +18 -0
- data/lib/timber/probes.rb +24 -0
- data/lib/timber/version.rb +3 -0
- data/lib/timber.rb +27 -0
- data/spec/spec_helper.rb +27 -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 +33 -0
- data/spec/support/simplecov.rb +9 -0
- data/spec/support/socket_hostname.rb +12 -0
- data/spec/support/timber.rb +23 -0
- data/spec/support/timecop.rb +3 -0
- data/spec/support/webmock.rb +2 -0
- data/spec/timber/bootstrap_spec.rb +31 -0
- data/spec/timber/context_snapshot_spec.rb +10 -0
- data/spec/timber/context_spec.rb +4 -0
- data/spec/timber/contexts/exception_spec.rb +34 -0
- data/spec/timber/contexts/organizations/action_controller_spec.rb +49 -0
- data/spec/timber/contexts/users/action_controller_spec.rb +65 -0
- data/spec/timber/current_line_indexes_spec.rb +40 -0
- data/spec/timber/frameworks/rails_spec.rb +9 -0
- data/spec/timber/log_devices/heroku_logplex_spec.rb +45 -0
- data/spec/timber/log_devices/http/log_truck/delivery_spec.rb +66 -0
- data/spec/timber/log_devices/http/log_truck_spec.rb +65 -0
- data/spec/timber/log_devices/io/hybrid_hidden_formatter_spec.rb +28 -0
- data/spec/timber/log_line_spec.rb +49 -0
- data/spec/timber/macros/compactor_spec.rb +19 -0
- data/spec/timber/macros/logfmt_encoder_spec.rb +89 -0
- data/spec/timber/patterns/to_json_spec.rb +40 -0
- data/spec/timber/probes/action_controller_base_spec.rb +43 -0
- data/spec/timber/probes/action_controller_log_subscriber/action_controller_spec.rb +35 -0
- data/spec/timber/probes/action_controller_log_subscriber/action_view_spec.rb +44 -0
- data/spec/timber/probes/action_controller_log_subscriber/active_record_spec.rb +26 -0
- data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +45 -0
- data/spec/timber/probes/logger_spec.rb +20 -0
- data/spec/timber/probes/rack_spec.rb +26 -0
- data/timberio.gemspec +20 -0
- metadata +210 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), "heroku_logplex", "hybrid_formatter")
|
|
2
|
+
|
|
3
|
+
module Timber
|
|
4
|
+
module LogDevices
|
|
5
|
+
class HerokuLogplex < IO
|
|
6
|
+
def initialize(_options = {})
|
|
7
|
+
super(STDOUT)
|
|
8
|
+
if formatter.is_a?(IO::HybridFormatter)
|
|
9
|
+
formatter.extend HybridFormatter
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module Timber
|
|
4
|
+
module LogDevices
|
|
5
|
+
class HTTP < LogDevice
|
|
6
|
+
# This is a thread safe queue for transporting logs to the Timber API.
|
|
7
|
+
# TODO: Have these log lines persist to a file where
|
|
8
|
+
# a daemon can pick them up.
|
|
9
|
+
class LogPile
|
|
10
|
+
class << self
|
|
11
|
+
def each(&block)
|
|
12
|
+
instances.values.each(&block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(application_key)
|
|
16
|
+
instances[application_key] ||= new(application_key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
def instances
|
|
21
|
+
@instances ||= {}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :application_key
|
|
26
|
+
|
|
27
|
+
def initialize(application_key)
|
|
28
|
+
@application_key = application_key
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def drop(log_line)
|
|
33
|
+
mutex.synchronize do
|
|
34
|
+
log_lines << log_line
|
|
35
|
+
end
|
|
36
|
+
rescue LogLine::InvalidMessageError => e
|
|
37
|
+
# Ignore the error and log it.
|
|
38
|
+
Config.logger.error(e)
|
|
39
|
+
rescue Exception => e
|
|
40
|
+
# Fail safe to ensure the Timber gem never fails the app.
|
|
41
|
+
Config.logger.exception(e)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def empty(&_block)
|
|
45
|
+
if log_lines.any?
|
|
46
|
+
copy = log_lines_copy
|
|
47
|
+
yield(copy) if block_given?
|
|
48
|
+
remove(copy)
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size
|
|
54
|
+
log_lines.size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
def mutex
|
|
59
|
+
@mutex
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def remove(log_lines_copy)
|
|
63
|
+
mutex.synchronize do
|
|
64
|
+
# Delete items by object_id since we are working
|
|
65
|
+
# with the same object. Do not use equality here.
|
|
66
|
+
log_lines_copy.each do |l1|
|
|
67
|
+
log_lines.delete_if { |l2| l2.object_id == l1.object_id }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def log_lines_copy
|
|
73
|
+
mutex.synchronize do
|
|
74
|
+
# Copy the array structure so we aren't dealing with
|
|
75
|
+
# a changing array, but do not copy the items.
|
|
76
|
+
log_lines.clone
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def log_lines
|
|
81
|
+
@log_lines ||= []
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "net/https"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Timber
|
|
7
|
+
module LogDevices
|
|
8
|
+
class HTTP < LogDevice
|
|
9
|
+
class LogTruck
|
|
10
|
+
class Delivery
|
|
11
|
+
class DeliveryError < StandardError; end
|
|
12
|
+
|
|
13
|
+
API_URI = URI.parse("https://timber-odin.herokuapp.com/agent_log_frames")
|
|
14
|
+
CONTENT_TYPE = 'application/json'.freeze
|
|
15
|
+
READ_TIMEOUT_SECONDS = 35.freeze
|
|
16
|
+
RETRY_BACKOFF_SECONDS = 1.freeze
|
|
17
|
+
RETRY_COUNT = 4.freeze
|
|
18
|
+
USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
|
|
19
|
+
|
|
20
|
+
HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
|
|
21
|
+
https.use_ssl = true
|
|
22
|
+
https.read_timeout = READ_TIMEOUT_SECONDS
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :application_key, :log_lines
|
|
26
|
+
|
|
27
|
+
def initialize(application_key, log_lines)
|
|
28
|
+
@application_key = application_key
|
|
29
|
+
@log_lines = log_lines
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def deliver!(retry_count = 0)
|
|
33
|
+
Config.logger.debug("Attempting delivery of: #{body_json}")
|
|
34
|
+
request!
|
|
35
|
+
# Catch them all because of all the unknown exceptions that can happen during
|
|
36
|
+
# a http request.
|
|
37
|
+
rescue Exception => e
|
|
38
|
+
# Ensure that we are always returning a consistent error.
|
|
39
|
+
# This ensures we handle it appropriately and don't kill the
|
|
40
|
+
# thread above.
|
|
41
|
+
Config.logger.warn("Failed delivery: #{e.message}")
|
|
42
|
+
|
|
43
|
+
retry_count += 1
|
|
44
|
+
if retry_count <= RETRY_COUNT
|
|
45
|
+
backoff_seconds = RETRY_BACKOFF_SECONDS ** retry_count
|
|
46
|
+
Config.logger.warn("Backing off #{backoff_seconds} seconds")
|
|
47
|
+
sleep backoff_seconds
|
|
48
|
+
Config.logger.warn("Retrying, attempt #{retry_count}")
|
|
49
|
+
deliver!(retry_count)
|
|
50
|
+
else
|
|
51
|
+
Config.logger.warn("Retry attempts exceeded, dropping logs")
|
|
52
|
+
raise DeliveryError.new(e.message)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
def https
|
|
58
|
+
@https ||= HTTPS
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request!
|
|
62
|
+
https.request(new_request).tap do |res|
|
|
63
|
+
code = res.code.to_i
|
|
64
|
+
if code < 200 || code >= 300
|
|
65
|
+
raise DeliveryError.new("Bad response from Timber API - #{res.code}: #{res.body}")
|
|
66
|
+
end
|
|
67
|
+
Config.logger.debug("Success! #{code}: #{res.body}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def new_request
|
|
72
|
+
Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
|
|
73
|
+
req['Authorization'] = authorization_payload
|
|
74
|
+
req['Body-Checksum'] = body_checksum # the API checks for duplicate requests
|
|
75
|
+
req['Content-Type'] = CONTENT_TYPE
|
|
76
|
+
req['Log-Line-Count'] = log_lines.size # additional check to ensure the correct # of log lines were sent
|
|
77
|
+
req['User-Agent'] = USER_AGENT
|
|
78
|
+
req.body = body_json
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Used by the API to check for duplicate requests.
|
|
83
|
+
def body_checksum
|
|
84
|
+
@body_checksum ||= Digest::MD5.hexdigest(body_json)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def body_json
|
|
88
|
+
return @body_json if defined?(@body_json)
|
|
89
|
+
# Build the json as a string since it is more efficient.
|
|
90
|
+
# We are also working with string upstream for the same reason.
|
|
91
|
+
@body_json ||= <<-JSON
|
|
92
|
+
{"agent_log_frame": {"log_lines": #{log_lines_json}}}
|
|
93
|
+
JSON
|
|
94
|
+
@body_json.strip!
|
|
95
|
+
@body_json
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_lines_json
|
|
99
|
+
return @log_lines_json if defined?(@log_lines_json)
|
|
100
|
+
@log_lines_json = "["
|
|
101
|
+
last_index = log_lines.size - 1
|
|
102
|
+
log_lines.each_with_index do |log_line, index|
|
|
103
|
+
@log_lines_json += log_line.to_json
|
|
104
|
+
@log_lines_json += ", " if index != last_index
|
|
105
|
+
end
|
|
106
|
+
@log_lines_json += "]"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def authorization_payload
|
|
110
|
+
@authorization_payload ||= "Basic #{Base64.strict_encode64(application_key).chomp}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "net/https"
|
|
4
|
+
require File.join(File.dirname(__FILE__), "log_truck", "delivery")
|
|
5
|
+
|
|
6
|
+
module Timber
|
|
7
|
+
module LogDevices
|
|
8
|
+
class HTTP < LogDevice
|
|
9
|
+
# Temporary class for alpha / beta purposes.
|
|
10
|
+
# Log lines will be written to a file where a daemon
|
|
11
|
+
# will pick them up. Most of this code will be moved
|
|
12
|
+
# to that daemon.
|
|
13
|
+
class LogTruck
|
|
14
|
+
THROTTLE_SECONDS = 3.freeze
|
|
15
|
+
|
|
16
|
+
class NoPayloadError < ArgumentError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def start!(options = {}, &_block)
|
|
20
|
+
return if @thread && @thread.alive?
|
|
21
|
+
|
|
22
|
+
# Old school options to support ruby 1.9 :(
|
|
23
|
+
options[:throttle_seconds] = THROTTLE_SECONDS if !options.key?(:throttle_seconds)
|
|
24
|
+
Config.logger.debug("Starting log truck with a #{options[:throttle_seconds]} second throttle")
|
|
25
|
+
|
|
26
|
+
# A new thread for looping and monitoring. We need to
|
|
27
|
+
# use a thread so that we can share memory.
|
|
28
|
+
@thread = Thread.new do
|
|
29
|
+
# ensure we always deliver upon exiting
|
|
30
|
+
at_exit { deliver }
|
|
31
|
+
|
|
32
|
+
# Keep looking for logs
|
|
33
|
+
loop do
|
|
34
|
+
deliver
|
|
35
|
+
|
|
36
|
+
# Yield a block, primarily for testing purposes
|
|
37
|
+
yield(Thread.current) if block_given?
|
|
38
|
+
|
|
39
|
+
# Throttle to reduce checking the pile
|
|
40
|
+
sleep options[:throttle_seconds]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
rescue Exception => e
|
|
45
|
+
# failsafe to ensure we don't kill the app
|
|
46
|
+
Config.logger.exception(e)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Deliver, return LogTruck object, otherwise
|
|
50
|
+
# raise an error.
|
|
51
|
+
def deliver
|
|
52
|
+
log_truck = nil
|
|
53
|
+
LogPile.each do |log_pile|
|
|
54
|
+
log_pile.empty do |log_lines|
|
|
55
|
+
# LogPile only empties if no exception is raised
|
|
56
|
+
begin
|
|
57
|
+
# This will retry a number of times. If we can't get it during the retries
|
|
58
|
+
# we drop the logs. Note, this strategy will improve when we write to a file
|
|
59
|
+
# and use an actual agent.
|
|
60
|
+
log_truck = new(log_pile.application_key, log_lines).tap(&:deliver!)
|
|
61
|
+
rescue Delivery::DeliveryError => e
|
|
62
|
+
Config.logger.exception(e)
|
|
63
|
+
# TODO: How do we handle server timeouts? The request could have still been processed.
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
log_truck
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_reader :application_key, :log_lines
|
|
72
|
+
|
|
73
|
+
def initialize(application_key, log_lines)
|
|
74
|
+
if log_lines.empty?
|
|
75
|
+
raise NoPayloadError.new("a truck must contain a payload (at least one log line)")
|
|
76
|
+
end
|
|
77
|
+
@application_key = application_key
|
|
78
|
+
@log_lines = log_lines
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deliver!
|
|
82
|
+
Delivery.new(application_key, log_lines).deliver!
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), "http", "log_pile")
|
|
2
|
+
require File.join(File.dirname(__FILE__), "http", "log_truck")
|
|
3
|
+
|
|
4
|
+
module Timber
|
|
5
|
+
module LogDevices
|
|
6
|
+
class HTTP < LogDevice
|
|
7
|
+
SPLIT_LINES = false
|
|
8
|
+
|
|
9
|
+
attr_reader :application_key
|
|
10
|
+
|
|
11
|
+
def initialize(application_key = nil)
|
|
12
|
+
@application_key = application_key || Config.application_key
|
|
13
|
+
if @application_key.nil?
|
|
14
|
+
raise ArgumentError.new("A Timber application_key is required")
|
|
15
|
+
end
|
|
16
|
+
LogTruck.start!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def close(*args)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
def write_log_line(log_line)
|
|
24
|
+
LogPile.get(application_key).drop(log_line)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Timber
|
|
2
|
+
module LogDevices
|
|
3
|
+
class IO < LogDevice
|
|
4
|
+
class Formatter
|
|
5
|
+
# Do not change this, the API matches on it. Otherwise nothing
|
|
6
|
+
# get parsed.
|
|
7
|
+
CALLOUT = "@timber.io "
|
|
8
|
+
CALLOUT_END = "@original "
|
|
9
|
+
|
|
10
|
+
# Embed in a String to clear all previous ANSI sequences.
|
|
11
|
+
CLEAR = "\e[0m"
|
|
12
|
+
BOLD = "\e[1m"
|
|
13
|
+
|
|
14
|
+
# Colors
|
|
15
|
+
BLACK = "\e[30m"
|
|
16
|
+
DARK_GRAY = "\e[1;30m"
|
|
17
|
+
RED = "\e[31m"
|
|
18
|
+
GREEN = "\e[32m"
|
|
19
|
+
YELLOW = "\e[33m"
|
|
20
|
+
BLUE = "\e[34m"
|
|
21
|
+
MAGENTA = "\e[35m"
|
|
22
|
+
CYAN = "\e[36m"
|
|
23
|
+
WHITE = "\e[37m"
|
|
24
|
+
|
|
25
|
+
def initialize(options = {})
|
|
26
|
+
@ansi_format = options.key?(:ansi_format) ? options[:ansi_format] == true : true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ansi_format?
|
|
30
|
+
@ansi_format == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def format(_log_line)
|
|
34
|
+
raise NotImplementedError.new("#format is not implemented")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
def ansi_format(*args)
|
|
39
|
+
text = args.pop
|
|
40
|
+
return text unless ansi_format?
|
|
41
|
+
"#{args.join}#{text}#{CLEAR}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Timber
|
|
2
|
+
module LogDevices
|
|
3
|
+
class IO < LogDevice
|
|
4
|
+
class HybridFormatter < Formatter
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
super
|
|
7
|
+
@date_prefix = options.key?(:date_prefix) ? options[:date_prefix] : false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def date_prefix?
|
|
11
|
+
@date_prefix == true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def format(log_line)
|
|
15
|
+
"#{log_line.message}#{context_message(log_line)}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
def base_message(log_line)
|
|
20
|
+
text = ""
|
|
21
|
+
if date_prefix?
|
|
22
|
+
text << "#{log_line.formatted_dt} "
|
|
23
|
+
end
|
|
24
|
+
text << log_line.message
|
|
25
|
+
text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def context_message(log_line)
|
|
29
|
+
# The callout must be before the formatting, otherwise we leave
|
|
30
|
+
# the message ending with a color formatting and not a reset.
|
|
31
|
+
# Anything before the callout modifies the original message.
|
|
32
|
+
CALLOUT + ansi_format(DARK_GRAY, encoded_context(log_line))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def encoded_context(log_line)
|
|
36
|
+
log_line.context_snapshot.to_logfmt
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Timber
|
|
2
|
+
module LogDevices
|
|
3
|
+
class IO < LogDevice
|
|
4
|
+
class HybridHiddenFormatter < HybridFormatter
|
|
5
|
+
CLEAR_SEQUENCE = "\e8\e[K".freeze
|
|
6
|
+
CLEAR_STEP_SIZE = 20.freeze
|
|
7
|
+
SAVE_CURSOR_POSITION = "\e7".freeze
|
|
8
|
+
|
|
9
|
+
def format(log_line)
|
|
10
|
+
"#{SAVE_CURSOR_POSITION}#{context_message(log_line)}#{base_message(log_line)}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
def context_message(log_line)
|
|
15
|
+
text = encoded_context(log_line)
|
|
16
|
+
position = CLEAR_STEP_SIZE
|
|
17
|
+
sequence_size = CLEAR_SEQUENCE.size
|
|
18
|
+
step_size = sequence_size + CLEAR_STEP_SIZE
|
|
19
|
+
while position < text.length
|
|
20
|
+
# ensure we don't insert before a \
|
|
21
|
+
while text[position - 1] == "\\"
|
|
22
|
+
position += 1
|
|
23
|
+
end
|
|
24
|
+
text.insert(position, CLEAR_SEQUENCE)
|
|
25
|
+
position += step_size
|
|
26
|
+
end
|
|
27
|
+
ansi_format(DARK_GRAY, "#{CALLOUT}#{CLEAR_SEQUENCE}#{text} #{CALLOUT_END}#{CLEAR_SEQUENCE}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def encoded_context(log_line)
|
|
31
|
+
log_line.context_snapshot.to_logfmt
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), "io", "formatter")
|
|
2
|
+
require File.join(File.dirname(__FILE__), "io", "hybrid_formatter")
|
|
3
|
+
require File.join(File.dirname(__FILE__), "io", "hybrid_hidden_formatter")
|
|
4
|
+
require File.join(File.dirname(__FILE__), "io", "json_formatter")
|
|
5
|
+
require File.join(File.dirname(__FILE__), "io", "logfmt_formatter")
|
|
6
|
+
|
|
7
|
+
module Timber
|
|
8
|
+
module LogDevices
|
|
9
|
+
# The purpose of a Timber log device is to take the raw log message and enrich it
|
|
10
|
+
# with the current context.
|
|
11
|
+
#
|
|
12
|
+
# The IO log device works with any IO object. That is, any object that
|
|
13
|
+
# response to #write(message).
|
|
14
|
+
class IO < LogDevice
|
|
15
|
+
attr_reader :formatter
|
|
16
|
+
|
|
17
|
+
# Instantiates a new Timber IO log device.
|
|
18
|
+
#
|
|
19
|
+
# @param io [IO] any object the responds to #write(message)
|
|
20
|
+
def initialize(io = STDOUT, options = {})
|
|
21
|
+
io.sync = true if io.respond_to?(:sync=) # ensures logs are written immediately instead of being buffered by ruby
|
|
22
|
+
@formatter = options[:formatter] || HybridHiddenFormatter.new
|
|
23
|
+
@io = io
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close(*_args)
|
|
27
|
+
io.close
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
def write_log_line(log_line)
|
|
32
|
+
formatted_message = formatter.format(log_line)
|
|
33
|
+
io.write(formatted_message + "\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def io
|
|
37
|
+
@io
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Timber
|
|
2
|
+
class LogLine
|
|
3
|
+
include Patterns::ToJSON
|
|
4
|
+
include Patterns::ToLogfmt
|
|
5
|
+
|
|
6
|
+
# Raised when there is an issue with the message being passed.
|
|
7
|
+
# Note: this is handled in Logger
|
|
8
|
+
class InvalidMessageError < ArgumentError; end
|
|
9
|
+
|
|
10
|
+
attr_reader :context_snapshot, :dt, :line_indexes, :message
|
|
11
|
+
|
|
12
|
+
def initialize(message)
|
|
13
|
+
@dt = Time.now.utc # Capture the time as soon as possible
|
|
14
|
+
message = message.to_s
|
|
15
|
+
if message.bytesize > APISettings::MESSAGE_BYTE_SIZE_MAX
|
|
16
|
+
Config.logger.warn("Log line message is too long, truncating")
|
|
17
|
+
message = message.byteslice(0, APISettings::MESSAGE_BYTE_SIZE_MAX)
|
|
18
|
+
end
|
|
19
|
+
@message = message
|
|
20
|
+
CurrentLineIndexes.log_line_added(self) # Bump the indexes
|
|
21
|
+
@context_snapshot = CurrentContext.snapshot
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def formatted_dt
|
|
25
|
+
@formatted_dt ||= Macros::DateFormatter.format(dt)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def json_payload
|
|
30
|
+
@json_payload ||= {:dt => formatted_dt, :message => message}.merge(context_snapshot.as_json)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module Timber
|
|
4
|
+
# A simple interface to instantiate a logger. It does a couple of things:
|
|
5
|
+
# 1. Simplifies Rails logger instantiation across Rails versions. This
|
|
6
|
+
# helps with simplifying the Readme / install instructions.
|
|
7
|
+
# 2. Serves as a placeholder should we want to extend the logger and add
|
|
8
|
+
# Timber specific functionality.
|
|
9
|
+
module Logger
|
|
10
|
+
def self.new(logger_or_logdev = nil)
|
|
11
|
+
logger = if logger_or_logdev.is_a?(::Logger)
|
|
12
|
+
logger_or_logdev
|
|
13
|
+
else
|
|
14
|
+
Frameworks.logger(logger_or_logdev)
|
|
15
|
+
end
|
|
16
|
+
logger.extend(self)
|
|
17
|
+
logger
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Timber
|
|
2
|
+
module Macros
|
|
3
|
+
module Compactor
|
|
4
|
+
def self.compact(hash)
|
|
5
|
+
new_hash = {}
|
|
6
|
+
hash.each do |k, v|
|
|
7
|
+
deep_v = v.is_a?(Hash) ? compact(v) : v
|
|
8
|
+
if !deep_v.nil? && deep_v != [] && deep_v != {}
|
|
9
|
+
new_hash[k] = deep_v
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
new_hash
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|