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