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.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +34 -0
  3. data/.gitignore +14 -0
  4. data/Appraisals +37 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +38 -0
  7. data/README.md +22 -0
  8. data/Rakefile +4 -0
  9. data/TODO +4 -0
  10. data/benchmark/README.md +26 -0
  11. data/benchmark/rails_request.rb +68 -0
  12. data/benchmark/support/rails.rb +69 -0
  13. data/circle.yml +27 -0
  14. data/docs/installation/rails_on_heroku.md +31 -0
  15. data/docs/installation/rails_over_http.md +22 -0
  16. data/gemfiles/rails_3.0.X.gemfile +25 -0
  17. data/gemfiles/rails_3.1.X.gemfile +25 -0
  18. data/gemfiles/rails_3.2.X.gemfile +25 -0
  19. data/gemfiles/rails_4.0.X.gemfile +26 -0
  20. data/gemfiles/rails_4.1.X.gemfile +26 -0
  21. data/gemfiles/rails_4.2.X.gemfile +26 -0
  22. data/gemfiles/rails_5.0.X.gemfile +26 -0
  23. data/gemfiles/rails_edge.gemfile +27 -0
  24. data/lib/timber/api_settings.rb +17 -0
  25. data/lib/timber/bootstrap.rb +45 -0
  26. data/lib/timber/config.rb +25 -0
  27. data/lib/timber/context.rb +76 -0
  28. data/lib/timber/context_snapshot.rb +64 -0
  29. data/lib/timber/contexts/dynamic_values.rb +59 -0
  30. data/lib/timber/contexts/exception.rb +40 -0
  31. data/lib/timber/contexts/http_request.rb +22 -0
  32. data/lib/timber/contexts/http_requests/action_controller_specific.rb +48 -0
  33. data/lib/timber/contexts/http_requests/rack/params.rb +26 -0
  34. data/lib/timber/contexts/http_requests/rack.rb +105 -0
  35. data/lib/timber/contexts/http_response.rb +19 -0
  36. data/lib/timber/contexts/http_responses/action_controller.rb +76 -0
  37. data/lib/timber/contexts/logger.rb +33 -0
  38. data/lib/timber/contexts/organization.rb +33 -0
  39. data/lib/timber/contexts/organizations/action_controller.rb +34 -0
  40. data/lib/timber/contexts/server.rb +21 -0
  41. data/lib/timber/contexts/servers/heroku_specific.rb +48 -0
  42. data/lib/timber/contexts/sql_queries/active_record.rb +30 -0
  43. data/lib/timber/contexts/sql_queries/active_record_specific/binds.rb +37 -0
  44. data/lib/timber/contexts/sql_queries/active_record_specific.rb +59 -0
  45. data/lib/timber/contexts/sql_query.rb +18 -0
  46. data/lib/timber/contexts/template_render.rb +17 -0
  47. data/lib/timber/contexts/template_renders/action_view.rb +29 -0
  48. data/lib/timber/contexts/template_renders/action_view_specific.rb +51 -0
  49. data/lib/timber/contexts/user.rb +39 -0
  50. data/lib/timber/contexts/users/action_controller.rb +34 -0
  51. data/lib/timber/contexts.rb +23 -0
  52. data/lib/timber/current_context.rb +58 -0
  53. data/lib/timber/current_line_indexes.rb +35 -0
  54. data/lib/timber/frameworks/rails.rb +24 -0
  55. data/lib/timber/frameworks.rb +21 -0
  56. data/lib/timber/internal_logger.rb +35 -0
  57. data/lib/timber/log_device.rb +40 -0
  58. data/lib/timber/log_devices/heroku_logplex/hybrid_formatter.rb +14 -0
  59. data/lib/timber/log_devices/heroku_logplex.rb +14 -0
  60. data/lib/timber/log_devices/http/log_pile.rb +86 -0
  61. data/lib/timber/log_devices/http/log_truck/delivery.rb +116 -0
  62. data/lib/timber/log_devices/http/log_truck.rb +87 -0
  63. data/lib/timber/log_devices/http.rb +28 -0
  64. data/lib/timber/log_devices/io/formatter.rb +46 -0
  65. data/lib/timber/log_devices/io/hybrid_formatter.rb +41 -0
  66. data/lib/timber/log_devices/io/hybrid_hidden_formatter.rb +36 -0
  67. data/lib/timber/log_devices/io/json_formatter.rb +11 -0
  68. data/lib/timber/log_devices/io/logfmt_formatter.rb +11 -0
  69. data/lib/timber/log_devices/io.rb +41 -0
  70. data/lib/timber/log_devices.rb +4 -0
  71. data/lib/timber/log_line.rb +33 -0
  72. data/lib/timber/logger.rb +20 -0
  73. data/lib/timber/macros/compactor.rb +16 -0
  74. data/lib/timber/macros/date_formatter.rb +9 -0
  75. data/lib/timber/macros/deep_merger.rb +11 -0
  76. data/lib/timber/macros/logfmt_encoder.rb +77 -0
  77. data/lib/timber/macros.rb +4 -0
  78. data/lib/timber/patterns/delegated_singleton.rb +21 -0
  79. data/lib/timber/patterns/to_json.rb +22 -0
  80. data/lib/timber/patterns/to_logfmt.rb +9 -0
  81. data/lib/timber/patterns.rb +3 -0
  82. data/lib/timber/probe.rb +21 -0
  83. data/lib/timber/probes/action_controller_base.rb +31 -0
  84. data/lib/timber/probes/action_dispatch_debug_exceptions.rb +57 -0
  85. data/lib/timber/probes/active_support_log_subscriber/action_controller.rb +15 -0
  86. data/lib/timber/probes/active_support_log_subscriber/action_view.rb +26 -0
  87. data/lib/timber/probes/active_support_log_subscriber/active_record.rb +13 -0
  88. data/lib/timber/probes/active_support_log_subscriber.rb +62 -0
  89. data/lib/timber/probes/heroku.rb +30 -0
  90. data/lib/timber/probes/logger.rb +31 -0
  91. data/lib/timber/probes/rack.rb +36 -0
  92. data/lib/timber/probes/server.rb +18 -0
  93. data/lib/timber/probes.rb +24 -0
  94. data/lib/timber/version.rb +3 -0
  95. data/lib/timber.rb +27 -0
  96. data/spec/spec_helper.rb +27 -0
  97. data/spec/support/action_controller.rb +4 -0
  98. data/spec/support/action_view.rb +4 -0
  99. data/spec/support/active_record.rb +28 -0
  100. data/spec/support/coveralls.rb +2 -0
  101. data/spec/support/rails/templates/_partial.html +1 -0
  102. data/spec/support/rails/templates/template.html +1 -0
  103. data/spec/support/rails.rb +33 -0
  104. data/spec/support/simplecov.rb +9 -0
  105. data/spec/support/socket_hostname.rb +12 -0
  106. data/spec/support/timber.rb +23 -0
  107. data/spec/support/timecop.rb +3 -0
  108. data/spec/support/webmock.rb +2 -0
  109. data/spec/timber/bootstrap_spec.rb +31 -0
  110. data/spec/timber/context_snapshot_spec.rb +10 -0
  111. data/spec/timber/context_spec.rb +4 -0
  112. data/spec/timber/contexts/exception_spec.rb +34 -0
  113. data/spec/timber/contexts/organizations/action_controller_spec.rb +49 -0
  114. data/spec/timber/contexts/users/action_controller_spec.rb +65 -0
  115. data/spec/timber/current_line_indexes_spec.rb +40 -0
  116. data/spec/timber/frameworks/rails_spec.rb +9 -0
  117. data/spec/timber/log_devices/heroku_logplex_spec.rb +45 -0
  118. data/spec/timber/log_devices/http/log_truck/delivery_spec.rb +66 -0
  119. data/spec/timber/log_devices/http/log_truck_spec.rb +65 -0
  120. data/spec/timber/log_devices/io/hybrid_hidden_formatter_spec.rb +28 -0
  121. data/spec/timber/log_line_spec.rb +49 -0
  122. data/spec/timber/macros/compactor_spec.rb +19 -0
  123. data/spec/timber/macros/logfmt_encoder_spec.rb +89 -0
  124. data/spec/timber/patterns/to_json_spec.rb +40 -0
  125. data/spec/timber/probes/action_controller_base_spec.rb +43 -0
  126. data/spec/timber/probes/action_controller_log_subscriber/action_controller_spec.rb +35 -0
  127. data/spec/timber/probes/action_controller_log_subscriber/action_view_spec.rb +44 -0
  128. data/spec/timber/probes/action_controller_log_subscriber/active_record_spec.rb +26 -0
  129. data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +45 -0
  130. data/spec/timber/probes/logger_spec.rb +20 -0
  131. data/spec/timber/probes/rack_spec.rb +26 -0
  132. data/timberio.gemspec +20 -0
  133. 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,11 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class JSONFormatter < Formatter
5
+ def format(log_line)
6
+ CALLOUT + log_line.to_json
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class LogfmtFormatter < Formatter
5
+ def format(log_line)
6
+ @CALLOUT + log_line.to_logfmt
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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,4 @@
1
+ # Order is relevant
2
+ require File.join(File.dirname(__FILE__), "log_devices", "io")
3
+ require File.join(File.dirname(__FILE__), "log_devices", "heroku_logplex")
4
+ require File.join(File.dirname(__FILE__), "log_devices", "http")
@@ -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
@@ -0,0 +1,9 @@
1
+ module Timber
2
+ module Macros
3
+ module DateFormatter
4
+ def self.format(dt)
5
+ dt.send(APISettings::DATE_FORMAT, APISettings::DATE_FORMAT_PRECISION)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Timber
2
+ module Macros
3
+ # Deep merges hash keys
4
+ module DeepMerger
5
+ def self.merge(first, second)
6
+ merger = proc { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
7
+ first.merge(second, &merger)
8
+ end
9
+ end
10
+ end
11
+ end